@fayz-ai/plugin-agenda 0.1.8 → 0.2.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 (68) hide show
  1. package/dist/chunk-GAJVIEXM.cjs +870 -0
  2. package/dist/chunk-GAJVIEXM.cjs.map +1 -0
  3. package/dist/chunk-U6VTLOHN.js +867 -0
  4. package/dist/chunk-U6VTLOHN.js.map +1 -0
  5. package/dist/components/AgendaMobileList.d.ts +10 -0
  6. package/dist/components/AgendaMobileList.d.ts.map +1 -0
  7. package/dist/components/AppointmentModal.d.ts +4 -2
  8. package/dist/components/AppointmentModal.d.ts.map +1 -1
  9. package/dist/components/AppointmentPopover.d.ts.map +1 -1
  10. package/dist/components/EventContextMenu.d.ts.map +1 -1
  11. package/dist/config.d.ts +62 -1
  12. package/dist/config.d.ts.map +1 -1
  13. package/dist/data/index.d.ts +1 -0
  14. package/dist/data/index.d.ts.map +1 -1
  15. package/dist/data/mock.d.ts +15 -1
  16. package/dist/data/mock.d.ts.map +1 -1
  17. package/dist/data/supabase.d.ts.map +1 -1
  18. package/dist/index.cjs +611 -1157
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.d.ts +2 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +445 -994
  23. package/dist/index.js.map +1 -1
  24. package/dist/locales/en.d.ts.map +1 -1
  25. package/dist/locales/pt-BR.d.ts.map +1 -1
  26. package/dist/public/BookingWidget.d.ts +9 -0
  27. package/dist/public/BookingWidget.d.ts.map +1 -0
  28. package/dist/public/context.d.ts +37 -0
  29. package/dist/public/context.d.ts.map +1 -0
  30. package/dist/public/createPublicBookingPlugin.d.ts +45 -0
  31. package/dist/public/createPublicBookingPlugin.d.ts.map +1 -0
  32. package/dist/public/hooks.d.ts +32 -0
  33. package/dist/public/hooks.d.ts.map +1 -0
  34. package/dist/public/index.cjs +1020 -0
  35. package/dist/public/index.cjs.map +1 -0
  36. package/dist/public/index.d.ts +9 -0
  37. package/dist/public/index.d.ts.map +1 -0
  38. package/dist/public/index.js +1012 -0
  39. package/dist/public/index.js.map +1 -0
  40. package/dist/public/types.d.ts +51 -0
  41. package/dist/public/types.d.ts.map +1 -0
  42. package/dist/store.d.ts +3 -0
  43. package/dist/store.d.ts.map +1 -1
  44. package/dist/types.d.ts +8 -0
  45. package/dist/types.d.ts.map +1 -1
  46. package/dist/views/CalendarView.d.ts.map +1 -1
  47. package/package.json +11 -8
  48. package/src/components/AgendaMobileList.tsx +103 -0
  49. package/src/components/AppointmentModal.tsx +86 -9
  50. package/src/components/AppointmentPopover.tsx +20 -10
  51. package/src/components/EventContextMenu.tsx +3 -0
  52. package/src/config.ts +104 -5
  53. package/src/data/index.ts +1 -0
  54. package/src/data/mock.ts +26 -6
  55. package/src/data/supabase.ts +13 -2
  56. package/src/index.ts +11 -3
  57. package/src/integrations/google-calendar/integration.test.ts +1 -1
  58. package/src/locales/en.ts +12 -0
  59. package/src/locales/pt-BR.ts +12 -0
  60. package/src/public/BookingWidget.tsx +892 -0
  61. package/src/public/context.tsx +43 -0
  62. package/src/public/createPublicBookingPlugin.ts +155 -0
  63. package/src/public/hooks.ts +117 -0
  64. package/src/public/index.ts +15 -0
  65. package/src/public/types.ts +63 -0
  66. package/src/store.ts +4 -2
  67. package/src/types.ts +8 -0
  68. package/src/views/CalendarView.tsx +220 -54
