@fayz-ai/plugin-financial 0.1.1

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 (98) hide show
  1. package/dist/FinancialContext.d.ts +44 -0
  2. package/dist/FinancialContext.d.ts.map +1 -0
  3. package/dist/FinancialPage.d.ts +18 -0
  4. package/dist/FinancialPage.d.ts.map +1 -0
  5. package/dist/components/FinancialGeneralSettings.d.ts +3 -0
  6. package/dist/components/FinancialGeneralSettings.d.ts.map +1 -0
  7. package/dist/components/FinancialOnboarding.d.ts +5 -0
  8. package/dist/components/FinancialOnboarding.d.ts.map +1 -0
  9. package/dist/components/FinancialSettings.d.ts +8 -0
  10. package/dist/components/FinancialSettings.d.ts.map +1 -0
  11. package/dist/components/PaymentModal.d.ts +8 -0
  12. package/dist/components/PaymentModal.d.ts.map +1 -0
  13. package/dist/data/index.d.ts +3 -0
  14. package/dist/data/index.d.ts.map +1 -0
  15. package/dist/data/mock.d.ts +9 -0
  16. package/dist/data/mock.d.ts.map +1 -0
  17. package/dist/data/supabase.d.ts +3 -0
  18. package/dist/data/supabase.d.ts.map +1 -0
  19. package/dist/data/types.d.ts +27 -0
  20. package/dist/data/types.d.ts.map +1 -0
  21. package/dist/index.cjs +4937 -0
  22. package/dist/index.cjs.map +1 -0
  23. package/dist/index.d.ts +91 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +4930 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/lib/tenant.d.ts +3 -0
  28. package/dist/lib/tenant.d.ts.map +1 -0
  29. package/dist/locales/en.d.ts +2 -0
  30. package/dist/locales/en.d.ts.map +1 -0
  31. package/dist/locales/index.d.ts +2 -0
  32. package/dist/locales/index.d.ts.map +1 -0
  33. package/dist/locales/pt-BR.d.ts +2 -0
  34. package/dist/locales/pt-BR.d.ts.map +1 -0
  35. package/dist/registries.d.ts +3 -0
  36. package/dist/registries.d.ts.map +1 -0
  37. package/dist/store.d.ts +38 -0
  38. package/dist/store.d.ts.map +1 -0
  39. package/dist/types.d.ts +302 -0
  40. package/dist/types.d.ts.map +1 -0
  41. package/dist/views/CardsView.d.ts +3 -0
  42. package/dist/views/CardsView.d.ts.map +1 -0
  43. package/dist/views/CashRegistersView.d.ts +3 -0
  44. package/dist/views/CashRegistersView.d.ts.map +1 -0
  45. package/dist/views/CommissionsView.d.ts +3 -0
  46. package/dist/views/CommissionsView.d.ts.map +1 -0
  47. package/dist/views/InvoiceDetailView.d.ts +9 -0
  48. package/dist/views/InvoiceDetailView.d.ts.map +1 -0
  49. package/dist/views/InvoiceFormView.d.ts +13 -0
  50. package/dist/views/InvoiceFormView.d.ts.map +1 -0
  51. package/dist/views/InvoiceListView.d.ts +8 -0
  52. package/dist/views/InvoiceListView.d.ts.map +1 -0
  53. package/dist/views/PayablesView.d.ts +7 -0
  54. package/dist/views/PayablesView.d.ts.map +1 -0
  55. package/dist/views/ReceivablesView.d.ts +7 -0
  56. package/dist/views/ReceivablesView.d.ts.map +1 -0
  57. package/dist/views/StatementsView.d.ts +3 -0
  58. package/dist/views/StatementsView.d.ts.map +1 -0
  59. package/dist/views/SummaryView.d.ts +9 -0
  60. package/dist/views/SummaryView.d.ts.map +1 -0
  61. package/dist/views/dashboardWidgets.d.ts +11 -0
  62. package/dist/views/dashboardWidgets.d.ts.map +1 -0
  63. package/package.json +55 -0
  64. package/src/FinancialContext.tsx +47 -0
  65. package/src/FinancialPage.tsx +261 -0
  66. package/src/README.md +155 -0
  67. package/src/components/FinancialGeneralSettings.tsx +26 -0
  68. package/src/components/FinancialOnboarding.tsx +87 -0
  69. package/src/components/FinancialSettings.tsx +31 -0
  70. package/src/components/PaymentModal.tsx +322 -0
  71. package/src/data/index.ts +2 -0
  72. package/src/data/mock.ts +520 -0
  73. package/src/data/supabase.ts +509 -0
  74. package/src/data/types.ts +53 -0
  75. package/src/index.ts +307 -0
  76. package/src/lib/tenant.ts +3 -0
  77. package/src/locales/en.ts +331 -0
  78. package/src/locales/index.ts +7 -0
  79. package/src/locales/pt-BR.ts +331 -0
  80. package/src/migrations/001_financial_base.sql +109 -0
  81. package/src/migrations/002_chart_of_accounts.sql +28 -0
  82. package/src/migrations/003_card_brands.sql +12 -0
  83. package/src/migrations/004_order_to_cash.sql +164 -0
  84. package/src/migrations/005_seed_defaults.sql +38 -0
  85. package/src/registries.ts +305 -0
  86. package/src/store.ts +221 -0
  87. package/src/types.ts +359 -0
  88. package/src/views/CardsView.tsx +74 -0
  89. package/src/views/CashRegistersView.tsx +597 -0
  90. package/src/views/CommissionsView.tsx +18 -0
  91. package/src/views/InvoiceDetailView.tsx +595 -0
  92. package/src/views/InvoiceFormView.tsx +900 -0
  93. package/src/views/InvoiceListView.tsx +177 -0
  94. package/src/views/PayablesView.tsx +37 -0
  95. package/src/views/ReceivablesView.tsx +37 -0
  96. package/src/views/StatementsView.tsx +123 -0
  97. package/src/views/SummaryView.tsx +12 -0
  98. package/src/views/dashboardWidgets.tsx +191 -0
