@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.
- package/README.md +51 -0
- package/dist/AgendaContext.d.ts +13 -0
- package/dist/AgendaContext.d.ts.map +1 -0
- package/dist/AgendaPage.d.ts +11 -0
- package/dist/AgendaPage.d.ts.map +1 -0
- package/dist/bridges/create-financial-bridge.d.ts +19 -0
- package/dist/bridges/create-financial-bridge.d.ts.map +1 -0
- package/dist/calendar-styles.d.ts +8 -0
- package/dist/calendar-styles.d.ts.map +1 -0
- package/dist/chunk-3LJKZQK4.js +12 -0
- package/dist/chunk-3LJKZQK4.js.map +1 -0
- package/dist/chunk-SYDBDI3Q.cjs +15 -0
- package/dist/chunk-SYDBDI3Q.cjs.map +1 -0
- package/dist/components/AgendaGeneralSettings.d.ts +3 -0
- package/dist/components/AgendaGeneralSettings.d.ts.map +1 -0
- package/dist/components/AppointmentModal.d.ts +16 -0
- package/dist/components/AppointmentModal.d.ts.map +1 -0
- package/dist/components/AppointmentPopover.d.ts +14 -0
- package/dist/components/AppointmentPopover.d.ts.map +1 -0
- package/dist/components/BookingPaymentPanel.d.ts +18 -0
- package/dist/components/BookingPaymentPanel.d.ts.map +1 -0
- package/dist/components/CalendarSidebar.d.ts +3 -0
- package/dist/components/CalendarSidebar.d.ts.map +1 -0
- package/dist/components/CalendarToolbar.d.ts +13 -0
- package/dist/components/CalendarToolbar.d.ts.map +1 -0
- package/dist/components/EventContextMenu.d.ts +14 -0
- package/dist/components/EventContextMenu.d.ts.map +1 -0
- package/dist/components/FloatingPanel.d.ts +27 -0
- package/dist/components/FloatingPanel.d.ts.map +1 -0
- package/dist/components/MiniCalendar.d.ts +9 -0
- package/dist/components/MiniCalendar.d.ts.map +1 -0
- package/dist/components/PaymentStatusBadge.d.ts +13 -0
- package/dist/components/PaymentStatusBadge.d.ts.map +1 -0
- package/dist/components/SearchCombobox.d.ts +38 -0
- package/dist/components/SearchCombobox.d.ts.map +1 -0
- package/dist/components/StatusBadge.d.ts +5 -0
- package/dist/components/StatusBadge.d.ts.map +1 -0
- package/dist/config.d.ts +151 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/data/index.d.ts +4 -0
- package/dist/data/index.d.ts.map +1 -0
- package/dist/data/mock.d.ts +3 -0
- package/dist/data/mock.d.ts.map +1 -0
- package/dist/data/supabase.d.ts +3 -0
- package/dist/data/supabase.d.ts.map +1 -0
- package/dist/data/types.d.ts +26 -0
- package/dist/data/types.d.ts.map +1 -0
- package/dist/financial-bridge.d.ts +92 -0
- package/dist/financial-bridge.d.ts.map +1 -0
- package/dist/hooks/useAgendaSettings.d.ts +7 -0
- package/dist/hooks/useAgendaSettings.d.ts.map +1 -0
- package/dist/index.cjs +5135 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5120 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/tenant.d.ts +3 -0
- package/dist/lib/tenant.d.ts.map +1 -0
- package/dist/locales/en.d.ts +2 -0
- package/dist/locales/en.d.ts.map +1 -0
- package/dist/locales/index.d.ts +2 -0
- package/dist/locales/index.d.ts.map +1 -0
- package/dist/locales/pt-BR.d.ts +2 -0
- package/dist/locales/pt-BR.d.ts.map +1 -0
- package/dist/registries.d.ts +3 -0
- package/dist/registries.d.ts.map +1 -0
- package/dist/store.d.ts +66 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/tenant-222EHYUC.cjs +16 -0
- package/dist/tenant-222EHYUC.cjs.map +1 -0
- package/dist/tenant-LAUWVUYB.js +3 -0
- package/dist/tenant-LAUWVUYB.js.map +1 -0
- package/dist/types.d.ts +202 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/views/CalendarView.d.ts +3 -0
- package/dist/views/CalendarView.d.ts.map +1 -0
- package/dist/views/ConfirmationsView.d.ts +3 -0
- package/dist/views/ConfirmationsView.d.ts.map +1 -0
- package/package.json +63 -0
- package/src/AgendaContext.tsx +11 -0
- package/src/AgendaPage.tsx +33 -0
- package/src/bridges/create-financial-bridge.ts +298 -0
- package/src/calendar-styles.css +245 -0
- package/src/calendar-styles.ts +285 -0
- package/src/components/AgendaGeneralSettings.tsx +64 -0
- package/src/components/AppointmentModal.tsx +1031 -0
- package/src/components/AppointmentPopover.tsx +221 -0
- package/src/components/BookingPaymentPanel.tsx +394 -0
- package/src/components/CalendarSidebar.tsx +119 -0
- package/src/components/CalendarToolbar.tsx +83 -0
- package/src/components/EventContextMenu.tsx +122 -0
- package/src/components/FloatingPanel.tsx +123 -0
- package/src/components/MiniCalendar.tsx +120 -0
- package/src/components/PaymentStatusBadge.tsx +62 -0
- package/src/components/SearchCombobox.tsx +216 -0
- package/src/components/StatusBadge.tsx +23 -0
- package/src/config.ts +269 -0
- package/src/data/index.ts +3 -0
- package/src/data/mock.ts +324 -0
- package/src/data/supabase.ts +570 -0
- package/src/data/types.ts +37 -0
- package/src/financial-bridge.ts +96 -0
- package/src/hooks/useAgendaSettings.ts +17 -0
- package/src/index.ts +180 -0
- package/src/lib/tenant.ts +3 -0
- package/src/locales/en.ts +207 -0
- package/src/locales/index.ts +7 -0
- package/src/locales/pt-BR.ts +207 -0
- package/src/registries.ts +36 -0
- package/src/store.ts +304 -0
- package/src/types.ts +265 -0
- package/src/views/CalendarView.tsx +904 -0
- package/src/views/ConfirmationsView.tsx +155 -0
package/src/data/mock.ts
ADDED
|
@@ -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
|
+
}
|