tasks/frontend/src/api.ts
jtricerolph 017f6dfdc1 Frontend: dynamic status pipeline, categories/projects, tag+assign+reallocate UI
- Board/List/Today views now render the admin-editable status pipeline
  instead of a hardcoded todo/doing/done enum (StatusesContext)
- Category filter chip-bar on Board and List views (ported the idea from
  reviewing the FD's own build, simplified to match our flat single-category
  model rather than his tree)
- New Projects list + detail views, project create/edit modal
- New Dispatch view for the 'reallocate' cap -- flat cross-user task list
  with an inline move action (deliberately not routed through the shared
  task drawer, since GET /api/tasks/:id only grants visibility to
  owner/assignee/tagged users, not dispatchers)
- Task drawer: dynamic status pills, category/project selects, tag
  picker (share/watch, no cap needed), assign field (owner + assign cap),
  reallocate field (reallocate cap) -- all fields disabled for non-owners
  since the backend's single PATCH endpoint is owner-gated
- Settings page: status pipeline + category management (settings cap)
- Fixed missing input[type="datetime-local"]/[type="time"] styling -- the
  Due/Remind fields were rendering with unstyled browser defaults
2026-09-17 08:52:08 +00:00

145 lines
7.1 KiB
TypeScript

import type { List, Task, TaskDetail, TaskItem, TaskComment, AppConfig, AppStatus, Category, Project, ProjectDetail } 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 interface TaskFilters {
list_id?: number
view?: 'today' | 'assigned' | 'shared' | 'all'
status?: string
category_id?: number
project_id?: number
}
export function fetchTasks(params: TaskFilters = {}): 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 }) })
}
export function reallocateTask(id: number, to: string): Promise<Task> {
return request(`/tasks/${id}/reallocate`, { method: 'POST', body: JSON.stringify({ to }) })
}
// ── Tags (share/watch) ───────────────────────────────────────
export function tagTask(id: number, email: string): Promise<{ ok: boolean }> {
return request(`/tasks/${id}/tags`, { method: 'POST', body: JSON.stringify({ email }) })
}
export function untagTask(id: number, email: string): Promise<{ ok: boolean }> {
return request(`/tasks/${id}/tags/${encodeURIComponent(email)}`, { method: 'DELETE' })
}
// ── Statuses ─────────────────────────────────────────────────
export function fetchStatuses(): Promise<AppStatus[]> {
return request('/statuses')
}
export function createStatus(body: { label: string; value: string; is_hold?: boolean }): Promise<AppStatus> {
return request('/statuses', { method: 'POST', body: JSON.stringify(body) })
}
export function updateStatus(id: number, body: Partial<AppStatus>): Promise<AppStatus> {
return request(`/statuses/${id}`, { method: 'PATCH', body: JSON.stringify(body) })
}
export function deleteStatus(id: number): Promise<{ ok: boolean }> {
return request(`/statuses/${id}`, { method: 'DELETE' })
}
// ── Categories ───────────────────────────────────────────────
export function fetchCategories(): Promise<Category[]> {
return request('/categories')
}
export function createCategory(body: { name: string; colour?: string }): Promise<Category> {
return request('/categories', { method: 'POST', body: JSON.stringify(body) })
}
export function updateCategory(id: number, body: Partial<Category>): Promise<Category> {
return request(`/categories/${id}`, { method: 'PATCH', body: JSON.stringify(body) })
}
export function deleteCategory(id: number): Promise<{ ok: boolean }> {
return request(`/categories/${id}`, { method: 'DELETE' })
}
// ── Projects ─────────────────────────────────────────────────
export function fetchProjects(): Promise<Project[]> {
return request('/projects')
}
export function fetchProject(id: number): Promise<ProjectDetail> {
return request(`/projects/${id}`)
}
export function createProject(body: { name: string; description?: string; colour?: string }): Promise<Project> {
return request('/projects', { method: 'POST', body: JSON.stringify(body) })
}
export function updateProject(id: number, body: Partial<Project>): Promise<Project> {
return request(`/projects/${id}`, { method: 'PATCH', body: JSON.stringify(body) })
}
export function deleteProject(id: number): Promise<{ ok: boolean }> {
return request(`/projects/${id}`, { method: 'DELETE' })
}
// ── 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 }) })
}