@venulog/phasing-engine-schemas 0.2.0-alpha.1 → 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,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
96
  }),
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'
97
+ created_by: z.uuid().nullable().openapi({
98
+ description: 'UUID of user who created the schedule',
99
+ example: '550e8400-e29b-41d4-a716-446655440000'
136
100
  }),
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
- }),
145
- created_at: z.string().openapi({
146
- description: 'Timestamp when dismantling was created',
147
- example: '2025-12-09T10:00:00.000Z'
148
- }),
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,127 @@ 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
+ 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()
206
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');
207
217
  export const createBookingBodySchema = z
208
218
  .object({
209
- company_id: z
219
+ // Event & Basic Info
220
+ event_id: z
210
221
  .number()
211
222
  .int()
212
223
  .positive({
213
- message: 'Company ID must be a positive integer'
224
+ message: 'Event ID must be a positive integer'
214
225
  })
215
226
  .openapi({
216
- description: 'ID of the company making the booking',
217
- example: 123
227
+ description: 'ID of the event to create the booking for',
228
+ example: 1
218
229
  }),
219
- phase_slot_id: z
220
- .number()
221
- .int()
222
- .positive({
223
- message: 'Phase slot ID must be a positive integer'
224
- })
230
+ request_type: z
231
+ .enum([PhaseSlotScheduleType.ASSEMBLY, PhaseSlotScheduleType.DISMANTLING])
232
+ .default(PhaseSlotScheduleType.ASSEMBLY)
225
233
  .openapi({
226
- description: 'ID of the phase slot to book',
227
- 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'
228
253
  })
229
254
  })
230
255
  .openapi('CreateBookingBody');
@@ -234,22 +259,32 @@ export const createBookingDataSchema = z
234
259
  description: 'ID of the created booking',
235
260
  example: 789
236
261
  }),
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',
262
+ phase_slot_schedule_id: z.number().openapi({
263
+ description: 'ID of the phase slot schedule',
243
264
  example: 456
244
265
  }),
245
- slot_number: z.number().openapi({
246
- description: 'Slot number',
247
- example: 42
248
- }),
249
266
  status: z.enum(BookingStatus).openapi({
250
267
  description: 'Booking status',
251
268
  example: BookingStatus.BOOKED
252
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
+ }),
253
288
  created_at: z.string().openapi({
254
289
  description: 'Timestamp when booking was created',
255
290
  example: '2025-12-05T10:30:00.000Z'
@@ -271,31 +306,6 @@ export const eventBookingsDataSchema = z
271
306
  })
272
307
  .openapi('EventBookingsData');
273
308
  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
309
  export const closeEventDataSchema = z
300
310
  .object({
301
311
  event_id: z.number().openapi({
@@ -342,7 +352,6 @@ export const confirmBookingResponseSchema = z.object({
342
352
  booking_id: z.number(),
343
353
  booking_status: z.enum(BookingStatus),
344
354
  slot_id: z.number(),
345
- slot_status: z.enum(SlotStatus),
346
355
  confirmed_at: z.string(),
347
356
  confirmed_by: z.string().nullable()
348
357
  })
@@ -376,10 +385,6 @@ export const refuseBookingDataSchema = z
376
385
  description: 'ID of the associated phase slot',
377
386
  example: 10
378
387
  }),
379
- slot_status: z.enum([SlotStatus.AVAILABLE]).openapi({
380
- description: 'New status of the phase slot',
381
- example: SlotStatus.AVAILABLE
382
- }),
383
388
  refused_at: z.string().openapi({
384
389
  description: 'ISO 8601 timestamp when the booking was refused',
385
390
  example: '2025-12-08T10:30:00.000Z'
@@ -409,44 +414,15 @@ export const createPhaseSlotsBodySchema = z
409
414
  }),
410
415
  slots: z
411
416
  .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
417
  company_role: z.string().min(1).nullable().optional().openapi({
435
418
  description: 'Optional company role for the slot',
436
419
  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
420
  })
442
421
  }))
443
422
  .min(1, 'At least one slot must be provided')
444
423
  .openapi({
445
424
  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
- ]
425
+ example: [{ company_role: 'buyer' }, { company_role: 'seller' }]
450
426
  })
451
427
  })
452
428
  .openapi('CreatePhaseSlotsBody');
@@ -460,22 +436,10 @@ export const createPhaseSlotDataSchema = z
460
436
  description: 'ID of the event',
461
437
  example: 1
462
438
  }),
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
439
  company_role: z.string().nullable().openapi({
472
440
  description: 'Company role',
473
441
  example: 'buyer'
474
442
  }),
