@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.
Files changed (153) 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-76ZOHRBM.js} +2 -2
  6. package/dist/{chunk-Q2PAWBPR.js.map → chunk-76ZOHRBM.js.map} +1 -1
  7. package/dist/components/PaymentStatusBadge.d.ts +1 -1
  8. package/dist/components/PaymentStatusBadge.d.ts.map +1 -1
  9. package/dist/config.d.ts +2 -2
  10. package/dist/config.d.ts.map +1 -1
  11. package/dist/{AgendaContext.d.ts → context.d.ts} +1 -1
  12. package/dist/context.d.ts.map +1 -0
  13. package/dist/data/registries.d.ts.map +1 -0
  14. package/dist/index.d.ts +2 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +170 -7
  17. package/dist/index.js.map +1 -1
  18. package/dist/lib/message-events.d.ts +12 -0
  19. package/dist/lib/message-events.d.ts.map +1 -0
  20. package/dist/public/{data.d.ts → data/index.d.ts} +4 -4
  21. package/dist/public/data/index.d.ts.map +1 -0
  22. package/dist/public/{data.supabase.d.ts → data/supabase.d.ts} +2 -2
  23. package/dist/public/data/supabase.d.ts.map +1 -0
  24. package/dist/public/{hooks.d.ts → hooks/index.d.ts} +4 -4
  25. package/dist/public/hooks/index.d.ts.map +1 -0
  26. package/dist/public/index.d.ts +4 -4
  27. package/dist/public/index.d.ts.map +1 -1
  28. package/dist/public/index.js +3 -3
  29. package/dist/public/index.js.map +1 -1
  30. package/dist/public/{format.d.ts → lib/format.d.ts} +1 -1
  31. package/dist/public/lib/format.d.ts.map +1 -0
  32. package/dist/public/views/BookingWidget.d.ts.map +1 -0
  33. package/dist/public/{steps → views/steps}/ContactStep.d.ts +1 -1
  34. package/dist/public/views/steps/ContactStep.d.ts.map +1 -0
  35. package/dist/public/{steps → views/steps}/DateTimeStep.d.ts +3 -3
  36. package/dist/public/views/steps/DateTimeStep.d.ts.map +1 -0
  37. package/dist/public/{steps → views/steps}/OverviewStep.d.ts +1 -1
  38. package/dist/public/views/steps/OverviewStep.d.ts.map +1 -0
  39. package/dist/public/{steps → views/steps}/PaymentStep.d.ts +1 -1
  40. package/dist/public/views/steps/PaymentStep.d.ts.map +1 -0
  41. package/dist/store.d.ts +1 -1
  42. package/dist/store.d.ts.map +1 -1
  43. package/dist/styles/calendar-styles.d.ts.map +1 -0
  44. package/dist/types.d.ts +1 -1
  45. package/dist/types.d.ts.map +1 -1
  46. package/dist/{AgendaPage.d.ts → views/AgendaPage.d.ts} +3 -3
  47. package/dist/views/AgendaPage.d.ts.map +1 -0
  48. package/package.json +6 -9
  49. package/dist/AgendaContext.d.ts.map +0 -1
  50. package/dist/AgendaPage.d.ts.map +0 -1
  51. package/dist/calendar-styles.d.ts.map +0 -1
  52. package/dist/financial-bridge.d.ts.map +0 -1
  53. package/dist/public/BookingWidget.d.ts.map +0 -1
  54. package/dist/public/data.d.ts.map +0 -1
  55. package/dist/public/data.supabase.d.ts.map +0 -1
  56. package/dist/public/format.d.ts.map +0 -1
  57. package/dist/public/hooks.d.ts.map +0 -1
  58. package/dist/public/steps/ContactStep.d.ts.map +0 -1
  59. package/dist/public/steps/DateTimeStep.d.ts.map +0 -1
  60. package/dist/public/steps/OverviewStep.d.ts.map +0 -1
  61. package/dist/public/steps/PaymentStep.d.ts.map +0 -1
  62. package/dist/registries.d.ts.map +0 -1
  63. package/src/AgendaContext.tsx +0 -11
  64. package/src/AgendaPage.tsx +0 -33
  65. package/src/bridges/create-financial-bridge.ts +0 -296
  66. package/src/calendar-styles.css +0 -245
  67. package/src/calendar-styles.ts +0 -285
  68. package/src/components/AgendaGeneralSettings.tsx +0 -86
  69. package/src/components/AgendaMobileList.tsx +0 -103
  70. package/src/components/AppointmentModal.tsx +0 -1050
  71. package/src/components/AppointmentPopover.tsx +0 -231
  72. package/src/components/BookingPaymentPanel.tsx +0 -394
  73. package/src/components/CalendarSidebar.tsx +0 -119
  74. package/src/components/CalendarToolbar.tsx +0 -86
  75. package/src/components/EventContextMenu.tsx +0 -125
  76. package/src/components/FloatingPanel.tsx +0 -123
  77. package/src/components/MiniCalendar.tsx +0 -120
  78. package/src/components/PaymentStatusBadge.tsx +0 -62
  79. package/src/components/StatusBadge.tsx +0 -23
  80. package/src/config.ts +0 -368
  81. package/src/data/index.ts +0 -4
  82. package/src/data/mock.ts +0 -344
  83. package/src/data/supabase.ts +0 -636
  84. package/src/data/types.ts +0 -37
  85. package/src/financial-bridge.ts +0 -96
  86. package/src/hooks/useAgendaSettings.ts +0 -17
  87. package/src/index.ts +0 -297
  88. package/src/integrations/google-calendar/GoogleCalendarSettings.tsx +0 -136
  89. package/src/integrations/google-calendar/components/CalendarColorDot.tsx +0 -23
  90. package/src/integrations/google-calendar/components/ChannelMappingList.tsx +0 -329
  91. package/src/integrations/google-calendar/components/ImportEventsWizard.tsx +0 -215
  92. package/src/integrations/google-calendar/components/useAgendaLookups.ts +0 -100
  93. package/src/integrations/google-calendar/connector.ts +0 -20
  94. package/src/integrations/google-calendar/connectorDef.tsx +0 -185
  95. package/src/integrations/google-calendar/data/supabase.ts +0 -170
  96. package/src/integrations/google-calendar/data/tables.ts +0 -12
  97. package/src/integrations/google-calendar/index.ts +0 -45
  98. package/src/integrations/google-calendar/integration.test.ts +0 -199
  99. package/src/integrations/google-calendar/mapping.ts +0 -183
  100. package/src/integrations/google-calendar/migrations/001_google_calendar.sql +0 -97
  101. package/src/integrations/google-calendar/migrations/002_smart_sync.sql +0 -517
  102. package/src/integrations/google-calendar/migrations/003_plg_rename.sql +0 -273
  103. package/src/integrations/google-calendar/types.ts +0 -142
  104. package/src/lib/tenant.ts +0 -3
  105. package/src/locales/en.ts +0 -220
  106. package/src/locales/index.ts +0 -7
  107. package/src/locales/pt-BR.ts +0 -220
  108. package/src/migrations/000b_v_bookings_rename.sql +0 -21
  109. package/src/migrations/000c_dedupe_customer_phones.sql +0 -43
  110. package/src/migrations/001_public_booking.sql +0 -296
  111. package/src/migrations/002_v_bookings_compat.sql +0 -14
  112. package/src/migrations/003_public_client_kind.sql +0 -175
  113. package/src/migrations/004_public_booking_assignee.sql +0 -176
  114. package/src/migrations/005_agent_rpcs.sql +0 -168
  115. package/src/migrations/006_agent_rpc_localtime.sql +0 -181
  116. package/src/migrations/007_schedule_exception_precedence.sql +0 -290
  117. package/src/migrations/008_agent_rpc_exception_precedence.sql +0 -172
  118. package/src/migrations/009_public_booking_location.sql +0 -162
  119. package/src/migrations/010_public_service_curation.sql +0 -31
  120. package/src/migrations/011_default_assignee.sql +0 -167
  121. package/src/migrations/012_drop_legacy_booking_overload.sql +0 -29
  122. package/src/migrations/013_normalize_v_appointments.sql +0 -199
  123. package/src/migrations/014_agent_rpc_update_appointment.sql +0 -199
  124. package/src/migrations/index.ts +0 -2377
  125. package/src/public/BookingWidget.tsx +0 -324
  126. package/src/public/components/BrandHeader.tsx +0 -27
  127. package/src/public/components/ServiceCard.tsx +0 -39
  128. package/src/public/components/SidebarExtras.tsx +0 -30
  129. package/src/public/components/StepRailItem.tsx +0 -27
  130. package/src/public/components/SuccessPanel.tsx +0 -27
  131. package/src/public/components/SummaryRail.tsx +0 -90
  132. package/src/public/components/defaults.ts +0 -14
  133. package/src/public/context.tsx +0 -103
  134. package/src/public/createPublicBookingPlugin.ts +0 -281
  135. package/src/public/data.supabase.ts +0 -97
  136. package/src/public/data.ts +0 -105
  137. package/src/public/format.ts +0 -96
  138. package/src/public/hooks.ts +0 -110
  139. package/src/public/index.ts +0 -69
  140. package/src/public/steps/ContactStep.tsx +0 -184
  141. package/src/public/steps/DateTimeStep.tsx +0 -138
  142. package/src/public/steps/OverviewStep.tsx +0 -81
  143. package/src/public/steps/PaymentStep.tsx +0 -180
  144. package/src/public/types.ts +0 -250
  145. package/src/registries.ts +0 -35
  146. package/src/store.ts +0 -311
  147. package/src/types.ts +0 -273
  148. package/src/views/CalendarView.tsx +0 -1112
  149. package/src/views/ConfirmationsView.tsx +0 -155
  150. /package/dist/{financial-bridge.d.ts → bridges/financial-bridge.d.ts} +0 -0
  151. /package/dist/{registries.d.ts → data/registries.d.ts} +0 -0
  152. /package/dist/public/{BookingWidget.d.ts → views/BookingWidget.d.ts} +0 -0
  153. /package/dist/{calendar-styles.d.ts → styles/calendar-styles.d.ts} +0 -0
