import { z } from 'zod'; // ============================================================================ // CORE ENTITY INTERFACES // ============================================================================ /** * Asset entity representing basic asset information */ export interface Asset { assetID: number; assetCode: string; assetType: string; brandModel: string; serialNumber: string; assetCondition: string; } /** * System Details entity representing technical specifications linked to assets */ export interface SystemDetails { systemDetailsID: number; assetID: number; systemModel: string; processor: string; installedRAM: string; graphics: string; screenResolution: string; diskModel: string; diskSize: string; manufacturer: string; } /** * Asset Assignment entity representing employee assignment tracking */ export interface AssetAssignment { assignmentID: number; assetID: number; employeeID: number; employeeName: string; assetCode: string; assignedDate: string; // ISO 8601 format returnedDate: string | null; // ISO 8601 format remarks: string; } /** * Asset Item entity representing individual components/items attached to an asset */ export interface AssetItem { itemID: number; assetID: number; itemType: string; itemName: string; quantity: number; specifications: string; serialNumber?: string; condition: string; notes?: string; addedDate: string; // ISO 8601 format } // ============================================================================ // API REQUEST/RESPONSE TYPES // ============================================================================ /** * Request type for creating a new asset (excludes auto-generated ID) */ export interface AssetCreateRequest { assetCode: string; assetType: string; brandModel: string; serialNumber: string; assetCondition: string; purchaseDate?: string; warrantyExpiry?: string; purchaseCost?: number; currentValue?: number; location?: string; notes?: string; } /** * Request type for updating an existing asset */ export interface AssetUpdateRequest { assetID: number; assetCode: string; assetType: string; brandModel: string; serialNumber: string; assetCondition: string; } /** * Request type for creating new system details (excludes auto-generated ID) */ export interface SystemDetailsCreateRequest { assetID: number; systemModel: string; processor: string; installedRAM: string; graphics: string; screenResolution: string; diskModel: string; diskSize: string; manufacturer: string; } /** * Request type for updating existing system details */ export interface SystemDetailsUpdateRequest { systemDetailsID: number; assetID: number; systemModel: string; processor: string; installedRAM: string; graphics: string; screenResolution: string; diskModel: string; diskSize: string; manufacturer: string; } /** * Request type for creating a new asset assignment (excludes auto-generated ID) */ export interface AssetAssignmentCreateRequest { assetID: number; employeeID: number; employeeName: string; assetCode: string; assignedDate: string; // ISO 8601 format returnedDate: string | null; // ISO 8601 format remarks: string; } /** * Request type for updating an existing asset assignment */ export interface AssetAssignmentUpdateRequest { assignmentID: number; assetID: number; employeeID: number; employeeName: string; assetCode: string; assignedDate: string; // ISO 8601 format returnedDate: string | null; // ISO 8601 format remarks: string; } /** * Request type for creating a new asset item (excludes auto-generated ID) */ export interface AssetItemCreateRequest { assetID: number; itemType: string; itemName: string; quantity: number; specifications: string; serialNumber?: string; condition: string; notes?: string; addedDate: string; // ISO 8601 format } /** * Request type for updating an existing asset item */ export interface AssetItemUpdateRequest { itemID: number; assetID: number; itemType: string; itemName: string; quantity: number; specifications: string; serialNumber?: string; condition: string; notes?: string; addedDate: string; // ISO 8601 format } // ============================================================================ // ENUM TYPES AND CONSTANTS // ============================================================================ /** * Valid asset condition values */ export type AssetCondition = 'Excellent' | 'Good' | 'Fair' | 'Poor'; /** * Common asset types */ export type AssetType = | 'Laptop' | 'Desktop' | 'Monitor' | 'Keyboard' | 'Mouse' | 'Wired Mouse' | 'Wireless Mouse' | 'Laptop Stand' | 'Extended Screen' | 'Printer' | 'Scanner' | 'Tablet' | 'Phone' | 'Other'; /** * Asset condition options for dropdowns */ export const ASSET_CONDITIONS: AssetCondition[] = ['Excellent', 'Good', 'Fair', 'Poor']; /** * Asset type options for dropdowns */ export const ASSET_TYPES: AssetType[] = [ 'Laptop', 'Desktop', 'Monitor', 'Keyboard', 'Mouse', 'Wired Mouse', 'Wireless Mouse', 'Laptop Stand', 'Extended Screen', 'Printer', 'Scanner', 'Tablet', 'Phone', 'Other' ]; /** * Asset item types */ export type AssetItemType = | 'RAM' | 'Storage' | 'Battery' | 'Charger' | 'Cable' | 'Adapter' | 'Keyboard' | 'Mouse' | 'Webcam' | 'Headset' | 'Docking Station' | 'Monitor' | 'Other'; /** * Asset item type options for dropdowns */ export const ASSET_ITEM_TYPES: AssetItemType[] = [ 'RAM', 'Storage', 'Battery', 'Charger', 'Cable', 'Adapter', 'Keyboard', 'Mouse', 'Webcam', 'Headset', 'Docking Station', 'Monitor', 'Other' ]; // ============================================================================ // ZOD VALIDATION SCHEMAS // ============================================================================ /** * Zod schema for Asset validation */ export const AssetSchema = z.object({ assetID: z.number().int().positive().optional(), assetCode: z.string() .min(1, 'Asset code is required') .max(20, 'Asset code must be 20 characters or less') .refine(val => val.trim().length > 0, { message: 'Asset code cannot be empty or contain only spaces' }) .refine(val => /^[A-Za-z0-9\-_]+$/.test(val.trim()), { message: 'Asset code can only contain letters, numbers, hyphens, and underscores' }), assetType: z.string() .min(1, 'Asset type is required') .max(50, 'Asset type must be 50 characters or less') .refine(val => ASSET_TYPES.includes(val as AssetType), { message: 'Please select a valid asset type from the list' }), brandModel: z.string() .min(1, 'Brand model is required') .max(100, 'Brand model must be 100 characters or less') .refine(val => val.trim().length > 0, { message: 'Brand model cannot be empty or contain only spaces' }), serialNumber: z.string() .min(1, 'Serial number is required') .max(50, 'Serial number must be 50 characters or less') .refine(val => val.trim().length > 0, { message: 'Serial number cannot be empty or contain only spaces' }) .refine(val => /^[A-Za-z0-9\-_]+$/.test(val.trim()), { message: 'Serial number can only contain letters, numbers, hyphens, and underscores' }), assetCondition: z.enum(['Excellent', 'Good', 'Fair', 'Poor'], { errorMap: () => ({ message: 'Asset condition must be Excellent, Good, Fair, or Poor' }) }), }); /** * Zod schema for Asset creation (excludes ID) */ export const AssetCreateSchema = AssetSchema.omit({ assetID: true }); /** * Zod schema for Asset updates (requires ID and assetCode) */ export const AssetUpdateSchema = AssetSchema.required({ assetID: true, assetCode: true }); /** * Zod schema for System Details validation */ export const SystemDetailsSchema = z.object({ systemDetailsID: z.number().int().positive().optional(), assetID: z.number().int().positive('Asset ID must be a positive number'), systemModel: z.string() .min(1, 'System model is required') .max(100, 'System model must be 100 characters or less') .refine(val => val.trim().length > 0, { message: 'System model cannot be empty or contain only spaces' }), processor: z.string() .min(1, 'Processor information is required') .max(100, 'Processor must be 100 characters or less') .refine(val => val.trim().length > 0, { message: 'Processor information cannot be empty or contain only spaces' }), installedRAM: z.string() .min(1, 'RAM information is required') .max(50, 'RAM must be 50 characters or less') .refine(val => val.trim().length > 0, { message: 'RAM information cannot be empty or contain only spaces' }), graphics: z.string() .min(1, 'Graphics information is required') .max(100, 'Graphics must be 100 characters or less') .refine(val => val.trim().length > 0, { message: 'Graphics information cannot be empty or contain only spaces' }), screenResolution: z.string() .min(1, 'Screen resolution is required') .max(50, 'Screen resolution must be 50 characters or less') .refine(val => val.trim().length > 0, { message: 'Screen resolution cannot be empty or contain only spaces' }) .refine(val => /^\d+x\d+$/.test(val.trim()) || val === 'custom', { message: 'Screen resolution must be in format "1920x1080" or select from the dropdown' }), diskModel: z.string() .min(1, 'Disk model is required') .max(100, 'Disk model must be 100 characters or less') .refine(val => val.trim().length > 0, { message: 'Disk model cannot be empty or contain only spaces' }), diskSize: z.string() .min(1, 'Disk size is required') .max(50, 'Disk size must be 50 characters or less') .refine(val => val.trim().length > 0, { message: 'Disk size cannot be empty or contain only spaces' }), manufacturer: z.string() .min(1, 'Manufacturer is required') .max(100, 'Manufacturer must be 100 characters or less') .refine(val => val.trim().length > 0, { message: 'Manufacturer cannot be empty or contain only spaces' }), }); /** * Zod schema for System Details creation (excludes ID) */ export const SystemDetailsCreateSchema = SystemDetailsSchema.omit({ systemDetailsID: true }); /** * Zod schema for System Details updates (requires ID) */ export const SystemDetailsUpdateSchema = SystemDetailsSchema.required({ systemDetailsID: true }); /** * Base Zod schema for Asset Assignment validation (without refinements) */ const AssetAssignmentBaseSchema = z.object({ assignmentID: z.number().int().positive().optional(), assetID: z.number().int().positive('Asset ID must be a positive number'), employeeID: z.number().int().positive('Employee ID must be a positive number'), employeeName: z.string() .min(1, 'Employee name is required') .max(100, 'Employee name must be 100 characters or less') .refine(val => val.trim().length > 0, { message: 'Employee name cannot be empty or contain only spaces' }), assetCode: z.string() .min(1, 'Asset code is required') .max(20, 'Asset code must be 20 characters or less') .refine(val => val.trim().length > 0, { message: 'Asset code cannot be empty or contain only spaces' }), assignedDate: z.string().datetime('Assigned date must be in ISO 8601 format'), returnedDate: z.string().datetime('Returned date must be in ISO 8601 format').nullable(), remarks: z.string() .min(1, 'Remarks are required') .max(500, 'Remarks must be 500 characters or less') .refine(val => val.trim().length > 0, { message: 'Remarks cannot be empty or contain only spaces' }), }); /** * Zod schema for Asset Assignment validation (with date validation refinement) */ export const AssetAssignmentSchema = AssetAssignmentBaseSchema.refine(data => { // If both dates are provided, returned date must be after assigned date if (data.returnedDate && data.assignedDate) { const assignedDate = new Date(data.assignedDate); const returnedDate = new Date(data.returnedDate); return returnedDate > assignedDate; } return true; }, { message: 'Returned date must be after the assigned date', path: ['returnedDate'] }); /** * Zod schema for Asset Assignment creation (excludes ID) */ export const AssetAssignmentCreateSchema = AssetAssignmentBaseSchema.omit({ assignmentID: true }).refine(data => { // If both dates are provided, returned date must be after assigned date if (data.returnedDate && data.assignedDate) { const assignedDate = new Date(data.assignedDate); const returnedDate = new Date(data.returnedDate); return returnedDate > assignedDate; } return true; }, { message: 'Returned date must be after the assigned date', path: ['returnedDate'] }); /** * Zod schema for Asset Assignment updates (requires ID) */ export const AssetAssignmentUpdateSchema = AssetAssignmentBaseSchema.required({ assignmentID: true }).refine(data => { // If both dates are provided, returned date must be after assigned date if (data.returnedDate && data.assignedDate) { const assignedDate = new Date(data.assignedDate); const returnedDate = new Date(data.returnedDate); return returnedDate > assignedDate; } return true; }, { message: 'Returned date must be after the assigned date', path: ['returnedDate'] }); /** * Zod schema for Asset Assignment form editing (excludes ID for form validation) * Uses Date objects instead of strings for form compatibility */ export const AssetAssignmentFormSchema = z.object({ assetID: z.number().int().positive('Asset ID must be a positive number'), employeeID: z.number().int().positive('Employee ID must be a positive number'), employeeName: z.string() .min(1, 'Employee name is required') .max(100, 'Employee name must be 100 characters or less') .refine(val => val.trim().length > 0, { message: 'Employee name cannot be empty or contain only spaces' }), assetCode: z.string() .min(1, 'Asset code is required') .max(20, 'Asset code must be 20 characters or less') .refine(val => val.trim().length > 0, { message: 'Asset code cannot be empty or contain only spaces' }), assignedDate: z.date({ required_error: 'Assigned date is required', invalid_type_error: 'Assigned date must be a valid date' }), returnedDate: z.date({ invalid_type_error: 'Returned date must be a valid date' }).nullable(), remarks: z.string() .min(1, 'Remarks are required') .max(500, 'Remarks must be 500 characters or less') .refine(val => val.trim().length > 0, { message: 'Remarks cannot be empty or contain only spaces' }), }).refine(data => { // If both dates are provided, returned date must be after assigned date if (data.returnedDate && data.assignedDate) { return data.returnedDate > data.assignedDate; } return true; }, { message: 'Returned date must be after the assigned date', path: ['returnedDate'] }); /** * Zod schema for Asset Item validation */ export const AssetItemSchema = z.object({ itemID: z.number().int().positive().optional(), assetID: z.number().int().positive('Asset ID must be a positive number'), itemType: z.string() .min(1, 'Item type is required') .max(50, 'Item type must be 50 characters or less'), itemName: z.string() .min(1, 'Item name is required') .max(100, 'Item name must be 100 characters or less') .refine(val => val.trim().length > 0, { message: 'Item name cannot be empty or contain only spaces' }), quantity: z.number().int().positive('Quantity must be a positive number').min(1, 'Quantity must be at least 1'), specifications: z.string() .min(1, 'Specifications are required') .max(200, 'Specifications must be 200 characters or less') .refine(val => val.trim().length > 0, { message: 'Specifications cannot be empty or contain only spaces' }), serialNumber: z.string() .max(50, 'Serial number must be 50 characters or less') .optional(), condition: z.enum(['Excellent', 'Good', 'Fair', 'Poor'], { errorMap: () => ({ message: 'Condition must be Excellent, Good, Fair, or Poor' }) }), notes: z.string() .max(500, 'Notes must be 500 characters or less') .optional(), addedDate: z.string().datetime('Added date must be in ISO 8601 format'), }); /** * Zod schema for Asset Item creation (excludes ID) */ export const AssetItemCreateSchema = AssetItemSchema.omit({ itemID: true }); /** * Zod schema for Asset Item updates (requires ID) */ export const AssetItemUpdateSchema = AssetItemSchema.required({ itemID: true }); // ============================================================================ // UTILITY TYPES AND INTERFACES // ============================================================================ /** * Generic API response wrapper */ export interface AssetManagementApiResponse { success: boolean; data?: T; message?: string; error?: string; } /** * Extended Asset interface with related data for detailed views */ export interface AssetWithDetails extends Asset { systemDetails?: SystemDetails; assignments?: AssetAssignment[]; currentAssignment?: AssetAssignment; items?: AssetItem[]; } /** * Extended System Details interface with asset information */ export interface SystemDetailsWithAsset extends SystemDetails { asset?: Asset; } /** * Extended Asset Assignment interface with related entity information */ export interface AssetAssignmentWithDetails extends AssetAssignment { asset?: Asset; employeeEmail?: string; } /** * Filter options for asset listing */ export interface AssetFilter { assetType?: string; assetCondition?: AssetCondition; searchTerm?: string; assignmentStatus?: 'assigned' | 'unassigned' | 'all'; } /** * Sort options for asset listing */ export interface AssetSort { field: keyof Asset; direction: 'asc' | 'desc'; } /** * Pagination options */ export interface PaginationOptions { page: number; limit: number; } /** * Paginated response wrapper */ export interface PaginatedResponse { data: T[]; total: number; page: number; limit: number; totalPages: number; } /** * Bulk import response interface */ export interface BulkImportResponse { success: boolean; message: string; results: { successful: number; failed: number; total: number; errors: ImportError[]; createdAssets: Asset[]; }; } /** * Import error interface for bulk operations */ export interface ImportError { index: number; asset: AssetCreateRequest; error: string; field?: string; } // ============================================================================ // TYPE GUARDS AND VALIDATION HELPERS // ============================================================================ /** * Type guard to check if an object is a valid Asset */ export function isAsset(obj: any): obj is Asset { return AssetSchema.safeParse(obj).success; } /** * Type guard to check if an object is a valid SystemDetails */ export function isSystemDetails(obj: any): obj is SystemDetails { return SystemDetailsSchema.safeParse(obj).success; } /** * Type guard to check if an object is a valid AssetAssignment */ export function isAssetAssignment(obj: any): obj is AssetAssignment { return AssetAssignmentSchema.safeParse(obj).success; } /** * Validates and transforms API response data to ensure type safety */ export function validateAssetApiResponse(data: any): Asset { // For API responses, we expect the ID to be present const schemaWithRequiredId = AssetSchema.required({ assetID: true }); const result = schemaWithRequiredId.safeParse(data); if (!result.success) { console.error('Asset validation failed:', result.error); throw new Error(`Invalid asset data: ${result.error.message}`); } return result.data as Asset; } /** * Validates and transforms System Details API response data */ export function validateSystemDetailsApiResponse(data: any): SystemDetails { // For API responses, we expect the ID to be present const schemaWithRequiredId = SystemDetailsSchema.required({ systemDetailsID: true }); const result = schemaWithRequiredId.safeParse(data); if (!result.success) { console.error('System Details validation failed:', result.error); throw new Error(`Invalid system details data: ${result.error.message}`); } return result.data as SystemDetails; } /** * Validates and transforms Asset Assignment API response data */ export function validateAssetAssignmentApiResponse(data: any): AssetAssignment { // For API responses, we expect the ID to be present const result = AssetAssignmentSchema.safeParse(data); if (!result.success) { console.error('Asset Assignment validation failed:', result.error); throw new Error(`Invalid asset assignment data: ${result.error.message}`); } return result.data as AssetAssignment; } // ============================================================================ // FORM VALIDATION HELPERS // ============================================================================ /** * Helper to get validation errors for Asset forms */ export function getAssetValidationErrors(data: any): Record { const result = AssetSchema.safeParse(data); if (result.success) return {}; const errors: Record = {}; result.error.errors.forEach(error => { const field = error.path.join('.'); errors[field] = error.message; }); return errors; } /** * Helper to get validation errors for System Details forms */ export function getSystemDetailsValidationErrors(data: any): Record { const result = SystemDetailsSchema.safeParse(data); if (result.success) return {}; const errors: Record = {}; result.error.errors.forEach(error => { const field = error.path.join('.'); errors[field] = error.message; }); return errors; } /** * Helper to get validation errors for Asset Assignment forms */ export function getAssetAssignmentValidationErrors(data: any): Record { const result = AssetAssignmentSchema.safeParse(data); if (result.success) return {}; const errors: Record = {}; result.error.errors.forEach(error => { const field = error.path.join('.'); errors[field] = error.message; }); return errors; }