475
- status: z.enum(SlotStatus).openapi({
476
- description: 'Slot status',
477
- example: SlotStatus.AVAILABLE
478
- }),
479
443
  is_active: z.boolean().openapi({
480
444
  description: 'Whether the slot is active',
481
445
  example: true,
@@ -507,13 +471,13 @@ export const createPhaseSlotsDataSchema = z
507
471
  }),
508
472
  failed_slots: z
509
473
  .array(z.object({
510
- slot_number: z.number().openapi({
511
- description: 'Slot number that failed to create',
512
- example: 44
474
+ index: z.number().openapi({
475
+ description: 'Index of the slot in the request that failed to create',
476
+ example: 0
513
477
  }),
514
478
  error: z.string().openapi({
515
479
  description: 'Error message explaining why the slot creation failed',
516
- example: 'Slot number 44 already exists for event 1'
480
+ example: 'Failed to create slot'
517
481
  })
518
482
  }))
519
483
  .openapi({
@@ -541,36 +505,9 @@ export const updatePhaseSlotParamsSchema = z
541
505
  .openapi('UpdatePhaseSlotParams');
542
506
  export const updatePhaseSlotBodySchema = z
543
507
  .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
508
  company_role: z.string().min(1).nullable().optional().openapi({
568
509
  description: 'New company role for the slot',
569
510
  example: 'seller'
570
- }),
571
- status: z.enum(SlotStatus).optional().openapi({
572
- description: 'New status for the slot',
573
- example: SlotStatus.RESERVED
574
511
  })
575
512
  })
576
513
  .refine(data => Object.keys(data).length > 0, {
@@ -587,22 +524,10 @@ export const updatePhaseSlotDataSchema = z
587
524
  description: 'ID of the event',
588
525
  example: 1
589
526
  }),
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
527
  company_role: z.string().nullable().openapi({
599
528
  description: 'Updated company role',
600
529
  example: 'seller'
601
530
  }),
602
- status: z.enum(SlotStatus).openapi({
603
- description: 'Updated slot status',
604
- example: SlotStatus.RESERVED
605
- }),
606
531
  is_active: z.boolean().openapi({
607
532
  description: 'Whether the slot is active',
608
533
  example: true
@@ -619,91 +544,29 @@ export const updatePhaseSlotDataSchema = z
619
544
  .openapi('UpdatePhaseSlotData');
620
545
  export const updatePhaseSlotResponseSchema = createMessageDataResponseSchema(updatePhaseSlotDataSchema, 'UpdatePhaseSlotResponse', 'Phase slot updated successfully', 'Details of the updated phase slot');
621
546
  // ------------------------------
622
- // Phase Slot Assemblies & Dismantlings Schemas
547
+ // Phase Slot Schedules Schemas
623
548
  // ------------------------------
624
- export const upsertPhaseSlotAssemblySchema = z
549
+ export const upsertPhaseSlotScheduleItemSchema = z
625
550
  .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
551
+ id: z.number().int().positive().optional().openapi({
552
+ description: 'Unique identifier for existing schedule (required for updates)',
553
+ example: 1
652
554
  }),
653
555
  phase_slot_id: z.number().int().positive().openapi({
654
556
  description: 'ID of the associated phase slot',
655
557
  example: 101
656
- })
657
- })
658
- .openapi('UpsertPhaseSlotAssembly');
659
- export const upsertPhaseSlotDismantlingSchema = z
660
- .object({
558
+ }),
661
559
  date: z
662
560
  .string()
663
561
  .regex(/^\d{4}-\d{2}-\d{2}$/, 'Date must be in YYYY-MM-DD format')
664
562
  .openapi({
665
- description: 'Date of the dismantling (YYYY-MM-DD format)',
563
+ description: 'Date of the schedule (YYYY-MM-DD format)',
666
564
  example: '2025-12-15'
667
565
  }),
668
566
  start_time: z
669
567
  .string()
670
568
  .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()
569
+ .default('06:00')
707
570
  .openapi({
708
571
  description: 'Start time (HH:MM format)',
709
572
  example: '06:00'
@@ -711,182 +574,183 @@ export const phaseSlotScheduleOperationSchema = z
711
574
  end_time: z
712
575
  .string()
713
576
  .regex(/^\d{2}:\d{2}$/, 'Time must be in HH:MM format')
714
- .optional()
577
+ .default('09:00')
715
578
  .openapi({
716
579
  description: 'End time (HH:MM format)',
717
580
  example: '09:00'
718
581
  }),
719
- duration: z.number().positive().optional().openapi({
582
+ duration: z.number().positive().default(60).openapi({
720
583
  description: 'Duration in minutes',
721
584
  example: 180
722
585
  }),
723
- date: z
724
- .string()
725
- .regex(/^\d{4}-\d{2}-\d{2}$/, 'Date must be in YYYY-MM-DD format')
726
- .optional()
586
+ phase_slot_schedule_type: z
587
+ .enum([PhaseSlotScheduleType.ASSEMBLY, PhaseSlotScheduleType.DISMANTLING])
727
588
  .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
589
+ description: 'Type of schedule',
590
+ example: PhaseSlotScheduleType.ASSEMBLY
734
591
  })
735
592
  })
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
593
+ .openapi('UpsertPhaseSlotScheduleItem');
594
+ export const deletePhaseSlotScheduleItemSchema = z
758
595
  .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'
596
+ id: z.number().int().positive().openapi({
597
+ description: 'ID of the schedule to delete',
598
+ example: 1
768
599
  })
769
600
  })
