1
10import { z } from "zod";
11
12
13export const TASK_STATUSES = ["todo", "in_progress", "done", "cancelled"] as const;
14export type TaskStatus = (typeof TASK_STATUSES)[number];
15
16
24export const TASK_TRANSITIONS: Record<TaskStatus, TaskStatus[]> = {
25 todo: ["in_progress", "done", "cancelled"],
26 in_progress: ["done", "cancelled", "todo"],
27 done: ["in_progress"],
28 cancelled: ["todo"]
29};
30
31
32export const ListTasksQuery = z.object({
33
34 scope: z.enum(["inbox", "all"]).default("inbox"),
35 status: z.enum(TASK_STATUSES).optional(),
36
37 dueBefore: z.string().datetime({ offset: true }).optional(),
38
39 dueAfter: z.string().datetime({ offset: true }).optional(),
40
41 month: z
42 .string()
43 .regex(/^\d{4}-(0[1-9]|1[0-2])$/, "month must be YYYY-MM")
44 .optional(),
45
46 overdue: z.coerce.boolean().optional(),
47 cursor: z.string().optional(),
48 limit: z.coerce.number().int().min(1).max(100).default(50)
49});
50export type ListTasksQueryInput = z.infer<typeof ListTasksQuery>;
51
52
53export const CreateTaskDto = z.object({
54 title: z.string().min(1).max(500),
55 parentId: z.string().min(1).optional(),
56 status: z.enum(TASK_STATUSES).default("todo"),
57 dueAt: z.string().datetime({ offset: true }).nullable().optional(),
58 assigneeIds: z.array(z.string().min(1)).max(50).default([])
59});
60export type CreateTaskInput = z.infer<typeof CreateTaskDto>;
61
62
63export const UpdateTaskDto = z
64 .object({
65 title: z.string().min(1).max(500).optional(),
66 status: z.enum(TASK_STATUSES).optional(),
67 dueAt: z.string().datetime({ offset: true }).nullable().optional(),
68
74 assigneeIds: z.array(z.string().min(1)).max(50).nullable().optional()
75 })
76 .refine((v) => Object.keys(v).length > 0, { message: "No fields to update" });
77export type UpdateTaskInput = z.infer<typeof UpdateTaskDto>;
78