@fayz-ai/plugin-agenda 0.9.0 → 0.9.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.
- package/README.md +3 -3
- package/dist/bridges/create-financial-bridge.d.ts +1 -1
- package/dist/bridges/create-financial-bridge.d.ts.map +1 -1
- package/dist/bridges/financial-bridge.d.ts.map +1 -0
- package/dist/{chunk-Q2PAWBPR.js → chunk-76ZOHRBM.js} +2 -2
- package/dist/{chunk-Q2PAWBPR.js.map → chunk-76ZOHRBM.js.map} +1 -1
- package/dist/components/PaymentStatusBadge.d.ts +1 -1
- package/dist/components/PaymentStatusBadge.d.ts.map +1 -1
- package/dist/config.d.ts +2 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/{AgendaContext.d.ts → context.d.ts} +1 -1
- package/dist/context.d.ts.map +1 -0
- package/dist/data/registries.d.ts.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +170 -7
- package/dist/index.js.map +1 -1
- package/dist/lib/message-events.d.ts +12 -0
- package/dist/lib/message-events.d.ts.map +1 -0
- package/dist/public/{data.d.ts → data/index.d.ts} +4 -4
- package/dist/public/data/index.d.ts.map +1 -0
- package/dist/public/{data.supabase.d.ts → data/supabase.d.ts} +2 -2
- package/dist/public/data/supabase.d.ts.map +1 -0
- package/dist/public/{hooks.d.ts → hooks/index.d.ts} +4 -4
- package/dist/public/hooks/index.d.ts.map +1 -0
- package/dist/public/index.d.ts +4 -4
- package/dist/public/index.d.ts.map +1 -1
- package/dist/public/index.js +3 -3
- package/dist/public/index.js.map +1 -1
- package/dist/public/{format.d.ts → lib/format.d.ts} +1 -1
- package/dist/public/lib/format.d.ts.map +1 -0
- package/dist/public/views/BookingWidget.d.ts.map +1 -0
- package/dist/public/{steps → views/steps}/ContactStep.d.ts +1 -1
- package/dist/public/views/steps/ContactStep.d.ts.map +1 -0
- package/dist/public/{steps → views/steps}/DateTimeStep.d.ts +3 -3
- package/dist/public/views/steps/DateTimeStep.d.ts.map +1 -0
- package/dist/public/{steps → views/steps}/OverviewStep.d.ts +1 -1
- package/dist/public/views/steps/OverviewStep.d.ts.map +1 -0
- package/dist/public/{steps → views/steps}/PaymentStep.d.ts +1 -1
- package/dist/public/views/steps/PaymentStep.d.ts.map +1 -0
- package/dist/store.d.ts +1 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/styles/calendar-styles.d.ts.map +1 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/{AgendaPage.d.ts → views/AgendaPage.d.ts} +3 -3
- package/dist/views/AgendaPage.d.ts.map +1 -0
- package/package.json +6 -9
- package/dist/AgendaContext.d.ts.map +0 -1
- package/dist/AgendaPage.d.ts.map +0 -1
- package/dist/calendar-styles.d.ts.map +0 -1
- package/dist/financial-bridge.d.ts.map +0 -1
- package/dist/public/BookingWidget.d.ts.map +0 -1
- package/dist/public/data.d.ts.map +0 -1
- package/dist/public/data.supabase.d.ts.map +0 -1
- package/dist/public/format.d.ts.map +0 -1
- package/dist/public/hooks.d.ts.map +0 -1
- package/dist/public/steps/ContactStep.d.ts.map +0 -1
- package/dist/public/steps/DateTimeStep.d.ts.map +0 -1
- package/dist/public/steps/OverviewStep.d.ts.map +0 -1
- package/dist/public/steps/PaymentStep.d.ts.map +0 -1
- package/dist/registries.d.ts.map +0 -1
- package/src/AgendaContext.tsx +0 -11
- package/src/AgendaPage.tsx +0 -33
- package/src/bridges/create-financial-bridge.ts +0 -296
- package/src/calendar-styles.css +0 -245
- package/src/calendar-styles.ts +0 -285
- package/src/components/AgendaGeneralSettings.tsx +0 -86
- package/src/components/AgendaMobileList.tsx +0 -103
- package/src/components/AppointmentModal.tsx +0 -1050
- package/src/components/AppointmentPopover.tsx +0 -231
- package/src/components/BookingPaymentPanel.tsx +0 -394
- package/src/components/CalendarSidebar.tsx +0 -119
- package/src/components/CalendarToolbar.tsx +0 -86
- package/src/components/EventContextMenu.tsx +0 -125
- package/src/components/FloatingPanel.tsx +0 -123
- package/src/components/MiniCalendar.tsx +0 -120
- package/src/components/PaymentStatusBadge.tsx +0 -62
- package/src/components/StatusBadge.tsx +0 -23
- package/src/config.ts +0 -368
- package/src/data/index.ts +0 -4
- package/src/data/mock.ts +0 -344
- package/src/data/supabase.ts +0 -636
- package/src/data/types.ts +0 -37
- package/src/financial-bridge.ts +0 -96
- package/src/hooks/useAgendaSettings.ts +0 -17
- package/src/index.ts +0 -297
- package/src/integrations/google-calendar/GoogleCalendarSettings.tsx +0 -136
- package/src/integrations/google-calendar/components/CalendarColorDot.tsx +0 -23
- package/src/integrations/google-calendar/components/ChannelMappingList.tsx +0 -329
- package/src/integrations/google-calendar/components/ImportEventsWizard.tsx +0 -215
- package/src/integrations/google-calendar/components/useAgendaLookups.ts +0 -100
- package/src/integrations/google-calendar/connector.ts +0 -20
- package/src/integrations/google-calendar/connectorDef.tsx +0 -185
- package/src/integrations/google-calendar/data/supabase.ts +0 -170
- package/src/integrations/google-calendar/data/tables.ts +0 -12
- package/src/integrations/google-calendar/index.ts +0 -45
- package/src/integrations/google-calendar/integration.test.ts +0 -199
- package/src/integrations/google-calendar/mapping.ts +0 -183
- package/src/integrations/google-calendar/migrations/001_google_calendar.sql +0 -97
- package/src/integrations/google-calendar/migrations/002_smart_sync.sql +0 -517
- package/src/integrations/google-calendar/migrations/003_plg_rename.sql +0 -273
- package/src/integrations/google-calendar/types.ts +0 -142
- package/src/lib/tenant.ts +0 -3
- package/src/locales/en.ts +0 -220
- package/src/locales/index.ts +0 -7
- package/src/locales/pt-BR.ts +0 -220
- package/src/migrations/000b_v_bookings_rename.sql +0 -21
- package/src/migrations/000c_dedupe_customer_phones.sql +0 -43
- package/src/migrations/001_public_booking.sql +0 -296
- package/src/migrations/002_v_bookings_compat.sql +0 -14
- package/src/migrations/003_public_client_kind.sql +0 -175
- package/src/migrations/004_public_booking_assignee.sql +0 -176
- package/src/migrations/005_agent_rpcs.sql +0 -168
- package/src/migrations/006_agent_rpc_localtime.sql +0 -181
- package/src/migrations/007_schedule_exception_precedence.sql +0 -290
- package/src/migrations/008_agent_rpc_exception_precedence.sql +0 -172
- package/src/migrations/009_public_booking_location.sql +0 -162
- package/src/migrations/010_public_service_curation.sql +0 -31
- package/src/migrations/011_default_assignee.sql +0 -167
- package/src/migrations/012_drop_legacy_booking_overload.sql +0 -29
- package/src/migrations/013_normalize_v_appointments.sql +0 -199
- package/src/migrations/014_agent_rpc_update_appointment.sql +0 -199
- package/src/migrations/index.ts +0 -2377
- package/src/public/BookingWidget.tsx +0 -324
- package/src/public/components/BrandHeader.tsx +0 -27
- package/src/public/components/ServiceCard.tsx +0 -39
- package/src/public/components/SidebarExtras.tsx +0 -30
- package/src/public/components/StepRailItem.tsx +0 -27
- package/src/public/components/SuccessPanel.tsx +0 -27
- package/src/public/components/SummaryRail.tsx +0 -90
- package/src/public/components/defaults.ts +0 -14
- package/src/public/context.tsx +0 -103
- package/src/public/createPublicBookingPlugin.ts +0 -281
- package/src/public/data.supabase.ts +0 -97
- package/src/public/data.ts +0 -105
- package/src/public/format.ts +0 -96
- package/src/public/hooks.ts +0 -110
- package/src/public/index.ts +0 -69
- package/src/public/steps/ContactStep.tsx +0 -184
- package/src/public/steps/DateTimeStep.tsx +0 -138
- package/src/public/steps/OverviewStep.tsx +0 -81
- package/src/public/steps/PaymentStep.tsx +0 -180
- package/src/public/types.ts +0 -250
- package/src/registries.ts +0 -35
- package/src/store.ts +0 -311
- package/src/types.ts +0 -273
- package/src/views/CalendarView.tsx +0 -1112
- package/src/views/ConfirmationsView.tsx +0 -155
- /package/dist/{financial-bridge.d.ts → bridges/financial-bridge.d.ts} +0 -0
- /package/dist/{registries.d.ts → data/registries.d.ts} +0 -0
- /package/dist/public/{BookingWidget.d.ts → views/BookingWidget.d.ts} +0 -0
- /package/dist/{calendar-styles.d.ts → styles/calendar-styles.d.ts} +0 -0
package/src/data/supabase.ts
DELETED
|
@@ -1,636 +0,0 @@
|
|
|
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
|
-
import { getSupabaseClientOptional, getActiveTenantId } from '@fayz-ai/core'
|
|
8
|
-
import { getAgendaTenantId } from '../lib/tenant'
|
|
9
|
-
|
|
10
|
-
// ---------------------------------------------------------------------------
|
|
11
|
-
// Helpers
|
|
12
|
-
// ---------------------------------------------------------------------------
|
|
13
|
-
|
|
14
|
-
function getTenantId(): string | undefined {
|
|
15
|
-
// Plugin-local override wins; else fall back to the app's active tenant so
|
|
16
|
-
// writes stamp the right tenant_id and pass orders/bookings RLS.
|
|
17
|
-
return getAgendaTenantId() ?? getActiveTenantId()
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function snakeToCamel(obj: Record<string, unknown>): Record<string, unknown> {
|
|
21
|
-
const result: Record<string, unknown> = {}
|
|
22
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
23
|
-
result[key.replace(/_([a-z])/g, (_, c) => c.toUpperCase())] = value
|
|
24
|
-
}
|
|
25
|
-
return result
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function getClients() {
|
|
29
|
-
const supabase = getSupabaseClientOptional() as any
|
|
30
|
-
if (!supabase) throw new Error('Supabase not initialized')
|
|
31
|
-
// Core v1 lives in PUBLIC — a single client serves both core and view reads.
|
|
32
|
-
return { core: supabase, pub: supabase }
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function mapBooking(row: Record<string, unknown>): CalendarBooking {
|
|
36
|
-
return snakeToCamel(row) as unknown as CalendarBooking
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function mapSchedule(row: Record<string, unknown>): Schedule {
|
|
40
|
-
return snakeToCamel(row) as unknown as Schedule
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function addMinutes(iso: string, minutes: number): string {
|
|
44
|
-
const d = new Date(iso)
|
|
45
|
-
d.setMinutes(d.getMinutes() + minutes)
|
|
46
|
-
return d.toISOString()
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function unique(values: Array<string | null | undefined>): string[] {
|
|
50
|
-
return Array.from(new Set(values.filter((value): value is string => !!value)))
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function indexById(rows: any[] | null | undefined): Map<string, any> {
|
|
54
|
-
return new Map((rows ?? []).filter((row) => row?.id).map((row) => [row.id, row]))
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function groupBy<T>(rows: T[], getKey: (row: T) => string | null | undefined): Map<string, T[]> {
|
|
58
|
-
const grouped = new Map<string, T[]>()
|
|
59
|
-
for (const row of rows) {
|
|
60
|
-
const key = getKey(row)
|
|
61
|
-
if (!key) continue
|
|
62
|
-
grouped.set(key, [...(grouped.get(key) ?? []), row])
|
|
63
|
-
}
|
|
64
|
-
return grouped
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function getDurationMinutes(startsAt: string, endsAt: string | null | undefined): number | null {
|
|
68
|
-
if (!endsAt) return null
|
|
69
|
-
return Math.round((new Date(endsAt).getTime() - new Date(startsAt).getTime()) / 60000)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function mergeBookings(viewBookings: CalendarBooking[], canonicalBookings: CalendarBooking[]): CalendarBooking[] {
|
|
73
|
-
const canonicalIds = new Set(canonicalBookings.map((booking) => booking.id))
|
|
74
|
-
const canonicalOrderIds = new Set(canonicalBookings.map((booking) => booking.orderId).filter(Boolean))
|
|
75
|
-
const merged = [
|
|
76
|
-
...canonicalBookings,
|
|
77
|
-
...viewBookings.filter((booking) => {
|
|
78
|
-
if (canonicalIds.has(booking.id)) return false
|
|
79
|
-
if (booking.orderId && canonicalOrderIds.has(booking.orderId)) return false
|
|
80
|
-
if (canonicalOrderIds.has(booking.id)) return false
|
|
81
|
-
return true
|
|
82
|
-
}),
|
|
83
|
-
]
|
|
84
|
-
|
|
85
|
-
return merged.sort((a, b) => new Date(a.startsAt).getTime() - new Date(b.startsAt).getTime())
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async function hydrateCanonicalBookings(core: any, bookings: any[]): Promise<CalendarBooking[]> {
|
|
89
|
-
const personIds = unique(bookings.flatMap((booking) => [booking.party_id, booking.assignee_id]))
|
|
90
|
-
const locationIds = unique(bookings.map((booking) => booking.location_id))
|
|
91
|
-
const orderIds = unique(bookings.map((booking) => booking.order_id))
|
|
92
|
-
const bookingIds = unique(bookings.map((booking) => booking.id))
|
|
93
|
-
|
|
94
|
-
const [
|
|
95
|
-
{ data: persons, error: personsError },
|
|
96
|
-
{ data: locations, error: locationsError },
|
|
97
|
-
{ data: orders, error: ordersError },
|
|
98
|
-
{ data: items, error: itemsError },
|
|
99
|
-
] = await Promise.all([
|
|
100
|
-
personIds.length
|
|
101
|
-
? core.from('people').select('id, name, phone, email, avatar_url').in('id', personIds)
|
|
102
|
-
: Promise.resolve({ data: [], error: null }),
|
|
103
|
-
locationIds.length
|
|
104
|
-
? core.from('locations').select('id, name').in('id', locationIds)
|
|
105
|
-
: Promise.resolve({ data: [], error: null }),
|
|
106
|
-
orderIds.length
|
|
107
|
-
? core.from('orders').select('id, total, status').in('id', orderIds)
|
|
108
|
-
: Promise.resolve({ data: [], error: null }),
|
|
109
|
-
orderIds.length
|
|
110
|
-
? core.from('order_items').select('id, order_id, service_id, name, duration_minutes, unit_price, assignee_id, sort_order').in('order_id', orderIds)
|
|
111
|
-
: Promise.resolve({ data: [], error: null }),
|
|
112
|
-
])
|
|
113
|
-
|
|
114
|
-
if (personsError) throw personsError
|
|
115
|
-
if (locationsError) throw locationsError
|
|
116
|
-
if (ordersError) throw ordersError
|
|
117
|
-
if (itemsError) throw itemsError
|
|
118
|
-
|
|
119
|
-
const personsById = indexById(persons)
|
|
120
|
-
const locationsById = indexById(locations)
|
|
121
|
-
const ordersById = indexById(orders)
|
|
122
|
-
// S4: line items come from order_items (single source), keyed by order_id.
|
|
123
|
-
const itemsByOrderId = groupBy(items ?? [], (item: any) => item.order_id)
|
|
124
|
-
|
|
125
|
-
return bookings.map((booking) => {
|
|
126
|
-
const client = booking.party_id ? personsById.get(booking.party_id) : null
|
|
127
|
-
const professional = booking.assignee_id ? personsById.get(booking.assignee_id) : null
|
|
128
|
-
const location = booking.location_id ? locationsById.get(booking.location_id) : null
|
|
129
|
-
const order = booking.order_id ? ordersById.get(booking.order_id) : null
|
|
130
|
-
const bookingItems = ((booking.order_id ? itemsByOrderId.get(booking.order_id) : null) ?? [])
|
|
131
|
-
.sort((a: any, b: any) => (a.sort_order ?? 0) - (b.sort_order ?? 0))
|
|
132
|
-
.map((item: any) => ({
|
|
133
|
-
id: item.id,
|
|
134
|
-
serviceId: item.service_id,
|
|
135
|
-
name: item.name,
|
|
136
|
-
durationMinutes: item.duration_minutes,
|
|
137
|
-
price: item.unit_price,
|
|
138
|
-
assigneeId: item.assignee_id,
|
|
139
|
-
}))
|
|
140
|
-
const itemDuration = bookingItems.reduce((sum: number, item: any) => sum + (item.durationMinutes ?? 0), 0)
|
|
141
|
-
|
|
142
|
-
return {
|
|
143
|
-
id: booking.id,
|
|
144
|
-
tenantId: booking.tenant_id,
|
|
145
|
-
kind: booking.kind,
|
|
146
|
-
startsAt: booking.starts_at,
|
|
147
|
-
endsAt: booking.ends_at,
|
|
148
|
-
status: booking.status,
|
|
149
|
-
notes: booking.notes,
|
|
150
|
-
orderId: booking.order_id,
|
|
151
|
-
locationId: booking.location_id,
|
|
152
|
-
metadata: booking.metadata ?? {},
|
|
153
|
-
clientId: booking.party_id,
|
|
154
|
-
// Simple events (no party) fall back to the title stored in metadata;
|
|
155
|
-
// partyless blocks/tasks (e.g. imported Google events) get their notes or
|
|
156
|
-
// a kind label so the calendar never renders an unnamed chip.
|
|
157
|
-
clientName: client?.name
|
|
158
|
-
?? (booking.metadata?.title as string | undefined)
|
|
159
|
-
?? (booking.kind === 'appointment'
|
|
160
|
-
? null
|
|
161
|
-
: booking.notes || (booking.kind === 'task' ? 'Tarefa' : 'Bloqueio')),
|
|
162
|
-
clientPhone: client?.phone ?? null,
|
|
163
|
-
clientEmail: client?.email ?? null,
|
|
164
|
-
clientAvatarUrl: client?.avatar_url ?? null,
|
|
165
|
-
professionalId: booking.assignee_id,
|
|
166
|
-
professionalName: professional?.name ?? null,
|
|
167
|
-
professionalAvatarUrl: professional?.avatar_url ?? null,
|
|
168
|
-
locationName: location?.name ?? null,
|
|
169
|
-
orderTotal: order?.total ?? null,
|
|
170
|
-
orderStatus: order?.status ?? null,
|
|
171
|
-
services: bookingItems.length ? bookingItems : null,
|
|
172
|
-
totalDurationMinutes: itemDuration || getDurationMinutes(booking.starts_at, booking.ends_at),
|
|
173
|
-
createdAt: booking.created_at,
|
|
174
|
-
updatedAt: booking.updated_at,
|
|
175
|
-
} satisfies CalendarBooking
|
|
176
|
-
})
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
async function getCanonicalBookings(core: any, query: BookingQuery): Promise<CalendarBooking[]> {
|
|
180
|
-
let q = core.from('appointments')
|
|
181
|
-
.select('id, tenant_id, kind, starts_at, ends_at, status, notes, order_id, location_id, metadata, party_id, assignee_id, created_at, updated_at')
|
|
182
|
-
.gte('starts_at', query.dateRange.start)
|
|
183
|
-
.lte('starts_at', query.dateRange.end)
|
|
184
|
-
|
|
185
|
-
if (query.kind) q = q.eq('kind', query.kind)
|
|
186
|
-
if (query.statuses?.length) q = q.in('status', query.statuses)
|
|
187
|
-
if (query.professionalIds?.length) q = q.in('assignee_id', query.professionalIds)
|
|
188
|
-
if (query.locationId) q = q.eq('location_id', query.locationId)
|
|
189
|
-
|
|
190
|
-
const { data, error } = await q.order('starts_at', { ascending: true })
|
|
191
|
-
if (error) throw error
|
|
192
|
-
return hydrateCanonicalBookings(core, data ?? [])
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
async function getCanonicalBookingById(core: any, id: string): Promise<CalendarBooking | null> {
|
|
196
|
-
const columns = 'id, tenant_id, kind, starts_at, ends_at, status, notes, order_id, location_id, metadata, party_id, assignee_id, created_at, updated_at'
|
|
197
|
-
let { data, error } = await core.from('appointments').select(columns).eq('id', id).maybeSingle()
|
|
198
|
-
if (error) throw error
|
|
199
|
-
|
|
200
|
-
if (!data) {
|
|
201
|
-
const fallback = await core.from('appointments').select(columns).eq('order_id', id).maybeSingle()
|
|
202
|
-
if (fallback.error) throw fallback.error
|
|
203
|
-
data = fallback.data
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
if (!data) return null
|
|
207
|
-
const [booking] = await hydrateCanonicalBookings(core, [data])
|
|
208
|
-
return booking ?? null
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// ---------------------------------------------------------------------------
|
|
212
|
-
// Supabase Agenda Provider
|
|
213
|
-
// ---------------------------------------------------------------------------
|
|
214
|
-
|
|
215
|
-
export interface SupabaseAgendaProviderOptions {
|
|
216
|
-
/** people.kind used for calendar resources/professionals (default 'staff' — schools use 'teacher'). */
|
|
217
|
-
professionalKind?: string
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
export function createSupabaseAgendaProvider(providerOptions?: SupabaseAgendaProviderOptions): AgendaDataProvider {
|
|
221
|
-
const professionalKind = providerOptions?.professionalKind ?? 'staff'
|
|
222
|
-
return {
|
|
223
|
-
// v_appointments is in public schema (PostgREST default)
|
|
224
|
-
async getBookings(query: BookingQuery): Promise<CalendarBooking[]> {
|
|
225
|
-
const { core, pub } = getClients()
|
|
226
|
-
let q = pub.from('v_appointments').select('*')
|
|
227
|
-
.gte('starts_at', query.dateRange.start)
|
|
228
|
-
.lte('starts_at', query.dateRange.end)
|
|
229
|
-
|
|
230
|
-
if (query.kind) q = q.eq('kind', query.kind)
|
|
231
|
-
if (query.statuses?.length) q = q.in('status', query.statuses)
|
|
232
|
-
if (query.professionalIds?.length) q = q.in('professional_id', query.professionalIds)
|
|
233
|
-
if (query.locationId) q = q.eq('location_id', query.locationId)
|
|
234
|
-
|
|
235
|
-
q = q.order('starts_at', { ascending: true })
|
|
236
|
-
|
|
237
|
-
const { data, error } = await q
|
|
238
|
-
if (error) throw error
|
|
239
|
-
const viewBookings = (data ?? []).map(mapBooking)
|
|
240
|
-
const canonicalBookings = await getCanonicalBookings(core, query)
|
|
241
|
-
return mergeBookings(viewBookings, canonicalBookings)
|
|
242
|
-
},
|
|
243
|
-
|
|
244
|
-
async getBookingById(id: string): Promise<CalendarBooking | null> {
|
|
245
|
-
const { core, pub } = getClients()
|
|
246
|
-
const { data, error } = await pub.from('v_appointments').select('*').eq('id', id).maybeSingle()
|
|
247
|
-
if (error) throw error
|
|
248
|
-
if (data) {
|
|
249
|
-
const booking = mapBooking(data as Record<string, unknown>)
|
|
250
|
-
// v_appointments is a FLAT read model — it exposes no line items. The
|
|
251
|
-
// calendar list path merges in the canonical hydration (which carries
|
|
252
|
-
// `services` from order_items), but this single-record lookup used by the
|
|
253
|
-
// edit modal previously returned the bare view row. That dropped
|
|
254
|
-
// `services`, so a booking whose type has `requiresServices` reopened with
|
|
255
|
-
// an empty service list and kept the "Update" button permanently disabled
|
|
256
|
-
// (canSubmit stayed false). Hydrate the line items here to stay consistent
|
|
257
|
-
// with getBookings.
|
|
258
|
-
if (booking.orderId) {
|
|
259
|
-
const { data: items } = await core.from('order_items')
|
|
260
|
-
.select('id, service_id, name, duration_minutes, unit_price, assignee_id, sort_order')
|
|
261
|
-
.eq('order_id', booking.orderId)
|
|
262
|
-
.order('sort_order', { ascending: true })
|
|
263
|
-
if (items && items.length) {
|
|
264
|
-
const services = items.map((item: any) => ({
|
|
265
|
-
id: item.id,
|
|
266
|
-
serviceId: item.service_id,
|
|
267
|
-
name: item.name,
|
|
268
|
-
durationMinutes: item.duration_minutes,
|
|
269
|
-
price: item.unit_price,
|
|
270
|
-
assigneeId: item.assignee_id,
|
|
271
|
-
}))
|
|
272
|
-
booking.services = services
|
|
273
|
-
const itemDuration = services.reduce((sum: number, s: any) => sum + (s.durationMinutes ?? 0), 0)
|
|
274
|
-
if (itemDuration) booking.totalDurationMinutes = itemDuration
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
return booking
|
|
278
|
-
}
|
|
279
|
-
return getCanonicalBookingById(core, id)
|
|
280
|
-
},
|
|
281
|
-
|
|
282
|
-
async createBooking(input: CreateBookingInput): Promise<CalendarBooking> {
|
|
283
|
-
const { core } = getClients()
|
|
284
|
-
const tenantId = getTenantId()
|
|
285
|
-
if (!tenantId) throw new Error('No active tenant')
|
|
286
|
-
|
|
287
|
-
const hasServices = input.services.length > 0
|
|
288
|
-
const totalDuration = input.services.reduce((sum, s) => sum + s.durationMinutes, 0)
|
|
289
|
-
const totalPrice = input.services.reduce((sum, s) => sum + s.price, 0)
|
|
290
|
-
const endsAt = addMinutes(input.startsAt, totalDuration || 30)
|
|
291
|
-
const serviceNames = input.services.map((s: any) => s.name).join(', ')
|
|
292
|
-
|
|
293
|
-
// Resolve client name for metadata (used by financial detail view)
|
|
294
|
-
let contactName: string | undefined
|
|
295
|
-
if (input.clientId) {
|
|
296
|
-
const { data: person } = await core.from('people').select('name').eq('id', input.clientId).single()
|
|
297
|
-
contactName = person?.name as string | undefined
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Simple / Google-Calendar-style events have no client party — persist the
|
|
301
|
-
// free-text title in metadata so the read model can surface it as the name.
|
|
302
|
-
const eventTitle = input.title?.trim() || undefined
|
|
303
|
-
|
|
304
|
-
const { data: order, error: orderErr } = await core.from('orders').insert({
|
|
305
|
-
tenant_id: tenantId,
|
|
306
|
-
kind: input.kind ?? 'appointment',
|
|
307
|
-
status: 'scheduled',
|
|
308
|
-
party_id: input.clientId || null,
|
|
309
|
-
assignee_id: input.professionalId || null,
|
|
310
|
-
location_id: input.locationId ?? null,
|
|
311
|
-
subtotal: totalPrice,
|
|
312
|
-
total: totalPrice,
|
|
313
|
-
notes: input.notes ?? null,
|
|
314
|
-
metadata: {
|
|
315
|
-
source: 'agenda',
|
|
316
|
-
...(serviceNames ? { serviceNames, itemsSummary: serviceNames } : {}),
|
|
317
|
-
...(contactName ? { contactName } : {}),
|
|
318
|
-
...(eventTitle ? { title: eventTitle } : {}),
|
|
319
|
-
},
|
|
320
|
-
}).select('id').single()
|
|
321
|
-
if (orderErr) throw orderErr
|
|
322
|
-
|
|
323
|
-
const { data: booking, error: bookingErr } = await core.from('appointments').insert({
|
|
324
|
-
tenant_id: tenantId,
|
|
325
|
-
kind: input.kind ?? 'appointment',
|
|
326
|
-
party_id: input.clientId || null,
|
|
327
|
-
assignee_id: input.professionalId || null,
|
|
328
|
-
location_id: input.locationId ?? null,
|
|
329
|
-
order_id: order.id,
|
|
330
|
-
starts_at: input.startsAt,
|
|
331
|
-
ends_at: endsAt,
|
|
332
|
-
status: 'scheduled',
|
|
333
|
-
notes: input.notes ?? null,
|
|
334
|
-
metadata: {
|
|
335
|
-
source: 'agenda',
|
|
336
|
-
...(serviceNames ? { serviceNames, itemsSummary: serviceNames } : {}),
|
|
337
|
-
...(contactName ? { contactName } : {}),
|
|
338
|
-
...(eventTitle ? { title: eventTitle } : {}),
|
|
339
|
-
},
|
|
340
|
-
}).select('id').single()
|
|
341
|
-
if (bookingErr) throw bookingErr
|
|
342
|
-
|
|
343
|
-
if (hasServices) {
|
|
344
|
-
// S4: line items live ONLY in order_items now (booking_items deprecated).
|
|
345
|
-
const orderItems = input.services.map((s, i) => ({
|
|
346
|
-
order_id: order.id,
|
|
347
|
-
service_id: s.serviceId,
|
|
348
|
-
name: s.name,
|
|
349
|
-
quantity: 1,
|
|
350
|
-
unit_price: s.price,
|
|
351
|
-
total: s.price,
|
|
352
|
-
sort_order: i,
|
|
353
|
-
duration_minutes: s.durationMinutes,
|
|
354
|
-
assignee_id: s.assigneeId || input.professionalId || null,
|
|
355
|
-
}))
|
|
356
|
-
const { error: oiErr } = await core.from('order_items').insert(orderItems)
|
|
357
|
-
if (oiErr) throw oiErr
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
const createdBooking = await this.getBookingById(booking.id)
|
|
361
|
-
if (!createdBooking) throw new Error('Created booking did not resolve from agenda read model')
|
|
362
|
-
return createdBooking
|
|
363
|
-
},
|
|
364
|
-
|
|
365
|
-
async updateBooking(id: string, data: UpdateBookingInput): Promise<CalendarBooking> {
|
|
366
|
-
const { core } = getClients()
|
|
367
|
-
const { data: existing, error: existingErr } = await core.from('appointments')
|
|
368
|
-
.select('id, order_id, starts_at, assignee_id, metadata')
|
|
369
|
-
.eq('id', id)
|
|
370
|
-
.single()
|
|
371
|
-
if (existingErr) throw existingErr
|
|
372
|
-
|
|
373
|
-
const updates: Record<string, unknown> = {}
|
|
374
|
-
if (data.startsAt) updates.starts_at = data.startsAt
|
|
375
|
-
if (data.endsAt) updates.ends_at = data.endsAt
|
|
376
|
-
// Simple-event title lives in metadata (no client party) — merge, don't clobber.
|
|
377
|
-
if (data.title !== undefined) {
|
|
378
|
-
updates.metadata = { ...(existing?.metadata as Record<string, unknown> ?? {}), title: data.title }
|
|
379
|
-
}
|
|
380
|
-
if (data.status) {
|
|
381
|
-
// Only update the agenda status — never touch the financial stage
|
|
382
|
-
updates.status = data.status
|
|
383
|
-
}
|
|
384
|
-
if (data.notes !== undefined) updates.notes = data.notes
|
|
385
|
-
if (data.professionalId) updates.assignee_id = data.professionalId
|
|
386
|
-
if (data.clientId) updates.party_id = data.clientId
|
|
387
|
-
if (data.locationId) updates.location_id = data.locationId
|
|
388
|
-
|
|
389
|
-
// Recalculate totals when services are updated
|
|
390
|
-
if (data.services) {
|
|
391
|
-
const totalDuration = data.services.reduce((s, svc) => s + svc.durationMinutes, 0)
|
|
392
|
-
const totalPrice = data.services.reduce((s, svc) => s + svc.price, 0)
|
|
393
|
-
// Recalculate ends_at from startsAt + total duration
|
|
394
|
-
const startsAt = data.startsAt ?? (existing?.starts_at as string | undefined)
|
|
395
|
-
if (startsAt) {
|
|
396
|
-
const ends = new Date(new Date(startsAt).getTime() + totalDuration * 60000)
|
|
397
|
-
updates.ends_at = ends.toISOString()
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
const effectiveAssigneeId = data.professionalId ?? (existing?.assignee_id as string | null | undefined) ?? null
|
|
402
|
-
|
|
403
|
-
const { error } = await core.from('appointments').update(updates).eq('id', id)
|
|
404
|
-
if (error) throw error
|
|
405
|
-
|
|
406
|
-
// S4: services are updated only in order_items below (booking_items deprecated).
|
|
407
|
-
const orderId = existing?.order_id as string | null | undefined
|
|
408
|
-
if (orderId) {
|
|
409
|
-
const orderUpdates: Record<string, unknown> = {}
|
|
410
|
-
if (data.notes !== undefined) orderUpdates.notes = data.notes
|
|
411
|
-
if (data.professionalId) orderUpdates.assignee_id = data.professionalId
|
|
412
|
-
if (data.clientId) orderUpdates.party_id = data.clientId
|
|
413
|
-
if (data.locationId) orderUpdates.location_id = data.locationId
|
|
414
|
-
|
|
415
|
-
if (data.services) {
|
|
416
|
-
const totalPrice = data.services.reduce((s, svc) => s + svc.price, 0)
|
|
417
|
-
orderUpdates.subtotal = totalPrice
|
|
418
|
-
orderUpdates.total = totalPrice
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
if (Object.keys(orderUpdates).length > 0) {
|
|
422
|
-
const { error: orderErr } = await core.from('orders').update(orderUpdates).eq('id', orderId)
|
|
423
|
-
if (orderErr) throw orderErr
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
if (data.services) {
|
|
427
|
-
await core.from('order_items').delete().eq('order_id', orderId)
|
|
428
|
-
if (data.services.length > 0) {
|
|
429
|
-
const items = data.services.map((svc, idx) => ({
|
|
430
|
-
order_id: orderId,
|
|
431
|
-
service_id: svc.serviceId || null,
|
|
432
|
-
name: svc.name,
|
|
433
|
-
quantity: 1,
|
|
434
|
-
unit_price: svc.price,
|
|
435
|
-
total: svc.price,
|
|
436
|
-
sort_order: idx,
|
|
437
|
-
duration_minutes: svc.durationMinutes,
|
|
438
|
-
assignee_id: svc.assigneeId ?? effectiveAssigneeId,
|
|
439
|
-
}))
|
|
440
|
-
const { error: itemErr } = await core.from('order_items').insert(items)
|
|
441
|
-
if (itemErr) throw itemErr
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
return (await this.getBookingById(id))!
|
|
447
|
-
},
|
|
448
|
-
|
|
449
|
-
async deleteBooking(id: string): Promise<void> {
|
|
450
|
-
const { core, pub } = getClients()
|
|
451
|
-
const { data: booking, error: bookingErr } = await core.from('appointments')
|
|
452
|
-
.select('order_id')
|
|
453
|
-
.eq('id', id)
|
|
454
|
-
.single()
|
|
455
|
-
if (bookingErr) throw bookingErr
|
|
456
|
-
|
|
457
|
-
const orderId = booking?.order_id as string | null | undefined
|
|
458
|
-
// Financial movements live in the financial plugin's tables. Pools that
|
|
459
|
-
// don't provision that plugin (e.g. school/event agendas) have no
|
|
460
|
-
// plg_financial_movements table, and the probe below returns a PostgREST
|
|
461
|
-
// "relation does not exist" error. Previously that error was rethrown,
|
|
462
|
-
// which aborted the ENTIRE delete — the modal caught it and left the
|
|
463
|
-
// booking in place, so "Delete" looked like a silent no-op (works in
|
|
464
|
-
// beauty, which has the table; fails in school, which doesn't). Treat any
|
|
465
|
-
// probe failure as "no linked movements" and fall through to a plain delete.
|
|
466
|
-
let movementCount = 0
|
|
467
|
-
if (orderId) {
|
|
468
|
-
const { count, error: movementErr } = await pub
|
|
469
|
-
.from('plg_financial_movements')
|
|
470
|
-
.select('id', { count: 'exact', head: true })
|
|
471
|
-
.eq('invoice_id', orderId)
|
|
472
|
-
if (!movementErr) movementCount = count ?? 0
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
if (orderId && movementCount > 0) {
|
|
476
|
-
// Cancel movements + order (don't delete financial records)
|
|
477
|
-
await pub.from('plg_financial_movements').update({ status: 'cancelled' }).eq('invoice_id', orderId)
|
|
478
|
-
await core.from('orders').update({ status: 'cancelled' }).eq('id', orderId)
|
|
479
|
-
const { error } = await core.from('appointments').update({ status: 'cancelled' }).eq('id', id)
|
|
480
|
-
if (error) throw error
|
|
481
|
-
} else {
|
|
482
|
-
// Safe to delete — remove any movements first (FK constraint)
|
|
483
|
-
if (orderId) {
|
|
484
|
-
await pub.from('plg_financial_movements').delete().eq('invoice_id', orderId)
|
|
485
|
-
}
|
|
486
|
-
const { error } = await core.from('appointments').delete().eq('id', id)
|
|
487
|
-
if (error) throw error
|
|
488
|
-
if (orderId) {
|
|
489
|
-
const { error: orderErr } = await core.from('orders').delete().eq('id', orderId)
|
|
490
|
-
if (orderErr) throw orderErr
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
},
|
|
494
|
-
|
|
495
|
-
async updateBookingStatus(id: string, status: string): Promise<CalendarBooking> {
|
|
496
|
-
return this.updateBooking(id, { status: status as BookingStatus })
|
|
497
|
-
},
|
|
498
|
-
|
|
499
|
-
async checkConflict(params: ConflictCheckParams): Promise<boolean> {
|
|
500
|
-
const { core } = getClients()
|
|
501
|
-
let q = core.from('appointments').select('id', { count: 'exact', head: true })
|
|
502
|
-
.eq('assignee_id', params.assigneeId)
|
|
503
|
-
.eq('kind', 'appointment')
|
|
504
|
-
.not('status', 'in', '("cancelled","no_show")')
|
|
505
|
-
.not('starts_at', 'is', null)
|
|
506
|
-
.lt('starts_at', params.endsAt)
|
|
507
|
-
.gt('ends_at', params.startsAt)
|
|
508
|
-
|
|
509
|
-
if (params.excludeBookingId) {
|
|
510
|
-
q = q.neq('id', params.excludeBookingId)
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
const { count, error } = await q
|
|
514
|
-
if (error) throw error
|
|
515
|
-
return (count ?? 0) > 0
|
|
516
|
-
},
|
|
517
|
-
|
|
518
|
-
async getAvailableSlots(params: AvailableSlotsParams): Promise<TimeSlot[]> {
|
|
519
|
-
const { pub } = getClients()
|
|
520
|
-
const { data, error } = await pub.rpc('get_available_slots', {
|
|
521
|
-
p_tenant_id: getTenantId(),
|
|
522
|
-
p_assignee_id: params.assigneeId,
|
|
523
|
-
p_date: params.date,
|
|
524
|
-
p_duration_minutes: params.durationMinutes,
|
|
525
|
-
p_slot_interval: params.slotInterval ?? 30,
|
|
526
|
-
})
|
|
527
|
-
if (error) throw error
|
|
528
|
-
return (data ?? []).map((row: any) => ({ start: row.slot_start, end: row.slot_end }))
|
|
529
|
-
},
|
|
530
|
-
|
|
531
|
-
async getSchedules(professionalId?: string): Promise<Schedule[]> {
|
|
532
|
-
const { core } = getClients()
|
|
533
|
-
let q = core.from('schedules').select('*').eq('kind', 'working_hours')
|
|
534
|
-
if (professionalId) q = q.eq('assignee_id', professionalId)
|
|
535
|
-
q = q.order('day_of_week').order('starts_at')
|
|
536
|
-
|
|
537
|
-
const { data, error } = await q
|
|
538
|
-
if (error) throw error
|
|
539
|
-
return (data ?? []).map(mapSchedule)
|
|
540
|
-
},
|
|
541
|
-
|
|
542
|
-
async saveSchedule(input: SaveScheduleInput): Promise<Schedule> {
|
|
543
|
-
const { core } = getClients()
|
|
544
|
-
const tenantId = getTenantId()
|
|
545
|
-
if (!tenantId) throw new Error('No active tenant')
|
|
546
|
-
|
|
547
|
-
const { data, error } = await core.from('schedules').insert({
|
|
548
|
-
tenant_id: tenantId,
|
|
549
|
-
kind: 'working_hours',
|
|
550
|
-
assignee_id: input.assigneeId,
|
|
551
|
-
location_id: input.locationId ?? null,
|
|
552
|
-
day_of_week: input.dayOfWeek ?? null,
|
|
553
|
-
specific_date: input.specificDate ?? null,
|
|
554
|
-
starts_at: input.startsAt,
|
|
555
|
-
ends_at: input.endsAt,
|
|
556
|
-
is_active: input.isActive !== false,
|
|
557
|
-
metadata: input.metadata ?? {},
|
|
558
|
-
}).select('*').single()
|
|
559
|
-
if (error) throw error
|
|
560
|
-
return mapSchedule(data as Record<string, unknown>)
|
|
561
|
-
},
|
|
562
|
-
|
|
563
|
-
async deleteSchedule(id: string): Promise<void> {
|
|
564
|
-
const { core } = getClients()
|
|
565
|
-
const { error } = await core.from('schedules').delete().eq('id', id)
|
|
566
|
-
if (error) throw error
|
|
567
|
-
},
|
|
568
|
-
|
|
569
|
-
async getProfessionals(): Promise<Professional[]> {
|
|
570
|
-
const { core } = getClients()
|
|
571
|
-
const { data, error } = await core.from('people')
|
|
572
|
-
.select('id, name, avatar_url, is_active')
|
|
573
|
-
.eq('kind', professionalKind)
|
|
574
|
-
.eq('is_active', true)
|
|
575
|
-
.order('name')
|
|
576
|
-
if (error) throw error
|
|
577
|
-
return (data ?? []).map((p: any) => ({
|
|
578
|
-
id: p.id, name: p.name, avatarUrl: p.avatar_url,
|
|
579
|
-
locationId: null, locationName: null, isActive: p.is_active,
|
|
580
|
-
}))
|
|
581
|
-
},
|
|
582
|
-
|
|
583
|
-
async getLocations(): Promise<Array<{ id: string; name: string }>> {
|
|
584
|
-
const { core } = getClients()
|
|
585
|
-
const { data, error } = await core.from('locations')
|
|
586
|
-
.select('id, name')
|
|
587
|
-
.eq('is_active', true)
|
|
588
|
-
.order('name')
|
|
589
|
-
if (error) throw error
|
|
590
|
-
return (data ?? []).map((l: any) => ({ id: l.id, name: l.name }))
|
|
591
|
-
},
|
|
592
|
-
|
|
593
|
-
async getConfirmationsPending(daysAhead = 2): Promise<CalendarBooking[]> {
|
|
594
|
-
const { pub } = getClients()
|
|
595
|
-
const now = new Date().toISOString()
|
|
596
|
-
const cutoff = new Date()
|
|
597
|
-
cutoff.setDate(cutoff.getDate() + daysAhead)
|
|
598
|
-
|
|
599
|
-
const { data, error } = await pub.from('v_appointments').select('*')
|
|
600
|
-
.eq('status', 'scheduled')
|
|
601
|
-
.gte('starts_at', now)
|
|
602
|
-
.lte('starts_at', cutoff.toISOString())
|
|
603
|
-
.order('starts_at')
|
|
604
|
-
if (error) throw error
|
|
605
|
-
return (data ?? []).map(mapBooking)
|
|
606
|
-
},
|
|
607
|
-
|
|
608
|
-
async sendConfirmation(bookingId: string, channel: string): Promise<void> {
|
|
609
|
-
const { core } = getClients()
|
|
610
|
-
const { data: booking } = await core.from('appointments').select('metadata').eq('id', bookingId).single()
|
|
611
|
-
const metadata = (booking?.metadata ?? {}) as Record<string, unknown>
|
|
612
|
-
const confirmations = (metadata.confirmations as any[] ?? [])
|
|
613
|
-
confirmations.push({ channel, sentAt: new Date().toISOString(), status: 'sent' })
|
|
614
|
-
await core.from('appointments').update({ metadata: { ...metadata, confirmations } }).eq('id', bookingId)
|
|
615
|
-
},
|
|
616
|
-
|
|
617
|
-
async completeBooking(bookingId: string): Promise<{ booking: CalendarBooking; orderId: string }> {
|
|
618
|
-
const { core } = getClients()
|
|
619
|
-
const { data: existing, error: existingErr } = await core.from('appointments')
|
|
620
|
-
.select('order_id')
|
|
621
|
-
.eq('id', bookingId)
|
|
622
|
-
.single()
|
|
623
|
-
if (existingErr) throw existingErr
|
|
624
|
-
|
|
625
|
-
const { error } = await core.from('appointments')
|
|
626
|
-
.update({ status: 'completed' })
|
|
627
|
-
.eq('id', bookingId)
|
|
628
|
-
if (error) throw error
|
|
629
|
-
|
|
630
|
-
const orderId = existing?.order_id as string | null | undefined
|
|
631
|
-
const booking = await this.getBookingById(bookingId)
|
|
632
|
-
if (!booking) throw new Error('Booking not found')
|
|
633
|
-
return { booking, orderId: orderId ?? bookingId }
|
|
634
|
-
},
|
|
635
|
-
}
|
|
636
|
-
}
|
package/src/data/types.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
CalendarBooking, Professional, Schedule, TimeSlot,
|
|
3
|
-
CreateBookingInput, UpdateBookingInput, SaveScheduleInput,
|
|
4
|
-
BookingQuery, ConflictCheckParams, AvailableSlotsParams,
|
|
5
|
-
} from '../types'
|
|
6
|
-
|
|
7
|
-
export interface AgendaDataProvider {
|
|
8
|
-
// --- Bookings (calendar events) ---
|
|
9
|
-
getBookings(query: BookingQuery): Promise<CalendarBooking[]>
|
|
10
|
-
getBookingById(id: string): Promise<CalendarBooking | null>
|
|
11
|
-
createBooking(input: CreateBookingInput): Promise<CalendarBooking>
|
|
12
|
-
updateBooking(id: string, data: UpdateBookingInput): Promise<CalendarBooking>
|
|
13
|
-
deleteBooking(id: string): Promise<void>
|
|
14
|
-
updateBookingStatus(id: string, status: string): Promise<CalendarBooking>
|
|
15
|
-
|
|
16
|
-
// --- Conflict detection ---
|
|
17
|
-
checkConflict(params: ConflictCheckParams): Promise<boolean>
|
|
18
|
-
getAvailableSlots(params: AvailableSlotsParams): Promise<TimeSlot[]>
|
|
19
|
-
|
|
20
|
-
// --- Schedules (working hours) ---
|
|
21
|
-
getSchedules(professionalId?: string): Promise<Schedule[]>
|
|
22
|
-
saveSchedule(input: SaveScheduleInput): Promise<Schedule>
|
|
23
|
-
deleteSchedule(id: string): Promise<void>
|
|
24
|
-
|
|
25
|
-
// --- Professionals (resources) ---
|
|
26
|
-
getProfessionals(): Promise<Professional[]>
|
|
27
|
-
|
|
28
|
-
// --- Locations ---
|
|
29
|
-
getLocations?(): Promise<Array<{ id: string; name: string }>>
|
|
30
|
-
|
|
31
|
-
// --- Confirmations ---
|
|
32
|
-
getConfirmationsPending(daysAhead?: number): Promise<CalendarBooking[]>
|
|
33
|
-
sendConfirmation(bookingId: string, channel: string): Promise<void>
|
|
34
|
-
|
|
35
|
-
// --- Financial bridge ---
|
|
36
|
-
completeBooking(bookingId: string): Promise<{ booking: CalendarBooking; orderId: string }>
|
|
37
|
-
}
|