@fayz-ai/plugin-agenda 0.2.1 → 0.8.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 (102) hide show
  1. package/README.md +2 -0
  2. package/dist/bridges/create-financial-bridge.d.ts.map +1 -1
  3. package/dist/chunk-DRZF6CV2.js +421 -0
  4. package/dist/chunk-DRZF6CV2.js.map +1 -0
  5. package/dist/chunk-LFUELUIT.cjs +423 -0
  6. package/dist/chunk-LFUELUIT.cjs.map +1 -0
  7. package/dist/config.d.ts +2 -2
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/data/supabase.d.ts.map +1 -1
  10. package/dist/index.cjs +765 -22
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +765 -22
  14. package/dist/index.js.map +1 -1
  15. package/dist/migrations/index.d.ts +9 -0
  16. package/dist/migrations/index.d.ts.map +1 -0
  17. package/dist/public/BookingWidget.d.ts +4 -2
  18. package/dist/public/BookingWidget.d.ts.map +1 -1
  19. package/dist/public/components/BrandHeader.d.ts +6 -0
  20. package/dist/public/components/BrandHeader.d.ts.map +1 -0
  21. package/dist/public/components/ServiceCard.d.ts +5 -0
  22. package/dist/public/components/ServiceCard.d.ts.map +1 -0
  23. package/dist/public/components/SidebarExtras.d.ts +4 -0
  24. package/dist/public/components/SidebarExtras.d.ts.map +1 -0
  25. package/dist/public/components/StepRailItem.d.ts +4 -0
  26. package/dist/public/components/StepRailItem.d.ts.map +1 -0
  27. package/dist/public/components/SuccessPanel.d.ts +4 -0
  28. package/dist/public/components/SuccessPanel.d.ts.map +1 -0
  29. package/dist/public/components/SummaryRail.d.ts +27 -0
  30. package/dist/public/components/SummaryRail.d.ts.map +1 -0
  31. package/dist/public/components/defaults.d.ts +3 -0
  32. package/dist/public/components/defaults.d.ts.map +1 -0
  33. package/dist/public/context.d.ts +22 -8
  34. package/dist/public/context.d.ts.map +1 -1
  35. package/dist/public/createPublicBookingPlugin.d.ts +27 -10
  36. package/dist/public/createPublicBookingPlugin.d.ts.map +1 -1
  37. package/dist/public/data.d.ts +37 -0
  38. package/dist/public/data.d.ts.map +1 -0
  39. package/dist/public/data.supabase.d.ts +7 -0
  40. package/dist/public/data.supabase.d.ts.map +1 -0
  41. package/dist/public/format.d.ts +18 -0
  42. package/dist/public/format.d.ts.map +1 -0
  43. package/dist/public/hooks.d.ts +13 -11
  44. package/dist/public/hooks.d.ts.map +1 -1
  45. package/dist/public/index.cjs +1018 -615
  46. package/dist/public/index.cjs.map +1 -1
  47. package/dist/public/index.d.ts +15 -4
  48. package/dist/public/index.d.ts.map +1 -1
  49. package/dist/public/index.js +1008 -618
  50. package/dist/public/index.js.map +1 -1
  51. package/dist/public/steps/ContactStep.d.ts +30 -0
  52. package/dist/public/steps/ContactStep.d.ts.map +1 -0
  53. package/dist/public/steps/DateTimeStep.d.ts +23 -0
  54. package/dist/public/steps/DateTimeStep.d.ts.map +1 -0
  55. package/dist/public/steps/OverviewStep.d.ts +16 -0
  56. package/dist/public/steps/OverviewStep.d.ts.map +1 -0
  57. package/dist/public/steps/PaymentStep.d.ts +20 -0
  58. package/dist/public/steps/PaymentStep.d.ts.map +1 -0
  59. package/dist/public/types.d.ts +134 -0
  60. package/dist/public/types.d.ts.map +1 -1
  61. package/dist/registries.d.ts.map +1 -1
  62. package/dist/types.d.ts +3 -3
  63. package/dist/types.d.ts.map +1 -1
  64. package/package.json +11 -5
  65. package/src/bridges/create-financial-bridge.ts +6 -8
  66. package/src/components/AppointmentModal.tsx +4 -4
  67. package/src/config.ts +2 -2
  68. package/src/data/supabase.ts +26 -25
  69. package/src/index.ts +11 -0
  70. package/src/integrations/google-calendar/index.ts +1 -1
  71. package/src/integrations/google-calendar/migrations/001_google_calendar.sql +6 -6
  72. package/src/migrations/000b_v_bookings_rename.sql +21 -0
  73. package/src/migrations/000c_dedupe_customer_phones.sql +43 -0
  74. package/src/migrations/001_public_booking.sql +291 -0
  75. package/src/migrations/002_v_bookings_compat.sql +14 -0
  76. package/src/migrations/index.ts +387 -0
  77. package/src/public/BookingWidget.tsx +136 -713
  78. package/src/public/components/BrandHeader.tsx +27 -0
  79. package/src/public/components/ServiceCard.tsx +39 -0
  80. package/src/public/components/SidebarExtras.tsx +30 -0
  81. package/src/public/components/StepRailItem.tsx +27 -0
  82. package/src/public/components/SuccessPanel.tsx +27 -0
  83. package/src/public/components/SummaryRail.tsx +90 -0
  84. package/src/public/components/defaults.ts +14 -0
  85. package/src/public/context.tsx +64 -7
  86. package/src/public/createPublicBookingPlugin.ts +138 -25
  87. package/src/public/data.supabase.ts +94 -0
  88. package/src/public/data.ts +99 -0
  89. package/src/public/format.ts +61 -0
  90. package/src/public/hooks.ts +22 -34
  91. package/src/public/index.ts +57 -4
  92. package/src/public/steps/ContactStep.tsx +179 -0
  93. package/src/public/steps/DateTimeStep.tsx +138 -0
  94. package/src/public/steps/OverviewStep.tsx +81 -0
  95. package/src/public/steps/PaymentStep.tsx +180 -0
  96. package/src/public/types.ts +169 -3
  97. package/src/registries.ts +1 -2
  98. package/src/types.ts +4 -4
  99. package/dist/chunk-GAJVIEXM.cjs +0 -870
  100. package/dist/chunk-GAJVIEXM.cjs.map +0 -1
  101. package/dist/chunk-U6VTLOHN.js +0 -867
  102. package/dist/chunk-U6VTLOHN.js.map +0 -1
