- New todos table in DB schema (title, description, priority, category, due date, completion) - Full CRUD + toggle API routes at /api/todos - Categories support with filtering - Bulk import endpoint for migration - New TodosPage with inline editing, priority badges, due date display - Add Todos to sidebar navigation - Dark mode support throughout
303 lines
9.6 KiB
TypeScript
303 lines
9.6 KiB
TypeScript
import type { Task, Project, ProjectWithTasks, VelocityStats, Recurrence, Todo, TodoPriority } from "./types";
|
|
|
|
const BASE = "/api/tasks";
|
|
|
|
export async function fetchTasks(): Promise<Task[]> {
|
|
const res = await fetch(BASE, { credentials: "include" });
|
|
if (!res.ok) throw new Error(res.status === 401 ? "Unauthorized" : "Failed to fetch tasks");
|
|
return res.json();
|
|
}
|
|
|
|
export async function updateTask(
|
|
id: string,
|
|
updates: Record<string, any>,
|
|
token?: string
|
|
): Promise<Task> {
|
|
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
const res = await fetch(`${BASE}/${id}`, {
|
|
method: "PATCH",
|
|
credentials: "include",
|
|
headers,
|
|
body: JSON.stringify(updates),
|
|
});
|
|
if (!res.ok) throw new Error("Failed to update task");
|
|
return res.json();
|
|
}
|
|
|
|
export async function reorderTasks(ids: string[], token?: string): Promise<void> {
|
|
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
const res = await fetch(`${BASE}/reorder`, {
|
|
method: "PATCH",
|
|
credentials: "include",
|
|
headers,
|
|
body: JSON.stringify({ ids }),
|
|
});
|
|
if (!res.ok) throw new Error("Failed to reorder tasks");
|
|
}
|
|
|
|
export async function createTask(
|
|
task: { title: string; description?: string; source?: string; priority?: string; status?: string; projectId?: string; dueDate?: string; estimatedHours?: number; recurrence?: Recurrence | null },
|
|
token?: string
|
|
): Promise<Task> {
|
|
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
const res = await fetch(BASE, {
|
|
method: "POST",
|
|
credentials: "include",
|
|
headers,
|
|
body: JSON.stringify(task),
|
|
});
|
|
if (!res.ok) throw new Error("Failed to create task");
|
|
return res.json();
|
|
}
|
|
|
|
export async function deleteTask(id: string, token?: string): Promise<void> {
|
|
const headers: Record<string, string> = {};
|
|
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
const res = await fetch(`${BASE}/${id}`, {
|
|
method: "DELETE",
|
|
credentials: "include",
|
|
headers,
|
|
});
|
|
if (!res.ok) throw new Error("Failed to delete task");
|
|
}
|
|
|
|
// ─── Velocity Stats ───
|
|
|
|
export async function fetchVelocityStats(): Promise<VelocityStats> {
|
|
const res = await fetch(`${BASE}/stats/velocity`, { credentials: "include" });
|
|
if (!res.ok) throw new Error("Failed to fetch velocity stats");
|
|
return res.json();
|
|
}
|
|
|
|
// ─── Projects API ───
|
|
|
|
const PROJECTS_BASE = "/api/projects";
|
|
|
|
export async function fetchProjects(): Promise<Project[]> {
|
|
const res = await fetch(PROJECTS_BASE, { credentials: "include" });
|
|
if (!res.ok) throw new Error(res.status === 401 ? "Unauthorized" : "Failed to fetch projects");
|
|
return res.json();
|
|
}
|
|
|
|
export async function fetchProject(id: string): Promise<ProjectWithTasks> {
|
|
const res = await fetch(`${PROJECTS_BASE}/${id}`, { credentials: "include" });
|
|
if (!res.ok) throw new Error("Failed to fetch project");
|
|
return res.json();
|
|
}
|
|
|
|
export async function createProject(
|
|
project: { name: string; description?: string; context?: string; repos?: string[]; links?: { label: string; url: string }[] }
|
|
): Promise<Project> {
|
|
const res = await fetch(PROJECTS_BASE, {
|
|
method: "POST",
|
|
credentials: "include",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(project),
|
|
});
|
|
if (!res.ok) throw new Error("Failed to create project");
|
|
return res.json();
|
|
}
|
|
|
|
export async function updateProject(
|
|
id: string,
|
|
updates: Record<string, any>
|
|
): Promise<Project> {
|
|
const res = await fetch(`${PROJECTS_BASE}/${id}`, {
|
|
method: "PATCH",
|
|
credentials: "include",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(updates),
|
|
});
|
|
if (!res.ok) throw new Error("Failed to update project");
|
|
return res.json();
|
|
}
|
|
|
|
export async function deleteProject(id: string): Promise<void> {
|
|
const res = await fetch(`${PROJECTS_BASE}/${id}`, {
|
|
method: "DELETE",
|
|
credentials: "include",
|
|
});
|
|
if (!res.ok) throw new Error("Failed to delete project");
|
|
}
|
|
|
|
// Subtasks
|
|
export async function addSubtask(taskId: string, title: string): Promise<Task> {
|
|
const res = await fetch(`${BASE}/${taskId}/subtasks`, {
|
|
method: "POST",
|
|
credentials: "include",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ title }),
|
|
});
|
|
if (!res.ok) throw new Error("Failed to add subtask");
|
|
return res.json();
|
|
}
|
|
|
|
export async function toggleSubtask(taskId: string, subtaskId: string, completed: boolean): Promise<Task> {
|
|
const res = await fetch(`${BASE}/${taskId}/subtasks/${subtaskId}`, {
|
|
method: "PATCH",
|
|
credentials: "include",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ completed }),
|
|
});
|
|
if (!res.ok) throw new Error("Failed to toggle subtask");
|
|
return res.json();
|
|
}
|
|
|
|
export async function deleteSubtask(taskId: string, subtaskId: string): Promise<Task> {
|
|
const res = await fetch(`${BASE}/${taskId}/subtasks/${subtaskId}`, {
|
|
method: "DELETE",
|
|
credentials: "include",
|
|
});
|
|
if (!res.ok) throw new Error("Failed to delete subtask");
|
|
return res.json();
|
|
}
|
|
|
|
// Progress Notes
|
|
export async function addProgressNote(taskId: string, note: string): Promise<Task> {
|
|
const res = await fetch(`${BASE}/${taskId}/notes`, {
|
|
method: "POST",
|
|
credentials: "include",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ note }),
|
|
});
|
|
if (!res.ok) throw new Error("Failed to add progress note");
|
|
return res.json();
|
|
}
|
|
|
|
// ─── Comments API ───
|
|
|
|
export interface TaskComment {
|
|
id: string;
|
|
taskId: string;
|
|
authorId: string | null;
|
|
authorName: string;
|
|
content: string;
|
|
createdAt: string;
|
|
}
|
|
|
|
export async function fetchComments(taskId: string): Promise<TaskComment[]> {
|
|
const res = await fetch(`${BASE}/${taskId}/comments`, { credentials: "include" });
|
|
if (!res.ok) throw new Error("Failed to fetch comments");
|
|
return res.json();
|
|
}
|
|
|
|
export async function addComment(taskId: string, content: string): Promise<TaskComment> {
|
|
const res = await fetch(`${BASE}/${taskId}/comments`, {
|
|
method: "POST",
|
|
credentials: "include",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ content }),
|
|
});
|
|
if (!res.ok) throw new Error("Failed to add comment");
|
|
return res.json();
|
|
}
|
|
|
|
export async function deleteComment(taskId: string, commentId: string): Promise<void> {
|
|
const res = await fetch(`${BASE}/${taskId}/comments/${commentId}`, {
|
|
method: "DELETE",
|
|
credentials: "include",
|
|
});
|
|
if (!res.ok) throw new Error("Failed to delete comment");
|
|
}
|
|
|
|
// Admin API
|
|
export async function fetchUsers(): Promise<any[]> {
|
|
const res = await fetch("/api/admin/users", { credentials: "include" });
|
|
if (!res.ok) throw new Error("Failed to fetch users");
|
|
return res.json();
|
|
}
|
|
|
|
export async function updateUserRole(userId: string, role: string): Promise<any> {
|
|
const res = await fetch(`/api/admin/users/${userId}/role`, {
|
|
method: "PATCH",
|
|
credentials: "include",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ role }),
|
|
});
|
|
if (!res.ok) throw new Error("Failed to update user role");
|
|
return res.json();
|
|
}
|
|
|
|
export async function deleteUser(userId: string): Promise<void> {
|
|
const res = await fetch(`/api/admin/users/${userId}`, {
|
|
method: "DELETE",
|
|
credentials: "include",
|
|
});
|
|
if (!res.ok) throw new Error("Failed to delete user");
|
|
}
|
|
|
|
// ─── Todos API ───
|
|
|
|
const TODOS_BASE = "/api/todos";
|
|
|
|
export async function fetchTodos(params?: { completed?: string; category?: string }): Promise<Todo[]> {
|
|
const url = new URL(TODOS_BASE, window.location.origin);
|
|
if (params?.completed) url.searchParams.set("completed", params.completed);
|
|
if (params?.category) url.searchParams.set("category", params.category);
|
|
const res = await fetch(url.toString(), { credentials: "include" });
|
|
if (!res.ok) throw new Error(res.status === 401 ? "Unauthorized" : "Failed to fetch todos");
|
|
return res.json();
|
|
}
|
|
|
|
export async function fetchTodoCategories(): Promise<string[]> {
|
|
const res = await fetch(`${TODOS_BASE}/categories`, { credentials: "include" });
|
|
if (!res.ok) throw new Error("Failed to fetch categories");
|
|
return res.json();
|
|
}
|
|
|
|
export async function createTodo(todo: {
|
|
title: string;
|
|
description?: string;
|
|
priority?: TodoPriority;
|
|
category?: string;
|
|
dueDate?: string | null;
|
|
}): Promise<Todo> {
|
|
const res = await fetch(TODOS_BASE, {
|
|
method: "POST",
|
|
credentials: "include",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(todo),
|
|
});
|
|
if (!res.ok) throw new Error("Failed to create todo");
|
|
return res.json();
|
|
}
|
|
|
|
export async function updateTodo(id: string, updates: Partial<{
|
|
title: string;
|
|
description: string;
|
|
priority: TodoPriority;
|
|
category: string | null;
|
|
dueDate: string | null;
|
|
isCompleted: boolean;
|
|
sortOrder: number;
|
|
}>): Promise<Todo> {
|
|
const res = await fetch(`${TODOS_BASE}/${id}`, {
|
|
method: "PATCH",
|
|
credentials: "include",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(updates),
|
|
});
|
|
if (!res.ok) throw new Error("Failed to update todo");
|
|
return res.json();
|
|
}
|
|
|
|
export async function toggleTodo(id: string): Promise<Todo> {
|
|
const res = await fetch(`${TODOS_BASE}/${id}/toggle`, {
|
|
method: "PATCH",
|
|
credentials: "include",
|
|
});
|
|
if (!res.ok) throw new Error("Failed to toggle todo");
|
|
return res.json();
|
|
}
|
|
|
|
export async function deleteTodo(id: string): Promise<void> {
|
|
const res = await fetch(`${TODOS_BASE}/${id}`, {
|
|
method: "DELETE",
|
|
credentials: "include",
|
|
});
|
|
if (!res.ok) throw new Error("Failed to delete todo");
|
|
}
|