@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.
- package/README.md +3 -3
- package/dist/components/PersonStatementWidget.d.ts +1 -1
- package/dist/components/PersonStatementWidget.d.ts.map +1 -1
- package/dist/{FinancialContext.d.ts → context.d.ts} +4 -2
- package/dist/context.d.ts.map +1 -0
- package/dist/data/registries.d.ts.map +1 -0
- package/dist/index.d.ts +13 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1426 -1426
- package/dist/index.js.map +1 -1
- package/dist/integrations/stripe/connectorDef.d.ts +14 -0
- package/dist/integrations/stripe/connectorDef.d.ts.map +1 -0
- package/dist/integrations/stripe/data.d.ts +43 -0
- package/dist/integrations/stripe/data.d.ts.map +1 -0
- package/dist/lib/message-events.d.ts +12 -0
- package/dist/lib/message-events.d.ts.map +1 -0
- package/dist/{quick-add.d.ts → lib/quick-add.d.ts} +1 -1
- package/dist/lib/quick-add.d.ts.map +1 -0
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/pt-BR.d.ts.map +1 -1
- package/dist/{FinancialPage.d.ts → views/FinancialPage.d.ts} +3 -3
- package/dist/views/FinancialPage.d.ts.map +1 -0
- package/dist/views/InvoiceDetailView.d.ts.map +1 -1
- package/dist/views/InvoiceFormView.d.ts.map +1 -1
- package/dist/views/InvoiceListView.d.ts.map +1 -1
- package/dist/views/PayablesView.d.ts +1 -1
- package/dist/views/PayablesView.d.ts.map +1 -1
- package/dist/views/ReceivablesView.d.ts +1 -1
- package/dist/views/ReceivablesView.d.ts.map +1 -1
- package/dist/views/StatementsView.d.ts.map +1 -1
- package/dist/views/dashboardWidgets.d.ts +1 -1
- package/dist/views/dashboardWidgets.d.ts.map +1 -1
- package/package.json +12 -14
- package/dist/FinancialContext.d.ts.map +0 -1
- package/dist/FinancialPage.d.ts.map +0 -1
- package/dist/contract.test.d.ts +0 -2
- package/dist/contract.test.d.ts.map +0 -1
- package/dist/quick-add.d.ts.map +0 -1
- package/dist/registries.d.ts.map +0 -1
- package/src/FinancialContext.tsx +0 -59
- package/src/FinancialPage.tsx +0 -413
- package/src/README.md +0 -155
- package/src/components/FinancialGeneralSettings.tsx +0 -39
- package/src/components/FinancialOnboarding.tsx +0 -87
- package/src/components/FinancialSettings.tsx +0 -31
- package/src/components/PaymentModal.tsx +0 -369
- package/src/components/PersonStatementWidget.tsx +0 -133
- package/src/components/TransferModal.tsx +0 -150
- package/src/contract.test.ts +0 -58
- package/src/data/capability.test.ts +0 -135
- package/src/data/index.ts +0 -3
- package/src/data/mock.ts +0 -842
- package/src/data/supabase.ts +0 -968
- package/src/data/tables.ts +0 -13
- package/src/data/types.ts +0 -86
- package/src/index.ts +0 -487
- package/src/lib/tenant.ts +0 -3
- package/src/locales/en.ts +0 -493
- package/src/locales/index.ts +0 -7
- package/src/locales/pt-BR.ts +0 -493
- package/src/migrations/000_plg_rename.sql +0 -30
- package/src/migrations/001_financial_base.sql +0 -109
- package/src/migrations/002_chart_of_accounts.sql +0 -28
- package/src/migrations/003_card_brands.sql +0 -12
- package/src/migrations/004_order_to_cash.sql +0 -178
- package/src/migrations/005_seed_defaults.sql +0 -38
- package/src/migrations/006_rls_policies.sql +0 -37
- package/src/migrations/006b_extract_fee_amount.sql +0 -20
- package/src/migrations/007_reconciliation.sql +0 -32
- package/src/migrations/007b_movement_payment_method_type.sql +0 -6
- package/src/migrations/008_split_payment_movements.sql +0 -29
- package/src/migrations/009_agent_rpcs.sql +0 -165
- package/src/migrations/010_invoice_internal.sql +0 -133
- package/src/migrations/011_nfe.sql +0 -58
- package/src/migrations/index.ts +0 -923
- package/src/quick-add.ts +0 -58
- package/src/registries.ts +0 -308
- package/src/store.ts +0 -329
- package/src/types.ts +0 -592
- package/src/views/CardsView.tsx +0 -213
- package/src/views/CashRegistersView.tsx +0 -597
- package/src/views/CommissionsView.tsx +0 -18
- package/src/views/InvoiceDetailView.tsx +0 -616
- package/src/views/InvoiceFormView.tsx +0 -945
- package/src/views/InvoiceListView.tsx +0 -323
- package/src/views/NotasFiscaisView.tsx +0 -209
- package/src/views/PayablesView.tsx +0 -48
- package/src/views/QuickTransactionForm.tsx +0 -548
- package/src/views/ReceivablesView.tsx +0 -48
- package/src/views/ReconciliationView.tsx +0 -211
- package/src/views/StatementsView.tsx +0 -335
- package/src/views/SummaryView.tsx +0 -203
- package/src/views/dashboardWidgets.tsx +0 -437
- package/src/views/navReferrer.ts +0 -24
- /package/dist/{registries.d.ts → data/registries.d.ts} +0 -0
|
@@ -1,323 +0,0 @@
|
|
|
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, useFinancialProvider, useFinancialStore, formatCurrency } from '../FinancialContext'
|
|
6
|
-
import { SubpageHeader } from '@fayz-ai/ui'
|
|
7
|
-
import { PersonLink, usePermissionOptional } from '@fayz-ai/saas'
|
|
8
|
-
import { useTranslation } from '@fayz-ai/core'
|
|
9
|
-
import type { TransactionDirection, InvoiceStatus, Invoice, ChartOfAccountsNode, CostCenter } 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
|
-
const DATE_PRESETS = ['thisMonth', 'lastMonth', 'last30', 'thisYear'] as const
|
|
20
|
-
type DatePreset = (typeof DATE_PRESETS)[number]
|
|
21
|
-
|
|
22
|
-
const isoDate = (d: Date) =>
|
|
23
|
-
`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
|
|
24
|
-
|
|
25
|
-
/** Local-time boundaries so "this month" means the user's month, not UTC's. */
|
|
26
|
-
function presetRange(preset: DatePreset): { from: string; to: string } {
|
|
27
|
-
const now = new Date()
|
|
28
|
-
switch (preset) {
|
|
29
|
-
case 'thisMonth':
|
|
30
|
-
return { from: isoDate(new Date(now.getFullYear(), now.getMonth(), 1)), to: isoDate(new Date(now.getFullYear(), now.getMonth() + 1, 0)) }
|
|
31
|
-
case 'lastMonth':
|
|
32
|
-
return { from: isoDate(new Date(now.getFullYear(), now.getMonth() - 1, 1)), to: isoDate(new Date(now.getFullYear(), now.getMonth(), 0)) }
|
|
33
|
-
case 'last30': {
|
|
34
|
-
const from = new Date(now)
|
|
35
|
-
from.setDate(from.getDate() - 29)
|
|
36
|
-
return { from: isoDate(from), to: isoDate(now) }
|
|
37
|
-
}
|
|
38
|
-
case 'thisYear':
|
|
39
|
-
return { from: isoDate(new Date(now.getFullYear(), 0, 1)), to: isoDate(new Date(now.getFullYear(), 11, 31)) }
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function StatusBadge({ status }: { status: string }) {
|
|
44
|
-
const t = useTranslation()
|
|
45
|
-
const opt = STATUS_OPTIONS.find((o) => o.value === status)
|
|
46
|
-
const Icon = opt?.icon ?? CircleDashed
|
|
47
|
-
return (
|
|
48
|
-
<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'}`}>
|
|
49
|
-
<Icon className="h-2.5 w-2.5" /> {opt ? t(opt.labelKey) : status}
|
|
50
|
-
</span>
|
|
51
|
-
)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function useInvoiceColumns(currency: { code: string; locale: string; symbol: string }): ColumnDef<Invoice, any>[] {
|
|
55
|
-
const t = useTranslation()
|
|
56
|
-
return useMemo(() => [
|
|
57
|
-
{
|
|
58
|
-
accessorKey: 'fiscalNumber', header: '#', size: 110,
|
|
59
|
-
cell: ({ getValue }) => {
|
|
60
|
-
const num = getValue() as string | undefined
|
|
61
|
-
return num ? <span className="text-xs font-mono text-muted-foreground">#{num}</span> : <span className="text-muted-foreground">—</span>
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
accessorKey: 'invoiceDate', header: t('financial.invoice.columnDate'),
|
|
66
|
-
cell: ({ getValue }) => <span className="text-muted-foreground text-xs">{getValue() as string}</span>,
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
id: 'description', header: t('financial.invoice.columnDescription'),
|
|
70
|
-
cell: ({ row }) => (
|
|
71
|
-
<div onClick={(e) => e.stopPropagation()}>
|
|
72
|
-
{row.original.contactName ? (
|
|
73
|
-
<PersonLink personId={row.original.contactId} name={row.original.contactName} size="sm" className="text-sm font-medium" />
|
|
74
|
-
) : (
|
|
75
|
-
<p className="font-medium">—</p>
|
|
76
|
-
)}
|
|
77
|
-
{row.original.itemsSummary && <p className="text-[10px] text-muted-foreground truncate max-w-[250px]">{row.original.itemsSummary}</p>}
|
|
78
|
-
</div>
|
|
79
|
-
),
|
|
80
|
-
},
|
|
81
|
-
{
|
|
82
|
-
accessorKey: 'totalAmount', header: t('financial.invoice.columnAmount'), enableSorting: true,
|
|
83
|
-
cell: ({ row }) => (
|
|
84
|
-
<span className={`font-medium ${row.original.status === 'cancelled' ? 'line-through text-muted-foreground' : ''}`}>
|
|
85
|
-
{formatCurrency(row.original.totalAmount, currency)}
|
|
86
|
-
</span>
|
|
87
|
-
),
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
accessorKey: 'paidAmount', header: t('financial.invoice.columnPaid'),
|
|
91
|
-
cell: ({ row }) => (
|
|
92
|
-
<span className={row.original.status === 'cancelled' ? 'line-through text-muted-foreground' : 'text-muted-foreground'}>
|
|
93
|
-
{formatCurrency(row.original.paidAmount, currency)}
|
|
94
|
-
</span>
|
|
95
|
-
),
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
accessorKey: 'status', header: t('financial.invoice.columnStatus'),
|
|
99
|
-
cell: ({ getValue }) => <StatusBadge status={getValue() as string} />,
|
|
100
|
-
},
|
|
101
|
-
{
|
|
102
|
-
accessorKey: 'totalInstallments', header: t('financial.invoice.columnInst'),
|
|
103
|
-
cell: ({ getValue }) => <span className="text-muted-foreground">{getValue() as number}</span>,
|
|
104
|
-
},
|
|
105
|
-
], [currency, t])
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export function InvoiceListView({ direction, onNew, onEdit }: {
|
|
109
|
-
direction: TransactionDirection
|
|
110
|
-
onNew?: () => void
|
|
111
|
-
onEdit?: (id: string) => void
|
|
112
|
-
}) {
|
|
113
|
-
const t = useTranslation()
|
|
114
|
-
// Role-side create gate: hide the "new invoice" affordance (header + empty-state
|
|
115
|
-
// action) when the role lacks `create` — the module's single route only guards read.
|
|
116
|
-
const can = usePermissionOptional()
|
|
117
|
-
const onNewGated = can('financial', 'create') ? onNew : undefined
|
|
118
|
-
const { currency } = useFinancialConfig()
|
|
119
|
-
const provider = useFinancialProvider()
|
|
120
|
-
const invoices = useFinancialStore((s) => s.invoices)
|
|
121
|
-
const invoicesTotal = useFinancialStore((s) => s.invoicesTotal)
|
|
122
|
-
const invoicesLoading = useFinancialStore((s) => s.invoicesLoading)
|
|
123
|
-
const fetchInvoices = useFinancialStore((s) => s.fetchInvoices)
|
|
124
|
-
|
|
125
|
-
const [search, setSearch] = useState('')
|
|
126
|
-
const [statusFilter, setStatusFilter] = useState<InvoiceStatus[]>([])
|
|
127
|
-
const [accountId, setAccountId] = useState('')
|
|
128
|
-
const [costCenterId, setCostCenterId] = useState('')
|
|
129
|
-
const [accounts, setAccounts] = useState<ChartOfAccountsNode[]>([])
|
|
130
|
-
const [costCenters, setCostCenters] = useState<CostCenter[]>([])
|
|
131
|
-
const [datePreset, setDatePreset] = useState<DatePreset | 'all' | 'custom'>('all')
|
|
132
|
-
const [dateFrom, setDateFrom] = useState('')
|
|
133
|
-
const [dateTo, setDateTo] = useState('')
|
|
134
|
-
|
|
135
|
-
// Open-ended on either side: a missing bound becomes a sentinel the providers can compare against.
|
|
136
|
-
const dateRange = useMemo(() => {
|
|
137
|
-
if (!dateFrom && !dateTo) return undefined
|
|
138
|
-
return { from: dateFrom || '0001-01-01', to: dateTo || '9999-12-31' }
|
|
139
|
-
}, [dateFrom, dateTo])
|
|
140
|
-
|
|
141
|
-
const applyPreset = (value: DatePreset | 'all' | 'custom') => {
|
|
142
|
-
setDatePreset(value)
|
|
143
|
-
if (value === 'all') { setDateFrom(''); setDateTo(''); return }
|
|
144
|
-
if (value === 'custom') return
|
|
145
|
-
const range = presetRange(value)
|
|
146
|
-
setDateFrom(range.from)
|
|
147
|
-
setDateTo(range.to)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
useEffect(() => {
|
|
151
|
-
let active = true
|
|
152
|
-
Promise.all([provider.getChartOfAccounts(), provider.getCostCenters()])
|
|
153
|
-
.then(([accountRows, costCenterRows]) => {
|
|
154
|
-
if (!active) return
|
|
155
|
-
setAccounts(accountRows.filter((account) => account.nodeType === 'leaf'))
|
|
156
|
-
setCostCenters(costCenterRows)
|
|
157
|
-
})
|
|
158
|
-
.catch(() => {
|
|
159
|
-
if (!active) return
|
|
160
|
-
setAccounts([])
|
|
161
|
-
setCostCenters([])
|
|
162
|
-
})
|
|
163
|
-
return () => { active = false }
|
|
164
|
-
}, [provider])
|
|
165
|
-
|
|
166
|
-
useEffect(() => {
|
|
167
|
-
fetchInvoices({
|
|
168
|
-
direction,
|
|
169
|
-
status: statusFilter.length > 0 ? statusFilter : undefined,
|
|
170
|
-
search: search || undefined,
|
|
171
|
-
accountId: accountId || undefined,
|
|
172
|
-
costCenterId: costCenterId || undefined,
|
|
173
|
-
dateRange,
|
|
174
|
-
})
|
|
175
|
-
}, [direction, statusFilter, search, accountId, costCenterId, dateRange])
|
|
176
|
-
|
|
177
|
-
const filtered = invoices.filter((inv) => inv.direction === direction)
|
|
178
|
-
const listTitle = direction === 'debit' ? t('financial.invoice.accountsPayable') : t('financial.invoice.accountsReceivable')
|
|
179
|
-
const columns = useInvoiceColumns(currency)
|
|
180
|
-
const tags = useMemo(
|
|
181
|
-
() => STATUS_OPTIONS.filter((o) => o.value !== 'cancelled').map((o) => ({ value: o.value, label: t(o.labelKey) })),
|
|
182
|
-
[t],
|
|
183
|
-
)
|
|
184
|
-
|
|
185
|
-
// Summary totals from currently visible data
|
|
186
|
-
const summary = useMemo(() => {
|
|
187
|
-
let totalAmount = 0
|
|
188
|
-
let paidAmount = 0
|
|
189
|
-
let openAmount = 0
|
|
190
|
-
for (const inv of filtered) {
|
|
191
|
-
if (inv.status === 'cancelled') continue
|
|
192
|
-
totalAmount += inv.totalAmount
|
|
193
|
-
paidAmount += inv.paidAmount
|
|
194
|
-
}
|
|
195
|
-
openAmount = totalAmount - paidAmount
|
|
196
|
-
return { totalAmount, paidAmount, openAmount }
|
|
197
|
-
}, [filtered])
|
|
198
|
-
|
|
199
|
-
const receivedLabel = direction === 'debit' ? t('financial.invoice.summaryPaid') : t('financial.invoice.summaryReceived')
|
|
200
|
-
const pendingLabel = direction === 'debit' ? t('financial.invoice.summaryPayable') : t('financial.invoice.summaryReceivable')
|
|
201
|
-
|
|
202
|
-
return (
|
|
203
|
-
<div className="space-y-4">
|
|
204
|
-
<SubpageHeader
|
|
205
|
-
title={listTitle}
|
|
206
|
-
subtitle={t('financial.invoice.invoices', { count: filtered.length })}
|
|
207
|
-
/>
|
|
208
|
-
<div className="flex flex-wrap items-end gap-3 rounded-lg border bg-muted/20 px-4 py-3">
|
|
209
|
-
<label className="flex min-w-[180px] flex-col gap-1 text-xs font-medium text-muted-foreground">
|
|
210
|
-
{t('financial.invoice.filterPeriod')}
|
|
211
|
-
<select
|
|
212
|
-
className="h-9 rounded-md border bg-background px-3 text-sm text-foreground"
|
|
213
|
-
value={datePreset}
|
|
214
|
-
onChange={(event) => applyPreset(event.target.value as DatePreset | 'all' | 'custom')}
|
|
215
|
-
>
|
|
216
|
-
<option value="all">{t('financial.invoice.filterAllPeriods')}</option>
|
|
217
|
-
<option value="thisMonth">{t('financial.invoice.filterThisMonth')}</option>
|
|
218
|
-
<option value="lastMonth">{t('financial.invoice.filterLastMonth')}</option>
|
|
219
|
-
<option value="last30">{t('financial.invoice.filterLast30')}</option>
|
|
220
|
-
<option value="thisYear">{t('financial.invoice.filterThisYear')}</option>
|
|
221
|
-
<option value="custom">{t('financial.invoice.filterCustom')}</option>
|
|
222
|
-
</select>
|
|
223
|
-
</label>
|
|
224
|
-
<label className="flex flex-col gap-1 text-xs font-medium text-muted-foreground">
|
|
225
|
-
{t('financial.invoice.filterFrom')}
|
|
226
|
-
<input
|
|
227
|
-
type="date"
|
|
228
|
-
className="h-9 rounded-md border bg-background px-3 text-sm text-foreground"
|
|
229
|
-
value={dateFrom}
|
|
230
|
-
max={dateTo || undefined}
|
|
231
|
-
onChange={(event) => { setDateFrom(event.target.value); setDatePreset('custom') }}
|
|
232
|
-
/>
|
|
233
|
-
</label>
|
|
234
|
-
<label className="flex flex-col gap-1 text-xs font-medium text-muted-foreground">
|
|
235
|
-
{t('financial.invoice.filterTo')}
|
|
236
|
-
<input
|
|
237
|
-
type="date"
|
|
238
|
-
className="h-9 rounded-md border bg-background px-3 text-sm text-foreground"
|
|
239
|
-
value={dateTo}
|
|
240
|
-
min={dateFrom || undefined}
|
|
241
|
-
onChange={(event) => { setDateTo(event.target.value); setDatePreset('custom') }}
|
|
242
|
-
/>
|
|
243
|
-
</label>
|
|
244
|
-
{(dateFrom || dateTo) && (
|
|
245
|
-
<button
|
|
246
|
-
type="button"
|
|
247
|
-
className="h-9 rounded-md px-2 text-xs font-medium text-muted-foreground hover:text-foreground"
|
|
248
|
-
onClick={() => applyPreset('all')}
|
|
249
|
-
>
|
|
250
|
-
{t('financial.invoice.filterClear')}
|
|
251
|
-
</button>
|
|
252
|
-
)}
|
|
253
|
-
{accounts.length > 0 && (
|
|
254
|
-
<label className="flex min-w-[220px] flex-col gap-1 text-xs font-medium text-muted-foreground">
|
|
255
|
-
{t('financial.invoice.filterAccount')}
|
|
256
|
-
<select
|
|
257
|
-
className="h-9 rounded-md border bg-background px-3 text-sm text-foreground"
|
|
258
|
-
value={accountId}
|
|
259
|
-
onChange={(event) => setAccountId(event.target.value)}
|
|
260
|
-
>
|
|
261
|
-
<option value="">{t('financial.invoice.filterAllAccounts')}</option>
|
|
262
|
-
{accounts.map((account) => (
|
|
263
|
-
<option key={account.id} value={account.id}>{account.code} - {account.name}</option>
|
|
264
|
-
))}
|
|
265
|
-
</select>
|
|
266
|
-
</label>
|
|
267
|
-
)}
|
|
268
|
-
{costCenters.length > 0 && (
|
|
269
|
-
<label className="flex min-w-[220px] flex-col gap-1 text-xs font-medium text-muted-foreground">
|
|
270
|
-
{t('financial.invoice.filterCostCenter')}
|
|
271
|
-
<select
|
|
272
|
-
className="h-9 rounded-md border bg-background px-3 text-sm text-foreground"
|
|
273
|
-
value={costCenterId}
|
|
274
|
-
onChange={(event) => setCostCenterId(event.target.value)}
|
|
275
|
-
>
|
|
276
|
-
<option value="">{t('financial.invoice.filterAllCostCenters')}</option>
|
|
277
|
-
{costCenters.map((costCenter) => (
|
|
278
|
-
<option key={costCenter.id} value={costCenter.id}>{costCenter.code} - {costCenter.name}</option>
|
|
279
|
-
))}
|
|
280
|
-
</select>
|
|
281
|
-
</label>
|
|
282
|
-
)}
|
|
283
|
-
</div>
|
|
284
|
-
<ListView<Invoice>
|
|
285
|
-
columns={columns}
|
|
286
|
-
data={filtered}
|
|
287
|
-
loading={invoicesLoading}
|
|
288
|
-
searchPlaceholder={t('financial.invoice.searchPlaceholder')}
|
|
289
|
-
search={search}
|
|
290
|
-
onSearchChange={setSearch}
|
|
291
|
-
searchDebounce={0}
|
|
292
|
-
tags={tags}
|
|
293
|
-
activeTag={statusFilter.length === 1 ? statusFilter[0] : undefined}
|
|
294
|
-
onTagChange={(v) => setStatusFilter(v ? [v as InvoiceStatus] : [])}
|
|
295
|
-
newLabel={t('financial.invoice.new')}
|
|
296
|
-
onNew={onNewGated}
|
|
297
|
-
onRowClick={(row) => onEdit?.(row.id)}
|
|
298
|
-
emptyIcon={FileText}
|
|
299
|
-
emptyMessage={t('financial.invoice.noInvoices')}
|
|
300
|
-
emptyActionLabel={onNewGated ? t('financial.invoice.createFirst') : undefined}
|
|
301
|
-
onEmptyAction={onNewGated}
|
|
302
|
-
/>
|
|
303
|
-
{filtered.length > 0 && !invoicesLoading && (
|
|
304
|
-
<div className="flex items-center justify-end gap-6 rounded-lg border bg-muted/30 px-5 py-3 text-sm">
|
|
305
|
-
<div className="flex items-center gap-2">
|
|
306
|
-
<span className="text-muted-foreground">{t('financial.invoice.summaryTotal')}</span>
|
|
307
|
-
<span className="font-semibold">{formatCurrency(summary.totalAmount, currency)}</span>
|
|
308
|
-
</div>
|
|
309
|
-
<div className="h-4 w-px bg-border" />
|
|
310
|
-
<div className="flex items-center gap-2">
|
|
311
|
-
<span className="text-muted-foreground">{receivedLabel}</span>
|
|
312
|
-
<span className="font-semibold text-success">{formatCurrency(summary.paidAmount, currency)}</span>
|
|
313
|
-
</div>
|
|
314
|
-
<div className="h-4 w-px bg-border" />
|
|
315
|
-
<div className="flex items-center gap-2">
|
|
316
|
-
<span className="text-muted-foreground">{pendingLabel}</span>
|
|
317
|
-
<span className="font-semibold text-warning">{formatCurrency(summary.openAmount, currency)}</span>
|
|
318
|
-
</div>
|
|
319
|
-
</div>
|
|
320
|
-
)}
|
|
321
|
-
</div>
|
|
322
|
-
)
|
|
323
|
-
}
|
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
import React, { useCallback, useEffect, useState } from 'react'
|
|
2
|
-
import { FileText, Plus, Send, Ban, ExternalLink, FileCheck2 } from 'lucide-react'
|
|
3
|
-
import { SubpageHeader, Button, Input, Skeleton } from '@fayz-ai/ui'
|
|
4
|
-
import { PermissionGate } from '@fayz-ai/saas'
|
|
5
|
-
import { useFinancialProvider, useFinancialConfig, formatCurrency } from '../FinancialContext'
|
|
6
|
-
import type { NotaFiscal, NotaFiscalModel, NotaFiscalStatus, CreateNotaFiscalInput } from '../types'
|
|
7
|
-
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
// Notas Fiscais — fiscal-document tracking (NF-e / NFC-e / NFS-e). Structure +
|
|
10
|
-
// UI; issuing is delegated (provider.issueNota assigns número/série locally
|
|
11
|
-
// until a SEFAZ emissor is wired). Draft → Emitir → Emitida, with cancel and
|
|
12
|
-
// links to the authorized XML / DANFE once an emissor populates them.
|
|
13
|
-
// ---------------------------------------------------------------------------
|
|
14
|
-
|
|
15
|
-
const MODEL_LABEL: Record<NotaFiscalModel, string> = {
|
|
16
|
-
nfe: 'NF-e', nfce: 'NFC-e', nfse: 'NFS-e',
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const STATUS_META: Record<NotaFiscalStatus, { label: string; className: string }> = {
|
|
20
|
-
draft: { label: 'Rascunho', className: 'bg-muted text-muted-foreground' },
|
|
21
|
-
processing: { label: 'Processando', className: 'bg-blue-100 text-blue-800' },
|
|
22
|
-
issued: { label: 'Emitida', className: 'bg-emerald-100 text-emerald-800' },
|
|
23
|
-
cancelled: { label: 'Cancelada', className: 'bg-red-100 text-red-700' },
|
|
24
|
-
error: { label: 'Erro', className: 'bg-orange-100 text-orange-700' },
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function NotasFiscaisView() {
|
|
28
|
-
const provider = useFinancialProvider()
|
|
29
|
-
const { currency } = useFinancialConfig()
|
|
30
|
-
const [notas, setNotas] = useState<NotaFiscal[]>([])
|
|
31
|
-
const [loading, setLoading] = useState(true)
|
|
32
|
-
const [creating, setCreating] = useState(false)
|
|
33
|
-
const [busy, setBusy] = useState<string | null>(null)
|
|
34
|
-
|
|
35
|
-
const supported = typeof provider.getNotas === 'function'
|
|
36
|
-
|
|
37
|
-
const reload = useCallback(async () => {
|
|
38
|
-
if (!provider.getNotas) { setLoading(false); return }
|
|
39
|
-
setLoading(true)
|
|
40
|
-
try {
|
|
41
|
-
const res = await provider.getNotas({ pageSize: 100 })
|
|
42
|
-
setNotas(res.data)
|
|
43
|
-
} finally {
|
|
44
|
-
setLoading(false)
|
|
45
|
-
}
|
|
46
|
-
}, [provider])
|
|
47
|
-
|
|
48
|
-
useEffect(() => { void reload() }, [reload])
|
|
49
|
-
|
|
50
|
-
const create = async (input: CreateNotaFiscalInput) => {
|
|
51
|
-
if (!provider.createNota) return
|
|
52
|
-
await provider.createNota(input)
|
|
53
|
-
setCreating(false)
|
|
54
|
-
await reload()
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const issue = async (id: string) => {
|
|
58
|
-
if (!provider.issueNota) return
|
|
59
|
-
setBusy(id)
|
|
60
|
-
try { await provider.issueNota(id); await reload() } finally { setBusy(null) }
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const cancel = async (id: string) => {
|
|
64
|
-
if (!provider.cancelNota) return
|
|
65
|
-
setBusy(id)
|
|
66
|
-
try { await provider.cancelNota(id); await reload() } finally { setBusy(null) }
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (!supported) {
|
|
70
|
-
return (
|
|
71
|
-
<div className="p-1">
|
|
72
|
-
<SubpageHeader title="Notas Fiscais" subtitle="Emissão e acompanhamento de NF-e / NFC-e / NFS-e" icon="FileText" />
|
|
73
|
-
<p className="mt-4 text-sm text-muted-foreground">Este provedor financeiro ainda não expõe notas fiscais.</p>
|
|
74
|
-
</div>
|
|
75
|
-
)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return (
|
|
79
|
-
<div className="p-1">
|
|
80
|
-
<SubpageHeader
|
|
81
|
-
title="Notas Fiscais"
|
|
82
|
-
subtitle="Emissão e acompanhamento de NF-e / NFC-e / NFS-e"
|
|
83
|
-
icon="FileText"
|
|
84
|
-
actions={
|
|
85
|
-
<PermissionGate feature="financial" action="create">
|
|
86
|
-
<Button size="sm" onClick={() => setCreating(true)}>
|
|
87
|
-
<Plus className="mr-1.5 h-4 w-4" /> Nova nota
|
|
88
|
-
</Button>
|
|
89
|
-
</PermissionGate>
|
|
90
|
-
}
|
|
91
|
-
/>
|
|
92
|
-
|
|
93
|
-
<div className="mt-3 rounded-md border border-amber-300/50 bg-amber-50 px-3 py-2 text-xs text-amber-800 dark:bg-amber-950/30 dark:text-amber-300">
|
|
94
|
-
Emissão fiscal (SEFAZ) ainda não integrada — "Emitir" atribui número/série localmente para acompanhamento. A integração com o emissor entra sem mudar esta tela.
|
|
95
|
-
</div>
|
|
96
|
-
|
|
97
|
-
{creating && <CreateForm onCancel={() => setCreating(false)} onCreate={create} />}
|
|
98
|
-
|
|
99
|
-
{loading ? (
|
|
100
|
-
<div className="mt-4 space-y-2">
|
|
101
|
-
{Array.from({ length: 4 }).map((_, i) => (
|
|
102
|
-
<div key={i} className="flex items-center gap-3 rounded-card border border-border bg-card p-3">
|
|
103
|
-
<Skeleton className="h-9 w-9 shrink-0 rounded-full" />
|
|
104
|
-
<div className="min-w-0 flex-1 space-y-2">
|
|
105
|
-
<Skeleton className="h-3.5 w-40 rounded" />
|
|
106
|
-
<Skeleton className="h-3 w-56 rounded" />
|
|
107
|
-
</div>
|
|
108
|
-
<Skeleton className="h-8 w-20 shrink-0 rounded-md" />
|
|
109
|
-
</div>
|
|
110
|
-
))}
|
|
111
|
-
</div>
|
|
112
|
-
) : notas.length === 0 ? (
|
|
113
|
-
<div className="mt-4 rounded-card border border-dashed border-border p-8 text-center">
|
|
114
|
-
<FileText className="mx-auto h-8 w-8 text-muted-foreground" />
|
|
115
|
-
<p className="mt-2 text-sm font-medium text-foreground">Nenhuma nota fiscal ainda</p>
|
|
116
|
-
<p className="mt-1 text-xs text-muted-foreground">Crie uma nota a partir de um pedido ou manualmente.</p>
|
|
117
|
-
</div>
|
|
118
|
-
) : (
|
|
119
|
-
<div className="mt-4 space-y-2">
|
|
120
|
-
{notas.map((n) => {
|
|
121
|
-
const meta = STATUS_META[n.status]
|
|
122
|
-
return (
|
|
123
|
-
<div key={n.id} className="flex items-center gap-3 rounded-card border border-border bg-card p-3">
|
|
124
|
-
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-primary/10">
|
|
125
|
-
<FileText className="h-4 w-4 text-primary" />
|
|
126
|
-
</div>
|
|
127
|
-
<div className="min-w-0 flex-1">
|
|
128
|
-
<div className="flex items-center gap-2">
|
|
129
|
-
<span className="text-sm font-medium text-foreground">
|
|
130
|
-
{MODEL_LABEL[n.model]} {n.number ? `#${n.number}` : '(rascunho)'}
|
|
131
|
-
</span>
|
|
132
|
-
<span className={`rounded-full px-2 py-0.5 text-xs font-medium ${meta.className}`}>{meta.label}</span>
|
|
133
|
-
</div>
|
|
134
|
-
<p className="truncate text-xs text-muted-foreground">
|
|
135
|
-
{n.contactName || 'Sem destinatário'} · {formatCurrency(n.totalAmount, currency)}
|
|
136
|
-
{n.issuedAt ? ` · ${n.issuedAt.slice(0, 10)}` : ''}
|
|
137
|
-
</p>
|
|
138
|
-
</div>
|
|
139
|
-
<div className="flex shrink-0 items-center gap-1">
|
|
140
|
-
{n.danfeUrl && (
|
|
141
|
-
<Button size="sm" variant="ghost" onClick={() => window.open(n.danfeUrl!, '_blank', 'noopener,noreferrer')} title="DANFE">
|
|
142
|
-
<FileCheck2 className="h-4 w-4" />
|
|
143
|
-
</Button>
|
|
144
|
-
)}
|
|
145
|
-
{n.xmlUrl && (
|
|
146
|
-
<Button size="sm" variant="ghost" onClick={() => window.open(n.xmlUrl!, '_blank', 'noopener,noreferrer')} title="XML">
|
|
147
|
-
<ExternalLink className="h-4 w-4" />
|
|
148
|
-
</Button>
|
|
149
|
-
)}
|
|
150
|
-
{n.status === 'draft' && (
|
|
151
|
-
<PermissionGate feature="financial" action="edit">
|
|
152
|
-
<Button size="sm" disabled={busy === n.id} onClick={() => issue(n.id)}>
|
|
153
|
-
<Send className="mr-1.5 h-3.5 w-3.5" /> Emitir
|
|
154
|
-
</Button>
|
|
155
|
-
</PermissionGate>
|
|
156
|
-
)}
|
|
157
|
-
{n.status === 'issued' && (
|
|
158
|
-
<PermissionGate feature="financial" action="edit">
|
|
159
|
-
<Button size="sm" variant="ghost" disabled={busy === n.id} onClick={() => cancel(n.id)} title="Cancelar">
|
|
160
|
-
<Ban className="h-4 w-4 text-red-500" />
|
|
161
|
-
</Button>
|
|
162
|
-
</PermissionGate>
|
|
163
|
-
)}
|
|
164
|
-
</div>
|
|
165
|
-
</div>
|
|
166
|
-
)
|
|
167
|
-
})}
|
|
168
|
-
</div>
|
|
169
|
-
)}
|
|
170
|
-
</div>
|
|
171
|
-
)
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function CreateForm({ onCancel, onCreate }: { onCancel: () => void; onCreate: (input: CreateNotaFiscalInput) => void }) {
|
|
175
|
-
const [model, setModel] = useState<NotaFiscalModel>('nfe')
|
|
176
|
-
const [contactName, setContactName] = useState('')
|
|
177
|
-
const [total, setTotal] = useState('')
|
|
178
|
-
const [observations, setObservations] = useState('')
|
|
179
|
-
|
|
180
|
-
const models: NotaFiscalModel[] = ['nfe', 'nfce', 'nfse']
|
|
181
|
-
|
|
182
|
-
return (
|
|
183
|
-
<div className="mt-4 rounded-card border border-border bg-card p-4">
|
|
184
|
-
<p className="mb-3 text-sm font-semibold text-foreground">Nova nota fiscal</p>
|
|
185
|
-
<div className="mb-3 flex flex-wrap gap-2">
|
|
186
|
-
{models.map((m) => (
|
|
187
|
-
<button key={m} type="button" onClick={() => setModel(m)} className={`rounded-full px-3 py-1 text-xs font-medium ${m === model ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground hover:text-foreground'}`}>
|
|
188
|
-
{MODEL_LABEL[m]}
|
|
189
|
-
</button>
|
|
190
|
-
))}
|
|
191
|
-
</div>
|
|
192
|
-
<div className="flex flex-col gap-2 sm:flex-row">
|
|
193
|
-
<Input value={contactName} placeholder="Destinatário" onChange={(e) => setContactName(e.target.value)} className="flex-1" />
|
|
194
|
-
<Input value={total} placeholder="Valor total" inputMode="decimal" onChange={(e) => setTotal(e.target.value)} className="sm:w-40" />
|
|
195
|
-
</div>
|
|
196
|
-
<Input value={observations} placeholder="Observações (opcional)" onChange={(e) => setObservations(e.target.value)} className="mt-2" />
|
|
197
|
-
<div className="mt-3 flex gap-2">
|
|
198
|
-
<Button
|
|
199
|
-
size="sm"
|
|
200
|
-
disabled={!total || Number.isNaN(Number(total))}
|
|
201
|
-
onClick={() => onCreate({ model, contactName: contactName || undefined, totalAmount: Number(total) || 0, observations: observations || undefined })}
|
|
202
|
-
>
|
|
203
|
-
Criar rascunho
|
|
204
|
-
</Button>
|
|
205
|
-
<Button size="sm" variant="outline" onClick={onCancel}>Cancelar</Button>
|
|
206
|
-
</div>
|
|
207
|
-
</div>
|
|
208
|
-
)
|
|
209
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import { useTranslation } from '@fayz-ai/core'
|
|
3
|
-
import { InvoiceListView } from './InvoiceListView'
|
|
4
|
-
import { InvoiceFormView } from './InvoiceFormView'
|
|
5
|
-
import { InvoiceDetailView } from './InvoiceDetailView'
|
|
6
|
-
import { referrerLabel } from './navReferrer'
|
|
7
|
-
import type { ViewIntent } from '../FinancialPage'
|
|
8
|
-
|
|
9
|
-
export function PayablesView({ intent, onNavigate, previousView, back }: {
|
|
10
|
-
intent: ViewIntent
|
|
11
|
-
onNavigate: (view: string) => void
|
|
12
|
-
/** The in-module view the user came from — drives the context-aware back link. */
|
|
13
|
-
previousView?: string | null
|
|
14
|
-
/** Return to the actual previous page (falls back to the given view). */
|
|
15
|
-
back?: (fallbackView?: string) => void
|
|
16
|
-
}) {
|
|
17
|
-
const t = useTranslation()
|
|
18
|
-
|
|
19
|
-
if (intent.mode === 'new') {
|
|
20
|
-
return <InvoiceFormView direction="debit" onSaved={(id) => onNavigate(id ? `payables-detail:${id}` : 'payables-list')} />
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (intent.mode === 'edit' && intent.editId) {
|
|
24
|
-
return <InvoiceFormView direction="debit" editId={intent.editId} onSaved={() => onNavigate(`payables-detail:${intent.editId}`)} />
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (intent.mode === 'detail' && intent.editId) {
|
|
28
|
-
// Back to where the user actually came from (e.g. statements), else the list.
|
|
29
|
-
const prevLabel = referrerLabel(previousView, t)
|
|
30
|
-
return (
|
|
31
|
-
<InvoiceDetailView
|
|
32
|
-
invoiceId={intent.editId}
|
|
33
|
-
direction="debit"
|
|
34
|
-
backLabel={prevLabel ?? undefined}
|
|
35
|
-
onBack={prevLabel && back ? () => back('payables-list') : () => onNavigate('payables-list')}
|
|
36
|
-
onEdit={() => onNavigate(`payables-edit:${intent.editId}`)}
|
|
37
|
-
/>
|
|
38
|
-
)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return (
|
|
42
|
-
<InvoiceListView
|
|
43
|
-
direction="debit"
|
|
44
|
-
onNew={() => onNavigate('payables-new')}
|
|
45
|
-
onEdit={(id) => onNavigate(`payables-detail:${id}`)}
|
|
46
|
-
/>
|
|
47
|
-
)
|
|
48
|
-
}
|