import { z } from 'zod'; import { ASSET_TYPES, AssetType } from './asset-management'; // ============================================================================ // CORE ENTITY INTERFACES // ============================================================================ /** * Request status type representing the lifecycle of an asset request */ export type RequestStatus = 'pending' | 'approved' | 'rejected' | 'assigned' | 'acknowledged'; /** * Requested Asset Item entity representing individual items in a request */ export interface RequestedAssetItem { assetType: string; quantity: number; specifications: string; } /** * Asset Request entity representing an employee's request for assets */ export interface AssetRequest { requestID: number; employeeID: number; employeeName: string; requestedAssets: RequestedAssetItem[]; justification: string; status: RequestStatus; submittedDate: string; // ISO 8601 reviewedDate: string | null; // ISO 8601 reviewedBy: number | null; // Admin employee ID reviewerName: string | null; adminComments: string | null; assignedDate: string | null; // ISO 8601 assignedBy: number | null; // Admin employee ID assignerName: string | null; acknowledgedDate: string | null; // ISO 8601 acknowledgedBy: number | null; // Employee ID who acknowledged acknowledgementComments: string | null; } /** * Assigned Asset Detail entity for tracking specific assets assigned to a request */ export interface AssignedAssetDetail { assetID: number; assetCode: string; assetType: string; quantity: number; } // ============================================================================ // API REQUEST/RESPONSE TYPES // ============================================================================ /** * Request type for creating a new asset request (excludes auto-generated fields) */ export interface AssetRequestCreateRequest { employeeID: number; requestedAssets: RequestedAssetItem[]; justification: string; } /** * Request type for approving an asset request */ export interface AssetRequestApprovalRequest { adminComments: string; reviewedBy: number; } /** * Request type for rejecting an asset request */ export interface AssetRequestRejectionRequest { adminComments: string; reviewedBy: number; } /** * Request type for assigning assets to an approved request */ export interface AssetRequestAssignmentRequest { assignedAssets: AssignedAssetDetail[]; assignedBy: number; } /** * Request type for adding admin comments without changing status */ export interface AssetRequestCommentRequest { adminComments: string; commentedBy: number; } /** * Availability status for a requested asset type */ export interface AssetAvailabilityStatus { assetType: string; requested: number; available: number; canFulfill: boolean; } /** * Extended Asset Request with availability information */ export interface AssetRequestWithAvailability extends AssetRequest { availabilityStatus: AssetAvailabilityStatus[]; } /** * Availability check result for requested assets */ export interface AvailabilityCheck { canFulfill: boolean; details: { assetType: string; requested: number; available: number; shortfall: number; }[]; } /** * Filter options for asset request listing */ export interface RequestFilters { status?: RequestStatus; employeeId?: number; dateFrom?: string; dateTo?: string; } // ============================================================================ // ZOD VALIDATION SCHEMAS // ============================================================================ /** * Zod schema for RequestedAssetItem validation */ export const RequestedAssetItemSchema = z.object({ assetType: z.string() .min(1, 'Asset type is required') .refine(val => ASSET_TYPES.includes(val as AssetType), { message: 'Please select a valid asset type' }), quantity: z.number() .int('Quantity must be a whole number') .positive('Quantity must be positive') .max(10, 'Cannot request more than 10 of the same item'), specifications: z.string() .max(500, 'Specifications must be 500 characters or less') .optional() .default('') }); /** * Zod schema for Asset Request creation */ export const AssetRequestCreateSchema = z.object({ employeeID: z.number() .int('Employee ID must be a whole number') .positive('Employee ID must be positive'), requestedAssets: z.array(RequestedAssetItemSchema) .min(1, 'At least one asset must be requested') .max(20, 'Cannot request more than 20 items at once'), justification: z.string() .min(10, 'Please provide at least 10 characters of justification') .max(1000, 'Justification must be 1000 characters or less') .refine(val => val.trim().length >= 10, { message: 'Justification cannot be empty or contain only spaces' }) }); /** * Zod schema for Asset Request approval */ export const AssetRequestApprovalSchema = z.object({ adminComments: z.string() .min(5, 'Please provide comments for your decision') .max(1000, 'Comments must be 1000 characters or less') .refine(val => val.trim().length >= 5, { message: 'Comments cannot be empty or contain only spaces' }), reviewedBy: z.number() .int('Reviewer ID must be a whole number') .positive('Reviewer ID must be positive') }); /** * Zod schema for Asset Request rejection */ export const AssetRequestRejectionSchema = z.object({ adminComments: z.string() .min(5, 'Please provide comments for your decision') .max(1000, 'Comments must be 1000 characters or less') .refine(val => val.trim().length >= 5, { message: 'Comments cannot be empty or contain only spaces' }), reviewedBy: z.number() .int('Reviewer ID must be a whole number') .positive('Reviewer ID must be positive') }); /** * Zod schema for Assigned Asset Detail */ export const AssignedAssetDetailSchema = z.object({ assetID: z.number() .int('Asset ID must be a whole number') .positive('Asset ID must be positive'), assetCode: z.string() .min(1, 'Asset code is required') .max(20, 'Asset code must be 20 characters or less'), assetType: z.string() .min(1, 'Asset type is required') .max(50, 'Asset type must be 50 characters or less'), quantity: z.number() .int('Quantity must be a whole number') .positive('Quantity must be positive') }); /** * Zod schema for Asset Request assignment */ export const AssetRequestAssignmentSchema = z.object({ assignedAssets: z.array(AssignedAssetDetailSchema) .min(1, 'At least one asset must be assigned'), assignedBy: z.number() .int('Assigner ID must be a whole number') .positive('Assigner ID must be positive') }); /** * Zod schema for full Asset Request validation */ export const AssetRequestSchema = z.object({ requestID: z.number() .int('Request ID must be a whole number') .positive('Request ID must be positive'), employeeID: z.number() .int('Employee ID must be a whole number') .positive('Employee ID must be positive'), employeeName: z.string() .min(1, 'Employee name is required') .max(200, 'Employee name must be 200 characters or less'), requestedAssets: z.array(RequestedAssetItemSchema) .min(1, 'At least one asset must be requested'), justification: z.string() .min(10, 'Justification must be at least 10 characters') .max(1000, 'Justification must be 1000 characters or less'), status: z.enum(['pending', 'approved', 'rejected', 'assigned', 'acknowledged'], { errorMap: () => ({ message: 'Status must be pending, approved, rejected, assigned, or acknowledged' }) }), submittedDate: z.string().datetime('Submitted date must be in ISO 8601 format'), reviewedDate: z.string().datetime('Reviewed date must be in ISO 8601 format').nullable(), reviewedBy: z.number() .int('Reviewer ID must be a whole number') .positive('Reviewer ID must be positive') .nullable(), reviewerName: z.string() .max(200, 'Reviewer name must be 200 characters or less') .nullable(), adminComments: z.string() .max(1000, 'Admin comments must be 1000 characters or less') .nullable(), assignedDate: z.string().datetime('Assigned date must be in ISO 8601 format').nullable(), assignedBy: z.number() .int('Assigner ID must be a whole number') .positive('Assigner ID must be positive') .nullable(), assignerName: z.string() .max(200, 'Assigner name must be 200 characters or less') .nullable(), acknowledgedDate: z.string().datetime('Acknowledged date must be in ISO 8601 format').nullable(), acknowledgedBy: z.number() .int('Acknowledger ID must be a whole number') .positive('Acknowledger ID must be positive') .nullable(), acknowledgementComments: z.string() .max(1000, 'Acknowledgement comments must be 1000 characters or less') .nullable() }); // ============================================================================ // TYPE GUARDS AND VALIDATION HELPERS // ============================================================================ /** * Type guard to check if an object is a valid RequestedAssetItem */ export function isRequestedAssetItem(obj: any): obj is RequestedAssetItem { return RequestedAssetItemSchema.safeParse(obj).success; } /** * Type guard to check if an object is a valid AssetRequest */ export function isAssetRequest(obj: any): obj is AssetRequest { return AssetRequestSchema.safeParse(obj).success; } /** * Type guard to check if a status is valid */ export function isValidRequestStatus(status: string): status is RequestStatus { return ['pending', 'approved', 'rejected', 'assigned', 'acknowledged'].includes(status); } /** * Validates status transition rules * @param currentStatus - The current status of the request * @param newStatus - The desired new status * @returns true if the transition is valid, false otherwise */ export function isValidStatusTransition( currentStatus: RequestStatus, newStatus: RequestStatus ): boolean { const validTransitions: Record = { pending: ['approved', 'rejected'], approved: ['assigned'], rejected: [], // Cannot transition from rejected assigned: ['acknowledged'], // Can be acknowledged by employee acknowledged: [] // Cannot transition from acknowledged }; return validTransitions[currentStatus]?.includes(newStatus) ?? false; } /** * Validates and transforms API response data to ensure type safety */ export function validateAssetRequestApiResponse(data: any): AssetRequest { const result = AssetRequestSchema.safeParse(data); if (!result.success) { console.error('Asset Request validation failed:', result.error); throw new Error(`Invalid asset request data: ${result.error.message}`); } return result.data as AssetRequest; } /** * Validates asset request creation data */ export function validateAssetRequestCreate(data: any): AssetRequestCreateRequest { const result = AssetRequestCreateSchema.safeParse(data); if (!result.success) { console.error('Asset Request creation validation failed:', result.error); throw new Error(`Invalid asset request creation data: ${result.error.message}`); } return result.data as AssetRequestCreateRequest; } /** * Validates asset request approval data */ export function validateAssetRequestApproval(data: any): AssetRequestApprovalRequest { const result = AssetRequestApprovalSchema.safeParse(data); if (!result.success) { console.error('Asset Request approval validation failed:', result.error); throw new Error(`Invalid approval data: ${result.error.message}`); } return result.data as AssetRequestApprovalRequest; } /** * Validates asset request rejection data */ export function validateAssetRequestRejection(data: any): AssetRequestRejectionRequest { const result = AssetRequestRejectionSchema.safeParse(data); if (!result.success) { console.error('Asset Request rejection validation failed:', result.error); throw new Error(`Invalid rejection data: ${result.error.message}`); } return result.data as AssetRequestRejectionRequest; } /** * Validates asset request assignment data */ export function validateAssetRequestAssignment(data: any): AssetRequestAssignmentRequest { const result = AssetRequestAssignmentSchema.safeParse(data); if (!result.success) { console.error('Asset Request assignment validation failed:', result.error); throw new Error(`Invalid assignment data: ${result.error.message}`); } return result.data as AssetRequestAssignmentRequest; } // ============================================================================ // FORM VALIDATION HELPERS // ============================================================================ /** * Helper to get validation errors for Asset Request creation forms */ export function getAssetRequestCreateValidationErrors(data: any): Record { const result = AssetRequestCreateSchema.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 Request approval forms */ export function getAssetRequestApprovalValidationErrors(data: any): Record { const result = AssetRequestApprovalSchema.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 Request assignment forms */ export function getAssetRequestAssignmentValidationErrors(data: any): Record { const result = AssetRequestAssignmentSchema.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; } // ============================================================================ // UTILITY CONSTANTS // ============================================================================ /** * Request status display labels */ export const REQUEST_STATUS_LABELS: Record = { pending: 'Pending Review', approved: 'Approved', rejected: 'Rejected', assigned: 'Assigned', acknowledged: 'Acknowledged' }; /** * Request status colors for UI display */ export const REQUEST_STATUS_COLORS: Record = { pending: 'warning', approved: 'success', rejected: 'error', assigned: 'info', acknowledged: 'success' }; /** * Configuration constants for asset requests */ export const ASSET_REQUEST_CONFIG = { MAX_ITEMS_PER_REQUEST: 20, MAX_QUANTITY_PER_ITEM: 10, MIN_JUSTIFICATION_LENGTH: 10, MAX_JUSTIFICATION_LENGTH: 1000, MIN_ADMIN_COMMENTS_LENGTH: 5, MAX_ADMIN_COMMENTS_LENGTH: 1000, MAX_SPECIFICATIONS_LENGTH: 500 } as const;