// Request validation schemas, shared so the web app can validate forms with the same rules as the API. import { z } from 'zod' import { actionKinds, actionStatuses, areaKinds, incidentStatuses, incidentTypes, rcaMethods, roles, rootCauseCategories, severities } from './enums.ts' const email = z.string().trim().toLowerCase().max(254).pipe(z.email()) const password = z.string().min(10, 'Use at least 10 characters').max(256) const name = z.string().trim().min(1).max(200) const optionalText = (max: number) => z.string().trim().max(max).nullish() const isoDateTime = z.iso.datetime({ offset: true }) export const setupSchema = z.object({ organizationName: name, name, email, password }) export const loginSchema = z.object({ email, password: z.string().min(1).max(256) }) export const changePasswordSchema = z.object({ currentPassword: z.string().min(1).max(256), newPassword: password }) /** Without a password, the user is emailed an invitation to set one. */ export const createUserSchema = z.object({ name, email, role: z.enum(roles), password: password.optional() }) /** `password` lets an admin set a new password when email isn't available. */ export const updateUserSchema = z.object({ name: name.optional(), role: z.enum(roles).optional(), active: z.boolean().optional(), password: password.optional() }) export const createSiteSchema = z.object({ name, address: optionalText(500), timezone: z.string().trim().max(64).optional() }) export const updateSiteSchema = createSiteSchema.partial().extend({ archived: z.boolean().optional() }) export const createAreaSchema = z.object({ name, kind: z.enum(areaKinds).default('area'), parentId: z.uuid().nullish() }) export const updateAreaSchema = z.object({ name: name.optional(), kind: z.enum(areaKinds).optional(), archived: z.boolean().optional() }) export const createIncidentSchema = z.object({ title: name, type: z.enum(incidentTypes), severity: z.enum(severities), occurredAt: isoDateTime, siteId: z.uuid(), areaId: z.uuid().nullish(), locationDetail: optionalText(500), description: optionalText(10_000), immediateAction: optionalText(10_000), anonymous: z.boolean().default(false), }) /** Status changes go through transitions, not updates. */ export const updateIncidentSchema = createIncidentSchema.omit({ anonymous: true }).partial() export const transitionSchema = z.object({ to: z.enum(incidentStatuses), note: optionalText(5_000) }) export const commentSchema = z.object({ body: z.string().trim().min(1).max(5_000) }) export const investigationSchema = z.object({ leadId: z.uuid().nullish(), method: z.enum(rcaMethods).optional(), summary: optionalText(20_000), completed: z.boolean().optional(), }) const rootCauseFields = { category: z.enum(rootCauseCategories), description: z.string().trim().min(1).max(2_000), whys: z.array(z.string().trim().min(1).max(1_000)).max(10), } export const rootCauseSchema = z.object({ ...rootCauseFields, whys: rootCauseFields.whys.default([]) }) // Update schemas are written out rather than derived with .partial(): in Zod 4, .partial() keeps // defaults, so a partial update would silently reset omitted fields. export const updateRootCauseSchema = z.object(rootCauseFields).partial() const isoDate = z.iso.date() const actionFields = { title: name, description: optionalText(5_000), kind: z.enum(actionKinds), assigneeId: z.uuid().nullish(), dueDate: isoDate.nullish(), rootCauseId: z.uuid().nullish(), } export const createActionSchema = z.object({ ...actionFields, kind: actionFields.kind.default('corrective') }) export const updateActionSchema = z.object({ ...actionFields, status: z.enum(actionStatuses) }).partial() export const actionListQuery = z.object({ scope: z.enum(['mine', 'all']).default('mine'), filter: z.enum(['open', 'overdue', 'done', 'all']).default('open'), }) export const emailOnlySchema = z.object({ email }) export const tokenSchema = z.object({ token: z.string().min(20).max(200) }) export const resetPasswordSchema = z.object({ token: z.string().min(20).max(200), newPassword: password }) export const incidentListQuery = z.object({ page: z.coerce.number().int().min(1).default(1), pageSize: z.coerce.number().int().min(1).max(100).default(25), q: z.string().trim().max(200).optional(), type: z.enum(incidentTypes).optional(), severity: z.enum(severities).optional(), status: z.enum(incidentStatuses).optional(), siteId: z.uuid().optional(), }) export type SetupInput = z.infer export type CreateIncidentInput = z.input export type UpdateIncidentInput = z.infer export type CreateActionInput = z.input export type UpdateActionInput = z.input export type RootCauseInput = z.input export type UpdateRootCauseInput = z.input export type InvestigationInput = z.input export type IncidentListQuery = z.infer