@@ -1,103 +0,0 @@
1
- import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from 'react'
2
- import type { PaymentProvider } from '@fayz-ai/core'
3
- import type { PublicBookingDataProvider } from './data'
4
- import type {
5
- BookingComponents,
6
- BookingWindow,
7
- PhoneVerificationMode,
8
- PublicBookingLabels,
9
- PublicService,
10
- ResolvedBookingBrand,
11
- ResolvedPayment,
12
- WorkingHours,
13
- } from './types'
14
-
15
- /** Emitted when the customer's phone is verified / identity captured, for the
16
- * host to bridge to auth (booking stays decoupled — data-only, optional). */
17
- export type OnIdentityVerified = (identity: { name: string; phone: string; email?: string }) => void
18
-
19
- export interface PublicBookingContextValue {
20
- provider: PublicBookingDataProvider
21
- /** The professional/resource bookings are created against; null → any. */
22
- professionalId: string | null
23
- professionalName: string
24
- /** Resolved catalog: static `services` option, or fetched from the provider. */
25
- services: PublicService[]
26
- servicesLoading: boolean
27
- /** Business opening hours, shown on the overview "Sobre" section. */
28
- businessHours: WorkingHours
29
- /** 'otp' shows the code modal; 'none' trusts the typed number (default). */
30
- phoneVerification: PhoneVerificationMode
31
- /** Payment step config, or null when the flow ends at confirmation. */
32
- payment: ResolvedPayment | null
33
- /** Injected charge provider (Pix). When absent, the payment step self-mocks. */
34
- paymentProvider: PaymentProvider | null
35
- /** Fired on phone-verify / details submit so the host can sign the user in. */
36
- onIdentityVerified: OnIdentityVerified | null
37
- window: BookingWindow
38
- labels: PublicBookingLabels
39
- /** Brand personalization (resolved with defaults). */
40
- brand: ResolvedBookingBrand
41
- /** Host-overridable subcomponents (resolved with defaults). */
42
- components: BookingComponents
43
- /** Currency code for price formatting (e.g. 'BRL'). */
44
- currency: string
45
- locale: string
46
- }
47
-
48
- const PublicBookingContext = createContext<PublicBookingContextValue | null>(null)
49
-
50
- export interface BookingProviderProps {
51
- /** Everything but the (possibly async) service catalog. */
52
- base: Omit<PublicBookingContextValue, 'services' | 'servicesLoading'>
53
- /** Static catalog from options — wins over the provider fetch when present. */
54
- staticServices: PublicService[] | null
55
- children: ReactNode
56
- }
57
-
58
- /**
59
- * Owns the service-catalog resolution: static services pass through untouched;
60
- * otherwise the provider is queried once (e.g. Supabase v_public_services).
61
- */
62
- export function BookingProvider({ base, staticServices, children }: BookingProviderProps) {
63
- const [fetched, setFetched] = useState<PublicService[] | null>(null)
64
- const [loading, setLoading] = useState(staticServices === null)
65
-
66
- useEffect(() => {
67
- if (staticServices !== null) return
68
- let active = true
69
- setLoading(true)
70
- base.provider
71
- .getServices()
72
- .then((result) => {
73
- if (active) setFetched(result)
74
- })
75
- .catch((err) => {
76
- // Surface in dev; the overview renders its empty state.
77
- console.error('[plugin-agenda/public] failed to load services:', err)
78
- if (active) setFetched([])
79
- })
80
- .finally(() => {
81
- if (active) setLoading(false)
82
- })
83
- return () => {
84
- active = false
85
- }
86
- // base.provider is stable (created once by the plugin factory).
87
- }, [base.provider, staticServices])
88
-
89
- const value = useMemo<PublicBookingContextValue>(
90
- () => ({ ...base, services: staticServices ?? fetched ?? [], servicesLoading: loading }),
91
- [base, staticServices, fetched, loading],
92
- )
93
-
94
- return <PublicBookingContext.Provider value={value}>{children}</PublicBookingContext.Provider>
95
- }
96
-
97
- export function useBooking(): PublicBookingContextValue {
98
- const ctx = useContext(PublicBookingContext)
99
- if (!ctx) {
100
- throw new Error('[plugin-agenda] useBooking must be used within <BookingProvider>.')
101
- }
102
- return ctx
103
- }
@@ -1,281 +0,0 @@
1
- import { createElement, type FC, type ReactNode } from 'react'
2
- import { CalendarCheck, ShieldCheck, Video } from 'lucide-react'
3
- import type { PluginManifest, PluginScope, VerticalId, PaymentProvider } from '@fayz-ai/core'
4
- import type { Professional, Schedule } from '../types'
5
- import type { MockAgendaSeed } from '../data/mock'
6
- import { createMockPublicBookingProvider, type PublicBookingDataProvider } from './data'
7
- import { createSupabasePublicBookingProvider } from './data.supabase'
8
- import { BookingProvider, type PublicBookingContextValue, type OnIdentityVerified } from './context'
9
- import { BookingWidget } from './BookingWidget'
10
- import { DEFAULT_BOOKING_COMPONENTS } from './components/defaults'
11
- import type {
12
- BookingBrand,
13
- BookingComponents,
14
- BookingWindow,
15
- PaymentConfig,
16
- PhoneVerificationMode,
17
- PublicBookingLabels,
18
- PublicService,
19
- ResolvedBookingBrand,
20
- ResolvedPayment,
21
- WorkingHours,
22
- } from './types'
23
-
24
- export interface PublicBookingOptions {
25
- /** Mount path for the public booking route. Default '/agendar'. */
26
- basePath?: string
27
- /**
28
- * Tenant this public page books against. REQUIRED when a Supabase client is
29
- * configured (setGlobalSupabaseClient) — it scopes services/slots/bookings.
30
- */
31
- tenantId?: string
32
- /**
33
- * The single professional/resource bookings target. With Supabase this must
34
- * be the staff person UUID; omit → any active professional (RPC unions
35
- * schedules across staff).
36
- */
37
- professional?: { id?: string; name?: string }
38
- /**
39
- * Bookable services. Optional: when omitted, the widget fetches the catalog
40
- * from the provider (Supabase v_public_services). Static wins when provided.
41
- */
42
- services?: PublicService[]
43
- /** Working hours used to seed mock availability + the overview "Sobre" card. */
44
- workingHours?: WorkingHours
45
- /** Booking-window bounds for the date/slot picker. */
46
- window?: Partial<BookingWindow>
47
- /** Every user-visible string (all defaulted, pt-BR). */
48
- labels?: Partial<PublicBookingLabels>
49
- /** Brand personalization: name/logo/tagline/icons/badges/highlights. */
50
- brand?: BookingBrand
51
- /** Swap whole subcomponents when structure must change (defaults exported). */
52
- components?: Partial<BookingComponents>
53
- /**
54
- * Phone verification before the details sub-step. Default 'none' — the typed
55
- * number is trusted. Set 'otp' only where a real code sender is wired: the
56
- * built-in modal is a POC that accepts the literal '0000' and sends nothing.
57
- */
58
- phoneVerification?: PhoneVerificationMode
59
- /** Adds a final Pagamento (Pix) step. Omit/disable to end the flow at confirmation. */
60
- payment?: PaymentConfig
61
- /** Charge provider (from a payments plugin) used by the Pagamento step. Optional. */
62
- paymentProvider?: PaymentProvider
63
- /** Host bridge: fired when the customer's phone is verified / details submitted. */
64
- onIdentityVerified?: OnIdentityVerified
65
- currency?: string
66
- locale?: string
67
- /** Inject a custom provider (overrides the mock/Supabase safe resolver). */
68
- dataProvider?: PublicBookingDataProvider
69
- /** Override the full mock seed (advanced). */
70
- seed?: MockAgendaSeed
71
- scope?: PluginScope
72
- verticalId?: VerticalId
73
- }
74
-
75
- export interface PublicBookingPlugin {
76
- manifest: PluginManifest
77
- Provider: FC<{ children: ReactNode }>
78
- dataProvider: PublicBookingDataProvider
79
- }
80
-
81
- const DEFAULT_LABELS: PublicBookingLabels = {
82
- title: 'Agende sua consulta',
83
- serviceStep: 'Escolha o serviço',
84
- dateStep: 'Escolha a data',
85
- contactStep: 'Seus dados',
86
- confirmCta: 'Confirmar agendamento',
87
- successTitle: 'Agendamento confirmado!',
88
- successBody: 'Você receberá os detalhes em breve. Até logo!',
89
- // overview
90
- servicesHeading: 'Serviços',
91
- aboutHeading: 'Sobre',
92
- openingHoursHeading: 'Horário de funcionamento',
93
- closedLabel: 'Fechado',
94
- bookNowCta: 'Agendar agora',
95
- // datetime
96
- timezoneNote: 'Fuso: Horário de Brasília',
97
- slotsHeading: 'Horários disponíveis',
98
- noSlots: 'Nenhum horário disponível neste dia. Tente outra data.',
99
- continueCta: 'Continuar',
100
- backToServices: 'Serviços',
101
- back: 'Voltar',
102
- prevWeek: 'Semana anterior',
103
- nextWeek: 'Próxima semana',
104
- // contact
105
- contactIntro: 'Preencha as informações abaixo para que seu agendamento seja identificado.',
106
- phoneLabel: 'Celular',
107
- phoneHint:
108
- 'Enviaremos um código para confirmar que o número de telefone é seu. Usaremos este número para associar as suas informações à sua conta.',
109
- sendCodeCta: 'Enviar código via WhatsApp',
110
- phoneHintNoVerification: 'Usaremos este número para confirmar seu agendamento pelo WhatsApp.',
111
- phoneContinueCta: 'Continuar',
112
- codeTitle: 'Insira o código',
113
- codeBody: 'Enviamos um código via WhatsApp para você confirmar seu número.',
114
- codeInvalid: 'Código inválido. Para testar, use 0000.',
115
- codeConfirmCta: 'Confirmar',
116
- codeResendHint: 'Reenviar código via WhatsApp em 28 segundos.',
117
- phoneVerified: 'Celular verificado',
118
- changePhone: 'Trocar telefone',
119
- nameLabel: 'Nome completo *',
120
- namePlaceholder: 'Seu nome',
121
- emailLabel: 'E-mail',
122
- emailPlaceholder: 'voce@email.com',
123
- emailHint: 'Enviaremos uma confirmação e um lembrete antes do agendamento.',
124
- notesLabel: 'Observação',
125
- notesPlaceholder: 'Use esse espaço para deixar uma observação sobre este agendamento',
126
- saveAndContinueCta: 'Salvar e continuar',
127
- submitError: 'Não foi possível concluir o agendamento. Tente novamente.',
128
- // Rendered ONLY when the payment step is off — so it must not promise a
129
- // payment method the flow never shows.
130
- noChargeNote: 'Sem cobrança agora · o pagamento é feito no atendimento',
131
- // right rail
132
- railTitle: 'Passos para agendar',
133
- railWaitingPayment: 'Aguardando pagamento',
134
- railService: 'Serviço',
135
- railProfessional: 'Profissional',
136
- railDatetime: 'Data e horário',
137
- railContact: 'Informações pessoais',
138
- railPayment: 'Pagamento',
139
- }
140
-
141
- const DEFAULT_WINDOW: BookingWindow = { minAdvanceHours: 2, maxAdvanceDays: 21, slotInterval: 30 }
142
-
143
- const DEFAULT_HOURS: WorkingHours = { daysOfWeek: [1, 2, 3, 4, 5, 6], start: '09:00', end: '18:00' }
144
-
145
- function resolveBrand(brand: BookingBrand | undefined, labels: PublicBookingLabels): ResolvedBookingBrand {
146
- const name = brand?.name ?? labels.title
147
- return {
148
- name,
149
- initials: brand?.initials ?? name.charAt(0).toUpperCase(),
150
- logoUrl: brand?.logoUrl,
151
- tagline: brand?.tagline === undefined ? 'Agendamento online seguro' : brand.tagline,
152
- serviceIcon: brand?.serviceIcon ?? Video,
153
- serviceMeta: brand?.serviceMeta === undefined ? 'Online' : brand.serviceMeta,
154
- featuredBadge: brand?.featuredBadge === undefined ? 'Mais procurado' : brand.featuredBadge,
155
- highlights: brand?.highlights ?? [
156
- { icon: CalendarCheck, text: 'Confirmação imediata' },
157
- { icon: ShieldCheck, text: 'Agendamento seguro' },
158
- ],
159
- }
160
- }
161
-
162
- let scheduleId = 0
163
-
164
- function buildSchedules(professionalId: string, hours: WorkingHours): Schedule[] {
165
- return hours.daysOfWeek.map((day) => ({
166
- id: `sched-${professionalId}-${day}-${scheduleId++}`,
167
- tenantId: 'public',
168
- kind: 'working_hours',
169
- assigneeId: professionalId,
170
- locationId: null,
171
- dayOfWeek: day,
172
- specificDate: null,
173
- startsAt: hours.start,
174
- endsAt: hours.end,
175
- isActive: true,
176
- metadata: {},
177
- }))
178
- }
179
-
180
- export function createPublicBookingPlugin(options: PublicBookingOptions): PublicBookingPlugin {
181
- const basePath = options.basePath ?? '/agendar'
182
- // Mock needs a concrete seed professional; Supabase can target any (null).
183
- const mockProfessionalId = options.professional?.id ?? 'pro-1'
184
- const professionalId = options.professional?.id ?? null
185
- const professionalName = options.professional?.name ?? 'Especialista'
186
- const window: BookingWindow = { ...DEFAULT_WINDOW, ...options.window }
187
- const labels: PublicBookingLabels = { ...DEFAULT_LABELS, ...options.labels }
188
- const brand = resolveBrand(options.brand, labels)
189
- const components: BookingComponents = { ...DEFAULT_BOOKING_COMPONENTS, ...options.components }
190
- const currency = options.currency ?? 'BRL'
191
- const locale = options.locale ?? 'pt-BR'
192
- const hours = options.workingHours ?? DEFAULT_HOURS
193
- const staticServices = options.services ?? null
194
- const phoneVerification: PhoneVerificationMode = options.phoneVerification ?? 'none'
195
- const payment: ResolvedPayment | null = options.payment?.enabled
196
- ? {
197
- reservationFeePercent: options.payment.reservationFeePercent ?? 100,
198
- method: options.payment.method ?? 'pix',
199
- }
200
- : null
201
-
202
- const professional: Professional = {
203
- id: mockProfessionalId,
204
- name: professionalName,
205
- avatarUrl: null,
206
- locationId: null,
207
- locationName: null,
208
- isActive: true,
209
- }
210
- const mockSeed: MockAgendaSeed = options.seed ?? {
211
- professionals: [professional],
212
- schedules: buildSchedules(mockProfessionalId, hours),
213
- bookings: [],
214
- }
215
-
216
- // Resolution: explicit dataProvider → Supabase whenever a tenantId is
217
- // declared (the app's global client if registered, else Fayz Cloud — the
218
- // shared hosted backend) → seeded mock. The mock fallback is LOUD on
219
- // purpose: the old silent fallback hid missing-env bugs as empty catalogs.
220
- let provider: PublicBookingDataProvider
221
- if (options.dataProvider) {
222
- provider = options.dataProvider
223
- } else if (options.tenantId) {
224
- provider = createSupabasePublicBookingProvider({ tenantId: options.tenantId })
225
- } else {
226
- if (typeof console !== 'undefined') {
227
- console.warn(
228
- '[plugin-agenda/public] no `tenantId` and no `dataProvider` — falling back to the seeded MOCK provider. ' +
229
- 'Pass `tenantId` to book against Fayz Cloud, or inject a dataProvider.',
230
- )
231
- }
232
- provider = createMockPublicBookingProvider({
233
- seed: mockSeed,
234
- services: staticServices ?? [],
235
- professionalId: mockProfessionalId,
236
- })
237
- }
238
-
239
- const base: Omit<PublicBookingContextValue, 'services' | 'servicesLoading'> = {
240
- provider,
241
- professionalId,
242
- professionalName,
243
- businessHours: hours,
244
- phoneVerification,
245
- payment,
246
- paymentProvider: options.paymentProvider ?? null,
247
- onIdentityVerified: options.onIdentityVerified ?? null,
248
- window,
249
- labels,
250
- brand,
251
- components,
252
- currency,
253
- locale,
254
- }
255
- const Provider: FC<{ children: ReactNode }> = ({ children }) =>
256
- createElement(BookingProvider, { base, staticServices, children })
257
- Provider.displayName = 'PublicBookingProvider'
258
-
259
- const BookingScreen: FC<unknown> = () =>
260
- createElement('section', { className: 'py-16 bg-background' },
261
- createElement('div', { className: 'container mx-auto px-6' },
262
- createElement(BookingWidget)))
263
- BookingScreen.displayName = 'BookingScreen'
264
-
265
- const manifest: PluginManifest = {
266
- id: 'booking',
267
- name: 'Agendamento',
268
- icon: 'CalendarCheck',
269
- version: '0.4.0',
270
- scope: options.scope ?? 'universal',
271
- verticalId: options.verticalId,
272
- scaffolds: ['website', 'landing_page'],
273
- defaultEnabled: true,
274
- dependencies: [],
275
- navigation: [],
276
- routes: [{ path: basePath, component: BookingScreen, guard: 'public' }],
277
- widgets: [],
278
- }
279
-
280
- return { manifest, Provider, dataProvider: provider }
281
- }
@@ -1,97 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // Supabase implementation of PublicBookingDataProvider — anon-safe.
3
- //
4
- // Public booking pages run with the publishable (anon) key and NO user session,
5
- // so the canonical tenant RLS (`tenant_id IN user_tenant_ids()`) yields nothing.
6
- // This provider therefore only touches objects designed for anon access:
7
- // - public.v_public_services (column-whitelisted view of active services)
8
- // - public.get_available_slots (SECURITY DEFINER RPC, mirrors the mock algo)
9
- // - public.create_public_booking (SECURITY DEFINER RPC: validates the slot,
10
- // upserts the customer person, writes order+booking+order_item atomically)
11
- // Shipped by plugins/plugin-agenda/src/migrations/001_public_booking.sql.
12
- // ---------------------------------------------------------------------------
13
-
14
- import { getSupabaseClientOptional } from '@fayz-ai/core'
15
- import type { TimeSlot } from '../types'
16
- import type { PublicService } from './types'
17
- import type { PublicBookingDataProvider } from './data'
18
-
19
- export interface SupabasePublicBookingOptions {
20
- /** The tenant this public page books against (RPC arg — required). */
21
- tenantId: string
22
- }
23
-
24
- interface SupabaseLikeClient {
25
- from(table: string): any
26
- rpc(fn: string, args: Record<string, unknown>): PromiseLike<{ data: unknown; error: { message: string } | null }>
27
- }
28
-
29
- function getClient(): SupabaseLikeClient {
30
- const client = getSupabaseClientOptional()
31
- if (!client) throw new Error('No Supabase client registered — call setGlobalSupabaseClient() at app startup.')
32
- return client as SupabaseLikeClient
33
- }
34
-
35
- /** Digits-only phone (RPC normalizes again server-side; keep the dial code). */
36
- function toPhoneDigits(phone: string | undefined): string {
37
- return (phone ?? '').replace(/\D/g, '')
38
- }
39
-
40
- export function createSupabasePublicBookingProvider(options: SupabasePublicBookingOptions): PublicBookingDataProvider {
41
- const { tenantId } = options
42
-
43
- return {
44
- async getServices(): Promise<PublicService[]> {
45
- const { data, error } = await getClient()
46
- .from('v_public_services')
47
- .select('id, name, description, price, currency, duration_minutes, sort_order')
48
- .eq('tenant_id', tenantId)
49
- .order('sort_order', { ascending: true })
50
- if (error) throw new Error(`[plugin-agenda/public] getServices: ${error.message}`)
51
- return ((data ?? []) as Array<Record<string, unknown>>).map((row) => ({
52
- id: String(row.id),
53
- name: String(row.name),
54
- description: (row.description as string | null) ?? undefined,
55
- price: Number(row.price ?? 0),
56
- durationMinutes: Number(row.duration_minutes ?? 30),
57
- }))
58
- },
59
-
60
- async getAvailableSlots(query): Promise<TimeSlot[]> {
61
- const { data, error } = await getClient().rpc('get_available_slots', {
62
- p_tenant_id: tenantId,
63
- p_assignee_id: query.assigneeId,
64
- p_date: query.date,
65
- p_duration_minutes: query.durationMinutes,
66
- p_slot_interval: query.slotInterval,
67
- })
68
- if (error) throw new Error(`[plugin-agenda/public] getAvailableSlots: ${error.message}`)
69
- return ((data ?? []) as Array<{ slot_start: string; slot_end: string }>).map((row) => ({
70
- start: row.slot_start,
71
- end: row.slot_end,
72
- }))
73
- },
74
-
75
- async createBooking(input) {
76
- const phone = toPhoneDigits(input.contact.phone)
77
- const { data, error } = await getClient().rpc('create_public_booking', {
78
- p_tenant_id: tenantId,
79
- p_service_id: input.serviceId,
80
- p_starts_at: input.startsAt,
81
- p_name: input.contact.name.trim(),
82
- p_phone: phone,
83
- p_email: input.contact.email?.trim() || null,
84
- p_notes: input.contact.notes?.trim() || null,
85
- // Only sent when pinned — keeps compatibility with pools still running
86
- // the pre-004 7-arg function (unknown named arg would 404 on PostgREST).
87
- ...(input.assigneeId ? { p_assignee_id: input.assigneeId } : {}),
88
- })
89
- if (error) throw new Error(`[plugin-agenda/public] createBooking: ${error.message}`)
90
- const row = (Array.isArray(data) ? data[0] : data) as
91
- | { booking_id: string; starts_at: string; ends_at: string }
92
- | undefined
93
- if (!row) throw new Error('[plugin-agenda/public] createBooking: empty RPC response')
94
- return { bookingId: row.booking_id, startsAt: row.starts_at, endsAt: row.ends_at }
95
- },
96
- }
97
- }
@@ -1,105 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // PublicBookingDataProvider — the narrow data seam the public booking surface
3
- // depends on. The admin calendar's 15-method AgendaDataProvider is deliberately
4
- // NOT used here: a public page needs exactly three capabilities, and the
5
- // Supabase implementation must be anon-safe (SECURITY DEFINER RPCs + whitelisted
6
- // views) rather than the authenticated table access the admin provider does.
7
- // ---------------------------------------------------------------------------
8
-
9
- import type { TimeSlot } from '../types'
10
- import { createMockAgendaProvider, type MockAgendaSeed } from '../data/mock'
11
- import type { PublicService, ContactInfo } from './types'
12
-
13
- export interface PublicSlotQuery {
14
- /** Staff person id; null → any active professional for the tenant. */
15
- assigneeId: string | null
16
- /** 'YYYY-MM-DD' local calendar day. */
17
- date: string
18
- durationMinutes: number
19
- slotInterval: number
20
- }
21
-
22
- export interface PublicCreateBookingInput {
23
- serviceId: string
24
- /** ISO datetime of the chosen slot start. */
25
- startsAt: string
26
- /** name required; phone should include the country dial for real backends. */
27
- contact: ContactInfo
28
- /**
29
- * Staff person id the visitor saw availability for. When set, the backend
30
- * books THAT professional (or fails if their hours don't cover the slot) —
31
- * never silently reassigns. null/undefined → backend auto-picks.
32
- */
33
- assigneeId?: string | null
34
- }
35
-
36
- export interface PublicBookingResult {
37
- bookingId: string
38
- startsAt: string
39
- endsAt: string
40
- }
41
-
42
- export interface PublicBookingDataProvider {
43
- /** Bookable catalog. Used when the host doesn't pass static `services`. */
44
- getServices(): Promise<PublicService[]>
45
- getAvailableSlots(query: PublicSlotQuery): Promise<TimeSlot[]>
46
- createBooking(input: PublicCreateBookingInput): Promise<PublicBookingResult>
47
- }
48
-
49
- // ---------------------------------------------------------------------------
50
- // Mock implementation — adapter over the admin mock provider (reuses its slot
51
- // algorithm + seed machinery, so mock and live behave identically).
52
- // ---------------------------------------------------------------------------
53
-
54
- export interface MockPublicBookingOptions {
55
- seed: MockAgendaSeed
56
- services: PublicService[]
57
- /** The seed professional bookings are created against. */
58
- professionalId: string
59
- }
60
-
61
- export function createMockPublicBookingProvider(options: MockPublicBookingOptions): PublicBookingDataProvider {
62
- const inner = createMockAgendaProvider({ seed: options.seed })
63
- const { services, professionalId } = options
64
-
65
- return {
66
- async getServices() {
67
- return services
68
- },
69
- async getAvailableSlots(query) {
70
- return inner.getAvailableSlots({
71
- assigneeId: query.assigneeId ?? professionalId,
72
- date: query.date,
73
- durationMinutes: query.durationMinutes,
74
- slotInterval: query.slotInterval,
75
- })
76
- },
77
- async createBooking(input) {
78
- const service = services.find((s) => s.id === input.serviceId)
79
- if (!service) throw new Error('Serviço inválido')
80
- const booking = await inner.createBooking({
81
- kind: 'appointment',
82
- // Mock: the customer is captured as the booking title; the Supabase
83
- // path upserts a person record server-side instead.
84
- clientId: 'public-guest',
85
- professionalId,
86
- startsAt: input.startsAt,
87
- title: input.contact.name,
88
- notes:
89
- [input.contact.phone, input.contact.email, input.contact.notes].filter(Boolean).join(' · ') || undefined,
90
- services: [
91
- {
92
- serviceId: service.id,
93
- name: service.name,
94
- durationMinutes: service.durationMinutes,
95
- price: service.price,
96
- assigneeId: professionalId,
97
- },
98
- ],
99
- })
100
- const endsAt =
101
- booking.endsAt ?? new Date(new Date(input.startsAt).getTime() + service.durationMinutes * 60_000).toISOString()
102
- return { bookingId: booking.id, startsAt: booking.startsAt, endsAt }
103
- },
104
- }
105
- }
@@ -1,96 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // Date/price formatting + calendar helpers shared by the booking widget and
3
- // its (host-overridable) subcomponents.
4
- // ---------------------------------------------------------------------------
5
-
6
- import { useMemo } from 'react'
7
- import type { BookingFormatters } from './types'
8
-
9
- export const DAY_NAMES = ['Domingo', 'Segunda-feira', 'Terça-feira', 'Quarta-feira', 'Quinta-feira', 'Sexta-feira', 'Sábado']
10
- export const WEEK_ORDER = [1, 2, 3, 4, 5, 6, 0] // Monday-first, like the reference
11
-
12
- export function useFormatters(locale: string, currency: string): BookingFormatters {
13
- return useMemo(
14
- () => ({
15
- price: new Intl.NumberFormat(locale, { style: 'currency', currency }),
16
- time: new Intl.DateTimeFormat(locale, { hour: '2-digit', minute: '2-digit' }),
17
- weekday: new Intl.DateTimeFormat(locale, { weekday: 'short' }),
18
- dayNum: new Intl.DateTimeFormat(locale, { day: '2-digit', month: '2-digit' }),
19
- longDay: new Intl.DateTimeFormat(locale, { weekday: 'long', day: '2-digit', month: 'long' }),
20
- dateShort: new Intl.DateTimeFormat(locale, { day: '2-digit', month: '2-digit', year: 'numeric' }),
21
- month: new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric' }),
22
- }),
23
- [locale, currency],
24
- )
25
- }
26
-
27
- /** Local 'YYYY-MM-DD' for a Date. */
28
- export function toIso(d: Date): string {
29
- return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
30
- }
31
-
32
- /** Read a preselected service from the URL (?service=…), router-agnostic. */
33
- export function readServiceParam(): string | null {
34
- if (typeof window === 'undefined') return null
35
- return new URLSearchParams(window.location.search).get('service')
36
- }
37
-
38
- /**
39
- * Comparable form of a service name: lowercase, accent-free, non-alphanumerics
40
- * collapsed to single dashes.
41
- *
42
- * Exists because the marketing site and the catalog spell the same service
43
- * differently — the site writes "Peeling Químico" and "Drenagem Linfática", the
44
- * catalog stores "Peeling Quimico" and "Drenagem Linfatica". Matching on the
45
- * raw name would miss most of them, and making the site link raw UUIDs instead
46
- * would scatter database ids through page markup and break on every reseed.
47
- */
48
- export function serviceSlug(name: string): string {
49
- return name
50
- .normalize('NFD')
51
- .replace(/[̀-ͯ]/g, '')
52
- .toLowerCase()
53
- .replace(/[^a-z0-9]+/g, '-')
54
- .replace(/^-|-$/g, '')
55
- }
56
-
57
- /**
58
- * Resolve `?service=` against the catalog. Accepts the service id or its
59
- * slugified name, so a site can link `/agendar?service=limpeza-de-pele`
60
- * without knowing any ids. Unknown value ⇒ null, and the widget opens on the
61
- * overview as if no service had been asked for.
62
- */
63
- export function resolvePreselectedService<T extends { id: string; name: string }>(
64
- param: string | null,
65
- services: T[],
66
- ): string | null {
67
- if (!param) return null
68
- if (services.some((s) => s.id === param)) return param
69
- const wanted = serviceSlug(param)
70
- return services.find((s) => serviceSlug(s.name) === wanted)?.id ?? null
71
- }
72
-
73
- export interface DayCell {
74
- iso: string
75
- date: Date
76
- isToday: boolean
77
- }
78
-
79
- /** 7 consecutive days starting `weekOffset*7` days from today. */
80
- export function buildWeek(weekOffset: number): DayCell[] {
81
- const base = new Date()
82
- base.setHours(0, 0, 0, 0)
83
- const cells: DayCell[] = []
84
- for (let i = 0; i < 7; i++) {
85
- const d = new Date(base.getFullYear(), base.getMonth(), base.getDate() + weekOffset * 7 + i)
86
- cells.push({ iso: toIso(d), date: d, isToday: weekOffset === 0 && i === 0 })
87
- }
88
- return cells
89
- }
90
-
91
- /** "13/07/2026 – 21:30 até 22:00 (30min)" */
92
- export function formatSlotRange(startIso: string, durationMin: number, fmt: BookingFormatters): string {
93
- const start = new Date(startIso)
94
- const end = new Date(start.getTime() + durationMin * 60_000)
95
- return `${fmt.dateShort.format(start)} – ${fmt.time.format(start)} até ${fmt.time.format(end)} (${durationMin}min)`
96
- }