@fayz-ai/plugin-agenda 0.2.1 → 0.3.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 (83) hide show
  1. package/dist/chunk-DRZF6CV2.js +421 -0
  2. package/dist/chunk-DRZF6CV2.js.map +1 -0
  3. package/dist/chunk-LFUELUIT.cjs +423 -0
  4. package/dist/chunk-LFUELUIT.cjs.map +1 -0
  5. package/dist/index.cjs +741 -10
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +741 -10
  9. package/dist/index.js.map +1 -1
  10. package/dist/migrations/index.d.ts +2 -0
  11. package/dist/migrations/index.d.ts.map +1 -0
  12. package/dist/public/BookingWidget.d.ts +4 -2
  13. package/dist/public/BookingWidget.d.ts.map +1 -1
  14. package/dist/public/components/BrandHeader.d.ts +6 -0
  15. package/dist/public/components/BrandHeader.d.ts.map +1 -0
  16. package/dist/public/components/ServiceCard.d.ts +5 -0
  17. package/dist/public/components/ServiceCard.d.ts.map +1 -0
  18. package/dist/public/components/SidebarExtras.d.ts +4 -0
  19. package/dist/public/components/SidebarExtras.d.ts.map +1 -0
  20. package/dist/public/components/StepRailItem.d.ts +4 -0
  21. package/dist/public/components/StepRailItem.d.ts.map +1 -0
  22. package/dist/public/components/SuccessPanel.d.ts +4 -0
  23. package/dist/public/components/SuccessPanel.d.ts.map +1 -0
  24. package/dist/public/components/SummaryRail.d.ts +27 -0
  25. package/dist/public/components/SummaryRail.d.ts.map +1 -0
  26. package/dist/public/components/defaults.d.ts +3 -0
  27. package/dist/public/components/defaults.d.ts.map +1 -0
  28. package/dist/public/context.d.ts +22 -8
  29. package/dist/public/context.d.ts.map +1 -1
  30. package/dist/public/createPublicBookingPlugin.d.ts +27 -10
  31. package/dist/public/createPublicBookingPlugin.d.ts.map +1 -1
  32. package/dist/public/data.d.ts +37 -0
  33. package/dist/public/data.d.ts.map +1 -0
  34. package/dist/public/data.supabase.d.ts +7 -0
  35. package/dist/public/data.supabase.d.ts.map +1 -0
  36. package/dist/public/format.d.ts +18 -0
  37. package/dist/public/format.d.ts.map +1 -0
  38. package/dist/public/hooks.d.ts +13 -11
  39. package/dist/public/hooks.d.ts.map +1 -1
  40. package/dist/public/index.cjs +1016 -613
  41. package/dist/public/index.cjs.map +1 -1
  42. package/dist/public/index.d.ts +15 -4
  43. package/dist/public/index.d.ts.map +1 -1
  44. package/dist/public/index.js +1006 -616
  45. package/dist/public/index.js.map +1 -1
  46. package/dist/public/steps/ContactStep.d.ts +30 -0
  47. package/dist/public/steps/ContactStep.d.ts.map +1 -0
  48. package/dist/public/steps/DateTimeStep.d.ts +23 -0
  49. package/dist/public/steps/DateTimeStep.d.ts.map +1 -0
  50. package/dist/public/steps/OverviewStep.d.ts +16 -0
  51. package/dist/public/steps/OverviewStep.d.ts.map +1 -0
  52. package/dist/public/steps/PaymentStep.d.ts +20 -0
  53. package/dist/public/steps/PaymentStep.d.ts.map +1 -0
  54. package/dist/public/types.d.ts +134 -0
  55. package/dist/public/types.d.ts.map +1 -1
  56. package/package.json +12 -12
  57. package/src/index.ts +11 -0
  58. package/src/migrations/001_public_booking.sql +278 -0
  59. package/src/migrations/index.ts +282 -0
  60. package/src/public/BookingWidget.tsx +136 -713
  61. package/src/public/components/BrandHeader.tsx +27 -0
  62. package/src/public/components/ServiceCard.tsx +39 -0
  63. package/src/public/components/SidebarExtras.tsx +30 -0
  64. package/src/public/components/StepRailItem.tsx +27 -0
  65. package/src/public/components/SuccessPanel.tsx +27 -0
  66. package/src/public/components/SummaryRail.tsx +90 -0
  67. package/src/public/components/defaults.ts +14 -0
  68. package/src/public/context.tsx +64 -7
  69. package/src/public/createPublicBookingPlugin.ts +131 -21
  70. package/src/public/data.supabase.ts +94 -0
  71. package/src/public/data.ts +99 -0
  72. package/src/public/format.ts +61 -0
  73. package/src/public/hooks.ts +22 -34
  74. package/src/public/index.ts +57 -4
  75. package/src/public/steps/ContactStep.tsx +179 -0
  76. package/src/public/steps/DateTimeStep.tsx +138 -0
  77. package/src/public/steps/OverviewStep.tsx +81 -0
  78. package/src/public/steps/PaymentStep.tsx +180 -0
  79. package/src/public/types.ts +169 -3
  80. package/dist/chunk-GAJVIEXM.cjs +0 -870
  81. package/dist/chunk-GAJVIEXM.cjs.map +0 -1
  82. package/dist/chunk-U6VTLOHN.js +0 -867
  83. 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,55 @@
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
4
  import { createSafeDataProvider } from '@fayz-ai/core'
