car_mms/app/lib/form-validation.ts
2025-09-11 14:22:27 +03:00

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;
}
}