diff --git a/backend/src/db/schema.ts b/backend/src/db/schema.ts index 62f5690..c70dc8a 100644 --- a/backend/src/db/schema.ts +++ b/backend/src/db/schema.ts @@ -3,6 +3,7 @@ import { uuid, text, integer, + serial, timestamp, jsonb, pgEnum, @@ -40,6 +41,7 @@ export interface ProgressNote { export const tasks = pgTable("tasks", { id: uuid("id").defaultRandom().primaryKey(), + taskNumber: serial("task_number").notNull(), title: text("title").notNull(), description: text("description"), source: taskSourceEnum("source").notNull().default("donovan"), diff --git a/backend/src/routes/tasks.ts b/backend/src/routes/tasks.ts index 5d44fd0..3330e30 100644 --- a/backend/src/routes/tasks.ts +++ b/backend/src/routes/tasks.ts @@ -1,7 +1,7 @@ import { Elysia, t } from "elysia"; import { db } from "../db"; import { tasks, type ProgressNote } from "../db/schema"; -import { eq, asc, desc, sql, inArray } from "drizzle-orm"; +import { eq, asc, desc, sql, inArray, or } from "drizzle-orm"; import { auth } from "../lib/auth"; const BEARER_TOKEN = process.env.API_BEARER_TOKEN || "hammer-dev-token"; @@ -68,6 +68,23 @@ async function requireSessionOrBearer(request: Request, headers: Record { const msg = error?.message || String(error); @@ -151,11 +168,26 @@ export const taskRoutes = new Elysia({ prefix: "/api/tasks" }) } ) + // GET single task by ID or number - requires session or bearer auth + .get( + "/:id", + async ({ params, request, headers }) => { + await requireSessionOrBearer(request, headers); + const task = await resolveTask(params.id); + if (!task) throw new Error("Task not found"); + return task; + }, + { params: t.Object({ id: t.String() }) } + ) + // PATCH update task - requires session or bearer auth .patch( "/:id", async ({ params, body, request, headers }) => { await requireSessionOrBearer(request, headers); + const task = await resolveTask(params.id); + if (!task) throw new Error("Task not found"); + const updates: Record = { updatedAt: new Date() }; if (body.title !== undefined) updates.title = body.title; if (body.description !== undefined) updates.description = body.description; @@ -179,7 +211,7 @@ export const taskRoutes = new Elysia({ prefix: "/api/tasks" }) const updated = await db .update(tasks) .set(updates) - .where(eq(tasks.id, params.id)) + .where(eq(tasks.id, task.id)) .returning(); if (!updated.length) throw new Error("Task not found"); @@ -208,13 +240,10 @@ export const taskRoutes = new Elysia({ prefix: "/api/tasks" }) "/:id/notes", async ({ params, body, request, headers }) => { await requireSessionOrBearer(request, headers); - const existing = await db - .select() - .from(tasks) - .where(eq(tasks.id, params.id)); - if (!existing.length) throw new Error("Task not found"); + const task = await resolveTask(params.id); + if (!task) throw new Error("Task not found"); - const currentNotes = (existing[0].progressNotes || []) as ProgressNote[]; + const currentNotes = (task.progressNotes || []) as ProgressNote[]; const newNote: ProgressNote = { timestamp: new Date().toISOString(), note: body.note, @@ -224,7 +253,7 @@ export const taskRoutes = new Elysia({ prefix: "/api/tasks" }) const updated = await db .update(tasks) .set({ progressNotes: currentNotes, updatedAt: new Date() }) - .where(eq(tasks.id, params.id)) + .where(eq(tasks.id, task.id)) .returning(); return updated[0]; }, @@ -259,11 +288,9 @@ export const taskRoutes = new Elysia({ prefix: "/api/tasks" }) "/:id", async ({ params, request, headers }) => { await requireSessionOrBearer(request, headers); - const deleted = await db - .delete(tasks) - .where(eq(tasks.id, params.id)) - .returning(); - if (!deleted.length) throw new Error("Task not found"); + const task = await resolveTask(params.id); + if (!task) throw new Error("Task not found"); + await db.delete(tasks).where(eq(tasks.id, task.id)); return { success: true }; }, { diff --git a/frontend/src/components/TaskCard.tsx b/frontend/src/components/TaskCard.tsx index a2eca34..7f20302 100644 --- a/frontend/src/components/TaskCard.tsx +++ b/frontend/src/components/TaskCard.tsx @@ -66,18 +66,9 @@ export function TaskCard({ isActive, onClick, }: TaskCardProps) { - const [idCopied, setIdCopied] = useState(false); const actions = statusActions[task.status] || []; const noteCount = task.progressNotes?.length || 0; - const shortId = task.id.slice(0, 8); - - const handleCopyId = (e: React.MouseEvent) => { - e.stopPropagation(); - navigator.clipboard.writeText(task.id).then(() => { - setIdCopied(true); - setTimeout(() => setIdCopied(false), 1500); - }); - }; + const displayId = task.taskNumber ? `HQ-${task.taskNumber}` : task.id.slice(0, 8); return (
{ e.stopPropagation(); - navigator.clipboard.writeText(task.id); + navigator.clipboard.writeText(displayId); }} > - {task.id.slice(0, 8)} + {displayId} {task.priority} @@ -127,17 +118,6 @@ export function TaskCard({ 💬 {noteCount} )} -
{task.description && ( diff --git a/frontend/src/components/TaskDetailPanel.tsx b/frontend/src/components/TaskDetailPanel.tsx index 2821eb0..07359a6 100644 --- a/frontend/src/components/TaskDetailPanel.tsx +++ b/frontend/src/components/TaskDetailPanel.tsx @@ -118,30 +118,46 @@ function ElapsedTimer({ since }: { since: string }) { ); } -function CopyableId({ id }: { id: string }) { - const [copied, setCopied] = useState(false); +function CopyableId({ id, taskNumber }: { id: string; taskNumber?: number }) { + const [copied, setCopied] = useState(null); + const displayId = taskNumber ? `HQ-${taskNumber}` : null; - const handleCopy = () => { - navigator.clipboard.writeText(id).then(() => { - setCopied(true); - setTimeout(() => setCopied(false), 2000); + const handleCopy = (value: string, label: string) => { + navigator.clipboard.writeText(value).then(() => { + setCopied(label); + setTimeout(() => setCopied(null), 2000); }); }; return (
- ID: + {displayId && ( + <> + {displayId} + + | + + )} {id}
); @@ -189,7 +205,10 @@ export function TaskDetailPanel({ task, onClose, onStatusChange, hasToken }: Tas {task.source} -

{task.title}

+

+ {task.taskNumber && HQ-{task.taskNumber}} + {task.title} +