@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,211 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useState, useCallback } from 'react'
|
|
2
|
-
import { Landmark, RefreshCw, Check, Link2, Sparkles, ArrowDownCircle, ArrowUpCircle } from 'lucide-react'
|
|
3
|
-
import { Button, toast } from '@fayz-ai/ui'
|
|
4
|
-
import { useFinancialConfig, useFinancialProvider, formatCurrency } from '../FinancialContext'
|
|
5
|
-
import { useTranslation } from '@fayz-ai/core'
|
|
6
|
-
import type { FinancialMovement, ReconciliationCandidate } from '../types'
|
|
7
|
-
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
// Reconciliation (conciliação) — match imported bank lines to internal movements
|
|
10
|
-
//
|
|
11
|
-
// Imported bank-statement lines live in financial_movements tagged with an
|
|
12
|
-
// externalSource. This screen lets the operator link each pending line to the
|
|
13
|
-
// internal movement it settles (suggested by amount + date proximity), or
|
|
14
|
-
// accept it standalone. Reusable across apps — fed by any bank connector that
|
|
15
|
-
// imports through the financial provider's reconciliation methods.
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
|
|
18
|
-
export function ReconciliationView() {
|
|
19
|
-
const t = useTranslation()
|
|
20
|
-
const { currency } = useFinancialConfig()
|
|
21
|
-
const provider = useFinancialProvider()
|
|
22
|
-
const fmt = (n: number) => formatCurrency(n, currency)
|
|
23
|
-
|
|
24
|
-
const [lines, setLines] = useState<FinancialMovement[]>([])
|
|
25
|
-
const [loading, setLoading] = useState(true)
|
|
26
|
-
const [selectedId, setSelectedId] = useState<string | null>(null)
|
|
27
|
-
const [candidates, setCandidates] = useState<ReconciliationCandidate[]>([])
|
|
28
|
-
const [candidatesLoading, setCandidatesLoading] = useState(false)
|
|
29
|
-
const [busy, setBusy] = useState(false)
|
|
30
|
-
|
|
31
|
-
const supported = typeof provider.getUnreconciled === 'function'
|
|
32
|
-
|
|
33
|
-
const load = useCallback(async () => {
|
|
34
|
-
if (!provider.getUnreconciled) return
|
|
35
|
-
setLoading(true)
|
|
36
|
-
try {
|
|
37
|
-
const data = await provider.getUnreconciled()
|
|
38
|
-
setLines(data)
|
|
39
|
-
// Keep the selection valid; otherwise drop the right panel.
|
|
40
|
-
setSelectedId((prev) => (prev && data.some((l) => l.id === prev) ? prev : null))
|
|
41
|
-
} finally {
|
|
42
|
-
setLoading(false)
|
|
43
|
-
}
|
|
44
|
-
}, [provider])
|
|
45
|
-
|
|
46
|
-
useEffect(() => { void load() }, [load])
|
|
47
|
-
|
|
48
|
-
// Load suggestions whenever the selected line changes.
|
|
49
|
-
useEffect(() => {
|
|
50
|
-
if (!selectedId || !provider.suggestReconciliation) { setCandidates([]); return }
|
|
51
|
-
let cancelled = false
|
|
52
|
-
setCandidatesLoading(true)
|
|
53
|
-
provider.suggestReconciliation(selectedId)
|
|
54
|
-
.then((c) => { if (!cancelled) setCandidates(c) })
|
|
55
|
-
.finally(() => { if (!cancelled) setCandidatesLoading(false) })
|
|
56
|
-
return () => { cancelled = true }
|
|
57
|
-
}, [selectedId, provider])
|
|
58
|
-
|
|
59
|
-
async function match(bankMovementId: string, matchedMovementId?: string) {
|
|
60
|
-
if (!provider.reconcileMovement) return
|
|
61
|
-
setBusy(true)
|
|
62
|
-
try {
|
|
63
|
-
await provider.reconcileMovement({ bankMovementId, matchedMovementId })
|
|
64
|
-
toast.success(t('financial.reconciliation.match'))
|
|
65
|
-
await load()
|
|
66
|
-
} catch {
|
|
67
|
-
toast.error(t('common.error'))
|
|
68
|
-
} finally {
|
|
69
|
-
setBusy(false)
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (!supported) {
|
|
74
|
-
return (
|
|
75
|
-
<div className="py-16 text-center">
|
|
76
|
-
<Landmark className="h-8 w-8 text-muted-foreground/30 mx-auto mb-2" />
|
|
77
|
-
<p className="text-sm text-muted-foreground">{t('financial.reconciliation.empty')}</p>
|
|
78
|
-
</div>
|
|
79
|
-
)
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const selected = lines.find((l) => l.id === selectedId) ?? null
|
|
83
|
-
|
|
84
|
-
return (
|
|
85
|
-
<div className="space-y-4">
|
|
86
|
-
{/* Header */}
|
|
87
|
-
<div className="flex items-start justify-between gap-3">
|
|
88
|
-
<div>
|
|
89
|
-
<h2 className="text-lg font-semibold tracking-tight">{t('financial.reconciliation.title')}</h2>
|
|
90
|
-
<p className="text-sm text-muted-foreground">{t('financial.reconciliation.subtitle')}</p>
|
|
91
|
-
</div>
|
|
92
|
-
<div className="flex items-center gap-2">
|
|
93
|
-
<span className="text-xs text-muted-foreground">
|
|
94
|
-
{lines.length > 0
|
|
95
|
-
? t('financial.reconciliation.pending', { count: lines.length })
|
|
96
|
-
: t('financial.reconciliation.allDone')}
|
|
97
|
-
</span>
|
|
98
|
-
<Button variant="outline" size="sm" onClick={() => void load()} disabled={loading || busy}>
|
|
99
|
-
<RefreshCw className={`h-3.5 w-3.5 ${loading ? 'animate-spin' : ''}`} />
|
|
100
|
-
{t('financial.reconciliation.refresh')}
|
|
101
|
-
</Button>
|
|
102
|
-
</div>
|
|
103
|
-
</div>
|
|
104
|
-
|
|
105
|
-
{lines.length === 0 && !loading ? (
|
|
106
|
-
<div className="rounded-lg border bg-card shadow-sm py-16 text-center">
|
|
107
|
-
<Check className="h-8 w-8 text-success/40 mx-auto mb-2" />
|
|
108
|
-
<p className="text-sm font-medium">{t('financial.reconciliation.empty')}</p>
|
|
109
|
-
<p className="text-xs text-muted-foreground mt-1">{t('financial.reconciliation.emptyHint')}</p>
|
|
110
|
-
</div>
|
|
111
|
-
) : (
|
|
112
|
-
<div className="grid gap-4 lg:grid-cols-2">
|
|
113
|
-
{/* Left — pending bank lines */}
|
|
114
|
-
<div className="rounded-lg border bg-card shadow-sm overflow-hidden">
|
|
115
|
-
<div className="flex items-center gap-2 px-4 py-3 border-b">
|
|
116
|
-
<Landmark className="h-4 w-4 text-muted-foreground" />
|
|
117
|
-
<h3 className="text-sm font-semibold">{t('financial.reconciliation.bankLines')}</h3>
|
|
118
|
-
</div>
|
|
119
|
-
<div className="divide-y max-h-[60vh] overflow-auto">
|
|
120
|
-
{lines.map((line) => {
|
|
121
|
-
const isCredit = line.direction === 'credit'
|
|
122
|
-
return (
|
|
123
|
-
<button
|
|
124
|
-
key={line.id}
|
|
125
|
-
onClick={() => setSelectedId(line.id)}
|
|
126
|
-
className={`w-full flex items-center gap-3 px-4 py-3 text-left hover:bg-muted/40 transition-colors ${
|
|
127
|
-
selectedId === line.id ? 'bg-muted/50' : ''
|
|
128
|
-
}`}
|
|
129
|
-
>
|
|
130
|
-
{isCredit
|
|
131
|
-
? <ArrowDownCircle className="h-4 w-4 text-success shrink-0" />
|
|
132
|
-
: <ArrowUpCircle className="h-4 w-4 text-destructive shrink-0" />}
|
|
133
|
-
<div className="min-w-0 flex-1">
|
|
134
|
-
<p className="text-sm truncate">{line.notes || line.externalId}</p>
|
|
135
|
-
<p className="text-[10px] text-muted-foreground">
|
|
136
|
-
{line.paymentDate ?? line.dueDate}
|
|
137
|
-
{line.externalSource && <span className="ml-1 uppercase tracking-wide">· {line.externalSource}</span>}
|
|
138
|
-
</p>
|
|
139
|
-
</div>
|
|
140
|
-
<span className={`text-sm font-semibold shrink-0 ${isCredit ? 'text-success' : 'text-destructive'}`}>
|
|
141
|
-
{fmt(line.amount)}
|
|
142
|
-
</span>
|
|
143
|
-
</button>
|
|
144
|
-
)
|
|
145
|
-
})}
|
|
146
|
-
</div>
|
|
147
|
-
</div>
|
|
148
|
-
|
|
149
|
-
{/* Right — suggested matches for the selected line */}
|
|
150
|
-
<div className="rounded-lg border bg-card shadow-sm overflow-hidden">
|
|
151
|
-
<div className="flex items-center gap-2 px-4 py-3 border-b">
|
|
152
|
-
<Sparkles className="h-4 w-4 text-muted-foreground" />
|
|
153
|
-
<h3 className="text-sm font-semibold">{t('financial.reconciliation.candidates')}</h3>
|
|
154
|
-
</div>
|
|
155
|
-
|
|
156
|
-
{!selected ? (
|
|
157
|
-
<p className="px-4 py-16 text-center text-sm text-muted-foreground">
|
|
158
|
-
{t('financial.reconciliation.selectLine')}
|
|
159
|
-
</p>
|
|
160
|
-
) : (
|
|
161
|
-
<div className="p-3 space-y-2">
|
|
162
|
-
{candidatesLoading ? (
|
|
163
|
-
<div className="py-10 text-center text-xs text-muted-foreground">…</div>
|
|
164
|
-
) : (
|
|
165
|
-
<>
|
|
166
|
-
{candidates.map((c) => (
|
|
167
|
-
<div key={c.movement.id} className="flex items-center gap-3 rounded-md border p-2.5">
|
|
168
|
-
<div className="min-w-0 flex-1">
|
|
169
|
-
<p className="text-sm truncate">
|
|
170
|
-
{c.invoice?.contactName || c.movement.notes || c.invoice?.fiscalNumber || t('financial.reconciliation.match')}
|
|
171
|
-
</p>
|
|
172
|
-
<p className="text-[10px] text-muted-foreground">
|
|
173
|
-
{c.movement.dueDate}
|
|
174
|
-
<span className="ml-1.5 inline-flex items-center rounded px-1 py-px bg-primary/10 text-primary font-medium">
|
|
175
|
-
{t('financial.reconciliation.matchScore', { score: Math.round(c.score * 100) })}
|
|
176
|
-
</span>
|
|
177
|
-
</p>
|
|
178
|
-
</div>
|
|
179
|
-
<span className="text-sm font-semibold shrink-0">{fmt(c.movement.amount)}</span>
|
|
180
|
-
<Button size="sm" disabled={busy} onClick={() => match(selected.id, c.movement.id)}>
|
|
181
|
-
<Link2 className="h-3.5 w-3.5" />
|
|
182
|
-
{t('financial.reconciliation.match')}
|
|
183
|
-
</Button>
|
|
184
|
-
</div>
|
|
185
|
-
))}
|
|
186
|
-
{candidates.length === 0 && (
|
|
187
|
-
<p className="py-6 text-center text-xs text-muted-foreground">
|
|
188
|
-
{t('financial.reconciliation.noCandidates')}
|
|
189
|
-
</p>
|
|
190
|
-
)}
|
|
191
|
-
|
|
192
|
-
{/* Accept the line standalone (no internal counterpart) */}
|
|
193
|
-
<div className="pt-2 mt-1 border-t">
|
|
194
|
-
<Button variant="outline" size="sm" className="w-full" disabled={busy} onClick={() => match(selected.id)}>
|
|
195
|
-
<Check className="h-3.5 w-3.5" />
|
|
196
|
-
{t('financial.reconciliation.accept')}
|
|
197
|
-
</Button>
|
|
198
|
-
<p className="text-[10px] text-muted-foreground text-center mt-1">
|
|
199
|
-
{t('financial.reconciliation.acceptHint')}
|
|
200
|
-
</p>
|
|
201
|
-
</div>
|
|
202
|
-
</>
|
|
203
|
-
)}
|
|
204
|
-
</div>
|
|
205
|
-
)}
|
|
206
|
-
</div>
|
|
207
|
-
</div>
|
|
208
|
-
)}
|
|
209
|
-
</div>
|
|
210
|
-
)
|
|
211
|
-
}
|
|
@@ -1,335 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useState, useMemo } from 'react'
|
|
2
|
-
import { Receipt, ArrowRightLeft, CheckCheck, Landmark } from 'lucide-react'
|
|
3
|
-
import type { ColumnDef } from '@tanstack/react-table'
|
|
4
|
-
import { DataTable, SubpageHeader, DatePicker, Button, Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '@fayz-ai/ui'
|
|
5
|
-
import { useFinancialConfig, useFinancialStore, formatCurrency } from '../FinancialContext'
|
|
6
|
-
import { useTranslation } from '@fayz-ai/core'
|
|
7
|
-
import { PersonLink } from '@fayz-ai/saas'
|
|
8
|
-
import type { StatementEntry } from '../types'
|
|
9
|
-
import { TransferModal } from '../components/TransferModal'
|
|
10
|
-
|
|
11
|
-
const ALL = 'all'
|
|
12
|
-
|
|
13
|
-
// Soft reconciliation marker for an extract row — a muted glyph with a hover
|
|
14
|
-
// tooltip explaining what it means. Rendered only when the movement carries the
|
|
15
|
-
// relevant state (reconciledAt / externalSource).
|
|
16
|
-
function ReconcileMarker({ kind, label }: { kind: 'reconciled' | 'imported'; label: string }) {
|
|
17
|
-
return (
|
|
18
|
-
<TooltipProvider delayDuration={150}>
|
|
19
|
-
<Tooltip>
|
|
20
|
-
<TooltipTrigger asChild>
|
|
21
|
-
<span className="inline-flex shrink-0 cursor-default">
|
|
22
|
-
{kind === 'reconciled'
|
|
23
|
-
? <CheckCheck className="h-3 w-3 text-success/70" />
|
|
24
|
-
: <Landmark className="h-3 w-3 text-muted-foreground/40" />}
|
|
25
|
-
</span>
|
|
26
|
-
</TooltipTrigger>
|
|
27
|
-
<TooltipContent className="text-xs">{label}</TooltipContent>
|
|
28
|
-
</Tooltip>
|
|
29
|
-
</TooltipProvider>
|
|
30
|
-
)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Reference to the internal transaction a reconciled bank line was matched to —
|
|
34
|
-
// a small "⇄ {ref}" chip, clickable when the counterpart has an invoice to open.
|
|
35
|
-
function ReconciledWithRef({ refLabel, tooltip, onOpen }: { refLabel: string; tooltip: string; onOpen?: () => void }) {
|
|
36
|
-
const inner = (
|
|
37
|
-
<span className="inline-flex items-center gap-0.5 text-[10px] text-muted-foreground max-w-[160px]">
|
|
38
|
-
<ArrowRightLeft className="h-2.5 w-2.5 shrink-0" />
|
|
39
|
-
<span className="truncate">{refLabel}</span>
|
|
40
|
-
</span>
|
|
41
|
-
)
|
|
42
|
-
return (
|
|
43
|
-
<TooltipProvider delayDuration={150}>
|
|
44
|
-
<Tooltip>
|
|
45
|
-
<TooltipTrigger asChild>
|
|
46
|
-
{onOpen
|
|
47
|
-
? <button onClick={onOpen} className="hover:text-primary transition-colors">{inner}</button>
|
|
48
|
-
: <span className="cursor-default">{inner}</span>}
|
|
49
|
-
</TooltipTrigger>
|
|
50
|
-
<TooltipContent className="text-xs">{tooltip}</TooltipContent>
|
|
51
|
-
</Tooltip>
|
|
52
|
-
</TooltipProvider>
|
|
53
|
-
)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function StatementsView({ onNavigate }: { onNavigate?: (view: string) => void }) {
|
|
57
|
-
const t = useTranslation()
|
|
58
|
-
const { currency } = useFinancialConfig()
|
|
59
|
-
const bankAccounts = useFinancialStore((s) => s.bankAccounts)
|
|
60
|
-
const statement = useFinancialStore((s) => s.statement)
|
|
61
|
-
const statementLoading = useFinancialStore((s) => s.statementLoading)
|
|
62
|
-
const paymentMethods = useFinancialStore((s) => s.paymentMethods)
|
|
63
|
-
const paymentMethodTypes = useFinancialStore((s) => s.paymentMethodTypes)
|
|
64
|
-
const fetchBankAccounts = useFinancialStore((s) => s.fetchBankAccounts)
|
|
65
|
-
const fetchPaymentMethods = useFinancialStore((s) => s.fetchPaymentMethods)
|
|
66
|
-
const fetchStatement = useFinancialStore((s) => s.fetchStatement)
|
|
67
|
-
|
|
68
|
-
const [accountId, setAccountId] = useState('')
|
|
69
|
-
const [dateFrom, setDateFrom] = useState(() => {
|
|
70
|
-
const d = new Date(); d.setMonth(d.getMonth() - 3)
|
|
71
|
-
return d.toISOString().slice(0, 10)
|
|
72
|
-
})
|
|
73
|
-
const [dateTo, setDateTo] = useState(() => new Date().toISOString().slice(0, 10))
|
|
74
|
-
const [showTransfer, setShowTransfer] = useState(false)
|
|
75
|
-
|
|
76
|
-
useEffect(() => { fetchBankAccounts(); fetchPaymentMethods() }, [])
|
|
77
|
-
|
|
78
|
-
// Resolve a human payment-method label for a movement (method → type → card brand).
|
|
79
|
-
const methodName = useMemo(() => {
|
|
80
|
-
const byId = new Map(paymentMethods.map((m) => [m.id, m.name]))
|
|
81
|
-
const typeById = new Map(paymentMethodTypes.map((tp) => [tp.id, tp.name]))
|
|
82
|
-
return (mov: StatementEntry['movement']): string => {
|
|
83
|
-
if (mov.paymentMethodId && byId.has(mov.paymentMethodId)) return byId.get(mov.paymentMethodId)!
|
|
84
|
-
if (mov.paymentMethodTypeId && typeById.has(mov.paymentMethodTypeId)) return typeById.get(mov.paymentMethodTypeId)!
|
|
85
|
-
if (mov.cardBrand) return mov.cardBrand
|
|
86
|
-
return '—'
|
|
87
|
-
}
|
|
88
|
-
}, [paymentMethods, paymentMethodTypes])
|
|
89
|
-
|
|
90
|
-
// Open the linked invoice (receivable/payable detail) for a statement row.
|
|
91
|
-
function openInvoice(e: StatementEntry) {
|
|
92
|
-
if (!onNavigate || !e.movement.invoiceId) return
|
|
93
|
-
const base = e.movement.direction === 'credit' ? 'receivables' : 'payables'
|
|
94
|
-
onNavigate(`${base}-detail:${e.movement.invoiceId}`)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
useEffect(() => {
|
|
98
|
-
if (accountId) {
|
|
99
|
-
fetchStatement({
|
|
100
|
-
bankAccountId: accountId === ALL ? undefined : accountId,
|
|
101
|
-
dateRange: { from: dateFrom, to: dateTo },
|
|
102
|
-
})
|
|
103
|
-
}
|
|
104
|
-
}, [accountId, dateFrom, dateTo])
|
|
105
|
-
|
|
106
|
-
// Default to the consolidated "All accounts" view so paid invoices show up even
|
|
107
|
-
// when payments weren't tied to a specific bank account.
|
|
108
|
-
useEffect(() => {
|
|
109
|
-
if (!accountId) setAccountId(ALL)
|
|
110
|
-
}, [])
|
|
111
|
-
|
|
112
|
-
const fmt = (n: number) => formatCurrency(n, currency)
|
|
113
|
-
|
|
114
|
-
const columns: ColumnDef<StatementEntry, any>[] = useMemo(() => [
|
|
115
|
-
{
|
|
116
|
-
id: 'date', header: t('financial.statements.columnDate'),
|
|
117
|
-
cell: ({ row }) => (
|
|
118
|
-
<span className="text-muted-foreground text-xs">{row.original.movement.paymentDate ?? row.original.movement.dueDate}</span>
|
|
119
|
-
),
|
|
120
|
-
},
|
|
121
|
-
{
|
|
122
|
-
id: 'description', header: t('financial.statements.columnDescription'),
|
|
123
|
-
cell: ({ row }) => {
|
|
124
|
-
const e = row.original
|
|
125
|
-
const isTransfer = e.entryKind !== 'movement'
|
|
126
|
-
const memo = e.movement.notes
|
|
127
|
-
const title = e.invoice?.contactName
|
|
128
|
-
|| (isTransfer ? t('financial.statements.transfer') : (memo || 'Transaction'))
|
|
129
|
-
// Show the memo on its own line unless it's already serving as the title.
|
|
130
|
-
const memoLine = memo && memo !== title ? memo : undefined
|
|
131
|
-
const doc = e.invoice?.fiscalNumber
|
|
132
|
-
const inst = e.movement.installmentNumber
|
|
133
|
-
const canOpen = !!onNavigate && !!e.movement.invoiceId
|
|
134
|
-
return (
|
|
135
|
-
<div>
|
|
136
|
-
<div className="flex items-center gap-1.5">
|
|
137
|
-
{/* Person name → hovercard with contact details (plain text when no contact) */}
|
|
138
|
-
<PersonLink personId={e.invoice?.contactId} name={title} size="sm" />
|
|
139
|
-
{/* Document reference → opens the linked invoice */}
|
|
140
|
-
{doc && (canOpen ? (
|
|
141
|
-
<button
|
|
142
|
-
onClick={() => openInvoice(e)}
|
|
143
|
-
className="text-[10px] text-muted-foreground font-mono hover:text-primary hover:underline"
|
|
144
|
-
title={t('financial.statements.viewInvoice')}
|
|
145
|
-
>
|
|
146
|
-
{doc}
|
|
147
|
-
</button>
|
|
148
|
-
) : (
|
|
149
|
-
<span className="text-[10px] text-muted-foreground font-mono">{doc}</span>
|
|
150
|
-
))}
|
|
151
|
-
{isTransfer && (
|
|
152
|
-
<span className="text-[9px] uppercase tracking-wide px-1 py-px rounded bg-warning/15 text-warning font-semibold">
|
|
153
|
-
{t('financial.statements.transfer')}
|
|
154
|
-
</span>
|
|
155
|
-
)}
|
|
156
|
-
{/* Soft reconciliation marker — optional, driven by what the plugin exposes:
|
|
157
|
-
reconciledAt = matched/accepted (conciliado); externalSource = imported from a
|
|
158
|
-
bank sync but not yet reconciled. Hover shows what it means. */}
|
|
159
|
-
{e.movement.reconciledAt ? (
|
|
160
|
-
<ReconcileMarker kind="reconciled" label={t('financial.statements.reconciled')} />
|
|
161
|
-
) : e.movement.externalSource ? (
|
|
162
|
-
<ReconcileMarker kind="imported" label={t('financial.statements.imported')} />
|
|
163
|
-
) : null}
|
|
164
|
-
{/* The transaction this bank line was conciliated with — link/id. */}
|
|
165
|
-
{e.reconciledWith && (() => {
|
|
166
|
-
const rw = e.reconciledWith
|
|
167
|
-
const refLabel = rw.fiscalNumber || rw.contactName || rw.description || rw.movementId.slice(0, 8)
|
|
168
|
-
const base = e.movement.direction === 'credit' ? 'receivables' : 'payables'
|
|
169
|
-
const onOpen = onNavigate && rw.invoiceId ? () => onNavigate(`${base}-detail:${rw.invoiceId}`) : undefined
|
|
170
|
-
return (
|
|
171
|
-
<ReconciledWithRef
|
|
172
|
-
refLabel={refLabel}
|
|
173
|
-
tooltip={t('financial.statements.reconciledWith', { ref: refLabel })}
|
|
174
|
-
onOpen={onOpen}
|
|
175
|
-
/>
|
|
176
|
-
)
|
|
177
|
-
})()}
|
|
178
|
-
</div>
|
|
179
|
-
{memoLine && <p className="text-[11px] text-foreground/80">{memoLine}</p>}
|
|
180
|
-
<p className="text-[10px] text-muted-foreground">
|
|
181
|
-
<span className="capitalize">
|
|
182
|
-
{isTransfer && e.counterAccountName
|
|
183
|
-
? (e.entryKind === 'transfer-in' ? `← ${e.counterAccountName}` : `→ ${e.counterAccountName}`)
|
|
184
|
-
: e.movement.movementKind}
|
|
185
|
-
</span>
|
|
186
|
-
{inst ? ` · ${t('financial.statements.installmentShort', { n: inst })}` : ''}
|
|
187
|
-
</p>
|
|
188
|
-
</div>
|
|
189
|
-
)
|
|
190
|
-
},
|
|
191
|
-
},
|
|
192
|
-
{
|
|
193
|
-
id: 'method', header: t('financial.statements.columnMethod'),
|
|
194
|
-
cell: ({ row }) => {
|
|
195
|
-
const mov = row.original.movement
|
|
196
|
-
const name = methodName(mov)
|
|
197
|
-
return (
|
|
198
|
-
<div>
|
|
199
|
-
<p className="text-xs">{name}</p>
|
|
200
|
-
{mov.cardInstallments && mov.cardInstallments > 1 && (
|
|
201
|
-
<p className="text-[10px] text-muted-foreground">{mov.cardInstallments}x</p>
|
|
202
|
-
)}
|
|
203
|
-
</div>
|
|
204
|
-
)
|
|
205
|
-
},
|
|
206
|
-
},
|
|
207
|
-
{
|
|
208
|
-
id: 'debit', header: t('financial.statements.columnDebit'),
|
|
209
|
-
cell: ({ row }) => (
|
|
210
|
-
<span className="text-right block text-destructive text-xs">
|
|
211
|
-
{row.original.movement.direction === 'debit' ? fmt(row.original.net) : ''}
|
|
212
|
-
</span>
|
|
213
|
-
),
|
|
214
|
-
},
|
|
215
|
-
{
|
|
216
|
-
id: 'credit', header: t('financial.statements.columnCredit'),
|
|
217
|
-
cell: ({ row }) => {
|
|
218
|
-
const e = row.original
|
|
219
|
-
if (e.movement.direction !== 'credit') return <span className="block text-right" />
|
|
220
|
-
return (
|
|
221
|
-
<div className="text-right">
|
|
222
|
-
<span className="block text-success text-xs">{fmt(e.net)}</span>
|
|
223
|
-
{e.fee > 0 && (
|
|
224
|
-
<span className="block text-[10px] text-muted-foreground">{fmt(e.gross)} − {fmt(e.fee)}</span>
|
|
225
|
-
)}
|
|
226
|
-
</div>
|
|
227
|
-
)
|
|
228
|
-
},
|
|
229
|
-
},
|
|
230
|
-
{
|
|
231
|
-
id: 'fee', header: t('financial.statements.columnFee'),
|
|
232
|
-
cell: ({ row }) => (
|
|
233
|
-
<span className="text-right block text-warning text-xs">
|
|
234
|
-
{row.original.fee > 0 ? fmt(row.original.fee) : ''}
|
|
235
|
-
</span>
|
|
236
|
-
),
|
|
237
|
-
},
|
|
238
|
-
{
|
|
239
|
-
id: 'balance', header: t('financial.statements.columnBalance'),
|
|
240
|
-
cell: ({ row }) => (
|
|
241
|
-
<span className="text-right block font-medium text-xs">{fmt(row.original.runningBalance)}</span>
|
|
242
|
-
),
|
|
243
|
-
},
|
|
244
|
-
], [currency, t, methodName, onNavigate])
|
|
245
|
-
|
|
246
|
-
const entries = statement?.entries ?? []
|
|
247
|
-
|
|
248
|
-
return (
|
|
249
|
-
<div className="space-y-4">
|
|
250
|
-
<SubpageHeader
|
|
251
|
-
title={t('financial.statements.title')}
|
|
252
|
-
subtitle={t('financial.statements.subtitle')}
|
|
253
|
-
actions={
|
|
254
|
-
<Button variant="outline" size="sm" onClick={() => setShowTransfer(true)} disabled={bankAccounts.length < 2}>
|
|
255
|
-
<ArrowRightLeft className="h-3.5 w-3.5" />
|
|
256
|
-
{t('financial.statements.newTransfer')}
|
|
257
|
-
</Button>
|
|
258
|
-
}
|
|
259
|
-
/>
|
|
260
|
-
|
|
261
|
-
{/* Filters */}
|
|
262
|
-
<div className="flex flex-col sm:flex-row gap-3 sm:items-end">
|
|
263
|
-
<div className="flex-1 max-w-xs">
|
|
264
|
-
<label className="text-[10px] font-medium text-muted-foreground uppercase">{t('financial.statements.account')}</label>
|
|
265
|
-
<select
|
|
266
|
-
value={accountId}
|
|
267
|
-
onChange={(e) => setAccountId(e.target.value)}
|
|
268
|
-
className="w-full mt-0.5 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-3 py-2 text-sm"
|
|
269
|
-
>
|
|
270
|
-
<option value={ALL}>{t('financial.summary.allAccounts')}</option>
|
|
271
|
-
{bankAccounts.map((a) => (
|
|
272
|
-
<option key={a.id} value={a.id}>{a.name}</option>
|
|
273
|
-
))}
|
|
274
|
-
</select>
|
|
275
|
-
</div>
|
|
276
|
-
<div>
|
|
277
|
-
<label className="text-[10px] font-medium text-muted-foreground uppercase">{t('financial.statements.from')}</label>
|
|
278
|
-
<DatePicker value={dateFrom} onChange={setDateFrom} className="mt-0.5" />
|
|
279
|
-
</div>
|
|
280
|
-
<div>
|
|
281
|
-
<label className="text-[10px] font-medium text-muted-foreground uppercase">{t('financial.statements.to')}</label>
|
|
282
|
-
<DatePicker value={dateTo} onChange={setDateTo} className="mt-0.5" />
|
|
283
|
-
</div>
|
|
284
|
-
</div>
|
|
285
|
-
|
|
286
|
-
{/* Summary header — standard bank-extract layout: opening → credits → debits → closing */}
|
|
287
|
-
{accountId && statement && (
|
|
288
|
-
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
|
289
|
-
<SummaryStat label={t('financial.statements.opening')} value={fmt(statement.openingBalance)} />
|
|
290
|
-
<SummaryStat label={t('financial.statements.totalCredits')} value={fmt(statement.totalCredits)} positive />
|
|
291
|
-
<SummaryStat label={t('financial.statements.totalDebits')} value={fmt(statement.totalDebits)} negative />
|
|
292
|
-
<SummaryStat label={t('financial.statements.closing')} value={fmt(statement.closingBalance)} accent />
|
|
293
|
-
</div>
|
|
294
|
-
)}
|
|
295
|
-
|
|
296
|
-
{/* Statement table */}
|
|
297
|
-
{!accountId ? (
|
|
298
|
-
<div className="flex flex-col items-center justify-center py-12 text-center rounded-lg border-2 border-dashed border-muted">
|
|
299
|
-
<Receipt className="h-8 w-8 text-muted-foreground mb-2" />
|
|
300
|
-
<p className="text-sm text-muted-foreground">{t('financial.statements.selectToView')}</p>
|
|
301
|
-
</div>
|
|
302
|
-
) : statementLoading ? (
|
|
303
|
-
<DataTable columns={columns} data={[]} loading skeletonRows={8} compact />
|
|
304
|
-
) : entries.length === 0 ? (
|
|
305
|
-
<DataTable columns={columns} data={[]} emptyMessage={t('financial.statements.noTransactions')} compact />
|
|
306
|
-
) : (
|
|
307
|
-
<DataTable columns={columns} data={entries} compact />
|
|
308
|
-
)}
|
|
309
|
-
|
|
310
|
-
{showTransfer && (
|
|
311
|
-
<TransferModal
|
|
312
|
-
defaultFromId={accountId && accountId !== ALL ? accountId : undefined}
|
|
313
|
-
onClose={() => setShowTransfer(false)}
|
|
314
|
-
onDone={() => setShowTransfer(false)}
|
|
315
|
-
/>
|
|
316
|
-
)}
|
|
317
|
-
</div>
|
|
318
|
-
)
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
function SummaryStat({ label, value, accent, positive, negative }: {
|
|
322
|
-
label: string
|
|
323
|
-
value: string
|
|
324
|
-
accent?: boolean
|
|
325
|
-
positive?: boolean
|
|
326
|
-
negative?: boolean
|
|
327
|
-
}) {
|
|
328
|
-
const valueClass = positive ? 'text-success' : negative ? 'text-destructive' : accent ? 'text-foreground' : 'text-foreground'
|
|
329
|
-
return (
|
|
330
|
-
<div className={`rounded-lg border px-3 py-2 ${accent ? 'bg-muted/40' : 'bg-card'}`}>
|
|
331
|
-
<p className="text-[10px] font-medium text-muted-foreground uppercase truncate">{label}</p>
|
|
332
|
-
<p className={`text-sm font-semibold mt-0.5 ${valueClass}`}>{value}</p>
|
|
333
|
-
</div>
|
|
334
|
-
)
|
|
335
|
-
}
|