- 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
145 lines
7.1 KiB
TypeScript
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 }) })
|
|
}
|