@fayz-ai/plugin-financial 0.1.7 → 0.2.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.
- package/dist/FinancialContext.d.ts +2 -0
- package/dist/FinancialContext.d.ts.map +1 -1
- package/dist/FinancialPage.d.ts.map +1 -1
- package/dist/data/index.d.ts +1 -0
- package/dist/data/index.d.ts.map +1 -1
- package/dist/data/mock.d.ts +15 -0
- package/dist/data/mock.d.ts.map +1 -1
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/data/types.d.ts +2 -1
- package/dist/data/types.d.ts.map +1 -1
- package/dist/index.cjs +1531 -201
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1393 -80
- package/dist/index.js.map +1 -1
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/pt-BR.d.ts.map +1 -1
- package/dist/quick-add.d.ts +13 -0
- package/dist/quick-add.d.ts.map +1 -0
- package/dist/store.d.ts +3 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/types.d.ts +42 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/views/CardsView.d.ts.map +1 -1
- package/dist/views/InvoiceListView.d.ts.map +1 -1
- package/dist/views/QuickTransactionForm.d.ts +16 -0
- package/dist/views/QuickTransactionForm.d.ts.map +1 -0
- package/dist/views/SummaryView.d.ts +5 -4
- package/dist/views/SummaryView.d.ts.map +1 -1
- package/dist/views/dashboardWidgets.d.ts.map +1 -1
- package/package.json +4 -7
- package/src/FinancialContext.tsx +2 -0
- package/src/FinancialPage.tsx +86 -7
- package/src/data/capability.test.ts +45 -0
- package/src/data/index.ts +1 -0
- package/src/data/mock.ts +50 -10
- package/src/data/supabase.ts +46 -3
- package/src/data/types.ts +2 -1
- package/src/index.ts +24 -4
- package/src/locales/en.ts +75 -0
- package/src/locales/pt-BR.ts +75 -0
- package/src/quick-add.ts +58 -0
- package/src/store.ts +88 -0
- package/src/types.ts +45 -0
- package/src/views/CardsView.tsx +186 -50
- package/src/views/InvoiceListView.tsx +60 -3
- package/src/views/QuickTransactionForm.tsx +537 -0
- package/src/views/SummaryView.tsx +197 -6
- package/src/views/dashboardWidgets.tsx +251 -5
package/src/views/CardsView.tsx
CHANGED
|
@@ -1,74 +1,210 @@
|
|
|
1
|
-
import React, { useEffect } from 'react'
|
|
2
|
-
import { CreditCard,
|
|
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
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
65
|
+
|
|
66
|
+
{bankAccountsLoading && cards.length === 0 ? (
|
|
28
67
|
<div className="text-center py-12 text-sm text-muted-foreground">Loading...</div>
|
|
29
|
-
) :
|
|
30
|
-
<div className="flex flex-col items-center justify-center py-12 text-center rounded-
|
|
31
|
-
<div className="flex h-
|
|
32
|
-
<CreditCard className="h-
|
|
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
|
|
35
|
-
<p className="text-xs text-muted-foreground mt-0.5">{t('financial.cards.
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
<
|
|
43
|
-
<
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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}
|