@@ -0,0 +1,43 @@
1
+ import { createContext, useContext, type ReactNode } from 'react'
2
+ import type { PaymentProvider } from '@fayz-ai/core'
3
+ import type { AgendaDataProvider } from '../data/types'
4
+ import type { PublicService, BookingWindow, PublicBookingLabels, WorkingHours, ResolvedPayment } from './types'
5
+
6
+ /** Emitted when the customer's phone is verified / identity captured, for the
7
+ * host to bridge to auth (booking stays decoupled — data-only, optional). */
8
+ export type OnIdentityVerified = (identity: { name: string; phone: string; email?: string }) => void
9
+
10
+ export interface PublicBookingContextValue {
11
+ provider: AgendaDataProvider
12
+ /** The professional/resource bookings are created against (POC: single professional). */
13
+ professionalId: string
14
+ professionalName: string
15
+ services: PublicService[]
16
+ /** Business opening hours, shown on the overview "Sobre" section. */
17
+ businessHours: WorkingHours
18
+ /** Payment step config, or null when the flow ends at confirmation. */
19
+ payment: ResolvedPayment | null
20
+ /** Injected charge provider (Pix). When absent, the payment step self-mocks. */
21
+ paymentProvider: PaymentProvider | null
22
+ /** Fired on phone-verify / details submit so the host can sign the user in. */
23
+ onIdentityVerified: OnIdentityVerified | null
24
+ window: BookingWindow
25
+ labels: PublicBookingLabels
26
+ /** Currency code for price formatting (e.g. 'BRL'). */
27
+ currency: string
28
+ locale: string
29
+ }
30
+
31
+ const PublicBookingContext = createContext<PublicBookingContextValue | null>(null)
32
+
33
+ export function BookingProvider({ value, children }: { value: PublicBookingContextValue; children: ReactNode }) {
34
+ return <PublicBookingContext.Provider value={value}>{children}</PublicBookingContext.Provider>
35
+ }
36
+
37
+ export function useBooking(): PublicBookingContextValue {
38
+ const ctx = useContext(PublicBookingContext)
39
+ if (!ctx) {
40
+ throw new Error('[plugin-agenda] useBooking must be used within <BookingProvider>.')
41
+ }
42
+ return ctx
43
+ }
@@ -0,0 +1,155 @@
1
+ import { createElement, type FC, type ReactNode } from 'react'
2
+ import type { PluginManifest, PluginScope, VerticalId, PaymentProvider } from '@fayz-ai/core'
3
+ import { createSafeDataProvider } from '@fayz-ai/core'
4
+ import type { AgendaDataProvider } from '../data/types'
5
+ import type { Professional, Schedule } from '../types'
6
+ import { createMockAgendaProvider, type MockAgendaSeed } from '../data/mock'
7
+ import { createSupabaseAgendaProvider } from '../data/supabase'
8
+ import { BookingProvider, type PublicBookingContextValue, type OnIdentityVerified } from './context'
9
+ import { BookingWidget } from './BookingWidget'
10
+ import type { PublicService, BookingWindow, PublicBookingLabels, WorkingHours, PaymentConfig, ResolvedPayment } from './types'
11
+
12
+ export interface PublicBookingOptions {
13
+ /** Mount path for the public booking route. Default '/agendar'. */
14
+ basePath?: string
15
+ /** The single professional/resource bookings target (POC). */
16
+ professional?: { id?: string; name?: string }
17
+ /** Bookable services shown in the widget. */
18
+ services: PublicService[]
19
+ /** Working hours used to seed the mock provider's availability. */
20
+ workingHours?: WorkingHours
21
+ /** Booking-window bounds for the date/slot picker. */
22
+ window?: Partial<BookingWindow>
23
+ labels?: Partial<PublicBookingLabels>
24
+ /** Adds a final Pagamento (Pix) step. Omit/disable to end the flow at confirmation. */
25
+ payment?: PaymentConfig
26
+ /** Charge provider (from a payments plugin) used by the Pagamento step. Optional. */
27
+ paymentProvider?: PaymentProvider
28
+ /** Host bridge: fired when the customer's phone is verified / details submitted. */
29
+ onIdentityVerified?: OnIdentityVerified
30
+ currency?: string
31
+ locale?: string
32
+ /** Inject a custom provider (real availability). Overrides the safe resolver. */
33
+ dataProvider?: AgendaDataProvider
34
+ /** Override the full mock seed (advanced). */
35
+ seed?: MockAgendaSeed
36
+ scope?: PluginScope
37
+ verticalId?: VerticalId
38
+ }
39
+
40
+ export interface PublicBookingPlugin {
41
+ manifest: PluginManifest
42
+ Provider: FC<{ children: ReactNode }>
43
+ dataProvider: AgendaDataProvider
44
+ }
45
+
46
+ const DEFAULT_LABELS: PublicBookingLabels = {
47
+ title: 'Agende sua consulta',
48
+ serviceStep: 'Escolha o serviço',
49
+ dateStep: 'Escolha a data',
50
+ contactStep: 'Seus dados',
51
+ confirmCta: 'Confirmar agendamento',
52
+ successTitle: 'Agendamento confirmado!',
53
+ successBody: 'Você receberá os detalhes em breve. Até logo!',
54
+ }
55
+
56
+ const DEFAULT_WINDOW: BookingWindow = { minAdvanceHours: 2, maxAdvanceDays: 21, slotInterval: 30 }
57
+
58
+ const DEFAULT_HOURS: WorkingHours = { daysOfWeek: [1, 2, 3, 4, 5, 6], start: '09:00', end: '18:00' }
59
+
60
+ let scheduleId = 0
61
+
62
+ function buildSchedules(professionalId: string, hours: WorkingHours): Schedule[] {
63
+ return hours.daysOfWeek.map((day) => ({
64
+ id: `sched-${professionalId}-${day}-${scheduleId++}`,
65
+ tenantId: 'public',
66
+ kind: 'working_hours',
67
+ assigneeId: professionalId,
68
+ locationId: null,
69
+ dayOfWeek: day,
70
+ specificDate: null,
71
+ startsAt: hours.start,
72
+ endsAt: hours.end,
73
+ isActive: true,
74
+ metadata: {},
75
+ }))
76
+ }
77
+
78
+ export function createPublicBookingPlugin(options: PublicBookingOptions): PublicBookingPlugin {
79
+ const basePath = options.basePath ?? '/agendar'
80
+ const professionalId = options.professional?.id ?? 'dr-hiago'
81
+ const professionalName = options.professional?.name ?? 'Especialista'
82
+ const window: BookingWindow = { ...DEFAULT_WINDOW, ...options.window }
83
+ const labels: PublicBookingLabels = { ...DEFAULT_LABELS, ...options.labels }
84
+ const currency = options.currency ?? 'BRL'
85
+ const locale = options.locale ?? 'pt-BR'
86
+ const hours = options.workingHours ?? DEFAULT_HOURS
87
+ const payment: ResolvedPayment | null = options.payment?.enabled
88
+ ? {
89
+ reservationFeePercent: options.payment.reservationFeePercent ?? 100,
90
+ method: options.payment.method ?? 'pix',
91
+ }
92
+ : null
93
+
94
+ const professional: Professional = {
95
+ id: professionalId,
96
+ name: professionalName,
97
+ avatarUrl: null,
98
+ locationId: null,
99
+ locationName: null,
100
+ isActive: true,
101
+ }
102
+ const mockSeed: MockAgendaSeed = options.seed ?? {
103
+ professionals: [professional],
104
+ schedules: buildSchedules(professionalId, hours),
105
+ bookings: [],
106
+ }
107
+
108
+ const provider =
109
+ options.dataProvider ??
110
+ createSafeDataProvider(
111
+ () => createSupabaseAgendaProvider(),
112
+ () => createMockAgendaProvider({ seed: mockSeed }),
113
+ )
114
+
115
+ const value: PublicBookingContextValue = {
116
+ provider,
117
+ professionalId,
118
+ professionalName,
119
+ services: options.services,
120
+ businessHours: hours,
121
+ payment,
122
+ paymentProvider: options.paymentProvider ?? null,
123
+ onIdentityVerified: options.onIdentityVerified ?? null,
124
+ window,
125
+ labels,
126
+ currency,
127
+ locale,
128
+ }
129
+ const Provider: FC<{ children: ReactNode }> = ({ children }) =>
130
+ createElement(BookingProvider, { value, children })
131
+ Provider.displayName = 'PublicBookingProvider'
132
+
133
+ const BookingScreen: FC<unknown> = () =>
134
+ createElement('section', { className: 'py-16 bg-background' },
135
+ createElement('div', { className: 'container mx-auto px-6' },
136
+ createElement(BookingWidget)))
137
+ BookingScreen.displayName = 'BookingScreen'
138
+
139
+ const manifest: PluginManifest = {
140
+ id: 'booking',
141
+ name: 'Agendamento',
142
+ icon: 'CalendarCheck',
143
+ version: '0.1.0',
144
+ scope: options.scope ?? 'universal',
145
+ verticalId: options.verticalId,
146
+ scaffolds: ['website', 'landing_page'],
147
+ defaultEnabled: true,
148
+ dependencies: [],
149
+ navigation: [],
150
+ routes: [{ path: basePath, component: BookingScreen, guard: 'public' }],
151
+ widgets: [],
152
+ }
153
+
154
+ return { manifest, Provider, dataProvider: provider }
155
+ }
@@ -0,0 +1,117 @@
1
+ import { useEffect, useState } from 'react'
2
+ import { useBooking } from './context'
3
+ import type { PublicService, ContactInfo } from './types'
4
+ import type { TimeSlot } from '../types'
5
+
6
+ /** The configured bookable services (from context; POC-static, ERP-driven later). */
7
+ export function useServices(): PublicService[] {
8
+ return useBooking().services
9
+ }
10
+
11
+ export interface UseAvailableSlotsResult {
12
+ slots: TimeSlot[]
13
+ loading: boolean
14
+ error: Error | null
15
+ }
16
+
17
+ /**
18
+ * Available time slots for a service on a given date. Delegates to
19
+ * provider.getAvailableSlots — mock now, real availability later, same call.
20
+ */
21
+ export function useAvailableSlots(params: { serviceId: string | null; date: string | null }): UseAvailableSlotsResult {
22
+ const { provider, professionalId, services, window } = useBooking()
23
+ const [slots, setSlots] = useState<TimeSlot[]>([])
24
+ const [loading, setLoading] = useState(false)
25
+ const [error, setError] = useState<Error | null>(null)
26
+
27
+ const { serviceId, date } = params
28
+ const service = services.find((s) => s.id === serviceId)
29
+ const durationMinutes = service?.durationMinutes ?? 0
30
+
31
+ useEffect(() => {
32
+ let active = true
33
+ if (!serviceId || !date || !durationMinutes) {
34
+ setSlots([])
35
+ return
36
+ }
37
+ setLoading(true)
38
+ provider
39
+ .getAvailableSlots({ assigneeId: professionalId, date, durationMinutes, slotInterval: window.slotInterval })
40
+ .then((result) => {
41
+ if (!active) return
42
+ setSlots(result)
43
+ setError(null)
44
+ })
45
+ .catch((err) => {
46
+ if (!active) return
47
+ setError(err instanceof Error ? err : new Error(String(err)))
48
+ })
49
+ .finally(() => {
50
+ if (active) setLoading(false)
51
+ })
52
+ return () => {
53
+ active = false
54
+ }
55
+ }, [provider, professionalId, serviceId, date, durationMinutes, window.slotInterval])
56
+
57
+ return { slots, loading, error }
58
+ }
59
+
60
+ export interface CreatePublicBookingInput {
61
+ serviceId: string
62
+ /** ISO datetime of the chosen slot start. */
63
+ startsAt: string
64
+ contact: ContactInfo
65
+ }
66
+
67
+ export interface UseCreateBookingResult {
68
+ submit: (input: CreatePublicBookingInput) => Promise<void>
69
+ submitting: boolean
70
+ error: Error | null
71
+ done: boolean
72
+ }
73
+
74
+ /** Create a booking via the provider. POC: mock append; real persistence later. */
75
+ export function useCreateBooking(): UseCreateBookingResult {
76
+ const { provider, professionalId, services } = useBooking()
77
+ const [submitting, setSubmitting] = useState(false)
78
+ const [error, setError] = useState<Error | null>(null)
79
+ const [done, setDone] = useState(false)
80
+
81
+ async function submit(input: CreatePublicBookingInput): Promise<void> {
82
+ const service = services.find((s) => s.id === input.serviceId)
83
+ if (!service) throw new Error('Serviço inválido')
84
+ setSubmitting(true)
85
+ setError(null)
86
+ try {
87
+ await provider.createBooking({
88
+ kind: 'appointment',
89
+ // POC: the customer is captured as the booking title; a real flow would
90
+ // upsert a client/person record and pass its id here.
91
+ clientId: 'public-guest',
92
+ professionalId,
93
+ startsAt: input.startsAt,
94
+ title: input.contact.name,
95
+ notes: [input.contact.phone, input.contact.email, input.contact.notes].filter(Boolean).join(' · ') || undefined,
96
+ services: [
97
+ {
98
+ serviceId: service.id,
99
+ name: service.name,
100
+ durationMinutes: service.durationMinutes,
101
+ price: service.price,
102
+ assigneeId: professionalId,
103
+ },
104
+ ],
105
+ })
106
+ setDone(true)
107
+ } catch (err) {
108
+ const e = err instanceof Error ? err : new Error(String(err))
109
+ setError(e)
110
+ throw e
111
+ } finally {
112
+ setSubmitting(false)
113
+ }
114
+ }
115
+
116
+ return { submit, submitting, error, done }
117
+ }
@@ -0,0 +1,15 @@
1
+ // ---------------------------------------------------------------------------
2
+ // @fayz-ai/plugin-agenda/public — customer-facing booking surface.
3
+ //
4
+ // Separate entry from the admin plugin so a website host imports only the lean
5
+ // booking widget + data seam (no @fayz-ai/saas / calendar admin graph).
6
+ // ---------------------------------------------------------------------------
7
+
8
+ export { createPublicBookingPlugin } from './createPublicBookingPlugin'
9
+ export type { PublicBookingOptions, PublicBookingPlugin } from './createPublicBookingPlugin'
10
+ export { BookingWidget } from './BookingWidget'
11
+ export { BookingProvider, useBooking } from './context'
12
+ export type { PublicBookingContextValue } from './context'
13
+ export { useServices, useAvailableSlots, useCreateBooking } from './hooks'
14
+ export type { UseAvailableSlotsResult, UseCreateBookingResult, CreatePublicBookingInput } from './hooks'
15
+ export type { PublicService, ContactInfo, BookingWindow, PublicBookingLabels, WorkingHours } from './types'
@@ -0,0 +1,63 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Public (customer-facing) booking types. This surface is deliberately decoupled
3
+ // from the admin calendar: it depends only on the AgendaDataProvider interface,
4
+ // so mock slots today and real availability (Supabase RPC / ERP / Google
5
+ // Calendar) later are a provider swap with no widget change.
6
+ // ---------------------------------------------------------------------------
7
+
8
+ export interface PublicService {
9
+ id: string
10
+ name: string
11
+ durationMinutes: number
12
+ price: number
13
+ description?: string
14
+ }
15
+
16
+ export interface ContactInfo {
17
+ name: string
18
+ email?: string
19
+ phone?: string
20
+ notes?: string
21
+ }
22
+
23
+ export interface WorkingHours {
24
+ /** 0=Sun … 6=Sat. */
25
+ daysOfWeek: number[]
26
+ /** 'HH:MM' */
27
+ start: string
28
+ /** 'HH:MM' */
29
+ end: string
30
+ }
31
+
32
+ export interface BookingWindow {
33
+ /** Earliest lead time before a slot can be booked. */
34
+ minAdvanceHours: number
35
+ /** How many days ahead the date picker offers. */
36
+ maxAdvanceDays: number
37
+ /** Slot granularity in minutes. */
38
+ slotInterval: number
39
+ }
40
+
41
+ export interface PaymentConfig {
42
+ /** Show a Pagamento step as the final stage of the flow. */
43
+ enabled?: boolean
44
+ /** % of the service price charged up-front as a reservation fee (default 100). */
45
+ reservationFeePercent?: number
46
+ /** Payment method (POC ships 'pix'). */
47
+ method?: 'pix'
48
+ }
49
+
50
+ export interface ResolvedPayment {
51
+ reservationFeePercent: number
52
+ method: 'pix'
53
+ }
54
+
55
+ export interface PublicBookingLabels {
56
+ title: string
57
+ serviceStep: string
58
+ dateStep: string
59
+ contactStep: string
60
+ confirmCta: string
61
+ successTitle: string
62
+ successBody: string
63
+ }
package/src/store.ts CHANGED
@@ -4,6 +4,7 @@ import { toast } from '@fayz-ai/ui'
4
4
  import { getScheduleBlockConfig, setScheduleBlockConfig } from '@fayz-ai/saas'
