@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,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
- }