@fayz-ai/plugin-agenda 0.9.0 → 0.10.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/README.md +3 -3
- package/dist/bridges/create-financial-bridge.d.ts +1 -1
- package/dist/bridges/create-financial-bridge.d.ts.map +1 -1
- package/dist/bridges/financial-bridge.d.ts.map +1 -0
- package/dist/{chunk-Q2PAWBPR.js → chunk-ISLJXP7O.js} +7 -2
- package/dist/chunk-ISLJXP7O.js.map +1 -0
- package/dist/components/AppointmentModal.d.ts.map +1 -1
- package/dist/components/PaymentStatusBadge.d.ts +1 -1
- package/dist/components/PaymentStatusBadge.d.ts.map +1 -1
- package/dist/config.d.ts +2 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/{AgendaContext.d.ts → context.d.ts} +1 -1
- package/dist/context.d.ts.map +1 -0
- package/dist/data/mock.d.ts.map +1 -1
- package/dist/data/realtime.test.d.ts +2 -0
- package/dist/data/realtime.test.d.ts.map +1 -0
- package/dist/data/registries.d.ts.map +1 -0
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/data/types.d.ts +7 -0
- package/dist/data/types.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2515 -187
- package/dist/index.js.map +1 -1
- package/dist/integrations/google-calendar/conflict.d.ts +25 -0
- package/dist/integrations/google-calendar/conflict.d.ts.map +1 -0
- package/dist/integrations/google-calendar/connectorDef.d.ts +12 -0
- package/dist/integrations/google-calendar/connectorDef.d.ts.map +1 -1
- package/dist/integrations/google-calendar/credentials.d.ts +27 -0
- package/dist/integrations/google-calendar/credentials.d.ts.map +1 -0
- package/dist/integrations/google-calendar/data/supabase.d.ts +10 -2
- package/dist/integrations/google-calendar/data/supabase.d.ts.map +1 -1
- package/dist/integrations/google-calendar/data/tables.d.ts +2 -0
- package/dist/integrations/google-calendar/data/tables.d.ts.map +1 -1
- package/dist/integrations/google-calendar/health.d.ts +23 -0
- package/dist/integrations/google-calendar/health.d.ts.map +1 -0
- package/dist/integrations/google-calendar/health.test.d.ts +2 -0
- package/dist/integrations/google-calendar/health.test.d.ts.map +1 -0
- package/dist/integrations/google-calendar/index.d.ts +6 -0
- package/dist/integrations/google-calendar/index.d.ts.map +1 -1
- package/dist/integrations/google-calendar/migrations/index.d.ts +12 -0
- package/dist/integrations/google-calendar/migrations/index.d.ts.map +1 -0
- package/dist/integrations/google-calendar/panel-data.d.ts +26 -0
- package/dist/integrations/google-calendar/panel-data.d.ts.map +1 -0
- package/dist/integrations/google-calendar/panel-data.test.d.ts +2 -0
- package/dist/integrations/google-calendar/panel-data.test.d.ts.map +1 -0
- package/dist/integrations/google-calendar/types.d.ts +17 -0
- package/dist/integrations/google-calendar/types.d.ts.map +1 -1
- package/dist/lib/booking-events.d.ts +5 -0
- package/dist/lib/booking-events.d.ts.map +1 -0
- package/dist/lib/calendar-events.d.ts +8 -0
- package/dist/lib/calendar-events.d.ts.map +1 -0
- package/dist/lib/calendar-events.test.d.ts +2 -0
- package/dist/lib/calendar-events.test.d.ts.map +1 -0
- package/dist/lib/message-events.d.ts +12 -0
- package/dist/lib/message-events.d.ts.map +1 -0
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/pt-BR.d.ts.map +1 -1
- package/dist/public/{data.d.ts → data/index.d.ts} +4 -4
- package/dist/public/data/index.d.ts.map +1 -0
- package/dist/public/{data.supabase.d.ts → data/supabase.d.ts} +2 -2
- package/dist/public/data/supabase.d.ts.map +1 -0
- package/dist/public/{hooks.d.ts → hooks/index.d.ts} +4 -4
- package/dist/public/hooks/index.d.ts.map +1 -0
- package/dist/public/index.d.ts +4 -4
- package/dist/public/index.d.ts.map +1 -1
- package/dist/public/index.js +3 -3
- package/dist/public/index.js.map +1 -1
- package/dist/public/{format.d.ts → lib/format.d.ts} +1 -1
- package/dist/public/lib/format.d.ts.map +1 -0
- package/dist/public/views/BookingWidget.d.ts.map +1 -0
- package/dist/public/{steps → views/steps}/ContactStep.d.ts +1 -1
- package/dist/public/views/steps/ContactStep.d.ts.map +1 -0
- package/dist/public/{steps → views/steps}/DateTimeStep.d.ts +3 -3
- package/dist/public/views/steps/DateTimeStep.d.ts.map +1 -0
- package/dist/public/{steps → views/steps}/OverviewStep.d.ts +1 -1
- package/dist/public/views/steps/OverviewStep.d.ts.map +1 -0
- package/dist/public/{steps → views/steps}/PaymentStep.d.ts +1 -1
- package/dist/public/views/steps/PaymentStep.d.ts.map +1 -0
- package/dist/store.d.ts +1 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/styles/calendar-styles.d.ts.map +1 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/{AgendaPage.d.ts → views/AgendaPage.d.ts} +3 -3
- package/dist/views/AgendaPage.d.ts.map +1 -0
- package/dist/views/CalendarView.d.ts.map +1 -1
- package/package.json +9 -11
- package/dist/AgendaContext.d.ts.map +0 -1
- package/dist/AgendaPage.d.ts.map +0 -1
- package/dist/calendar-styles.d.ts.map +0 -1
- package/dist/chunk-Q2PAWBPR.js.map +0 -1
- package/dist/financial-bridge.d.ts.map +0 -1
- package/dist/integrations/google-calendar/GoogleCalendarSettings.d.ts +0 -3
- package/dist/integrations/google-calendar/GoogleCalendarSettings.d.ts.map +0 -1
- package/dist/public/BookingWidget.d.ts.map +0 -1
- package/dist/public/data.d.ts.map +0 -1
- package/dist/public/data.supabase.d.ts.map +0 -1
- package/dist/public/format.d.ts.map +0 -1
- package/dist/public/hooks.d.ts.map +0 -1
- package/dist/public/steps/ContactStep.d.ts.map +0 -1
- package/dist/public/steps/DateTimeStep.d.ts.map +0 -1
- package/dist/public/steps/OverviewStep.d.ts.map +0 -1
- package/dist/public/steps/PaymentStep.d.ts.map +0 -1
- package/dist/registries.d.ts.map +0 -1
- package/src/AgendaContext.tsx +0 -11
- package/src/AgendaPage.tsx +0 -33
- package/src/bridges/create-financial-bridge.ts +0 -296
- package/src/calendar-styles.css +0 -245
- package/src/calendar-styles.ts +0 -285
- package/src/components/AgendaGeneralSettings.tsx +0 -86
- package/src/components/AgendaMobileList.tsx +0 -103
- package/src/components/AppointmentModal.tsx +0 -1050
- package/src/components/AppointmentPopover.tsx +0 -231
- package/src/components/BookingPaymentPanel.tsx +0 -394
- package/src/components/CalendarSidebar.tsx +0 -119
- package/src/components/CalendarToolbar.tsx +0 -86
- package/src/components/EventContextMenu.tsx +0 -125
- package/src/components/FloatingPanel.tsx +0 -123
- package/src/components/MiniCalendar.tsx +0 -120
- package/src/components/PaymentStatusBadge.tsx +0 -62
- package/src/components/StatusBadge.tsx +0 -23
- package/src/config.ts +0 -368
- package/src/data/index.ts +0 -4
- package/src/data/mock.ts +0 -344
- package/src/data/supabase.ts +0 -636
- package/src/data/types.ts +0 -37
- package/src/financial-bridge.ts +0 -96
- package/src/hooks/useAgendaSettings.ts +0 -17
- package/src/index.ts +0 -297
- package/src/integrations/google-calendar/GoogleCalendarSettings.tsx +0 -136
- package/src/integrations/google-calendar/components/CalendarColorDot.tsx +0 -23
- package/src/integrations/google-calendar/components/ChannelMappingList.tsx +0 -329
- package/src/integrations/google-calendar/components/ImportEventsWizard.tsx +0 -215
- package/src/integrations/google-calendar/components/useAgendaLookups.ts +0 -100
- package/src/integrations/google-calendar/connector.ts +0 -20
- package/src/integrations/google-calendar/connectorDef.tsx +0 -185
- package/src/integrations/google-calendar/data/supabase.ts +0 -170
- package/src/integrations/google-calendar/data/tables.ts +0 -12
- package/src/integrations/google-calendar/index.ts +0 -45
- package/src/integrations/google-calendar/integration.test.ts +0 -199
- package/src/integrations/google-calendar/mapping.ts +0 -183
- package/src/integrations/google-calendar/migrations/001_google_calendar.sql +0 -97
- package/src/integrations/google-calendar/migrations/002_smart_sync.sql +0 -517
- package/src/integrations/google-calendar/migrations/003_plg_rename.sql +0 -273
- package/src/integrations/google-calendar/types.ts +0 -142
- package/src/lib/tenant.ts +0 -3
- package/src/locales/en.ts +0 -220
- package/src/locales/index.ts +0 -7
- package/src/locales/pt-BR.ts +0 -220
- package/src/migrations/000b_v_bookings_rename.sql +0 -21
- package/src/migrations/000c_dedupe_customer_phones.sql +0 -43
- package/src/migrations/001_public_booking.sql +0 -296
- package/src/migrations/002_v_bookings_compat.sql +0 -14
- package/src/migrations/003_public_client_kind.sql +0 -175
- package/src/migrations/004_public_booking_assignee.sql +0 -176
- package/src/migrations/005_agent_rpcs.sql +0 -168
- package/src/migrations/006_agent_rpc_localtime.sql +0 -181
- package/src/migrations/007_schedule_exception_precedence.sql +0 -290
- package/src/migrations/008_agent_rpc_exception_precedence.sql +0 -172
- package/src/migrations/009_public_booking_location.sql +0 -162
- package/src/migrations/010_public_service_curation.sql +0 -31
- package/src/migrations/011_default_assignee.sql +0 -167
- package/src/migrations/012_drop_legacy_booking_overload.sql +0 -29
- package/src/migrations/013_normalize_v_appointments.sql +0 -199
- package/src/migrations/014_agent_rpc_update_appointment.sql +0 -199
- package/src/migrations/index.ts +0 -2377
- package/src/public/BookingWidget.tsx +0 -324
- package/src/public/components/BrandHeader.tsx +0 -27
- package/src/public/components/ServiceCard.tsx +0 -39
- package/src/public/components/SidebarExtras.tsx +0 -30
- package/src/public/components/StepRailItem.tsx +0 -27
- package/src/public/components/SuccessPanel.tsx +0 -27
- package/src/public/components/SummaryRail.tsx +0 -90
- package/src/public/components/defaults.ts +0 -14
- package/src/public/context.tsx +0 -103
- package/src/public/createPublicBookingPlugin.ts +0 -281
- package/src/public/data.supabase.ts +0 -97
- package/src/public/data.ts +0 -105
- package/src/public/format.ts +0 -96
- package/src/public/hooks.ts +0 -110
- package/src/public/index.ts +0 -69
- package/src/public/steps/ContactStep.tsx +0 -184
- package/src/public/steps/DateTimeStep.tsx +0 -138
- package/src/public/steps/OverviewStep.tsx +0 -81
- package/src/public/steps/PaymentStep.tsx +0 -180
- package/src/public/types.ts +0 -250
- package/src/registries.ts +0 -35
- package/src/store.ts +0 -311
- package/src/types.ts +0 -273
- package/src/views/CalendarView.tsx +0 -1112
- package/src/views/ConfirmationsView.tsx +0 -155
- /package/dist/{financial-bridge.d.ts → bridges/financial-bridge.d.ts} +0 -0
- /package/dist/{registries.d.ts → data/registries.d.ts} +0 -0
- /package/dist/public/{BookingWidget.d.ts → views/BookingWidget.d.ts} +0 -0
- /package/dist/{calendar-styles.d.ts → styles/calendar-styles.d.ts} +0 -0
|
@@ -1,324 +0,0 @@
|
|
|
1
|
-
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
2
|
-
import { ChevronLeft } from 'lucide-react'
|
|
3
|
-
import { DEFAULT_COUNTRY, getCountry, maskPhone, unmaskPhone } from '@fayz-ai/core'
|
|
4
|
-
import type { PixCharge } from '@fayz-ai/core'
|
|
5
|
-
import { useBooking } from './context'
|
|
6
|
-
import { useAvailableSlots, useCreateBooking } from './hooks'
|
|
7
|
-
import { buildWeek, toIso, readServiceParam, useFormatters } from './format'
|
|
8
|
-
import type { StepState } from './types'
|
|
9
|
-
import { OverviewStep } from './steps/OverviewStep'
|
|
10
|
-
import { DateTimeStep } from './steps/DateTimeStep'
|
|
11
|
-
import { ContactStep, type ContactDraft } from './steps/ContactStep'
|
|
12
|
-
import { PaymentStep, PIX_PAYLOAD } from './steps/PaymentStep'
|
|
13
|
-
import { SummaryRail, type RailState } from './components/SummaryRail'
|
|
14
|
-
|
|
15
|
-
type Step = 'overview' | 'datetime' | 'contact' | 'payment' | 'success'
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Customer-facing booking surface. Default view is an overview (services +
|
|
19
|
-
* opening hours + CTA) like Quaddro; picking a service — or landing with
|
|
20
|
-
* ?service=… — enters a stepped flow (date → time → contact → [payment])
|
|
21
|
-
* with a live progress + summary rail. Token-only, keyboard-accessible, 44px
|
|
22
|
-
* targets. Brand-visible parts render via the host-overridable
|
|
23
|
-
* `components`/`brand` seams; data flows through PublicBookingDataProvider
|
|
24
|
-
* (mock seed or Supabase RPCs — same calls).
|
|
25
|
-
*/
|
|
26
|
-
export function BookingWidget() {
|
|
27
|
-
const {
|
|
28
|
-
services, servicesLoading, professionalName, businessHours, payment, paymentProvider,
|
|
29
|
-
phoneVerification, onIdentityVerified, labels, brand, components: C,
|
|
30
|
-
window: bookingWindow, locale, currency,
|
|
31
|
-
} = useBooking()
|
|
32
|
-
const fmt = useFormatters(locale, currency)
|
|
33
|
-
|
|
34
|
-
const preselected = useMemo(() => {
|
|
35
|
-
const q = readServiceParam()
|
|
36
|
-
if (q && services.some((s) => s.id === q)) return q
|
|
37
|
-
return null
|
|
38
|
-
}, [services])
|
|
39
|
-
const todayIso = useMemo(() => toIso(new Date()), [])
|
|
40
|
-
|
|
41
|
-
const [step, setStep] = useState<Step>(preselected ? 'datetime' : 'overview')
|
|
42
|
-
const [serviceId, setServiceId] = useState<string | null>(preselected)
|
|
43
|
-
const [weekOffset, setWeekOffset] = useState(0)
|
|
44
|
-
const [date, setDate] = useState<string | null>(todayIso)
|
|
45
|
-
const [slotStart, setSlotStart] = useState<string | null>(null)
|
|
46
|
-
const [contact, setContact] = useState<ContactDraft>({ name: '', phone: '', email: '', notes: '' })
|
|
47
|
-
// Contact step is phone-verification-first (like Quaddro): phone → code → details.
|
|
48
|
-
const [contactStep, setContactStep] = useState<'phone' | 'code' | 'details'>('phone')
|
|
49
|
-
const [code, setCode] = useState('')
|
|
50
|
-
const [codeError, setCodeError] = useState(false)
|
|
51
|
-
// Payment sub-flow: choose method → Pix QR ("aguardando pagamento").
|
|
52
|
-
const [paymentStep, setPaymentStep] = useState<'choose' | 'pix'>('choose')
|
|
53
|
-
const [copied, setCopied] = useState(false)
|
|
54
|
-
const [countryIso, setCountryIso] = useState(DEFAULT_COUNTRY)
|
|
55
|
-
const country = getCountry(countryIso)
|
|
56
|
-
// Pix charge from the injected payment provider (null → self-mocked fallback).
|
|
57
|
-
const [charge, setCharge] = useState<PixCharge | null>(null)
|
|
58
|
-
const [charging, setCharging] = useState(false)
|
|
59
|
-
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
|
60
|
-
|
|
61
|
-
// ?service=… deep link: enter the flow once the (possibly async) catalog resolves.
|
|
62
|
-
useEffect(() => {
|
|
63
|
-
if (preselected && step === 'overview') {
|
|
64
|
-
setServiceId(preselected)
|
|
65
|
-
setStep('datetime')
|
|
66
|
-
}
|
|
67
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
68
|
-
}, [preselected])
|
|
69
|
-
|
|
70
|
-
// Poll the charge status while on the Pix screen; settle → success.
|
|
71
|
-
useEffect(() => {
|
|
72
|
-
if (!(step === 'payment' && paymentStep === 'pix' && charge && paymentProvider)) return
|
|
73
|
-
let active = true
|
|
74
|
-
const clear = () => { if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null } }
|
|
75
|
-
const tick = () => {
|
|
76
|
-
paymentProvider.getChargeStatus(charge.chargeId).then((s) => {
|
|
77
|
-
if (!active) return
|
|
78
|
-
if (s === 'paid') { clear(); setStep('success') }
|
|
79
|
-
else if (s === 'expired' || s === 'failed') clear()
|
|
80
|
-
}).catch(() => { /* keep polling on transient errors */ })
|
|
81
|
-
}
|
|
82
|
-
tick()
|
|
83
|
-
pollRef.current = setInterval(tick, 2500)
|
|
84
|
-
return () => { active = false; clear() }
|
|
85
|
-
}, [step, paymentStep, charge, paymentProvider])
|
|
86
|
-
|
|
87
|
-
const week = useMemo(() => buildWeek(weekOffset), [weekOffset])
|
|
88
|
-
const { slots, loading: slotsLoading } = useAvailableSlots({ serviceId, date })
|
|
89
|
-
const { submit, submitting, error } = useCreateBooking()
|
|
90
|
-
|
|
91
|
-
const minTime = Date.now() + bookingWindow.minAdvanceHours * 3600_000
|
|
92
|
-
const bookableSlots = slots.filter((s) => new Date(s.start).getTime() >= minTime)
|
|
93
|
-
const service = services.find((s) => s.id === serviceId) ?? null
|
|
94
|
-
const maxWeekOffset = Math.floor(bookingWindow.maxAdvanceDays / 7)
|
|
95
|
-
|
|
96
|
-
/** Contact with the full international phone, as real backends expect. */
|
|
97
|
-
function contactForSubmit(): ContactDraft {
|
|
98
|
-
return { ...contact, phone: `${country.dial} ${contact.phone}` }
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function startBooking(id?: string) {
|
|
102
|
-
setServiceId(id ?? serviceId ?? services[0]?.id ?? null)
|
|
103
|
-
setDate(todayIso)
|
|
104
|
-
setSlotStart(null)
|
|
105
|
-
setStep('datetime')
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async function handleConfirm() {
|
|
109
|
-
if (!serviceId || !slotStart || !contact.name.trim()) return
|
|
110
|
-
try {
|
|
111
|
-
await submit({ serviceId, startsAt: slotStart, contact: contactForSubmit() })
|
|
112
|
-
setStep('success')
|
|
113
|
-
} catch {
|
|
114
|
-
/* error surfaced inline */
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function emitIdentity() {
|
|
119
|
-
onIdentityVerified?.({
|
|
120
|
-
name: contact.name,
|
|
121
|
-
phone: `${country.dial} ${contact.phone}`,
|
|
122
|
-
email: contact.email || undefined,
|
|
123
|
-
})
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Contact sub-step submit handlers (also used by Enter via <form onSubmit>).
|
|
127
|
-
// With phoneVerification 'none' the typed number is trusted: skip the code
|
|
128
|
-
// modal and go straight to details (identity emitted just the same).
|
|
129
|
-
function sendCode() {
|
|
130
|
-
if (unmaskPhone(contact.phone).length < 8) return
|
|
131
|
-
if (phoneVerification === 'none') {
|
|
132
|
-
emitIdentity()
|
|
133
|
-
setContactStep('details')
|
|
134
|
-
return
|
|
135
|
-
}
|
|
136
|
-
setCode(''); setCodeError(false); setContactStep('code')
|
|
137
|
-
}
|
|
138
|
-
function confirmCode() {
|
|
139
|
-
if (code.trim() === '0000') {
|
|
140
|
-
setCodeError(false)
|
|
141
|
-
emitIdentity() // phone verified → host signs the user in
|
|
142
|
-
setContactStep('details')
|
|
143
|
-
} else {
|
|
144
|
-
setCodeError(true)
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
function submitDetails() {
|
|
148
|
-
if (!contact.name.trim() || submitting) return
|
|
149
|
-
emitIdentity() // re-fire with the full name to enrich the account
|
|
150
|
-
if (payment) { setPaymentStep('choose'); setStep('payment') } else void handleConfirm()
|
|
151
|
-
}
|
|
152
|
-
async function payWithPix(fee: number) {
|
|
153
|
-
if (charging || !service || !slotStart) return
|
|
154
|
-
setCharging(true)
|
|
155
|
-
try {
|
|
156
|
-
await submit({ serviceId: service.id, startsAt: slotStart, contact: contactForSubmit() })
|
|
157
|
-
if (paymentProvider) {
|
|
158
|
-
const c = await paymentProvider.createCharge({
|
|
159
|
-
amount: fee, currency, method: 'pix', description: service.name,
|
|
160
|
-
customer: { name: contact.name, email: contact.email || undefined, phone: contact.phone || undefined },
|
|
161
|
-
})
|
|
162
|
-
setCharge(c)
|
|
163
|
-
}
|
|
164
|
-
setPaymentStep('pix')
|
|
165
|
-
} catch {
|
|
166
|
-
/* error surfaced inline */
|
|
167
|
-
} finally {
|
|
168
|
-
setCharging(false)
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
function copyPixPayload() {
|
|
172
|
-
const payload = charge?.pixCopyPaste ?? PIX_PAYLOAD
|
|
173
|
-
try { void navigator.clipboard?.writeText(payload); setCopied(true) } catch { setCopied(true) }
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// ---- OVERVIEW (default view, no service chosen) ----
|
|
177
|
-
if (step === 'overview') {
|
|
178
|
-
return (
|
|
179
|
-
<div className="mx-auto max-w-4xl">
|
|
180
|
-
<C.BrandHeader brand={brand} />
|
|
181
|
-
<OverviewStep
|
|
182
|
-
services={services}
|
|
183
|
-
servicesLoading={servicesLoading}
|
|
184
|
-
businessHours={businessHours}
|
|
185
|
-
professionalName={professionalName}
|
|
186
|
-
labels={labels}
|
|
187
|
-
brand={brand}
|
|
188
|
-
fmt={fmt}
|
|
189
|
-
components={C}
|
|
190
|
-
onStartBooking={startBooking}
|
|
191
|
-
/>
|
|
192
|
-
</div>
|
|
193
|
-
)
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// ---- WIZARD (datetime → contact → [payment] → success) ----
|
|
197
|
-
const railState: RailState = {
|
|
198
|
-
service: 'done',
|
|
199
|
-
professional: 'done',
|
|
200
|
-
datetime: (slotStart ? 'done' : step === 'datetime' ? 'active' : 'todo') as StepState,
|
|
201
|
-
contact: (step === 'payment' || step === 'success' ? 'done' : step === 'contact' ? 'active' : 'todo') as StepState,
|
|
202
|
-
payment: (step === 'success' ? 'done' : step === 'payment' ? 'active' : 'todo') as StepState,
|
|
203
|
-
}
|
|
204
|
-
const railTitle = step === 'payment' && paymentStep === 'pix' ? labels.railWaitingPayment : labels.railTitle
|
|
205
|
-
const contactSummary =
|
|
206
|
-
(contact.name || contact.phone) && (step === 'payment' || step === 'success' || contactStep === 'details')
|
|
207
|
-
? `${contact.name || 'Contato'}${contact.phone ? ` · ${country.dial} ${contact.phone}` : ''}`
|
|
208
|
-
: null
|
|
209
|
-
|
|
210
|
-
return (
|
|
211
|
-
<div className="mx-auto max-w-4xl">
|
|
212
|
-
<C.BrandHeader brand={brand} />
|
|
213
|
-
<div className="grid gap-6 lg:grid-cols-[1fr_18rem]">
|
|
214
|
-
{/* LEFT — step panel */}
|
|
215
|
-
<div className="rounded-2xl border border-border bg-card p-6 md:p-7 shadow-sm">
|
|
216
|
-
<div className="mb-5 flex items-center gap-1.5 lg:hidden">
|
|
217
|
-
{(['datetime', 'contact'] as const).map((s) => (
|
|
218
|
-
<span key={s} className={`h-1.5 flex-1 rounded-full ${railState[s] !== 'todo' ? 'bg-primary' : 'bg-muted'}`} />
|
|
219
|
-
))}
|
|
220
|
-
</div>
|
|
221
|
-
|
|
222
|
-
{step !== 'success' ? (
|
|
223
|
-
<button
|
|
224
|
-
type="button"
|
|
225
|
-
onClick={() => setStep(step === 'payment' ? 'contact' : step === 'contact' ? 'datetime' : 'overview')}
|
|
226
|
-
className="mb-4 inline-flex items-center gap-1 rounded text-sm font-medium text-muted-foreground hover:text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
227
|
-
>
|
|
228
|
-
<ChevronLeft className="h-4 w-4" /> {step === 'datetime' ? labels.backToServices : labels.back}
|
|
229
|
-
</button>
|
|
230
|
-
) : null}
|
|
231
|
-
|
|
232
|
-
{step === 'datetime' ? (
|
|
233
|
-
<DateTimeStep
|
|
234
|
-
labels={labels}
|
|
235
|
-
fmt={fmt}
|
|
236
|
-
week={week}
|
|
237
|
-
weekOffset={weekOffset}
|
|
238
|
-
maxWeekOffset={maxWeekOffset}
|
|
239
|
-
onWeekOffset={setWeekOffset}
|
|
240
|
-
businessHours={businessHours}
|
|
241
|
-
date={date}
|
|
242
|
-
onSelectDate={(iso) => { setDate(iso); setSlotStart(null) }}
|
|
243
|
-
slots={bookableSlots}
|
|
244
|
-
slotsLoading={slotsLoading}
|
|
245
|
-
slotStart={slotStart}
|
|
246
|
-
onSelectSlot={setSlotStart}
|
|
247
|
-
onContinue={() => setStep('contact')}
|
|
248
|
-
/>
|
|
249
|
-
) : null}
|
|
250
|
-
|
|
251
|
-
{step === 'contact' ? (
|
|
252
|
-
<ContactStep
|
|
253
|
-
labels={labels}
|
|
254
|
-
contact={contact}
|
|
255
|
-
onContactChange={setContact}
|
|
256
|
-
contactStep={contactStep}
|
|
257
|
-
country={country}
|
|
258
|
-
onCountryChange={(iso2) => {
|
|
259
|
-
const c = getCountry(iso2)
|
|
260
|
-
setCountryIso(c.iso2)
|
|
261
|
-
setContact((prev) => ({ ...prev, phone: maskPhone(unmaskPhone(prev.phone), c.mask) }))
|
|
262
|
-
}}
|
|
263
|
-
code={code}
|
|
264
|
-
onCodeChange={(v) => { setCode(v); setCodeError(false) }}
|
|
265
|
-
phoneVerification={phoneVerification}
|
|
266
|
-
codeError={codeError}
|
|
267
|
-
onSendCode={sendCode}
|
|
268
|
-
onConfirmCode={confirmCode}
|
|
269
|
-
onBackToPhone={() => setContactStep('phone')}
|
|
270
|
-
onSubmitDetails={submitDetails}
|
|
271
|
-
payment={payment}
|
|
272
|
-
submitting={submitting}
|
|
273
|
-
error={error}
|
|
274
|
-
/>
|
|
275
|
-
) : null}
|
|
276
|
-
|
|
277
|
-
{step === 'payment' && service && payment ? (
|
|
278
|
-
<PaymentStep
|
|
279
|
-
service={service}
|
|
280
|
-
payment={payment}
|
|
281
|
-
paymentStep={paymentStep}
|
|
282
|
-
charge={charge}
|
|
283
|
-
busy={submitting || charging}
|
|
284
|
-
error={error}
|
|
285
|
-
copied={copied}
|
|
286
|
-
fmt={fmt}
|
|
287
|
-
onPayWithPix={(fee) => void payWithPix(fee)}
|
|
288
|
-
onCopy={copyPixPayload}
|
|
289
|
-
onSimulatePaid={() => setStep('success')}
|
|
290
|
-
/>
|
|
291
|
-
) : null}
|
|
292
|
-
|
|
293
|
-
{step === 'success' ? (
|
|
294
|
-
<C.SuccessPanel
|
|
295
|
-
service={service}
|
|
296
|
-
slotStart={slotStart}
|
|
297
|
-
professionalName={professionalName}
|
|
298
|
-
labels={labels}
|
|
299
|
-
fmt={fmt}
|
|
300
|
-
/>
|
|
301
|
-
) : null}
|
|
302
|
-
</div>
|
|
303
|
-
|
|
304
|
-
{/* RIGHT — progress + summary rail */}
|
|
305
|
-
<aside className="hidden lg:block">
|
|
306
|
-
<SummaryRail
|
|
307
|
-
title={railTitle}
|
|
308
|
-
railState={railState}
|
|
309
|
-
service={service}
|
|
310
|
-
professionalName={professionalName}
|
|
311
|
-
slotStart={slotStart}
|
|
312
|
-
contactSummary={contactSummary}
|
|
313
|
-
payment={payment}
|
|
314
|
-
paymentReached={step === 'payment' || step === 'success'}
|
|
315
|
-
labels={labels}
|
|
316
|
-
fmt={fmt}
|
|
317
|
-
components={C}
|
|
318
|
-
serviceMeta={brand.serviceMeta}
|
|
319
|
-
/>
|
|
320
|
-
</aside>
|
|
321
|
-
</div>
|
|
322
|
-
</div>
|
|
323
|
-
)
|
|
324
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,90 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
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
|
-
}
|