import { z } from "zod" import { JobCardStatus, EstimateTo, TaxInclusive, DiscountType, DiscountAt, FuelLevel, } from "@garage/api" const relationFieldSchema = z .object({ value: z.string(), label: z.string() }) .nullable() const optionalStringMax255 = z.string().max(255).optional() const optionalTimeString = z.string().max(50).optional() const documentSchema = z.object({ document_type_id: z.coerce.number().int().optional(), customer_id: z.coerce.number().int().optional(), vehicle_id: z.coerce.number().int().optional(), document_number: z.string().optional(), show_in_invoice: z.boolean().optional(), show_in_estimate: z.boolean().optional(), show_in_statement: z.boolean().optional(), }) // ── Job Card Statuses ── export const JOB_CARD_STATUS_OPTIONS = JobCardStatus.map((v) => ({ value: v, label: v .replace(/_/g, " ") .replace(/\b\w/g, (c) => c.toUpperCase()), })) const ESTIMATE_TO_OPTIONS = EstimateTo.map((v) => ({ value: v, label: v })) const TAX_INCLUSIVE_OPTIONS = TaxInclusive.map((v) => ({ value: v, label: v })) const DISCOUNT_TYPE_OPTIONS = DiscountType.map((v) => ({ value: v, label: v .replace(/_/g, " ") .replace(/\b\w/g, (c) => c.toUpperCase()), })) const DISCOUNT_AT_OPTIONS = DiscountAt.map((v) => ({ value: v, label: v .replace(/_/g, " ") .replace(/\b\w/g, (c) => c.toUpperCase()), })) const FUEL_LEVEL_OPTIONS = FuelLevel.map((v) => ({ value: v, label: v .replace(/_/g, " ") .replace(/\b\w/g, (c) => c.toUpperCase()), })) const jobCardFormSchema = z.object({ // ── Required fields ── title: z.string().min(1, "Title is required").max(255, "Title must be at most 255 characters"), // ── Relations ── customer: relationFieldSchema, vehicle: relationFieldSchema, estimate: relationFieldSchema.optional(), department: relationFieldSchema, service_writer: relationFieldSchema, primary_technician: relationFieldSchema, sales_person: relationFieldSchema, insurance_type: relationFieldSchema, insurer: relationFieldSchema, tax: relationFieldSchema, // ── Numbers & identifiers ── order_number: optionalStringMax255, estimate_number: optionalStringMax255, // ── Status & settings ── status: z.enum(JobCardStatus).optional(), estimate_to: z.enum(EstimateTo).optional(), tax_inclusive: z.enum(TaxInclusive).optional(), discount_type: z.enum(DiscountType).optional(), discount_amount: z.coerce.number().min(0).optional(), discount_at: z.enum(DiscountAt).optional(), // ── Dates & times ── order_date: z.string().optional(), check_in_date: z.string().optional(), check_in_time: optionalTimeString, start_date: z.string().optional(), start_time: optionalTimeString, delivery_date: z.string().optional(), delivery_time: optionalTimeString, // ── Vehicle state ── km_in: z.union([z.string(), z.number()]).optional(), fuel_level: optionalTimeString, // ── Boolean options ── has_insurance: z.boolean().optional(), enable_parts_issuing: z.boolean().optional(), enable_digital_authorisation: z.boolean().optional(), // ── Notes ── footer: z.string().optional(), attachments: z.string().optional(), // ── Customer Remarks ── customer_remarks: z.array(z.string()).optional(), documents: z.array(documentSchema).optional(), attachment_files: z.array(z.unknown()).max(20, "Attachment files cannot exceed 20 files").optional(), // ── Labels ── labels: z .array( z.object({ id: z.number(), title: z.string(), color_code: z.string(), }), ) .optional(), label_ids: z.array(z.coerce.number().int()).optional(), }).superRefine((values, ctx) => { if (!values.customer) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["customer"], message: "Customer is required", }) } if (!values.vehicle) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["vehicle"], message: "Vehicle is required", }) } }) type JobCardFormValues = z.infer export { jobCardFormSchema, relationFieldSchema, TAX_INCLUSIVE_OPTIONS, DISCOUNT_TYPE_OPTIONS, DISCOUNT_AT_OPTIONS, ESTIMATE_TO_OPTIONS, FUEL_LEVEL_OPTIONS, } // Backward-compat alias used by job-card-status-stepper export const JOB_CARD_STATUSES = JOB_CARD_STATUS_OPTIONS export type { JobCardFormValues } export type { JobCardStatus } from "@garage/api"