@venulog/phasing-engine-schemas 0.2.0-alpha.0 → 0.2.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@ import { paginationSchema } from './pagination.js';
3
3
  import { BookingStatus } from './enums/bookingStatus.js';
4
4
  import { createSuccessResponseSchema, createMessageDataResponseSchema } from './common.js';
5
5
  import { z } from './zod.js';
6
- import { SlotStatus } from './enums/slotStatus.js';
6
+ import { PhaseSlotScheduleType } from './enums/phaseSlotScheduleType.js';
7
7
  // ------------------------------
8
8
  // Query parameters schema
9
9
  // ------------------------------
@@ -25,22 +25,6 @@ export const getEventBookingsQuerySchema = z
25
25
  })
26
26
  .extend(paginationSchema.shape)
27
27
  .openapi('GetEventBookingsQuery');
28
- // ------------------------------
29
- // Path parameters schema
30
- // ------------------------------
31
- export const cancelPhaseSlotParamsSchema = z
32
- .object({
33
- slotId: z
34
- .string()
35
- .min(1, 'Slot ID is required')
36
- .transform(val => parseInt(val, 10))
37
- .refine(val => !isNaN(val) && val > 0, { message: 'Slot ID must be positive' })
38
- .openapi({
39
- description: 'The ID of the phase slot to cancel',
40
- example: '1'
41
- })
42
- })
43
- .openapi('CancelPhaseSlotParams');
44
28
  export const closeEventParamsSchema = z
45
29
  .object({
46
30
  eventId: z
@@ -54,17 +38,6 @@ export const closeEventParamsSchema = z
54
38
  })
55
39
  })
56
40
  .openapi('CloseEventParams');
57
- // ------------------------------
58
- // Request body schemas
59
- // ------------------------------
60
- export const cancelPhaseSlotBodySchema = z
61
- .object({
62
- reason: z.string().optional().openapi({
63
- description: 'Optional reason for cancellation (for audit purposes)',
64
- example: 'Event postponed'
65
- })
66
- })
67
- .openapi('CancelPhaseSlotBody');
68
41
  export const closeEventBodySchema = z
