/** * T-159…T-161 — Task DTOs (Zod, validated by the global ZodValidate pipe). * * A Task is a 1:1 sidecar of a `pages.kind='task'` page (A-006): the page * carries the blocks/comments/permissions tree; the sidecar carries the * queryable task fields (status/due/assignees). `assigneeIds` uses Prisma's * `String[]` — the API exposes it as a JSON array (additive-only, no new * column types). */ import { z } from "zod"; /** Task status machine (T-159): todo | in_progress | done | cancelled. */ export const TASK_STATUSES = ["todo", "in_progress", "done", "cancelled"] as const; export type TaskStatus = (typeof TASK_STATUSES)[number]; /** * Legal transitions (T-160's "inbox" semantics lean on this; enforced in the * service — an illegal transition is 400, not a silent overwrite). * todo → in_progress | done | cancelled * in_progress → done | cancelled | todo * done → in_progress (re-open) * cancelled → todo (re-open) */ export const TASK_TRANSITIONS: Record = { todo: ["in_progress", "done", "cancelled"], in_progress: ["done", "cancelled", "todo"], done: ["in_progress"], cancelled: ["todo"] }; /** `GET /workspaces/:workspaceId/tasks` — inbox + filters. */ export const ListTasksQuery = z.object({ /** `inbox` = tasks where the CALLER is an assignee (the default); `all` = every task in the workspace. */ scope: z.enum(["inbox", "all"]).default("inbox"), status: z.enum(TASK_STATUSES).optional(), /** due before this ISO date (overdue tab). */ dueBefore: z.string().datetime({ offset: true }).optional(), /** due after this ISO date. */ dueAfter: z.string().datetime({ offset: true }).optional(), /** calendar tab: tasks whose dueAt falls in [monthStart, monthEnd) UTC. */ month: z .string() .regex(/^\d{4}-(0[1-9]|1[0-2])$/, "month must be YYYY-MM") .optional(), /** overdue = dueAt < now && status not in (done, cancelled). Shorthand for the client. */ overdue: z.coerce.boolean().optional(), cursor: z.string().optional(), limit: z.coerce.number().int().min(1).max(100).default(50) }); export type ListTasksQueryInput = z.infer; /** `POST /workspaces/:workspaceId/tasks` — create a task page + sidecar. */ export const CreateTaskDto = z.object({ title: z.string().min(1).max(500), parentId: z.string().min(1).optional(), status: z.enum(TASK_STATUSES).default("todo"), dueAt: z.string().datetime({ offset: true }).nullable().optional(), assigneeIds: z.array(z.string().min(1)).max(50).default([]) }); export type CreateTaskInput = z.infer; /** `PATCH /tasks/:id` — partial update of the sidecar fields. */ export const UpdateTaskDto = z .object({ title: z.string().min(1).max(500).optional(), status: z.enum(TASK_STATUSES).optional(), dueAt: z.string().datetime({ offset: true }).nullable().optional(), /** * Assignees: an ARRAY replaces the whole list (the client sends the full * set); `null` clears it. Individual user add/remove is a convenience * `addAssignee`/`removeAssignee` pair of single-element patches — but the * canonical wire shape is the full list (matches Prisma `String[]`). */ assigneeIds: z.array(z.string().min(1)).max(50).nullable().optional() }) .refine((v) => Object.keys(v).length > 0, { message: "No fields to update" }); export type UpdateTaskInput = z.infer;