This commit is contained in:
phaichayon
2026-06-16 13:42:38 +07:00
parent 56683ee7b9
commit edee45375e
29 changed files with 4240 additions and 51 deletions

View File

@@ -1,5 +1,43 @@
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'),
@@ -12,14 +50,11 @@ export const quotationSchema = z.object({
attention: z.string().optional(),
branchId: z.string().optional().nullable(),
currency: z.string().min(1, 'Please select a currency'),
exchangeRate: z.number().nullable().optional(),
exchangeRate: coerceOptionalNumber('Exchange rate must be a number'),
status: z.string().min(1, 'Please select a status'),
chancePercent: z
.number()
.nullable()
.optional()
chancePercent: coerceOptionalNumber('Chance percent must be a number')
.refine(
(value) => value === null || value === undefined || (value >= 0 && value <= 100),
(value) => value === undefined || (value >= 0 && value <= 100),
'Chance percent must be between 0 and 100'
),
isHotProject: z.boolean().default(false),
@@ -27,9 +62,9 @@ export const quotationSchema = z.object({
reference: z.string().optional(),
notes: z.string().optional(),
salesmanId: z.string().optional().nullable(),
discount: z.number().nullable().optional(),
discount: coerceOptionalNumber('Discount must be a number'),
discountType: z.string().optional().nullable(),
taxRate: z.number().nullable().optional(),
taxRate: coerceOptionalNumber('Tax rate must be a number'),
isActive: z.boolean().default(true),
sentVia: z.string().optional().nullable(),
revisionRemark: z.string().optional().nullable()
@@ -38,14 +73,26 @@ export const quotationSchema = z.object({
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: z.number().positive('Quantity must be greater than 0'),
quantity: coerceRequiredNumber('Quantity must be a number').refine(
(value) => value > 0,
'Quantity must be greater than 0'
),
unit: z.string().optional(),
unitPrice: z.number().min(0, 'Unit price must be 0 or greater'),
discount: z.number().min(0).nullable().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: z.number().min(0).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: z.number().nullable().optional()
sortOrder: coerceOptionalNumber('Sort order must be a number')
});
export const quotationCustomerSchema = z.object({
@@ -58,13 +105,13 @@ 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: z.number().nullable().optional(),
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: z.number().nullable().optional()
sortOrder: coerceOptionalNumber('Sort order must be a number')
})
)
.min(1, 'At least one topic item is required')
@@ -86,7 +133,7 @@ export const quotationAttachmentSchema = z.object({
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: z.number().nullable().optional(),
fileSize: coerceOptionalNumber('File size must be a number'),
fileType: z.string().nullable().optional(),
description: z.string().optional()
});