@fayz-ai/plugin-financial 0.1.7 → 0.8.0

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 (72) hide show
  1. package/README.md +2 -0
  2. package/dist/FinancialContext.d.ts +2 -0
  3. package/dist/FinancialContext.d.ts.map +1 -1
  4. package/dist/FinancialPage.d.ts.map +1 -1
  5. package/dist/data/index.d.ts +1 -0
  6. package/dist/data/index.d.ts.map +1 -1
  7. package/dist/data/mock.d.ts +15 -0
  8. package/dist/data/mock.d.ts.map +1 -1
  9. package/dist/data/supabase.d.ts.map +1 -1
  10. package/dist/data/tables.d.ts +11 -0
  11. package/dist/data/tables.d.ts.map +1 -0
  12. package/dist/data/types.d.ts +2 -1
  13. package/dist/data/types.d.ts.map +1 -1
  14. package/dist/index.cjs +1594 -252
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.ts +13 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +1456 -131
  19. package/dist/index.js.map +1 -1
  20. package/dist/locales/en.d.ts.map +1 -1
  21. package/dist/locales/pt-BR.d.ts.map +1 -1
  22. package/dist/migrations/index.d.ts +16 -0
  23. package/dist/migrations/index.d.ts.map +1 -0
  24. package/dist/quick-add.d.ts +13 -0
  25. package/dist/quick-add.d.ts.map +1 -0
  26. package/dist/registries.d.ts.map +1 -1
  27. package/dist/store.d.ts +3 -1
  28. package/dist/store.d.ts.map +1 -1
  29. package/dist/types.d.ts +42 -0
  30. package/dist/types.d.ts.map +1 -1
  31. package/dist/views/CardsView.d.ts.map +1 -1
  32. package/dist/views/InvoiceListView.d.ts.map +1 -1
  33. package/dist/views/QuickTransactionForm.d.ts +16 -0
  34. package/dist/views/QuickTransactionForm.d.ts.map +1 -0
  35. package/dist/views/SummaryView.d.ts +5 -4
  36. package/dist/views/SummaryView.d.ts.map +1 -1
  37. package/dist/views/dashboardWidgets.d.ts.map +1 -1
  38. package/package.json +7 -4
  39. package/src/FinancialContext.tsx +2 -0
  40. package/src/FinancialPage.tsx +86 -7
  41. package/src/README.md +5 -5
  42. package/src/contract.test.ts +1 -1
  43. package/src/data/capability.test.ts +45 -0
  44. package/src/data/index.ts +1 -0
  45. package/src/data/mock.ts +50 -10
  46. package/src/data/supabase.ts +95 -49
  47. package/src/data/tables.ts +12 -0
  48. package/src/data/types.ts +2 -1
  49. package/src/index.ts +24 -4
  50. package/src/locales/en.ts +75 -0
  51. package/src/locales/pt-BR.ts +75 -0
  52. package/src/migrations/000_plg_rename.sql +30 -0
  53. package/src/migrations/001_financial_base.sql +30 -30
  54. package/src/migrations/002_chart_of_accounts.sql +10 -10
  55. package/src/migrations/003_card_brands.sql +4 -4
  56. package/src/migrations/004_order_to_cash.sql +28 -28
  57. package/src/migrations/005_seed_defaults.sql +6 -6
  58. package/src/migrations/006_rls_policies.sql +5 -5
  59. package/src/migrations/{006_extract_fee_amount.sql → 006b_extract_fee_amount.sql} +4 -4
  60. package/src/migrations/007_reconciliation.sql +9 -9
  61. package/src/migrations/007b_movement_payment_method_type.sql +6 -0
  62. package/src/migrations/008_split_payment_movements.sql +5 -5
  63. package/src/migrations/index.ts +544 -0
  64. package/src/quick-add.ts +58 -0
  65. package/src/registries.ts +8 -7
  66. package/src/store.ts +88 -0
  67. package/src/types.ts +45 -0
  68. package/src/views/CardsView.tsx +186 -50
  69. package/src/views/InvoiceListView.tsx +60 -3
  70. package/src/views/QuickTransactionForm.tsx +537 -0
  71. package/src/views/SummaryView.tsx +197 -6
  72. package/src/views/dashboardWidgets.tsx +251 -5
