Initial commit: personal task planner with categories, projects, and tiered assignment

Personal-first lists/tasks/reminders/recurrence, plus an admin-editable status
pipeline, shared categories, project containers, and a three-tier permission
model for cross-user work: assign (hand off your own tasks), reallocate
(dispatcher — move anyone's tasks), admin (reserved, full manage-all).
This commit is contained in:
jtricerolph 2026-09-17 08:07:00 +00:00
commit d8dc4e2b81
54 changed files with 10908 additions and 0 deletions

82
frontend/src/api.ts Normal file
View file

@ -0,0 +1,82 @@
import type { List, Task, TaskDetail, TaskItem, TaskComment, AppConfig } from './types'
const BASE = '/tasks/api'
async function request<T>(path: string, opts: RequestInit = {}): Promise<T> {
const res = await fetch(`${BASE}${path}`, {
credentials: 'include',
headers: { 'Content-Type': 'application/json', ...opts.headers },
...opts,
})
if (res.status === 401) {
;(window.top ?? window).location.href = '/login'
throw new Error('Unauthenticated')
}
if (!res.ok) {
const err = await res.json().catch(() => ({ error: res.statusText }))
throw new Error(err.error || `Request failed: ${res.status}`)
}
if (res.status === 204) return undefined as T
return res.json()
}
// ── Lists ────────────────────────────────────────────────────
export function fetchLists(includeArchived = false): Promise<List[]> {
return request(`/lists${includeArchived ? '?archived=1' : ''}`)
}
export function createList(body: { name: string; colour?: string; icon?: string }): Promise<List> {
return request('/lists', { method: 'POST', body: JSON.stringify(body) })
}
export function updateList(id: number, body: Partial<List>): Promise<List> {
return request(`/lists/${id}`, { method: 'PATCH', body: JSON.stringify(body) })
}
export function deleteList(id: number): Promise<{ ok: boolean }> {
return request(`/lists/${id}`, { method: 'DELETE' })
}
// ── Tasks ────────────────────────────────────────────────────
export function fetchTasks(params: { list_id?: number; view?: 'today'; status?: string } = {}): Promise<Task[]> {
const qs = new URLSearchParams()
for (const [k, v] of Object.entries(params)) if (v !== undefined) qs.set(k, String(v))
const s = qs.toString()
return request(`/tasks${s ? `?${s}` : ''}`)
}
export function fetchTask(id: number): Promise<TaskDetail> {
return request(`/tasks/${id}`)
}
export function createTask(body: Record<string, unknown>): Promise<Task> {
return request('/tasks', { method: 'POST', body: JSON.stringify(body) })
}
export function updateTask(id: number, body: Record<string, unknown>): Promise<Task> {
return request(`/tasks/${id}`, { method: 'PATCH', body: JSON.stringify(body) })
}
export function deleteTask(id: number): Promise<{ ok: boolean }> {
return request(`/tasks/${id}`, { method: 'DELETE' })
}
export function reorderTasks(ids: number[]): Promise<{ ok: boolean }> {
return request('/tasks/reorder', { method: 'POST', body: JSON.stringify({ ids }) })
}
// ── Checklist items ──────────────────────────────────────────
export function addItem(taskId: number, label: string): Promise<TaskItem> {
return request(`/tasks/${taskId}/items`, { method: 'POST', body: JSON.stringify({ label }) })
}
export function updateItem(itemId: number, body: Partial<TaskItem>): Promise<TaskItem> {
return request(`/tasks/items/${itemId}`, { method: 'PATCH', body: JSON.stringify(body) })
}
export function deleteItem(itemId: number): Promise<{ ok: boolean }> {
return request(`/tasks/items/${itemId}`, { method: 'DELETE' })
}
// ── Comments ─────────────────────────────────────────────────
export function addComment(taskId: number, body: string): Promise<TaskComment> {
return request(`/tasks/${taskId}/comments`, { method: 'POST', body: JSON.stringify({ body }) })
}
// ── Config ───────────────────────────────────────────────────
export function fetchConfig(): Promise<AppConfig> {
return request('/config')
}
export function updateConfig(key: string, value: unknown): Promise<{ ok: boolean }> {
return request(`/config/${key}`, { method: 'PUT', body: JSON.stringify({ value }) })
}