5
5
  import type { AgendaDataProvider } from './data/types'
6
6
  import type { AgendaFinancialBridge } from './financial-bridge'
7
+ import type { ExternalBookingSource } from './config'
7
8
  import type {
8
9
  CalendarBooking, Professional, Schedule,
9
10
  CreateBookingInput, UpdateBookingInput, BookingQuery,
@@ -48,6 +49,7 @@ export interface AgendaUIState {
48
49
  bookingId?: string
49
50
  prefill?: Partial<CreateBookingInput> & { endsAt?: string }
50
51
  initialTab?: string
52
+ externalSource?: ExternalBookingSource
51
53
  }
52
54
 
53
55
  // Actions
@@ -64,7 +66,7 @@ export interface AgendaUIState {
64
66
  setFilters(filters: Partial<{ professionalIds: string[]; locationId: string | null; statuses: string[] }>): void
65
67
  setView(view: string): void
66
68
  setSelectedDate(date: string): void
67
- openAppointmentModal(mode: 'create' | 'edit', options?: { bookingId?: string; prefill?: Partial<CreateBookingInput> & { endsAt?: string }; initialTab?: string }): void
69
+ openAppointmentModal(mode: 'create' | 'edit', options?: { bookingId?: string; prefill?: Partial<CreateBookingInput> & { endsAt?: string }; initialTab?: string; externalSource?: ExternalBookingSource }): void
68
70
  closeAppointmentModal(): void
69
71
  }
70
72
 
@@ -292,7 +294,7 @@ export function createAgendaStore(provider: AgendaDataProvider, financialBridge?
292
294
  setSelectedDate(date) { set({ selectedDate: date }) },
293
295
 
294
296
  openAppointmentModal(mode, options) {
295
- set({ appointmentModal: { open: true, mode, bookingId: options?.bookingId, prefill: options?.prefill, initialTab: options?.initialTab } })
297
+ set({ appointmentModal: { open: true, mode, bookingId: options?.bookingId, prefill: options?.prefill, initialTab: options?.initialTab, externalSource: options?.externalSource } })
296
298
  },
297
299
 
298
300
  closeAppointmentModal() {
package/src/types.ts CHANGED
@@ -29,6 +29,9 @@ export interface BookingTypeConfig {
29
29
  icon: string
30
30
  color: string
31
31
  fields: {
32
+ /** Free-text event title (maps to the booking's display name). Used by
33
+ * simple / Google-Calendar-style events that have no client party. */
34
+ title?: boolean
32
35
  client: boolean
33
36
  professional: boolean
34
37
  services: boolean
@@ -205,6 +208,9 @@ export interface CreateBookingInput {
205
208
  locationId?: string
206
209
  startsAt: string
207
210
  notes?: string
211
+ /** Free-text event title for simple events (no client party). Maps to the
212
+ * booking's display name (clientName in the read model). */
213
+ title?: string
208
214
  services: CreateBookingServiceInput[]
209
215
  }
210
216
 
@@ -224,6 +230,8 @@ export interface UpdateBookingInput {
224
230
  endsAt?: string
225
231
  status?: BookingStatus
226
232
  notes?: string
233
+ /** Free-text event title for simple events (no client party). */
234
+ title?: string
227
235
  services?: CreateBookingServiceInput[]
228
236
  }
229
237