File size: 15,677 Bytes
d97b8f9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
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<RequestStatus, RequestStatus[]> = {
    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<string, string> {
  const result = AssetRequestCreateSchema.safeParse(data);
  if (result.success) return {};

  const errors: Record<string, string> = {};
  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<string, string> {
  const result = AssetRequestApprovalSchema.safeParse(data);
  if (result.success) return {};

  const errors: Record<string, string> = {};
  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<string, string> {
  const result = AssetRequestAssignmentSchema.safeParse(data);
  if (result.success) return {};

  const errors: Record<string, string> = {};
  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<RequestStatus, string> = {
  pending: 'Pending Review',
  approved: 'Approved',
  rejected: 'Rejected',
  assigned: 'Assigned',
  acknowledged: 'Acknowledged'
};

/**

 * Request status colors for UI display

 */
export const REQUEST_STATUS_COLORS: Record<RequestStatus, string> = {
  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;