import * as z from 'zod'; function coerceOptionalNumber(message: string) { return z.preprocess((value) => { if (value === '' || value === null || value === undefined) { return undefined; } if (typeof value === 'number') { return value; } if (typeof value === 'string') { const trimmed = value.trim(); if (!trimmed) { return undefined; } return Number(trimmed); } return value; }, z.number().refine((value) => !Number.isNaN(value), message).optional()); } function coerceRequiredNumber(message: string) { return z.preprocess((value) => { if (typeof value === 'number') { return value; } if (typeof value === 'string') { return Number(value.trim()); } return value; }, z.number().refine((value) => !Number.isNaN(value), message)); } export const quotationSchema = z.object({ enquiryId: z.string().optional().nullable(), customerId: z.string().min(1, 'Please select a customer'), contactId: z.string().optional().nullable(), quotationDate: z.string().min(1, 'Quotation date is required'), validUntil: z.string().optional().nullable(), quotationType: z.string().min(1, 'Please select a quotation type'), projectName: z.string().optional(), projectLocation: z.string().optional(), attention: z.string().optional(), branchId: z.string().optional().nullable(), currency: z.string().min(1, 'Please select a currency'), exchangeRate: coerceOptionalNumber('Exchange rate must be a number'), status: z.string().min(1, 'Please select a status'), chancePercent: coerceOptionalNumber('Chance percent must be a number') .refine( (value) => value === undefined || (value >= 0 && value <= 100), 'Chance percent must be between 0 and 100' ), isHotProject: z.boolean().default(false), competitor: z.string().optional(), reference: z.string().optional(), notes: z.string().optional(), salesmanId: z.string().optional().nullable(), discount: coerceOptionalNumber('Discount must be a number'), discountType: z.string().optional().nullable(), taxRate: coerceOptionalNumber('Tax rate must be a number'), isActive: z.boolean().default(true), sentVia: z.string().optional().nullable(), revisionRemark: z.string().optional().nullable(), projectParties: z .array( z.object({ customerId: z.string().min(1, 'Please select a customer'), role: z.string().min(1, 'Please select a role'), isPrimary: z.boolean().default(false).optional(), remark: z.string().nullable().optional() }) ) .optional() }); export const quotationItemSchema = z.object({ productType: z.string().min(1, 'Please select a product type'), description: z.string().min(2, 'Description must be at least 2 characters'), quantity: coerceRequiredNumber('Quantity must be a number').refine( (value) => value > 0, 'Quantity must be greater than 0' ), unit: z.string().optional(), unitPrice: coerceRequiredNumber('Unit price must be a number').refine( (value) => value >= 0, 'Unit price must be 0 or greater' ), discount: coerceOptionalNumber('Discount must be a number').refine( (value) => value === undefined || value >= 0, 'Discount must be 0 or greater' ), discountType: z.string().nullable().optional(), taxRate: coerceOptionalNumber('Tax rate must be a number').refine( (value) => value === undefined || value >= 0, 'Tax rate must be 0 or greater' ), notes: z.string().optional(), sortOrder: coerceOptionalNumber('Sort order must be a number') }); export const quotationCustomerSchema = z.object({ customerId: z.string().min(1, 'Please select a customer'), role: z.string().min(1, 'Please select a role'), isPrimary: z.boolean().default(false), remark: z.string().nullable().optional() }); export const quotationTopicSchema = z.object({ id: z.string().optional(), topicType: z.string().min(1, 'Please select a topic type'), title: z.string().min(2, 'Title must be at least 2 characters'), sortOrder: coerceOptionalNumber('Sort order must be a number'), items: z .array( z.object({ id: z.string().optional(), content: z.string().min(1, 'Topic item content is required'), sortOrder: coerceOptionalNumber('Sort order must be a number') }) ) .min(1, 'At least one topic item is required') }); export const quotationFollowupSchema = z.object({ id: z.string().optional(), followupDate: z.string().min(1, 'Follow-up date is required'), followupType: z.string().min(1, 'Please select a follow-up type'), contactId: z.string().optional().nullable(), outcome: z.string().optional(), notes: z.string().optional(), nextFollowupDate: z.string().optional().nullable(), nextAction: z.string().optional() }); export const quotationAttachmentSchema = z.object({ id: z.string().optional(), fileName: z.string().min(1, 'Stored file name is required'), originalFileName: z.string().min(1, 'Original file name is required'), filePath: z.string().min(1, 'File path is required'), fileSize: coerceOptionalNumber('File size must be a number'), fileType: z.string().nullable().optional(), description: z.string().optional() }); export const quotationRevisionSchema = z.object({ revisionRemark: z.string().optional() }); export type QuotationFormValues = z.input; export type QuotationItemFormValues = z.input; export type QuotationCustomerFormValues = z.input; export type QuotationTopicFormValues = z.input; export type QuotationFollowupFormValues = z.input; export type QuotationAttachmentFormValues = z.input;