@fayz-ai/plugin-agenda 0.1.3

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.
Files changed (114) hide show
  1. package/README.md +51 -0
  2. package/dist/AgendaContext.d.ts +13 -0
  3. package/dist/AgendaContext.d.ts.map +1 -0
  4. package/dist/AgendaPage.d.ts +11 -0
  5. package/dist/AgendaPage.d.ts.map +1 -0
  6. package/dist/bridges/create-financial-bridge.d.ts +19 -0
  7. package/dist/bridges/create-financial-bridge.d.ts.map +1 -0
  8. package/dist/calendar-styles.d.ts +8 -0
  9. package/dist/calendar-styles.d.ts.map +1 -0
  10. package/dist/chunk-3LJKZQK4.js +12 -0
  11. package/dist/chunk-3LJKZQK4.js.map +1 -0
  12. package/dist/chunk-SYDBDI3Q.cjs +15 -0
  13. package/dist/chunk-SYDBDI3Q.cjs.map +1 -0
  14. package/dist/components/AgendaGeneralSettings.d.ts +3 -0
  15. package/dist/components/AgendaGeneralSettings.d.ts.map +1 -0
  16. package/dist/components/AppointmentModal.d.ts +16 -0
  17. package/dist/components/AppointmentModal.d.ts.map +1 -0
  18. package/dist/components/AppointmentPopover.d.ts +14 -0
  19. package/dist/components/AppointmentPopover.d.ts.map +1 -0
  20. package/dist/components/BookingPaymentPanel.d.ts +18 -0
  21. package/dist/components/BookingPaymentPanel.d.ts.map +1 -0
  22. package/dist/components/CalendarSidebar.d.ts +3 -0
  23. package/dist/components/CalendarSidebar.d.ts.map +1 -0
  24. package/dist/components/CalendarToolbar.d.ts +13 -0
  25. package/dist/components/CalendarToolbar.d.ts.map +1 -0
  26. package/dist/components/EventContextMenu.d.ts +14 -0
  27. package/dist/components/EventContextMenu.d.ts.map +1 -0
  28. package/dist/components/FloatingPanel.d.ts +27 -0
  29. package/dist/components/FloatingPanel.d.ts.map +1 -0
  30. package/dist/components/MiniCalendar.d.ts +9 -0
  31. package/dist/components/MiniCalendar.d.ts.map +1 -0
  32. package/dist/components/PaymentStatusBadge.d.ts +13 -0
  33. package/dist/components/PaymentStatusBadge.d.ts.map +1 -0
  34. package/dist/components/SearchCombobox.d.ts +38 -0
  35. package/dist/components/SearchCombobox.d.ts.map +1 -0
  36. package/dist/components/StatusBadge.d.ts +5 -0
  37. package/dist/components/StatusBadge.d.ts.map +1 -0
  38. package/dist/config.d.ts +151 -0
  39. package/dist/config.d.ts.map +1 -0
  40. package/dist/data/index.d.ts +4 -0
  41. package/dist/data/index.d.ts.map +1 -0
  42. package/dist/data/mock.d.ts +3 -0
  43. package/dist/data/mock.d.ts.map +1 -0
  44. package/dist/data/supabase.d.ts +3 -0
  45. package/dist/data/supabase.d.ts.map +1 -0
  46. package/dist/data/types.d.ts +26 -0
  47. package/dist/data/types.d.ts.map +1 -0
  48. package/dist/financial-bridge.d.ts +92 -0
  49. package/dist/financial-bridge.d.ts.map +1 -0
  50. package/dist/hooks/useAgendaSettings.d.ts +7 -0
  51. package/dist/hooks/useAgendaSettings.d.ts.map +1 -0
  52. package/dist/index.cjs +5135 -0
  53. package/dist/index.cjs.map +1 -0
  54. package/dist/index.d.ts +9 -0
  55. package/dist/index.d.ts.map +1 -0
  56. package/dist/index.js +5120 -0
  57. package/dist/index.js.map +1 -0
  58. package/dist/lib/tenant.d.ts +3 -0
  59. package/dist/lib/tenant.d.ts.map +1 -0
  60. package/dist/locales/en.d.ts +2 -0
  61. package/dist/locales/en.d.ts.map +1 -0
  62. package/dist/locales/index.d.ts +2 -0
  63. package/dist/locales/index.d.ts.map +1 -0
  64. package/dist/locales/pt-BR.d.ts +2 -0
  65. package/dist/locales/pt-BR.d.ts.map +1 -0
  66. package/dist/registries.d.ts +3 -0
  67. package/dist/registries.d.ts.map +1 -0
  68. package/dist/store.d.ts +66 -0
  69. package/dist/store.d.ts.map +1 -0
  70. package/dist/tenant-222EHYUC.cjs +16 -0
  71. package/dist/tenant-222EHYUC.cjs.map +1 -0
  72. package/dist/tenant-LAUWVUYB.js +3 -0
  73. package/dist/tenant-LAUWVUYB.js.map +1 -0
  74. package/dist/types.d.ts +202 -0
  75. package/dist/types.d.ts.map +1 -0
  76. package/dist/views/CalendarView.d.ts +3 -0
  77. package/dist/views/CalendarView.d.ts.map +1 -0
  78. package/dist/views/ConfirmationsView.d.ts +3 -0
  79. package/dist/views/ConfirmationsView.d.ts.map +1 -0
  80. package/package.json +63 -0
  81. package/src/AgendaContext.tsx +11 -0
  82. package/src/AgendaPage.tsx +33 -0
  83. package/src/bridges/create-financial-bridge.ts +298 -0
  84. package/src/calendar-styles.css +245 -0
  85. package/src/calendar-styles.ts +285 -0
  86. package/src/components/AgendaGeneralSettings.tsx +64 -0
  87. package/src/components/AppointmentModal.tsx +1031 -0
  88. package/src/components/AppointmentPopover.tsx +221 -0
  89. package/src/components/BookingPaymentPanel.tsx +394 -0
  90. package/src/components/CalendarSidebar.tsx +119 -0
  91. package/src/components/CalendarToolbar.tsx +83 -0
  92. package/src/components/EventContextMenu.tsx +122 -0
  93. package/src/components/FloatingPanel.tsx +123 -0
  94. package/src/components/MiniCalendar.tsx +120 -0
  95. package/src/components/PaymentStatusBadge.tsx +62 -0
  96. package/src/components/SearchCombobox.tsx +216 -0
  97. package/src/components/StatusBadge.tsx +23 -0
  98. package/src/config.ts +269 -0
  99. package/src/data/index.ts +3 -0
  100. package/src/data/mock.ts +324 -0
  101. package/src/data/supabase.ts +570 -0
  102. package/src/data/types.ts +37 -0
  103. package/src/financial-bridge.ts +96 -0
  104. package/src/hooks/useAgendaSettings.ts +17 -0
  105. package/src/index.ts +180 -0
  106. package/src/lib/tenant.ts +3 -0
  107. package/src/locales/en.ts +207 -0
  108. package/src/locales/index.ts +7 -0
  109. package/src/locales/pt-BR.ts +207 -0
  110. package/src/registries.ts +36 -0
  111. package/src/store.ts +304 -0
  112. package/src/types.ts +265 -0
  113. package/src/views/CalendarView.tsx +904 -0
  114. package/src/views/ConfirmationsView.tsx +155 -0
