@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,231 +0,0 @@
1
- import React, { useState, useRef, useEffect } from 'react'
2
- import { X, Pencil, Trash2, Clock, MapPin, User, FileText, DollarSign, Check, AlertTriangle, HandCoins } from 'lucide-react'
3
- import { PersonLink } from '@fayz-ai/saas'
4
- import { FloatingPanel, type FloatingPanelRef } from './FloatingPanel'
5
- import { useAgendaConfig, useAgendaStore } from '../AgendaContext'
6
- import { useTranslation } from '@fayz-ai/core'
7
- import { isStatusAvailable } from '../types'
8
- import type { CalendarBooking } from '../types'
9
-
10
- interface Props {
11
- booking: CalendarBooking
12
- position: { x: number; y: number }
13
- onClose: () => void
14
- onEdit: (id: string) => void
15
- }
16
-
17
- function fmtTime(iso: string) {
18
- return new Date(iso).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })
19
- }
20
- function fmtDate(iso: string) {
21
- return new Date(iso).toLocaleDateString(undefined, { weekday: 'long', month: 'long', day: 'numeric' })
22
- }
23
- function fmtCurrency(amount: number, locale: string, code: string) {
24
- return new Intl.NumberFormat(locale, { style: 'currency', currency: code }).format(amount)
25
- }
26
-
27
- // Inline status select for the popover
28
- function PopoverStatusSelect({ value, statuses, bookingStartsAt, onChange }: {
29
- value: string; statuses: Array<{ value: string; label: string; color: string; availableWhen?: string }>
30
- bookingStartsAt: string; onChange: (v: string) => void
31
- }) {
32
- const [open, setOpen] = useState(false)
33
- const ref = useRef<HTMLDivElement>(null)
34
- const current = statuses.find((s) => s.value === value)
35
-
36
- useEffect(() => {
37
- if (!open) return
38
- const h = (e: MouseEvent) => { if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false) }
39
- document.addEventListener('mousedown', h)
40
- return () => document.removeEventListener('mousedown', h)
41
- }, [open])
42
-
43
- return (
44
- <div ref={ref} className="relative inline-block">
45
- <button type="button" onClick={() => setOpen((p) => !p)}
46
- className="inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-medium transition-colors hover:opacity-80"
47
- style={{ backgroundColor: (current?.color ?? '#6b7280') + '18', color: current?.color ?? '#6b7280' }}>
48
- <span className="h-2 w-2 rounded-full" style={{ backgroundColor: current?.color }} />
49
- {current?.label ?? value}
50
- <svg className="h-3 w-3 opacity-50" viewBox="0 0 16 16" fill="none">
51
- <path d="M4 6l4 4 4-4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
52
- </svg>
53
- </button>
54
- {open && (
55
- <div className="fayz-glass-surface absolute bottom-full left-0 z-50 mb-1 rounded-lg border bg-popover shadow-lg py-1 animate-in fade-in zoom-in-95 duration-100 min-w-[140px]">
56
- {statuses.map((s) => {
57
- const available = isStatusAvailable(s as any, bookingStartsAt)
58
- return (
59
- <button key={s.value} type="button" disabled={!available}
60
- onClick={() => { if (available) { onChange(s.value); setOpen(false) } }}
61
- className={`flex w-full items-center gap-2 px-3 py-1.5 text-xs transition-colors ${
62
- !available ? 'opacity-30 cursor-not-allowed' : s.value === value ? 'bg-muted/30' : 'hover:bg-muted/50'
63
- }`}>
64
- <span className="h-2 w-2 rounded-full shrink-0" style={{ backgroundColor: s.color }} />
65
- <span>{s.label}</span>
66
- </button>
67
- )
68
- })}
69
- </div>
70
- )}
71
- </div>
72
- )
73
- }
74
-
75
- export function AppointmentPopover({ booking, position, onClose, onEdit }: Props) {
76
- const t = useTranslation()
77
- const config = useAgendaConfig()
78
- const deleteBooking = useAgendaStore((s) => s.deleteBooking)
79
- const updateStatus = useAgendaStore((s) => s.updateBookingStatus)
80
- const openModal = useAgendaStore((s) => s.openAppointmentModal)
81
- const [confirmingDelete, setConfirmingDelete] = useState(false)
82
- const [deleting, setDeleting] = useState(false)
83
- const statusConfig = config.statuses.find((s) => s.value === booking.status)
84
- const color = statusConfig?.color ?? '#6b7280'
85
- const timeRange = `${fmtTime(booking.startsAt)}${booking.endsAt ? ' – ' + fmtTime(booking.endsAt) : ''}`
86
-
87
- async function handleDelete() {
88
- setDeleting(true)
89
- try { await deleteBooking(booking.id); onClose() } catch { setDeleting(false) }
90
- }
91
-
92
- return (
93
- <>
94
- <FloatingPanel position={position} width={320} height={340} onClose={onClose}>
95
- {(panel) => (
96
- <>
97
- {/* Action bar */}
98
- <div className="flex items-center justify-end gap-0.5 px-3 pt-3 pb-1">
99
- {confirmingDelete ? (
100
- <>
101
- <span className="text-xs text-destructive font-medium mr-auto pl-1">{t('agenda.appointment.deleteConfirm')}</span>
102
- <button onClick={() => setConfirmingDelete(false)}
103
- className="rounded px-2 py-0.5 text-[11px] font-medium hover:bg-muted/50 transition-colors">{t('agenda.appointment.cancel')}</button>
104
- <button onClick={handleDelete} disabled={deleting}
105
- className="rounded bg-destructive border border-destructive px-2 py-0.5 text-[11px] font-medium text-destructive-foreground hover:bg-destructive/90 shadow-button-primary active:shadow-button-inset disabled:opacity-40 transition-colors">{deleting ? t('agenda.appointment.deleting') : t('agenda.appointment.confirmDelete')}</button>
106
- </>
107
- ) : (
108
- <>
109
- <button onClick={() => panel.expandThen(() => onEdit(booking.id))}
110
- className="p-1.5 rounded-full hover:bg-muted transition-colors" title={t('common.edit')}>
111
- <Pencil className="h-4 w-4 text-muted-foreground" />
112
- </button>
113
- <button onClick={() => setConfirmingDelete(true)}
114
- className="p-1.5 rounded-full hover:bg-destructive/10 transition-colors" title={t('common.delete')}>
115
- <Trash2 className="h-4 w-4 text-muted-foreground hover:text-destructive" />
116
- </button>
117
- <button onClick={panel.dismiss} className="p-1.5 rounded-full hover:bg-muted transition-colors">
118
- <X className="h-4 w-4 text-muted-foreground" />
119
- </button>
120
- </>
121
- )}
122
- </div>
123
-
124
- {/* Content */}
125
- <div className="px-5 pb-5 space-y-3">
126
- <div className="flex items-start gap-3">
127
- <span className="mt-1.5 h-3 w-3 rounded shrink-0" style={{ backgroundColor: color }} />
128
- <div className="min-w-0">
129
- {/* Client agendas link to the person; simple events (no client
130
- capability) render the free-text title as a plain heading —
131
- booking.clientName carries the event title in that model. */}
132
- {config.capabilities.client ? (
133
- <PersonLink personId={booking.clientId} name={booking.clientName ?? 'Unknown'} className="text-base" />
134
- ) : (
135
- <p className="text-base font-medium truncate">{booking.clientName || t('agenda.appointment.untitled')}</p>
136
- )}
137
- <p className="text-sm text-muted-foreground mt-0.5">{fmtDate(booking.startsAt)} &middot; {timeRange}</p>
138
- </div>
139
- </div>
140
-
141
- {booking.services && booking.services.length > 0 && (
142
- <div className="flex items-start gap-3">
143
- <Clock className="h-4 w-4 mt-0.5 text-muted-foreground shrink-0" />
144
- <div className="text-sm space-y-0.5">
145
- {booking.services.map((svc) => (
146
- <p key={svc.id}>{svc.name} <span className="text-muted-foreground">({svc.durationMinutes}min)</span></p>
147
- ))}
148
- </div>
149
- </div>
150
- )}
151
-
152
- {config.capabilities.professional && booking.professionalName && (
153
- <div className="flex items-center gap-3">
154
- <User className="h-4 w-4 text-muted-foreground shrink-0" />
155
- <span className="text-sm">{booking.professionalName}</span>
156
- </div>
157
- )}
158
-
159
- {booking.locationName && (
160
- <div className="flex items-center gap-3">
161
- <MapPin className="h-4 w-4 text-muted-foreground shrink-0" />
162
- <span className="text-sm">{booking.locationName}</span>
163
- </div>
164
- )}
165
-
166
- {booking.notes && (
167
- <div className="flex items-start gap-3">
168
- <FileText className="h-4 w-4 mt-0.5 text-muted-foreground shrink-0" />
169
- <p className="text-sm text-muted-foreground">{booking.notes}</p>
170
- </div>
171
- )}
172
-
173
- {booking.orderTotal != null && booking.orderTotal > 0 && (() => {
174
- const total = fmtCurrency(booking.orderTotal, config.currency.locale, config.currency.code)
175
- const ps = booking.paymentStatus ?? 'none'
176
- const hasFinancial = config.modules.financial && booking.orderId
177
-
178
- // No financial module — just show total
179
- if (!hasFinancial) {
180
- return (
181
- <div className="pt-2 border-t fayz-glass-divider flex items-center justify-between text-sm">
182
- <span className="text-muted-foreground">{t('agenda.appointment.total')}</span>
183
- <span className="font-medium">{total}</span>
184
- </div>
185
- )
186
- }
187
-
188
- const badgeConfig: Record<string, { icon: React.ElementType; bg: string; text: string; label: string }> = {
189
- none: { icon: HandCoins, bg: 'bg-warning/10 hover:bg-warning/15', text: 'text-warning', label: `${total} · ${t('agenda.payment.collect')}` },
190
- pending: { icon: Clock, bg: 'bg-warning/10 hover:bg-warning/15', text: 'text-warning', label: `${total} · ${t('agenda.payment.pending')}` },
191
- partial: { icon: DollarSign, bg: 'bg-warning/10 hover:bg-warning/15', text: 'text-warning', label: `${total} · ${t('agenda.payment.partial')}` },
192
- paid: { icon: Check, bg: 'bg-success/10 hover:bg-success/15', text: 'text-success', label: `${total} · ${t('agenda.payment.paid')}` },
193
- overdue: { icon: AlertTriangle, bg: 'bg-destructive/10 hover:bg-destructive/15', text: 'text-destructive', label: `${total} · ${t('agenda.payment.overdue')}` },
194
- cancelled: { icon: X, bg: 'bg-muted hover:bg-muted/80', text: 'text-muted-foreground', label: `${total} · ${t('agenda.payment.cancelled')}` },
195
- }
196
- const cfg = badgeConfig[ps] ?? badgeConfig.none
197
- const StatusIcon = cfg.icon
198
-
199
- return (
200
- <div className="pt-2 border-t fayz-glass-divider flex items-center justify-between">
201
- <span className="text-xs text-muted-foreground">{t('agenda.appointment.total')}</span>
202
- <button
203
- type="button"
204
- onClick={() => { onClose(); openModal('edit', { bookingId: booking.id, initialTab: 'financial' }) }}
205
- className={`inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[11px] font-medium transition-colors ${cfg.bg} ${cfg.text}`}
206
- >
207
- <StatusIcon className="h-3 w-3" />
208
- {cfg.label}
209
- </button>
210
- </div>
211
- )
212
- })()}
213
-
214
- {/* Status workflow — hidden for simple events (no status capability). */}
215
- {config.capabilities.status && (
216
- <div className="pt-2 border-t fayz-glass-divider">
217
- <PopoverStatusSelect
218
- value={booking.status}
219
- statuses={config.statuses}
220
- bookingStartsAt={booking.startsAt}
221
- onChange={(v) => { updateStatus(booking.id, v); onClose() }}
222
- />
223
- </div>
224
- )}
225
- </div>
226
- </>
227
- )}
228
- </FloatingPanel>
229
- </>
230
- )
231
- }
@@ -1,394 +0,0 @@
1
- import React, { useState, useEffect } from 'react'
2
- import { createPortal } from 'react-dom'
3
- import { X, Check, Banknote, QrCode, CreditCard, ArrowRightLeft, FileCheck, ExternalLink } from 'lucide-react'
4
- import { useAgendaConfig } from '../AgendaContext'
5
- import { useTranslation } from '@fayz-ai/core'
6
- import type { BookingPaymentDetail } from '../financial-bridge'
7
-
8
- // ---------------------------------------------------------------------------
9
- // Payment method type visuals (mirrors financial PaymentModal)
10
- // ---------------------------------------------------------------------------
11
-
12
- const TYPE_ICONS: Record<string, React.ElementType> = {
13
- cash: Banknote, pix: QrCode, credit_card: CreditCard, debit_card: CreditCard,
14
- bank_transfer: ArrowRightLeft, check: FileCheck,
15
- }
16
-
17
- const TYPE_COLORS: Record<string, string> = {
18
- cash: 'border-success/30 bg-success/5 hover:bg-success/10 data-[active=true]:border-success data-[active=true]:bg-success/15',
19
- pix: 'border-teal-500/30 bg-teal-500/5 hover:bg-teal-500/10 data-[active=true]:border-teal-500 data-[active=true]:bg-teal-500/15',
20
- credit_card: 'border-info/30 bg-info/5 hover:bg-info/10 data-[active=true]:border-info data-[active=true]:bg-info/15',
21
- debit_card: 'border-magic/30 bg-magic/5 hover:bg-magic/10 data-[active=true]:border-magic data-[active=true]:bg-magic/15',
22
- bank_transfer: 'border-warning/30 bg-warning/5 hover:bg-warning/10 data-[active=true]:border-warning data-[active=true]:bg-warning/15',
23
- check: 'border-input/30 bg-muted/5 hover:bg-muted/10 data-[active=true]:border-input data-[active=true]:bg-muted/15',
24
- }
25
-
26
- const ICON_COLORS: Record<string, string> = {
27
- cash: 'text-success', pix: 'text-teal-500', credit_card: 'text-info',
28
- debit_card: 'text-magic', bank_transfer: 'text-warning', check: 'text-muted-foreground',
29
- }
30
-
31
- // ---------------------------------------------------------------------------
32
- // Component
33
- // ---------------------------------------------------------------------------
34
-
35
- interface Props {
36
- orderId: string
37
- orderTotal: number
38
- services?: Array<{ name: string; price: number; durationMinutes: number }>
39
- onClose: () => void
40
- onPaymentChange?: () => void
41
- formatCurrency: (amount: number) => string
42
- /** Render inline (no portal overlay) — for embedding in tabs */
43
- inline?: boolean
44
- }
45
-
46
- export function BookingPaymentPanel({ orderId, orderTotal, services, onClose, onPaymentChange, formatCurrency, inline }: Props) {
47
- const t = useTranslation()
48
- const config = useAgendaConfig()
49
- const bridge = config.financialBridge!
50
-
51
- const [visible, setVisible] = useState(false)
52
- const [loading, setLoading] = useState(true)
53
- const [detail, setDetail] = useState<BookingPaymentDetail | null>(null)
54
-
55
- // Payment form
56
- const [selectedTypeId, setSelectedTypeId] = useState('')
57
- const [paymentTypes, setPaymentTypes] = useState<Array<{ id: string; name: string; transactionType?: string }>>([])
58
- const [bankAccounts, setBankAccounts] = useState<Array<{ id: string; name: string; accountType: string }>>([])
59
- const [filteredMethods, setFilteredMethods] = useState<Array<{ id: string; name: string }>>([])
60
- const [selectedBankId, setSelectedBankId] = useState('')
61
- const [paymentMethodId, setPaymentMethodId] = useState('')
62
- const [cardBrand, setCardBrand] = useState('')
63
- const [cardInstallments, setCardInstallments] = useState(1)
64
- const [processing, setProcessing] = useState(false)
65
- const [justPaid, setJustPaid] = useState(false)
66
-
67
- useEffect(() => { requestAnimationFrame(() => setVisible(true)) }, [])
68
-
69
- // Single combined fetch for all initial data
70
- useEffect(() => {
71
- let cancelled = false
72
- setLoading(true)
73
- Promise.all([
74
- bridge.getPaymentDetail(orderId).catch(() => null),
75
- bridge.getPaymentMethodTypes().catch(() => [] as typeof paymentTypes),
76
- bridge.getBankAccounts().catch(() => [] as typeof bankAccounts),
77
- ]).then(([d, types, accounts]) => {
78
- if (cancelled) return
79
- setDetail(d)
80
- setPaymentTypes(types)
81
- setBankAccounts(accounts)
82
- setLoading(false)
83
- })
84
- return () => { cancelled = true }
85
- }, [orderId])
86
-
87
- function handleClose() {
88
- setVisible(false)
89
- setTimeout(onClose, 200)
90
- }
91
-
92
- const selectedType = paymentTypes.find((t) => t.id === selectedTypeId)
93
- const txType = selectedType?.transactionType ?? ''
94
- const needsBank = ['bank_transfer', 'pix', 'check'].includes(txType)
95
- const needsCard = txType === 'credit_card' || txType === 'debit_card'
96
- const needsCardInstallments = txType === 'credit_card'
97
-
98
- function handleSelectType(typeId: string) {
99
- setSelectedTypeId(typeId)
100
- setSelectedBankId('')
101
- setPaymentMethodId('')
102
- setCardBrand('')
103
- setCardInstallments(1)
104
- // Fetch specific payment methods for this type
105
- if (bridge.getPaymentMethods) {
106
- bridge.getPaymentMethods(typeId).then(setFilteredMethods).catch(() => setFilteredMethods([]))
107
- }
108
- }
109
-
110
- async function handleCheckout() {
111
- if (!selectedTypeId) return
112
- setProcessing(true)
113
- try {
114
- const result = await bridge.checkout(orderId, {
115
- paymentMethodTypeId: selectedTypeId,
116
- paymentMethodId: paymentMethodId || undefined,
117
- bankAccountId: needsBank ? selectedBankId || undefined : undefined,
118
- cardBrand: needsCard ? cardBrand || undefined : undefined,
119
- cardInstallments: needsCardInstallments ? cardInstallments : undefined,
120
- })
121
- setDetail(result)
122
- setJustPaid(true)
123
- onPaymentChange?.()
124
- // Show confirmation animation, then transition to summary
125
- setTimeout(() => setJustPaid(false), 2200)
126
- } catch { /* toast */ }
127
- setProcessing(false)
128
- }
129
-
130
- const isPaid = detail?.status === 'paid'
131
-
132
- const content = (
133
- <div className="px-5 py-4 space-y-4">
134
-
135
- {/* Loading */}
136
- {loading && (
137
- <div className="space-y-3 py-2">
138
- {[...Array(3)].map((_, i) => (
139
- <div key={i} className="h-8 rounded-lg bg-muted animate-pulse" />
140
- ))}
141
- </div>
142
- )}
143
-
144
- {/* Just paid — confirmation animation */}
145
- {!loading && isPaid && justPaid && (
146
- <div className="text-center py-8 space-y-2 animate-in fade-in zoom-in-95 duration-300">
147
- <div className="flex h-14 w-14 mx-auto items-center justify-center rounded-full bg-success/10 animate-bounce" style={{ animationDuration: '600ms', animationIterationCount: '2' }}>
148
- <Check className="h-7 w-7 text-success" />
149
- </div>
150
- <p className="text-sm font-semibold text-success">{t('agenda.payment.checkoutComplete')}</p>
151
- <p className="text-xs text-muted-foreground">{formatCurrency(detail!.totalAmount)}</p>
152
- </div>
153
- )}
154
-
155
- {/* Invoice summary — shown after animation or when returning to an already-paid/pending booking */}
156
- {!loading && detail && detail.status !== 'none' && !justPaid && (
157
- <div className="space-y-3 animate-in fade-in duration-300">
158
- {/* Movement rows with inline status badge */}
159
- {detail.movements.length > 0 ? (
160
- <div className="space-y-1.5">
161
- {detail.movements.map((mov) => {
162
- const methodName = mov.paymentMethodTypeName
163
- const txKey = (methodName ?? '').toLowerCase().replace(/\s+/g, '_')
164
- const Icon = TYPE_ICONS[txKey] ?? Banknote
165
- const iconColor = ICON_COLORS[txKey] ?? 'text-muted-foreground'
166
- const isMovPaid = mov.status === 'paid'
167
- const isPartial = mov.status === 'partial'
168
- const isOverdue = mov.status === 'overdue'
169
-
170
- return (
171
- <div key={mov.id} className="flex items-center gap-3 rounded-lg border px-3 py-2.5">
172
- <Icon className={`h-4 w-4 shrink-0 ${isMovPaid ? iconColor : 'text-muted-foreground'}`} />
173
- <div className="flex-1 min-w-0">
174
- <p className="text-sm font-medium truncate">
175
- {methodName ?? '—'}
176
- {mov.cardBrand ? ` · ${mov.cardBrand}` : ''}
177
- {mov.cardInstallments && mov.cardInstallments > 1 ? ` ${mov.cardInstallments}x` : ''}
178
- </p>
179
- <p className="text-[10px] text-muted-foreground">
180
- {mov.paymentDate ?? mov.dueDate}
181
- {detail.movements.length > 1 && mov.installmentNumber ? ` · ${t('agenda.payment.installment')} ${mov.installmentNumber}` : ''}
182
- </p>
183
- </div>
184
- <div className="flex items-center gap-2 shrink-0">
185
- {isMovPaid && (
186
- <span className="inline-flex items-center gap-1 rounded-full bg-success/10 px-2 py-0.5 text-[10px] font-medium text-success">
187
- <Check className="h-2.5 w-2.5" /> {t('agenda.payment.paid')}
188
- </span>
189
- )}
190
- {isPartial && (
191
- <span className="inline-flex items-center gap-1 rounded-full bg-warning/10 px-2 py-0.5 text-[10px] font-medium text-warning">
192
- ! {formatCurrency(mov.paidAmount)}/{formatCurrency(mov.amount)}
193
- </span>
194
- )}
195
- {isOverdue && (
196
- <span className="inline-flex items-center gap-1 rounded-full bg-destructive/10 px-2 py-0.5 text-[10px] font-medium text-destructive">
197
- ! {t('agenda.payment.overdue')}
198
- </span>
199
- )}
200
- {mov.status === 'pending' && (
201
- <span className="inline-flex items-center rounded-full bg-muted px-2 py-0.5 text-[10px] font-medium text-muted-foreground">
202
- {t('agenda.payment.pending')}
203
- </span>
204
- )}
205
- <span className="text-sm font-semibold">{formatCurrency(mov.paidAmount || mov.amount)}</span>
206
- </div>
207
- </div>
208
- )
209
- })}
210
- </div>
211
- ) : (
212
- <div className="flex items-center justify-between text-sm px-1">
213
- <span className="text-muted-foreground">{t('agenda.payment.total')}</span>
214
- <span className="font-semibold">{formatCurrency(detail.totalAmount)}</span>
215
- </div>
216
- )}
217
-
218
- {/* Invoice ref + link */}
219
- <div className="flex items-center justify-between">
220
- {detail.referenceNumber && (
221
- <span className="text-[10px] font-mono text-muted-foreground">#{detail.referenceNumber}</span>
222
- )}
223
- <a
224
- href={`#/financial/receivables/detail/${detail.invoiceId}`}
225
- className="inline-flex items-center gap-1.5 text-xs font-medium text-primary hover:underline ml-auto"
226
- >
227
- {t('agenda.payment.viewInvoice')}
228
- <ExternalLink className="h-3 w-3" />
229
- </a>
230
- </div>
231
- </div>
232
- )}
233
-
234
- {/* Checkout view — only when no invoice exists yet */}
235
- {!loading && (!detail || detail.movements.length === 0) && (
236
- <>
237
- {/* Service summary */}
238
- {services && services.length > 0 && (
239
- <div>
240
- <p className="text-[11px] font-medium text-muted-foreground mb-2">{t('agenda.payment.serviceSummary')}</p>
241
- <div className="space-y-0.5">
242
- {services.map((svc, i) => (
243
- <div key={i} className="flex items-center justify-between text-sm py-1">
244
- <span className="truncate">{svc.name}</span>
245
- <span className="shrink-0 text-muted-foreground">{formatCurrency(svc.price)}</span>
246
- </div>
247
- ))}
248
- </div>
249
- </div>
250
- )}
251
-
252
- {/* Total */}
253
- <div className="flex items-center justify-between text-sm font-semibold pt-2 border-t">
254
- <span>{t('agenda.payment.total')}</span>
255
- <span>{formatCurrency(orderTotal)}</span>
256
- </div>
257
-
258
- {/* Payment method grid */}
259
- <div>
260
- <p className="text-[11px] font-medium text-muted-foreground mb-2">{t('agenda.payment.paymentMethod')}</p>
261
- {paymentTypes.length === 0 ? (
262
- <div className="rounded-lg border border-dashed border-input bg-muted/30 px-3 py-3 text-center">
263
- <p className="text-xs text-muted-foreground">{t('agenda.payment.noMethods')}</p>
264
- <a href="#/financial/settings" className="mt-1.5 inline-block text-xs font-medium text-primary hover:underline">
265
- {t('agenda.payment.configureMethods')} →
266
- </a>
267
- </div>
268
- ) : (
269
- <div className="grid grid-cols-3 gap-1.5">
270
- {paymentTypes.map((pt) => {
271
- const ptTx = pt.transactionType ?? pt.name.toLowerCase().replace(/\s+/g, '_')
272
- const Icon = TYPE_ICONS[ptTx] ?? Banknote
273
- const colorClass = TYPE_COLORS[ptTx] ?? TYPE_COLORS.cash
274
- const iconColor = ICON_COLORS[ptTx] ?? ICON_COLORS.cash
275
- const isActive = selectedTypeId === pt.id
276
- return (
277
- <button
278
- key={pt.id}
279
- type="button"
280
- data-active={isActive}
281
- onClick={() => handleSelectType(pt.id)}
282
- className={`flex flex-col items-center gap-1 rounded-lg border p-2.5 text-[10px] font-medium transition-all ${colorClass}`}
283
- >
284
- <Icon className={`h-4 w-4 ${iconColor}`} />
285
- <span className="truncate w-full text-center">{pt.name}</span>
286
- </button>
287
- )
288
- })}
289
- </div>
290
- )}
291
- </div>
292
-
293
- {/* Extra fields based on payment type */}
294
- {selectedTypeId && (
295
- <div className="space-y-3">
296
- {/* Specific payment method (filtered by type) */}
297
- {filteredMethods.length > 0 && (
298
- <div>
299
- <label className="text-[11px] font-medium text-muted-foreground">
300
- {needsCard ? t('agenda.payment.card') : t('agenda.payment.method')}
301
- </label>
302
- <select value={paymentMethodId} onChange={(e) => setPaymentMethodId(e.target.value)}
303
- className="w-full mt-1 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-2.5 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-ring">
304
- <option value="">{t('agenda.payment.select')}</option>
305
- {filteredMethods.map((m) => <option key={m.id} value={m.id}>{m.name}</option>)}
306
- </select>
307
- </div>
308
- )}
309
-
310
- {/* Card brand + installments */}
311
- {needsCard && (
312
- <div className={`grid gap-2.5 ${needsCardInstallments ? 'grid-cols-2' : 'grid-cols-1'}`}>
313
- <div>
314
- <label className="text-[11px] font-medium text-muted-foreground">{t('agenda.payment.cardBrand')}</label>
315
- <select value={cardBrand} onChange={(e) => setCardBrand(e.target.value)}
316
- className="w-full mt-1 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-2.5 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-ring">
317
- <option value="">{t('agenda.payment.selectBrand')}</option>
318
- {['Visa', 'Mastercard', 'American Express', 'Elo', 'Hipercard', 'Diners Club'].map((b) => (
319
- <option key={b} value={b}>{b}</option>
320
- ))}
321
- </select>
322
- </div>
323
- {needsCardInstallments && (
324
- <div>
325
- <label className="text-[11px] font-medium text-muted-foreground">{t('agenda.payment.cardInstallments')}</label>
326
- <select value={cardInstallments} onChange={(e) => setCardInstallments(Number(e.target.value))}
327
- className="w-full mt-1 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-2.5 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-ring">
328
- {Array.from({ length: 12 }, (_, i) => i + 1).map((n) => (
329
- <option key={n} value={n}>{n}x {n > 1 ? formatCurrency(orderTotal / n) : ''}</option>
330
- ))}
331
- </select>
332
- </div>
333
- )}
334
- </div>
335
- )}
336
-
337
- {/* Bank account */}
338
- {needsBank && bankAccounts.length > 0 && (
339
- <div>
340
- <label className="text-[11px] font-medium text-muted-foreground">{t('agenda.payment.bankAccount')}</label>
341
- <select value={selectedBankId} onChange={(e) => setSelectedBankId(e.target.value)}
342
- className="w-full mt-1 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-2.5 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-ring">
343
- <option value="">{t('agenda.payment.selectAccount')}</option>
344
- {bankAccounts.map((a) => (
345
- <option key={a.id} value={a.id}>{a.name}</option>
346
- ))}
347
- </select>
348
- </div>
349
- )}
350
- </div>
351
- )}
352
-
353
- {/* Confirm button */}
354
- <button
355
- type="button"
356
- onClick={handleCheckout}
357
- disabled={processing || !selectedTypeId}
358
- className="w-full rounded-lg bg-primary border border-primary py-2.5 text-sm font-medium text-primary-foreground hover:bg-primary/90 shadow-button-primary active:shadow-button-inset disabled:opacity-40 transition-colors"
359
- >
360
- {processing
361
- ? t('agenda.payment.checkingOut')
362
- : `${t('agenda.payment.checkout')} ${formatCurrency(orderTotal)}`
363
- }
364
- </button>
365
- </>
366
- )}
367
- </div>
368
- )
369
-
370
- if (inline) return content
371
-
372
- return createPortal(
373
- <div
374
- className="fixed inset-0 z-50 flex items-center justify-center transition-colors duration-200"
375
- style={{ backgroundColor: visible ? 'rgba(0,0,0,0.5)' : 'rgba(0,0,0,0)' }}
376
- onClick={handleClose}
377
- >
378
- <div
379
- className="w-full max-w-md rounded-xl border bg-card shadow-2xl mx-4 transition-all duration-200 max-h-[90vh] overflow-y-auto"
380
- style={{ opacity: visible ? 1 : 0, transform: visible ? 'scale(1) translateY(0)' : 'scale(0.95) translateY(8px)' }}
381
- onClick={(e) => e.stopPropagation()}
382
- >
383
- <div className="flex items-center justify-between px-5 py-4 border-b sticky top-0 bg-card z-10">
384
- <h3 className="text-base font-semibold">{t('agenda.payment.title')}</h3>
385
- <button onClick={handleClose} className="p-1 text-muted-foreground hover:text-foreground transition-colors">
386
- <X className="h-4 w-4" />
387
- </button>
388
- </div>
389
- {content}
390
- </div>
391
- </div>,
392
- document.body,
393
- )
394
- }