@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,203 +0,0 @@
1
- import React, { useEffect, useMemo } from 'react'
2
- import {
3
- ShoppingCart, Car, Home, Utensils, Zap, Film, HeartPulse, Dumbbell,
4
- Music, ShoppingBag, Landmark, Wallet, CircleDollarSign, Paperclip,
5
- } from 'lucide-react'
6
- import { DashboardCanvas } from '@fayz-ai/ui'
7
- import { useTranslation } from '@fayz-ai/core'
8
- import { useFinancialConfig, useFinancialStore, formatCurrency } from '../FinancialContext'
9
- import type { StatementEntry } from '../types'
10
-
11
- /**
12
- * Financial summary. On desktop (md+) it renders the rich multi-column dashboard
13
- * (KPI cards + Fluxo de Caixa chart + breakdown + overdue alerts + recent-
14
- * transactions table) via the shared DashboardCanvas. On mobile (<md) it swaps
15
- * to a Mobills-grade experience: a compact KPI strip over a date-grouped
16
- * transaction list. The two paths share the same store — no forked screen.
17
- */
18
- export function SummaryView() {
19
- return (
20
- <>
21
- {/* Mobile: Mobills-style transaction-led summary */}
22
- <div className="md:hidden">
23
- <MobileSummary />
24
- </div>
25
- {/* Desktop: rich dashboard canvas (unchanged) */}
26
- <DashboardCanvas
27
- surface="plugin-home"
28
- domain="financial"
29
- showHeader={false}
30
- className="hidden space-y-6 md:block"
31
- />
32
- </>
33
- )
34
- }
35
-
36
- // ---------------------------------------------------------------------------
37
- // Mobile summary (Mobills-grade)
38
- // ---------------------------------------------------------------------------
39
-
40
- // keyword → category glyph, so each row leads with a recognizable colored circle
41
- // (Mobills-style). Purely cosmetic and safe while categories aren't modeled yet;
42
- // falls back to a generic money glyph.
43
- function categoryIcon(label: string): React.ElementType {
44
- const s = label.toLowerCase()
45
- if (/(mercado|super|padaria|extra|p[aã]o|hortifr)/.test(s)) return ShoppingCart
46
- if (/(uber|99|posto|shell|gasol|combust|carro)/.test(s)) return Car
47
- if (/(aluguel|casa|condom|im[oó]vel)/.test(s)) return Home
48
- if (/(ifood|restaurante|outback|lanche|food|bar\b)/.test(s)) return Utensils
49
- if (/(luz|energia|enel|internet|vivo|conta de|[aá]gua)/.test(s)) return Zap
50
- if (/(cinema|netflix|cinemark|filme|disney|prime)/.test(s)) return Film
51
- if (/(farm|drogasil|sa[uú]de|plano|hospital|m[eé]dic)/.test(s)) return HeartPulse
52
- if (/(academia|smartfit|gym|treino)/.test(s)) return Dumbbell
53
- if (/(spotify|music|deezer)/.test(s)) return Music
54
- if (/(amazon|shopping|loja|magaz|americanas|mercado livre)/.test(s)) return ShoppingBag
55
- if (/(fatura|cart[aã]o|nubank|banco|boleto)/.test(s)) return Landmark
56
- if (/(sal[aá]rio|freel|pix|reembolso|receb|renda|d[ií]vidend)/.test(s)) return Wallet
57
- return CircleDollarSign
58
- }
59
-
60
- function useDateLabel() {
61
- const t = useTranslation()
62
- const { currency } = useFinancialConfig()
63
- return useMemo(() => {
64
- const todayStr = new Date().toISOString().slice(0, 10)
65
- const y = new Date(); y.setDate(y.getDate() - 1)
66
- const yesterdayStr = y.toISOString().slice(0, 10)
67
- return (dateStr: string): string => {
68
- if (dateStr === todayStr) return t('financial.summary.today')
69
- if (dateStr === yesterdayStr) return t('financial.summary.yesterday')
70
- try {
71
- return new Date(`${dateStr}T00:00:00`).toLocaleDateString(currency.locale, {
72
- weekday: 'short', day: '2-digit', month: 'short',
73
- })
74
- } catch {
75
- return dateStr
76
- }
77
- }
78
- }, [t, currency.locale])
79
- }
80
-
81
- function MobileSummary() {
82
- const t = useTranslation()
83
- const { currency } = useFinancialConfig()
84
- const summary = useFinancialStore((s) => s.summary)
85
- const statement = useFinancialStore((s) => s.statement)
86
- const bankAccounts = useFinancialStore((s) => s.bankAccounts)
87
- const fetchSummary = useFinancialStore((s) => s.fetchSummary)
88
- const fetchBankAccounts = useFinancialStore((s) => s.fetchBankAccounts)
89
- const fetchStatement = useFinancialStore((s) => s.fetchStatement)
90
- const dateLabel = useDateLabel()
91
-
92
- useEffect(() => {
93
- void fetchSummary()
94
- void fetchBankAccounts()
95
- const to = new Date().toISOString().slice(0, 10)
96
- const from = new Date(); from.setDate(from.getDate() - 90)
97
- void fetchStatement({ dateRange: { from: from.toISOString().slice(0, 10), to } })
98
- }, [])
99
-
100
- const fmt = (n: number) => formatCurrency(n, currency)
101
- const inflow = summary?.monthlyInflow ?? 0
102
- const outflow = summary?.monthlyOutflow ?? 0
103
-
104
- const accountName = useMemo(() => {
105
- const map = new Map(bankAccounts.map((a) => [a.id, a.name]))
106
- return (id?: string) => (id ? map.get(id) ?? '' : '')
107
- }, [bankAccounts])
108
-
109
- // Realized cash events grouped by day (most recent first). Transfers are
110
- // dropped — they're internal and would double-count in a personal feed.
111
- const groups = useMemo(() => {
112
- const entries = (statement?.entries ?? []).filter((e) => e.entryKind === 'movement')
113
- const byDate = new Map<string, StatementEntry[]>()
114
- for (const e of entries) {
115
- const d = e.movement.paymentDate ?? e.movement.dueDate
116
- const bucket = byDate.get(d)
117
- if (bucket) bucket.push(e)
118
- else byDate.set(d, [e])
119
- }
120
- return [...byDate.entries()].sort((a, b) => b[0].localeCompare(a[0]))
121
- }, [statement])
122
-
123
- return (
124
- <div className="space-y-4">
125
- {/* Compact KPI strip */}
126
- <div className="grid grid-cols-2 gap-2">
127
- <div className="col-span-2 rounded-xl border bg-card p-4">
128
- <p className="text-xs text-muted-foreground">{t('financial.summary.totalBalance')}</p>
129
- <p className="mt-0.5 text-2xl font-semibold">{fmt(summary?.totalBalance ?? 0)}</p>
130
- </div>
131
- <div className="rounded-xl border bg-card p-3">
132
- <p className="text-[11px] text-muted-foreground">{t('financial.summary.income')}</p>
133
- <p className="mt-0.5 text-base font-semibold text-success">{fmt(inflow)}</p>
134
- </div>
135
- <div className="rounded-xl border bg-card p-3">
136
- <p className="text-[11px] text-muted-foreground">{t('financial.summary.expenses')}</p>
137
- <p className="mt-0.5 text-base font-semibold text-destructive">{fmt(outflow)}</p>
138
- </div>
139
- </div>
140
-
141
- {/* Date-grouped transaction list */}
142
- {groups.length === 0 ? (
143
- <div className="rounded-xl border border-dashed py-12 text-center text-sm text-muted-foreground">
144
- {t('financial.summary.noTransactions')}
145
- </div>
146
- ) : (
147
- <div className="space-y-4">
148
- {groups.map(([date, rows]) => (
149
- <div key={date} className="space-y-1.5">
150
- <p className="px-1 text-xs font-medium uppercase tracking-wide text-muted-foreground">
151
- {dateLabel(date)}
152
- </p>
153
- <div className="divide-y overflow-hidden rounded-xl border bg-card">
154
- {rows.map((e) => {
155
- const isCredit = e.movement.direction === 'credit'
156
- const title = e.invoice?.contactName || e.movement.notes || t('financial.summary.transactionsTitle')
157
- const Icon = categoryIcon(title)
158
- const category = isCredit ? t('financial.summary.income') : t('financial.summary.expenses')
159
- const acct = accountName(e.movement.bankAccountId)
160
- const subtitle = acct ? `${category} · ${acct}` : category
161
- // FAY-1226: a snapped receipt lives on the invoice metadata.
162
- const receiptUrl = (e.invoice?.metadata as { receiptUrl?: string } | undefined)?.receiptUrl
163
- return (
164
- <div key={e.movement.id} className="flex items-center gap-3 px-3 py-2.5">
165
- <span
166
- className={`flex h-9 w-9 shrink-0 items-center justify-center rounded-full ${
167
- isCredit
168
- ? 'bg-success-soft text-success-soft-foreground'
169
- : 'bg-destructive-soft text-destructive-soft-foreground'
170
- }`}
171
- >
172
- <Icon className="h-4 w-4" />
173
- </span>
174
- <div className="min-w-0 flex-1">
175
- <p className="flex items-center gap-1.5 truncate text-sm font-medium">
176
- {title}
177
- {receiptUrl && (
178
- <Paperclip className="h-3 w-3 shrink-0 text-muted-foreground" aria-label={t('financial.quickTx.receiptAttached')} />
179
- )}
180
- </p>
181
- <p className="truncate text-xs text-muted-foreground">{subtitle}</p>
182
- </div>
183
- {receiptUrl && (
184
- <img
185
- src={receiptUrl}
186
- alt={t('financial.quickTx.receiptAttached')}
187
- className="h-9 w-9 shrink-0 rounded-md border object-cover"
188
- />
189
- )}
190
- <span className={`shrink-0 text-sm font-semibold ${isCredit ? 'text-success' : 'text-destructive'}`}>
191
- {isCredit ? '+' : '-'}{fmt(e.net)}
192
- </span>
193
- </div>
194
- )
195
- })}
196
- </div>
197
- </div>
198
- ))}
199
- </div>
200
- )}
201
- </div>
202
- )
203
- }
@@ -1,437 +0,0 @@
1
- import React, { useEffect } from 'react'
2
- import { ArrowUpRight, ArrowDownRight, AlertTriangle, CreditCard as CreditCardIcon, Eye, EyeOff } from 'lucide-react'
3
- import type { ColumnDef } from '@tanstack/react-table'
4
- import type { StoreApi } from 'zustand'
5
- import type { DashboardWidgetDef } from '@fayz-ai/core'
6
- import { useTranslation } from '@fayz-ai/core'
7
- import {
8
- KpiCard, ChartWidget, TableWidget, Card, CardHeader, CardTitle, CardContent, GoldCard,
9
- defineKpiWidget, defineChartWidget, defineCustomWidget, defineTableWidget, useDashboardNavigate,
10
- } from '@fayz-ai/ui'
11
- import { FinancialContextProvider, useFinancialConfig, useFinancialProvider, useFinancialStore, formatCurrency, type ResolvedFinancialConfig } from '../FinancialContext'
12
- import type { FinancialDataProvider } from '../data/types'
13
- import type { FinancialUIState } from '../store'
14
- import type { Invoice, FinancialMovement } from '../types'
15
-
16
- function useEnsureSummary() {
17
- const fetchSummary = useFinancialStore((s) => s.fetchSummary)
18
- useEffect(() => { void fetchSummary() }, [])
19
- }
20
-
21
- // ---------------------------------------------------------------------------
22
- // KPIs
23
- // ---------------------------------------------------------------------------
24
-
25
- function TotalBalanceKpi() {
26
- const t = useTranslation()
27
- const { currency } = useFinancialConfig()
28
- const summary = useFinancialStore((s) => s.summary)
29
- const loading = useFinancialStore((s) => s.summaryLoading)
30
- useEnsureSummary()
31
- return <KpiCard label={t('financial.summary.totalBalance')} icon="Wallet" value={formatCurrency(summary?.totalBalance ?? 0, currency)} sub={loading ? undefined : t('financial.summary.allAccounts')} />
32
- }
33
-
34
- function ReceivableKpi() {
35
- const t = useTranslation()
36
- const { currency } = useFinancialConfig()
37
- const summary = useFinancialStore((s) => s.summary)
38
- useEnsureSummary()
39
- return <KpiCard label={t('financial.summary.receivable')} icon="TrendingUp" value={formatCurrency(summary?.totalReceivable ?? 0, currency)} sub={t('financial.summary.overdue', { count: String(summary?.overdueReceivableCount ?? 0) })} />
40
- }
41
-
42
- function PayableKpi() {
43
- const t = useTranslation()
44
- const { currency } = useFinancialConfig()
45
- const summary = useFinancialStore((s) => s.summary)
46
- useEnsureSummary()
47
- return <KpiCard label={t('financial.summary.payable')} icon="TrendingDown" value={formatCurrency(summary?.totalPayable ?? 0, currency)} sub={t('financial.summary.overdue', { count: String(summary?.overduePayableCount ?? 0) })} />
48
- }
49
-
50
- function MonthlyFlowKpi() {
51
- const t = useTranslation()
52
- const { currency } = useFinancialConfig()
53
- const summary = useFinancialStore((s) => s.summary)
54
- useEnsureSummary()
55
- const inflow = summary?.monthlyInflow ?? 0
56
- const outflow = summary?.monthlyOutflow ?? 0
57
- return <KpiCard label={t('financial.summary.monthlyFlow')} icon="BarChart3" value={formatCurrency(inflow - outflow, currency)} sub={inflow >= outflow ? t('financial.summary.positiveBalance') : t('financial.summary.negativeBalance')} />
58
- }
59
-
60
- // ---------------------------------------------------------------------------
61
- // Cash flow (recharts) + breakdown + overdue alerts
62
- // ---------------------------------------------------------------------------
63
-
64
- function CashFlowChart() {
65
- const t = useTranslation()
66
- const summary = useFinancialStore((s) => s.summary)
67
- useEnsureSummary()
68
- const data = [{ name: t('financial.summary.cashFlow'), income: summary?.monthlyInflow ?? 0, expenses: summary?.monthlyOutflow ?? 0 }]
69
- return (
70
- <ChartWidget
71
- type="bar" title={t('financial.summary.cashFlow')} icon="BarChart3" categoryKey="name" data={data}
72
- series={[
73
- { dataKey: 'income', label: t('financial.summary.income'), color: 'hsl(var(--success))' },
74
- { dataKey: 'expenses', label: t('financial.summary.expenses'), color: 'hsl(var(--destructive))' },
75
- ]}
76
- />
77
- )
78
- }
79
-
80
- function CashFlowBreakdown() {
81
- const t = useTranslation()
82
- const { currency } = useFinancialConfig()
83
- const summary = useFinancialStore((s) => s.summary)
84
- useEnsureSummary()
85
- const inflow = summary?.monthlyInflow ?? 0
86
- const outflow = summary?.monthlyOutflow ?? 0
87
- const total = Math.max(inflow + outflow, 1)
88
- return (
89
- <Card>
90
- <CardHeader><CardTitle>{t('financial.summary.cashFlowBreakdown')}</CardTitle></CardHeader>
91
- <CardContent className="space-y-4">
92
- <div className="space-y-1.5">
93
- <div className="flex items-center justify-between text-xs">
94
- <span className="flex items-center gap-1.5 text-muted-foreground"><span className="h-2 w-2 rounded-full bg-success" />{t('financial.summary.received')}</span>
95
- <span className="font-medium">{formatCurrency(inflow, currency)}</span>
96
- </div>
97
- <div className="h-2 overflow-hidden rounded-full bg-muted"><div className="h-full rounded-full bg-success" style={{ width: `${(inflow / total) * 100}%` }} /></div>
98
- </div>
99
- <div className="space-y-1.5">
100
- <div className="flex items-center justify-between text-xs">
101
- <span className="flex items-center gap-1.5 text-muted-foreground"><span className="h-2 w-2 rounded-full bg-destructive" />{t('financial.summary.paid')}</span>
102
- <span className="font-medium">{formatCurrency(outflow, currency)}</span>
103
- </div>
104
- <div className="h-2 overflow-hidden rounded-full bg-muted"><div className="h-full rounded-full bg-destructive" style={{ width: `${(outflow / total) * 100}%` }} /></div>
105
- </div>
106
- <div className="border-t pt-2">
107
- <div className="flex items-center justify-between text-xs">
108
- <span className="text-muted-foreground">{t('financial.summary.net')}</span>
109
- <span className={`font-semibold ${inflow - outflow >= 0 ? 'text-success' : 'text-destructive'}`}>{formatCurrency(inflow - outflow, currency)}</span>
110
- </div>
111
- </div>
112
- </CardContent>
113
- </Card>
114
- )
115
- }
116
-
117
- function OverdueAlerts() {
118
- const t = useTranslation()
119
- const { currency } = useFinancialConfig()
120
- const summary = useFinancialStore((s) => s.summary)
121
- useEnsureSummary()
122
- const items = [
123
- { label: t('financial.summary.overdueReceivable'), value: summary?.overdueReceivableAmount ?? 0, count: summary?.overdueReceivableCount ?? 0, Icon: ArrowUpRight, color: 'text-warning' },
124
- { label: t('financial.summary.overduePayable'), value: summary?.overduePayableAmount ?? 0, count: summary?.overduePayableCount ?? 0, Icon: ArrowDownRight, color: 'text-destructive' },
125
- ]
126
- return (
127
- <Card>
128
- <CardHeader><CardTitle className="flex items-center gap-2"><AlertTriangle className="h-4 w-4 text-warning" />{t('financial.summary.overdueTitle')}</CardTitle></CardHeader>
129
- <CardContent className="space-y-3">
130
- {items.map((item) => (
131
- <div key={item.label} className="flex items-center justify-between">
132
- <div className="flex items-center gap-2"><item.Icon className={`h-3.5 w-3.5 ${item.color}`} /><span className="text-sm text-muted-foreground">{item.label}</span></div>
133
- <span className="text-sm font-semibold">{formatCurrency(item.value, currency)}</span>
134
- </div>
135
- ))}
136
- {items.every((i) => i.count === 0) ? <p className="border-t pt-3 text-xs text-muted-foreground">{t('financial.summary.noOverdue')}</p> : null}
137
- </CardContent>
138
- </Card>
139
- )
140
- }
141
-
142
- function RecentTransactions() {
143
- const t = useTranslation()
144
- const { currency } = useFinancialConfig()
145
- const invoices = useFinancialStore((s) => s.invoices)
146
- const fetchInvoices = useFinancialStore((s) => s.fetchInvoices)
147
- useEffect(() => { void fetchInvoices({ pageSize: 5 }) }, [])
148
- const columns: ColumnDef<Invoice, unknown>[] = [
149
- { accessorKey: 'contactName', header: t('financial.summary.recentTransactions'), cell: ({ row }) => <span className="font-medium">{row.original.contactName || 'Unknown'}</span> },
150
- { id: 'date', header: '', cell: ({ row }) => <span className="text-xs capitalize text-muted-foreground">{row.original.direction} · {row.original.invoiceDate}</span> },
151
- { accessorKey: 'totalAmount', header: '', cell: ({ row }) => (
152
- <span className={`font-semibold ${row.original.direction === 'credit' ? 'text-success' : 'text-destructive'}`}>
153
- {row.original.direction === 'credit' ? '+' : '-'}{formatCurrency(row.original.totalAmount, currency)}
154
- </span>
155
- ) },
156
- ]
157
- return <TableWidget title={t('financial.summary.recentTransactions')} icon="CircleDollarSign" columns={columns} data={invoices.slice(0, 5)} emptyMessage={t('financial.summary.noTransactions')} />
158
- }
159
-
160
- // ---------------------------------------------------------------------------
161
- // HOME surface (B2C) — a phone-first money dashboard that reflows into cells on
162
- // desktop. These render on surface="home" only; the plugin-home Resumo widgets
163
- // above are untouched. Same store/provider, so figures match the Financeiro
164
- // module (FAY-1233 mobile-summary logic, generalized into reusable widgets).
165
- // ---------------------------------------------------------------------------
166
-
167
- /** Human "due in N days" label from a YYYY-MM-DD due date. */
168
- function useDueLabel() {
169
- const t = useTranslation()
170
- return (dueDate: string): string => {
171
- const today = new Date()
172
- today.setHours(0, 0, 0, 0)
173
- const due = new Date(`${dueDate}T00:00:00`)
174
- const days = Math.round((due.getTime() - today.getTime()) / 86_400_000)
175
- if (days < 0) return t('financial.home.overdueDays', { count: String(Math.abs(days)) })
176
- if (days === 0) return t('financial.home.dueToday')
177
- if (days === 1) return t('financial.home.dueTomorrow')
178
- return t('financial.home.dueInDays', { count: String(days) })
179
- }
180
- }
181
-
182
- /** Branded balance hero — Saldo + eye-toggle + clickable Entradas/Saídas pills.
183
- * Compact band (span-4): tight padding so it never becomes a tall empty block on
184
- * desktop. Pills deep-link into the Financeiro module (receivables / payables). */
185
- function BalanceHero() {
186
- const t = useTranslation()
187
- const { currency } = useFinancialConfig()
188
- const summary = useFinancialStore((s) => s.summary)
189
- const navigate = useDashboardNavigate()
190
- useEnsureSummary()
191
- const [hidden, setHidden] = React.useState(false)
192
- const money = (v: number | null | undefined) => (hidden ? '••••••' : formatCurrency(v ?? 0, currency))
193
- const inflow = summary?.monthlyInflow ?? 0
194
- const outflow = summary?.monthlyOutflow ?? 0
195
- return (
196
- <GoldCard branded className="px-5 py-4 text-center md:px-8 md:py-5">
197
- <p className="text-xs font-medium uppercase tracking-wide opacity-75">{t('financial.home.balance')}</p>
198
- <div className="mt-0.5 flex items-center justify-center gap-2.5">
199
- <span className="text-3xl font-bold tracking-tight tabular-nums md:text-4xl">{money(summary?.totalBalance)}</span>
200
- <button
201
- type="button"
202
- onClick={() => setHidden((h) => !h)}
203
- aria-label={hidden ? t('financial.home.showBalance') : t('financial.home.hideBalance')}
204
- className="rounded-full p-1.5 text-primary-foreground/80 transition-colors hover:bg-white/10"
205
- >
206
- {hidden ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
207
- </button>
208
- </div>
209
- <div className="mx-auto mt-3 grid max-w-lg grid-cols-2 gap-3">
210
- <button
211
- type="button"
212
- onClick={() => navigate('/financial/receivables-list')}
213
- aria-label={t('financial.home.inflow')}
214
- className="flex cursor-pointer items-center gap-2 rounded-2xl bg-white/10 px-3 py-2 text-left transition-colors hover:bg-white/20"
215
- >
216
- <span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-emerald-500/90">
217
- <ArrowUpRight className="h-4 w-4 text-white" />
218
- </span>
219
- <span className="min-w-0">
220
- <span className="block text-[11px] uppercase tracking-wide opacity-70">{t('financial.home.inflow')}</span>
221
- <span className="block truncate text-sm font-semibold tabular-nums">{money(inflow)}</span>
222
- </span>
223
- </button>
224
- <button
225
- type="button"
226
- onClick={() => navigate('/financial/payables-list')}
227
- aria-label={t('financial.home.outflow')}
228
- className="flex cursor-pointer items-center gap-2 rounded-2xl bg-white/10 px-3 py-2 text-left transition-colors hover:bg-white/20"
229
- >
230
- <span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-rose-500/90">
231
- <ArrowDownRight className="h-4 w-4 text-white" />
232
- </span>
233
- <span className="min-w-0">
234
- <span className="block text-[11px] uppercase tracking-wide opacity-70">{t('financial.home.outflow')}</span>
235
- <span className="block truncate text-sm font-semibold tabular-nums">{money(outflow)}</span>
236
- </span>
237
- </button>
238
- </div>
239
- </GoldCard>
240
- )
241
- }
242
-
243
- // ---------------------------------------------------------------------------
244
- // HOME charts — spend-by-category (donut) + cash-flow (in vs out). Both read the
245
- // same provider/store as the module, so figures stay coherent. surface 'home'
246
- // only (the plugin-home Resumo keeps its own cash-flow chart untouched).
247
- // ---------------------------------------------------------------------------
248
-
249
- /** Provider-agnostic expense classifier: maps a movement's label to a spend
250
- * bucket by keyword (pt-BR + en). Checked in priority order; first hit wins. */
251
- const SPEND_CATEGORIES: Array<{ key: string; keywords: string[] }> = [
252
- { key: 'subscriptions', keywords: ['netflix', 'spotify', 'prime video', 'disney', 'hbo', ' max', 'youtube', 'apple', 'assinatura', 'subscription'] },
253
- { key: 'groceries', keywords: ['supermercado', 'mercado', 'padaria', 'hortifruti', 'feira', 'açougue', 'grocery', 'market'] },
254
- { key: 'transport', keywords: ['uber', '99', 'taxi', 'posto', 'shell', 'ipiranga', 'combustível', 'gasolina', 'metrô', 'ônibus', 'transport', 'fuel'] },
255
- { key: 'dining', keywords: ['ifood', 'rappi', 'restaurante', 'outback', 'lanchonete', 'pizzaria', 'café', 'cafeteria', 'mcdonald', 'burger', 'food'] },
256
- { key: 'home', keywords: ['aluguel', 'condomínio', 'luz', 'enel', 'água', 'sabesp', 'internet', 'vivo', 'claro', 'gás', 'iptu', 'rent', 'utilit'] },
257
- { key: 'health', keywords: ['farmácia', 'farmacia', 'drogasil', 'drogaria', 'academia', 'smartfit', 'clínica', 'hospital', 'médico', 'saúde', 'gym', 'fitness', 'health', 'pharmac'] },
258
- { key: 'leisure', keywords: ['cinema', 'cinemark', 'ingresso', 'show', 'teatro', 'viagem', 'hotel', 'steam', 'leisure'] },
259
- { key: 'shopping', keywords: ['amazon', 'magalu', 'shopee', 'aliexpress', 'loja', 'shopping', 'zara', 'renner', 'store'] },
260
- ]
261
-
262
- function categorizeSpend(notes: string | undefined): string {
263
- const s = (notes ?? '').toLowerCase()
264
- for (const c of SPEND_CATEGORIES) if (c.keywords.some((k) => s.includes(k))) return c.key
265
- return 'other'
266
- }
267
-
268
- /** "Gastos por categoria" — realized expenses grouped into a donut. */
269
- function SpendByCategoryChart() {
270
- const t = useTranslation()
271
- const provider = useFinancialProvider()
272
- const [rows, setRows] = React.useState<Array<{ name: string; value: number }>>([])
273
- useEffect(() => {
274
- let alive = true
275
- void (async () => {
276
- const res = await provider.getMovements({ status: 'paid', direction: 'debit' })
277
- if (!alive) return
278
- const totals = new Map<string, number>()
279
- for (const m of res.data) {
280
- if (m.movementKind === 'transfer') continue
281
- const key = categorizeSpend(m.notes)
282
- totals.set(key, (totals.get(key) ?? 0) + (m.paidAmount || m.amount))
283
- }
284
- setRows(
285
- [...totals.entries()]
286
- .sort((a, b) => b[1] - a[1])
287
- .map(([key, value]) => ({ name: t(`financial.home.category.${key}`), value: Math.round(value * 100) / 100 })),
288
- )
289
- })()
290
- return () => { alive = false }
291
- }, [provider])
292
- if (rows.length === 0) {
293
- return (
294
- <Card>
295
- <CardHeader><CardTitle>{t('financial.home.spendByCategory')}</CardTitle></CardHeader>
296
- <CardContent className="flex h-[200px] items-center justify-center text-sm text-muted-foreground">
297
- {t('financial.home.noSpend')}
298
- </CardContent>
299
- </Card>
300
- )
301
- }
302
- return (
303
- <ChartWidget
304
- type="pie" title={t('financial.home.spendByCategory')} icon="PieChart" categoryKey="name" data={rows}
305
- series={[{ dataKey: 'value', label: t('financial.home.spendByCategory') }]}
306
- />
307
- )
308
- }
309
-
310
- /** "Próximas contas" — pending payable bills, soonest due first. */
311
- function UpcomingBills() {
312
- const t = useTranslation()
313
- const { currency } = useFinancialConfig()
314
- const provider = useFinancialProvider()
315
- const dueLabel = useDueLabel()
316
- const [bills, setBills] = React.useState<FinancialMovement[]>([])
317
- useEffect(() => {
318
- let alive = true
319
- void (async () => {
320
- const pending = await provider.getMovements({ status: 'pending' })
321
- if (!alive) return
322
- setBills(
323
- pending.data
324
- .filter((m) => m.direction === 'debit' && m.movementKind === 'bill')
325
- .sort((a, b) => a.dueDate.localeCompare(b.dueDate)),
326
- )
327
- })()
328
- return () => { alive = false }
329
- }, [provider])
330
- return (
331
- <GoldCard className="p-0 overflow-hidden">
332
- <div className="flex items-center justify-between px-4 pt-4">
333
- <h2 className="text-sm font-semibold text-foreground">{t('financial.home.upcomingBills')}</h2>
334
- </div>
335
- <div className="mt-3">
336
- {bills.length === 0 ? (
337
- <div className="px-4 py-6 text-center text-xs text-muted-foreground">{t('financial.home.noBills')}</div>
338
- ) : (
339
- bills.map((bill, i) => (
340
- <div key={bill.id} className={'flex items-center justify-between px-4 py-3.5' + (i > 0 ? ' border-t border-border' : '')}>
341
- <div className="min-w-0">
342
- <p className="truncate text-sm font-medium text-foreground">{bill.notes ?? t('financial.home.bill')}</p>
343
- <p className="text-xs text-muted-foreground">{dueLabel(bill.dueDate)}</p>
344
- </div>
345
- <span className="shrink-0 text-sm font-semibold tabular-nums text-foreground">
346
- {formatCurrency(bill.amount - bill.paidAmount, currency)}
347
- </span>
348
- </div>
349
- ))
350
- )}
351
- </div>
352
- </GoldCard>
353
- )
354
- }
355
-
356
- /** "Cartões" — credit-card accounts with current bill + available limit. */
357
- function CreditCards() {
358
- const t = useTranslation()
359
- const { currency } = useFinancialConfig()
360
- const bankAccounts = useFinancialStore((s) => s.bankAccounts)
361
- const fetchBankAccounts = useFinancialStore((s) => s.fetchBankAccounts)
362
- useEffect(() => { void fetchBankAccounts() }, [])
363
- const cards = bankAccounts.filter((a) => a.accountType === 'credit_card')
364
- return (
365
- <GoldCard className="bg-transparent border-0 p-0 shadow-none">
366
- <h2 className="mb-3 text-sm font-semibold text-foreground">{t('financial.home.cards')}</h2>
367
- <div className="space-y-3">
368
- {cards.map((card) => {
369
- const fatura = Math.abs(Math.min(0, card.currentBalance))
370
- const limite = Math.max(0, (card.creditLimit ?? 0) - fatura)
371
- return (
372
- <div key={card.id} className="rounded-2xl bg-gradient-to-br from-violet-600 to-indigo-700 p-4 text-white shadow-md">
373
- <div className="flex items-center justify-between">
374
- <span className="text-sm font-semibold">{card.bankName ?? card.name}</span>
375
- <CreditCardIcon className="h-5 w-5 opacity-80" />
376
- </div>
377
- <p className="mt-6 text-[11px] uppercase tracking-wide opacity-70">{t('financial.home.currentBill')}</p>
378
- <p className="text-xl font-bold tabular-nums">{formatCurrency(fatura, currency)}</p>
379
- <p className="mt-2 text-[11px] opacity-70">{t('financial.home.availableLimit')} {formatCurrency(limite, currency)}</p>
380
- </div>
381
- )
382
- })}
383
- {cards.length === 0 ? (
384
- <div className="flex items-center justify-center rounded-2xl border border-dashed border-border py-6 text-sm font-medium text-muted-foreground">
385
- + {t('financial.home.addCard')}
386
- </div>
387
- ) : null}
388
- </div>
389
- </GoldCard>
390
- )
391
- }
392
-
393
- // ---------------------------------------------------------------------------
394
- // Registry builder
395
- // ---------------------------------------------------------------------------
396
-
397
- export function createFinancialDashboardWidgets(ctx: {
398
- config: ResolvedFinancialConfig
399
- provider: FinancialDataProvider
400
- store: StoreApi<FinancialUIState>
401
- }): DashboardWidgetDef[] {
402
- const withCtx = (Inner: React.ComponentType): React.ComponentType<unknown> => {
403
- const Wrapped = () => (
404
- <FinancialContextProvider config={ctx.config} provider={ctx.provider} store={ctx.store}>
405
- <Inner />
406
- </FinancialContextProvider>
407
- )
408
- Wrapped.displayName = `FinancialWidget(${Inner.displayName ?? Inner.name})`
409
- return Wrapped
410
- }
411
-
412
- return [
413
- // Total balance is finance's headline KPI on the global home; the rest are
414
- // home-hidden by default (shown on the financial plugin-home, addable via Customize).
415
- defineKpiWidget({ id: 'financial.kpi.total-balance', title: 'financial.summary.totalBalance', domain: 'financial', defaultOrder: 0, component: withCtx(TotalBalanceKpi) }),
416
- defineKpiWidget({ id: 'financial.kpi.receivable', title: 'financial.summary.receivable', domain: 'financial', defaultOrder: 1, defaultVisible: false, component: withCtx(ReceivableKpi) }),
417
- defineKpiWidget({ id: 'financial.kpi.payable', title: 'financial.summary.payable', domain: 'financial', defaultOrder: 2, defaultVisible: false, component: withCtx(PayableKpi) }),
418
- defineKpiWidget({ id: 'financial.kpi.monthly-flow', title: 'financial.summary.monthlyFlow', domain: 'financial', defaultOrder: 3, defaultVisible: false, component: withCtx(MonthlyFlowKpi) }),
419
- defineChartWidget({ id: 'financial.chart.cash-flow', title: 'financial.summary.cashFlow', domain: 'financial', span: 2, defaultOrder: 10, surfaces: ['plugin-home'], component: withCtx(CashFlowChart) }),
420
- defineCustomWidget({ id: 'financial.panel.breakdown', title: 'financial.summary.cashFlowBreakdown', domain: 'financial', span: 1, defaultOrder: 11, surfaces: ['plugin-home'], component: withCtx(CashFlowBreakdown) }),
421
- defineCustomWidget({ id: 'financial.panel.overdue', title: 'financial.summary.overdueTitle', domain: 'financial', span: 1, defaultOrder: 12, surfaces: ['plugin-home'], component: withCtx(OverdueAlerts) }),
422
- defineTableWidget({ id: 'financial.table.recent', title: 'financial.summary.recentTransactions', domain: 'financial', span: 4, defaultOrder: 20, surfaces: ['plugin-home'], component: withCtx(RecentTransactions) }),
423
-
424
- // FINANCE-HOME surface (B2C phone-first dashboard) — a compact analytical
425
- // layout: hero band → charts row (spend-by-category + cash-flow) → bills +
426
- // cards. This is a dedicated consumer surface (norman-ai's money screen), NOT
427
- // the universal B2B 'home': a full-bleed balance hero and credit-card tiles
428
- // are personal-finance chrome that would leak into every app that merely
429
- // enables plugin-financial (e.g. the beauty-saas Painel). Apps that want them
430
- // opt in explicitly by rendering <DashboardCanvas surface="finance-home">.
431
- defineCustomWidget({ id: 'financial.hero.balance', title: 'financial.home.balance', domain: 'financial', span: 4, defaultOrder: 0, surfaces: ['finance-home'], component: withCtx(BalanceHero) }),
432
- defineChartWidget({ id: 'financial.chart.spend-by-category', title: 'financial.home.spendByCategory', domain: 'financial', span: 2, defaultOrder: 1, surfaces: ['finance-home'], component: withCtx(SpendByCategoryChart) }),
433
- defineChartWidget({ id: 'financial.chart.cash-flow-home', title: 'financial.summary.cashFlow', domain: 'financial', span: 2, defaultOrder: 2, surfaces: ['finance-home'], component: withCtx(CashFlowChart) }),
434
- defineCustomWidget({ id: 'financial.list.upcoming-bills', title: 'financial.home.upcomingBills', domain: 'financial', span: 2, defaultOrder: 3, surfaces: ['finance-home'], component: withCtx(UpcomingBills) }),
435
- defineCustomWidget({ id: 'financial.cards', title: 'financial.home.cards', domain: 'financial', span: 2, defaultOrder: 4, surfaces: ['finance-home'], component: withCtx(CreditCards) }),
436
- ]
437
- }