uup
This commit is contained in:
274
app/lib/form-validation.ts
Normal file
274
app/lib/form-validation.ts
Normal file
@@ -0,0 +1,274 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user