770
- .openapi('PhaseSlotSchedule');
601
+ .openapi('DeletePhaseSlotScheduleItem');
771
602
  export const upsertPhaseSlotSchedulesBodySchema = z
772
603
  .object({
773
- schedules: z
774
- .array(phaseSlotScheduleSchema)
775
- .min(1, 'At least one schedule is required')
604
+ upsert: z
605
+ .array(upsertPhaseSlotScheduleItemSchema)
606
+ .optional()
776
607
  .openapi({
777
- description: 'Array of phase slot schedules to upsert',
608
+ description: 'Array of schedules to create or update',
778
609
  example: [
779
610
  {
780
611
  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
- }
612
+ date: '2025-12-15',
613
+ start_time: '06:00',
614
+ end_time: '09:00',
615
+ duration: 180,
616
+ phase_slot_schedule_type: 'assembly'
793
617
  },
794
618
  {
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
- }
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'
807
626
  }
808
627
  ]
628
+ }),
629
+ delete: z
630
+ .array(deletePhaseSlotScheduleItemSchema)
631
+ .optional()
632
+ .openapi({
633
+ description: 'Array of schedule IDs to delete',
634
+ example: [{ id: 42 }]
809
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'
810
639
  })
811
640
  .openapi('UpsertPhaseSlotSchedulesBody');
812
641
  export const upsertPhaseSlotSchedulesDataSchema = z
813
642
  .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
643
+ total_upserted: z.number().int().nonnegative().openapi({
644
+ description: 'Total number of schedules successfully upserted',
645
+ example: 3
821
646
  }),
822
- total_dismantlings_upserted: z.number().int().nonnegative().openapi({
823
- description: 'Total number of dismantlings successfully upserted',
647
+ total_deleted: z.number().int().nonnegative().openapi({
648
+ description: 'Total number of schedules successfully deleted',
824
649
  example: 1
825
650
  }),
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: []
651
+ upserted_schedules: z.array(phaseSlotScheduleSchema).openapi({
652
+ description: 'Array of successfully upserted schedules'
848
653
  }),
849
- deleted_dismantlings: z
654
+ deleted_schedules: z
850
655
  .array(z.object({
851
- id: z.number().int().positive(),
852
- phase_slot_id: z.number().int().positive()
656
+ id: z.number().int().positive()
853
657
  }))
854
658
  .openapi({
855
- description: 'Array of successfully deleted dismantlings',
856
- example: []
659
+ description: 'Array of successfully deleted schedule IDs',
660
+ example: [{ id: 42 }]
857
661
  }),
858
662
  failed_operations: z
859
663
  .array(z.object({
860
- phase_slot_id: z.number().int().positive(),
861
- operation: z.enum(['assembly', 'dismantling']),
862
- action: z.enum(['upsert', 'delete']),
664
+ operation: z.enum(['upsert', 'delete']),
863
665
  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
- ])
666
+ data: z.union([upsertPhaseSlotScheduleItemSchema, deletePhaseSlotScheduleItemSchema])
877
667
  }))
878
668
  .openapi({
879
669
  description: 'Array of operations that failed to process',
880
670
  example: [
881
671
  {
882
- phase_slot_id: 999,
883
- operation: 'assembly',
884
- action: 'upsert',
672
+ operation: 'upsert',
885
673
  error: 'Phase slot 999 not found or inactive',
886
- 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
+ }
887
682
  }
888
683
  ]
889
684
  })
890
685
  })
891
686
  .openapi('UpsertPhaseSlotSchedulesData');
892
- 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');