Files
hammer-queue/frontend/src/lib/api.ts
Hammer dd2c80224e feat: add personal todos feature
- 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
2026-01-30 04:44:34 +00:00

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");
}