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
This commit is contained in:
jtricerolph 2026-09-17 08:52:08 +00:00
parent d8dc4e2b81
commit 017f6dfdc1
19 changed files with 1008 additions and 71 deletions

View file

@ -1,4 +1,4 @@
import type { List, Task, TaskDetail, TaskItem, TaskComment, AppConfig } from './types'
import type { List, Task, TaskDetail, TaskItem, TaskComment, AppConfig, AppStatus, Category, Project, ProjectDetail } from './types'
const BASE = '/tasks/api'
@ -35,7 +35,14 @@ export function deleteList(id: number): Promise<{ ok: boolean }> {
}
// ── Tasks ────────────────────────────────────────────────────
export function fetchTasks(params: { list_id?: number; view?: 'today'; status?: string } = {}): Promise<Task[]> {
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()
@ -56,6 +63,62 @@ export function deleteTask(id: number): Promise<{ ok: boolean }> {
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> {