274 lines
10 KiB
TypeScript
274 lines
10 KiB
TypeScript
import { z } from 'zod';
|
|
import { VALIDATION, AUTH_LEVELS, USER_STATUS, TRANSMISSION_TYPES, FUEL_TYPES, USE_TYPES, PAYMENT_STATUS } from './constants';
|
|
|
|
// Zod schemas for server-side validation
|
|
export const userSchema = z.object({
|
|
name: z.string()
|
|
.min(1, 'الاسم مطلوب')
|
|
.max(VALIDATION.MAX_NAME_LENGTH, `الاسم يجب أن يكون أقل من ${VALIDATION.MAX_NAME_LENGTH} حرف`)
|
|
.trim(),
|
|
username: z.string()
|
|
.min(3, 'اسم المستخدم يجب أن يكون على الأقل 3 أحرف')
|
|
.regex(/^[a-zA-Z0-9_]+$/, 'اسم المستخدم يجب أن يحتوي على أحرف وأرقام فقط')
|
|
.trim(),
|
|
email: z.string()
|
|
.email('البريد الإلكتروني غير صحيح')
|
|
.trim(),
|
|
password: z.string()
|
|
.min(VALIDATION.MIN_PASSWORD_LENGTH, `كلمة المرور يجب أن تكون على الأقل ${VALIDATION.MIN_PASSWORD_LENGTH} أحرف`),
|
|
authLevel: z.number()
|
|
.refine(val => Object.values(AUTH_LEVELS).includes(val as any), 'مستوى الصلاحية غير صحيح'),
|
|
status: z.enum([USER_STATUS.ACTIVE, USER_STATUS.INACTIVE], {
|
|
errorMap: () => ({ message: 'حالة المستخدم غير صحيحة' })
|
|
}),
|
|
});
|
|
|
|
export const customerSchema = z.object({
|
|
name: z.string()
|
|
.min(1, 'اسم العميل مطلوب')
|
|
.max(VALIDATION.MAX_NAME_LENGTH, `الاسم يجب أن يكون أقل من ${VALIDATION.MAX_NAME_LENGTH} حرف`)
|
|
.trim(),
|
|
phone: z.string()
|
|
.regex(/^[\+]?[0-9\s\-\(\)]*$/, 'رقم الهاتف غير صحيح')
|
|
.optional()
|
|
.or(z.literal('')),
|
|
email: z.string()
|
|
.email('البريد الإلكتروني غير صحيح')
|
|
.optional()
|
|
.or(z.literal('')),
|
|
address: z.string()
|
|
.optional()
|
|
.or(z.literal('')),
|
|
});
|
|
|
|
export const vehicleSchema = z.object({
|
|
plateNumber: z.string()
|
|
.min(1, 'رقم اللوحة مطلوب')
|
|
.trim(),
|
|
bodyType: z.string()
|
|
.min(1, 'نوع الهيكل مطلوب')
|
|
.trim(),
|
|
manufacturer: z.string()
|
|
.min(1, 'الشركة المصنعة مطلوبة')
|
|
.trim(),
|
|
model: z.string()
|
|
.min(1, 'الموديل مطلوب')
|
|
.trim(),
|
|
trim: z.string()
|
|
.optional()
|
|
.or(z.literal('')),
|
|
year: z.number()
|
|
.min(VALIDATION.MIN_YEAR, `السنة يجب أن تكون بين ${VALIDATION.MIN_YEAR} و ${VALIDATION.MAX_YEAR}`)
|
|
.max(VALIDATION.MAX_YEAR, `السنة يجب أن تكون بين ${VALIDATION.MIN_YEAR} و ${VALIDATION.MAX_YEAR}`),
|
|
transmission: z.enum(TRANSMISSION_TYPES.map(t => t.value) as [string, ...string[]], {
|
|
errorMap: () => ({ message: 'نوع ناقل الحركة غير صحيح' })
|
|
}),
|
|
fuel: z.enum(FUEL_TYPES.map(f => f.value) as [string, ...string[]], {
|
|
errorMap: () => ({ message: 'نوع الوقود غير صحيح' })
|
|
}),
|
|
cylinders: z.number()
|
|
.min(1, `عدد الأسطوانات يجب أن يكون بين 1 و ${VALIDATION.MAX_CYLINDERS}`)
|
|
.max(VALIDATION.MAX_CYLINDERS, `عدد الأسطوانات يجب أن يكون بين 1 و ${VALIDATION.MAX_CYLINDERS}`)
|
|
.optional()
|
|
.nullable(),
|
|
engineDisplacement: z.number()
|
|
.min(0.1, `سعة المحرك يجب أن تكون بين 0.1 و ${VALIDATION.MAX_ENGINE_DISPLACEMENT}`)
|
|
.max(VALIDATION.MAX_ENGINE_DISPLACEMENT, `سعة المحرك يجب أن تكون بين 0.1 و ${VALIDATION.MAX_ENGINE_DISPLACEMENT}`)
|
|
.optional()
|
|
.nullable(),
|
|
useType: z.enum(USE_TYPES.map(u => u.value) as [string, ...string[]], {
|
|
errorMap: () => ({ message: 'نوع الاستخدام غير صحيح' })
|
|
}),
|
|
ownerId: z.number()
|
|
.min(1, 'مالك المركبة مطلوب'),
|
|
});
|
|
|
|
export const maintenanceVisitSchema = z.object({
|
|
vehicleId: z.number()
|
|
.min(1, 'المركبة مطلوبة'),
|
|
customerId: z.number()
|
|
.min(1, 'العميل مطلوب'),
|
|
maintenanceType: z.string()
|
|
.min(1, 'نوع الصيانة مطلوب')
|
|
.trim(),
|
|
description: z.string()
|
|
.min(1, 'وصف الصيانة مطلوب')
|
|
.max(VALIDATION.MAX_DESCRIPTION_LENGTH, `الوصف يجب أن يكون أقل من ${VALIDATION.MAX_DESCRIPTION_LENGTH} حرف`)
|
|
.trim(),
|
|
cost: z.number()
|
|
.min(VALIDATION.MIN_COST, `التكلفة يجب أن تكون بين ${VALIDATION.MIN_COST} و ${VALIDATION.MAX_COST}`)
|
|
.max(VALIDATION.MAX_COST, `التكلفة يجب أن تكون بين ${VALIDATION.MIN_COST} و ${VALIDATION.MAX_COST}`),
|
|
paymentStatus: z.enum(Object.values(PAYMENT_STATUS) as [string, ...string[]], {
|
|
errorMap: () => ({ message: 'حالة الدفع غير صحيحة' })
|
|
}),
|
|
kilometers: z.number()
|
|
.min(0, 'عدد الكيلومترات يجب أن يكون رقم موجب'),
|
|
nextVisitDelay: z.number()
|
|
.refine(val => [1, 2, 3, 4].includes(val), 'فترة الزيارة التالية يجب أن تكون 1، 2، 3، أو 4 أشهر'),
|
|
});
|
|
|
|
export const expenseSchema = z.object({
|
|
description: z.string()
|
|
.min(1, 'وصف المصروف مطلوب')
|
|
.max(VALIDATION.MAX_DESCRIPTION_LENGTH, `الوصف يجب أن يكون أقل من ${VALIDATION.MAX_DESCRIPTION_LENGTH} حرف`)
|
|
.trim(),
|
|
category: z.string()
|
|
.min(1, 'فئة المصروف مطلوبة')
|
|
.trim(),
|
|
amount: z.number()
|
|
.min(VALIDATION.MIN_COST + 0.01, `المبلغ يجب أن يكون بين ${VALIDATION.MIN_COST + 0.01} و ${VALIDATION.MAX_COST}`)
|
|
.max(VALIDATION.MAX_COST, `المبلغ يجب أن يكون بين ${VALIDATION.MIN_COST + 0.01} و ${VALIDATION.MAX_COST}`),
|
|
});
|
|
|
|
// Validation result type
|
|
export interface ValidationResult {
|
|
success: boolean;
|
|
errors: Record<string, string>;
|
|
data?: any;
|
|
}
|
|
|
|
// Server-side validation functions
|
|
export function validateUserData(data: any): ValidationResult {
|
|
try {
|
|
const validatedData = userSchema.parse(data);
|
|
return { success: true, errors: {}, data: validatedData };
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
const errors: Record<string, string> = {};
|
|
error.issues.forEach((issue) => {
|
|
if (issue.path.length > 0) {
|
|
errors[issue.path[0] as string] = issue.message;
|
|
}
|
|
});
|
|
return { success: false, errors };
|
|
}
|
|
return { success: false, errors: { general: 'خطأ في التحقق من البيانات' } };
|
|
}
|
|
}
|
|
|
|
export function validateCustomerData(data: any): ValidationResult {
|
|
try {
|
|
const validatedData = customerSchema.parse(data);
|
|
return { success: true, errors: {}, data: validatedData };
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
const errors: Record<string, string> = {};
|
|
error.issues.forEach((issue) => {
|
|
if (issue.path.length > 0) {
|
|
errors[issue.path[0] as string] = issue.message;
|
|
}
|
|
});
|
|
return { success: false, errors };
|
|
}
|
|
return { success: false, errors: { general: 'خطأ في التحقق من البيانات' } };
|
|
}
|
|
}
|
|
|
|
export function validateVehicleData(data: any): ValidationResult {
|
|
try {
|
|
// Convert string numbers to actual numbers
|
|
const processedData = {
|
|
...data,
|
|
year: data.year ? parseInt(data.year) : undefined,
|
|
cylinders: data.cylinders ? parseInt(data.cylinders) : null,
|
|
engineDisplacement: data.engineDisplacement ? parseFloat(data.engineDisplacement) : null,
|
|
ownerId: data.ownerId ? parseInt(data.ownerId) : undefined,
|
|
};
|
|
|
|
const validatedData = vehicleSchema.parse(processedData);
|
|
return { success: true, errors: {}, data: validatedData };
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
const errors: Record<string, string> = {};
|
|
error.issues.forEach((issue) => {
|
|
if (issue.path.length > 0) {
|
|
errors[issue.path[0] as string] = issue.message;
|
|
}
|
|
});
|
|
return { success: false, errors };
|
|
}
|
|
return { success: false, errors: { general: 'خطأ في التحقق من البيانات' } };
|
|
}
|
|
}
|
|
|
|
export function validateMaintenanceVisitData(data: any): ValidationResult {
|
|
try {
|
|
// Convert string numbers to actual numbers
|
|
const processedData = {
|
|
...data,
|
|
vehicleId: data.vehicleId ? parseInt(data.vehicleId) : undefined,
|
|
customerId: data.customerId ? parseInt(data.customerId) : undefined,
|
|
cost: data.cost ? parseFloat(data.cost) : undefined,
|
|
kilometers: data.kilometers ? parseInt(data.kilometers) : undefined,
|
|
nextVisitDelay: data.nextVisitDelay ? parseInt(data.nextVisitDelay) : undefined,
|
|
};
|
|
|
|
const validatedData = maintenanceVisitSchema.parse(processedData);
|
|
return { success: true, errors: {}, data: validatedData };
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
const errors: Record<string, string> = {};
|
|
error.issues.forEach((issue) => {
|
|
if (issue.path.length > 0) {
|
|
errors[issue.path[0] as string] = issue.message;
|
|
}
|
|
});
|
|
return { success: false, errors };
|
|
}
|
|
return { success: false, errors: { general: 'خطأ في التحقق من البيانات' } };
|
|
}
|
|
}
|
|
|
|
export function validateExpenseData(data: any): ValidationResult {
|
|
try {
|
|
// Convert string numbers to actual numbers
|
|
const processedData = {
|
|
...data,
|
|
amount: data.amount ? parseFloat(data.amount) : undefined,
|
|
};
|
|
|
|
const validatedData = expenseSchema.parse(processedData);
|
|
return { success: true, errors: {}, data: validatedData };
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
const errors: Record<string, string> = {};
|
|
error.issues.forEach((issue) => {
|
|
if (issue.path.length > 0) {
|
|
errors[issue.path[0] as string] = issue.message;
|
|
}
|
|
});
|
|
return { success: false, errors };
|
|
}
|
|
return { success: false, errors: { general: 'خطأ في التحقق من البيانات' } };
|
|
}
|
|
}
|
|
|
|
// Client-side validation helpers
|
|
export function validateField(value: any, schema: z.ZodSchema, fieldName: string): string | null {
|
|
try {
|
|
schema.parse({ [fieldName]: value });
|
|
return null;
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
const fieldError = error.errors.find(err => err.path.includes(fieldName));
|
|
return fieldError?.message || null;
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Real-time validation for forms
|
|
export function validateFormField(fieldName: string, value: any, schema: z.ZodSchema): string | null {
|
|
try {
|
|
const fieldSchema = (schema as any).shape[fieldName];
|
|
if (fieldSchema) {
|
|
fieldSchema.parse(value);
|
|
}
|
|
return null;
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
return error.issues[0]?.message || null;
|
|
}
|
|
return null;
|
|
}
|
|
} |