package/src/store.ts CHANGED
@@ -9,6 +9,7 @@ import type {
9
9
  InvoiceQuery, MovementQuery, StatementQuery,
10
10
  CreateInvoiceInput, PayMovementInput, CreateTransferInput,
11
11
  OpenCashSessionInput, CloseCashSessionInput,
12
+ QuickTransactionInput,
12
13
  DateRange,
13
14
  } from './types'
14
15
 
@@ -55,6 +56,8 @@ export interface FinancialUIState {
55
56
  fetchStatement(query: StatementQuery): Promise<void>
56
57
  fetchCardTransactions(dateRange?: DateRange): Promise<void>
57
58
  createInvoice(input: CreateInvoiceInput): Promise<Invoice>
59
+ /** FAY-1225 "log money in a few taps": one call → the right provider plumbing + full surface refresh. */
60
+ createQuickTransaction(input: QuickTransactionInput): Promise<void>
58
61
  payMovement(input: PayMovementInput): Promise<void>
59
62
  createTransfer(input: CreateTransferInput): Promise<void>
60
63
  cancelInvoice(invoiceId: string): Promise<void>
@@ -169,6 +172,91 @@ export function createFinancialStore(provider: FinancialDataProvider): StoreApi<
169
172
  }
170
173
  },
171
174
 
