@fayz-ai/plugin-agenda 0.1.3

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 (114) hide show
  1. package/README.md +51 -0
  2. package/dist/AgendaContext.d.ts +13 -0
  3. package/dist/AgendaContext.d.ts.map +1 -0
  4. package/dist/AgendaPage.d.ts +11 -0
  5. package/dist/AgendaPage.d.ts.map +1 -0
  6. package/dist/bridges/create-financial-bridge.d.ts +19 -0
  7. package/dist/bridges/create-financial-bridge.d.ts.map +1 -0
  8. package/dist/calendar-styles.d.ts +8 -0
  9. package/dist/calendar-styles.d.ts.map +1 -0
  10. package/dist/chunk-3LJKZQK4.js +12 -0
  11. package/dist/chunk-3LJKZQK4.js.map +1 -0
  12. package/dist/chunk-SYDBDI3Q.cjs +15 -0
  13. package/dist/chunk-SYDBDI3Q.cjs.map +1 -0
  14. package/dist/components/AgendaGeneralSettings.d.ts +3 -0
  15. package/dist/components/AgendaGeneralSettings.d.ts.map +1 -0
  16. package/dist/components/AppointmentModal.d.ts +16 -0
  17. package/dist/components/AppointmentModal.d.ts.map +1 -0
  18. package/dist/components/AppointmentPopover.d.ts +14 -0
  19. package/dist/components/AppointmentPopover.d.ts.map +1 -0
  20. package/dist/components/BookingPaymentPanel.d.ts +18 -0
  21. package/dist/components/BookingPaymentPanel.d.ts.map +1 -0
  22. package/dist/components/CalendarSidebar.d.ts +3 -0
  23. package/dist/components/CalendarSidebar.d.ts.map +1 -0
  24. package/dist/components/CalendarToolbar.d.ts +13 -0
  25. package/dist/components/CalendarToolbar.d.ts.map +1 -0
  26. package/dist/components/EventContextMenu.d.ts +14 -0
  27. package/dist/components/EventContextMenu.d.ts.map +1 -0
  28. package/dist/components/FloatingPanel.d.ts +27 -0
  29. package/dist/components/FloatingPanel.d.ts.map +1 -0
  30. package/dist/components/MiniCalendar.d.ts +9 -0
  31. package/dist/components/MiniCalendar.d.ts.map +1 -0
  32. package/dist/components/PaymentStatusBadge.d.ts +13 -0
  33. package/dist/components/PaymentStatusBadge.d.ts.map +1 -0
  34. package/dist/components/SearchCombobox.d.ts +38 -0
  35. package/dist/components/SearchCombobox.d.ts.map +1 -0
  36. package/dist/components/StatusBadge.d.ts +5 -0
  37. package/dist/components/StatusBadge.d.ts.map +1 -0
  38. package/dist/config.d.ts +151 -0
  39. package/dist/config.d.ts.map +1 -0
  40. package/dist/data/index.d.ts +4 -0
  41. package/dist/data/index.d.ts.map +1 -0
  42. package/dist/data/mock.d.ts +3 -0
  43. package/dist/data/mock.d.ts.map +1 -0
  44. package/dist/data/supabase.d.ts +3 -0
  45. package/dist/data/supabase.d.ts.map +1 -0
  46. package/dist/data/types.d.ts +26 -0
  47. package/dist/data/types.d.ts.map +1 -0
  48. package/dist/financial-bridge.d.ts +92 -0
  49. package/dist/financial-bridge.d.ts.map +1 -0
  50. package/dist/hooks/useAgendaSettings.d.ts +7 -0
  51. package/dist/hooks/useAgendaSettings.d.ts.map +1 -0
  52. package/dist/index.cjs +5135 -0
  53. package/dist/index.cjs.map +1 -0
  54. package/dist/index.d.ts +9 -0
  55. package/dist/index.d.ts.map +1 -0
  56. package/dist/index.js +5120 -0
  57. package/dist/index.js.map +1 -0
  58. package/dist/lib/tenant.d.ts +3 -0
  59. package/dist/lib/tenant.d.ts.map +1 -0
  60. package/dist/locales/en.d.ts +2 -0
  61. package/dist/locales/en.d.ts.map +1 -0
  62. package/dist/locales/index.d.ts +2 -0
  63. package/dist/locales/index.d.ts.map +1 -0
  64. package/dist/locales/pt-BR.d.ts +2 -0
  65. package/dist/locales/pt-BR.d.ts.map +1 -0
  66. package/dist/registries.d.ts +3 -0
  67. package/dist/registries.d.ts.map +1 -0
  68. package/dist/store.d.ts +66 -0
  69. package/dist/store.d.ts.map +1 -0
  70. package/dist/tenant-222EHYUC.cjs +16 -0
  71. package/dist/tenant-222EHYUC.cjs.map +1 -0
  72. package/dist/tenant-LAUWVUYB.js +3 -0
  73. package/dist/tenant-LAUWVUYB.js.map +1 -0
  74. package/dist/types.d.ts +202 -0
  75. package/dist/types.d.ts.map +1 -0
  76. package/dist/views/CalendarView.d.ts +3 -0
  77. package/dist/views/CalendarView.d.ts.map +1 -0
  78. package/dist/views/ConfirmationsView.d.ts +3 -0
  79. package/dist/views/ConfirmationsView.d.ts.map +1 -0
  80. package/package.json +63 -0
  81. package/src/AgendaContext.tsx +11 -0
  82. package/src/AgendaPage.tsx +33 -0
  83. package/src/bridges/create-financial-bridge.ts +298 -0
  84. package/src/calendar-styles.css +245 -0
  85. package/src/calendar-styles.ts +285 -0
  86. package/src/components/AgendaGeneralSettings.tsx +64 -0
  87. package/src/components/AppointmentModal.tsx +1031 -0
  88. package/src/components/AppointmentPopover.tsx +221 -0
  89. package/src/components/BookingPaymentPanel.tsx +394 -0
  90. package/src/components/CalendarSidebar.tsx +119 -0
  91. package/src/components/CalendarToolbar.tsx +83 -0
  92. package/src/components/EventContextMenu.tsx +122 -0
  93. package/src/components/FloatingPanel.tsx +123 -0
  94. package/src/components/MiniCalendar.tsx +120 -0
  95. package/src/components/PaymentStatusBadge.tsx +62 -0
  96. package/src/components/SearchCombobox.tsx +216 -0
  97. package/src/components/StatusBadge.tsx +23 -0
  98. package/src/config.ts +269 -0
  99. package/src/data/index.ts +3 -0
  100. package/src/data/mock.ts +324 -0
  101. package/src/data/supabase.ts +570 -0
  102. package/src/data/types.ts +37 -0
  103. package/src/financial-bridge.ts +96 -0
  104. package/src/hooks/useAgendaSettings.ts +17 -0
  105. package/src/index.ts +180 -0
  106. package/src/lib/tenant.ts +3 -0
  107. package/src/locales/en.ts +207 -0
  108. package/src/locales/index.ts +7 -0
  109. package/src/locales/pt-BR.ts +207 -0
  110. package/src/registries.ts +36 -0
  111. package/src/store.ts +304 -0
  112. package/src/types.ts +265 -0
  113. package/src/views/CalendarView.tsx +904 -0
  114. package/src/views/ConfirmationsView.tsx +155 -0
