@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.
Files changed (196) hide show
  1. package/README.md +3 -3
  2. package/dist/bridges/create-financial-bridge.d.ts +1 -1
  3. package/dist/bridges/create-financial-bridge.d.ts.map +1 -1
  4. package/dist/bridges/financial-bridge.d.ts.map +1 -0
  5. package/dist/{chunk-Q2PAWBPR.js → chunk-ISLJXP7O.js} +7 -2
  6. package/dist/chunk-ISLJXP7O.js.map +1 -0
  7. package/dist/components/AppointmentModal.d.ts.map +1 -1
  8. package/dist/components/PaymentStatusBadge.d.ts +1 -1
  9. package/dist/components/PaymentStatusBadge.d.ts.map +1 -1
  10. package/dist/config.d.ts +2 -2
  11. package/dist/config.d.ts.map +1 -1
  12. package/dist/{AgendaContext.d.ts → context.d.ts} +1 -1
  13. package/dist/context.d.ts.map +1 -0
  14. package/dist/data/mock.d.ts.map +1 -1
  15. package/dist/data/realtime.test.d.ts +2 -0
  16. package/dist/data/realtime.test.d.ts.map +1 -0
  17. package/dist/data/registries.d.ts.map +1 -0
  18. package/dist/data/supabase.d.ts.map +1 -1
  19. package/dist/data/types.d.ts +7 -0
  20. package/dist/data/types.d.ts.map +1 -1
  21. package/dist/index.d.ts +2 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +2515 -187
  24. package/dist/index.js.map +1 -1
  25. package/dist/integrations/google-calendar/conflict.d.ts +25 -0
  26. package/dist/integrations/google-calendar/conflict.d.ts.map +1 -0
  27. package/dist/integrations/google-calendar/connectorDef.d.ts +12 -0
  28. package/dist/integrations/google-calendar/connectorDef.d.ts.map +1 -1
  29. package/dist/integrations/google-calendar/credentials.d.ts +27 -0
  30. package/dist/integrations/google-calendar/credentials.d.ts.map +1 -0
  31. package/dist/integrations/google-calendar/data/supabase.d.ts +10 -2
  32. package/dist/integrations/google-calendar/data/supabase.d.ts.map +1 -1
  33. package/dist/integrations/google-calendar/data/tables.d.ts +2 -0
  34. package/dist/integrations/google-calendar/data/tables.d.ts.map +1 -1
  35. package/dist/integrations/google-calendar/health.d.ts +23 -0
  36. package/dist/integrations/google-calendar/health.d.ts.map +1 -0
  37. package/dist/integrations/google-calendar/health.test.d.ts +2 -0
  38. package/dist/integrations/google-calendar/health.test.d.ts.map +1 -0
  39. package/dist/integrations/google-calendar/index.d.ts +6 -0
  40. package/dist/integrations/google-calendar/index.d.ts.map +1 -1
  41. package/dist/integrations/google-calendar/migrations/index.d.ts +12 -0
  42. package/dist/integrations/google-calendar/migrations/index.d.ts.map +1 -0
  43. package/dist/integrations/google-calendar/panel-data.d.ts +26 -0
  44. package/dist/integrations/google-calendar/panel-data.d.ts.map +1 -0
  45. package/dist/integrations/google-calendar/panel-data.test.d.ts +2 -0
  46. package/dist/integrations/google-calendar/panel-data.test.d.ts.map +1 -0
  47. package/dist/integrations/google-calendar/types.d.ts +17 -0
  48. package/dist/integrations/google-calendar/types.d.ts.map +1 -1
  49. package/dist/lib/booking-events.d.ts +5 -0
  50. package/dist/lib/booking-events.d.ts.map +1 -0
  51. package/dist/lib/calendar-events.d.ts +8 -0
  52. package/dist/lib/calendar-events.d.ts.map +1 -0
  53. package/dist/lib/calendar-events.test.d.ts +2 -0
  54. package/dist/lib/calendar-events.test.d.ts.map +1 -0
  55. package/dist/lib/message-events.d.ts +12 -0
  56. package/dist/lib/message-events.d.ts.map +1 -0
  57. package/dist/locales/en.d.ts.map +1 -1
  58. package/dist/locales/pt-BR.d.ts.map +1 -1
  59. package/dist/public/{data.d.ts → data/index.d.ts} +4 -4
  60. package/dist/public/data/index.d.ts.map +1 -0
  61. package/dist/public/{data.supabase.d.ts → data/supabase.d.ts} +2 -2
  62. package/dist/public/data/supabase.d.ts.map +1 -0
  63. package/dist/public/{hooks.d.ts → hooks/index.d.ts} +4 -4
  64. package/dist/public/hooks/index.d.ts.map +1 -0
  65. package/dist/public/index.d.ts +4 -4
  66. package/dist/public/index.d.ts.map +1 -1
  67. package/dist/public/index.js +3 -3
  68. package/dist/public/index.js.map +1 -1
  69. package/dist/public/{format.d.ts → lib/format.d.ts} +1 -1
  70. package/dist/public/lib/format.d.ts.map +1 -0
  71. package/dist/public/views/BookingWidget.d.ts.map +1 -0
  72. package/dist/public/{steps → views/steps}/ContactStep.d.ts +1 -1
  73. package/dist/public/views/steps/ContactStep.d.ts.map +1 -0
  74. package/dist/public/{steps → views/steps}/DateTimeStep.d.ts +3 -3
  75. package/dist/public/views/steps/DateTimeStep.d.ts.map +1 -0
  76. package/dist/public/{steps → views/steps}/OverviewStep.d.ts +1 -1
  77. package/dist/public/views/steps/OverviewStep.d.ts.map +1 -0
  78. package/dist/public/{steps → views/steps}/PaymentStep.d.ts +1 -1
  79. package/dist/public/views/steps/PaymentStep.d.ts.map +1 -0
  80. package/dist/store.d.ts +1 -1
  81. package/dist/store.d.ts.map +1 -1
  82. package/dist/styles/calendar-styles.d.ts.map +1 -0
  83. package/dist/types.d.ts +1 -1
  84. package/dist/types.d.ts.map +1 -1
  85. package/dist/{AgendaPage.d.ts → views/AgendaPage.d.ts} +3 -3
  86. package/dist/views/AgendaPage.d.ts.map +1 -0
  87. package/dist/views/CalendarView.d.ts.map +1 -1
  88. package/package.json +9 -11
  89. package/dist/AgendaContext.d.ts.map +0 -1
  90. package/dist/AgendaPage.d.ts.map +0 -1
  91. package/dist/calendar-styles.d.ts.map +0 -1
  92. package/dist/chunk-Q2PAWBPR.js.map +0 -1
  93. package/dist/financial-bridge.d.ts.map +0 -1
  94. package/dist/integrations/google-calendar/GoogleCalendarSettings.d.ts +0 -3
  95. package/dist/integrations/google-calendar/GoogleCalendarSettings.d.ts.map +0 -1
  96. package/dist/public/BookingWidget.d.ts.map +0 -1
  97. package/dist/public/data.d.ts.map +0 -1
  98. package/dist/public/data.supabase.d.ts.map +0 -1
  99. package/dist/public/format.d.ts.map +0 -1
  100. package/dist/public/hooks.d.ts.map +0 -1
  101. package/dist/public/steps/ContactStep.d.ts.map +0 -1
  102. package/dist/public/steps/DateTimeStep.d.ts.map +0 -1
  103. package/dist/public/steps/OverviewStep.d.ts.map +0 -1
  104. package/dist/public/steps/PaymentStep.d.ts.map +0 -1
  105. package/dist/registries.d.ts.map +0 -1
  106. package/src/AgendaContext.tsx +0 -11
  107. package/src/AgendaPage.tsx +0 -33
  108. package/src/bridges/create-financial-bridge.ts +0 -296
  109. package/src/calendar-styles.css +0 -245
  110. package/src/calendar-styles.ts +0 -285
  111. package/src/components/AgendaGeneralSettings.tsx +0 -86
  112. package/src/components/AgendaMobileList.tsx +0 -103
  113. package/src/components/AppointmentModal.tsx +0 -1050
  114. package/src/components/AppointmentPopover.tsx +0 -231
  115. package/src/components/BookingPaymentPanel.tsx +0 -394
  116. package/src/components/CalendarSidebar.tsx +0 -119
  117. package/src/components/CalendarToolbar.tsx +0 -86
  118. package/src/components/EventContextMenu.tsx +0 -125
  119. package/src/components/FloatingPanel.tsx +0 -123
  120. package/src/components/MiniCalendar.tsx +0 -120
  121. package/src/components/PaymentStatusBadge.tsx +0 -62
  122. package/src/components/StatusBadge.tsx +0 -23
  123. package/src/config.ts +0 -368
  124. package/src/data/index.ts +0 -4
  125. package/src/data/mock.ts +0 -344
  126. package/src/data/supabase.ts +0 -636
  127. package/src/data/types.ts +0 -37
  128. package/src/financial-bridge.ts +0 -96
  129. package/src/hooks/useAgendaSettings.ts +0 -17
  130. package/src/index.ts +0 -297
  131. package/src/integrations/google-calendar/GoogleCalendarSettings.tsx +0 -136
  132. package/src/integrations/google-calendar/components/CalendarColorDot.tsx +0 -23
  133. package/src/integrations/google-calendar/components/ChannelMappingList.tsx +0 -329
  134. package/src/integrations/google-calendar/components/ImportEventsWizard.tsx +0 -215
  135. package/src/integrations/google-calendar/components/useAgendaLookups.ts +0 -100
  136. package/src/integrations/google-calendar/connector.ts +0 -20
  137. package/src/integrations/google-calendar/connectorDef.tsx +0 -185
  138. package/src/integrations/google-calendar/data/supabase.ts +0 -170
  139. package/src/integrations/google-calendar/data/tables.ts +0 -12
  140. package/src/integrations/google-calendar/index.ts +0 -45
  141. package/src/integrations/google-calendar/integration.test.ts +0 -199
  142. package/src/integrations/google-calendar/mapping.ts +0 -183
  143. package/src/integrations/google-calendar/migrations/001_google_calendar.sql +0 -97
  144. package/src/integrations/google-calendar/migrations/002_smart_sync.sql +0 -517
  145. package/src/integrations/google-calendar/migrations/003_plg_rename.sql +0 -273
  146. package/src/integrations/google-calendar/types.ts +0 -142
  147. package/src/lib/tenant.ts +0 -3
  148. package/src/locales/en.ts +0 -220
  149. package/src/locales/index.ts +0 -7
  150. package/src/locales/pt-BR.ts +0 -220
  151. package/src/migrations/000b_v_bookings_rename.sql +0 -21
  152. package/src/migrations/000c_dedupe_customer_phones.sql +0 -43
  153. package/src/migrations/001_public_booking.sql +0 -296
  154. package/src/migrations/002_v_bookings_compat.sql +0 -14
  155. package/src/migrations/003_public_client_kind.sql +0 -175
  156. package/src/migrations/004_public_booking_assignee.sql +0 -176
  157. package/src/migrations/005_agent_rpcs.sql +0 -168
  158. package/src/migrations/006_agent_rpc_localtime.sql +0 -181
  159. package/src/migrations/007_schedule_exception_precedence.sql +0 -290
  160. package/src/migrations/008_agent_rpc_exception_precedence.sql +0 -172
  161. package/src/migrations/009_public_booking_location.sql +0 -162
  162. package/src/migrations/010_public_service_curation.sql +0 -31
  163. package/src/migrations/011_default_assignee.sql +0 -167
  164. package/src/migrations/012_drop_legacy_booking_overload.sql +0 -29
  165. package/src/migrations/013_normalize_v_appointments.sql +0 -199
  166. package/src/migrations/014_agent_rpc_update_appointment.sql +0 -199
  167. package/src/migrations/index.ts +0 -2377
  168. package/src/public/BookingWidget.tsx +0 -324
  169. package/src/public/components/BrandHeader.tsx +0 -27
  170. package/src/public/components/ServiceCard.tsx +0 -39
  171. package/src/public/components/SidebarExtras.tsx +0 -30
  172. package/src/public/components/StepRailItem.tsx +0 -27
  173. package/src/public/components/SuccessPanel.tsx +0 -27
  174. package/src/public/components/SummaryRail.tsx +0 -90
  175. package/src/public/components/defaults.ts +0 -14
  176. package/src/public/context.tsx +0 -103
  177. package/src/public/createPublicBookingPlugin.ts +0 -281
  178. package/src/public/data.supabase.ts +0 -97
  179. package/src/public/data.ts +0 -105
  180. package/src/public/format.ts +0 -96
  181. package/src/public/hooks.ts +0 -110
  182. package/src/public/index.ts +0 -69
  183. package/src/public/steps/ContactStep.tsx +0 -184
  184. package/src/public/steps/DateTimeStep.tsx +0 -138
  185. package/src/public/steps/OverviewStep.tsx +0 -81
  186. package/src/public/steps/PaymentStep.tsx +0 -180
  187. package/src/public/types.ts +0 -250
  188. package/src/registries.ts +0 -35
  189. package/src/store.ts +0 -311
  190. package/src/types.ts +0 -273
  191. package/src/views/CalendarView.tsx +0 -1112
  192. package/src/views/ConfirmationsView.tsx +0 -155
  193. /package/dist/{financial-bridge.d.ts → bridges/financial-bridge.d.ts} +0 -0
  194. /package/dist/{registries.d.ts → data/registries.d.ts} +0 -0
  195. /package/dist/public/{BookingWidget.d.ts → views/BookingWidget.d.ts} +0 -0
  196. /package/dist/{calendar-styles.d.ts → styles/calendar-styles.d.ts} +0 -0
