Files
alla-allaos-fullstack/src/features/crm/quotations/schemas/quotation.schema.ts
phaichayon 5be6c54272 taks-d.2.1
2026-06-17 14:46:52 +07:00

162 lines
5.7 KiB
TypeScript

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<typeof quotationSchema>;
export type QuotationItemFormValues = z.input<typeof quotationItemSchema>;
export type QuotationCustomerFormValues = z.input<typeof quotationCustomerSchema>;
export type QuotationTopicFormValues = z.input<typeof quotationTopicSchema>;
export type QuotationFollowupFormValues = z.input<typeof quotationFollowupSchema>;
export type QuotationAttachmentFormValues = z.input<typeof quotationAttachmentSchema>;