@fayz-ai/plugin-financial 0.9.0 → 0.9.2

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 (95) hide show
  1. package/README.md +3 -3
  2. package/dist/components/PersonStatementWidget.d.ts +1 -1
  3. package/dist/components/PersonStatementWidget.d.ts.map +1 -1
  4. package/dist/{FinancialContext.d.ts → context.d.ts} +4 -2
  5. package/dist/context.d.ts.map +1 -0
  6. package/dist/data/registries.d.ts.map +1 -0
  7. package/dist/index.d.ts +13 -4
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +1426 -1426
  10. package/dist/index.js.map +1 -1
  11. package/dist/integrations/stripe/connectorDef.d.ts +14 -0
  12. package/dist/integrations/stripe/connectorDef.d.ts.map +1 -0
  13. package/dist/integrations/stripe/data.d.ts +43 -0
  14. package/dist/integrations/stripe/data.d.ts.map +1 -0
  15. package/dist/lib/message-events.d.ts +12 -0
  16. package/dist/lib/message-events.d.ts.map +1 -0
  17. package/dist/{quick-add.d.ts → lib/quick-add.d.ts} +1 -1
  18. package/dist/lib/quick-add.d.ts.map +1 -0
  19. package/dist/locales/en.d.ts.map +1 -1
  20. package/dist/locales/pt-BR.d.ts.map +1 -1
  21. package/dist/{FinancialPage.d.ts → views/FinancialPage.d.ts} +3 -3
  22. package/dist/views/FinancialPage.d.ts.map +1 -0
  23. package/dist/views/InvoiceDetailView.d.ts.map +1 -1
  24. package/dist/views/InvoiceFormView.d.ts.map +1 -1
  25. package/dist/views/InvoiceListView.d.ts.map +1 -1
  26. package/dist/views/PayablesView.d.ts +1 -1
  27. package/dist/views/PayablesView.d.ts.map +1 -1
  28. package/dist/views/ReceivablesView.d.ts +1 -1
  29. package/dist/views/ReceivablesView.d.ts.map +1 -1
  30. package/dist/views/StatementsView.d.ts.map +1 -1
  31. package/dist/views/dashboardWidgets.d.ts +1 -1
  32. package/dist/views/dashboardWidgets.d.ts.map +1 -1
  33. package/package.json +12 -14
  34. package/dist/FinancialContext.d.ts.map +0 -1
  35. package/dist/FinancialPage.d.ts.map +0 -1
  36. package/dist/contract.test.d.ts +0 -2
  37. package/dist/contract.test.d.ts.map +0 -1
  38. package/dist/quick-add.d.ts.map +0 -1
  39. package/dist/registries.d.ts.map +0 -1
  40. package/src/FinancialContext.tsx +0 -59
  41. package/src/FinancialPage.tsx +0 -413
  42. package/src/README.md +0 -155
  43. package/src/components/FinancialGeneralSettings.tsx +0 -39
  44. package/src/components/FinancialOnboarding.tsx +0 -87
  45. package/src/components/FinancialSettings.tsx +0 -31
  46. package/src/components/PaymentModal.tsx +0 -369
  47. package/src/components/PersonStatementWidget.tsx +0 -133
  48. package/src/components/TransferModal.tsx +0 -150
  49. package/src/contract.test.ts +0 -58
  50. package/src/data/capability.test.ts +0 -135
  51. package/src/data/index.ts +0 -3
  52. package/src/data/mock.ts +0 -842
  53. package/src/data/supabase.ts +0 -968
  54. package/src/data/tables.ts +0 -13
  55. package/src/data/types.ts +0 -86
  56. package/src/index.ts +0 -487
  57. package/src/lib/tenant.ts +0 -3
  58. package/src/locales/en.ts +0 -493
  59. package/src/locales/index.ts +0 -7
  60. package/src/locales/pt-BR.ts +0 -493
  61. package/src/migrations/000_plg_rename.sql +0 -30
  62. package/src/migrations/001_financial_base.sql +0 -109
  63. package/src/migrations/002_chart_of_accounts.sql +0 -28
  64. package/src/migrations/003_card_brands.sql +0 -12
  65. package/src/migrations/004_order_to_cash.sql +0 -178
  66. package/src/migrations/005_seed_defaults.sql +0 -38
  67. package/src/migrations/006_rls_policies.sql +0 -37
  68. package/src/migrations/006b_extract_fee_amount.sql +0 -20
  69. package/src/migrations/007_reconciliation.sql +0 -32
  70. package/src/migrations/007b_movement_payment_method_type.sql +0 -6
  71. package/src/migrations/008_split_payment_movements.sql +0 -29
  72. package/src/migrations/009_agent_rpcs.sql +0 -165
  73. package/src/migrations/010_invoice_internal.sql +0 -133
  74. package/src/migrations/011_nfe.sql +0 -58
  75. package/src/migrations/index.ts +0 -923
  76. package/src/quick-add.ts +0 -58
  77. package/src/registries.ts +0 -308
  78. package/src/store.ts +0 -329
  79. package/src/types.ts +0 -592
  80. package/src/views/CardsView.tsx +0 -213
  81. package/src/views/CashRegistersView.tsx +0 -597
  82. package/src/views/CommissionsView.tsx +0 -18
  83. package/src/views/InvoiceDetailView.tsx +0 -616
  84. package/src/views/InvoiceFormView.tsx +0 -945
  85. package/src/views/InvoiceListView.tsx +0 -323
  86. package/src/views/NotasFiscaisView.tsx +0 -209
  87. package/src/views/PayablesView.tsx +0 -48
  88. package/src/views/QuickTransactionForm.tsx +0 -548
  89. package/src/views/ReceivablesView.tsx +0 -48
  90. package/src/views/ReconciliationView.tsx +0 -211
  91. package/src/views/StatementsView.tsx +0 -335
  92. package/src/views/SummaryView.tsx +0 -203
  93. package/src/views/dashboardWidgets.tsx +0 -437
  94. package/src/views/navReferrer.ts +0 -24
  95. /package/dist/{registries.d.ts → data/registries.d.ts} +0 -0
