@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.
- package/README.md +3 -3
- package/dist/components/PersonStatementWidget.d.ts +1 -1
- package/dist/components/PersonStatementWidget.d.ts.map +1 -1
- package/dist/{FinancialContext.d.ts → context.d.ts} +4 -2
- package/dist/context.d.ts.map +1 -0
- package/dist/data/registries.d.ts.map +1 -0
- package/dist/index.d.ts +13 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1426 -1426
- package/dist/index.js.map +1 -1
- package/dist/integrations/stripe/connectorDef.d.ts +14 -0
- package/dist/integrations/stripe/connectorDef.d.ts.map +1 -0
- package/dist/integrations/stripe/data.d.ts +43 -0
- package/dist/integrations/stripe/data.d.ts.map +1 -0
- package/dist/lib/message-events.d.ts +12 -0
- package/dist/lib/message-events.d.ts.map +1 -0
- package/dist/{quick-add.d.ts → lib/quick-add.d.ts} +1 -1
- package/dist/lib/quick-add.d.ts.map +1 -0
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/pt-BR.d.ts.map +1 -1
- package/dist/{FinancialPage.d.ts → views/FinancialPage.d.ts} +3 -3
- package/dist/views/FinancialPage.d.ts.map +1 -0
- package/dist/views/InvoiceDetailView.d.ts.map +1 -1
- package/dist/views/InvoiceFormView.d.ts.map +1 -1
- package/dist/views/InvoiceListView.d.ts.map +1 -1
- package/dist/views/PayablesView.d.ts +1 -1
- package/dist/views/PayablesView.d.ts.map +1 -1
- package/dist/views/ReceivablesView.d.ts +1 -1
- package/dist/views/ReceivablesView.d.ts.map +1 -1
- package/dist/views/StatementsView.d.ts.map +1 -1
- package/dist/views/dashboardWidgets.d.ts +1 -1
- package/dist/views/dashboardWidgets.d.ts.map +1 -1
- package/package.json +12 -14
- package/dist/FinancialContext.d.ts.map +0 -1
- package/dist/FinancialPage.d.ts.map +0 -1
- package/dist/contract.test.d.ts +0 -2
- package/dist/contract.test.d.ts.map +0 -1
- package/dist/quick-add.d.ts.map +0 -1
- package/dist/registries.d.ts.map +0 -1
- package/src/FinancialContext.tsx +0 -59
- package/src/FinancialPage.tsx +0 -413
- package/src/README.md +0 -155
- package/src/components/FinancialGeneralSettings.tsx +0 -39
- package/src/components/FinancialOnboarding.tsx +0 -87
- package/src/components/FinancialSettings.tsx +0 -31
- package/src/components/PaymentModal.tsx +0 -369
- package/src/components/PersonStatementWidget.tsx +0 -133
- package/src/components/TransferModal.tsx +0 -150
- package/src/contract.test.ts +0 -58
- package/src/data/capability.test.ts +0 -135
- package/src/data/index.ts +0 -3
- package/src/data/mock.ts +0 -842
- package/src/data/supabase.ts +0 -968
- package/src/data/tables.ts +0 -13
- package/src/data/types.ts +0 -86
- package/src/index.ts +0 -487
- package/src/lib/tenant.ts +0 -3
- package/src/locales/en.ts +0 -493
- package/src/locales/index.ts +0 -7
- package/src/locales/pt-BR.ts +0 -493
- package/src/migrations/000_plg_rename.sql +0 -30
- package/src/migrations/001_financial_base.sql +0 -109
- package/src/migrations/002_chart_of_accounts.sql +0 -28
- package/src/migrations/003_card_brands.sql +0 -12
- package/src/migrations/004_order_to_cash.sql +0 -178
- package/src/migrations/005_seed_defaults.sql +0 -38
- package/src/migrations/006_rls_policies.sql +0 -37
- package/src/migrations/006b_extract_fee_amount.sql +0 -20
- package/src/migrations/007_reconciliation.sql +0 -32
- package/src/migrations/007b_movement_payment_method_type.sql +0 -6
- package/src/migrations/008_split_payment_movements.sql +0 -29
- package/src/migrations/009_agent_rpcs.sql +0 -165
- package/src/migrations/010_invoice_internal.sql +0 -133
- package/src/migrations/011_nfe.sql +0 -58
- package/src/migrations/index.ts +0 -923
- package/src/quick-add.ts +0 -58
- package/src/registries.ts +0 -308
- package/src/store.ts +0 -329
- package/src/types.ts +0 -592
- package/src/views/CardsView.tsx +0 -213
- package/src/views/CashRegistersView.tsx +0 -597
- package/src/views/CommissionsView.tsx +0 -18
- package/src/views/InvoiceDetailView.tsx +0 -616
- package/src/views/InvoiceFormView.tsx +0 -945
- package/src/views/InvoiceListView.tsx +0 -323
- package/src/views/NotasFiscaisView.tsx +0 -209
- package/src/views/PayablesView.tsx +0 -48
- package/src/views/QuickTransactionForm.tsx +0 -548
- package/src/views/ReceivablesView.tsx +0 -48
- package/src/views/ReconciliationView.tsx +0 -211
- package/src/views/StatementsView.tsx +0 -335
- package/src/views/SummaryView.tsx +0 -203
- package/src/views/dashboardWidgets.tsx +0 -437
- package/src/views/navReferrer.ts +0 -24
- /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) })} · {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
|
-
}
|