@venulog/phasing-engine-schemas 0.2.0-alpha.1 → 0.2.1

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