@@ -1,87 +0,0 @@
1
- import React, { useState } from 'react'
2
- import { DollarSign, Landmark, CreditCard, BookOpen, ChevronRight, Check } from 'lucide-react'
3
- import { useTranslation } from '@fayz-ai/core'
4
- import { Button } from '@fayz-ai/ui'
5
-
6
- const STEPS = [
7
- { id: 'welcome', icon: DollarSign, titleKey: 'financial.onboarding.welcome', descKey: 'financial.onboarding.description' },
8
- { id: 'accounts', icon: Landmark, titleKey: 'financial.onboarding.accounts.title', descKey: 'financial.onboarding.accounts.description' },
9
- { id: 'payments', icon: CreditCard, titleKey: 'financial.onboarding.payments.title', descKey: 'financial.onboarding.payments.description' },
10
- { id: 'chart', icon: BookOpen, titleKey: 'financial.onboarding.chart.title', descKey: 'financial.onboarding.chart.description' },
11
- ]
12
-
13
- export function FinancialOnboarding({ onComplete }: { onComplete: () => void }) {
14
- const t = useTranslation()
15
- const [step, setStep] = useState(0)
16
- const current = STEPS[step]
17
- const isLast = step === STEPS.length - 1
18
- const Icon = current.icon
19
-
20
- return (
21
- <div className="flex items-center justify-center py-12">
22
- <div className="w-full max-w-lg">
23
- {/* Progress */}
24
- <div className="flex items-center gap-1 mb-8">
25
- {STEPS.map((s, i) => (
26
- <div
27
- key={s.id}
28
- className={`h-1 flex-1 rounded-full transition-colors ${
29
- i <= step ? 'bg-primary' : 'bg-muted'
30
- }`}
31
- />
32
- ))}
33
- </div>
34
-
35
- {/* Card */}
36
- <div className="rounded-card border bg-card p-8 text-center space-y-6 shadow-sm">
37
- <div className="flex justify-center">
38
- <div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-primary/10">
39
- <Icon className="h-8 w-8 text-primary" />
40
- </div>
41
- </div>
42
-
43
- <div>
44
- <h2 className="text-xl font-bold">{t(current.titleKey)}</h2>
45
- <p className="text-sm text-muted-foreground mt-2 max-w-sm mx-auto">{t(current.descKey)}</p>
46
- </div>
47
-
48
- {/* Step-specific hint */}
49
- {step > 0 && step < STEPS.length - 1 && (
50
- <div className="rounded-lg bg-muted/30 px-4 py-3 text-xs text-muted-foreground">
51
- {t('financial.onboarding.configureHint')}
52
- </div>
53
- )}
54
-
55
- {/* Actions */}
56
- <div className="flex items-center justify-center gap-3 pt-2">
57
- {step > 0 && (
58
- <Button variant="outline" size="lg" onClick={() => setStep(step - 1)}>
59
- {t('financial.onboarding.back')}
60
- </Button>
61
- )}
62
- {isLast ? (
63
- <Button variant="default" size="lg" onClick={onComplete}>
64
- <Check className="h-4 w-4" /> {t('financial.onboarding.startUsing')}
65
- </Button>
66
- ) : (
67
- <Button variant="default" size="lg" onClick={() => setStep(step + 1)}>
68
- {step === 0 ? t('financial.onboarding.getStarted') : t('financial.onboarding.next')}
69
- <ChevronRight className="h-4 w-4" />
70
- </Button>
71
- )}
72
- </div>
73
-
74
- {/* Skip */}
75
- {!isLast && step === 0 && (
76
- <button
77
- onClick={onComplete}
78
- className="text-xs text-muted-foreground hover:text-foreground transition-colors"
79
- >
80
- {t('financial.onboarding.skip')}
81
- </button>
82
- )}
83
- </div>
84
- </div>
85
- </div>
86
- )
87
- }
@@ -1,31 +0,0 @@
1
- import React from 'react'
2
- import { ChevronLeft } from 'lucide-react'
3
- import { PluginRegistryManager } from '@fayz-ai/saas'
4
- import type { PluginRegistryDef } from '@fayz-ai/core'
5
- import { useTranslation } from '@fayz-ai/core'
6
-
7
- export function FinancialSettings({ registries, routeBase, onClose }: {
8
- registries: PluginRegistryDef[]
9
- routeBase: string
10
- onClose: () => void
11
- }) {
12
- const t = useTranslation()
13
- return (
14
- <div className="space-y-4">
15
- <div className="flex items-center gap-3">
16
- <button
17
- onClick={onClose}
18
- className="flex h-8 w-8 items-center justify-center rounded-lg border hover:bg-muted bg-card shadow-button active:shadow-button-inset transition-colors"
19
- >
20
- <ChevronLeft className="h-4 w-4" />
21
- </button>
22
- <div>
23
- <h2 className="text-lg font-bold">{t('financial.settingsPage.title')}</h2>
24
- <p className="text-xs text-muted-foreground">{t('financial.settingsPage.subtitle')}</p>
25
- </div>
26
- </div>
27
-
28
- <PluginRegistryManager registries={registries} routeBase={routeBase} />
29
- </div>
30
- )
31
- }
@@ -1,369 +0,0 @@
1
- import React, { useState, useEffect, useMemo } from 'react'
2
- import { createPortal } from 'react-dom'
3
- import { X, Check, Banknote, QrCode, CreditCard, Building2, ArrowRightLeft, FileCheck, Pencil } from 'lucide-react'
4
- import { useFinancialConfig, useFinancialStore, formatCurrency } from '../FinancialContext'
5
- import { useTranslation } from '@fayz-ai/core'
6
- import { CurrencyInput } from '@fayz-ai/ui'
7
- import { DatePicker } from '@fayz-ai/ui'
8
- import { Button } from '@fayz-ai/ui'
9
- import type { FinancialMovement, PaymentMethodType, BankAccountType } from '../types'
10
-
11
- // Which bank-account types make sense for each payment type — mirrors beautyplace's
12
- // payment_method_types.allowed_account_types. Used to filter + default the destination
13
- // account so every payment lands in a real account (and shows up in that account's extract).
14
- const ACCOUNT_TYPES_FOR_TX: Record<string, BankAccountType[]> = {
15
- cash: ['cash_register'],
16
- credit_card: ['credit_card', 'bank_account'],
17
- debit_card: ['bank_account', 'cash_register'],
18
- pix: ['bank_account', 'digital_wallet'],
19
- bank_transfer: ['bank_account'],
20
- check: ['bank_account'],
21
- voucher: ['bank_account', 'digital_wallet'],
22
- }
23
-
24
- const TYPE_ICONS: Record<string, React.ElementType> = {
25
- cash: Banknote,
26
- pix: QrCode,
27
- credit_card: CreditCard,
28
- debit_card: CreditCard,
29
- bank_transfer: ArrowRightLeft,
30
- check: FileCheck,
31
- }
32
-
33
- const TYPE_COLORS: Record<string, string> = {
34
- cash: 'border-success/30 bg-success/5 hover:bg-success/10 data-[active=true]:border-success data-[active=true]:bg-success/15',
35
- 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',
36
- credit_card: 'border-info/30 bg-info/5 hover:bg-info/10 data-[active=true]:border-info data-[active=true]:bg-info/15',
37
- debit_card: 'border-magic/30 bg-magic/5 hover:bg-magic/10 data-[active=true]:border-magic data-[active=true]:bg-magic/15',
38
- bank_transfer: 'border-warning/30 bg-warning/5 hover:bg-warning/10 data-[active=true]:border-warning data-[active=true]:bg-warning/15',
39
- check: 'border-border bg-muted/40 hover:bg-muted/60 data-[active=true]:border-foreground data-[active=true]:bg-muted',
40
- }
41
-
42
- const ICON_COLORS: Record<string, string> = {
43
- cash: 'text-success',
44
- pix: 'text-teal-500',
45
- credit_card: 'text-info',
46
- debit_card: 'text-magic',
47
- bank_transfer: 'text-warning',
48
- check: 'text-muted-foreground',
49
- }
50
-
51
- export function PaymentModal({ movement, onClose, onPaid }: {
52
- movement: FinancialMovement
53
- onClose: () => void
54
- onPaid: () => void
55
- }) {
56
- const t = useTranslation()
57
- const { currency } = useFinancialConfig()
58
- const payMovement = useFinancialStore((s) => s.payMovement)
59
- const bankAccounts = useFinancialStore((s) => s.bankAccounts)
60
- const paymentMethods = useFinancialStore((s) => s.paymentMethods)
61
- const paymentMethodTypes = useFinancialStore((s) => s.paymentMethodTypes)
62
- const fetchBankAccounts = useFinancialStore((s) => s.fetchBankAccounts)
63
- const fetchPaymentMethods = useFinancialStore((s) => s.fetchPaymentMethods)
64
-
65
- const remaining = movement.amount - movement.paidAmount
66
- const [amount, setAmount] = useState(remaining)
67
- const [paymentDate, setPaymentDate] = useState(new Date().toISOString().slice(0, 10))
68
- const [selectedTypeId, setSelectedTypeId] = useState('')
69
- const [paymentMethodId, setPaymentMethodId] = useState('')
70
- const [bankAccountId, setBankAccountId] = useState('')
71
- const [cardBrand, setCardBrand] = useState('')
72
- const [cardInstallments, setCardInstallments] = useState(1)
73
- const [notes, setNotes] = useState('')
74
- const [saving, setSaving] = useState(false)
75
- const [visible, setVisible] = useState(false)
76
- const [expandMethodPicker, setExpandMethodPicker] = useState(true)
77
-
78
- useEffect(() => {
79
- fetchBankAccounts()
80
- fetchPaymentMethods()
81
- requestAnimationFrame(() => setVisible(true))
82
- }, [])
83
-
84
- const selectedType = useMemo(() =>
85
- paymentMethodTypes.find((tp) => tp.id === selectedTypeId),
86
- [paymentMethodTypes, selectedTypeId]
87
- )
88
-
89
- const txType = selectedType?.transactionType ?? ''
90
-
91
- const filteredMethods = useMemo(() =>
92
- paymentMethods.filter((m) => m.paymentMethodTypeId === selectedTypeId),
93
- [paymentMethods, selectedTypeId]
94
- )
95
-
96
- const needsCard = txType === 'credit_card' || txType === 'debit_card'
97
- const needsCardInstallments = txType === 'credit_card'
98
-
99
- // Accounts eligible for the selected payment type. Prefer the type's configured
100
- // allowedAccountTypes, else the tx-type mapping, else all active accounts.
101
- const eligibleAccounts = useMemo(() => {
102
- const allowed = (selectedType?.allowedAccountTypes && selectedType.allowedAccountTypes.length)
103
- ? selectedType.allowedAccountTypes
104
- : ACCOUNT_TYPES_FOR_TX[txType]
105
- if (!allowed || !allowed.length) return bankAccounts
106
- const filtered = bankAccounts.filter((a) => allowed.includes(a.accountType))
107
- return filtered.length ? filtered : bankAccounts
108
- }, [bankAccounts, selectedType, txType])
109
-
110
- // Auto-pick a sensible destination account so the payment is always linked to one.
111
- useEffect(() => {
112
- if (selectedTypeId && !bankAccountId && eligibleAccounts.length) {
113
- setBankAccountId(eligibleAccounts[0].id)
114
- }
115
- }, [selectedTypeId, eligibleAccounts])
116
-
117
- function handleSelectType(typeId: string) {
118
- setSelectedTypeId(typeId)
119
- setExpandMethodPicker(false)
120
- setPaymentMethodId('')
121
- setBankAccountId('')
122
- setCardBrand('')
123
- setCardInstallments(1)
124
- }
125
-
126
- function handleClose() {
127
- setVisible(false)
128
- setTimeout(onClose, 200)
129
- }
130
-
131
- // Enter anywhere in the modal submits (unless typing in a textarea / Shift+Enter).
132
- function handleKeyDown(e: React.KeyboardEvent) {
133
- if (e.key !== 'Enter' || e.shiftKey) return
134
- if ((e.target as HTMLElement).tagName === 'TEXTAREA') return
135
- e.preventDefault()
136
- if (amount > 0 && selectedTypeId && bankAccountId && !saving) void handlePay()
137
- }
138
-
139
- async function handlePay() {
140
- if (amount <= 0 || !selectedTypeId || !bankAccountId) return
141
- setSaving(true)
142
- try {
143
- await payMovement({
144
- movementId: movement.id,
145
- amount,
146
- paymentDate,
147
- paymentMethodId: paymentMethodId || undefined,
148
- paymentMethodTypeId: selectedTypeId || undefined,
149
- bankAccountId: bankAccountId || undefined,
150
- cardBrand: needsCard ? cardBrand || undefined : undefined,
151
- cardInstallments: needsCardInstallments ? cardInstallments : undefined,
152
- notes: notes.trim() || undefined,
153
- })
154
- setVisible(false)
155
- setTimeout(onPaid, 200)
156
- } catch {
157
- // toast handled by store
158
- } finally {
159
- setSaving(false)
160
- }
161
- }
162
-
163
- const SelectedIcon = selectedType ? (TYPE_ICONS[txType] ?? Banknote) : null
164
-
165
- return createPortal(
166
- <div
167
- className="fixed inset-0 z-50 flex items-center justify-center transition-colors duration-200"
168
- style={{ backgroundColor: visible ? 'rgba(0,0,0,0.5)' : 'rgba(0,0,0,0)' }}
169
- onClick={handleClose}
170
- >
171
- <div
172
- className="w-full max-w-md rounded-modal border bg-card shadow-lg mx-4 transition-all duration-200 max-h-[90vh] overflow-y-auto"
173
- style={{
174
- opacity: visible ? 1 : 0,
175
- transform: visible ? 'scale(1) translateY(0)' : 'scale(0.95) translateY(8px)',
176
- }}
177
- onClick={(e) => e.stopPropagation()}
178
- onKeyDown={handleKeyDown}
179
- >
180
- {/* Header */}
181
- <div className="flex items-center justify-between px-5 py-4 border-b sticky top-0 bg-card z-10">
182
- <div>
183
- <h3 className="text-base font-semibold">{t('financial.payment.title')}</h3>
184
- <p className="text-xs text-muted-foreground">
185
- {t('financial.payment.installment', { number: String(movement.installmentNumber ?? 1) })} &middot; {t('financial.payment.remaining', { amount: formatCurrency(remaining, currency) })}
186
- </p>
187
- </div>
188
- <button onClick={handleClose} className="p-1 text-muted-foreground hover:text-foreground transition-colors">
189
- <X className="h-4 w-4" />
190
- </button>
191
- </div>
192
-
193
- <div className="px-5 py-4 space-y-4">
194
- {/* Payment method type selector */}
195
- <div>
196
- <label className="text-xs font-medium text-muted-foreground">{t('financial.payment.method')}</label>
197
-
198
- {/* Collapsed: show selected as inline chip */}
199
- <div
200
- className="grid transition-all duration-200 ease-out"
201
- style={{ gridTemplateRows: selectedTypeId && !expandMethodPicker ? '1fr' : '0fr' }}
202
- >
203
- <div className="overflow-hidden">
204
- {selectedType && (
205
- <button
206
- onClick={() => setExpandMethodPicker(true)}
207
- className={`flex items-center gap-2 mt-1.5 w-full rounded-lg border px-3 py-2.5 text-left transition-colors hover:bg-muted/30 ${TYPE_COLORS[txType] ?? ''}`}
208
- data-active="true"
209
- >
210
- {SelectedIcon && <SelectedIcon className={`h-4 w-4 ${ICON_COLORS[txType] ?? ''}`} />}
211
- <span className="text-sm font-medium flex-1">{selectedType?.name}</span>
212
- <Pencil className="h-3 w-3 text-muted-foreground" />
213
- </button>
214
- )}
215
- </div>
216
- </div>
217
-
218
- {/* Expanded: full grid */}
219
- <div
220
- className="grid transition-all duration-200 ease-out"
221
- style={{ gridTemplateRows: expandMethodPicker ? '1fr' : '0fr' }}
222
- >
223
- <div className="overflow-hidden">
224
- {paymentMethodTypes.length === 0 ? (
225
- <div className="rounded-lg border border-dashed border-input bg-muted/30 px-3 py-3 text-center mt-1.5">
226
- <p className="text-xs text-muted-foreground">{t('financial.payment.noMethods')}</p>
227
- <a href="#/financial/settings" className="mt-1.5 inline-block text-xs font-medium text-primary hover:underline">
228
- {t('financial.payment.configureMethods')} →
229
- </a>
230
- </div>
231
- ) : (
232
- <div className="grid grid-cols-3 gap-2 mt-1.5">
233
- {paymentMethodTypes.map((type) => {
234
- const tt = type.transactionType ?? ''
235
- const Icon = TYPE_ICONS[tt] ?? Banknote
236
- const isActive = selectedTypeId === type.id
237
- return (
238
- <button
239
- key={type.id}
240
- data-active={isActive}
241
- onClick={() => handleSelectType(type.id)}
242
- className={`flex flex-col items-center gap-1.5 rounded-lg border px-2 py-3 text-center transition-all ${TYPE_COLORS[tt] ?? TYPE_COLORS.cash}`}
243
- >
244
- <Icon className={`h-5 w-5 ${isActive ? ICON_COLORS[tt] ?? '' : 'text-muted-foreground'}`} />
245
- <span className={`text-[11px] font-medium leading-tight ${isActive ? 'text-foreground' : 'text-muted-foreground'}`}>{type.name}</span>
246
- </button>
247
- )
248
- })}
249
- </div>
250
- )}
251
- </div>
252
- </div>
253
- </div>
254
-
255
- {/* Form fields — only after method selected */}
256
- {selectedTypeId && (
257
- <div key={selectedTypeId} className="space-y-4">
258
- {/* Amount + Date side by side */}
259
- <div className="grid grid-cols-2 gap-3 stagger-field" style={{ animationDelay: '0ms' }}>
260
- <CurrencyInput
261
- label={t('financial.payment.amount')}
262
- value={amount}
263
- onChange={setAmount}
264
- symbol={currency.symbol}
265
- locale={currency.locale}
266
- currencyCode={currency.code}
267
- />
268
- <div>
269
- <label className="text-xs font-medium text-muted-foreground">{t('financial.payment.date')}</label>
270
- <DatePicker value={paymentDate} onChange={setPaymentDate} className="mt-1" />
271
- </div>
272
- </div>
273
-
274
- {/* Specific payment method */}
275
- {filteredMethods.length > 0 && (
276
- <div className="stagger-field" style={{ animationDelay: '50ms' }}>
277
- <label className="text-xs font-medium text-muted-foreground">
278
- {needsCard ? t('financial.payment.card') : t('financial.payment.account')}
279
- </label>
280
- <select value={paymentMethodId} onChange={(e) => setPaymentMethodId(e.target.value)} className="w-full mt-1 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-3 py-2 text-sm">
281
- <option value="">{t('financial.payment.select')}</option>
282
- {filteredMethods.map((m) => <option key={m.id} value={m.id}>{m.name}</option>)}
283
- </select>
284
- </div>
285
- )}
286
-
287
- {/* Card fields side by side */}
288
- {needsCard && (
289
- <div className={`grid gap-3 stagger-field ${needsCardInstallments ? 'grid-cols-2' : 'grid-cols-1'}`} style={{ animationDelay: '100ms' }}>
290
- <div>
291
- <label className="text-xs font-medium text-muted-foreground">{t('financial.payment.cardBrand')}</label>
292
- <select value={cardBrand} onChange={(e) => setCardBrand(e.target.value)} className="w-full mt-1 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-3 py-2 text-sm">
293
- <option value="">{t('financial.payment.selectBrand')}</option>
294
- {['Visa', 'Mastercard', 'American Express', 'Elo', 'Hipercard', 'Diners Club'].map((b) => (
295
- <option key={b} value={b}>{b}</option>
296
- ))}
297
- </select>
298
- </div>
299
- {needsCardInstallments && (
300
- <div>
301
- <label className="text-xs font-medium text-muted-foreground">{t('financial.payment.cardInstallments')}</label>
302
- <select value={cardInstallments} onChange={(e) => setCardInstallments(Number(e.target.value))} className="w-full mt-1 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-3 py-2 text-sm">
303
- {Array.from({ length: 12 }, (_, i) => i + 1).map((n) => (
304
- <option key={n} value={n}>{n}x {n > 1 ? formatCurrency(amount / n, currency) : ''}</option>
305
- ))}
306
- </select>
307
- </div>
308
- )}
309
- </div>
310
- )}
311
-
312
- {/* Destination account — always shown so every payment links to an account */}
313
- <div className="stagger-field" style={{ animationDelay: '100ms' }}>
314
- <label className="text-xs font-medium text-muted-foreground">{t('financial.payment.bankAccount')}</label>
315
- {eligibleAccounts.length === 0 ? (
316
- <div className="mt-1 rounded-lg border border-dashed border-input bg-muted/30 px-3 py-2.5 text-center">
317
- <p className="text-xs text-muted-foreground">{t('financial.payment.noAccounts')}</p>
318
- <a href="#/settings/financial/bank-accounts" className="mt-1 inline-block text-xs font-medium text-primary hover:underline">
319
- {t('financial.payment.configureAccounts')} →
320
- </a>
321
- </div>
322
- ) : (
323
- <select value={bankAccountId} onChange={(e) => setBankAccountId(e.target.value)} className="w-full mt-1 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-3 py-2 text-sm">
324
- <option value="">{t('financial.payment.selectAccount')}</option>
325
- {eligibleAccounts.map((a) => <option key={a.id} value={a.id}>{a.name}</option>)}
326
- </select>
327
- )}
328
- </div>
329
-
330
- {/* Notes */}
331
- <div className="stagger-field" style={{ animationDelay: '150ms' }}>
332
- <label className="text-xs font-medium text-muted-foreground">{t('financial.payment.notes')}</label>
333
- <input type="text" value={notes} onChange={(e) => setNotes(e.target.value)} placeholder={t('financial.payment.optionalNotes')} className="w-full mt-1 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring" />
334
- </div>
335
- </div>
336
- )}
337
- </div>
338
-
339
- {/* Keyframe for staggered field entrance */}
340
- <style>{`
341
- .stagger-field {
342
- animation: field-slide-in 250ms ease-out both;
343
- }
344
- @keyframes field-slide-in {
345
- from { opacity: 0; transform: translateY(8px); }
346
- to { opacity: 1; transform: translateY(0); }
347
- }
348
- `}</style>
349
-
350
- {/* Actions */}
351
- <div className="flex justify-end gap-2 px-5 py-4 border-t sticky bottom-0 bg-card">
352
- <Button variant="outline" size="sm" onClick={handleClose}>
353
- {t('financial.payment.cancel')}
354
- </Button>
355
- <Button
356
- variant="default"
357
- size="sm"
358
- onClick={handlePay}
359
- disabled={amount <= 0 || !selectedTypeId || !bankAccountId || saving}
360
- >
361
- <Check className="h-3.5 w-3.5" />
362
- {saving ? t('financial.payment.processing') : t('financial.payment.payAmount', { amount: formatCurrency(amount, currency) })}
363
- </Button>
364
- </div>
365
- </div>
366
- </div>,
367
- document.body
368
- )
369
- }
@@ -1,133 +0,0 @@
1
- import React, { useEffect, useMemo, useState } from 'react'
2
- import { Wallet } from 'lucide-react'
3
- import type { ColumnDef } from '@tanstack/react-table'
4
- import { DataTable } from '@fayz-ai/ui'
5
- import { useTranslation } from '@fayz-ai/core'
6
- import { formatCurrency, type ResolvedFinancialConfig } from '../FinancialContext'
7
- import type { FinancialDataProvider } from '../data/types'
8
- import type { Invoice } from '../types'
9
-
10
- const STATUS_STYLE: Record<string, string> = {
11
- paid: 'bg-success/15 text-success',
12
- partial: 'bg-warning/15 text-warning',
13
- overdue: 'bg-destructive/15 text-destructive',
14
- open: 'bg-muted text-muted-foreground',
15
- cancelled: 'bg-muted text-muted-foreground line-through',
16
- }
17
-
18
- /**
19
- * Per-person financial statement (account_central-style extract): every invoice where
20
- * this person is the contact — a client paying receivables, or a professional receiving
21
- * commissions/payables. Rendered as a tab on the person detail page via the
22
- * `person.detail.financial` widget zone.
23
- */
24
- export function PersonStatementWidget({ person, provider, config }: {
25
- person: { id?: string; name?: string }
26
- provider: FinancialDataProvider
27
- config: ResolvedFinancialConfig
28
- }) {
29
- const t = useTranslation()
30
- const currency = config.currency
31
- const [invoices, setInvoices] = useState<Invoice[]>([])
32
- const [loading, setLoading] = useState(true)
33
-
34
- useEffect(() => {
35
- let alive = true
36
- if (!person?.id) { setLoading(false); return }
37
- setLoading(true)
38
- provider.getInvoices({ contactId: person.id, pageSize: 200 })
39
- .then((r) => { if (alive) { setInvoices(r.data); setLoading(false) } })
40
- .catch(() => { if (alive) setLoading(false) })
41
- return () => { alive = false }
42
- }, [person?.id, provider])
43
-
44
- const fmt = (n: number) => formatCurrency(n, currency)
45
-
46
- const totals = useMemo(() => {
47
- const active = invoices.filter((i) => i.status !== 'cancelled')
48
- const billed = active.reduce((s, i) => s + (i.totalAmount ?? 0), 0)
49
- const paid = active.reduce((s, i) => s + (i.paidAmount ?? 0), 0)
50
- return { billed, paid, balance: billed - paid }
51
- }, [invoices])
52
-
53
- const openInvoice = (inv: Invoice) => {
54
- const base = inv.direction === 'credit' ? 'receivables' : 'payables'
55
- window.location.hash = `/financial/${base}/detail/${inv.id}`
56
- }
57
-
58
- const columns: ColumnDef<Invoice, any>[] = useMemo(() => [
59
- {
60
- id: 'date', header: t('financial.invoice.columnDate'),
61
- cell: ({ row }) => <span className="text-muted-foreground text-xs">{row.original.invoiceDate}</span>,
62
- },
63
- {
64
- id: 'description', header: t('financial.invoice.columnDescription'),
65
- cell: ({ row }) => {
66
- const inv = row.original
67
- return (
68
- <button onClick={() => openInvoice(inv)} className="text-left">
69
- <span className="text-xs font-medium text-primary hover:underline">
70
- {inv.itemsSummary || inv.contactName || t('financial.invoice.invoice')}
71
- </span>
72
- {inv.fiscalNumber && <span className="ml-1.5 text-[10px] text-muted-foreground font-mono">{inv.fiscalNumber}</span>}
73
- </button>
74
- )
75
- },
76
- },
77
- {
78
- id: 'amount', header: t('financial.invoice.columnAmount'),
79
- cell: ({ row }) => (
80
- <span className={`text-right block text-xs ${row.original.direction === 'credit' ? 'text-success' : 'text-destructive'}`}>
81
- {fmt(row.original.totalAmount)}
82
- </span>
83
- ),
84
- },
85
- {
86
- id: 'paid', header: t('financial.invoice.columnPaid'),
87
- cell: ({ row }) => <span className="text-right block text-xs text-muted-foreground">{fmt(row.original.paidAmount)}</span>,
88
- },
89
- {
90
- id: 'balance', header: t('financial.statements.columnBalance'),
91
- cell: ({ row }) => <span className="text-right block text-xs font-medium">{fmt((row.original.totalAmount ?? 0) - (row.original.paidAmount ?? 0))}</span>,
92
- },
93
- {
94
- id: 'status', header: t('financial.invoice.columnStatus'),
95
- cell: ({ row }) => (
96
- <span className={`inline-flex rounded-full px-1.5 py-0.5 text-[9px] font-medium capitalize ${STATUS_STYLE[row.original.status] ?? STATUS_STYLE.open}`}>
97
- {row.original.status}
98
- </span>
99
- ),
100
- },
101
- ], [currency, t])
102
-
103
- return (
104
- <div className="space-y-3">
105
- {/* Totals */}
106
- <div className="grid grid-cols-3 gap-2">
107
- <Stat label={t('financial.invoice.summaryTotal')} value={fmt(totals.billed)} />
108
- <Stat label={t('financial.invoice.summaryPaid')} value={fmt(totals.paid)} positive />
109
- <Stat label={t('financial.statements.columnBalance')} value={fmt(totals.balance)} accent />
110
- </div>
111
-
112
- {loading ? (
113
- <DataTable columns={columns} data={[]} loading skeletonRows={5} compact />
114
- ) : invoices.length === 0 ? (
115
- <div className="flex flex-col items-center justify-center py-10 text-center rounded-lg border-2 border-dashed border-muted">
116
- <Wallet className="h-7 w-7 text-muted-foreground mb-2" />
117
- <p className="text-sm text-muted-foreground">{t('financial.person.empty')}</p>
118
- </div>
119
- ) : (
120
- <DataTable columns={columns} data={invoices} compact />
121
- )}
122
- </div>
123
- )
124
- }
125
-
126
- function Stat({ label, value, accent, positive }: { label: string; value: string; accent?: boolean; positive?: boolean }) {
127
- return (
128
- <div className={`rounded-lg border px-3 py-2 ${accent ? 'bg-muted/40' : 'bg-card'}`}>
129
- <p className="text-[10px] font-medium text-muted-foreground uppercase truncate">{label}</p>
130
- <p className={`text-sm font-semibold mt-0.5 ${positive ? 'text-success' : 'text-foreground'}`}>{value}</p>
131
- </div>
132
- )
133
- }