@@ -0,0 +1,27 @@
1
+ import { ShieldCheck } from 'lucide-react'
2
+ import type { BrandHeaderProps } from '../types'
3
+
4
+ /** Default brand header shown above every booking view. Override via
5
+ * `components.BrandHeader` when the structure must change; text/logo-only
6
+ * changes need just the `brand` option. */
7
+ export function DefaultBrandHeader({ brand }: BrandHeaderProps) {
8
+ return (
9
+ <div className="mb-5 flex items-center gap-3 rounded-2xl border border-border bg-card px-5 py-4 shadow-sm">
10
+ {brand.logoUrl ? (
11
+ <img src={brand.logoUrl} alt={brand.name} className="h-11 w-11 rounded-xl object-cover" />
12
+ ) : (
13
+ <span className="flex h-11 w-11 items-center justify-center rounded-xl bg-primary text-lg font-bold text-primary-foreground">
14
+ {brand.initials}
15
+ </span>
16
+ )}
17
+ <div>
18
+ <p className="font-heading text-base font-bold text-foreground">{brand.name}</p>
19
+ {brand.tagline ? (
20
+ <p className="flex items-center gap-1.5 text-xs text-muted-foreground">
21
+ <ShieldCheck className="h-3.5 w-3.5 text-primary" /> {brand.tagline}
22
+ </p>
23
+ ) : null}
24
+ </div>
25
+ </div>
26
+ )
27
+ }
@@ -0,0 +1,39 @@
1
+ import { ChevronRight, Clock } from 'lucide-react'
2
+ import type { ServiceCardProps } from '../types'
3
+
4
+ /** Default service row (overview list). Icon/badge/meta come from `brand`;
5
+ * swap the whole card via `components.ServiceCard`. */
6
+ export function DefaultServiceCard({ service, featured, onSelect, fmt, brand }: ServiceCardProps) {
7
+ const Icon = brand.serviceIcon
8
+ return (
9
+ <button
10
+ type="button"
11
+ onClick={onSelect}
12
+ className="group flex w-full items-center gap-4 rounded-2xl border border-border p-4 text-left transition-colors hover:border-primary hover:bg-accent/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
13
+ >
14
+ <span className="flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-accent text-primary">
15
+ <Icon className="h-5 w-5" />
16
+ </span>
17
+ <span className="min-w-0 flex-1">
18
+ <span className="flex items-center gap-2">
19
+ <span className="font-semibold text-foreground">{service.name}</span>
20
+ {featured && brand.featuredBadge ? (
21
+ <span className="rounded-full bg-accent px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-primary">
22
+ {brand.featuredBadge}
23
+ </span>
24
+ ) : null}
25
+ </span>
26
+ <span className="mt-0.5 flex items-center gap-1 text-xs text-muted-foreground">
27
+ <Clock className="h-3 w-3" /> {service.durationMinutes} min{brand.serviceMeta ? ` · ${brand.serviceMeta}` : ''}
28
+ </span>
29
+ {service.description ? (
30
+ <span className="mt-1 block text-sm text-muted-foreground">{service.description}</span>
31
+ ) : null}
32
+ </span>
33
+ <span className="shrink-0 text-right">
34
+ <span className="block font-semibold tabular-nums text-primary">{fmt.price.format(service.price)}</span>
35
+ <ChevronRight className="ml-auto mt-1 h-4 w-4 text-muted-foreground transition-transform group-hover:translate-x-0.5" />
36
+ </span>
37
+ </button>
38
+ )
39
+ }
@@ -0,0 +1,30 @@
1
+ import { User } from 'lucide-react'
2
+ import type { SidebarExtrasProps } from '../types'
3
+
4
+ /** Default overview sidebar: primary CTA + brand highlights + professional. */
5
+ export function DefaultSidebarExtras({ professionalName, brand, onStartBooking, labels }: SidebarExtrasProps) {
6
+ return (
7
+ <div className="rounded-2xl border border-border bg-card p-5 shadow-sm">
8
+ <button
9
+ type="button"
10
+ onClick={onStartBooking}
11
+ className="w-full rounded-xl bg-primary py-3 font-semibold text-primary-foreground transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
12
+ >
13
+ {labels.bookNowCta}
14
+ </button>
15
+ <div className="mt-5 space-y-2 border-t border-border pt-4 text-xs text-muted-foreground">
16
+ {brand.highlights.map((h, i) => {
17
+ const Icon = h.icon ?? User
18
+ return (
19
+ <p key={i} className="flex items-center gap-2">
20
+ <Icon className="h-3.5 w-3.5 text-primary" /> {h.text}
21
+ </p>
22
+ )
23
+ })}
24
+ <p className="flex items-center gap-2">
25
+ <User className="h-3.5 w-3.5 text-primary" /> {professionalName}
26
+ </p>
27
+ </div>
28
+ </div>
29
+ )
30
+ }
@@ -0,0 +1,27 @@
1
+ import { Check } from 'lucide-react'
2
+ import type { StepRailItemProps } from '../types'
3
+
4
+ /** One row in the wizard's right-hand progress rail. */
5
+ export function DefaultStepRailItem({ n, label, state, children }: StepRailItemProps) {
6
+ return (
7
+ <li className="relative pl-11">
8
+ <span aria-hidden className="absolute left-[15px] top-8 bottom-[-18px] w-px bg-border last:hidden" />
9
+ <span
10
+ className={[
11
+ 'absolute left-0 top-0 flex h-8 w-8 items-center justify-center rounded-full text-sm font-semibold transition-colors',
12
+ state === 'done'
13
+ ? 'bg-primary text-primary-foreground'
14
+ : state === 'active'
15
+ ? 'bg-accent text-primary ring-2 ring-primary'
16
+ : 'bg-muted text-muted-foreground',
17
+ ].join(' ')}
18
+ >
19
+ {state === 'done' ? <Check className="h-4 w-4" /> : n}
20
+ </span>
21
+ <p className={`pt-1 text-sm font-semibold ${state === 'todo' ? 'text-muted-foreground' : 'text-foreground'}`}>
22
+ {label}
23
+ </p>
24
+ {children ? <div className="mt-2">{children}</div> : null}
25
+ </li>
26
+ )
27
+ }
@@ -0,0 +1,27 @@
1
+ import { Calendar, Check, User } from 'lucide-react'
2
+ import { formatSlotRange } from '../format'
3
+ import type { SuccessPanelProps } from '../types'
4
+
5
+ /** Default confirmation panel (final step). */
6
+ export function DefaultSuccessPanel({ service, slotStart, professionalName, labels, fmt }: SuccessPanelProps) {
7
+ return (
8
+ <div className="py-8 text-center">
9
+ <div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-accent text-primary">
10
+ <Check className="h-8 w-8" />
11
+ </div>
12
+ <h3 className="font-heading text-2xl font-bold text-foreground">{labels.successTitle}</h3>
13
+ <p className="mx-auto mt-2 max-w-sm text-muted-foreground">{labels.successBody}</p>
14
+ {service && slotStart ? (
15
+ <div className="mx-auto mt-6 max-w-sm rounded-2xl border border-border bg-muted/50 p-4 text-left">
16
+ <p className="font-semibold text-foreground">{service.name}</p>
17
+ <p className="mt-1 flex items-center gap-1.5 text-sm tabular-nums text-muted-foreground">
18
+ <Calendar className="h-4 w-4 text-primary" /> {formatSlotRange(slotStart, service.durationMinutes, fmt)}
19
+ </p>
20
+ <p className="mt-1 flex items-center gap-1.5 text-sm text-muted-foreground">
21
+ <User className="h-4 w-4 text-primary" /> {professionalName}
22
+ </p>
23
+ </div>
24
+ ) : null}
25
+ </div>
26
+ )
27
+ }
@@ -0,0 +1,90 @@
1
+ import { CreditCard } from 'lucide-react'
2
+ import { formatSlotRange } from '../format'
3
+ import type {
4
+ BookingComponents,
5
+ BookingFormatters,
6
+ PublicBookingLabels,
7
+ PublicService,
8
+ ResolvedPayment,
9
+ StepState,
10
+ } from '../types'
11
+
12
+ export interface RailState {
13
+ service: StepState
14
+ professional: StepState
15
+ datetime: StepState
16
+ contact: StepState
17
+ payment: StepState
18
+ }
19
+
20
+ interface SummaryRailProps {
21
+ title: string
22
+ railState: RailState
23
+ service: PublicService | null
24
+ professionalName: string
25
+ slotStart: string | null
26
+ /** Display string for the verified contact ("Nome · +55 (11) …"), or null. */
27
+ contactSummary: string | null
28
+ payment: ResolvedPayment | null
29
+ paymentReached: boolean
30
+ labels: PublicBookingLabels
31
+ fmt: BookingFormatters
32
+ components: BookingComponents
33
+ serviceMeta: string | null
34
+ }
35
+
36
+ /** Right-hand progress + summary rail (internal; rows via components.StepRailItem). */
37
+ export function SummaryRail({
38
+ title, railState, service, professionalName, slotStart, contactSummary,
39
+ payment, paymentReached, labels, fmt, components: C, serviceMeta,
40
+ }: SummaryRailProps) {
41
+ return (
42
+ <div className="sticky top-24 rounded-2xl border border-border bg-card p-5 shadow-sm">
43
+ <p className="mb-5 font-heading text-sm font-bold uppercase tracking-wide text-muted-foreground">{title}</p>
44
+ <ol className="space-y-6">
45
+ <C.StepRailItem n={1} label={labels.railService} state={railState.service}>
46
+ {service ? (
47
+ <div className="rounded-xl border border-border bg-background p-3">
48
+ <p className="truncate text-sm font-semibold text-foreground">{service.name}</p>
49
+ <p className="mt-0.5 flex items-center justify-between text-xs text-muted-foreground">
50
+ <span>{serviceMeta ? `${serviceMeta} · ` : ''}{service.durationMinutes} min</span>
51
+ <span className="font-semibold tabular-nums text-primary">{fmt.price.format(service.price)}</span>
52
+ </p>
53
+ </div>
54
+ ) : null}
55
+ </C.StepRailItem>
56
+
57
+ <C.StepRailItem n={2} label={labels.railProfessional} state={railState.professional}>
58
+ <div className="flex items-center gap-2">
59
+ <span className="flex h-7 w-7 items-center justify-center rounded-full bg-primary text-xs font-semibold text-primary-foreground">
60
+ {professionalName.charAt(0)}
61
+ </span>
62
+ <span className="text-sm text-foreground">{professionalName}</span>
63
+ </div>
64
+ </C.StepRailItem>
65
+
66
+ <C.StepRailItem n={3} label={labels.railDatetime} state={railState.datetime}>
67
+ {slotStart && service ? (
68
+ <p className="text-sm tabular-nums text-foreground">
69
+ {formatSlotRange(slotStart, service.durationMinutes, fmt)}
70
+ </p>
71
+ ) : null}
72
+ </C.StepRailItem>
73
+
74
+ <C.StepRailItem n={4} label={labels.railContact} state={railState.contact}>
75
+ {contactSummary ? <p className="text-sm text-foreground">{contactSummary}</p> : null}
76
+ </C.StepRailItem>
77
+
78
+ {payment ? (
79
+ <C.StepRailItem n={5} label={labels.railPayment} state={railState.payment}>
80
+ {paymentReached ? (
81
+ <p className="flex items-center gap-1.5 text-sm text-foreground">
82
+ <CreditCard className="h-3.5 w-3.5 text-primary" /> Pix à vista
83
+ </p>
84
+ ) : null}
85
+ </C.StepRailItem>
86
+ ) : null}
87
+ </ol>
88
+ </div>
89
+ )
90
+ }
@@ -0,0 +1,14 @@
1
+ import type { BookingComponents } from '../types'
2
+ import { DefaultBrandHeader } from './BrandHeader'
3
+ import { DefaultServiceCard } from './ServiceCard'
4
+ import { DefaultSidebarExtras } from './SidebarExtras'
5
+ import { DefaultStepRailItem } from './StepRailItem'
6
+ import { DefaultSuccessPanel } from './SuccessPanel'
7
+
8
+ export const DEFAULT_BOOKING_COMPONENTS: BookingComponents = {
9
+ BrandHeader: DefaultBrandHeader,
10
+ ServiceCard: DefaultServiceCard,
11
+ SidebarExtras: DefaultSidebarExtras,
12
+ StepRailItem: DefaultStepRailItem,
13
+ SuccessPanel: DefaultSuccessPanel,
14
+ }
@@ -1,18 +1,28 @@
1
- import { createContext, useContext, type ReactNode } from 'react'
1
+ import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from 'react'
2
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'
3
+ import type { PublicBookingDataProvider } from './data'
4
+ import type {
5
+ BookingComponents,
6
+ BookingWindow,
7
+ PublicBookingLabels,
8
+ PublicService,
9
+ ResolvedBookingBrand,
10
+ ResolvedPayment,
11
+ WorkingHours,
12
+ } from './types'
5
13
 
