import type { Task, TaskInput, TaskSort, TaskStatus, TaskUpdate, } from "@/types/task"; import { getDatabase } from "./db"; type TaskRow = { id: number; title: string; description: string; due_date: string; topic: string; status: TaskStatus; archived_at: string | null; created_at: string; updated_at: string; }; const statuses: TaskStatus[] = ["Todo", "In-Progress", "Complete"]; function isTaskStatus(value: unknown): value is TaskStatus { return typeof value === "string" && statuses.includes(value as TaskStatus); } function todayIsoDate() { const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, "0"); const day = String(now.getDate()).padStart(2, "0"); return `${year}-${month}-${day}`; } function isDateInput(value: unknown): value is string { return typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value); } function cleanString(value: unknown, field: string) { if (typeof value !== "string") { throw new Error(`${field} must be text.`); } return value.trim(); } function normalizeTaskInput(input: Partial): TaskInput { const title = cleanString(input.title, "Title"); const description = typeof input.description === "string" ? input.description.trim() : ""; const dueDate = input.dueDate; const topic = cleanString(input.topic, "Topic"); if (!title) { throw new Error("Title is required."); } if (!topic) { throw new Error("Topic is required."); } if (!isDateInput(dueDate)) { throw new Error("Due date must use YYYY-MM-DD format."); } return { title, description, dueDate, topic, }; } function toTask(row: TaskRow): Task { return { id: row.id, title: row.title, description: row.description, dueDate: row.due_date, topic: row.topic, status: row.status, archivedAt: row.archived_at, createdAt: row.created_at, updatedAt: row.updated_at, isOverdue: !row.archived_at && row.status !== "Complete" && row.due_date < todayIsoDate(), }; } function rowToTask(row: unknown) { if (!row) { return null; } return toTask(row as TaskRow); } function sortClause(sort: TaskSort) { if (sort === "topic") { return "topic COLLATE NOCASE ASC, due_date ASC, id ASC"; } if (sort === "status") { return "CASE status WHEN 'Todo' THEN 1 WHEN 'In-Progress' THEN 2 ELSE 3 END ASC, due_date ASC, id ASC"; } if (sort === "dueDate") { return "due_date ASC, topic COLLATE NOCASE ASC, id ASC"; } return "created_at DESC, id DESC"; } export function listTasks({ includeArchived = false, sort = "createdAt", }: { includeArchived?: boolean; sort?: TaskSort; } = {}) { const database = getDatabase(); const where = includeArchived ? "" : "WHERE archived_at IS NULL"; const rows = database .prepare(`SELECT * FROM tasks ${where} ORDER BY ${sortClause(sort)}`) .all() as TaskRow[]; return rows.map(toTask); } export function getTask(id: number) { const database = getDatabase(); return rowToTask(database.prepare("SELECT * FROM tasks WHERE id = ?").get(id)); } export function createTask(input: Partial) { const database = getDatabase(); const task = normalizeTaskInput(input); const result = database .prepare( `INSERT INTO tasks (title, description, due_date, topic, status) VALUES (?, ?, ?, ?, 'Todo')` ) .run(task.title, task.description, task.dueDate, task.topic); const created = getTask(Number(result.lastInsertRowid)); if (!created) { throw new Error("Task could not be created."); } return created; } export function updateTask(id: number, updates: TaskUpdate) { const current = getTask(id); if (!current) { throw new Error("Task not found."); } const nextStatus = updates.status ?? current.status; if (!isTaskStatus(nextStatus)) { throw new Error("Status must be Todo, In-Progress, or Complete."); } const nextTask = normalizeTaskInput({ title: updates.title ?? current.title, description: updates.description ?? current.description, dueDate: updates.dueDate ?? current.dueDate, topic: updates.topic ?? current.topic, }); const database = getDatabase(); database .prepare( `UPDATE tasks SET title = ?, description = ?, due_date = ?, topic = ?, status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?` ) .run( nextTask.title, nextTask.description, nextTask.dueDate, nextTask.topic, nextStatus, id ); const updated = getTask(id); if (!updated) { throw new Error("Task could not be updated."); } return updated; } export function archiveTask(id: number) { if (!getTask(id)) { throw new Error("Task not found."); } const database = getDatabase(); database .prepare( `UPDATE tasks SET archived_at = COALESCE(archived_at, CURRENT_TIMESTAMP), updated_at = CURRENT_TIMESTAMP WHERE id = ?` ) .run(id); const archived = getTask(id); if (!archived) { throw new Error("Task could not be archived."); } return archived; }