@fayz-ai/plugin-financial 0.1.2 → 0.1.4
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/LICENSE +21 -0
- package/dist/FinancialContext.d.ts +2 -0
- package/dist/FinancialContext.d.ts.map +1 -1
- package/dist/FinancialPage.d.ts.map +1 -1
- package/dist/components/PaymentModal.d.ts.map +1 -1
- package/dist/components/PersonStatementWidget.d.ts +18 -0
- package/dist/components/PersonStatementWidget.d.ts.map +1 -0
- package/dist/components/TransferModal.d.ts +7 -0
- package/dist/components/TransferModal.d.ts.map +1 -0
- package/dist/contract.test.d.ts +2 -0
- package/dist/contract.test.d.ts.map +1 -0
- package/dist/data/capability.test.d.ts +2 -0
- package/dist/data/capability.test.d.ts.map +1 -0
- package/dist/data/mock.d.ts.map +1 -1
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/data/types.d.ts +13 -2
- package/dist/data/types.d.ts.map +1 -1
- package/dist/index.cjs +1529 -263
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1437 -171
- package/dist/index.js.map +1 -1
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/pt-BR.d.ts.map +1 -1
- package/dist/registries.d.ts.map +1 -1
- package/dist/store.d.ts +4 -2
- package/dist/store.d.ts.map +1 -1
- package/dist/types.d.ts +111 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/views/InvoiceDetailView.d.ts +3 -1
- package/dist/views/InvoiceDetailView.d.ts.map +1 -1
- package/dist/views/InvoiceFormView.d.ts.map +1 -1
- package/dist/views/PayablesView.d.ts +5 -1
- package/dist/views/PayablesView.d.ts.map +1 -1
- package/dist/views/ReceivablesView.d.ts +5 -1
- package/dist/views/ReceivablesView.d.ts.map +1 -1
- package/dist/views/ReconciliationView.d.ts +3 -0
- package/dist/views/ReconciliationView.d.ts.map +1 -0
- package/dist/views/StatementsView.d.ts +3 -1
- package/dist/views/StatementsView.d.ts.map +1 -1
- package/dist/views/navReferrer.d.ts +5 -0
- package/dist/views/navReferrer.d.ts.map +1 -0
- package/package.json +8 -5
- package/src/FinancialContext.tsx +2 -0
- package/src/FinancialPage.tsx +32 -17
- package/src/components/PaymentModal.tsx +58 -11
- package/src/components/PersonStatementWidget.tsx +133 -0
- package/src/components/TransferModal.tsx +150 -0
- package/src/contract.test.ts +58 -0
- package/src/data/capability.test.ts +90 -0
- package/src/data/mock.ts +265 -52
- package/src/data/supabase.ts +359 -30
- package/src/data/types.ts +22 -3
- package/src/index.ts +28 -2
- package/src/locales/en.ts +76 -0
- package/src/locales/pt-BR.ts +76 -0
- package/src/migrations/006_extract_fee_amount.sql +20 -0
- package/src/migrations/006_rls_policies.sql +37 -0
- package/src/migrations/007_reconciliation.sql +32 -0
- package/src/migrations/008_split_payment_movements.sql +29 -0
- package/src/registries.ts +3 -1
- package/src/store.ts +27 -7
- package/src/types.ts +126 -1
- package/src/views/InvoiceDetailView.tsx +70 -51
- package/src/views/InvoiceFormView.tsx +55 -20
- package/src/views/PayablesView.tsx +13 -2
- package/src/views/ReceivablesView.tsx +13 -2
- package/src/views/ReconciliationView.tsx +211 -0
- package/src/views/StatementsView.tsx +241 -29
- package/src/views/navReferrer.ts +24 -0
|
@@ -1,40 +1,115 @@
|
|
|
1
1
|
import React, { useEffect, useState, useMemo } from 'react'
|
|
2
|
-
import { Receipt } from 'lucide-react'
|
|
2
|
+
import { Receipt, ArrowRightLeft, CheckCheck, Landmark } from 'lucide-react'
|
|
3
3
|
import type { ColumnDef } from '@tanstack/react-table'
|
|
4
|
-
import { DataTable } from '@fayz-ai/ui'
|
|
4
|
+
import { DataTable, SubpageHeader, DatePicker, Button, Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '@fayz-ai/ui'
|
|
5
5
|
import { useFinancialConfig, useFinancialStore, formatCurrency } from '../FinancialContext'
|
|
6
|
-
import { SubpageHeader } from '@fayz-ai/ui'
|
|
7
|
-
import { DatePicker } from '@fayz-ai/ui'
|
|
8
6
|
import { useTranslation } from '@fayz-ai/core'
|
|
7
|
+
import { PersonLink } from '@fayz-ai/saas'
|
|
9
8
|
import type { StatementEntry } from '../types'
|
|
9
|
+
import { TransferModal } from '../components/TransferModal'
|
|
10
10
|
|
|
11
|
-
|
|
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 }) {
|
|
12
57
|
const t = useTranslation()
|
|
13
58
|
const { currency } = useFinancialConfig()
|
|
14
59
|
const bankAccounts = useFinancialStore((s) => s.bankAccounts)
|
|
15
|
-
const
|
|
60
|
+
const statement = useFinancialStore((s) => s.statement)
|
|
16
61
|
const statementLoading = useFinancialStore((s) => s.statementLoading)
|
|
62
|
+
const paymentMethods = useFinancialStore((s) => s.paymentMethods)
|
|
63
|
+
const paymentMethodTypes = useFinancialStore((s) => s.paymentMethodTypes)
|
|
17
64
|
const fetchBankAccounts = useFinancialStore((s) => s.fetchBankAccounts)
|
|
65
|
+
const fetchPaymentMethods = useFinancialStore((s) => s.fetchPaymentMethods)
|
|
18
66
|
const fetchStatement = useFinancialStore((s) => s.fetchStatement)
|
|
19
67
|
|
|
20
68
|
const [accountId, setAccountId] = useState('')
|
|
21
69
|
const [dateFrom, setDateFrom] = useState(() => {
|
|
22
|
-
const d = new Date(); d.setMonth(d.getMonth() -
|
|
70
|
+
const d = new Date(); d.setMonth(d.getMonth() - 3)
|
|
23
71
|
return d.toISOString().slice(0, 10)
|
|
24
72
|
})
|
|
25
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])
|
|
26
89
|
|
|
27
|
-
|
|
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
|
+
}
|
|
28
96
|
|
|
29
97
|
useEffect(() => {
|
|
30
98
|
if (accountId) {
|
|
31
|
-
fetchStatement({
|
|
99
|
+
fetchStatement({
|
|
100
|
+
bankAccountId: accountId === ALL ? undefined : accountId,
|
|
101
|
+
dateRange: { from: dateFrom, to: dateTo },
|
|
102
|
+
})
|
|
32
103
|
}
|
|
33
104
|
}, [accountId, dateFrom, dateTo])
|
|
34
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.
|
|
35
108
|
useEffect(() => {
|
|
36
|
-
if (!accountId
|
|
37
|
-
}, [
|
|
109
|
+
if (!accountId) setAccountId(ALL)
|
|
110
|
+
}, [])
|
|
111
|
+
|
|
112
|
+
const fmt = (n: number) => formatCurrency(n, currency)
|
|
38
113
|
|
|
39
114
|
const columns: ColumnDef<StatementEntry, any>[] = useMemo(() => [
|
|
40
115
|
{
|
|
@@ -45,40 +120,143 @@ export function StatementsView() {
|
|
|
45
120
|
},
|
|
46
121
|
{
|
|
47
122
|
id: 'description', header: t('financial.statements.columnDescription'),
|
|
48
|
-
cell: ({ row }) =>
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
+
},
|
|
54
206
|
},
|
|
55
207
|
{
|
|
56
208
|
id: 'debit', header: t('financial.statements.columnDebit'),
|
|
57
209
|
cell: ({ row }) => (
|
|
58
210
|
<span className="text-right block text-destructive text-xs">
|
|
59
|
-
{row.original.movement.direction === 'debit' ?
|
|
211
|
+
{row.original.movement.direction === 'debit' ? fmt(row.original.net) : ''}
|
|
60
212
|
</span>
|
|
61
213
|
),
|
|
62
214
|
},
|
|
63
215
|
{
|
|
64
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'),
|
|
65
232
|
cell: ({ row }) => (
|
|
66
|
-
<span className="text-right block text-
|
|
67
|
-
{row.original.
|
|
233
|
+
<span className="text-right block text-warning text-xs">
|
|
234
|
+
{row.original.fee > 0 ? fmt(row.original.fee) : ''}
|
|
68
235
|
</span>
|
|
69
236
|
),
|
|
70
237
|
},
|
|
71
238
|
{
|
|
72
239
|
id: 'balance', header: t('financial.statements.columnBalance'),
|
|
73
240
|
cell: ({ row }) => (
|
|
74
|
-
<span className="text-right block font-medium text-xs">{
|
|
241
|
+
<span className="text-right block font-medium text-xs">{fmt(row.original.runningBalance)}</span>
|
|
75
242
|
),
|
|
76
243
|
},
|
|
77
|
-
], [currency, t])
|
|
244
|
+
], [currency, t, methodName, onNavigate])
|
|
245
|
+
|
|
246
|
+
const entries = statement?.entries ?? []
|
|
78
247
|
|
|
79
248
|
return (
|
|
80
249
|
<div className="space-y-4">
|
|
81
|
-
<SubpageHeader
|
|
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
|
+
/>
|
|
82
260
|
|
|
83
261
|
{/* Filters */}
|
|
84
262
|
<div className="flex flex-col sm:flex-row gap-3 sm:items-end">
|
|
@@ -87,9 +265,9 @@ export function StatementsView() {
|
|
|
87
265
|
<select
|
|
88
266
|
value={accountId}
|
|
89
267
|
onChange={(e) => setAccountId(e.target.value)}
|
|
90
|
-
className="w-full mt-0.5 rounded-input border border-input
|
|
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"
|
|
91
269
|
>
|
|
92
|
-
<option value=
|
|
270
|
+
<option value={ALL}>{t('financial.summary.allAccounts')}</option>
|
|
93
271
|
{bankAccounts.map((a) => (
|
|
94
272
|
<option key={a.id} value={a.id}>{a.name}</option>
|
|
95
273
|
))}
|
|
@@ -105,6 +283,16 @@ export function StatementsView() {
|
|
|
105
283
|
</div>
|
|
106
284
|
</div>
|
|
107
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
|
+
|
|
108
296
|
{/* Statement table */}
|
|
109
297
|
{!accountId ? (
|
|
110
298
|
<div className="flex flex-col items-center justify-center py-12 text-center rounded-lg border-2 border-dashed border-muted">
|
|
@@ -112,12 +300,36 @@ export function StatementsView() {
|
|
|
112
300
|
<p className="text-sm text-muted-foreground">{t('financial.statements.selectToView')}</p>
|
|
113
301
|
</div>
|
|
114
302
|
) : statementLoading ? (
|
|
115
|
-
<DataTable columns={columns} data={[]} loading skeletonRows={
|
|
116
|
-
) :
|
|
117
|
-
<DataTable columns={columns} data={[]} emptyMessage={t('financial.statements.noTransactions')} />
|
|
303
|
+
<DataTable columns={columns} data={[]} loading skeletonRows={8} compact />
|
|
304
|
+
) : entries.length === 0 ? (
|
|
305
|
+
<DataTable columns={columns} data={[]} emptyMessage={t('financial.statements.noTransactions')} compact />
|
|
118
306
|
) : (
|
|
119
|
-
<DataTable columns={columns} data={
|
|
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
|
+
/>
|
|
120
316
|
)}
|
|
121
317
|
</div>
|
|
122
318
|
)
|
|
123
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
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
type TFunc = (key: string, params?: Record<string, string | number>) => string
|
|
2
|
+
|
|
3
|
+
// Maps a previous in-module view to the i18n key for its human label, used to
|
|
4
|
+
// render a context-aware back link ("Back to {where you came from}") on detail
|
|
5
|
+
// pages. Views not listed here (e.g. the edit/detail of the record we're
|
|
6
|
+
// returning from, or unknown views) are not meaningful referrers — callers fall
|
|
7
|
+
// back to the default parent label.
|
|
8
|
+
const REFERRER_LABEL_KEYS: Record<string, string> = {
|
|
9
|
+
summary: 'financial.nav.summary',
|
|
10
|
+
statements: 'financial.nav.statements',
|
|
11
|
+
'cash-registers': 'financial.nav.cashRegisters',
|
|
12
|
+
reconciliation: 'financial.nav.reconciliation',
|
|
13
|
+
commissions: 'financial.nav.commissions',
|
|
14
|
+
cards: 'financial.nav.cards',
|
|
15
|
+
'receivables-list': 'financial.nav.receivables',
|
|
16
|
+
'payables-list': 'financial.nav.payables',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Human label for the page the user came from, or null when it isn't a meaningful referrer. */
|
|
20
|
+
export function referrerLabel(view: string | null | undefined, t: TFunc): string | null {
|
|
21
|
+
if (!view) return null
|
|
22
|
+
const key = REFERRER_LABEL_KEYS[view]
|
|
23
|
+
return key ? t(key) : null
|
|
24
|
+
}
|