162 lines
5.7 KiB
TypeScript
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>;
|