@@ -0,0 +1,324 @@
1
+ import type { AgendaDataProvider } from './types'
2
+ import type {
3
+ CalendarBooking, Professional, Schedule, TimeSlot,
4
+ CreateBookingInput, UpdateBookingInput, SaveScheduleInput,
5
+ BookingQuery, ConflictCheckParams, AvailableSlotsParams, BookingStatus,
6
+ } from '../types'
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Helpers
10
+ // ---------------------------------------------------------------------------
11
+
12
+ let nextId = 1
13
+ function uid(): string { return `mock-${nextId++}` }
14
+ function now(): string { return new Date().toISOString() }
15
+
16
+ function addMinutes(date: string, minutes: number): string {
17
+ const d = new Date(date)
18
+ d.setMinutes(d.getMinutes() + minutes)
19
+ return d.toISOString()
20
+ }
21
+
22
+ function timeOverlaps(aStart: string, aEnd: string, bStart: string, bEnd: string): boolean {
23
+ return aStart < bEnd && aEnd > bStart
24
+ }
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Sample data
28
+ // ---------------------------------------------------------------------------
29
+
30
+ const SAMPLE_PROFESSIONALS: Professional[] = [
31
+ { id: 'prof-1', name: 'Ana Silva', avatarUrl: null, locationId: 'loc-1', locationName: 'Main Branch', isActive: true },
32
+ { id: 'prof-2', name: 'Carlos Santos', avatarUrl: null, locationId: 'loc-1', locationName: 'Main Branch', isActive: true },
33
+ { id: 'prof-3', name: 'Maria Oliveira', avatarUrl: null, locationId: 'loc-1', locationName: 'Main Branch', isActive: true },
34
+ ]
35
+
36
+ function createSampleBookings(): CalendarBooking[] {
37
+ const today = new Date()
38
+ const year = today.getFullYear()
39
+ const month = today.getMonth()
40
+ const day = today.getDate()
41
+
42
+ function dt(hour: number, minute = 0): string {
43
+ return new Date(year, month, day, hour, minute).toISOString()
44
+ }
45
+
46
+ return [
47
+ {
48
+ id: 'bk-1', tenantId: 'mock', kind: 'appointment',
49
+ startsAt: dt(9, 0), endsAt: dt(10, 30), status: 'confirmed',
50
+ notes: null, orderId: 'ord-1', locationId: 'loc-1', metadata: {},
51
+ clientId: 'cl-1', clientName: 'Sarah Johnson', clientPhone: '(11) 99999-1111', clientEmail: 'sarah@email.com', clientAvatarUrl: null,
52
+ professionalId: 'prof-1', professionalName: 'Ana Silva', professionalAvatarUrl: null,
53
+ locationName: 'Main Branch', orderTotal: 250, orderStatus: 'draft',
54
+ services: [{ id: 'bi-1', serviceId: 'svc-1', name: 'Balayage', durationMinutes: 90, price: 250, assigneeId: 'prof-1' }],
55
+ totalDurationMinutes: 90, createdAt: now(), updatedAt: now(),
56
+ },
57
+ {
58
+ id: 'bk-2', tenantId: 'mock', kind: 'appointment',
59
+ startsAt: dt(10, 0), endsAt: dt(11, 0), status: 'scheduled',
60
+ notes: null, orderId: 'ord-2', locationId: 'loc-1', metadata: {},
61
+ clientId: 'cl-2', clientName: 'Emily Carter', clientPhone: '(11) 99999-2222', clientEmail: 'emily@email.com', clientAvatarUrl: null,
62
+ professionalId: 'prof-2', professionalName: 'Carlos Santos', professionalAvatarUrl: null,
63
+ locationName: 'Main Branch', orderTotal: 80, orderStatus: 'draft',
64
+ services: [{ id: 'bi-2', serviceId: 'svc-2', name: 'Haircut', durationMinutes: 60, price: 80, assigneeId: 'prof-2' }],
65
+ totalDurationMinutes: 60, createdAt: now(), updatedAt: now(),
66
+ },
67
+ {
68
+ id: 'bk-3', tenantId: 'mock', kind: 'appointment',
69
+ startsAt: dt(14, 0), endsAt: dt(15, 0), status: 'confirmed',
70
+ notes: 'First visit', orderId: 'ord-3', locationId: 'loc-1', metadata: {},
71
+ clientId: 'cl-3', clientName: 'Rachel Kim', clientPhone: '(11) 99999-3333', clientEmail: null, clientAvatarUrl: null,
72
+ professionalId: 'prof-3', professionalName: 'Maria Oliveira', professionalAvatarUrl: null,
73
+ locationName: 'Main Branch', orderTotal: 120, orderStatus: 'draft',
74
+ services: [{ id: 'bi-3', serviceId: 'svc-3', name: 'Facial Treatment', durationMinutes: 60, price: 120, assigneeId: 'prof-3' }],
75
+ totalDurationMinutes: 60, createdAt: now(), updatedAt: now(),
76
+ },
77
+ {
78
+ id: 'bk-4', tenantId: 'mock', kind: 'appointment',
79
+ startsAt: dt(11, 0), endsAt: dt(12, 0), status: 'scheduled',
80
+ notes: null, orderId: 'ord-4', locationId: 'loc-1', metadata: {},
81
+ clientId: 'cl-4', clientName: 'Jessica Lima', clientPhone: '(11) 99999-4444', clientEmail: null, clientAvatarUrl: null,
82
+ professionalId: 'prof-1', professionalName: 'Ana Silva', professionalAvatarUrl: null,
83
+ locationName: 'Main Branch', orderTotal: 60, orderStatus: 'draft',
84
+ services: [{ id: 'bi-4', serviceId: 'svc-4', name: 'Manicure', durationMinutes: 60, price: 60, assigneeId: 'prof-1' }],
85
+ totalDurationMinutes: 60, createdAt: now(), updatedAt: now(),
86
+ },
87
+ {
88
+ id: 'bk-5', tenantId: 'mock', kind: 'appointment',
89
+ startsAt: dt(15, 30), endsAt: dt(17, 0), status: 'confirmed',
90
+ notes: null, orderId: 'ord-5', locationId: 'loc-1', metadata: {},
91
+ clientId: 'cl-5', clientName: 'Amanda Wright', clientPhone: '(11) 99999-5555', clientEmail: 'amanda@email.com', clientAvatarUrl: null,
92
+ professionalId: 'prof-2', professionalName: 'Carlos Santos', professionalAvatarUrl: null,
93
+ locationName: 'Main Branch', orderTotal: 350, orderStatus: 'draft',
94
+ services: [
95
+ { id: 'bi-5', serviceId: 'svc-1', name: 'Highlights', durationMinutes: 60, price: 200, assigneeId: 'prof-2' },
96
+ { id: 'bi-6', serviceId: 'svc-2', name: 'Haircut', durationMinutes: 30, price: 150, assigneeId: 'prof-2' },
97
+ ],
98
+ totalDurationMinutes: 90, createdAt: now(), updatedAt: now(),
99
+ },
100
+ ]
101
+ }
102
+
103
+ function createSampleSchedules(): Schedule[] {
104
+ const schedules: Schedule[] = []
105
+ for (const prof of SAMPLE_PROFESSIONALS) {
106
+ // Mon-Fri 08:00-12:00, 13:00-18:00
107
+ for (let day = 1; day <= 5; day++) {
108
+ schedules.push({
109
+ id: uid(), tenantId: 'mock', kind: 'working_hours',
110
+ assigneeId: prof.id, locationId: prof.locationId,
111
+ dayOfWeek: day, specificDate: null,
112
+ startsAt: '08:00', endsAt: '12:00', isActive: true, metadata: {},
113
+ })
114
+ schedules.push({
115
+ id: uid(), tenantId: 'mock', kind: 'working_hours',
116
+ assigneeId: prof.id, locationId: prof.locationId,
117
+ dayOfWeek: day, specificDate: null,
118
+ startsAt: '13:00', endsAt: '18:00', isActive: true, metadata: {},
119
+ })
120
+ }
121
+ // Saturday 08:00-14:00
122
+ schedules.push({
123
+ id: uid(), tenantId: 'mock', kind: 'working_hours',
124
+ assigneeId: prof.id, locationId: prof.locationId,
125
+ dayOfWeek: 6, specificDate: null,
126
+ startsAt: '08:00', endsAt: '14:00', isActive: true, metadata: {},
127
+ })
128
+ }
129
+ return schedules
130
+ }
131
+
132
+ // ---------------------------------------------------------------------------
133
+ // Mock provider factory
134
+ // ---------------------------------------------------------------------------
135
+
136
+ export function createMockAgendaProvider(): AgendaDataProvider {
137
+ const bookings: CalendarBooking[] = createSampleBookings()
138
+ const schedules: Schedule[] = createSampleSchedules()
139
+ const professionals: Professional[] = [...SAMPLE_PROFESSIONALS]
140
+
141
+ return {
142
+ async getLocations(): Promise<Array<{ id: string; name: string }>> {
143
+ return []
144
+ },
145
+ async getBookings(query: BookingQuery): Promise<CalendarBooking[]> {
146
+ return bookings.filter((b) => {
147
+ if (query.kind && b.kind !== query.kind) return false
148
+ if (query.statuses?.length && !query.statuses.includes(b.status)) return false
149
+ if (query.professionalIds?.length && !query.professionalIds.includes(b.professionalId ?? '')) return false
150
+ if (query.locationId && b.locationId !== query.locationId) return false
151
+ if (query.dateRange) {
152
+ if (b.startsAt > query.dateRange.end || (b.endsAt ?? b.startsAt) < query.dateRange.start) return false
153
+ }
154
+ return true
155
+ })
156
+ },
157
+
158
+ async getBookingById(id: string): Promise<CalendarBooking | null> {
159
+ return bookings.find((b) => b.id === id) ?? null
160
+ },
161
+
162
+ async createBooking(input: CreateBookingInput): Promise<CalendarBooking> {
163
+ const totalDuration = input.services.reduce((sum, s) => sum + s.durationMinutes, 0)
164
+ const totalPrice = input.services.reduce((sum, s) => sum + s.price, 0)
165
+ const booking: CalendarBooking = {
166
+ id: uid(), tenantId: 'mock', kind: input.kind ?? 'appointment',
167
+ startsAt: input.startsAt, endsAt: addMinutes(input.startsAt, totalDuration || 30),
168
+ status: 'scheduled', notes: input.notes ?? null,
169
+ orderId: input.services.length > 0 ? uid() : null, locationId: input.locationId ?? null, metadata: {},
170
+ clientId: input.clientId, clientName: 'New Client', clientPhone: null, clientEmail: null, clientAvatarUrl: null,
171
+ professionalId: input.professionalId,
172
+ professionalName: professionals.find((p) => p.id === input.professionalId)?.name ?? 'Unknown',
173
+ professionalAvatarUrl: null,
174
+ locationName: null, orderTotal: totalPrice, orderStatus: 'draft',
175
+ services: input.services.map((s) => ({
176
+ id: uid(), serviceId: s.serviceId, name: s.name,
177
+ durationMinutes: s.durationMinutes, price: s.price,
178
+ assigneeId: s.assigneeId ?? input.professionalId,
179
+ })),
180
+ totalDurationMinutes: totalDuration, createdAt: now(), updatedAt: now(),
181
+ }
182
+ bookings.push(booking)
183
+ return booking
184
+ },
185
+
186
+ async updateBooking(id: string, data: UpdateBookingInput): Promise<CalendarBooking> {
187
+ const idx = bookings.findIndex((b) => b.id === id)
188
+ if (idx === -1) throw new Error('Booking not found')
189
+ const existing = bookings[idx]
190
+ const serviceUpdates: Partial<CalendarBooking> = {}
191
+ if (data.services) {
192
+ serviceUpdates.services = data.services.map((s) => ({
193
+ id: uid(), serviceId: s.serviceId, name: s.name,
194
+ durationMinutes: s.durationMinutes, price: s.price,
195
+ assigneeId: s.assigneeId ?? existing.professionalId ?? '',
196
+ }))
197
+ serviceUpdates.totalDurationMinutes = data.services.reduce((sum, s) => sum + s.durationMinutes, 0)
198
+ serviceUpdates.orderTotal = data.services.reduce((sum, s) => sum + s.price, 0)
199
+ }
200
+ const updated: CalendarBooking = {
201
+ ...existing,
202
+ ...(data.startsAt && { startsAt: data.startsAt }),
203
+ ...(data.endsAt && { endsAt: data.endsAt }),
204
+ ...(data.status && { status: data.status }),
205
+ ...(data.notes !== undefined && { notes: data.notes ?? null }),
206
+ ...(data.professionalId && { professionalId: data.professionalId }),
207
+ ...(data.clientId && { clientId: data.clientId }),
208
+ ...(data.locationId && { locationId: data.locationId }),
209
+ ...serviceUpdates,
210
+ updatedAt: now(),
211
+ }
212
+ bookings[idx] = updated
213
+ return updated
214
+ },
215
+
216
+ async deleteBooking(id: string): Promise<void> {
217
+ const idx = bookings.findIndex((b) => b.id === id)
218
+ if (idx !== -1) bookings.splice(idx, 1)
219
+ },
220
+
221
+ async updateBookingStatus(id: string, status: string): Promise<CalendarBooking> {
222
+ const idx = bookings.findIndex((b) => b.id === id)
223
+ if (idx === -1) throw new Error('Booking not found')
224
+ bookings[idx] = { ...bookings[idx], status: status as BookingStatus, updatedAt: now() }
225
+ return bookings[idx]
226
+ },
227
+
228
+ async checkConflict(params: ConflictCheckParams): Promise<boolean> {
229
+ return bookings.some((b) => {
230
+ if (params.excludeBookingId && b.id === params.excludeBookingId) return false
231
+ if (b.professionalId !== params.assigneeId) return false
232
+ if (b.status === 'cancelled' || b.status === 'no_show') return false
233
+ return timeOverlaps(b.startsAt, b.endsAt ?? b.startsAt, params.startsAt, params.endsAt)
234
+ })
235
+ },
236
+
237
+ async getAvailableSlots(params: AvailableSlotsParams): Promise<TimeSlot[]> {
238
+ const date = new Date(params.date)
239
+ const dayOfWeek = date.getDay()
240
+ const interval = params.slotInterval ?? 30
241
+
242
+ // Get working hours for this professional on this day
243
+ const daySchedules = schedules.filter((s) =>
244
+ s.assigneeId === params.assigneeId && s.isActive &&
245
+ (s.dayOfWeek === dayOfWeek || s.specificDate === params.date)
246
+ )
247
+
248
+ const slots: TimeSlot[] = []
249
+ for (const sched of daySchedules) {
250
+ const [startH, startM] = sched.startsAt.split(':').map(Number)
251
+ const [endH, endM] = sched.endsAt.split(':').map(Number)
252
+ const schedStart = new Date(date); schedStart.setHours(startH, startM, 0, 0)
253
+ const schedEnd = new Date(date); schedEnd.setHours(endH, endM, 0, 0)
254
+
255
+ let slotStart = new Date(schedStart)
256
+ while (true) {
257
+ const slotEnd = new Date(slotStart.getTime() + params.durationMinutes * 60000)
258
+ if (slotEnd > schedEnd) break
259
+ const startIso = slotStart.toISOString()
260
+ const endIso = slotEnd.toISOString()
261
+ const hasConflict = bookings.some((b) => {
262
+ if (b.professionalId !== params.assigneeId) return false
263
+ if (b.status === 'cancelled' || b.status === 'no_show') return false
264
+ return timeOverlaps(b.startsAt, b.endsAt ?? b.startsAt, startIso, endIso)
265
+ })
266
+ if (!hasConflict) {
267
+ slots.push({ start: startIso, end: endIso })
268
+ }
269
+ slotStart = new Date(slotStart.getTime() + interval * 60000)
270
+ }
271
+ }
272
+ return slots
273
+ },
274
+
275
+ async getSchedules(professionalId?: string): Promise<Schedule[]> {
276
+ if (professionalId) return schedules.filter((s) => s.assigneeId === professionalId)
277
+ return [...schedules]
278
+ },
279
+
280
+ async saveSchedule(input: SaveScheduleInput): Promise<Schedule> {
281
+ const schedule: Schedule = {
282
+ id: uid(), tenantId: 'mock', kind: 'working_hours',
283
+ assigneeId: input.assigneeId, locationId: input.locationId ?? null,
284
+ dayOfWeek: input.dayOfWeek ?? null, specificDate: input.specificDate ?? null,
285
+ startsAt: input.startsAt, endsAt: input.endsAt,
286
+ isActive: input.isActive !== false, metadata: input.metadata ?? {},
287
+ }
288
+ schedules.push(schedule)
289
+ return schedule
290
+ },
291
+
292
+ async deleteSchedule(id: string): Promise<void> {
293
+ const idx = schedules.findIndex((s) => s.id === id)
294
+ if (idx !== -1) schedules.splice(idx, 1)
295
+ },
296
+
297
+ async getProfessionals(): Promise<Professional[]> {
298
+ return [...professionals]
299
+ },
300
+
301
+ async getConfirmationsPending(daysAhead = 2): Promise<CalendarBooking[]> {
302
+ const cutoff = new Date()
303
+ cutoff.setDate(cutoff.getDate() + daysAhead)
304
+ return bookings.filter((b) =>
305
+ b.status === 'scheduled' && new Date(b.startsAt) <= cutoff && new Date(b.startsAt) >= new Date()
306
+ )
307
+ },
308
+
309
+ async sendConfirmation(bookingId: string, channel: string): Promise<void> {
310
+ const booking = bookings.find((b) => b.id === bookingId)
311
+ if (!booking) throw new Error('Booking not found')
312
+ const confirmations = (booking.metadata.confirmations as any[] ?? [])
313
+ confirmations.push({ channel, sentAt: now(), status: 'sent' })
314
+ booking.metadata = { ...booking.metadata, confirmations }
315
+ },
316
+
317
+ async completeBooking(bookingId: string): Promise<{ booking: CalendarBooking; orderId: string }> {
318
+ const idx = bookings.findIndex((b) => b.id === bookingId)
319
+ if (idx === -1) throw new Error('Booking not found')
320
+ bookings[idx] = { ...bookings[idx], status: 'completed', updatedAt: now() }
321
+ return { booking: bookings[idx], orderId: bookings[idx].orderId ?? '' }
322
+ },
323
+ }
324
+ }