69
42
  .object({
70
43
  reason: z.string().optional().openapi({
@@ -80,20 +53,57 @@ export const vehicleTypeSchema = z.object({
80
53
  id: z.number(),
81
54
  name: z.string()
82
55
  });
83
- export const phaseSlotSchema = z.object({
84
- id: z.number(),
85
- event_id: z.number(),
86
- slot_number: z.number(),
87
- vehicle_type_id: z.number().nullable(),
88
- company_role: z.string().nullable(),
89
- status: z.enum(BookingStatus),
90
- is_active: z.boolean(),
91
- created_at: z.string(),
92
- updated_at: z.string(),
93
- created_by: z.string().nullable(),
94
- updated_by: z.string().nullable(),
95
- vehicle_type: vehicleTypeSchema.nullable().optional()
96
- });
56
+ // New unified phase slot schedule schema (from database response)
57
+ export const phaseSlotScheduleSchema = z
58
+ .object({
59
+ id: z.number().int().positive().openapi({
60
+ description: 'Unique identifier for the schedule',
61
+ example: 1
62
+ }),
63
+ date: z.string().openapi({
64
+ description: 'Date of the schedule (YYYY-MM-DD format)',
65
+ example: '2025-12-15'
66
+ }),
67
+ start_time: z.string().openapi({
68
+ description: 'Start time (HH:MM format)',
69
+ example: '06:00'
70
+ }),
71
+ end_time: z.string().openapi({
72
+ description: 'End time (HH:MM format)',
73
+ example: '09:00'
74
+ }),
75
+ duration: z.number().openapi({
76
+ description: 'Duration in minutes',
77
+ example: 180
78
+ }),
79
+ phase_slot_id: z.number().int().positive().openapi({
80
+ description: 'ID of the associated phase slot',
81
+ example: 101
82
+ }),
83
+ phase_slot_schedule_type: z
84
+ .enum([PhaseSlotScheduleType.ASSEMBLY, PhaseSlotScheduleType.DISMANTLING])
85
+ .openapi({
86
+ description: 'Type of schedule',
87
+ example: PhaseSlotScheduleType.ASSEMBLY
88
+ }),
89
+ created_at: z.string().openapi({
90
+ description: 'Timestamp when schedule was created',
91
+ example: '2025-12-09T10:00:00.000Z'
92
+ }),
93
+ updated_at: z.string().openapi({
94
+ description: 'Timestamp when schedule was updated',
95
+ example: '2025-12-09T10:30:00.000Z'
96
+ }),
97
+ created_by: z.uuid().nullable().openapi({
98
+ description: 'UUID of user who created the schedule',
99
+ example: '550e8400-e29b-41d4-a716-446655440000'
100
+ }),
101
+ updated_by: z.uuid().nullable().openapi({
102
+ description: 'UUID of user who last updated the schedule',
103
+ example: '550e8400-e29b-41d4-a716-446655440000'
104
+ })
105
+ })
106
+ .openapi('PhaseSlotSchedule');
97
107
  export const companySchema = z.object({
98
108
  id: z.number(),
99
109
  company_name: z.string(),
@@ -119,38 +129,127 @@ export const companySchema = z.object({
119
129
  });
120
130
  export const phaseBookingSchema = z.object({
121
131
  id: z.number(),
122
- company_id: z.number(),
123
- phase_slot_id: z.number(),
132
+ phase_slot_schedule_id: z.number(),
124
133
  status: z.enum(BookingStatus),
125
134
  is_active: z.boolean(),
126
135
  created_at: z.string(),
127
136
  updated_at: z.string(),
128
137
  created_by: z.string().nullable(),
129
138
  updated_by: z.string().nullable(),
130
- company: companySchema,
131
- phase_slot: phaseSlotSchema
139
+ vehicle: z.record(z.string(), z.unknown()).nullable().optional(),
140
+ company: z.record(z.string(), z.unknown()).nullable().optional(),
141
+ booking_date: z.string().nullable().optional(),
142
+ start_time: z.string().nullable().optional(),
143
+ end_time: z.string().nullable().optional()
132
144
  });
145
+ export const companyDetailsSchema = z
146
+ .object({
147
+ company_role: z.string().min(1, 'Company role is required').openapi({
148
+ description: 'Company role',
149
+ example: 'exhibitor'
150
+ }),
151
+ hall: z.string().min(1, 'Hall is required').openapi({
152
+ description: 'Hall location',
153
+ example: 'Hall 1'
154
+ }),
155
+ stand_number: z.string().min(1, 'Stand number is required').openapi({
156
+ description: 'Stand number',
157
+ example: 'A-123'
158
+ }),
159
+ company_name: z.string().min(1, 'Company name is required').openapi({
160
+ description: 'Company name',
161
+ example: 'Acme Corp'
162
+ }),
163
+ business: z.string().min(1, 'Business is required').openapi({
164
+ description: 'Type of business',
165
+ example: 'Technology'
166
+ }),
167
+ departure_city: z.string().min(1, 'Departure city is required').openapi({
168
+ description: 'City of departure',
169
+ example: 'Paris'
170
+ }),
171
+ contact_name: z.string().min(1, 'Contact name is required').openapi({
172
+ description: 'Contact person name',
173
+ example: 'John Doe'
174
+ }),
175
+ email: z.email('Valid email is required').openapi({
176
+ description: 'Contact email',
177
+ example: 'john.doe@acme.com'
178
+ }),
179
+ phone: z.string().min(1, 'Phone is required').openapi({
180
+ description: 'Contact phone number',
181
+ example: '+33 1 23 45 67 89'
182
+ }),
183
+ driver_name: z.string().min(1, 'Driver name is required').openapi({
184
+ description: 'Driver name',
185
+ example: 'Jean Martin'
186
+ }),
187
+ driver_phone: z.string().min(1, 'Driver phone is required').openapi({
188
+ description: 'Driver phone number',
189
+ example: '+33 6 12 34 56 78'
190
+ }),
191
+ transport_company: z.string().min(1, 'Transport company is required').openapi({
192
+ description: 'Transport company name',
193
+ example: 'Fast Transport Ltd'
194
+ })
195
+ })
196
+ .openapi('CompanyDetails');
197
+ export const vehicleDetailsSchema = z
198
+ .object({
199
+ vehicle_type: z.string().min(1, 'Vehicle type is required').openapi({
200
+ description: 'Type of vehicle',
201
+ example: 'Truck'
202
+ }),
203
+ unloading_method: z.string().min(1, 'Unloading method is required').openapi({
204
+ description: 'Method of unloading',
205
+ example: 'Manual'
206
+ }),
207
+ license_plate: z.string().min(1, 'License plate is required').openapi({
208
+ description: 'Vehicle license plate',
209
+ example: 'AB-123-CD'
210
+ }),
211
+ trailer_registration: z.string().optional().openapi({
212
+ description: 'Trailer registration number',
213
+ example: 'TR-456-EF'
214
+ })
215
+ })
216
+ .openapi('VehicleDetails');
133
217
  export const createBookingBodySchema = z
134
218
  .object({
135
- company_id: z
219
+ // Event & Basic Info
220
+ event_id: z
136
221
  .number()
137
222
  .int()
138
223
  .positive({
139
- message: 'Company ID must be a positive integer'
224
+ message: 'Event ID must be a positive integer'
140
225
  })
141
226
  .openapi({
142
- description: 'ID of the company making the booking',
143
- example: 123
227
+ description: 'ID of the event to create the booking for',
228
+ example: 1
144
229
  }),
145
- phase_slot_id: z
146
- .number()
147
- .int()
148
- .positive({
149
- message: 'Phase slot ID must be a positive integer'
150
- })
230
+ request_type: z
231
+ .enum([PhaseSlotScheduleType.ASSEMBLY, PhaseSlotScheduleType.DISMANTLING])
232
+ .default(PhaseSlotScheduleType.ASSEMBLY)
151
233
  .openapi({
152
- description: 'ID of the phase slot to book',
153
- example: 456
234
+ description: 'Type of request (assembly or dismantling)',
235
+ example: PhaseSlotScheduleType.ASSEMBLY
236
+ }),
237
+ // Time slot selection
238
+ selected_date: z.string().min(1, 'Date is required').openapi({
239
+ description: 'Selected date (YYYY-MM-DD)',
240
+ example: '2025-12-15'
241
+ }),
242
+ selected_time: z.string().min(1, 'Time is required').openapi({
243
+ description: 'Selected time slot (HH:MM)',
244
+ example: '08:00'
245
+ }),
246
+ // Company details
247
+ company: companyDetailsSchema.openapi({
248
+ description: 'Company details including stand, contact, and driver info'
249
+ }),
250
+ // Vehicle details
251
+ vehicle: vehicleDetailsSchema.openapi({
252
+ description: 'Vehicle details'
154
253
  })
155
254
  })
156
255
  .openapi('CreateBookingBody');
@@ -160,22 +259,32 @@ export const createBookingDataSchema = z
160
259
  description: 'ID of the created booking',
161
260
  example: 789
162
261
  }),
163
- company_id: z.number().openapi({
164
- description: 'ID of the company',
165
- example: 123
166
- }),
167
- phase_slot_id: z.number().openapi({
168
- description: 'ID of the booked phase slot',
262
+ phase_slot_schedule_id: z.number().openapi({
263
+ description: 'ID of the phase slot schedule',
169
264
  example: 456
170
265
  }),
171
- slot_number: z.number().openapi({
172
- description: 'Slot number',
173
- example: 42
174
- }),
175
266
  status: z.enum(BookingStatus).openapi({
176
267
  description: 'Booking status',
177
268
  example: BookingStatus.BOOKED
178
269
  }),
270
+ booking_date: z.string().openapi({
271
+ description: 'Booking date',
272
+ example: '2025-12-15'
273
+ }),
274
+ start_time: z.string().openapi({
275
+ description: 'Start time',
276
+ example: '08:00'
277
+ }),
278
+ end_time: z.string().openapi({
279
+ description: 'End time',
280
+ example: '08:30'
281
+ }),
282
+ company: companyDetailsSchema.openapi({
283
+ description: 'Company details including stand, contact, and driver info'
284
+ }),
285
+ vehicle: vehicleDetailsSchema.openapi({
286
+ description: 'Vehicle details'
287
+ }),
179
288
  created_at: z.string().openapi({
180
289
  description: 'Timestamp when booking was created',
181
290
  example: '2025-12-05T10:30:00.000Z'
@@ -197,31 +306,6 @@ export const eventBookingsDataSchema = z
197
306
  })
198
307
  .openapi('EventBookingsData');
199
308
  export const eventBookingsResponseSchema = createSuccessResponseSchema(eventBookingsDataSchema, 'EventBookingsResponse', 'Event bookings data with phase slot details');
200
- export const cancelPhaseSlotDataSchema = z
201
- .object({
202
- slot_id: z.number().openapi({
203
- description: 'ID of the cancelled slot',
204
- example: 1
205
- }),
206
- status: z.enum(SlotStatus).openapi({
207
- description: 'New status of the slot',
208
- example: SlotStatus.AVAILABLE
209
- }),
210
- cancelled_at: z.string().openapi({
211
- description: 'Timestamp when the slot was cancelled',
212
- example: '2025-12-03T20:47:00.000Z'
213
- }),
214
- cancelled_by: z.string().nullable().openapi({
215
- description: 'ID of the user who cancelled the slot',
216
- example: null
217
- }),
218
- reason: z.string().nullable().openapi({
219
- description: 'Reason for cancellation',
220
- example: 'Event postponed'
221
- })
222
- })
223
- .openapi('CancelPhaseSlotData');
224
- export const cancelPhaseSlotResponseSchema = createMessageDataResponseSchema(cancelPhaseSlotDataSchema, 'CancelPhaseSlotResponse', 'Phase slot cancelled successfully', 'Details of the cancelled phase slot');
225
309
  export const closeEventDataSchema = z
226
310
  .object({
227
311
  event_id: z.number().openapi({
@@ -268,7 +352,6 @@ export const confirmBookingResponseSchema = z.object({
268
352
  booking_id: z.number(),
269
353
  booking_status: z.enum(BookingStatus),
270
354
  slot_id: z.number(),
271
- slot_status: z.enum(SlotStatus),
272
355
  confirmed_at: z.string(),
273
356
  confirmed_by: z.string().nullable()
274
357
  })
@@ -302,10 +385,6 @@ export const refuseBookingDataSchema = z
302
385
  description: 'ID of the associated phase slot',
303
386
  example: 10
304
387
  }),
305
- slot_status: z.enum([SlotStatus.AVAILABLE]).openapi({
306
- description: 'New status of the phase slot',
307
- example: SlotStatus.AVAILABLE
308
- }),
309
388
  refused_at: z.string().openapi({
310
389
  description: 'ISO 8601 timestamp when the booking was refused',
311
390
  example: '2025-12-08T10:30:00.000Z'
@@ -335,44 +414,15 @@ export const createPhaseSlotsBodySchema = z
335
414
  }),
336
415
  slots: z
337
416
  .array(z.object({
338
- slot_number: z
339
- .number()
340
- .int()
341
- .positive({
342
- message: 'Slot number must be a positive integer'
343
- })
344
- .openapi({
345
- description: 'Unique slot number within the event',
346
- example: 42
347
- }),
348
- vehicle_type_id: z
349
- .number()
350
- .int()
351
- .positive({
352
- message: 'Vehicle type ID must be a positive integer'
353
- })
354
- .nullable()
355
- .optional()
356
- .openapi({
357
- description: 'Optional vehicle type ID for the slot',
358
- example: 1
359
- }),
360
417
  company_role: z.string().min(1).nullable().optional().openapi({
361
418
  description: 'Optional company role for the slot',
362
419
  example: 'buyer'
363
- }),
364
- status: z.enum(SlotStatus).optional().openapi({
365
- description: 'Initial status of the slot (defaults to available)',
366
- example: SlotStatus.AVAILABLE
367
420
  })
368
421
  }))
369
422
  .min(1, 'At least one slot must be provided')
370
423
  .openapi({
371
424
  description: 'Array of slots to create',
372
- example: [
373
- { slot_number: 42, vehicle_type_id: 1, company_role: 'buyer' },
374
- { slot_number: 43, vehicle_type_id: 2, company_role: 'seller' }
375
- ]
425
+ example: [{ company_role: 'buyer' }, { company_role: 'seller' }]
376
426
  })
377
427
  })
378
428
  .openapi('CreatePhaseSlotsBody');
@@ -386,22 +436,10 @@ export const createPhaseSlotDataSchema = z
386
436
  description: 'ID of the event',
387
437
  example: 1
388
438
  }),
389
- slot_number: z.number().openapi({
390
- description: 'Slot number',
391
- example: 42
392
- }),
393
- vehicle_type_id: z.number().nullable().openapi({
394
- description: 'Vehicle type ID',
395
- example: 1
396
- }),
397
439
  company_role: z.string().nullable().openapi({
398
440
  description: 'Company role',
399
441
  example: 'buyer'
400
442
  }),
401
- status: z.enum(SlotStatus).openapi({
402
- description: 'Slot status',
403
- example: SlotStatus.AVAILABLE
404
- }),
405
443
  is_active: z.boolean().openapi({
406
444
  description: 'Whether the slot is active',
407
445
  example: true,
@@ -433,13 +471,13 @@ export const createPhaseSlotsDataSchema = z
433
471
  }),
434
472
  failed_slots: z
435
473
  .array(z.object({
436
- slot_number: z.number().openapi({
437
- description: 'Slot number that failed to create',
438
- example: 44
474
+ index: z.number().openapi({
475
+ description: 'Index of the slot in the request that failed to create',
476
+ example: 0
439
477
  }),
440
478
  error: z.string().openapi({
441
479
  description: 'Error message explaining why the slot creation failed',
442
- example: 'Slot number 44 already exists for event 1'
480
+ example: 'Failed to create slot'
443
481
  })
444
482
  }))
445
483
  .openapi({
@@ -467,36 +505,9 @@ export const updatePhaseSlotParamsSchema = z
467
505
  .openapi('UpdatePhaseSlotParams');
468
506
  export const updatePhaseSlotBodySchema = z
469
507
  .object({
470
- slot_number: z
471
- .number()
472
- .int()
473
- .positive({
474
- message: 'Slot number must be a positive integer'
475
- })
476
- .optional()
477
- .openapi({
478
- description: 'New slot number (must be unique within the event)',
479
- example: 43
480
- }),
481
- vehicle_type_id: z
482
- .number()
483
- .int()
484
- .positive({
485
- message: 'Vehicle type ID must be a positive integer'
486
- })
487
- .nullable()
488
- .optional()
489
- .openapi({
490
- description: 'New vehicle type ID for the slot',
491
- example: 2
492
- }),
493
508
  company_role: z.string().min(1).nullable().optional().openapi({
494
509
  description: 'New company role for the slot',
495
510
  example: 'seller'
496
- }),
497
- status: z.enum(SlotStatus).optional().openapi({
498
- description: 'New status for the slot',
499
- example: SlotStatus.RESERVED
500
511
  })
501
512
  })
502
513
  .refine(data => Object.keys(data).length > 0, {
@@ -513,22 +524,10 @@ export const updatePhaseSlotDataSchema = z
513
524
  description: 'ID of the event',
514
525
  example: 1
515
526
  }),
516
- slot_number: z.number().openapi({
517
- description: 'Updated slot number',
518
- example: 43
519
- }),
520
- vehicle_type_id: z.number().nullable().openapi({
521
- description: 'Updated vehicle type ID',
522
- example: 2
523
- }),
524
527
  company_role: z.string().nullable().openapi({
525
528
  description: 'Updated company role',
526
529
  example: 'seller'
527
530
  }),
528
- status: z.enum(SlotStatus).openapi({
529
- description: 'Updated slot status',
530
- example: SlotStatus.RESERVED
531
- }),
532
531
  is_active: z.boolean().openapi({
533
532
  description: 'Whether the slot is active',
534
533
  example: true
@@ -545,163 +544,29 @@ export const updatePhaseSlotDataSchema = z
545
544
  .openapi('UpdatePhaseSlotData');
546
545
  export const updatePhaseSlotResponseSchema = createMessageDataResponseSchema(updatePhaseSlotDataSchema, 'UpdatePhaseSlotResponse', 'Phase slot updated successfully', 'Details of the updated phase slot');
547
546
  // ------------------------------
548
- // Phase Slot Assemblies & Dismantlings Schemas
547
+ // Phase Slot Schedules Schemas
549
548
  // ------------------------------
550
- export const phaseSlotAssemblySchema = z
551
- .object({
552
- id: z.number().int().positive().openapi({
553
- description: 'Unique identifier for the assembly',
554
- example: 1
555
- }),
556
- date: z.string().openapi({
557
- description: 'Date of the assembly (YYYY-MM-DD format)',
558
- example: '2025-12-15'
559
- }),
560
- start_time: z.string().openapi({
561
- description: 'Start time of the assembly (HH:MM format)',
562
- example: '06:00'
563
- }),
564
- end_time: z.string().openapi({
565
- description: 'End time of the assembly (HH:MM format)',
566
- example: '09:00'
567
- }),
568
- duration: z.number().openapi({
569
- description: 'Duration of the assembly in minutes',
570
- example: 180
571
- }),
572
- phase_slot_id: z.number().int().positive().openapi({
573
- description: 'ID of the associated phase slot',
574
- example: 101
575
- }),
576
- created_at: z.string().openapi({
577
- description: 'Timestamp when assembly was created',
578
- example: '2025-12-09T10:00:00.000Z'
579
- }),
580
- updated_at: z.string().openapi({
581
- description: 'Timestamp when assembly was updated',
582
- example: '2025-12-09T10:30:00.000Z'
583
- })
584
- })
585
- .openapi('PhaseSlotAssembly');
586
- export const phaseSlotDismantlingSchema = z
549
+ export const upsertPhaseSlotScheduleItemSchema = z
587
550
  .object({
588
- id: z.number().int().positive().openapi({
589
- description: 'Unique identifier for the dismantling',
551
+ id: z.number().int().positive().optional().openapi({
552
+ description: 'Unique identifier for existing schedule (required for updates)',
590
553
  example: 1
591
554
  }),
592
- date: z.string().openapi({
593
- description: 'Date of the dismantling (YYYY-MM-DD format)',
594
- example: '2025-12-15'
595
- }),
596
- start_time: z.string().openapi({
597
- description: 'Start time of the dismantling (HH:MM format)',
598
- example: '06:00'
599
- }),
600
- end_time: z.string().openapi({
601
- description: 'End time of the dismantling (HH:MM format)',
602
- example: '09:00'
603
- }),
604
- duration: z.number().openapi({
605
- description: 'Duration of the dismantling in minutes',
606
- example: 180
607
- }),
608
555
  phase_slot_id: z.number().int().positive().openapi({
609
556
  description: 'ID of the associated phase slot',
610
557
  example: 101
611
558
  }),
612
- created_at: z.string().openapi({
613
- description: 'Timestamp when dismantling was created',
614
- example: '2025-12-09T10:00:00.000Z'
615
- }),
616
- updated_at: z.string().openapi({
617
- description: 'Timestamp when dismantling was updated',
618
- example: '2025-12-09T10:30:00.000Z'
619
- })
620
- })
621
- .openapi('PhaseSlotDismantling');
622
- export const upsertPhaseSlotAssemblySchema = z
623
- .object({
624
- date: z
625
- .string()
626
- .regex(/^\d{4}-\d{2}-\d{2}$/, 'Date must be in YYYY-MM-DD format')
627
- .openapi({
628
- description: 'Date of the assembly (YYYY-MM-DD format)',
629
- example: '2025-12-15'
630
- }),
631
- start_time: z
632
- .string()
633
- .regex(/^\d{2}:\d{2}$/, 'Time must be in HH:MM format')
634
- .optional()
635
- .openapi({
636
- description: 'Start time of the assembly (HH:MM format). Defaults to 06:00',
637
- example: '06:00'
638
- }),
639
- end_time: z
640
- .string()
641
- .regex(/^\d{2}:\d{2}$/, 'Time must be in HH:MM format')
642
- .optional()
643
- .openapi({
644
- description: 'End time of the assembly (HH:MM format). Defaults to 09:00',
645
- example: '09:00'
646
- }),
647
- duration: z.number().positive().optional().openapi({
648
- description: 'Duration of the assembly in minutes. Defaults to 60',
649
- example: 180
650
- }),
651
- phase_slot_id: z.number().int().positive().openapi({
652
- description: 'ID of the associated phase slot',
653
- example: 101
654
- })
655
- })
656
- .openapi('UpsertPhaseSlotAssembly');
657
- export const upsertPhaseSlotDismantlingSchema = z
658
- .object({
659
559
  date: z
660
560
  .string()
661
561
  .regex(/^\d{4}-\d{2}-\d{2}$/, 'Date must be in YYYY-MM-DD format')
662
562
  .openapi({
663
- description: 'Date of the dismantling (YYYY-MM-DD format)',
563
+ description: 'Date of the schedule (YYYY-MM-DD format)',
664
564
  example: '2025-12-15'
665
565
  }),
666
566
  start_time: z
667
567
  .string()
668
568
  .regex(/^\d{2}:\d{2}$/, 'Time must be in HH:MM format')
669
- .optional()
670
- .openapi({
671
- description: 'Start time of the dismantling (HH:MM format). Defaults to 06:00',
672
- example: '06:00'
673
- }),
674
- end_time: z
675
- .string()
676
- .regex(/^\d{2}:\d{2}$/, 'Time must be in HH:MM format')
677
- .optional()
678
- .openapi({
679
- description: 'End time of the dismantling (HH:MM format). Defaults to 09:00',
680
- example: '09:00'
681
- }),
682
- duration: z.number().positive().optional().openapi({
683
- description: 'Duration of the dismantling in minutes. Defaults to 60',
684
- example: 180
685
- }),
686
- phase_slot_id: z.number().int().positive().openapi({
687
- description: 'ID of the associated phase slot',
688
- example: 101
689
- })
690
- })
691
- .openapi('UpsertPhaseSlotDismantling');
692
- // ------------------------------
693
- // Combined Phase Slot Schedules Schemas (Assembly + Dismantling)
694
- // ------------------------------
695
- export const phaseSlotScheduleOperationSchema = z
696
- .object({
697
- id: z.number().int().positive().optional().openapi({
698
- description: 'Unique identifier for existing records (required for updates and deletes)',
699
- example: 1
700
- }),
701
- start_time: z
702
- .string()
703
- .regex(/^\d{2}:\d{2}$/, 'Time must be in HH:MM format')
704
- .optional()
569
+ .default('06:00')
705
570
  .openapi({
706
571
  description: 'Start time (HH:MM format)',
707
572
  example: '06:00'
@@ -709,182 +574,183 @@ export const phaseSlotScheduleOperationSchema = z
709
574
  end_time: z
710
575
  .string()
711
576
  .regex(/^\d{2}:\d{2}$/, 'Time must be in HH:MM format')
712
- .optional()
577
+ .default('09:00')
713
578
  .openapi({
714
579
  description: 'End time (HH:MM format)',
715
580
  example: '09:00'
716
581
  }),
717
- duration: z.number().positive().optional().openapi({
582
+ duration: z.number().positive().default(60).openapi({
718
583
  description: 'Duration in minutes',
719
584
  example: 180
720
585
  }),
721
- date: z
722
- .string()
723
- .regex(/^\d{4}-\d{2}-\d{2}$/, 'Date must be in YYYY-MM-DD format')
724
- .optional()
586
+ phase_slot_schedule_type: z
587
+ .enum([PhaseSlotScheduleType.ASSEMBLY, PhaseSlotScheduleType.DISMANTLING])
725
588
  .openapi({
726
- description: 'Date for assembly (YYYY-MM-DD format)',
727
- example: '2025-12-15'
728
- }),
729
- _delete: z.boolean().optional().openapi({
730
- description: 'Flag to mark this record for deletion. Requires existing id.',
731
- example: false
589
+ description: 'Type of schedule',
590
+ example: PhaseSlotScheduleType.ASSEMBLY
732
591
  })
733
592
  })
734
- .check(data => {
735
- // If delete is true, id is required
736
- if (data.value._delete === true && !data.value.id) {
737
- data.issues.push({
738
- code: 'custom',
739
- message: 'ID is required when delete is true',
740
- path: ['id'],
741
- input: data.value
742
- });
743
- }
744
- // If delete is false or undefined, date is required
745
- if ((data.value._delete === false || data.value._delete === undefined) && !data.value.date) {
746
- data.issues.push({
747
- code: 'custom',
748
- message: 'Date is required when delete is false or undefined',
749
- path: ['date'],
750
- input: data.value
751
- });
752
- }
753
- })
754
- .openapi('PhaseSlotScheduleOperation');
755
- export const phaseSlotScheduleSchema = z
593
+ .openapi('UpsertPhaseSlotScheduleItem');
594
+ export const deletePhaseSlotScheduleItemSchema = z
756
595
  .object({
757
- phase_slot_id: z.number().int().positive().openapi({
758
- description: 'ID of the associated phase slot',
759
- example: 101
760
- }),
761
- assembly: phaseSlotScheduleOperationSchema.openapi({
762
- description: 'Assembly operation details with date'
763
- }),
764
- dismantling: phaseSlotScheduleOperationSchema.openapi({
765
- description: 'Dismantling operation details with date'
596
+ id: z.number().int().positive().openapi({
597
+ description: 'ID of the schedule to delete',
598
+ example: 1
766
599
  })
767
600
  })
768
- .openapi('PhaseSlotSchedule');
601
+ .openapi('DeletePhaseSlotScheduleItem');
769
602
  export const upsertPhaseSlotSchedulesBodySchema = z
770
603
  .object({
771
- schedules: z
772
- .array(phaseSlotScheduleSchema)
773
- .min(1, 'At least one schedule is required')
604
+ upsert: z
605
+ .array(upsertPhaseSlotScheduleItemSchema)
606
+ .optional()
774
607
  .openapi({
775
- description: 'Array of phase slot schedules to upsert',
608
+ description: 'Array of schedules to create or update',
776
609
  example: [
777
610
  {
778
611
  phase_slot_id: 101,
779
- assembly: {
780
- date: '2025-12-15',
781
- start_time: '06:00',
782
- end_time: '09:00',
783
- duration: 180
784
- },
785
- dismantling: {
786
- date: '2025-12-16',
787
- start_time: '15:00',
788
- end_time: '18:00',
789
- duration: 180
790
- }
791
- },
792
- {
793
- phase_slot_id: 102,
794
- assembly: {
795
- id: 15,
796
- _delete: true
797
- }
612
+ date: '2025-12-15',
613
+ start_time: '06:00',
614
+ end_time: '09:00',
615
+ duration: 180,
616
+ phase_slot_schedule_type: 'assembly'
798
617
  },
799
618
  {
800
- phase_slot_id: 103,
801
- dismantling: {
802
- id: 42,
803
- _delete: true
804
- }
619
+ id: 5,
620
+ phase_slot_id: 101,
621
+ date: '2025-12-16',
622
+ start_time: '15:00',
623
+ end_time: '18:00',
624
+ duration: 180,
625
+ phase_slot_schedule_type: 'dismantling'
805
626
  }
806
627
  ]
628
+ }),
629
+ delete: z
630
+ .array(deletePhaseSlotScheduleItemSchema)
631
+ .optional()
632
+ .openapi({
633
+ description: 'Array of schedule IDs to delete',
634
+ example: [{ id: 42 }]
807
635
  })
636
+ })
637
+ .refine(data => (data.upsert && data.upsert.length > 0) || (data.delete && data.delete.length > 0), {
638
+ message: 'At least one upsert or delete operation is required'
808
639
  })
809
640
  .openapi('UpsertPhaseSlotSchedulesBody');
810
641
  export const upsertPhaseSlotSchedulesDataSchema = z
811
642
  .object({
812
- total_processed: z.number().int().nonnegative().openapi({
813
- description: 'Total number of schedules processed',
814
- example: 2
643
+ total_upserted: z.number().int().nonnegative().openapi({
644
+ description: 'Total number of schedules successfully upserted',
645
+ example: 3
815
646
  }),
816
- total_assemblies_upserted: z.number().int().nonnegative().openapi({
817
- description: 'Total number of assemblies successfully upserted',
818
- example: 2
819
- }),
820
- total_dismantlings_upserted: z.number().int().nonnegative().openapi({
821
- description: 'Total number of dismantlings successfully upserted',
647
+ total_deleted: z.number().int().nonnegative().openapi({
648
+ description: 'Total number of schedules successfully deleted',
822
649
  example: 1
823
650
  }),
824
- total_assemblies_deleted: z.number().int().nonnegative().openapi({
825
- description: 'Total number of assemblies successfully deleted',
826
- example: 0
827
- }),
828
- total_dismantlings_deleted: z.number().int().nonnegative().openapi({
829
- description: 'Total number of dismantlings successfully deleted',
830
- example: 0
831
- }),
832
- upserted_assemblies: z.array(phaseSlotAssemblySchema).openapi({
833
- description: 'Array of successfully upserted assemblies'
834
- }),
835
- upserted_dismantlings: z.array(phaseSlotDismantlingSchema).openapi({
836
- description: 'Array of successfully upserted dismantlings'
837
- }),
838
- deleted_assemblies: z
839
- .array(z.object({
840
- id: z.number().int().positive(),
841
- phase_slot_id: z.number().int().positive()
842
- }))
843
- .openapi({
844
- description: 'Array of successfully deleted assemblies',
845
- example: []
651
+ upserted_schedules: z.array(phaseSlotScheduleSchema).openapi({
652
+ description: 'Array of successfully upserted schedules'
846
653
  }),
847
- deleted_dismantlings: z
654
+ deleted_schedules: z
848
655
  .array(z.object({
849
- id: z.number().int().positive(),
850
- phase_slot_id: z.number().int().positive()
656
+ id: z.number().int().positive()
851
657
  }))
852
658
  .openapi({
853
- description: 'Array of successfully deleted dismantlings',
854
- example: []
659
+ description: 'Array of successfully deleted schedule IDs',
660
+ example: [{ id: 42 }]
855
661
  }),
856
662
  failed_operations: z
857
663
  .array(z.object({
858
- phase_slot_id: z.number().int().positive(),
859
- operation: z.enum(['assembly', 'dismantling']),
860
- action: z.enum(['upsert', 'delete']),
664
+ operation: z.enum(['upsert', 'delete']),
861
665
  error: z.string(),
862
- data: z.union([
863
- z.object({
864
- id: z.number().int().positive().optional(),
865
- date: z.string().optional(),
866
- start_time: z.string().optional(),
867
- end_time: z.string().optional(),
868
- duration: z.number().positive().optional(),
869
- _delete: z.boolean().optional()
870
- }),
871
- z.object({
872
- id: z.number().int().positive()
873
- })
874
- ])
666
+ data: z.union([upsertPhaseSlotScheduleItemSchema, deletePhaseSlotScheduleItemSchema])
875
667
  }))
876
668
  .openapi({
877
669
  description: 'Array of operations that failed to process',
878
670
  example: [
879
671
  {
880
- phase_slot_id: 999,
881
- operation: 'assembly',
882
- action: 'upsert',
672
+ operation: 'upsert',
883
673
  error: 'Phase slot 999 not found or inactive',
884
- data: { date: '2025-12-15', start_time: '06:00' }
674
+ data: {
675
+ phase_slot_id: 999,
676
+ date: '2025-12-15',
677
+ start_time: '06:00',
678
+ end_time: '09:00',
679
+ duration: 180,
680
+ phase_slot_schedule_type: 'assembly'
681
+ }
885
682
  }
886
683
  ]
887
684
  })
888
685
  })
889
686
  .openapi('UpsertPhaseSlotSchedulesData');
890
- export const upsertPhaseSlotSchedulesResponseSchema = createMessageDataResponseSchema(upsertPhaseSlotSchedulesDataSchema, 'UpsertPhaseSlotSchedulesResponse', 'Bulk phase slot schedules upsert completed', 'Results of the combined upsert operation');
687
+ export const upsertPhaseSlotSchedulesResponseSchema = createMessageDataResponseSchema(upsertPhaseSlotSchedulesDataSchema, 'UpsertPhaseSlotSchedulesResponse', 'Phase slot schedules operation completed', 'Results of the upsert/delete operations');
688
+ // ------------------------------
689
+ // Check Slot Availability schemas
690
+ // ------------------------------
691
+ export const checkSlotAvailabilityQuerySchema = z
692
+ .object({
693
+ schedule_id: z
694
+ .string()
695
+ .min(1)
696
+ .transform(val => parseInt(val, 10))
697
+ .refine(val => !isNaN(val) && val > 0, {
698
+ message: 'Schedule ID must be a positive number'
699
+ })
700
+ .openapi({
701
+ description: 'The ID of the phase slot schedule',
702
+ example: '1'
703
+ }),
704
+ date: z
705
+ .string()
706
+ .regex(/^\d{4}-\d{2}-\d{2}$/, 'Date must be in YYYY-MM-DD format')
707
+ .openapi({
708
+ description: 'The date to check availability (YYYY-MM-DD)',
709
+ example: '2025-12-15'
710
+ }),
711
+ start_time: z
712
+ .string()
713
+ .regex(/^\d{2}:\d{2}$/, 'Time must be in HH:MM format')
714
+ .openapi({
715
+ description: 'The start time to check availability (HH:MM)',
716
+ example: '08:00'
717
+ })
718
+ })
719
+ .openapi('CheckSlotAvailabilityQuery');
720
+ export const checkSlotAvailabilityDataSchema = z
721
+ .object({
722
+ schedule_id: z.number().openapi({
723
+ description: 'ID of the phase slot schedule',
724
+ example: 1
725
+ }),
726
+ date: z.string().openapi({
727
+ description: 'Date checked',
728
+ example: '2025-12-15'
729
+ }),
730
+ start_time: z.string().openapi({
731
+ description: 'Time slot checked',
732
+ example: '08:00'
733
+ }),
734
+ is_available: z.boolean().openapi({
735
+ description: 'Whether the slot is available for booking',
736
+ example: true
737
+ }),
738
+ max_capacity: z.number().openapi({
739
+ description: 'Maximum booking capacity',
740
+ example: 5
741
+ }),
742
+ current_bookings: z.number().openapi({
743
+ description: 'Current number of total bookings',
744
+ example: 3
745
+ }),
746
+ confirmed_bookings: z.number().openapi({
747
+ description: 'Current number of confirmed bookings',
748
+ example: 1
749
+ }),
750
+ available_capacity: z.number().openapi({
751
+ description: 'Remaining available capacity',
752
+ example: 2
753
+ })
754
+ })
755
+ .openapi('CheckSlotAvailabilityData');
756
+ export const checkSlotAvailabilityResponseSchema = createSuccessResponseSchema(checkSlotAvailabilityDataSchema, 'CheckSlotAvailabilityResponse', 'Slot availability information with capacity details');