import { z } from "zod"; // ---- shared -------------------------------------------------------------- const NonEmpty = z.string().trim().min(1, "Required").max(200); const Code = z.string().trim().min(1).max(64).regex(/^[A-Za-z0-9._\-/]+$/, "Use letters, digits, . _ - /"); const OptionalText = z .string() .trim() .max(5000) .transform((v) => (v.length === 0 ? null : v)) .nullable() .optional(); const JsonString = z .string() .max(10_000) .refine( (s) => { if (s.length === 0) return true; try { JSON.parse(s); return true; } catch { return false; } }, { message: "Must be valid JSON" }, ) .transform((s) => (s.length === 0 ? null : s)) .nullable() .optional(); const Pin = z.string().regex(/^\d{4}$/, "PIN must be exactly 4 digits"); // ---- users --------------------------------------------------------------- export const CreateAdminSchema = z.object({ role: z.literal("admin"), name: NonEmpty, email: z.string().email().max(200), password: z.string().min(8).max(200), }); export const CreateOperatorSchema = z.object({ role: z.literal("operator"), name: NonEmpty, pin: Pin, }); export const CreateUserSchema = z.discriminatedUnion("role", [ CreateAdminSchema, CreateOperatorSchema, ]); export const UpdateUserSchema = z .object({ name: NonEmpty.optional(), active: z.boolean().optional(), email: z.string().email().max(200).optional(), password: z.string().min(8).max(200).optional(), pin: Pin.optional(), }) .strict(); // ---- machines ------------------------------------------------------------ export const MachineKinds = [ "NCT_PUNCH", "PRESS_BRAKE", "RIVET", "WELD", "LASER", "SHEAR", "ASSEMBLY", "OTHER", ] as const; export const CreateMachineSchema = z.object({ name: NonEmpty, kind: z.enum(MachineKinds), location: OptionalText, notes: OptionalText, }); export const UpdateMachineSchema = z .object({ name: NonEmpty.optional(), kind: z.enum(MachineKinds).optional(), location: OptionalText, notes: OptionalText, active: z.boolean().optional(), }) .strict(); // ---- operation templates ------------------------------------------------- export const CreateTemplateSchema = z.object({ name: NonEmpty, machineId: z.string().min(1).nullable().optional(), defaultSettings: JsonString, defaultInstructions: OptionalText, qcRequired: z.boolean().default(false), }); export const UpdateTemplateSchema = z .object({ name: NonEmpty.optional(), machineId: z.string().min(1).nullable().optional(), defaultSettings: JsonString, defaultInstructions: OptionalText, qcRequired: z.boolean().optional(), active: z.boolean().optional(), }) .strict(); // ---- projects ------------------------------------------------------------ export const ProjectStatuses = ["planning", "in_progress", "completed", "cancelled"] as const; export const CreateProjectSchema = z.object({ code: Code, name: NonEmpty, customerCode: z .string() .trim() .max(64) .transform((v) => (v.length === 0 ? null : v)) .nullable() .optional(), dueDate: z.coerce.date().nullable().optional(), notes: OptionalText, }); export const UpdateProjectSchema = z .object({ code: Code.optional(), name: NonEmpty.optional(), customerCode: z .string() .trim() .max(64) .transform((v) => (v.length === 0 ? null : v)) .nullable() .optional(), dueDate: z.coerce.date().nullable().optional(), status: z.enum(ProjectStatuses).optional(), notes: OptionalText, }) .strict(); // ---- assemblies / parts -------------------------------------------------- export const CreateAssemblySchema = z.object({ code: Code, name: NonEmpty, qty: z.coerce.number().int().positive().max(100000).default(1), notes: OptionalText, }); export const UpdateAssemblySchema = z .object({ code: Code.optional(), name: NonEmpty.optional(), qty: z.coerce.number().int().positive().max(100000).optional(), notes: OptionalText, }) .strict(); export const CreatePartSchema = z.object({ code: Code, name: NonEmpty, material: z .string() .trim() .max(120) .transform((v) => (v.length === 0 ? null : v)) .nullable() .optional(), qty: z.coerce.number().int().positive().max(100000).default(1), notes: OptionalText, }); export const UpdatePartSchema = z .object({ code: Code.optional(), name: NonEmpty.optional(), material: z .string() .trim() .max(120) .transform((v) => (v.length === 0 ? null : v)) .nullable() .optional(), qty: z.coerce.number().int().positive().max(100000).optional(), notes: OptionalText, stepFileId: z.string().min(1).nullable().optional(), drawingFileId: z.string().min(1).nullable().optional(), cutFileId: z.string().min(1).nullable().optional(), }) .strict(); // ---- operations --------------------------------------------------------- export const OperationStatuses = ["pending", "in_progress", "completed"] as const; export const CreateOperationSchema = z.object({ templateId: z.string().min(1).nullable().optional(), name: NonEmpty, machineId: z.string().min(1).nullable().optional(), settings: JsonString, materialNotes: OptionalText, instructions: OptionalText, qcRequired: z.boolean().default(false), plannedMinutes: z.coerce.number().int().positive().max(100000).nullable().optional(), plannedUnits: z.coerce.number().int().positive().max(100000).nullable().optional(), sequence: z.coerce.number().int().positive().max(10000).optional(), }); export const UpdateOperationSchema = z .object({ templateId: z.string().min(1).nullable().optional(), name: NonEmpty.optional(), machineId: z.string().min(1).nullable().optional(), settings: JsonString, materialNotes: OptionalText, instructions: OptionalText, qcRequired: z.boolean().optional(), plannedMinutes: z.coerce.number().int().positive().max(100000).nullable().optional(), plannedUnits: z.coerce.number().int().positive().max(100000).nullable().optional(), sequence: z.coerce.number().int().positive().max(10000).optional(), status: z.enum(OperationStatuses).optional(), }) .strict(); export const ReorderOperationsSchema = z.object({ order: z.array(z.string().min(1)).min(1), });