4
- import type { AgendaDataProvider } from '../data/types'
5
5
  import type { Professional, Schedule } from '../types'
6
- import { createMockAgendaProvider, type MockAgendaSeed } from '../data/mock'
7
- import { createSupabaseAgendaProvider } from '../data/supabase'
6
+ import type { MockAgendaSeed } from '../data/mock'
7
+ import { createMockPublicBookingProvider, type PublicBookingDataProvider } from './data'
8
+ import { createSupabasePublicBookingProvider } from './data.supabase'
8
9
  import { BookingProvider, type PublicBookingContextValue, type OnIdentityVerified } from './context'
9
10
  import { BookingWidget } from './BookingWidget'
10
- import type { PublicService, BookingWindow, PublicBookingLabels, WorkingHours, PaymentConfig, ResolvedPayment } from './types'
11
+ import { DEFAULT_BOOKING_COMPONENTS } from './components/defaults'
12
+ import type {
13
+ BookingBrand,
14
+ BookingComponents,
15
+ BookingWindow,
16
+ PaymentConfig,
17
+ PublicBookingLabels,
18
+ PublicService,
19
+ ResolvedBookingBrand,
20
+ ResolvedPayment,
21
+ WorkingHours,
22
+ } from './types'
11
23
 
12
24
  export interface PublicBookingOptions {
13
25
  /** Mount path for the public booking route. Default '/agendar'. */
14
26
  basePath?: string
15
- /** The single professional/resource bookings target (POC). */
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
+ */
16
37
  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. */
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. */
20
44
  workingHours?: WorkingHours
21
45
  /** Booking-window bounds for the date/slot picker. */
22
46
  window?: Partial<BookingWindow>
47
+ /** Every user-visible string (all defaulted, pt-BR). */
23
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>
24
53
  /** Adds a final Pagamento (Pix) step. Omit/disable to end the flow at confirmation. */
25
54
  payment?: PaymentConfig
26
55
  /** Charge provider (from a payments plugin) used by the Pagamento step. Optional. */
@@ -29,8 +58,8 @@ export interface PublicBookingOptions {
29
58
  onIdentityVerified?: OnIdentityVerified
30
59
  currency?: string
31
60
  locale?: string
32
- /** Inject a custom provider (real availability). Overrides the safe resolver. */
33
- dataProvider?: AgendaDataProvider
61
+ /** Inject a custom provider (overrides the mock/Supabase safe resolver). */
62
+ dataProvider?: PublicBookingDataProvider
34
63
  /** Override the full mock seed (advanced). */
35
64
  seed?: MockAgendaSeed
36
65
  scope?: PluginScope
@@ -40,7 +69,7 @@ export interface PublicBookingOptions {
40
69
  export interface PublicBookingPlugin {
41
70
  manifest: PluginManifest
42
71
  Provider: FC<{ children: ReactNode }>
43
- dataProvider: AgendaDataProvider
72
+ dataProvider: PublicBookingDataProvider
44
73
  }
45
74
 
46
75
  const DEFAULT_LABELS: PublicBookingLabels = {
@@ -51,12 +80,75 @@ const DEFAULT_LABELS: PublicBookingLabels = {
51
80
  confirmCta: 'Confirmar agendamento',
52
81
  successTitle: 'Agendamento confirmado!',
53
82
  successBody: 'Você receberá os detalhes em breve. Até logo!',
83
+ // overview
84
+ servicesHeading: 'Serviços',
85
+ aboutHeading: 'Sobre',
86
+ openingHoursHeading: 'Horário de funcionamento',
87
+ closedLabel: 'Fechado',
88
+ bookNowCta: 'Agendar agora',
89
+ // datetime
90
+ timezoneNote: 'Fuso: Horário de Brasília',
91
+ slotsHeading: 'Horários disponíveis',
92
+ noSlots: 'Nenhum horário disponível neste dia. Tente outra data.',
93
+ continueCta: 'Continuar',
94
+ backToServices: 'Serviços',
95
+ back: 'Voltar',
96
+ prevWeek: 'Semana anterior',
97
+ nextWeek: 'Próxima semana',
98
+ // contact
99
+ contactIntro: 'Preencha as informações abaixo para que seu agendamento seja identificado.',
100
+ phoneLabel: 'Celular',
101
+ phoneHint:
102
+ '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.',
103
+ sendCodeCta: 'Enviar código via WhatsApp',
104
+ codeTitle: 'Insira o código',
105
+ codeBody: 'Enviamos um código via WhatsApp para você confirmar seu número.',
106
+ codeInvalid: 'Código inválido. Para testar, use 0000.',
107
+ codeConfirmCta: 'Confirmar',
108
+ codeResendHint: 'Reenviar código via WhatsApp em 28 segundos.',
109
+ phoneVerified: 'Celular verificado',
110
+ changePhone: 'Trocar telefone',
111
+ nameLabel: 'Nome completo *',
112
+ namePlaceholder: 'Seu nome',
113
+ emailLabel: 'E-mail',
114
+ emailPlaceholder: 'voce@email.com',
115
+ emailHint: 'Enviaremos uma confirmação e um lembrete antes do agendamento.',
116
+ notesLabel: 'Observação',
117
+ notesPlaceholder: 'Use esse espaço para deixar uma observação sobre este agendamento',
118
+ saveAndContinueCta: 'Salvar e continuar',
119
+ submitError: 'Não foi possível concluir o agendamento. Tente novamente.',
120
+ noChargeNote: 'Pagamento via Pix · Sem cobrança agora',
121
+ // right rail
122
+ railTitle: 'Passos para agendar',
123
+ railWaitingPayment: 'Aguardando pagamento',
124
+ railService: 'Serviço',
125
+ railProfessional: 'Profissional',
126
+ railDatetime: 'Data e horário',
127
+ railContact: 'Informações pessoais',
128
+ railPayment: 'Pagamento',
54
129
  }
55
130
 
56
131
  const DEFAULT_WINDOW: BookingWindow = { minAdvanceHours: 2, maxAdvanceDays: 21, slotInterval: 30 }
57
132
 
58
133
  const DEFAULT_HOURS: WorkingHours = { daysOfWeek: [1, 2, 3, 4, 5, 6], start: '09:00', end: '18:00' }
59
134
 
135
+ function resolveBrand(brand: BookingBrand | undefined, labels: PublicBookingLabels): ResolvedBookingBrand {
136
+ const name = brand?.name ?? labels.title
137
+ return {
138
+ name,
139
+ initials: brand?.initials ?? name.charAt(0).toUpperCase(),
140
+ logoUrl: brand?.logoUrl,
141
+ tagline: brand?.tagline === undefined ? 'Agendamento online seguro' : brand.tagline,
142
+ serviceIcon: brand?.serviceIcon ?? Video,
143
+ serviceMeta: brand?.serviceMeta === undefined ? 'Online' : brand.serviceMeta,
144
+ featuredBadge: brand?.featuredBadge === undefined ? 'Mais procurado' : brand.featuredBadge,
145
+ highlights: brand?.highlights ?? [
146
+ { icon: CalendarCheck, text: 'Confirmação imediata' },
147
+ { icon: ShieldCheck, text: 'Agendamento seguro' },
148
+ ],
149
+ }
150
+ }
151
+
60
152
  let scheduleId = 0
61
153
 
62
154
  function buildSchedules(professionalId: string, hours: WorkingHours): Schedule[] {
@@ -77,13 +169,18 @@ function buildSchedules(professionalId: string, hours: WorkingHours): Schedule[]
77
169
 
78
170
  export function createPublicBookingPlugin(options: PublicBookingOptions): PublicBookingPlugin {
79
171
  const basePath = options.basePath ?? '/agendar'
80
- const professionalId = options.professional?.id ?? 'dr-hiago'
172
+ // Mock needs a concrete seed professional; Supabase can target any (null).
173
+ const mockProfessionalId = options.professional?.id ?? 'pro-1'
174
+ const professionalId = options.professional?.id ?? null
81
175
  const professionalName = options.professional?.name ?? 'Especialista'
82
176
  const window: BookingWindow = { ...DEFAULT_WINDOW, ...options.window }
83
177
  const labels: PublicBookingLabels = { ...DEFAULT_LABELS, ...options.labels }
178
+ const brand = resolveBrand(options.brand, labels)
179
+ const components: BookingComponents = { ...DEFAULT_BOOKING_COMPONENTS, ...options.components }
84
180
  const currency = options.currency ?? 'BRL'
85
181
  const locale = options.locale ?? 'pt-BR'
86
182
  const hours = options.workingHours ?? DEFAULT_HOURS
183
+ const staticServices = options.services ?? null
87
184
  const payment: ResolvedPayment | null = options.payment?.enabled
88
185
  ? {
89
186
  reservationFeePercent: options.payment.reservationFeePercent ?? 100,
@@ -92,7 +189,7 @@ export function createPublicBookingPlugin(options: PublicBookingOptions): Public
92
189
  : null
93
190
 
94
191
  const professional: Professional = {
95
- id: professionalId,
192
+ id: mockProfessionalId,
96
193
  name: professionalName,
97
194
  avatarUrl: null,
98
195
  locationId: null,
@@ -101,33 +198,46 @@ export function createPublicBookingPlugin(options: PublicBookingOptions): Public
101
198
  }
102
199
  const mockSeed: MockAgendaSeed = options.seed ?? {
103
200
  professionals: [professional],
104
- schedules: buildSchedules(professionalId, hours),
201
+ schedules: buildSchedules(mockProfessionalId, hours),
105
202
  bookings: [],
106
203
  }
107
204
 
108
205
  const provider =
109
206
  options.dataProvider ??
110
- createSafeDataProvider(
111
- () => createSupabaseAgendaProvider(),
112
- () => createMockAgendaProvider({ seed: mockSeed }),
207
+ createSafeDataProvider<PublicBookingDataProvider>(
208
+ () => {
209
+ if (!options.tenantId) {
210
+ throw new Error(
211
+ '[plugin-agenda/public] `tenantId` is required when a Supabase client is configured (setGlobalSupabaseClient).',
212
+ )
213
+ }
214
+ return createSupabasePublicBookingProvider({ tenantId: options.tenantId })
215
+ },
216
+ () =>
217
+ createMockPublicBookingProvider({
218
+ seed: mockSeed,
219
+ services: staticServices ?? [],
220
+ professionalId: mockProfessionalId,
221
+ }),
113
222
  )
114
223
 
115
- const value: PublicBookingContextValue = {
224
+ const base: Omit<PublicBookingContextValue, 'services' | 'servicesLoading'> = {
116
225
  provider,
117
226
  professionalId,
118
227
  professionalName,
119
- services: options.services,
120
228
  businessHours: hours,
121
229
  payment,
122
230
  paymentProvider: options.paymentProvider ?? null,
123
231
  onIdentityVerified: options.onIdentityVerified ?? null,
124
232
  window,
125
233
  labels,
234
+ brand,
235
+ components,
126
236
  currency,
127
237
  locale,
128
238
  }
129
239
  const Provider: FC<{ children: ReactNode }> = ({ children }) =>
130
- createElement(BookingProvider, { value, children })
240
+ createElement(BookingProvider, { base, staticServices, children })
131
241
  Provider.displayName = 'PublicBookingProvider'
132
242
 
133
243
  const BookingScreen: FC<unknown> = () =>
@@ -140,7 +250,7 @@ export function createPublicBookingPlugin(options: PublicBookingOptions): Public
140
250
  id: 'booking',
141
251
  name: 'Agendamento',
142
252
  icon: 'CalendarCheck',
143
- version: '0.1.0',
253
+ version: '0.3.0',
144
254
  scope: options.scope ?? 'universal',
145
255
  verticalId: options.verticalId,
146
256
  scaffolds: ['website', 'landing_page'],