@fayz-ai/plugin-agenda 0.9.0 → 0.10.0

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 (196) hide show
  1. package/README.md +3 -3
  2. package/dist/bridges/create-financial-bridge.d.ts +1 -1
  3. package/dist/bridges/create-financial-bridge.d.ts.map +1 -1
  4. package/dist/bridges/financial-bridge.d.ts.map +1 -0
  5. package/dist/{chunk-Q2PAWBPR.js → chunk-ISLJXP7O.js} +7 -2
  6. package/dist/chunk-ISLJXP7O.js.map +1 -0
  7. package/dist/components/AppointmentModal.d.ts.map +1 -1
  8. package/dist/components/PaymentStatusBadge.d.ts +1 -1
  9. package/dist/components/PaymentStatusBadge.d.ts.map +1 -1
  10. package/dist/config.d.ts +2 -2
  11. package/dist/config.d.ts.map +1 -1
  12. package/dist/{AgendaContext.d.ts → context.d.ts} +1 -1
  13. package/dist/context.d.ts.map +1 -0
  14. package/dist/data/mock.d.ts.map +1 -1
  15. package/dist/data/realtime.test.d.ts +2 -0
  16. package/dist/data/realtime.test.d.ts.map +1 -0
  17. package/dist/data/registries.d.ts.map +1 -0
  18. package/dist/data/supabase.d.ts.map +1 -1
  19. package/dist/data/types.d.ts +7 -0
  20. package/dist/data/types.d.ts.map +1 -1
  21. package/dist/index.d.ts +2 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +2515 -187
  24. package/dist/index.js.map +1 -1
  25. package/dist/integrations/google-calendar/conflict.d.ts +25 -0
  26. package/dist/integrations/google-calendar/conflict.d.ts.map +1 -0
  27. package/dist/integrations/google-calendar/connectorDef.d.ts +12 -0
  28. package/dist/integrations/google-calendar/connectorDef.d.ts.map +1 -1
  29. package/dist/integrations/google-calendar/credentials.d.ts +27 -0
  30. package/dist/integrations/google-calendar/credentials.d.ts.map +1 -0
  31. package/dist/integrations/google-calendar/data/supabase.d.ts +10 -2
  32. package/dist/integrations/google-calendar/data/supabase.d.ts.map +1 -1
  33. package/dist/integrations/google-calendar/data/tables.d.ts +2 -0
  34. package/dist/integrations/google-calendar/data/tables.d.ts.map +1 -1
  35. package/dist/integrations/google-calendar/health.d.ts +23 -0
  36. package/dist/integrations/google-calendar/health.d.ts.map +1 -0
  37. package/dist/integrations/google-calendar/health.test.d.ts +2 -0
  38. package/dist/integrations/google-calendar/health.test.d.ts.map +1 -0
  39. package/dist/integrations/google-calendar/index.d.ts +6 -0
  40. package/dist/integrations/google-calendar/index.d.ts.map +1 -1
  41. package/dist/integrations/google-calendar/migrations/index.d.ts +12 -0
  42. package/dist/integrations/google-calendar/migrations/index.d.ts.map +1 -0
  43. package/dist/integrations/google-calendar/panel-data.d.ts +26 -0
  44. package/dist/integrations/google-calendar/panel-data.d.ts.map +1 -0
  45. package/dist/integrations/google-calendar/panel-data.test.d.ts +2 -0
  46. package/dist/integrations/google-calendar/panel-data.test.d.ts.map +1 -0
  47. package/dist/integrations/google-calendar/types.d.ts +17 -0
  48. package/dist/integrations/google-calendar/types.d.ts.map +1 -1
  49. package/dist/lib/booking-events.d.ts +5 -0
  50. package/dist/lib/booking-events.d.ts.map +1 -0
  51. package/dist/lib/calendar-events.d.ts +8 -0
  52. package/dist/lib/calendar-events.d.ts.map +1 -0
  53. package/dist/lib/calendar-events.test.d.ts +2 -0
  54. package/dist/lib/calendar-events.test.d.ts.map +1 -0
  55. package/dist/lib/message-events.d.ts +12 -0
  56. package/dist/lib/message-events.d.ts.map +1 -0
  57. package/dist/locales/en.d.ts.map +1 -1
  58. package/dist/locales/pt-BR.d.ts.map +1 -1
  59. package/dist/public/{data.d.ts → data/index.d.ts} +4 -4
  60. package/dist/public/data/index.d.ts.map +1 -0
  61. package/dist/public/{data.supabase.d.ts → data/supabase.d.ts} +2 -2
  62. package/dist/public/data/supabase.d.ts.map +1 -0
  63. package/dist/public/{hooks.d.ts → hooks/index.d.ts} +4 -4
  64. package/dist/public/hooks/index.d.ts.map +1 -0
  65. package/dist/public/index.d.ts +4 -4
  66. package/dist/public/index.d.ts.map +1 -1
  67. package/dist/public/index.js +3 -3
  68. package/dist/public/index.js.map +1 -1
  69. package/dist/public/{format.d.ts → lib/format.d.ts} +1 -1
  70. package/dist/public/lib/format.d.ts.map +1 -0
  71. package/dist/public/views/BookingWidget.d.ts.map +1 -0
  72. package/dist/public/{steps → views/steps}/ContactStep.d.ts +1 -1
  73. package/dist/public/views/steps/ContactStep.d.ts.map +1 -0
  74. package/dist/public/{steps → views/steps}/DateTimeStep.d.ts +3 -3
  75. package/dist/public/views/steps/DateTimeStep.d.ts.map +1 -0
  76. package/dist/public/{steps → views/steps}/OverviewStep.d.ts +1 -1
  77. package/dist/public/views/steps/OverviewStep.d.ts.map +1 -0
  78. package/dist/public/{steps → views/steps}/PaymentStep.d.ts +1 -1
  79. package/dist/public/views/steps/PaymentStep.d.ts.map +1 -0
  80. package/dist/store.d.ts +1 -1
  81. package/dist/store.d.ts.map +1 -1
  82. package/dist/styles/calendar-styles.d.ts.map +1 -0
  83. package/dist/types.d.ts +1 -1
  84. package/dist/types.d.ts.map +1 -1
  85. package/dist/{AgendaPage.d.ts → views/AgendaPage.d.ts} +3 -3
  86. package/dist/views/AgendaPage.d.ts.map +1 -0
  87. package/dist/views/CalendarView.d.ts.map +1 -1
  88. package/package.json +9 -11
  89. package/dist/AgendaContext.d.ts.map +0 -1
  90. package/dist/AgendaPage.d.ts.map +0 -1
  91. package/dist/calendar-styles.d.ts.map +0 -1
  92. package/dist/chunk-Q2PAWBPR.js.map +0 -1
  93. package/dist/financial-bridge.d.ts.map +0 -1
  94. package/dist/integrations/google-calendar/GoogleCalendarSettings.d.ts +0 -3
  95. package/dist/integrations/google-calendar/GoogleCalendarSettings.d.ts.map +0 -1
  96. package/dist/public/BookingWidget.d.ts.map +0 -1
  97. package/dist/public/data.d.ts.map +0 -1
  98. package/dist/public/data.supabase.d.ts.map +0 -1
  99. package/dist/public/format.d.ts.map +0 -1
  100. package/dist/public/hooks.d.ts.map +0 -1
  101. package/dist/public/steps/ContactStep.d.ts.map +0 -1
  102. package/dist/public/steps/DateTimeStep.d.ts.map +0 -1
  103. package/dist/public/steps/OverviewStep.d.ts.map +0 -1
  104. package/dist/public/steps/PaymentStep.d.ts.map +0 -1
  105. package/dist/registries.d.ts.map +0 -1
  106. package/src/AgendaContext.tsx +0 -11
  107. package/src/AgendaPage.tsx +0 -33
  108. package/src/bridges/create-financial-bridge.ts +0 -296
  109. package/src/calendar-styles.css +0 -245
  110. package/src/calendar-styles.ts +0 -285
  111. package/src/components/AgendaGeneralSettings.tsx +0 -86
  112. package/src/components/AgendaMobileList.tsx +0 -103
  113. package/src/components/AppointmentModal.tsx +0 -1050
  114. package/src/components/AppointmentPopover.tsx +0 -231
  115. package/src/components/BookingPaymentPanel.tsx +0 -394
  116. package/src/components/CalendarSidebar.tsx +0 -119
  117. package/src/components/CalendarToolbar.tsx +0 -86
  118. package/src/components/EventContextMenu.tsx +0 -125
  119. package/src/components/FloatingPanel.tsx +0 -123
  120. package/src/components/MiniCalendar.tsx +0 -120
  121. package/src/components/PaymentStatusBadge.tsx +0 -62
  122. package/src/components/StatusBadge.tsx +0 -23
  123. package/src/config.ts +0 -368
  124. package/src/data/index.ts +0 -4
  125. package/src/data/mock.ts +0 -344
  126. package/src/data/supabase.ts +0 -636
  127. package/src/data/types.ts +0 -37
  128. package/src/financial-bridge.ts +0 -96
  129. package/src/hooks/useAgendaSettings.ts +0 -17
  130. package/src/index.ts +0 -297
  131. package/src/integrations/google-calendar/GoogleCalendarSettings.tsx +0 -136
  132. package/src/integrations/google-calendar/components/CalendarColorDot.tsx +0 -23
  133. package/src/integrations/google-calendar/components/ChannelMappingList.tsx +0 -329
  134. package/src/integrations/google-calendar/components/ImportEventsWizard.tsx +0 -215
  135. package/src/integrations/google-calendar/components/useAgendaLookups.ts +0 -100
  136. package/src/integrations/google-calendar/connector.ts +0 -20
  137. package/src/integrations/google-calendar/connectorDef.tsx +0 -185
  138. package/src/integrations/google-calendar/data/supabase.ts +0 -170
  139. package/src/integrations/google-calendar/data/tables.ts +0 -12
  140. package/src/integrations/google-calendar/index.ts +0 -45
  141. package/src/integrations/google-calendar/integration.test.ts +0 -199
  142. package/src/integrations/google-calendar/mapping.ts +0 -183
  143. package/src/integrations/google-calendar/migrations/001_google_calendar.sql +0 -97
  144. package/src/integrations/google-calendar/migrations/002_smart_sync.sql +0 -517
  145. package/src/integrations/google-calendar/migrations/003_plg_rename.sql +0 -273
  146. package/src/integrations/google-calendar/types.ts +0 -142
  147. package/src/lib/tenant.ts +0 -3
  148. package/src/locales/en.ts +0 -220
  149. package/src/locales/index.ts +0 -7
  150. package/src/locales/pt-BR.ts +0 -220
  151. package/src/migrations/000b_v_bookings_rename.sql +0 -21
  152. package/src/migrations/000c_dedupe_customer_phones.sql +0 -43
  153. package/src/migrations/001_public_booking.sql +0 -296
  154. package/src/migrations/002_v_bookings_compat.sql +0 -14
  155. package/src/migrations/003_public_client_kind.sql +0 -175
  156. package/src/migrations/004_public_booking_assignee.sql +0 -176
  157. package/src/migrations/005_agent_rpcs.sql +0 -168
  158. package/src/migrations/006_agent_rpc_localtime.sql +0 -181
  159. package/src/migrations/007_schedule_exception_precedence.sql +0 -290
  160. package/src/migrations/008_agent_rpc_exception_precedence.sql +0 -172
  161. package/src/migrations/009_public_booking_location.sql +0 -162
  162. package/src/migrations/010_public_service_curation.sql +0 -31
  163. package/src/migrations/011_default_assignee.sql +0 -167
  164. package/src/migrations/012_drop_legacy_booking_overload.sql +0 -29
  165. package/src/migrations/013_normalize_v_appointments.sql +0 -199
  166. package/src/migrations/014_agent_rpc_update_appointment.sql +0 -199
  167. package/src/migrations/index.ts +0 -2377
  168. package/src/public/BookingWidget.tsx +0 -324
  169. package/src/public/components/BrandHeader.tsx +0 -27
  170. package/src/public/components/ServiceCard.tsx +0 -39
  171. package/src/public/components/SidebarExtras.tsx +0 -30
  172. package/src/public/components/StepRailItem.tsx +0 -27
  173. package/src/public/components/SuccessPanel.tsx +0 -27
  174. package/src/public/components/SummaryRail.tsx +0 -90
  175. package/src/public/components/defaults.ts +0 -14
  176. package/src/public/context.tsx +0 -103
  177. package/src/public/createPublicBookingPlugin.ts +0 -281
  178. package/src/public/data.supabase.ts +0 -97
  179. package/src/public/data.ts +0 -105
  180. package/src/public/format.ts +0 -96
  181. package/src/public/hooks.ts +0 -110
  182. package/src/public/index.ts +0 -69
  183. package/src/public/steps/ContactStep.tsx +0 -184
  184. package/src/public/steps/DateTimeStep.tsx +0 -138
  185. package/src/public/steps/OverviewStep.tsx +0 -81
  186. package/src/public/steps/PaymentStep.tsx +0 -180
  187. package/src/public/types.ts +0 -250
  188. package/src/registries.ts +0 -35
  189. package/src/store.ts +0 -311
  190. package/src/types.ts +0 -273
  191. package/src/views/CalendarView.tsx +0 -1112
  192. package/src/views/ConfirmationsView.tsx +0 -155
  193. /package/dist/{financial-bridge.d.ts → bridges/financial-bridge.d.ts} +0 -0
  194. /package/dist/{registries.d.ts → data/registries.d.ts} +0 -0
  195. /package/dist/public/{BookingWidget.d.ts → views/BookingWidget.d.ts} +0 -0
  196. /package/dist/{calendar-styles.d.ts → styles/calendar-styles.d.ts} +0 -0
@@ -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
- }