175
+ async createQuickTransaction(input) {
176
+ try {
177
+ if (input.type === 'transfer') {
178
+ if (!input.bankAccountId || !input.toAccountId) {
179
+ throw new Error('Select source and destination accounts')
180
+ }
181
+ await provider.createTransfer({
182
+ fromAccountId: input.bankAccountId,
183
+ toAccountId: input.toAccountId,
184
+ amount: input.amount,
185
+ date: input.date,
186
+ notes: input.description?.trim() || undefined,
187
+ })
188
+ } else {
189
+ const direction: 'debit' | 'credit' = input.type === 'income' ? 'credit' : 'debit'
190
+ const desc = input.description?.trim() || (input.type === 'income' ? 'Receita' : 'Despesa')
191
+ // 1. The obligation (invoice + bill movement).
192
+ // Persist the recurring flag + the snapped-receipt data URL (FAY-1226)
193
+ // on the invoice metadata so the transaction feed can indicate/preview it.
194
+ const invoiceMeta: Record<string, unknown> = {}
195
+ if (input.recurring) invoiceMeta.recurring = true
196
+ if (input.receiptUrl) invoiceMeta.receiptUrl = input.receiptUrl
197
+ const invoice = await provider.createInvoice({
198
+ direction,
199
+ invoiceDate: input.date,
200
+ contactName: desc,
201
+ metadata: Object.keys(invoiceMeta).length > 0 ? invoiceMeta : undefined,
202
+ items: [{
203
+ itemKind: 'other',
204
+ description: desc,
205
+ quantity: 1,
206
+ unitPrice: input.amount,
207
+ accountId: input.categoryId,
208
+ }],
209
+ installments: [{
210
+ direction,
211
+ movementKind: 'bill',
212
+ amount: input.amount,
213
+ dueDate: input.date,
214
+ installmentNumber: 1,
215
+ bankAccountId: input.bankAccountId,
216
+ notes: desc,
217
+ }],
218
+ })
219
+ // 2. Settle it now when marked paid → a realized cash event that shows in
220
+ // the statement/extract and feeds the monthly cash-flow KPIs.
221
+ if (input.paid) {
222
+ const movements = await provider.getMovements({ direction, status: 'pending' })
223
+ const bill = movements.data.find((m) => m.invoiceId === invoice.id && m.movementKind === 'bill')
224
+ if (bill) {
225
+ await provider.payMovement({
226
+ movementId: bill.id,
227
+ amount: input.amount,
228
+ paymentDate: input.date,
229
+ bankAccountId: input.bankAccountId,
230
+ notes: desc,
231
+ })
232
+ }
233
+ }
234
+ }
235
+ // Refresh every surface this store feeds: Home/Resumo KPIs (summary),
236
+ // the mobile transaction list (statement), balances, and the payables/
237
+ // receivables tables (invoices).
238
+ const invoiceQuery = get().invoiceQuery
239
+ const statementQuery = get().statementQuery
240
+ const [summary, bankAccounts, invoicesRes, statement] = await Promise.all([
241
+ provider.getSummary(),
242
+ provider.getBankAccounts(),
243
+ provider.getInvoices(invoiceQuery),
244
+ statementQuery ? provider.getStatement(statementQuery) : Promise.resolve(get().statement),
245
+ ])
246
+ set({
247
+ summary,
248
+ bankAccounts,
249
+ invoices: invoicesRes.data,
250
+ invoicesTotal: invoicesRes.total,
251
+ statement,
252
+ })
253
+ toast.success('Transaction saved')
254
+ } catch (err: any) {
255
+ toast.error('Failed to save transaction', { description: err?.message })
256
+ throw err
257
+ }
258
+ },
259
+
172
260
  async payMovement(input) {
173
261
  try {
174
262
  await provider.payMovement(input)
package/src/types.ts CHANGED
@@ -199,6 +199,16 @@ export interface ChartOfAccountsNode {
199
199
  createdAt: string
200
200
  }
201
201
 
202
+ export interface CostCenter {
203
+ id: string
204
+ code: string
205
+ name: string
206
+ isActive: boolean
207
+ tenantId: string
208
+ createdAt: string
209
+ updatedAt: string
210
+ }
211
+
202
212
  export interface CardTransaction {
203
213
  id: string
204
214
  movementId: string
@@ -316,6 +326,8 @@ export interface InvoiceQuery {
316
326
  direction?: TransactionDirection
317
327
  status?: InvoiceStatus | InvoiceStatus[]
318
328
  contactId?: string
329
+ accountId?: string
330
+ costCenterId?: string
319
331
  dateRange?: DateRange
320
332
  search?: string
321
333
  page?: number
@@ -347,6 +359,39 @@ export interface CreateTransferInput {
347
359
  notes?: string
348
360
  }
349
361
 
362
+ /**
363
+ * "Log money in a few taps" (FAY-1225). A single-transaction quick-add that the
364
+ * store expands into the right provider calls: expense/income → createInvoice
365
+ * (+ payMovement when marked paid); transfer → createTransfer. Keeps the
366
+ * Mobills-style form dumb — it only collects fields, the store does the plumbing.
367
+ */
368
+ export type QuickTransactionType = 'expense' | 'income' | 'transfer'
369
+
370
+ export interface QuickTransactionInput {
371
+ type: QuickTransactionType
372
+ /** Positive amount in the account currency */
373
+ amount: number
374
+ /** YYYY-MM-DD */
375
+ date: string
376
+ /** When true, the movement is settled immediately (creates a paid cash event). */
377
+ paid: boolean
378
+ description?: string
379
+ /** Chart-of-accounts / cost-center id (category chip) */
380
+ categoryId?: string
381
+ /** expense/income: bank account or card. transfer: source account. */
382
+ bankAccountId?: string
383
+ /** transfer: destination account */
384
+ toAccountId?: string
385
+ /** Marks the transaction as recurring (stored in metadata; UI affordance). */
386
+ recurring?: boolean
387
+ /**
388
+ * FAY-1226 "snap a receipt": a captured/attached receipt image as a data URL
389
+ * (mock mode has no Supabase bucket). Persisted on the created invoice's
390
+ * metadata so the transaction feed can show a 📎 indicator + preview.
391
+ */
392
+ receiptUrl?: string
393
+ }
394
+
350
395
  export interface TransferResult {
351
396
  /** Shared correlation id stored in both legs' metadata */
352
397
  transferId: string
@@ -1,74 +1,210 @@
1
- import React, { useEffect } from 'react'
2
- import { CreditCard, Check } from 'lucide-react'
1
+ import React, { useEffect, useMemo, useState } from 'react'
2
+ import { CreditCard, Plus, CalendarClock } from 'lucide-react'
3
3
  import { useFinancialConfig, useFinancialStore, formatCurrency } from '../FinancialContext'
4
4
  import { SubpageHeader } from '@fayz-ai/ui'
5
5
  import { useTranslation } from '@fayz-ai/core'
6
+ import type { BankAccount } from '../types'
7
+ import { QuickTransactionForm } from './QuickTransactionForm'
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Per-card money math. Credit-card balances are stored as debt (negative), so
11
+ // the current bill (fatura) is the magnitude owed; the available limit is what
12
+ // remains of the credit line.
13
+ // ---------------------------------------------------------------------------
14
+
15
+ function cardMath(acc: BankAccount) {
16
+ const limit = acc.creditLimit ?? 0
17
+ const used = Math.max(0, -acc.currentBalance)
18
+ const available = Math.max(0, limit - used)
19
+ const pct = limit > 0 ? Math.min(100, (used / limit) * 100) : 0
20
+ return { limit, used, available, pct }
21
+ }
6
22
 
7
23
  export function CardsView() {
8
24
  const t = useTranslation()
9
- const { currency, labels } = useFinancialConfig()
10
- const cardTransactions = useFinancialStore((s) => s.cardTransactions)
11
- const cardsLoading = useFinancialStore((s) => s.cardsLoading)
12
- const fetchCardTransactions = useFinancialStore((s) => s.fetchCardTransactions)
13
-
14
- useEffect(() => { fetchCardTransactions() }, [])
15
-
16
- // Group by card brand
17
- const byBrand: Record<string, typeof cardTransactions> = {}
18
- for (const tx of cardTransactions) {
19
- const brand = tx.cardBrand || 'Other'
20
- if (!byBrand[brand]) byBrand[brand] = []
21
- byBrand[brand].push(tx)
25
+ const { currency } = useFinancialConfig()
26
+ const bankAccounts = useFinancialStore((s) => s.bankAccounts)
27
+ const bankAccountsLoading = useFinancialStore((s) => s.bankAccountsLoading)
28
+ const fetchBankAccounts = useFinancialStore((s) => s.fetchBankAccounts)
29
+
30
+ const [tab, setTab] = useState<'open' | 'closed'>('open')
31
+ const [addOpen, setAddOpen] = useState(false)
32
+ const [addCardId, setAddCardId] = useState<string | undefined>()
33
+
34
+ useEffect(() => { fetchBankAccounts() }, [])
35
+
36
+ const cards = useMemo(
37
+ () => bankAccounts.filter((a) => a.accountType === 'credit_card'),
38
+ [bankAccounts],
39
+ )
40
+ const openCards = useMemo(() => cards.filter((c) => c.isActive), [cards])
41
+ const closedCards = useMemo(() => cards.filter((c) => !c.isActive), [cards])
42
+ const visibleCards = tab === 'open' ? openCards : closedCards
43
+
44
+ // Summary strip is computed over the active (open) cards — the money that is
45
+ // actually in play this cycle.
46
+ const summary = useMemo(() => {
47
+ let totalAvailable = 0
48
+ let totalBills = 0
49
+ for (const c of openCards) {
50
+ const { available, used } = cardMath(c)
51
+ totalAvailable += available
52
+ totalBills += used
53
+ }
54
+ return { totalAvailable, totalBills }
55
+ }, [openCards])
56
+
57
+ function openAddExpense(cardId: string) {
58
+ setAddCardId(cardId)
59
+ setAddOpen(true)
22
60
  }
23
61
 
24
62
  return (
25
63
  <div className="space-y-4">
26
64
  <SubpageHeader title={t('financial.cards.title')} subtitle={t('financial.cards.subtitle')} />
27
- {cardsLoading ? (
65
+
66
+ {bankAccountsLoading && cards.length === 0 ? (
28
67
  <div className="text-center py-12 text-sm text-muted-foreground">Loading...</div>
29
- ) : cardTransactions.length === 0 ? (
30
- <div className="flex flex-col items-center justify-center py-12 text-center rounded-lg border-2 border-dashed border-muted">
31
- <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-indigo-50 dark:bg-indigo-500/20 mb-2">
32
- <CreditCard className="h-5 w-5 text-indigo-500" />
68
+ ) : cards.length === 0 ? (
69
+ <div className="flex flex-col items-center justify-center py-12 text-center rounded-2xl border-2 border-dashed border-muted">
70
+ <div className="flex h-12 w-12 items-center justify-center rounded-xl bg-gradient-to-br from-amber-300 to-yellow-600 mb-3 text-white shadow-sm">
71
+ <CreditCard className="h-6 w-6" />
33
72
  </div>
34
- <p className="text-sm text-muted-foreground">{t('financial.cards.noTransactions')}</p>
35
- <p className="text-xs text-muted-foreground mt-0.5">{t('financial.cards.transactionsAppear')}</p>
73
+ <p className="text-sm font-medium">{t('financial.cards.noCards')}</p>
74
+ <p className="text-xs text-muted-foreground mt-0.5 max-w-xs">{t('financial.cards.noCardsHint')}</p>
36
75
  </div>
37
76
  ) : (
38
- Object.entries(byBrand).map(([brand, txs]) => (
39
- <div key={brand} className="rounded-lg border bg-card shadow-sm">
40
- <div className="flex items-center gap-2 px-4 py-3 border-b">
41
- <CreditCard className="h-4 w-4 text-indigo-500" />
42
- <h3 className="text-sm font-semibold">{brand}</h3>
43
- <span className="text-xs text-muted-foreground">({txs.length} transactions)</span>
77
+ <>
78
+ {/* Summary strip — available limit + total bills across open cards */}
79
+ <div className="grid grid-cols-2 gap-px overflow-hidden rounded-2xl border bg-border shadow-sm">
80
+ <div className="bg-card px-4 py-3">
81
+ <p className="text-xs text-muted-foreground">{t('financial.cards.availableLimit')}</p>
82
+ <p className="mt-0.5 text-lg font-bold tabular-nums text-success">
83
+ {formatCurrency(summary.totalAvailable, currency)}
84
+ </p>
85
+ </div>
86
+ <div className="bg-card px-4 py-3">
87
+ <p className="text-xs text-muted-foreground">{t('financial.cards.allBillsValue')}</p>
88
+ <p className="mt-0.5 text-lg font-bold tabular-nums">
89
+ {formatCurrency(summary.totalBills, currency)}
90
+ </p>
44
91
  </div>
45
- <div className="divide-y">
46
- {txs.map((tx) => (
47
- <div key={tx.id} className="px-4 py-3 flex items-center justify-between">
48
- <div>
49
- <p className="text-sm font-medium">
50
- Installment {tx.currentInstallment}/{tx.installmentCount}
51
- </p>
52
- <p className="text-xs text-muted-foreground">Due: {tx.expectedDate}</p>
92
+ </div>
93
+
94
+ {/* Open / Closed tabs (only when there are closed cards to switch to) */}
95
+ {closedCards.length > 0 && (
96
+ <div className="flex gap-1 rounded-full bg-muted p-1 w-full max-w-xs">
97
+ {(['open', 'closed'] as const).map((k) => (
98
+ <button
99
+ key={k}
100
+ onClick={() => setTab(k)}
101
+ className={`flex-1 rounded-full px-3 py-1.5 text-xs font-medium transition-colors ${
102
+ tab === k ? 'bg-card text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'
103
+ }`}
104
+ >
105
+ {k === 'open' ? t('financial.cards.tabOpen') : t('financial.cards.tabClosed')}
106
+ </button>
107
+ ))}
108
+ </div>
109
+ )}
110
+
111
+ {/* Card tiles — stacked on mobile, grid on desktop */}
112
+ <div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
113
+ {visibleCards.map((card) => {
114
+ const { limit, used, available, pct } = cardMath(card)
115
+ const over = pct >= 90
116
+ // Per-brand color so cards match the bank (and the Home card), not a uniform gold.
117
+ const brand = `${card.bankName ?? ''} ${card.name ?? ''}`.toLowerCase()
118
+ const headerGrad =
119
+ brand.includes('nubank') || brand.includes('nu ') ? 'from-purple-500 via-purple-600 to-indigo-700'
120
+ : brand.includes('ita') ? 'from-orange-400 via-orange-500 to-amber-600'
121
+ : brand.includes('inter') ? 'from-orange-500 via-orange-600 to-orange-700'
122
+ : 'from-indigo-500 via-indigo-600 to-violet-700'
123
+ const barGrad =
124
+ brand.includes('nubank') || brand.includes('nu ') ? 'from-purple-500 to-indigo-600'
125
+ : brand.includes('ita') ? 'from-orange-400 to-amber-500'
126
+ : brand.includes('inter') ? 'from-orange-500 to-orange-600'
127
+ : 'from-indigo-500 to-violet-600'
128
+ return (
129
+ <div key={card.id} className="overflow-hidden rounded-2xl border bg-card shadow-sm">
130
+ {/* Per-brand header */}
131
+ <div className={`bg-gradient-to-br ${headerGrad} px-4 py-4 text-white`}>
132
+ <div className="flex items-start justify-between">
133
+ <div className="flex items-center gap-2">
134
+ <CreditCard className="h-5 w-5" />
135
+ <span className="text-sm font-semibold">{card.name}</span>
136
+ </div>
137
+ {card.bankName && (
138
+ <span className="text-[11px] font-medium uppercase tracking-wide opacity-80">
139
+ {card.bankName}
140
+ </span>
141
+ )}
142
+ </div>
143
+ <div className="mt-4">
144
+ <p className="text-[11px] uppercase tracking-wide opacity-80">
145
+ {t('financial.cards.currentBill')}
146
+ </p>
147
+ <p className="text-2xl font-bold tabular-nums leading-tight">
148
+ {formatCurrency(used, currency)}
149
+ </p>
150
+ </div>
53
151
  </div>
54
- <div className="flex items-center gap-3">
55
- <span className="text-sm font-medium">{formatCurrency(tx.installmentAmount, currency)}</span>
56
- {tx.status === 'received' ? (
57
- <span className="inline-flex items-center gap-1 rounded-full bg-success/15 px-2 py-0.5 text-[10px] font-medium text-success dark:bg-success/20">
58
- <Check className="h-2.5 w-2.5" /> Received
59
- </span>
60
- ) : (
61
- <span className="inline-flex items-center rounded-full bg-warning/15 px-2 py-0.5 text-[10px] font-medium text-warning dark:bg-warning/20">
62
- Pending
63
- </span>
64
- )}
152
+
153
+ {/* Body */}
154
+ <div className="space-y-3 px-4 py-3">
155
+ {/* Usage progress bar */}
156
+ <div>
157
+ <div className="flex items-center justify-between text-[11px] text-muted-foreground">
158
+ <span>{t('financial.cards.usedOfLimit', { percent: String(Math.round(pct)) })}</span>
159
+ <span className="tabular-nums">{formatCurrency(limit, currency)}</span>
160
+ </div>
161
+ <div className="mt-1 h-2 overflow-hidden rounded-full bg-muted">
162
+ <div
163
+ className={`h-full rounded-full ${
164
+ over ? 'bg-destructive' : `bg-gradient-to-r ${barGrad}`
165
+ }`}
166
+ style={{ width: `${pct}%` }}
167
+ />
168
+ </div>
169
+ </div>
170
+
171
+ {/* Available limit + due date */}
172
+ <div className="flex items-center justify-between">
173
+ <div>
174
+ <p className="text-[11px] text-muted-foreground">{t('financial.cards.availableLimit')}</p>
175
+ <p className="text-sm font-semibold tabular-nums">{formatCurrency(available, currency)}</p>
176
+ </div>
177
+ {card.dueDay != null && (
178
+ <div className="flex items-center gap-1.5 text-right text-xs text-muted-foreground">
179
+ <CalendarClock className="h-3.5 w-3.5" />
180
+ <span>{t('financial.cards.dueOn', { day: String(card.dueDay) })}</span>
181
+ </div>
182
+ )}
183
+ </div>
184
+
185
+ {/* Add expense against this card */}
186
+ <button
187
+ type="button"
188
+ onClick={() => openAddExpense(card.id)}
189
+ className="flex w-full items-center justify-center gap-1.5 rounded-input border border-input bg-card px-3 py-2 text-xs font-medium text-foreground transition-colors hover:bg-muted/40"
190
+ >
191
+ <Plus className="h-3.5 w-3.5" />
192
+ {t('financial.cards.addExpense')}
193
+ </button>
65
194
  </div>
66
195
  </div>
67
- ))}
68
- </div>
196
+ )
197
+ })}
69
198
  </div>
70
- ))
199
+ </>
71
200
  )}
201
+
202
+ <QuickTransactionForm
203
+ open={addOpen}
204
+ onOpenChange={setAddOpen}
205
+ defaultType="expense"
206
+ defaultAccountId={addCardId}
207
+ />
72
208
  </div>
73
209
  )
74
210
  }
@@ -2,11 +2,11 @@ import React, { useEffect, useState, useMemo } from 'react'
2
2
  import { FileText, CircleDashed, CircleEllipsis, CircleCheckBig, CircleAlert, Ban } from 'lucide-react'
3
3
  import type { ColumnDef } from '@tanstack/react-table'
4
4
  import { ListView } from '@fayz-ai/ui'
5
- import { useFinancialConfig, useFinancialStore, formatCurrency } from '../FinancialContext'
5
+ import { useFinancialConfig, useFinancialProvider, useFinancialStore, formatCurrency } from '../FinancialContext'
6
6
  import { SubpageHeader } from '@fayz-ai/ui'
7
7
  import { PersonLink } from '@fayz-ai/saas'
8
8
  import { useTranslation } from '@fayz-ai/core'
9
- import type { TransactionDirection, InvoiceStatus, Invoice } from '../types'
9
+ import type { TransactionDirection, InvoiceStatus, Invoice, ChartOfAccountsNode, CostCenter } from '../types'
10
10
 
11
11
  const STATUS_OPTIONS: { value: InvoiceStatus; labelKey: string; color: string; icon: React.ElementType }[] = [
12
12
  { value: 'open', labelKey: 'financial.invoice.statusOpen', color: 'bg-info-soft text-info-soft-foreground', icon: CircleDashed },
@@ -88,6 +88,7 @@ export function InvoiceListView({ direction, onNew, onEdit }: {
88
88
  }) {
89
89
  const t = useTranslation()
90
90
  const { currency } = useFinancialConfig()
91
+ const provider = useFinancialProvider()
91
92
  const invoices = useFinancialStore((s) => s.invoices)
92
93
  const invoicesTotal = useFinancialStore((s) => s.invoicesTotal)
93
94
  const invoicesLoading = useFinancialStore((s) => s.invoicesLoading)
@@ -95,14 +96,36 @@ export function InvoiceListView({ direction, onNew, onEdit }: {
95
96
 
96
97
  const [search, setSearch] = useState('')
97
98
  const [statusFilter, setStatusFilter] = useState<InvoiceStatus[]>([])
99
+ const [accountId, setAccountId] = useState('')
100
+ const [costCenterId, setCostCenterId] = useState('')
101
+ const [accounts, setAccounts] = useState<ChartOfAccountsNode[]>([])
102
+ const [costCenters, setCostCenters] = useState<CostCenter[]>([])
103
+
104
+ useEffect(() => {
105
+ let active = true
106
+ Promise.all([provider.getChartOfAccounts(), provider.getCostCenters()])
107
+ .then(([accountRows, costCenterRows]) => {
108
+ if (!active) return
109
+ setAccounts(accountRows.filter((account) => account.nodeType === 'leaf'))
110
+ setCostCenters(costCenterRows)
111
+ })
112
+ .catch(() => {
113
+ if (!active) return
114
+ setAccounts([])
115
+ setCostCenters([])
116
+ })
117
+ return () => { active = false }
118
+ }, [provider])
98
119
 
99
120
  useEffect(() => {
100
121
  fetchInvoices({
101
122
  direction,
102
123
  status: statusFilter.length > 0 ? statusFilter : undefined,
103
124
  search: search || undefined,
125
+ accountId: accountId || undefined,
126
+ costCenterId: costCenterId || undefined,
104
127
  })
105
- }, [direction, statusFilter, search])
128
+ }, [direction, statusFilter, search, accountId, costCenterId])
106
129
 
107
130
  const filtered = invoices.filter((inv) => inv.direction === direction)
108
131
  const listTitle = direction === 'debit' ? t('financial.invoice.accountsPayable') : t('financial.invoice.accountsReceivable')
@@ -135,6 +158,40 @@ export function InvoiceListView({ direction, onNew, onEdit }: {
135
158
  title={listTitle}
136
159
  subtitle={t('financial.invoice.invoices', { count: filtered.length })}
137
160
  />
161
+ {(accounts.length > 0 || costCenters.length > 0) && (
162
+ <div className="flex flex-wrap items-end gap-3 rounded-lg border bg-muted/20 px-4 py-3">
163
+ {accounts.length > 0 && (
164
+ <label className="flex min-w-[220px] flex-col gap-1 text-xs font-medium text-muted-foreground">
165
+ {t('financial.invoice.filterAccount')}
166
+ <select
167
+ className="h-9 rounded-md border bg-background px-3 text-sm text-foreground"
168
+ value={accountId}
169
+ onChange={(event) => setAccountId(event.target.value)}
170
+ >
171
+ <option value="">{t('financial.invoice.filterAllAccounts')}</option>
172
+ {accounts.map((account) => (
173
+ <option key={account.id} value={account.id}>{account.code} - {account.name}</option>
174
+ ))}
175
+ </select>
176
+ </label>
177
+ )}
178
+ {costCenters.length > 0 && (
179
+ <label className="flex min-w-[220px] flex-col gap-1 text-xs font-medium text-muted-foreground">
180
+ {t('financial.invoice.filterCostCenter')}
181
+ <select
182
+ className="h-9 rounded-md border bg-background px-3 text-sm text-foreground"
183
+ value={costCenterId}
184
+ onChange={(event) => setCostCenterId(event.target.value)}
185
+ >
186
+ <option value="">{t('financial.invoice.filterAllCostCenters')}</option>
187
+ {costCenters.map((costCenter) => (
188
+ <option key={costCenter.id} value={costCenter.id}>{costCenter.code} - {costCenter.name}</option>
189
+ ))}
190
+ </select>
191
+ </label>
192
+ )}
193
+ </div>
194
+ )}
138
195
  <ListView<Invoice>
139
196
  columns={columns}
140
197
  data={filtered}