import { z } from "zod"; // --------------------------------------------------------------------------- // Common // --------------------------------------------------------------------------- export const HealthResponse = z.object({ ok: z.literal(true), version: z.string(), uptime: z.number(), }); export type HealthResponse = z.infer; export const AuthVerifyResponse = z.object({ ok: z.literal(true), }); export type AuthVerifyResponse = z.infer; // --------------------------------------------------------------------------- // Phase 1 placeholders — fully fleshed out in Phase 1 // --------------------------------------------------------------------------- export const RemoteType = z.enum(["remote", "hybrid", "onsite"]); export type RemoteType = z.infer; // --------------------------------------------------------------------------- // Phase 1 — Jobs // --------------------------------------------------------------------------- export const JobIngestDto = z.object({ source: z.string().min(1), source_url: z.string().url(), title: z.string().min(1), company: z.string().optional().nullable(), description: z.string().optional().nullable(), location: z.string().optional().nullable(), remote_type: RemoteType.optional().nullable(), salary_min: z.number().int().nonnegative().optional().nullable(), salary_max: z.number().int().nonnegative().optional().nullable(), stack: z.array(z.string()).default([]), apply_url: z.string().url().optional().nullable(), }); export type JobIngestDto = z.infer; export const JobIngestRequest = z.object({ jobs: z.array(JobIngestDto).min(1).max(500), }); export type JobIngestRequest = z.infer; export const JobIngestResponse = z.object({ inserted: z.number().int().nonnegative(), updated: z.number().int().nonnegative(), archived: z.number().int().nonnegative(), }); export type JobIngestResponse = z.infer; export const Job = JobIngestDto.extend({ id: z.string().uuid(), first_seen_at: z.string(), last_seen_at: z.string(), archived: z.boolean(), starred: z.boolean(), applied_at: z.string().nullable(), notes: z.string().nullable(), }); export type Job = z.infer; export const JobPatchDto = z.object({ starred: z.boolean().optional(), archived: z.boolean().optional(), applied_at: z.string().datetime().nullable().optional(), notes: z.string().nullable().optional(), }); export type JobPatchDto = z.infer; export const JobListQuery = z.object({ archived: z .union([z.boolean(), z.enum(["true", "false"])]) .transform((v) => (typeof v === "boolean" ? v : v === "true")) .optional(), remote_type: RemoteType.optional(), stack: z.array(z.string()).optional(), since: z.string().datetime().optional(), starred: z .union([z.boolean(), z.enum(["true", "false"])]) .transform((v) => (typeof v === "boolean" ? v : v === "true")) .optional(), }); export type JobListQuery = z.infer; export const JobSearchCriteria = z.object({ id: z.string().uuid(), name: z.string().nullable(), titles: z.array(z.string()), locations: z.array(z.string()), stack: z.array(z.string()), remote_types: z.array(RemoteType), salary_min: z.number().int().nullable(), active: z.boolean(), created_at: z.string(), updated_at: z.string(), }); export type JobSearchCriteria = z.infer; export const JobSearchCriteriaUpsert = z.object({ name: z.string().optional().nullable(), titles: z.array(z.string()).default([]), locations: z.array(z.string()).default([]), stack: z.array(z.string()).default([]), remote_types: z.array(RemoteType).default([]), salary_min: z.number().int().nonnegative().nullable().optional(), active: z.boolean().default(true), }); export type JobSearchCriteriaUpsert = z.infer; // --------------------------------------------------------------------------- // Phase 2 — Todos // --------------------------------------------------------------------------- export const TodoStatus = z.enum(["inbox", "todo", "doing", "done", "archived"]); export type TodoStatus = z.infer; export const TodoEnergy = z.enum(["low", "med", "high"]); export type TodoEnergy = z.infer; export const TodoChecklistItem = z.object({ text: z.string().min(1), done: z.boolean().default(false), }); export type TodoChecklistItem = z.infer; export const TodoPriority = z.number().int().min(0).max(3); export type TodoPriority = z.infer; export const Todo = z.object({ id: z.string().uuid(), title: z.string(), description: z.string().nullable(), status: TodoStatus, priority: TodoPriority.nullable(), due_at: z.string().nullable(), tags: z.array(z.string()), project_id: z.string().uuid().nullable(), checklist: z.array(TodoChecklistItem), energy: TodoEnergy.nullable(), context: z.string().nullable(), recurrence: z.string().nullable(), ticket_url: z.string().nullable(), verification_steps: z.array(z.string()), ai_enriched: z.boolean(), created_at: z.string(), completed_at: z.string().nullable(), }); export type Todo = z.infer; export const TodoCreateDto = z.object({ title: z.string().min(1), description: z.string().optional().nullable(), status: TodoStatus.default("inbox"), priority: TodoPriority.optional().nullable(), due_at: z.string().datetime().optional().nullable(), tags: z.array(z.string()).default([]), project_id: z.string().uuid().optional().nullable(), checklist: z.array(TodoChecklistItem).default([]), energy: TodoEnergy.optional().nullable(), context: z.string().optional().nullable(), recurrence: z.string().optional().nullable(), ticket_url: z.string().url().optional().nullable(), verification_steps: z.array(z.string()).default([]), }); export type TodoCreateDto = z.infer; export const TodoPatchDto = TodoCreateDto.partial().extend({ ai_enriched: z.boolean().optional(), completed_at: z.string().datetime().optional().nullable(), }); export type TodoPatchDto = z.infer; export const TodoListQuery = z.object({ status: TodoStatus.optional(), priority: z.coerce.number().int().min(0).max(3).optional(), context: z.string().optional(), tags: z.array(z.string()).optional(), project_id: z.string().uuid().optional(), archived: z .union([z.boolean(), z.enum(["true", "false"])]) .transform((v) => (typeof v === "boolean" ? v : v === "true")) .optional(), }); export type TodoListQuery = z.infer; export const TodoAiEnrichDraft = z.object({ description: z.string().optional(), priority: TodoPriority.optional(), tags: z.array(z.string()).optional(), context: z.string().optional(), energy: TodoEnergy.optional(), verification_steps: z.array(z.string()).optional(), }); export type TodoAiEnrichDraft = z.infer; export const TodoAiEnrichResponse = z.object({ todo_id: z.string().uuid(), draft: TodoAiEnrichDraft, }); export type TodoAiEnrichResponse = z.infer; export const TodoAiEnrichApplyRequest = z.object({ draft: TodoAiEnrichDraft, }); export type TodoAiEnrichApplyRequest = z.infer; // --------------------------------------------------------------------------- // Phase 3 — Projects // --------------------------------------------------------------------------- export const ProjectStatus = z.enum(["active", "archived"]); export type ProjectStatus = z.infer; export const StepStatus = z.enum(["backlog", "todo", "doing", "review", "done"]); export type StepStatus = z.infer; export const Project = z.object({ id: z.string().uuid(), name: z.string(), description: z.string().nullable(), status: ProjectStatus, created_at: z.string(), updated_at: z.string(), }); export type Project = z.infer; export const ProjectCreateDto = z.object({ name: z.string().min(1), description: z.string().optional().nullable(), status: ProjectStatus.default("active"), }); export type ProjectCreateDto = z.infer; export const ProjectPatchDto = ProjectCreateDto.partial(); export type ProjectPatchDto = z.infer; export const ProjectStep = z.object({ id: z.string().uuid(), project_id: z.string().uuid(), title: z.string(), status: StepStatus, position: z.number(), created_at: z.string(), updated_at: z.string(), }); export type ProjectStep = z.infer; export const ProjectStepCreateDto = z.object({ title: z.string().min(1), status: StepStatus.default("backlog"), position: z.number().optional(), }); export type ProjectStepCreateDto = z.infer; export const ProjectStepPatchDto = z.object({ title: z.string().min(1).optional(), status: StepStatus.optional(), position: z.number().optional(), }); export type ProjectStepPatchDto = z.infer; export const ProjectIdea = z.object({ id: z.string().uuid(), project_id: z.string().uuid().nullable(), content: z.string(), priority: z.number().int().min(0).max(3).nullable(), created_at: z.string(), }); export type ProjectIdea = z.infer; export const ProjectIdeaCreateDto = z.object({ content: z.string().min(1), priority: z.number().int().min(0).max(3).optional().nullable(), project_id: z.string().uuid().optional().nullable(), }); export type ProjectIdeaCreateDto = z.infer; export const ProjectIdeaPatchDto = ProjectIdeaCreateDto.partial(); export type ProjectIdeaPatchDto = z.infer; // --------------------------------------------------------------------------- // Phase 5 — AI proposed actions (kept here so PWA + API agree) // --------------------------------------------------------------------------- export const ProposedAction = z.discriminatedUnion("fn", [ z.object({ fn: z.literal("create_todo"), args: z.object({ title: z.string(), description: z.string().optional(), due_at: z.string().datetime({ offset: true }).optional(), priority: z.number().int().min(0).max(3).optional(), project_id: z.string().uuid().optional(), tags: z.array(z.string()).optional(), }), }), z.object({ fn: z.literal("add_project_idea"), args: z.object({ project_id: z.string().uuid(), content: z.string(), }), }), z.object({ fn: z.literal("add_project_step"), args: z.object({ project_id: z.string().uuid(), title: z.string(), status: z.enum(["backlog", "todo", "doing", "review", "done"]).optional(), }), }), z.object({ fn: z.literal("create_calendar_event"), args: z.object({ title: z.string(), starts_at: z.string().datetime({ offset: true }), ends_at: z.string().datetime({ offset: true }), location: z.string().optional(), description: z.string().optional(), }), }), z.object({ fn: z.literal("toggle_daily_checkin"), args: z.object({ note: z.string().optional(), }), }), z.object({ fn: z.literal("capture_idea"), args: z.object({ content: z.string(), priority: z.number().int().min(0).max(3).optional(), project_id: z.string().uuid().optional(), }), }), ]); export type ProposedAction = z.infer; /** * Chaque action proposée est persistée dans `ai_actions` en status='proposed'. * L'API renvoie un id par action pour que la PWA puisse les confirmer/annuler. */ export const ProposedActionWithId = z.object({ id: z.string().uuid(), action: ProposedAction, }); export type ProposedActionWithId = z.infer; // POST /ai/command — texte brut (ex: Cmd-K) export const AiCommandRequest = z.object({ text: z.string().min(1).max(4000), }); export type AiCommandRequest = z.infer; export const AiCommandResponse = z.object({ actions: z.array(ProposedActionWithId), }); export type AiCommandResponse = z.infer; // POST /ai/voice — réponse multipart (transcript + actions) export const AiVoiceResponse = z.object({ transcript: z.string(), actions: z.array(ProposedActionWithId), }); export type AiVoiceResponse = z.infer; // POST /ai/command/confirm — la PWA confirme/annule les actions proposées export const AiConfirmRequest = z.object({ action_ids: z.array(z.string().uuid()).min(1), }); export type AiConfirmRequest = z.infer; export const AiActionResult = z.object({ id: z.string().uuid(), status: z.enum(["confirmed", "failed"]), result: z.unknown().optional(), error: z.string().optional(), }); export type AiActionResult = z.infer; export const AiConfirmResponse = z.object({ results: z.array(AiActionResult), }); export type AiConfirmResponse = z.infer; // --------------------------------------------------------------------------- // Phase 4 — Agenda / Calendar events // --------------------------------------------------------------------------- export const CalendarEvent = z.object({ id: z.string().uuid(), google_event_id: z.string().nullable(), title: z.string(), description: z.string().nullable(), location: z.string().nullable(), starts_at: z.string(), ends_at: z.string(), all_day: z.boolean(), source: z.string(), created_at: z.string(), updated_at: z.string(), }); export type CalendarEvent = z.infer; export const CalendarEventCreateDto = z.object({ title: z.string().min(1), description: z.string().nullable().optional(), location: z.string().nullable().optional(), starts_at: z.string().datetime({ offset: true }), ends_at: z.string().datetime({ offset: true }), all_day: z.boolean().optional(), }); export type CalendarEventCreateDto = z.infer; export const CalendarEventPatchDto = CalendarEventCreateDto.partial(); export type CalendarEventPatchDto = z.infer; export const CalendarEventListQuery = z.object({ from: z.string().datetime().optional(), to: z.string().datetime().optional(), }); export type CalendarEventListQuery = z.infer; export const GoogleSyncResponse = z.object({ inserted: z.number().int(), updated: z.number().int(), }); export type GoogleSyncResponse = z.infer; // --------------------------------------------------------------------------- // Phase 7 — Daily check-in (médocs + note) // --------------------------------------------------------------------------- export const DailyCheckin = z.object({ day: z.string(), // YYYY-MM-DD meds_taken: z.boolean(), note: z.string().nullable(), updated_at: z.string(), }); export type DailyCheckin = z.infer; export const DailyCheckinToggleRequest = z.object({ meds_taken: z.boolean().optional(), note: z.string().nullable().optional(), }); export type DailyCheckinToggleRequest = z.infer;