6
14
  /** Emitted when the customer's phone is verified / identity captured, for the
7
15
  * host to bridge to auth (booking stays decoupled — data-only, optional). */
8
16
  export type OnIdentityVerified = (identity: { name: string; phone: string; email?: string }) => void
9
17
 
10
18
  export interface PublicBookingContextValue {
11
- provider: AgendaDataProvider
12
- /** The professional/resource bookings are created against (POC: single professional). */
13
- professionalId: string
19
+ provider: PublicBookingDataProvider
20
+ /** The professional/resource bookings are created against; null → any. */
21
+ professionalId: string | null
14
22
  professionalName: string
23
+ /** Resolved catalog: static `services` option, or fetched from the provider. */
15
24
  services: PublicService[]
25
+ servicesLoading: boolean
16
26
  /** Business opening hours, shown on the overview "Sobre" section. */
17
27
  businessHours: WorkingHours
18
28
  /** Payment step config, or null when the flow ends at confirmation. */
@@ -23,6 +33,10 @@ export interface PublicBookingContextValue {
23
33
  onIdentityVerified: OnIdentityVerified | null
24
34
  window: BookingWindow
25
35
  labels: PublicBookingLabels
36
+ /** Brand personalization (resolved with defaults). */
37
+ brand: ResolvedBookingBrand
38
+ /** Host-overridable subcomponents (resolved with defaults). */
39
+ components: BookingComponents
26
40
  /** Currency code for price formatting (e.g. 'BRL'). */
27
41
  currency: string
28
42
  locale: string
@@ -30,7 +44,50 @@ export interface PublicBookingContextValue {
30
44
 
31
45
  const PublicBookingContext = createContext<PublicBookingContextValue | null>(null)
32
46
 
33
- export function BookingProvider({ value, children }: { value: PublicBookingContextValue; children: ReactNode }) {
47
+ export interface BookingProviderProps {
48
+ /** Everything but the (possibly async) service catalog. */
49
+ base: Omit<PublicBookingContextValue, 'services' | 'servicesLoading'>
50
+ /** Static catalog from options — wins over the provider fetch when present. */
51
+ staticServices: PublicService[] | null
52
+ children: ReactNode
53
+ }
54
+
55
+ /**
56
+ * Owns the service-catalog resolution: static services pass through untouched;
57
+ * otherwise the provider is queried once (e.g. Supabase v_public_services).
58
+ */
59
+ export function BookingProvider({ base, staticServices, children }: BookingProviderProps) {
60
+ const [fetched, setFetched] = useState<PublicService[] | null>(null)
61
+ const [loading, setLoading] = useState(staticServices === null)
62
+
63
+ useEffect(() => {
64
+ if (staticServices !== null) return
65
+ let active = true
66
+ setLoading(true)
67
+ base.provider
68
+ .getServices()
69
+ .then((result) => {
70
+ if (active) setFetched(result)
71
+ })
72
+ .catch((err) => {
73
+ // Surface in dev; the overview renders its empty state.
74
+ console.error('[plugin-agenda/public] failed to load services:', err)
75
+ if (active) setFetched([])
76
+ })
77
+ .finally(() => {
78
+ if (active) setLoading(false)
79
+ })
80
+ return () => {
81
+ active = false
82
+ }
83
+ // base.provider is stable (created once by the plugin factory).
84
+ }, [base.provider, staticServices])
85
+
86
+ const value = useMemo<PublicBookingContextValue>(
87
+ () => ({ ...base, services: staticServices ?? fetched ?? [], servicesLoading: loading }),
88
+ [base, staticServices, fetched, loading],
89
+ )
90
+
34
91
  return <PublicBookingContext.Provider value={value}>{children}</PublicBookingContext.Provider>
35
92
  }
36
93
 
@@ -1,26 +1,54 @@
1
1
  import { createElement, type FC, type ReactNode } from 'react'
2
+ import { CalendarCheck, ShieldCheck, Video } from 'lucide-react'
2
3
  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
4
  import type { Professional, Schedule } from '../types'
6
- import { createMockAgendaProvider, type MockAgendaSeed } from '../data/mock'
7
- import { createSupabaseAgendaProvider } from '../data/supabase'
5
+ import type { MockAgendaSeed } from '../data/mock'
6
+ import { createMockPublicBookingProvider, type PublicBookingDataProvider } from './data'
7
+ import { createSupabasePublicBookingProvider } from './data.supabase'
8
8
  import { BookingProvider, type PublicBookingContextValue, type OnIdentityVerified } from './context'
9
9
  import { BookingWidget } from './BookingWidget'
10
- import type { PublicService, BookingWindow, PublicBookingLabels, WorkingHours, PaymentConfig, ResolvedPayment } from './types'
10
+ import { DEFAULT_BOOKING_COMPONENTS } from './components/defaults'
11
+ import type {
12
+ BookingBrand,
13
+ BookingComponents,
14
+ BookingWindow,
15
+ PaymentConfig,
16
+ PublicBookingLabels,
17
+ PublicService,
18
+ ResolvedBookingBrand,
19
+ ResolvedPayment,
20
+ WorkingHours,
21
+ } from './types'
11
22
 
12
23
  export interface PublicBookingOptions {
13
24
  /** Mount path for the public booking route. Default '/agendar'. */
14
25
  basePath?: string
15
- /** The single professional/resource bookings target (POC). */
26
+ /**
27
+ * Tenant this public page books against. REQUIRED when a Supabase client is
28
+ * configured (setGlobalSupabaseClient) — it scopes services/slots/bookings.
29
+ */
30
+ tenantId?: string
31
+ /**
32
+ * The single professional/resource bookings target. With Supabase this must
33
+ * be the staff person UUID; omit → any active professional (RPC unions
34
+ * schedules across staff).
35
+ */
16
36
  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. */
37
+ /**
38
+ * Bookable services. Optional: when omitted, the widget fetches the catalog
39
+ * from the provider (Supabase v_public_services). Static wins when provided.
40
+ */
41
+ services?: PublicService[]
42
+ /** Working hours used to seed mock availability + the overview "Sobre" card. */
20
43
  workingHours?: WorkingHours
21
44
  /** Booking-window bounds for the date/slot picker. */
22
45
  window?: Partial<BookingWindow>
46
+ /** Every user-visible string (all defaulted, pt-BR). */
23
47
  labels?: Partial<PublicBookingLabels>
48
+ /** Brand personalization: name/logo/tagline/icons/badges/highlights. */
49
+ brand?: BookingBrand
50
+ /** Swap whole subcomponents when structure must change (defaults exported). */
51
+ components?: Partial<BookingComponents>
24
52
  /** Adds a final Pagamento (Pix) step. Omit/disable to end the flow at confirmation. */
25
53
  payment?: PaymentConfig
26
54
  /** Charge provider (from a payments plugin) used by the Pagamento step. Optional. */
@@ -29,8 +57,8 @@ export interface PublicBookingOptions {
29
57
  onIdentityVerified?: OnIdentityVerified
30
58
  currency?: string
31
59
  locale?: string
32
- /** Inject a custom provider (real availability). Overrides the safe resolver. */
33
- dataProvider?: AgendaDataProvider
60
+ /** Inject a custom provider (overrides the mock/Supabase safe resolver). */
61
+ dataProvider?: PublicBookingDataProvider
34
62
  /** Override the full mock seed (advanced). */
35
63
  seed?: MockAgendaSeed
36
64
  scope?: PluginScope
@@ -40,7 +68,7 @@ export interface PublicBookingOptions {
40
68
  export interface PublicBookingPlugin {
41
69
  manifest: PluginManifest
42
70
  Provider: FC<{ children: ReactNode }>
43
- dataProvider: AgendaDataProvider
71
+ dataProvider: PublicBookingDataProvider
44
72
  }
45
73
 
46
74
  const DEFAULT_LABELS: PublicBookingLabels = {
@@ -51,12 +79,75 @@ const DEFAULT_LABELS: PublicBookingLabels = {
51
79
  confirmCta: 'Confirmar agendamento',
52
80
  successTitle: 'Agendamento confirmado!',
53
81
  successBody: 'Você receberá os detalhes em breve. Até logo!',
82
+ // overview
83
+ servicesHeading: 'Serviços',
84
+ aboutHeading: 'Sobre',
85
+ openingHoursHeading: 'Horário de funcionamento',
86
+ closedLabel: 'Fechado',
87
+ bookNowCta: 'Agendar agora',
88
+ // datetime
89
+ timezoneNote: 'Fuso: Horário de Brasília',
90
+ slotsHeading: 'Horários disponíveis',
91
+ noSlots: 'Nenhum horário disponível neste dia. Tente outra data.',
92
+ continueCta: 'Continuar',
93
+ backToServices: 'Serviços',
94
+ back: 'Voltar',
95
+ prevWeek: 'Semana anterior',
96
+ nextWeek: 'Próxima semana',
97
+ // contact
98
+ contactIntro: 'Preencha as informações abaixo para que seu agendamento seja identificado.',
99
+ phoneLabel: 'Celular',
100
+ phoneHint:
101
+ '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.',
102
+ sendCodeCta: 'Enviar código via WhatsApp',
103
+ codeTitle: 'Insira o código',
104
+ codeBody: 'Enviamos um código via WhatsApp para você confirmar seu número.',
105
+ codeInvalid: 'Código inválido. Para testar, use 0000.',
106
+ codeConfirmCta: 'Confirmar',
107
+ codeResendHint: 'Reenviar código via WhatsApp em 28 segundos.',
108
+ phoneVerified: 'Celular verificado',
109
+ changePhone: 'Trocar telefone',
110
+ nameLabel: 'Nome completo *',
111
+ namePlaceholder: 'Seu nome',
112
+ emailLabel: 'E-mail',
113
+ emailPlaceholder: 'voce@email.com',
114
+ emailHint: 'Enviaremos uma confirmação e um lembrete antes do agendamento.',
115
+ notesLabel: 'Observação',
116
+ notesPlaceholder: 'Use esse espaço para deixar uma observação sobre este agendamento',
117
+ saveAndContinueCta: 'Salvar e continuar',
118
+ submitError: 'Não foi possível concluir o agendamento. Tente novamente.',
119
+ noChargeNote: 'Pagamento via Pix · Sem cobrança agora',
120
+ // right rail
121
+ railTitle: 'Passos para agendar',
122
+ railWaitingPayment: 'Aguardando pagamento',
123
+ railService: 'Serviço',
124
+ railProfessional: 'Profissional',
125
+ railDatetime: 'Data e horário',
126
+ railContact: 'Informações pessoais',
127
+ railPayment: 'Pagamento',
54
128
  }
55
129
 
56
130
  const DEFAULT_WINDOW: BookingWindow = { minAdvanceHours: 2, maxAdvanceDays: 21, slotInterval: 30 }
57
131
 
58
132
  const DEFAULT_HOURS: WorkingHours = { daysOfWeek: [1, 2, 3, 4, 5, 6], start: '09:00', end: '18:00' }
59
133
 
134
+ function resolveBrand(brand: BookingBrand | undefined, labels: PublicBookingLabels): ResolvedBookingBrand {
135
+ const name = brand?.name ?? labels.title
136
+ return {
137
+ name,
138
+ initials: brand?.initials ?? name.charAt(0).toUpperCase(),
139
+ logoUrl: brand?.logoUrl,
140
+ tagline: brand?.tagline === undefined ? 'Agendamento online seguro' : brand.tagline,
141
+ serviceIcon: brand?.serviceIcon ?? Video,
142
+ serviceMeta: brand?.serviceMeta === undefined ? 'Online' : brand.serviceMeta,
143
+ featuredBadge: brand?.featuredBadge === undefined ? 'Mais procurado' : brand.featuredBadge,
144
+ highlights: brand?.highlights ?? [
145
+ { icon: CalendarCheck, text: 'Confirmação imediata' },
146
+ { icon: ShieldCheck, text: 'Agendamento seguro' },
147
+ ],
148
+ }
149
+ }
150
+
60
151
  let scheduleId = 0
61
152
 
62
153
  function buildSchedules(professionalId: string, hours: WorkingHours): Schedule[] {
@@ -77,13 +168,18 @@ function buildSchedules(professionalId: string, hours: WorkingHours): Schedule[]
77
168
 
78
169
  export function createPublicBookingPlugin(options: PublicBookingOptions): PublicBookingPlugin {
79
170
  const basePath = options.basePath ?? '/agendar'
80
- const professionalId = options.professional?.id ?? 'dr-hiago'
171
+ // Mock needs a concrete seed professional; Supabase can target any (null).
172
+ const mockProfessionalId = options.professional?.id ?? 'pro-1'
173
+ const professionalId = options.professional?.id ?? null
81
174
  const professionalName = options.professional?.name ?? 'Especialista'
82
175
  const window: BookingWindow = { ...DEFAULT_WINDOW, ...options.window }
83
176
  const labels: PublicBookingLabels = { ...DEFAULT_LABELS, ...options.labels }
177
+ const brand = resolveBrand(options.brand, labels)
178
+ const components: BookingComponents = { ...DEFAULT_BOOKING_COMPONENTS, ...options.components }
84
179
  const currency = options.currency ?? 'BRL'
85
180
  const locale = options.locale ?? 'pt-BR'
86
181
  const hours = options.workingHours ?? DEFAULT_HOURS
182
+ const staticServices = options.services ?? null
87
183
  const payment: ResolvedPayment | null = options.payment?.enabled
88
184
  ? {
89
185
  reservationFeePercent: options.payment.reservationFeePercent ?? 100,
@@ -92,7 +188,7 @@ export function createPublicBookingPlugin(options: PublicBookingOptions): Public
92
188
  : null
93
189
 
94
190
  const professional: Professional = {
95
- id: professionalId,
191
+ id: mockProfessionalId,
96
192
  name: professionalName,
97
193
  avatarUrl: null,
98
194
  locationId: null,
@@ -101,33 +197,50 @@ export function createPublicBookingPlugin(options: PublicBookingOptions): Public
101
197
  }
102
198
  const mockSeed: MockAgendaSeed = options.seed ?? {
103
199
  professionals: [professional],
104
- schedules: buildSchedules(professionalId, hours),
200
+ schedules: buildSchedules(mockProfessionalId, hours),
105
201
  bookings: [],
106
202
  }
107
203
 
108
- const provider =
109
- options.dataProvider ??
110
- createSafeDataProvider(
111
- () => createSupabaseAgendaProvider(),
112
- () => createMockAgendaProvider({ seed: mockSeed }),
113
- )
204
+ // Resolution: explicit dataProvider → Supabase whenever a tenantId is
205
+ // declared (the app's global client if registered, else Fayz Cloud — the
206
+ // shared hosted backend) → seeded mock. The mock fallback is LOUD on
207
+ // purpose: the old silent fallback hid missing-env bugs as empty catalogs.
208
+ let provider: PublicBookingDataProvider
209
+ if (options.dataProvider) {
210
+ provider = options.dataProvider
211
+ } else if (options.tenantId) {
212
+ provider = createSupabasePublicBookingProvider({ tenantId: options.tenantId })
213
+ } else {
214
+ if (typeof console !== 'undefined') {
215
+ console.warn(
216
+ '[plugin-agenda/public] no `tenantId` and no `dataProvider` — falling back to the seeded MOCK provider. ' +
217
+ 'Pass `tenantId` to book against Fayz Cloud, or inject a dataProvider.',
218
+ )
219
+ }
220
+ provider = createMockPublicBookingProvider({
221
+ seed: mockSeed,
222
+ services: staticServices ?? [],
223
+ professionalId: mockProfessionalId,
224
+ })
225
+ }
114
226
 
115
- const value: PublicBookingContextValue = {
227
+ const base: Omit<PublicBookingContextValue, 'services' | 'servicesLoading'> = {
116
228
  provider,
117
229
  professionalId,
118
230
  professionalName,
119
- services: options.services,
120
231
  businessHours: hours,
121
232
  payment,
122
233
  paymentProvider: options.paymentProvider ?? null,
123
234
  onIdentityVerified: options.onIdentityVerified ?? null,
124
235
  window,
125
236
  labels,
237
+ brand,
238
+ components,
126
239
  currency,
127
240
  locale,
128
241
  }
129
242
  const Provider: FC<{ children: ReactNode }> = ({ children }) =>
130
- createElement(BookingProvider, { value, children })
243
+ createElement(BookingProvider, { base, staticServices, children })
131
244
  Provider.displayName = 'PublicBookingProvider'
132
245
 
133
246
  const BookingScreen: FC<unknown> = () =>
@@ -140,7 +253,7 @@ export function createPublicBookingPlugin(options: PublicBookingOptions): Public
140
253
  id: 'booking',
141
254
  name: 'Agendamento',
142
255
  icon: 'CalendarCheck',
143
- version: '0.1.0',
256
+ version: '0.4.0',
144
257
  scope: options.scope ?? 'universal',
145
258
  verticalId: options.verticalId,
146
259
  scaffolds: ['website', 'landing_page'],