@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.
- package/dist/chunk-DRZF6CV2.js +421 -0
- package/dist/chunk-DRZF6CV2.js.map +1 -0
- package/dist/chunk-LFUELUIT.cjs +423 -0
- package/dist/chunk-LFUELUIT.cjs.map +1 -0
- package/dist/index.cjs +741 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +741 -10
- package/dist/index.js.map +1 -1
- package/dist/migrations/index.d.ts +2 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/public/BookingWidget.d.ts +4 -2
- package/dist/public/BookingWidget.d.ts.map +1 -1
- package/dist/public/components/BrandHeader.d.ts +6 -0
- package/dist/public/components/BrandHeader.d.ts.map +1 -0
- package/dist/public/components/ServiceCard.d.ts +5 -0
- package/dist/public/components/ServiceCard.d.ts.map +1 -0
- package/dist/public/components/SidebarExtras.d.ts +4 -0
- package/dist/public/components/SidebarExtras.d.ts.map +1 -0
- package/dist/public/components/StepRailItem.d.ts +4 -0
- package/dist/public/components/StepRailItem.d.ts.map +1 -0
- package/dist/public/components/SuccessPanel.d.ts +4 -0
- package/dist/public/components/SuccessPanel.d.ts.map +1 -0
- package/dist/public/components/SummaryRail.d.ts +27 -0
- package/dist/public/components/SummaryRail.d.ts.map +1 -0
- package/dist/public/components/defaults.d.ts +3 -0
- package/dist/public/components/defaults.d.ts.map +1 -0
- package/dist/public/context.d.ts +22 -8
- package/dist/public/context.d.ts.map +1 -1
- package/dist/public/createPublicBookingPlugin.d.ts +27 -10
- package/dist/public/createPublicBookingPlugin.d.ts.map +1 -1
- package/dist/public/data.d.ts +37 -0
- package/dist/public/data.d.ts.map +1 -0
- package/dist/public/data.supabase.d.ts +7 -0
- package/dist/public/data.supabase.d.ts.map +1 -0
- package/dist/public/format.d.ts +18 -0
- package/dist/public/format.d.ts.map +1 -0
- package/dist/public/hooks.d.ts +13 -11
- package/dist/public/hooks.d.ts.map +1 -1
- package/dist/public/index.cjs +1016 -613
- package/dist/public/index.cjs.map +1 -1
- package/dist/public/index.d.ts +15 -4
- package/dist/public/index.d.ts.map +1 -1
- package/dist/public/index.js +1006 -616
- package/dist/public/index.js.map +1 -1
- package/dist/public/steps/ContactStep.d.ts +30 -0
- package/dist/public/steps/ContactStep.d.ts.map +1 -0
- package/dist/public/steps/DateTimeStep.d.ts +23 -0
- package/dist/public/steps/DateTimeStep.d.ts.map +1 -0
- package/dist/public/steps/OverviewStep.d.ts +16 -0
- package/dist/public/steps/OverviewStep.d.ts.map +1 -0
- package/dist/public/steps/PaymentStep.d.ts +20 -0
- package/dist/public/steps/PaymentStep.d.ts.map +1 -0
- package/dist/public/types.d.ts +134 -0
- package/dist/public/types.d.ts.map +1 -1
- package/package.json +12 -12
- package/src/index.ts +11 -0
- package/src/migrations/001_public_booking.sql +278 -0
- package/src/migrations/index.ts +282 -0
- package/src/public/BookingWidget.tsx +136 -713
- package/src/public/components/BrandHeader.tsx +27 -0
- package/src/public/components/ServiceCard.tsx +39 -0
- package/src/public/components/SidebarExtras.tsx +30 -0
- package/src/public/components/StepRailItem.tsx +27 -0
- package/src/public/components/SuccessPanel.tsx +27 -0
- package/src/public/components/SummaryRail.tsx +90 -0
- package/src/public/components/defaults.ts +14 -0
- package/src/public/context.tsx +64 -7
- package/src/public/createPublicBookingPlugin.ts +131 -21
- package/src/public/data.supabase.ts +94 -0
- package/src/public/data.ts +99 -0
- package/src/public/format.ts +61 -0
- package/src/public/hooks.ts +22 -34
- package/src/public/index.ts +57 -4
- package/src/public/steps/ContactStep.tsx +179 -0
- package/src/public/steps/DateTimeStep.tsx +138 -0
- package/src/public/steps/OverviewStep.tsx +81 -0
- package/src/public/steps/PaymentStep.tsx +180 -0
- package/src/public/types.ts +169 -3
- package/dist/chunk-GAJVIEXM.cjs +0 -870
- package/dist/chunk-GAJVIEXM.cjs.map +0 -1
- package/dist/chunk-U6VTLOHN.js +0 -867
- 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
|
+
}
|
package/src/public/context.tsx
CHANGED
|
@@ -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 {
|
|
4
|
-
import type {
|
|
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:
|
|
12
|
-
/** The professional/resource bookings are created against
|
|
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
|
|
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 {
|
|
7
|
-
import {
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
18
|
-
|
|
19
|
-
|
|
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 (
|
|
33
|
-
dataProvider?:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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(
|
|
201
|
+
schedules: buildSchedules(mockProfessionalId, hours),
|
|
105
202
|
bookings: [],
|
|
106
203
|
}
|
|
107
204
|
|
|
108
205
|
const provider =
|
|
109
206
|
options.dataProvider ??
|
|
110
|
-
createSafeDataProvider(
|
|
111
|
-
() =>
|
|
112
|
-
|
|
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
|
|
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, {
|
|
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.
|
|
253
|
+
version: '0.3.0',
|
|
144
254
|
scope: options.scope ?? 'universal',
|
|
145
255
|
verticalId: options.verticalId,
|
|
146
256
|
scaffolds: ['website', 'landing_page'],
|