@@ -0,0 +1,221 @@
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
+ <PersonLink personId={booking.clientId} name={booking.clientName ?? 'Unknown'} className="text-base" />
130
+ <p className="text-sm text-muted-foreground mt-0.5">{fmtDate(booking.startsAt)} &middot; {timeRange}</p>
131
+ </div>
132
+ </div>
133
+
134
+ {booking.services && booking.services.length > 0 && (
135
+ <div className="flex items-start gap-3">
136
+ <Clock className="h-4 w-4 mt-0.5 text-muted-foreground shrink-0" />
137
+ <div className="text-sm space-y-0.5">
138
+ {booking.services.map((svc) => (
139
+ <p key={svc.id}>{svc.name} <span className="text-muted-foreground">({svc.durationMinutes}min)</span></p>
140
+ ))}
141
+ </div>
142
+ </div>
143
+ )}
144
+
145
+ {booking.professionalName && (
146
+ <div className="flex items-center gap-3">
147
+ <User className="h-4 w-4 text-muted-foreground shrink-0" />
148
+ <span className="text-sm">{booking.professionalName}</span>
149
+ </div>
150
+ )}
151
+
152
+ {booking.locationName && (
153
+ <div className="flex items-center gap-3">
154
+ <MapPin className="h-4 w-4 text-muted-foreground shrink-0" />
155
+ <span className="text-sm">{booking.locationName}</span>
156
+ </div>
157
+ )}
158
+
159
+ {booking.notes && (
160
+ <div className="flex items-start gap-3">
161
+ <FileText className="h-4 w-4 mt-0.5 text-muted-foreground shrink-0" />
162
+ <p className="text-sm text-muted-foreground">{booking.notes}</p>
163
+ </div>
164
+ )}
165
+
166
+ {booking.orderTotal != null && booking.orderTotal > 0 && (() => {
167
+ const total = fmtCurrency(booking.orderTotal, config.currency.locale, config.currency.code)
168
+ const ps = booking.paymentStatus ?? 'none'
169
+ const hasFinancial = config.modules.financial && booking.orderId
170
+
171
+ // No financial module — just show total
172
+ if (!hasFinancial) {
173
+ return (
174
+ <div className="pt-2 border-t fayz-glass-divider flex items-center justify-between text-sm">
175
+ <span className="text-muted-foreground">{t('agenda.appointment.total')}</span>
176
+ <span className="font-medium">{total}</span>
177
+ </div>
178
+ )
179
+ }
180
+
181
+ const badgeConfig: Record<string, { icon: React.ElementType; bg: string; text: string; label: string }> = {
182
+ none: { icon: HandCoins, bg: 'bg-warning/10 hover:bg-warning/15', text: 'text-warning', label: `${total} · ${t('agenda.payment.collect')}` },
183
+ pending: { icon: Clock, bg: 'bg-warning/10 hover:bg-warning/15', text: 'text-warning', label: `${total} · ${t('agenda.payment.pending')}` },
184
+ partial: { icon: DollarSign, bg: 'bg-warning/10 hover:bg-warning/15', text: 'text-warning', label: `${total} · ${t('agenda.payment.partial')}` },
185
+ paid: { icon: Check, bg: 'bg-success/10 hover:bg-success/15', text: 'text-success', label: `${total} · ${t('agenda.payment.paid')}` },
186
+ overdue: { icon: AlertTriangle, bg: 'bg-destructive/10 hover:bg-destructive/15', text: 'text-destructive', label: `${total} · ${t('agenda.payment.overdue')}` },
187
+ cancelled: { icon: X, bg: 'bg-muted hover:bg-muted/80', text: 'text-muted-foreground', label: `${total} · ${t('agenda.payment.cancelled')}` },
188
+ }
189
+ const cfg = badgeConfig[ps] ?? badgeConfig.none
190
+ const StatusIcon = cfg.icon
191
+
192
+ return (
193
+ <div className="pt-2 border-t fayz-glass-divider flex items-center justify-between">
194
+ <span className="text-xs text-muted-foreground">{t('agenda.appointment.total')}</span>
195
+ <button
196
+ type="button"
197
+ onClick={() => { onClose(); openModal('edit', { bookingId: booking.id, initialTab: 'financial' }) }}
198
+ className={`inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[11px] font-medium transition-colors ${cfg.bg} ${cfg.text}`}
199
+ >
200
+ <StatusIcon className="h-3 w-3" />
201
+ {cfg.label}
202
+ </button>
203
+ </div>
204
+ )
205
+ })()}
206
+
207
+ <div className="pt-2 border-t fayz-glass-divider">
208
+ <PopoverStatusSelect
209
+ value={booking.status}
210
+ statuses={config.statuses}
211
+ bookingStartsAt={booking.startsAt}
212
+ onChange={(v) => { updateStatus(booking.id, v); onClose() }}
213
+ />
214
+ </div>
215
+ </div>
216
+ </>
217
+ )}
218
+ </FloatingPanel>
219
+ </>
220
+ )
221
+ }
@@ -0,0 +1,394 @@
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
+ }