@@ -1,110 +0,0 @@
1
- import { useEffect, useState } from 'react'
2
- import { useBooking } from './context'
3
- import type { PublicService } from './types'
4
- import type { PublicCreateBookingInput, PublicBookingResult } from './data'
5
- import type { TimeSlot } from '../types'
6
-
7
- /** The resolved bookable services (static option or fetched from the provider). */
8
- export function useServices(): PublicService[] {
9
- return useBooking().services
10
- }
11
-
12
- export interface UsePublicServicesResult {
13
- services: PublicService[]
14
- loading: boolean
15
- }
16
-
17
- /** Like useServices, but exposes the fetch state for skeletons. */
18
- export function usePublicServices(): UsePublicServicesResult {
19
- const { services, servicesLoading } = useBooking()
20
- return { services, loading: servicesLoading }
21
- }
22
-
23
- export interface UseAvailableSlotsResult {
24
- slots: TimeSlot[]
25
- loading: boolean
26
- error: Error | null
27
- }
28
-
29
- /**
30
- * Available time slots for a service on a given date. Delegates to
31
- * provider.getAvailableSlots — mock now, Supabase RPC when configured, same call.
32
- */
33
- export function useAvailableSlots(params: { serviceId: string | null; date: string | null }): UseAvailableSlotsResult {
34
- const { provider, professionalId, services, window } = useBooking()
35
- const [slots, setSlots] = useState<TimeSlot[]>([])
36
- const [loading, setLoading] = useState(false)
37
- const [error, setError] = useState<Error | null>(null)
38
-
39
- const { serviceId, date } = params
40
- const service = services.find((s) => s.id === serviceId)
41
- const durationMinutes = service?.durationMinutes ?? 0
42
-
43
- useEffect(() => {
44
- let active = true
45
- if (!serviceId || !date || !durationMinutes) {
46
- setSlots([])
47
- return
48
- }
49
- setLoading(true)
50
- provider
51
- .getAvailableSlots({ assigneeId: professionalId, date, durationMinutes, slotInterval: window.slotInterval })
52
- .then((result) => {
53
- if (!active) return
54
- setSlots(result)
55
- setError(null)
56
- })
57
- .catch((err) => {
58
- if (!active) return
59
- setError(err instanceof Error ? err : new Error(String(err)))
60
- })
61
- .finally(() => {
62
- if (active) setLoading(false)
63
- })
64
- return () => {
65
- active = false
66
- }
67
- }, [provider, professionalId, serviceId, date, durationMinutes, window.slotInterval])
68
-
69
- return { slots, loading, error }
70
- }
71
-
72
- export type CreatePublicBookingInput = PublicCreateBookingInput
73
-
74
- export interface UseCreateBookingResult {
75
- submit: (input: CreatePublicBookingInput) => Promise<PublicBookingResult>
76
- submitting: boolean
77
- error: Error | null
78
- done: boolean
79
- }
80
-
81
- /** Create a booking via the provider (mock append / Supabase RPC). */
82
- export function useCreateBooking(): UseCreateBookingResult {
83
- const { provider, professionalId } = useBooking()
84
- const [submitting, setSubmitting] = useState(false)
85
- const [error, setError] = useState<Error | null>(null)
86
- const [done, setDone] = useState(false)
87
-
88
- async function submit(input: CreatePublicBookingInput): Promise<PublicBookingResult> {
89
- setSubmitting(true)
90
- setError(null)
91
- try {
92
- // The professional the visitor saw availability for must be the one
93
- // booked — thread it unless the caller already pinned another.
94
- const result = await provider.createBooking({
95
- ...input,
96
- assigneeId: input.assigneeId ?? professionalId ?? null,
97
- })
98
- setDone(true)
99
- return result
100
- } catch (err) {
101
- const e = err instanceof Error ? err : new Error(String(err))
102
- setError(e)
103
- throw e
104
- } finally {
105
- setSubmitting(false)
106
- }
107
- }
108
-
109
- return { submit, submitting, error, done }
110
- }
@@ -1,69 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // @fayz-ai/plugin-agenda/public — customer-facing booking surface.
3
- //
4
- // Separate entry from the admin plugin so a website host imports only the lean
5
- // booking widget + data seam (no @fayz-ai/saas / calendar admin graph).
6
- //
7
- // Customization levers (cheapest first): `labels` → `brand` → `components`
8
- // (swap subcomponents; defaults exported below for wrapping/composition).
9
- // Data: PublicBookingDataProvider — mock seed by default, Supabase RPCs when a
10
- // global client is configured (requires `tenantId`).
11
- // ---------------------------------------------------------------------------
12
-
13
- export { createPublicBookingPlugin } from './createPublicBookingPlugin'
14
- export type { PublicBookingOptions, PublicBookingPlugin } from './createPublicBookingPlugin'
15
- export { BookingWidget } from './BookingWidget'
16
- export { BookingProvider, useBooking } from './context'
17
- export type { PublicBookingContextValue, OnIdentityVerified } from './context'
18
- export { useServices, usePublicServices, useAvailableSlots, useCreateBooking } from './hooks'
19
- export type {
20
- UsePublicServicesResult,
21
- UseAvailableSlotsResult,
22
- UseCreateBookingResult,
23
- CreatePublicBookingInput,
24
- } from './hooks'
25
-
26
- // Data seam
27
- export type {
28
- PublicBookingDataProvider,
29
- PublicSlotQuery,
30
- PublicCreateBookingInput,
31
- PublicBookingResult,
32
- MockPublicBookingOptions,
33
- } from './data'
34
- export { createMockPublicBookingProvider } from './data'
35
- export { createSupabasePublicBookingProvider } from './data.supabase'
36
- export type { SupabasePublicBookingOptions } from './data.supabase'
37
-
38
- // Customization seam — default components (wrap or replace via `components`)
39
- export { DEFAULT_BOOKING_COMPONENTS } from './components/defaults'
40
- export { DefaultBrandHeader } from './components/BrandHeader'
41
- export { DefaultServiceCard } from './components/ServiceCard'
42
- export { DefaultSidebarExtras } from './components/SidebarExtras'
43
- export { DefaultStepRailItem } from './components/StepRailItem'
44
- export { DefaultSuccessPanel } from './components/SuccessPanel'
45
-
46
- // Formatting helpers (useful inside custom components)
47
- export { useFormatters, formatSlotRange, DAY_NAMES, WEEK_ORDER } from './format'
48
-
49
- export type {
50
- PublicService,
51
- ContactInfo,
52
- BookingWindow,
53
- PublicBookingLabels,
54
- WorkingHours,
55
- PaymentConfig,
56
- ResolvedPayment,
57
- PhoneVerificationMode,
58
- BookingBrand,
59
- ResolvedBookingBrand,
60
- BookingHighlight,
61
- BookingFormatters,
62
- BookingComponents,
63
- BrandHeaderProps,
64
- ServiceCardProps,
65
- SidebarExtrasProps,
66
- SuccessPanelProps,
67
- StepRailItemProps,
68
- StepState,
69
- } from './types'
@@ -1,184 +0,0 @@
1
- import { Check, ChevronDown, CreditCard, Loader2, ShieldCheck } from 'lucide-react'
2
- import { COUNTRIES, maskPhone, unmaskPhone, type CountryDef } from '@fayz-ai/core'
3
- import type { PhoneVerificationMode, PublicBookingLabels, ResolvedPayment } from '../types'
4
-
5
- export interface ContactDraft {
6
- name: string
7
- phone: string
8
- email: string
9
- notes: string
10
- }
11
-
12
- interface ContactStepProps {
13
- labels: PublicBookingLabels
14
- contact: ContactDraft
15
- onContactChange: (contact: ContactDraft) => void
16
- contactStep: 'phone' | 'code' | 'details'
17
- /** 'none' → no code is sent, so the copy drops every OTP promise. */
18
- phoneVerification: PhoneVerificationMode
19
- country: CountryDef
20
- onCountryChange: (iso2: string) => void
21
- code: string
22
- onCodeChange: (code: string) => void
23
- codeError: boolean
24
- onSendCode: () => void
25
- onConfirmCode: () => void
26
- onBackToPhone: () => void
27
- onSubmitDetails: () => void
28
- payment: ResolvedPayment | null
29
- submitting: boolean
30
- error: Error | null
31
- }
32
-
33
- /** Contact step — phone verification first (Quaddro-style), then details. */
34
- export function ContactStep({
35
- labels, contact, onContactChange, contactStep, phoneVerification, country, onCountryChange,
36
- code, onCodeChange, codeError, onSendCode, onConfirmCode, onBackToPhone,
37
- onSubmitDetails, payment, submitting, error,
38
- }: ContactStepProps) {
39
- return (
40
- <div>
41
- <h3 className="font-heading text-xl font-bold text-foreground">{labels.contactStep}</h3>
42
-
43
- {/* Sub-step: phone */}
44
- {contactStep === 'phone' ? (
45
- <form onSubmit={(e) => { e.preventDefault(); onSendCode() }}>
46
- <p className="mt-1 text-sm text-muted-foreground">{labels.contactIntro}</p>
47
- <div className="mt-5">
48
- <label htmlFor="bk-phone" className="mb-1 block text-sm font-medium text-foreground">{labels.phoneLabel}</label>
49
- <div className="flex">
50
- <div className="relative">
51
- <select
52
- aria-label="País"
53
- value={country.iso2}
54
- onChange={(e) => onCountryChange(e.target.value)}
55
- className="h-full appearance-none rounded-l-xl border border-r-0 border-border bg-muted py-3 pl-3 pr-8 text-sm font-medium text-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring"
56
- >
57
- {COUNTRIES.map((c) => (
58
- <option key={c.iso2} value={c.iso2}>{c.flag} {c.dial}</option>
59
- ))}
60
- </select>
61
- <ChevronDown className="pointer-events-none absolute right-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
62
- </div>
63
- <input
64
- id="bk-phone" type="tel" autoComplete="tel" inputMode="tel" value={contact.phone}
65
- onChange={(e) => onContactChange({ ...contact, phone: maskPhone(e.target.value, country.mask) })}
66
- className="w-full rounded-r-xl border border-border bg-background px-4 py-3 text-foreground outline-none transition-colors focus:border-primary focus-visible:ring-2 focus-visible:ring-ring"
67
- placeholder={`Ex.: ${maskPhone('11987654321', country.mask)}`}
68
- />
69
- </div>
70
- <p className="mt-2 text-xs text-muted-foreground">
71
- {phoneVerification === 'none' ? labels.phoneHintNoVerification : labels.phoneHint}
72
- </p>
73
- </div>
74
- <button
75
- type="submit"
76
- disabled={unmaskPhone(contact.phone).length < 8}
77
- className="mt-5 w-full rounded-xl bg-primary py-3 font-semibold text-primary-foreground transition-opacity hover:opacity-90 disabled:opacity-40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
78
- >
79
- {phoneVerification === 'none' ? labels.phoneContinueCta : labels.sendCodeCta}
80
- </button>
81
- </form>
82
- ) : null}
83
-
84
- {/* Sub-step: details (after verification) */}
85
- {contactStep === 'details' ? (
86
- <form onSubmit={(e) => { e.preventDefault(); onSubmitDetails() }}>
87
- <p className="mt-1 text-sm text-muted-foreground">{labels.contactIntro}</p>
88
- <div className="mt-4 flex items-center justify-between rounded-xl border border-border bg-muted/40 px-4 py-3">
89
- <span className="flex items-center gap-2 text-sm text-foreground">
90
- <ShieldCheck className="h-4 w-4 text-primary" />{' '}
91
- {phoneVerification === 'none' ? labels.phoneLabel : labels.phoneVerified} · {country.flag} {country.dial} {contact.phone}
92
- </span>
93
- <button
94
- type="button"
95
- onClick={onBackToPhone}
96
- className="rounded text-sm font-medium text-primary hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
97
- >
98
- {labels.changePhone}
99
- </button>
100
- </div>
101
- <div className="mt-4 space-y-3">
102
- <div>
103
- <label htmlFor="bk-name" className="mb-1 block text-sm font-medium text-foreground">{labels.nameLabel}</label>
104
- <input
105
- id="bk-name" type="text" autoComplete="name" value={contact.name}
106
- onChange={(e) => onContactChange({ ...contact, name: e.target.value })}
107
- className="w-full rounded-xl border border-border bg-background px-4 py-3 text-foreground outline-none transition-colors focus:border-primary focus-visible:ring-2 focus-visible:ring-ring"
108
- placeholder={labels.namePlaceholder}
109
- />
110
- </div>
111
- <div>
112
- <label htmlFor="bk-email" className="mb-1 block text-sm font-medium text-foreground">{labels.emailLabel}</label>
113
- <input
114
- id="bk-email" type="email" autoComplete="email" inputMode="email" value={contact.email}
115
- onChange={(e) => onContactChange({ ...contact, email: e.target.value })}
116
- className="w-full rounded-xl border border-border bg-background px-4 py-3 text-foreground outline-none transition-colors focus:border-primary focus-visible:ring-2 focus-visible:ring-ring"
117
- placeholder={labels.emailPlaceholder}
118
- />
119
- <p className="mt-1 text-xs text-muted-foreground">{labels.emailHint}</p>
120
- </div>
121
- <div>
122
- <label htmlFor="bk-notes" className="mb-1 block text-sm font-medium text-foreground">{labels.notesLabel}</label>
123
- <textarea
124
- id="bk-notes" rows={3} value={contact.notes}
125
- onChange={(e) => onContactChange({ ...contact, notes: e.target.value })}
126
- className="w-full resize-none rounded-xl border border-border bg-background px-4 py-3 text-foreground outline-none transition-colors focus:border-primary focus-visible:ring-2 focus-visible:ring-ring"
127
- placeholder={labels.notesPlaceholder}
128
- />
129
- </div>
130
- </div>
131
- {error ? (
132
- <p role="alert" className="mt-3 text-sm text-destructive">{labels.submitError}</p>
133
- ) : null}
134
- <button
135
- type="submit"
136
- disabled={!contact.name.trim() || submitting}
137
- className="mt-5 flex w-full items-center justify-center gap-2 rounded-xl bg-primary py-3 font-semibold text-primary-foreground transition-opacity hover:opacity-90 disabled:opacity-40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
138
- >
139
- {submitting ? <Loader2 className="h-4 w-4 animate-spin" /> : <Check className="h-4 w-4" />}
140
- {payment ? labels.saveAndContinueCta : labels.confirmCta}
141
- </button>
142
- {!payment ? (
143
- <p className="mt-3 flex items-center justify-center gap-1.5 text-xs text-muted-foreground">
144
- <CreditCard className="h-3.5 w-3.5" /> {labels.noChargeNote}
145
- </p>
146
- ) : null}
147
- </form>
148
- ) : null}
149
-
150
- {/* Code verification modal */}
151
- {contactStep === 'code' ? (
152
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-foreground/40 p-4" role="dialog" aria-modal="true" aria-label={labels.codeTitle}>
153
- <div className="w-full max-w-md rounded-2xl border border-border bg-card p-6 shadow-xl">
154
- <h4 className="font-heading text-lg font-bold text-foreground">{labels.codeTitle}</h4>
155
- <p className="mt-1 text-sm text-muted-foreground">{labels.codeBody}</p>
156
- <form onSubmit={(e) => { e.preventDefault(); onConfirmCode() }}>
157
- <input
158
- type="text" inputMode="numeric" value={code} autoFocus
159
- onChange={(e) => onCodeChange(e.target.value)}
160
- className="mt-4 w-full rounded-xl border border-border bg-background px-4 py-3 text-center text-lg tracking-[0.4em] text-foreground outline-none transition-colors focus:border-primary focus-visible:ring-2 focus-visible:ring-ring placeholder:text-sm placeholder:tracking-normal"
161
- placeholder="Digite o código"
162
- />
163
- {codeError ? (
164
- <p role="alert" className="mt-2 text-sm text-destructive">{labels.codeInvalid}</p>
165
- ) : null}
166
- <button
167
- type="submit"
168
- className="mt-4 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"
169
- >
170
- {labels.codeConfirmCta}
171
- </button>
172
- </form>
173
- <div className="mt-3 flex items-center justify-between text-xs text-muted-foreground">
174
- <span>{labels.codeResendHint}</span>
175
- <button type="button" onClick={onBackToPhone} className="font-medium text-primary hover:underline">
176
- {labels.back}
177
- </button>
178
- </div>
179
- </div>
180
- </div>
181
- ) : null}
182
- </div>
183
- )
184
- }
@@ -1,138 +0,0 @@
1
- import { ChevronLeft, ChevronRight, Clock } from 'lucide-react'
2
- import type { TimeSlot } from '../../types'
3
- import type { DayCell } from '../format'
4
- import type { BookingFormatters, PublicBookingLabels, WorkingHours } from '../types'
5
-
6
- interface DateTimeStepProps {
7
- labels: PublicBookingLabels
8
- fmt: BookingFormatters
9
- week: DayCell[]
10
- weekOffset: number
11
- maxWeekOffset: number
12
- onWeekOffset: (updater: (w: number) => number) => void
13
- businessHours: WorkingHours
14
- date: string | null
15
- onSelectDate: (iso: string) => void
16
- slots: TimeSlot[]
17
- slotsLoading: boolean
18
- slotStart: string | null
19
- onSelectSlot: (startIso: string) => void
20
- onContinue: () => void
21
- }
22
-
23
- /** Week strip + available-slot grid. */
24
- export function DateTimeStep({
25
- labels, fmt, week, weekOffset, maxWeekOffset, onWeekOffset, businessHours,
26
- date, onSelectDate, slots, slotsLoading, slotStart, onSelectSlot, onContinue,
27
- }: DateTimeStepProps) {
28
- const openDays = new Set(businessHours.daysOfWeek)
29
- return (
30
- <div>
31
- <div className="flex flex-wrap items-baseline justify-between gap-2">
32
- <h3 className="font-heading text-xl font-bold text-foreground">{labels.dateStep}</h3>
33
- <span className="flex items-center gap-1 text-xs text-muted-foreground">
34
- <Clock className="h-3.5 w-3.5" /> {labels.timezoneNote}
35
- </span>
36
- </div>
37
-
38
- <div className="mt-5 flex items-center justify-between">
39
- <span className="text-sm font-semibold capitalize text-foreground">{fmt.month.format(week[0].date)}</span>
40
- <div className="flex items-center gap-1">
41
- <button
42
- type="button"
43
- disabled={weekOffset === 0}
44
- onClick={() => onWeekOffset((w) => Math.max(0, w - 1))}
45
- className="flex h-9 w-9 items-center justify-center rounded-lg border border-border text-foreground transition-colors hover:bg-accent disabled:opacity-40 disabled:hover:bg-transparent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
46
- aria-label={labels.prevWeek}
47
- >
48
- <ChevronLeft className="h-4 w-4" />
49
- </button>
50
- <button
51
- type="button"
52
- disabled={weekOffset >= maxWeekOffset}
53
- onClick={() => onWeekOffset((w) => Math.min(maxWeekOffset, w + 1))}
54
- className="flex h-9 w-9 items-center justify-center rounded-lg border border-border text-foreground transition-colors hover:bg-accent disabled:opacity-40 disabled:hover:bg-transparent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
55
- aria-label={labels.nextWeek}
56
- >
57
- <ChevronRight className="h-4 w-4" />
58
- </button>
59
- </div>
60
- </div>
61
-
62
- <div className="mt-3 grid grid-cols-7 gap-1.5">
63
- {week.map((d) => {
64
- const selected = date === d.iso
65
- const closed = !openDays.has(d.date.getDay())
66
- return (
67
- <button
68
- key={d.iso}
69
- type="button"
70
- disabled={closed}
71
- onClick={() => onSelectDate(d.iso)}
72
- className={[
73
- 'flex flex-col items-center gap-0.5 rounded-xl border py-2.5 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
74
- closed
75
- ? 'cursor-not-allowed border-transparent text-muted-foreground/40'
76
- : selected
77
- ? 'border-primary bg-primary text-primary-foreground'
78
- : 'border-border text-foreground hover:border-primary/60 hover:bg-accent/40',
79
- ].join(' ')}
80
- >
81
- <span className="text-[11px] font-medium capitalize opacity-80">
82
- {fmt.weekday.format(d.date).replace('.', '')}
83
- </span>
84
- <span className="text-sm font-bold tabular-nums">{fmt.dayNum.format(d.date)}</span>
85
- </button>
86
- )
87
- })}
88
- </div>
89
-
90
- {date ? (
91
- <div className="mt-6">
92
- <p className="mb-2 text-sm font-semibold text-foreground">{labels.slotsHeading}</p>
93
- {slotsLoading ? (
94
- <div className="grid grid-cols-3 gap-2 sm:grid-cols-4">
95
- {Array.from({ length: 8 }).map((_, i) => (
96
- <div key={i} className="h-10 animate-pulse rounded-lg bg-muted" />
97
- ))}
98
- </div>
99
- ) : slots.length === 0 ? (
100
- <div className="rounded-xl border border-dashed border-border p-6 text-center text-sm text-muted-foreground">
101
- {labels.noSlots}
102
- </div>
103
- ) : (
104
- <div className="grid grid-cols-3 gap-2 sm:grid-cols-4">
105
- {slots.map((s) => {
106
- const selected = slotStart === s.start
107
- return (
108
- <button
109
- key={s.start}
110
- type="button"
111
- onClick={() => onSelectSlot(s.start)}
112
- className={[
113
- 'rounded-lg border py-2.5 text-sm font-semibold tabular-nums transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
114
- selected
115
- ? 'border-primary bg-primary text-primary-foreground'
116
- : 'border-border text-foreground hover:border-primary/60 hover:bg-accent/40',
117
- ].join(' ')}
118
- >
119
- {fmt.time.format(new Date(s.start))}
120
- </button>
121
- )
122
- })}
123
- </div>
124
- )}
125
- </div>
126
- ) : null}
127
-
128
- <button
129
- type="button"
130
- disabled={!slotStart}
131
- onClick={onContinue}
132
- className="mt-6 w-full rounded-xl bg-primary py-3 font-semibold text-primary-foreground transition-opacity hover:opacity-90 disabled:opacity-40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
133
- >
134
- {labels.continueCta}
135
- </button>
136
- </div>
137
- )
138
- }
@@ -1,81 +0,0 @@
1
- import { Clock } from 'lucide-react'
2
- import { DAY_NAMES, WEEK_ORDER } from '../format'
3
- import type { BookingComponents, BookingFormatters, PublicBookingLabels, PublicService, ResolvedBookingBrand, WorkingHours } from '../types'
4
-
5
- interface OverviewStepProps {
6
- services: PublicService[]
7
- servicesLoading: boolean
8
- businessHours: WorkingHours
9
- professionalName: string
10
- labels: PublicBookingLabels
11
- brand: ResolvedBookingBrand
12
- fmt: BookingFormatters
13
- components: BookingComponents
14
- onStartBooking: (serviceId?: string) => void
15
- }
16
-
17
- /** Default view: services + opening hours + sidebar CTA (Quaddro-style). */
18
- export function OverviewStep({
19
- services, servicesLoading, businessHours, professionalName, labels, brand, fmt, components: C, onStartBooking,
20
- }: OverviewStepProps) {
21
- const openDays = new Set(businessHours.daysOfWeek)
22
- return (
23
- <div className="grid gap-6 lg:grid-cols-[1fr_18rem]">
24
- <div className="space-y-6">
25
- {/* Serviços */}
26
- <section className="rounded-2xl border border-border bg-card p-6 shadow-sm">
27
- <h3 className="mb-4 font-heading text-lg font-bold text-foreground">{labels.servicesHeading}</h3>
28
- <div className="space-y-3">
29
- {servicesLoading ? (
30
- Array.from({ length: 2 }).map((_, i) => (
31
- <div key={i} className="h-24 animate-pulse rounded-2xl bg-muted" />
32
- ))
33
- ) : (
34
- services.map((s, i) => (
35
- <C.ServiceCard
36
- key={s.id}
37
- service={s}
38
- featured={i === 0}
39
- onSelect={() => onStartBooking(s.id)}
40
- fmt={fmt}
41
- brand={brand}
42
- />
43
- ))
44
- )}
45
- </div>
46
- </section>
47
-
48
- {/* Sobre — horário de funcionamento */}
49
- <section className="rounded-2xl border border-border bg-card p-6 shadow-sm">
50
- <h3 className="font-heading text-lg font-bold text-foreground">{labels.aboutHeading}</h3>
51
- <p className="mt-3 mb-2 flex items-center gap-1.5 text-sm font-medium text-muted-foreground">
52
- <Clock className="h-4 w-4" /> {labels.openingHoursHeading}
53
- </p>
54
- <ul className="divide-y divide-border border-l-2 border-primary/20 pl-4">
55
- {WEEK_ORDER.map((day) => {
56
- const open = openDays.has(day)
57
- return (
58
- <li key={day} className="flex items-center justify-between py-2 text-sm">
59
- <span className="text-foreground">{DAY_NAMES[day]}</span>
60
- <span className={open ? 'tabular-nums text-foreground' : 'text-muted-foreground'}>
61
- {open ? `${businessHours.start} – ${businessHours.end}` : labels.closedLabel}
62
- </span>
63
- </li>
64
- )
65
- })}
66
- </ul>
67
- </section>
68
- </div>
69
-
70
- {/* Sidebar CTA + highlights */}
71
- <aside className="lg:sticky lg:top-24 lg:self-start">
72
- <C.SidebarExtras
73
- professionalName={professionalName}
74
- brand={brand}
75
- labels={labels}
76
- onStartBooking={() => onStartBooking()}
77
- />
78
- </aside>
79
- </div>
80
- )
81
- }