@fayz-ai/plugin-financial 0.8.3 → 0.8.4
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/mock.d.ts.map +1 -1
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/data/tables.d.ts +1 -0
- package/dist/data/tables.d.ts.map +1 -1
- package/dist/data/types.d.ts +8 -1
- package/dist/data/types.d.ts.map +1 -1
- package/dist/index.cjs +494 -188
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +323 -17
- 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/migrations/index.d.ts +1 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/types.d.ts +49 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/views/InvoiceDetailView.d.ts.map +1 -1
- package/dist/views/NotasFiscaisView.d.ts +3 -0
- package/dist/views/NotasFiscaisView.d.ts.map +1 -0
- package/package.json +4 -4
- package/src/FinancialContext.tsx +2 -0
- package/src/FinancialPage.tsx +14 -0
- package/src/data/mock.ts +69 -0
- package/src/data/supabase.ts +79 -0
- package/src/data/tables.ts +1 -0
- package/src/data/types.ts +13 -0
- package/src/index.ts +6 -0
- package/src/locales/en.ts +1 -0
- package/src/locales/pt-BR.ts +1 -0
- package/src/migrations/011_nfe.sql +58 -0
- package/src/migrations/index.ts +62 -1
- package/src/types.ts +63 -0
- package/src/views/InvoiceDetailView.tsx +3 -1
- package/src/views/NotasFiscaisView.tsx +209 -0
package/src/migrations/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// AUTO-GENERATED from 000_plg_rename.sql, 001_financial_base.sql, 002_chart_of_accounts.sql, 003_card_brands.sql, 004_order_to_cash.sql, 005_seed_defaults.sql, 006_rls_policies.sql, 006b_extract_fee_amount.sql, 007_reconciliation.sql, 007b_movement_payment_method_type.sql, 008_split_payment_movements.sql, 009_agent_rpcs.sql, 010_invoice_internal.sql — regenerate with scripts/embed-migrations.mjs
|
|
1
|
+
// AUTO-GENERATED from 000_plg_rename.sql, 001_financial_base.sql, 002_chart_of_accounts.sql, 003_card_brands.sql, 004_order_to_cash.sql, 005_seed_defaults.sql, 006_rls_policies.sql, 006b_extract_fee_amount.sql, 007_reconciliation.sql, 007b_movement_payment_method_type.sql, 008_split_payment_movements.sql, 009_agent_rpcs.sql, 010_invoice_internal.sql, 011_nfe.sql — regenerate with scripts/embed-migrations.mjs
|
|
2
2
|
// SQL files are the source of truth; this inline copy lets the manifest declare
|
|
3
3
|
// migrations as data. Do not edit by hand — run the embed script instead.
|
|
4
4
|
|
|
@@ -845,6 +845,66 @@ GRANT EXECUTE ON FUNCTION public.fn_invoice_from_order(uuid, date, text, int, te
|
|
|
845
845
|
NOTIFY pgrst, 'reload schema';
|
|
846
846
|
`
|
|
847
847
|
|
|
848
|
+
export const MIGRATION_011_NFE = `-- 011_nfe.sql — Nota Fiscal Eletrônica (NF-e / NFC-e / NFS-e)
|
|
849
|
+
-- ----------------------------------------------------------------------------
|
|
850
|
+
-- Fiscal documents the merchant issues, tracked against orders. This migration
|
|
851
|
+
-- ships the STRUCTURE only: the actual authorization with SEFAZ / the prefeitura
|
|
852
|
+
-- is delegated to an external emissor (Focus NFe, NFe.io, PlugNotas…) wired
|
|
853
|
+
-- later behind the provider's issueNota. Rows here carry the número/série,
|
|
854
|
+
-- status, access key and the links to the authorized XML + DANFE once emitted.
|
|
855
|
+
--
|
|
856
|
+
-- Per-tenant RLS, canonical form (tenant_id IN user_tenant_ids()). Idempotent.
|
|
857
|
+
|
|
858
|
+
CREATE TABLE IF NOT EXISTS public.plg_financial_nfe (
|
|
859
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
860
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
861
|
+
-- nfe (55, mercadoria) | nfce (65, consumidor) | nfse (serviço)
|
|
862
|
+
model text NOT NULL DEFAULT 'nfe',
|
|
863
|
+
-- draft | processing | issued | cancelled | error
|
|
864
|
+
status text NOT NULL DEFAULT 'draft',
|
|
865
|
+
number text,
|
|
866
|
+
series text,
|
|
867
|
+
order_id uuid,
|
|
868
|
+
contact_id uuid,
|
|
869
|
+
contact_name text,
|
|
870
|
+
total_amount numeric(14,2) NOT NULL DEFAULT 0,
|
|
871
|
+
issued_at timestamptz,
|
|
872
|
+
access_key text,
|
|
873
|
+
xml_url text,
|
|
874
|
+
danfe_url text,
|
|
875
|
+
emissor text,
|
|
876
|
+
error_message text,
|
|
877
|
+
observations text,
|
|
878
|
+
metadata jsonb NOT NULL DEFAULT '{}',
|
|
879
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
880
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
881
|
+
);
|
|
882
|
+
|
|
883
|
+
CREATE INDEX IF NOT EXISTS idx_plg_financial_nfe_tenant ON public.plg_financial_nfe(tenant_id);
|
|
884
|
+
CREATE INDEX IF NOT EXISTS idx_plg_financial_nfe_order ON public.plg_financial_nfe(order_id);
|
|
885
|
+
|
|
886
|
+
ALTER TABLE public.plg_financial_nfe ENABLE ROW LEVEL SECURITY;
|
|
887
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_financial_nfe TO authenticated;
|
|
888
|
+
|
|
889
|
+
DO $$
|
|
890
|
+
BEGIN
|
|
891
|
+
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='plg_financial_nfe' AND policyname='plg_financial_nfe_select') THEN
|
|
892
|
+
CREATE POLICY plg_financial_nfe_select ON public.plg_financial_nfe FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
893
|
+
END IF;
|
|
894
|
+
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='plg_financial_nfe' AND policyname='plg_financial_nfe_insert') THEN
|
|
895
|
+
CREATE POLICY plg_financial_nfe_insert ON public.plg_financial_nfe FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
896
|
+
END IF;
|
|
897
|
+
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='plg_financial_nfe' AND policyname='plg_financial_nfe_update') THEN
|
|
898
|
+
CREATE POLICY plg_financial_nfe_update ON public.plg_financial_nfe FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
899
|
+
END IF;
|
|
900
|
+
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='plg_financial_nfe' AND policyname='plg_financial_nfe_delete') THEN
|
|
901
|
+
CREATE POLICY plg_financial_nfe_delete ON public.plg_financial_nfe FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
902
|
+
END IF;
|
|
903
|
+
END $$;
|
|
904
|
+
|
|
905
|
+
NOTIFY pgrst, 'reload schema';
|
|
906
|
+
`
|
|
907
|
+
|
|
848
908
|
export const MIGRATIONS: Array<{ id: string; sql: string }> = [
|
|
849
909
|
{ id: "000_plg_rename", sql: MIGRATION_000_PLG_RENAME },
|
|
850
910
|
{ id: "001_financial_base", sql: MIGRATION_001_FINANCIAL_BASE },
|
|
@@ -859,4 +919,5 @@ export const MIGRATIONS: Array<{ id: string; sql: string }> = [
|
|
|
859
919
|
{ id: "008_split_payment_movements", sql: MIGRATION_008_SPLIT_PAYMENT_MOVEMENTS },
|
|
860
920
|
{ id: "009_agent_rpcs", sql: MIGRATION_009_AGENT_RPCS },
|
|
861
921
|
{ id: "010_invoice_internal", sql: MIGRATION_010_INVOICE_INTERNAL },
|
|
922
|
+
{ id: "011_nfe", sql: MIGRATION_011_NFE },
|
|
862
923
|
]
|
package/src/types.ts
CHANGED
|
@@ -339,6 +339,8 @@ export interface MovementQuery {
|
|
|
339
339
|
status?: MovementStatus | MovementStatus[]
|
|
340
340
|
bankAccountId?: string
|
|
341
341
|
cashSessionId?: string
|
|
342
|
+
/** Scope to a single invoice's movements (bills + payments). */
|
|
343
|
+
invoiceId?: string
|
|
342
344
|
dateRange?: DateRange
|
|
343
345
|
page?: number
|
|
344
346
|
pageSize?: number
|
|
@@ -527,3 +529,64 @@ export interface ReconcileInput {
|
|
|
527
529
|
/** Internal movement to link; omit to accept the line standalone */
|
|
528
530
|
matchedMovementId?: string
|
|
529
531
|
}
|
|
532
|
+
|
|
533
|
+
// ============================================================
|
|
534
|
+
// NOTA FISCAL ELETRÔNICA (NF-e / NFC-e / NFS-e)
|
|
535
|
+
// ------------------------------------------------------------
|
|
536
|
+
// Structure + UI live here now; actual authorization (SEFAZ / prefeitura) is
|
|
537
|
+
// delegated to an external emissor (Focus NFe, NFe.io, PlugNotas…) behind the
|
|
538
|
+
// provider's `issueNota`. Until an emissor is wired, `issueNota` assigns a
|
|
539
|
+
// number/série locally and marks the nota `issued` — enough to track fiscal
|
|
540
|
+
// documents against orders without the emission integration.
|
|
541
|
+
// ============================================================
|
|
542
|
+
|
|
543
|
+
/** 55 = NF-e (mercadoria), 65 = NFC-e (consumidor), nfse = serviço (prefeitura). */
|
|
544
|
+
export type NotaFiscalModel = 'nfe' | 'nfce' | 'nfse'
|
|
545
|
+
export type NotaFiscalStatus = 'draft' | 'processing' | 'issued' | 'cancelled' | 'error'
|
|
546
|
+
|
|
547
|
+
export interface NotaFiscal {
|
|
548
|
+
id: string
|
|
549
|
+
tenantId: string
|
|
550
|
+
model: NotaFiscalModel
|
|
551
|
+
status: NotaFiscalStatus
|
|
552
|
+
/** Número da nota — null while draft (assigned on issue). */
|
|
553
|
+
number: string | null
|
|
554
|
+
series: string | null
|
|
555
|
+
/** Order this nota documents (plg_shop_orders / orders id), if any. */
|
|
556
|
+
orderId?: string
|
|
557
|
+
contactId?: string
|
|
558
|
+
contactName?: string
|
|
559
|
+
totalAmount: number
|
|
560
|
+
issuedAt?: string
|
|
561
|
+
/** Chave de acesso (44 dígitos), when authorized. */
|
|
562
|
+
accessKey?: string
|
|
563
|
+
/** Links to the authorized XML and the DANFE/DANFCE PDF. */
|
|
564
|
+
xmlUrl?: string
|
|
565
|
+
danfeUrl?: string
|
|
566
|
+
/** External emissor id (focusnfe | nfeio | plugnotas | manual). */
|
|
567
|
+
emissor?: string
|
|
568
|
+
errorMessage?: string
|
|
569
|
+
observations?: string
|
|
570
|
+
metadata?: Record<string, unknown>
|
|
571
|
+
createdAt: string
|
|
572
|
+
updatedAt: string
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
export interface CreateNotaFiscalInput {
|
|
576
|
+
model?: NotaFiscalModel
|
|
577
|
+
orderId?: string
|
|
578
|
+
contactId?: string
|
|
579
|
+
contactName?: string
|
|
580
|
+
totalAmount: number
|
|
581
|
+
series?: string
|
|
582
|
+
observations?: string
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
export interface NotaFiscalQuery {
|
|
586
|
+
status?: NotaFiscalStatus | NotaFiscalStatus[]
|
|
587
|
+
model?: NotaFiscalModel
|
|
588
|
+
orderId?: string
|
|
589
|
+
search?: string
|
|
590
|
+
page?: number
|
|
591
|
+
pageSize?: number
|
|
592
|
+
}
|
|
@@ -60,7 +60,9 @@ export function InvoiceDetailView({ invoiceId, direction, onBack, onEdit, backLa
|
|
|
60
60
|
const [inv, invItems, movResult] = await Promise.all([
|
|
61
61
|
provider.getInvoiceById(invoiceId),
|
|
62
62
|
provider.getInvoiceItems(invoiceId),
|
|
63
|
-
|
|
63
|
+
// Scope to THIS invoice (not the tenant-wide first-50 page) so every
|
|
64
|
+
// installment/payment loads regardless of how many movements exist.
|
|
65
|
+
provider.getMovements({ direction, invoiceId, pageSize: 500 }),
|
|
64
66
|
])
|
|
65
67
|
setInvoice(inv)
|
|
66
68
|
setItems(invItems)
|
|
@@ -0,0 +1,209 @@
|
|
|
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
|
+
}
|