@@ -0,0 +1,177 @@
1
+ import React, { useEffect, useState, useMemo } from 'react'
2
+ import { FileText, CircleDashed, CircleEllipsis, CircleCheckBig, CircleAlert, Ban } from 'lucide-react'
3
+ import type { ColumnDef } from '@tanstack/react-table'
4
+ import { ListView } from '@fayz-ai/ui'
5
+ import { useFinancialConfig, useFinancialStore, formatCurrency } from '../FinancialContext'
6
+ import { SubpageHeader } from '@fayz-ai/ui'
7
+ import { PersonLink } from '@fayz-ai/saas'
8
+ import { useTranslation } from '@fayz-ai/core'
9
+ import type { TransactionDirection, InvoiceStatus, Invoice } from '../types'
10
+
11
+ const STATUS_OPTIONS: { value: InvoiceStatus; labelKey: string; color: string; icon: React.ElementType }[] = [
12
+ { value: 'open', labelKey: 'financial.invoice.statusOpen', color: 'bg-info-soft text-info-soft-foreground', icon: CircleDashed },
13
+ { value: 'partial', labelKey: 'financial.invoice.statusPartial', color: 'bg-warning-soft text-warning-soft-foreground', icon: CircleEllipsis },
14
+ { value: 'paid', labelKey: 'financial.invoice.statusPaid', color: 'bg-success-soft text-success-soft-foreground', icon: CircleCheckBig },
15
+ { value: 'overdue', labelKey: 'financial.invoice.statusOverdue', color: 'bg-destructive-soft text-destructive-soft-foreground', icon: CircleAlert },
16
+ { value: 'cancelled', labelKey: 'financial.invoice.statusCancelled', color: 'bg-muted text-muted-foreground', icon: Ban },
17
+ ]
18
+
19
+ function StatusBadge({ status }: { status: string }) {
20
+ const t = useTranslation()
21
+ const opt = STATUS_OPTIONS.find((o) => o.value === status)
22
+ const Icon = opt?.icon ?? CircleDashed
23
+ return (
24
+ <span className={`inline-flex items-center gap-0.5 rounded-full px-2 py-0.5 text-[10px] font-medium ${opt?.color ?? 'bg-muted text-muted-foreground'}`}>
25
+ <Icon className="h-2.5 w-2.5" /> {opt ? t(opt.labelKey) : status}
26
+ </span>
27
+ )
28
+ }
29
+
30
+ function useInvoiceColumns(currency: { code: string; locale: string; symbol: string }): ColumnDef<Invoice, any>[] {
31
+ const t = useTranslation()
32
+ return useMemo(() => [
33
+ {
34
+ accessorKey: 'fiscalNumber', header: '#', size: 110,
35
+ cell: ({ getValue }) => {
36
+ const num = getValue() as string | undefined
37
+ return num ? <span className="text-xs font-mono text-muted-foreground">#{num}</span> : <span className="text-muted-foreground">—</span>
38
+ },
39
+ },
40
+ {
41
+ accessorKey: 'invoiceDate', header: t('financial.invoice.columnDate'),
42
+ cell: ({ getValue }) => <span className="text-muted-foreground text-xs">{getValue() as string}</span>,
43
+ },
44
+ {
45
+ id: 'description', header: t('financial.invoice.columnDescription'),
46
+ cell: ({ row }) => (
47
+ <div onClick={(e) => e.stopPropagation()}>
48
+ {row.original.contactName ? (
49
+ <PersonLink personId={row.original.contactId} name={row.original.contactName} size="sm" className="text-sm font-medium" />
50
+ ) : (
51
+ <p className="font-medium">—</p>
52
+ )}
53
+ {row.original.itemsSummary && <p className="text-[10px] text-muted-foreground truncate max-w-[250px]">{row.original.itemsSummary}</p>}
54
+ </div>
55
+ ),
56
+ },
57
+ {
58
+ accessorKey: 'totalAmount', header: t('financial.invoice.columnAmount'), enableSorting: true,
59
+ cell: ({ row }) => (
60
+ <span className={`font-medium ${row.original.status === 'cancelled' ? 'line-through text-muted-foreground' : ''}`}>
61
+ {formatCurrency(row.original.totalAmount, currency)}
62
+ </span>
63
+ ),
64
+ },
65
+ {
66
+ accessorKey: 'paidAmount', header: t('financial.invoice.columnPaid'),
67
+ cell: ({ row }) => (
68
+ <span className={row.original.status === 'cancelled' ? 'line-through text-muted-foreground' : 'text-muted-foreground'}>
69
+ {formatCurrency(row.original.paidAmount, currency)}
70
+ </span>
71
+ ),
72
+ },
73
+ {
74
+ accessorKey: 'status', header: t('financial.invoice.columnStatus'),
75
+ cell: ({ getValue }) => <StatusBadge status={getValue() as string} />,
76
+ },
77
+ {
78
+ accessorKey: 'totalInstallments', header: t('financial.invoice.columnInst'),
79
+ cell: ({ getValue }) => <span className="text-muted-foreground">{getValue() as number}</span>,
80
+ },
81
+ ], [currency, t])
82
+ }
83
+
84
+ export function InvoiceListView({ direction, onNew, onEdit }: {
85
+ direction: TransactionDirection
86
+ onNew?: () => void
87
+ onEdit?: (id: string) => void
88
+ }) {
89
+ const t = useTranslation()
90
+ const { currency } = useFinancialConfig()
91
+ const invoices = useFinancialStore((s) => s.invoices)
92
+ const invoicesTotal = useFinancialStore((s) => s.invoicesTotal)
93
+ const invoicesLoading = useFinancialStore((s) => s.invoicesLoading)
94
+ const fetchInvoices = useFinancialStore((s) => s.fetchInvoices)
95
+
96
+ const [search, setSearch] = useState('')
97
+ const [statusFilter, setStatusFilter] = useState<InvoiceStatus[]>([])
98
+
99
+ useEffect(() => {
100
+ fetchInvoices({
101
+ direction,
102
+ status: statusFilter.length > 0 ? statusFilter : undefined,
103
+ search: search || undefined,
104
+ })
105
+ }, [direction, statusFilter, search])
106
+
107
+ const filtered = invoices.filter((inv) => inv.direction === direction)
108
+ const listTitle = direction === 'debit' ? t('financial.invoice.accountsPayable') : t('financial.invoice.accountsReceivable')
109
+ const columns = useInvoiceColumns(currency)
110
+ const tags = useMemo(
111
+ () => STATUS_OPTIONS.filter((o) => o.value !== 'cancelled').map((o) => ({ value: o.value, label: t(o.labelKey) })),
112
+ [t],
113
+ )
114
+
115
+ // Summary totals from currently visible data
116
+ const summary = useMemo(() => {
117
+ let totalAmount = 0
118
+ let paidAmount = 0
119
+ let openAmount = 0
120
+ for (const inv of filtered) {
121
+ if (inv.status === 'cancelled') continue
122
+ totalAmount += inv.totalAmount
123
+ paidAmount += inv.paidAmount
124
+ }
125
+ openAmount = totalAmount - paidAmount
126
+ return { totalAmount, paidAmount, openAmount }
127
+ }, [filtered])
128
+
129
+ const receivedLabel = direction === 'debit' ? t('financial.invoice.summaryPaid') : t('financial.invoice.summaryReceived')
130
+ const pendingLabel = direction === 'debit' ? t('financial.invoice.summaryPayable') : t('financial.invoice.summaryReceivable')
131
+
132
+ return (
133
+ <div className="space-y-4">
134
+ <SubpageHeader
135
+ title={listTitle}
136
+ subtitle={t('financial.invoice.invoices', { count: filtered.length })}
137
+ />
138
+ <ListView<Invoice>
139
+ columns={columns}
140
+ data={filtered}
141
+ loading={invoicesLoading}
142
+ searchPlaceholder={t('financial.invoice.searchPlaceholder')}
143
+ search={search}
144
+ onSearchChange={setSearch}
145
+ searchDebounce={0}
146
+ tags={tags}
147
+ activeTag={statusFilter.length === 1 ? statusFilter[0] : undefined}
148
+ onTagChange={(v) => setStatusFilter(v ? [v as InvoiceStatus] : [])}
149
+ newLabel={t('financial.invoice.new')}
150
+ onNew={onNew}
151
+ onRowClick={(row) => onEdit?.(row.id)}
152
+ emptyIcon={FileText}
153
+ emptyMessage={t('financial.invoice.noInvoices')}
154
+ emptyActionLabel={onNew ? t('financial.invoice.createFirst') : undefined}
155
+ onEmptyAction={onNew}
156
+ />
157
+ {filtered.length > 0 && !invoicesLoading && (
158
+ <div className="flex items-center justify-end gap-6 rounded-lg border bg-muted/30 px-5 py-3 text-sm">
159
+ <div className="flex items-center gap-2">
160
+ <span className="text-muted-foreground">{t('financial.invoice.summaryTotal')}</span>
161
+ <span className="font-semibold">{formatCurrency(summary.totalAmount, currency)}</span>
162
+ </div>
163
+ <div className="h-4 w-px bg-border" />
164
+ <div className="flex items-center gap-2">
165
+ <span className="text-muted-foreground">{receivedLabel}</span>
166
+ <span className="font-semibold text-success">{formatCurrency(summary.paidAmount, currency)}</span>
167
+ </div>
168
+ <div className="h-4 w-px bg-border" />
169
+ <div className="flex items-center gap-2">
170
+ <span className="text-muted-foreground">{pendingLabel}</span>
171
+ <span className="font-semibold text-warning">{formatCurrency(summary.openAmount, currency)}</span>
172
+ </div>
173
+ </div>
174
+ )}
175
+ </div>
176
+ )
177
+ }
@@ -0,0 +1,37 @@
1
+ import React from 'react'
2
+ import { InvoiceListView } from './InvoiceListView'
3
+ import { InvoiceFormView } from './InvoiceFormView'
4
+ import { InvoiceDetailView } from './InvoiceDetailView'
5
+ import type { ViewIntent } from '../FinancialPage'
6
+
7
+ export function PayablesView({ intent, onNavigate }: {
8
+ intent: ViewIntent
9
+ onNavigate: (view: string) => void
10
+ }) {
11
+ if (intent.mode === 'new') {
12
+ return <InvoiceFormView direction="debit" onSaved={(id) => onNavigate(id ? `payables-detail:${id}` : 'payables-list')} />
13
+ }
14
+
15
+ if (intent.mode === 'edit' && intent.editId) {
16
+ return <InvoiceFormView direction="debit" editId={intent.editId} onSaved={() => onNavigate(`payables-detail:${intent.editId}`)} />
17
+ }
18
+
19
+ if (intent.mode === 'detail' && intent.editId) {
20
+ return (
21
+ <InvoiceDetailView
22
+ invoiceId={intent.editId}
23
+ direction="debit"
24
+ onBack={() => onNavigate('payables-list')}
25
+ onEdit={() => onNavigate(`payables-edit:${intent.editId}`)}
26
+ />
27
+ )
28
+ }
29
+
30
+ return (
31
+ <InvoiceListView
32
+ direction="debit"
33
+ onNew={() => onNavigate('payables-new')}
34
+ onEdit={(id) => onNavigate(`payables-detail:${id}`)}
35
+ />
36
+ )
37
+ }
@@ -0,0 +1,37 @@
1
+ import React from 'react'
2
+ import { InvoiceListView } from './InvoiceListView'
3
+ import { InvoiceFormView } from './InvoiceFormView'
4
+ import { InvoiceDetailView } from './InvoiceDetailView'
5
+ import type { ViewIntent } from '../FinancialPage'
6
+
7
+ export function ReceivablesView({ intent, onNavigate }: {
8
+ intent: ViewIntent
9
+ onNavigate: (view: string) => void
10
+ }) {
11
+ if (intent.mode === 'new') {
12
+ return <InvoiceFormView direction="credit" onSaved={(id) => onNavigate(id ? `receivables-detail:${id}` : 'receivables-list')} />
13
+ }
14
+
15
+ if (intent.mode === 'edit' && intent.editId) {
16
+ return <InvoiceFormView direction="credit" editId={intent.editId} onSaved={() => onNavigate(`receivables-detail:${intent.editId}`)} />
17
+ }
18
+
19
+ if (intent.mode === 'detail' && intent.editId) {
20
+ return (
21
+ <InvoiceDetailView
22
+ invoiceId={intent.editId}
23
+ direction="credit"
24
+ onBack={() => onNavigate('receivables-list')}
25
+ onEdit={() => onNavigate(`receivables-edit:${intent.editId}`)}
26
+ />
27
+ )
28
+ }
29
+
30
+ return (
31
+ <InvoiceListView
32
+ direction="credit"
33
+ onNew={() => onNavigate('receivables-new')}
34
+ onEdit={(id) => onNavigate(`receivables-detail:${id}`)}
35
+ />
36
+ )
37
+ }
@@ -0,0 +1,123 @@
1
+ import React, { useEffect, useState, useMemo } from 'react'
2
+ import { Receipt } from 'lucide-react'
3
+ import type { ColumnDef } from '@tanstack/react-table'
4
+ import { DataTable } from '@fayz-ai/ui'
5
+ import { useFinancialConfig, useFinancialStore, formatCurrency } from '../FinancialContext'
6
+ import { SubpageHeader } from '@fayz-ai/ui'
7
+ import { DatePicker } from '@fayz-ai/ui'
8
+ import { useTranslation } from '@fayz-ai/core'
9
+ import type { StatementEntry } from '../types'
10
+
11
+ export function StatementsView() {
12
+ const t = useTranslation()
13
+ const { currency } = useFinancialConfig()
14
+ const bankAccounts = useFinancialStore((s) => s.bankAccounts)
15
+ const statementEntries = useFinancialStore((s) => s.statementEntries)
16
+ const statementLoading = useFinancialStore((s) => s.statementLoading)
17
+ const fetchBankAccounts = useFinancialStore((s) => s.fetchBankAccounts)
18
+ const fetchStatement = useFinancialStore((s) => s.fetchStatement)
19
+
20
+ const [accountId, setAccountId] = useState('')
21
+ const [dateFrom, setDateFrom] = useState(() => {
22
+ const d = new Date(); d.setMonth(d.getMonth() - 1)
23
+ return d.toISOString().slice(0, 10)
24
+ })
25
+ const [dateTo, setDateTo] = useState(() => new Date().toISOString().slice(0, 10))
26
+
27
+ useEffect(() => { fetchBankAccounts() }, [])
28
+
29
+ useEffect(() => {
30
+ if (accountId) {
31
+ fetchStatement({ bankAccountId: accountId, dateRange: { from: dateFrom, to: dateTo } })
32
+ }
33
+ }, [accountId, dateFrom, dateTo])
34
+
35
+ useEffect(() => {
36
+ if (!accountId && bankAccounts.length > 0) setAccountId(bankAccounts[0].id)
37
+ }, [bankAccounts])
38
+
39
+ const columns: ColumnDef<StatementEntry, any>[] = useMemo(() => [
40
+ {
41
+ id: 'date', header: t('financial.statements.columnDate'),
42
+ cell: ({ row }) => (
43
+ <span className="text-muted-foreground text-xs">{row.original.movement.paymentDate ?? row.original.movement.dueDate}</span>
44
+ ),
45
+ },
46
+ {
47
+ id: 'description', header: t('financial.statements.columnDescription'),
48
+ cell: ({ row }) => (
49
+ <div>
50
+ <p className="text-xs font-medium">{row.original.invoice?.contactName || row.original.movement.notes || 'Transaction'}</p>
51
+ <p className="text-[10px] text-muted-foreground capitalize">{row.original.movement.movementKind}</p>
52
+ </div>
53
+ ),
54
+ },
55
+ {
56
+ id: 'debit', header: t('financial.statements.columnDebit'),
57
+ cell: ({ row }) => (
58
+ <span className="text-right block text-destructive text-xs">
59
+ {row.original.movement.direction === 'debit' ? formatCurrency(row.original.movement.paidAmount, currency) : ''}
60
+ </span>
61
+ ),
62
+ },
63
+ {
64
+ id: 'credit', header: t('financial.statements.columnCredit'),
65
+ cell: ({ row }) => (
66
+ <span className="text-right block text-success text-xs">
67
+ {row.original.movement.direction === 'credit' ? formatCurrency(row.original.movement.paidAmount, currency) : ''}
68
+ </span>
69
+ ),
70
+ },
71
+ {
72
+ id: 'balance', header: t('financial.statements.columnBalance'),
73
+ cell: ({ row }) => (
74
+ <span className="text-right block font-medium text-xs">{formatCurrency(row.original.runningBalance, currency)}</span>
75
+ ),
76
+ },
77
+ ], [currency, t])
78
+
79
+ return (
80
+ <div className="space-y-4">
81
+ <SubpageHeader title={t('financial.statements.title')} subtitle={t('financial.statements.subtitle')} />
82
+
83
+ {/* Filters */}
84
+ <div className="flex flex-col sm:flex-row gap-3 sm:items-end">
85
+ <div className="flex-1 max-w-xs">
86
+ <label className="text-[10px] font-medium text-muted-foreground uppercase">{t('financial.statements.account')}</label>
87
+ <select
88
+ value={accountId}
89
+ onChange={(e) => setAccountId(e.target.value)}
90
+ 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
+ >
92
+ <option value="">{t('financial.statements.selectAccount')}</option>
93
+ {bankAccounts.map((a) => (
94
+ <option key={a.id} value={a.id}>{a.name}</option>
95
+ ))}
96
+ </select>
97
+ </div>
98
+ <div>
99
+ <label className="text-[10px] font-medium text-muted-foreground uppercase">{t('financial.statements.from')}</label>
100
+ <DatePicker value={dateFrom} onChange={setDateFrom} className="mt-0.5" />
101
+ </div>
102
+ <div>
103
+ <label className="text-[10px] font-medium text-muted-foreground uppercase">{t('financial.statements.to')}</label>
104
+ <DatePicker value={dateTo} onChange={setDateTo} className="mt-0.5" />
105
+ </div>
106
+ </div>
107
+
108
+ {/* Statement table */}
109
+ {!accountId ? (
110
+ <div className="flex flex-col items-center justify-center py-12 text-center rounded-lg border-2 border-dashed border-muted">
111
+ <Receipt className="h-8 w-8 text-muted-foreground mb-2" />
112
+ <p className="text-sm text-muted-foreground">{t('financial.statements.selectToView')}</p>
113
+ </div>
114
+ ) : statementLoading ? (
115
+ <DataTable columns={columns} data={[]} loading skeletonRows={5} />
116
+ ) : statementEntries.length === 0 ? (
117
+ <DataTable columns={columns} data={[]} emptyMessage={t('financial.statements.noTransactions')} />
118
+ ) : (
119
+ <DataTable columns={columns} data={statementEntries} />
120
+ )}
121
+ </div>
122
+ )
123
+ }
@@ -0,0 +1,12 @@
1
+ import React from 'react'
2
+ import { DashboardCanvas } from '@fayz-ai/ui'
3
+
4
+ /**
5
+ * Financial summary. KPIs, cash-flow chart, breakdown, overdue alerts and the
6
+ * recent-transactions table are registered dashboard widgets (see
7
+ * ./dashboardWidgets), rendered through the shared DashboardCanvas; the
8
+ * total-balance KPI also surfaces on the global app home.
9
+ */
10
+ export function SummaryView() {
11
+ return <DashboardCanvas surface="plugin-home" domain="financial" showHeader={false} className="space-y-6" />
12
+ }
@@ -0,0 +1,191 @@
1
+ import React, { useEffect } from 'react'
2
+ import { ArrowUpRight, ArrowDownRight, AlertTriangle } 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,
9
+ defineKpiWidget, defineChartWidget, defineCustomWidget, defineTableWidget,
10
+ } from '@fayz-ai/ui'
11
+ import { FinancialContextProvider, useFinancialConfig, useFinancialStore, formatCurrency, type ResolvedFinancialConfig } from '../FinancialContext'
12
+ import type { FinancialDataProvider } from '../data/types'
13
+ import type { FinancialUIState } from '../store'
14
+ import type { Invoice } 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
+ // Registry builder
162
+ // ---------------------------------------------------------------------------
163
+
164
+ export function createFinancialDashboardWidgets(ctx: {
165
+ config: ResolvedFinancialConfig
166
+ provider: FinancialDataProvider
167
+ store: StoreApi<FinancialUIState>
168
+ }): DashboardWidgetDef[] {
169
+ const withCtx = (Inner: React.ComponentType): React.ComponentType<unknown> => {
170
+ const Wrapped = () => (
171
+ <FinancialContextProvider config={ctx.config} provider={ctx.provider} store={ctx.store}>
172
+ <Inner />
173
+ </FinancialContextProvider>
174
+ )
175
+ Wrapped.displayName = `FinancialWidget(${Inner.displayName ?? Inner.name})`
176
+ return Wrapped
177
+ }
178
+
179
+ return [
180
+ // Total balance is finance's headline KPI on the global home; the rest are
181
+ // home-hidden by default (shown on the financial plugin-home, addable via Customize).
182
+ defineKpiWidget({ id: 'financial.kpi.total-balance', title: 'financial.summary.totalBalance', domain: 'financial', defaultOrder: 0, component: withCtx(TotalBalanceKpi) }),
183
+ defineKpiWidget({ id: 'financial.kpi.receivable', title: 'financial.summary.receivable', domain: 'financial', defaultOrder: 1, defaultVisible: false, component: withCtx(ReceivableKpi) }),
184
+ defineKpiWidget({ id: 'financial.kpi.payable', title: 'financial.summary.payable', domain: 'financial', defaultOrder: 2, defaultVisible: false, component: withCtx(PayableKpi) }),
185
+ defineKpiWidget({ id: 'financial.kpi.monthly-flow', title: 'financial.summary.monthlyFlow', domain: 'financial', defaultOrder: 3, defaultVisible: false, component: withCtx(MonthlyFlowKpi) }),
186
+ defineChartWidget({ id: 'financial.chart.cash-flow', title: 'financial.summary.cashFlow', domain: 'financial', span: 2, defaultOrder: 10, surfaces: ['plugin-home'], component: withCtx(CashFlowChart) }),
187
+ defineCustomWidget({ id: 'financial.panel.breakdown', title: 'financial.summary.cashFlowBreakdown', domain: 'financial', span: 1, defaultOrder: 11, surfaces: ['plugin-home'], component: withCtx(CashFlowBreakdown) }),
188
+ defineCustomWidget({ id: 'financial.panel.overdue', title: 'financial.summary.overdueTitle', domain: 'financial', span: 1, defaultOrder: 12, surfaces: ['plugin-home'], component: withCtx(OverdueAlerts) }),
189
+ defineTableWidget({ id: 'financial.table.recent', title: 'financial.summary.recentTransactions', domain: 'financial', span: 4, defaultOrder: 20, surfaces: ['plugin-home'], component: withCtx(RecentTransactions) }),
190
+ ]
191
+ }