@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,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
- }