@fayz-ai/plugin-fiscal-br 0.3.7 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
- import { recordRailFields, RECORD_GROUP, activeFacet, connectionSettings, connectionStatus, definePlugin, createConnectionStore, settingsStatus, getSupabaseClientOptional } from '@fayz-ai/core';
2
- import { useState, useRef, useEffect, useCallback, useMemo } from 'react';
3
- import { Trash2, Inbox, Loader2, UploadCloud, FileDown, Gift, RefreshCw, AlertTriangle, ChevronRight, Check, Plus, X, ShieldCheck, FileText, ExternalLink } from 'lucide-react';
4
- import { useFilterDraft, Button, PageHeaderActions, Card, FilterPanel, FilterPanelField, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, SearchField, DataTable, SubpageHeader, Skeleton, EmBreve, SearchSelect, Input, DatePicker, Hint, ListFilterBar, ComingSoon } from '@fayz-ai/ui';
1
+ import { recordRailFields, RECORD_GROUP, activeFacet, getSupabaseClientOptional, connectionSettings, connectionStatus, definePlugin, createConnectionStore, settingsStatus, maskCpfCnpj } from '@fayz-ai/core';
2
+ import React5, { useState, useCallback, useEffect, useMemo, useRef } from 'react';
3
+ import { ModalHeader, ModalTitle, ModalBody, Skeleton, AiActionButton, Card, Button, EmBreve, SearchSelect, Input, DatePicker, Hint, ModalFooter, SubpageHeader, useFilterDraft, PageHeaderActions, FilterPanel, FilterPanelField, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, SearchField, DataTable, ListFilterBar, Modal, ModalContent, ComingSoon, CardHeader, CardTitle, CardContent, toast, Badge, CardDescription, Switch } from '@fayz-ai/ui';
5
4
  export { ComingSoon, EmBreve } from '@fayz-ai/ui';
6
- import { useUnitFilter } from '@fayz-ai/admin';
5
+ import { registerDocumentOpener, getActiveTenantId, useUnitFilter, UNIT_DETAIL_ZONES, adminNavigateTo } from '@fayz-ai/admin';
6
+ import { Gift, RefreshCw, Loader2, FileDown, AlertTriangle, ChevronRight, Check, Trash2, Plus, Inbox, UploadCloud, ShieldCheck, FileText, ExternalLink, X, Info } from 'lucide-react';
7
7
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
8
8
 
9
9
  // src/index.ts
@@ -2832,6 +2832,821 @@ END $function$;
2832
2832
  REVOKE ALL ON FUNCTION public.fiscalbr_accept_inbox_document(uuid, uuid) FROM PUBLIC;
2833
2833
  GRANT EXECUTE ON FUNCTION public.fiscalbr_accept_inbox_document(uuid, uuid) TO authenticated;
2834
2834
  `;
2835
+ var MIGRATION_011_O_IMPOSTO_DO_PRODUTO_E_DO_FISCAL = `-- O imposto do PRODUTO mora no fiscal, n\xE3o no estoque.
2836
+ --
2837
+ -- No V1 as dezessete colunas de tributo (NCM, CFOP, CST, PIS, COFINS, CEST, FCP
2838
+ -- e os dois campos novos da reforma) ficam numa tabela do m\xF3dulo de estoque,
2839
+ -- ao lado de quantidade m\xEDnima e temperatura de armazenamento. O sal\xE3o que
2840
+ -- nunca emitiu uma nota carrega as dezessete do mesmo jeito.
2841
+ --
2842
+ -- Aqui elas ficam onde o assunto \xE9: \`plg_fiscalbr_tax_groups\` j\xE1 guarda a
2843
+ -- mesma lista para o GRUPO fiscal, e esta tabela \xE9 o que um produto tem de
2844
+ -- diferente do grupo dele. Produto sem nada de fiscal n\xE3o tem linha nenhuma, e
2845
+ -- o estoque de quem n\xE3o emite nota n\xE3o muda de forma.
2846
+ --
2847
+ -- \`tax_group_id\` \xE9 a heran\xE7a e as outras colunas s\xE3o a exce\xE7\xE3o: NULL aqui quer
2848
+ -- dizer \xABvale o que o grupo diz\xBB, n\xE3o \xABzero\xBB. Por isso nenhuma tem DEFAULT 0 \u2014
2849
+ -- um zero gravado afirmaria al\xEDquota zero e sobrescreveria o grupo em sil\xEAncio.
2850
+
2851
+ CREATE TABLE IF NOT EXISTS public.plg_fiscalbr_product_tax_info (
2852
+ product_id uuid PRIMARY KEY REFERENCES public.products(id) ON DELETE CASCADE,
2853
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
2854
+ tax_group_id uuid REFERENCES public.plg_fiscalbr_tax_groups(id) ON DELETE SET NULL,
2855
+
2856
+ -- Nomenclatura Comum do Mercosul: \xE9 do PRODUTO, nunca do grupo. \xC9 o campo que
2857
+ -- o V1 preenche mesmo quando nenhum outro est\xE1 preenchido.
2858
+ ncm text,
2859
+ cest text,
2860
+ icms_origin text,
2861
+ icms_rate numeric(9,4),
2862
+ cfop text,
2863
+ cst text,
2864
+ pis_situation text,
2865
+ pis_rate numeric(9,4),
2866
+ cofins_situation text,
2867
+ cofins_rate numeric(9,4),
2868
+ base_calculation_mode text,
2869
+ benefit_code text,
2870
+ poverty_fund_rate numeric(9,4),
2871
+
2872
+ -- Reforma tribut\xE1ria. Entram agora pelo mesmo motivo que entraram no grupo:
2873
+ -- deixar de fora obrigaria a recadastrar o cat\xE1logo inteiro no dia em que
2874
+ -- IBS e CBS valerem.
2875
+ ibs_cbs_situation text,
2876
+ ibs_cbs_classification text,
2877
+
2878
+ metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
2879
+ created_at timestamptz NOT NULL DEFAULT now(),
2880
+ updated_at timestamptz NOT NULL DEFAULT now()
2881
+ );
2882
+
2883
+ CREATE INDEX IF NOT EXISTS plg_fiscalbr_product_tax_info_tenant_idx
2884
+ ON public.plg_fiscalbr_product_tax_info (tenant_id);
2885
+ CREATE INDEX IF NOT EXISTS plg_fiscalbr_product_tax_info_group_idx
2886
+ ON public.plg_fiscalbr_product_tax_info (tax_group_id);
2887
+
2888
+ ALTER TABLE public.plg_fiscalbr_product_tax_info ENABLE ROW LEVEL SECURITY;
2889
+
2890
+ -- Quem l\xEA e escreve \xE9 quem cuida do CADASTRO do produto, n\xE3o quem emite a
2891
+ -- nota: a aba Fiscal do V1 vive dentro do formul\xE1rio do produto, e exigir
2892
+ -- permiss\xE3o de financeiro ali esconderia a se\xE7\xE3o do estoquista que digita o
2893
+ -- NCM da mercadoria que acabou de chegar.
2894
+ DROP POLICY IF EXISTS plg_fiscalbr_product_tax_info_authz_select ON public.plg_fiscalbr_product_tax_info;
2895
+ CREATE POLICY plg_fiscalbr_product_tax_info_authz_select
2896
+ ON public.plg_fiscalbr_product_tax_info FOR SELECT TO authenticated
2897
+ USING (
2898
+ tenant_id = (SELECT app.current_tenant_id())
2899
+ AND (SELECT app.has_permission('inventory.read', NULL::uuid)));
2900
+
2901
+ DROP POLICY IF EXISTS plg_fiscalbr_product_tax_info_authz_write ON public.plg_fiscalbr_product_tax_info;
2902
+ CREATE POLICY plg_fiscalbr_product_tax_info_authz_write
2903
+ ON public.plg_fiscalbr_product_tax_info FOR ALL TO authenticated
2904
+ USING (
2905
+ tenant_id = (SELECT app.current_tenant_id())
2906
+ AND (SELECT app.has_permission('inventory.edit', NULL::uuid)))
2907
+ WITH CHECK (
2908
+ tenant_id = (SELECT app.current_tenant_id())
2909
+ AND (SELECT app.has_permission('inventory.edit', NULL::uuid)));
2910
+
2911
+ GRANT ALL ON TABLE public.plg_fiscalbr_product_tax_info TO authenticated;
2912
+ GRANT ALL ON TABLE public.plg_fiscalbr_product_tax_info TO service_role;
2913
+
2914
+ COMMENT ON TABLE public.plg_fiscalbr_product_tax_info IS
2915
+ 'Tributos do produto \u2014 a exce\xE7\xE3o ao grupo fiscal. Espelha a aba Fiscal do cadastro de produto do V1 (product_fiscal_info). NULL herda do grupo.';
2916
+ `;
2917
+ var MIGRATION_012_QUEM_E_O_PRODUTO_SE_PERGUNTA_AO_INDICE = `-- ---------------------------------------------------------------------------
2918
+ -- Quem \xE9 o produto desta linha da nota: pergunta-se ao \xEDndice, n\xE3o ao modelo.
2919
+ -- ---------------------------------------------------------------------------
2920
+ -- A sugest\xE3o de IA da entrada de notas mandava o CAT\xC1LOGO para o modelo. O V1
2921
+ -- mandava 400 produtos em todo turno (\`.slice(0, 400)\` sobre um \`.limit(800)\`
2922
+ -- sem \`order by\` \u2014 o corte era arbitr\xE1rio, e acima de 400 produtos a function
2923
+ -- respondia \xABcrie este produto\xBB para produto cadastrado, sem aviso).
2924
+ --
2925
+ -- O custo disso \xE9 \`cat\xE1logo \xD7 notas\`. Num tenant de 171 produtos s\xE3o ~3,5k
2926
+ -- tokens por nota; num distribuidor de 8.000 s\xE3o ~160k e n\xE3o cabe no turno. E a
2927
+ -- varredura de cat\xE1logo n\xE3o \xE9 trabalho de modelo: \xE9 trabalho de \xEDndice.
2928
+ --
2929
+ -- Esta fun\xE7\xE3o \xE9 a pr\xE9-sele\xE7\xE3o, em TR\xCAS fases, e a primeira que acerta encerra a
2930
+ -- linha:
2931
+ --
2932
+ -- supplier_map o de-para do fornecedor. O que aquele CNPJ j\xE1 ensinou n\xE3o se
2933
+ -- pergunta de novo \u2014 e numa nota repetida isso resolve a nota
2934
+ -- inteira, ent\xE3o a IA nem roda.
2935
+ -- ean c\xF3digo de barras contra o cadastro. Igualdade, n\xE3o palpite.
2936
+ -- text semelhan\xE7a de texto, com os candidatos que sobraram.
2937
+ --
2938
+ -- \`reason\` \xE9 o que a tela usa para decidir: \`supplier_map\` e \`ean\` s\xE3o V\xCDNCULO
2939
+ -- (liga sozinho), \`text\` \xE9 CANDIDATO (vai para o modelo desambiguar). Sem essa
2940
+ -- coluna a tela trataria um EAN exato como um chute de trigrama.
2941
+ --
2942
+ -- A fase de texto N\xC3O reimplementa busca: ela chama \`public.global_search\`, que
2943
+ -- j\xE1 tem \xEDndice GIN de trigrama, \`fayz_norm()\` (tira acento, min\xFAscula, colapsa
2944
+ -- pontua\xE7\xE3o) e a escada de pontua\xE7\xE3o da casa. Reescrever isso pior era o erro
2945
+ -- que o \`ILIKE\` da descri\xE7\xE3o fatiada em 24 caracteres j\xE1 estava cometendo do
2946
+ -- lado do cliente \u2014 \`name.ilike.%SACO PLASTICO 10X15 TRAN%\` n\xE3o casa acento,
2947
+ -- ordem de palavra nem abrevia\xE7\xE3o, e o resultado mais comum era zero candidatos.
2948
+ --
2949
+ -- O RPC foi renomeado na 137 (\`fayz_global_search\` \u2192 \`global_search\`) e h\xE1 pool
2950
+ -- em cada lado. Aqui se descobre o nome em tempo de execu\xE7\xE3o, como o cliente j\xE1
2951
+ -- faz em \`@fayz-ai/core/search\`.
2952
+ --
2953
+ -- N\xC3O \xE9 SECURITY DEFINER. Roda como quem chamou, a RLS de \`products\` vale, e o
2954
+ -- tenant vem de \`app.current_tenant_id()\` \u2014 o cliente n\xE3o passa tenant e por
2955
+ -- isso n\xE3o pode passar o de outra conta.
2956
+ --
2957
+ -- \`SET search_path\` \xE9 expl\xEDcito porque \`similarity\`/\`word_similarity\` moram em
2958
+ -- \`extensions\`: corpo com search_path vazio passa no DDL e estoura na primeira
2959
+ -- chamada.
2960
+ -- ---------------------------------------------------------------------------
2961
+
2962
+ CREATE OR REPLACE FUNCTION public.fiscalbr_match_product_candidates(
2963
+ -- [{ "ref": "<id do item>", "text": "SACO PLASTICO 10X15", "ean": "789\u2026", "code": "ABC1" }]
2964
+ p_queries jsonb,
2965
+ p_supplier_cnpj text DEFAULT NULL,
2966
+ p_per_query integer DEFAULT 8
2967
+ ) RETURNS TABLE (
2968
+ ref text,
2969
+ product_id uuid,
2970
+ name text,
2971
+ sku text,
2972
+ unit text,
2973
+ cost numeric,
2974
+ score real,
2975
+ reason text
2976
+ )
2977
+ LANGUAGE plpgsql
2978
+ STABLE
2979
+ SET search_path TO 'public', 'extensions', 'pg_temp'
2980
+ AS $fn$
2981
+ DECLARE
2982
+ v_tenant uuid := (SELECT app.current_tenant_id());
2983
+ v_cnpj text := public.fayz_digits(coalesce(p_supplier_cnpj, ''));
2984
+ v_barcodes boolean := to_regclass('public.plg_inventory_product_barcodes') IS NOT NULL;
2985
+ v_map boolean := to_regclass('public.plg_fiscalbr_supplier_product_map') IS NOT NULL;
2986
+ v_search text;
2987
+ v_q record;
2988
+ v_found uuid;
2989
+ BEGIN
2990
+ IF v_tenant IS NULL THEN RETURN; END IF;
2991
+ IF p_queries IS NULL OR jsonb_typeof(p_queries) <> 'array' THEN RETURN; END IF;
2992
+ p_per_query := least(greatest(coalesce(p_per_query, 8), 1), 50);
2993
+
2994
+ v_search := CASE
2995
+ WHEN to_regprocedure('public.global_search(text,uuid,text[],integer,integer)') IS NOT NULL
2996
+ THEN 'public.global_search'
2997
+ WHEN to_regprocedure('public.fayz_global_search(text,uuid,text[],integer,integer)') IS NOT NULL
2998
+ THEN 'public.fayz_global_search'
2999
+ ELSE NULL
3000
+ END;
3001
+
3002
+ FOR v_q IN
3003
+ SELECT e->>'ref' AS ref,
3004
+ coalesce(e->>'text', '') AS text,
3005
+ public.fayz_digits(coalesce(e->>'ean', '')) AS ean,
3006
+ nullif(btrim(coalesce(e->>'code', '')), '') AS code
3007
+ FROM jsonb_array_elements(p_queries) AS e
3008
+ WHERE e->>'ref' IS NOT NULL
3009
+ LOOP
3010
+ v_found := NULL;
3011
+
3012
+ -- \u2460 O de-para do fornecedor. Aprendizado da casa, e ele ganha de tudo.
3013
+ IF v_map AND v_cnpj <> '' AND v_q.code IS NOT NULL THEN
3014
+ SELECT p.id INTO v_found
3015
+ FROM public.plg_fiscalbr_supplier_product_map m
3016
+ JOIN public.products p ON p.id = m.product_id
3017
+ WHERE m.supplier_cnpj = v_cnpj
3018
+ AND m.supplier_product_code = v_q.code
3019
+ AND p.is_active
3020
+ ORDER BY m.hit_count DESC NULLS LAST, m.last_used_at DESC NULLS LAST
3021
+ LIMIT 1;
3022
+
3023
+ IF v_found IS NOT NULL THEN
3024
+ RETURN QUERY
3025
+ SELECT v_q.ref, p.id, p.name, p.sku, p.unit, p.cost, 1.0::real, 'supplier_map'::text
3026
+ FROM public.products p WHERE p.id = v_found;
3027
+ CONTINUE;
3028
+ END IF;
3029
+ END IF;
3030
+
3031
+ -- \u2461 O c\xF3digo de barras. O produto guarda o principal em \`metadata->>barcode\`
3032
+ -- (no V1 era coluna) e os demais na tabela de c\xF3digos do estoque.
3033
+ IF length(v_q.ean) >= 8 THEN
3034
+ SELECT p.id INTO v_found
3035
+ FROM public.products p
3036
+ WHERE p.is_active
3037
+ AND (
3038
+ public.fayz_digits(coalesce(p.metadata->>'barcode', '')) = v_q.ean
3039
+ OR (v_barcodes AND EXISTS (
3040
+ SELECT 1 FROM public.plg_inventory_product_barcodes b
3041
+ WHERE b.product_id = p.id
3042
+ AND public.fayz_digits(coalesce(b.barcode, '')) = v_q.ean))
3043
+ )
3044
+ LIMIT 1;
3045
+
3046
+ IF v_found IS NOT NULL THEN
3047
+ RETURN QUERY
3048
+ SELECT v_q.ref, p.id, p.name, p.sku, p.unit, p.cost, 1.0::real, 'ean'::text
3049
+ FROM public.products p WHERE p.id = v_found;
3050
+ CONTINUE;
3051
+ END IF;
3052
+ END IF;
3053
+
3054
+ -- \u2462 Semelhan\xE7a de texto, pela busca indexada que j\xE1 existe.
3055
+ CONTINUE WHEN v_search IS NULL OR length(public.fayz_norm(v_q.text)) < 3;
3056
+
3057
+ RETURN QUERY EXECUTE format($q$
3058
+ SELECT $4::text, p.id, p.name, p.sku, p.unit, p.cost, g.score, 'text'::text
3059
+ FROM %s($1, $2, ARRAY['product']::text[], $3, $3) g
3060
+ JOIN public.products p ON p.id = g.record_id::uuid
3061
+ WHERE p.is_active
3062
+ ORDER BY g.score DESC
3063
+ $q$, v_search)
3064
+ USING v_q.text, v_tenant, p_per_query, v_q.ref;
3065
+ END LOOP;
3066
+ END
3067
+ $fn$;
3068
+
3069
+ GRANT EXECUTE ON FUNCTION public.fiscalbr_match_product_candidates(jsonb, text, integer) TO authenticated;
3070
+
3071
+ COMMENT ON FUNCTION public.fiscalbr_match_product_candidates(jsonb, text, integer) IS
3072
+ 'Pr\xE9-sele\xE7\xE3o de produtos para a sugest\xE3o de IA da entrada de notas: de-para do fornecedor e EAN resolvem sozinhos (reason supplier_map/ean, v\xEDnculo direto); o resto volta como candidatos de texto (reason text) para o modelo desambiguar. Uma viagem para a nota inteira \u2014 substitui o despejo de 400 produtos do V1 e o ILIKE da descri\xE7\xE3o fatiada.';
3073
+ `;
3074
+ var MIGRATION_013_A_SUGESTAO_SOBREVIVE_A_FECHAR_A_JANELA = `-- ---------------------------------------------------------------------------
3075
+ -- A sugest\xE3o sobrevive a fechar a janela \u2014 e diz QUEM escreveu no campo.
3076
+ -- ---------------------------------------------------------------------------
3077
+ -- A concilia\xE7\xE3o virou modal para a lista n\xE3o ser desmontada. O efeito colateral
3078
+ -- \xE9 que o modal fecha com clique fora e com Esc, e a sugest\xE3o s\xF3 existia em
3079
+ -- mem\xF3ria at\xE9 algu\xE9m clicar em \xABSalvar concilia\xE7\xE3o\xBB:
3080
+ --
3081
+ -- sugerir (30s de IA) \u2192 revisar 7 de 11 itens \u2192 clique fora \u2192 perdeu os 11
3082
+ --
3083
+ -- No V1 fechar n\xE3o custava nada, porque a edge function j\xE1 tinha gravado. Ent\xE3o
3084
+ -- a V2 grava tamb\xE9m, logo depois da sugest\xE3o, pelo caminho que j\xE1 existia
3085
+ -- (\`fiscalbr_save_inbox_decision\`, que n\xE3o marca \`reconciled\`) \u2014 nenhum segundo
3086
+ -- caminho de escrita, nenhuma coluna \`ai_*\` paralela aos campos de verdade.
3087
+ --
3088
+ -- O que faltava era a MEM\xD3RIA DE AUTORIA. Depois de recarregar, a tela via um
3089
+ -- fator 22,056 e n\xE3o sabia se foi a Laissa que digitou ou a m\xE1quina que chutou \u2014
3090
+ -- e as duas coisas se conferem de maneiras diferentes.
3091
+ --
3092
+ -- O V1 guardava um booleano, \`ai_suggested\`. Aqui \xE9 texto, porque a V2 tem TR\xCAS
3093
+ -- autores de m\xE1quina e eles n\xE3o merecem o mesmo selo:
3094
+ --
3095
+ -- supplier_map o de-para deste fornecedor. Igualdade, e aprendizado da casa.
3096
+ -- ean c\xF3digo de barras. Igualdade.
3097
+ -- ai semelhan\xE7a julgada por modelo. \xC9 o \xFAnico que se confere item
3098
+ -- a item, e \`suggestion_confidence\` diz o quanto.
3099
+ --
3100
+ -- NULL \xE9 a resposta mais importante: significa \xABuma pessoa escolheu isto\xBB. O
3101
+ -- cliente limpa a coluna em qualquer edi\xE7\xE3o do campo, ent\xE3o o selo nunca mente
3102
+ -- sobre um valor que algu\xE9m corrigiu \xE0 m\xE3o.
3103
+ -- ---------------------------------------------------------------------------
3104
+
3105
+ ALTER TABLE public.plg_fiscalbr_inbox_items
3106
+ ADD COLUMN IF NOT EXISTS suggested_by text,
3107
+ ADD COLUMN IF NOT EXISTS suggestion_confidence numeric;
3108
+
3109
+ DO $$
3110
+ BEGIN
3111
+ IF NOT EXISTS (
3112
+ SELECT 1 FROM pg_constraint
3113
+ WHERE conname = 'plg_fiscalbr_inbox_items_suggested_by_check'
3114
+ AND conrelid = 'public.plg_fiscalbr_inbox_items'::regclass
3115
+ ) THEN
3116
+ ALTER TABLE public.plg_fiscalbr_inbox_items
3117
+ ADD CONSTRAINT plg_fiscalbr_inbox_items_suggested_by_check
3118
+ CHECK (suggested_by IS NULL OR suggested_by IN ('ai', 'ean', 'supplier_map'));
3119
+ END IF;
3120
+ END $$;
3121
+
3122
+ COMMENT ON COLUMN public.plg_fiscalbr_inbox_items.suggested_by IS
3123
+ 'Quem escreveu a decis\xE3o desta linha: ai (modelo, confira), ean / supplier_map (igualdade), NULL (uma pessoa). O cliente limpa em qualquer edi\xE7\xE3o manual.';
3124
+ COMMENT ON COLUMN public.plg_fiscalbr_inbox_items.suggestion_confidence IS
3125
+ '0..1, s\xF3 para suggested_by = ai. Abaixo de 0,6 a tela marca a linha como \xABconfira\xBB.';
3126
+
3127
+ -- \u2500\u2500 A grava\xE7\xE3o da decis\xE3o passa a carregar a autoria \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3128
+ -- Mesma assinatura, ent\xE3o \xE9 \`CREATE OR REPLACE\` e nenhum chamador muda. O corpo
3129
+ -- \xE9 o da 009 com duas linhas a mais: o resto fica palavra por palavra, porque
3130
+ -- esta fun\xE7\xE3o \xE9 o \xFAnico caminho de escrita da decis\xE3o e reescrev\xEA-la de mem\xF3ria
3131
+ -- \xE9 como se perde uma regra (a de \`reconciled\`, por exemplo).
3132
+
3133
+ CREATE OR REPLACE FUNCTION public.fiscalbr_save_inbox_decision(
3134
+ p_document_id uuid,
3135
+ p_items jsonb,
3136
+ p_default_chart_account_id uuid DEFAULT NULL,
3137
+ p_default_location_id uuid DEFAULT NULL,
3138
+ p_duplicates jsonb DEFAULT NULL)
3139
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $function$
3140
+ DECLARE
3141
+ v_tenant uuid := public.plg_financial_require_tenant();
3142
+ v_doc public.plg_fiscalbr_inbox_documents%ROWTYPE;
3143
+ v_row jsonb;
3144
+ v_done integer := 0;
3145
+ v_total integer;
3146
+ BEGIN
3147
+ SELECT * INTO v_doc FROM public.plg_fiscalbr_inbox_documents
3148
+ WHERE tenant_id = v_tenant AND id = p_document_id;
3149
+ IF NOT FOUND THEN
3150
+ RAISE EXCEPTION 'fiscalbr: nota % n\xE3o encontrada neste tenant', p_document_id USING ERRCODE = 'P0002';
3151
+ END IF;
3152
+ PERFORM public.plg_financial_require_permission('financial.edit', v_doc.unit_id);
3153
+
3154
+ IF v_doc.stocked_at IS NOT NULL OR v_doc.invoice_id IS NOT NULL THEN
3155
+ RAISE EXCEPTION 'fiscalbr: nota % j\xE1 foi aceita \u2014 a decis\xE3o n\xE3o muda depois do estoque', p_document_id
3156
+ USING ERRCODE = '23514';
3157
+ END IF;
3158
+
3159
+ UPDATE public.plg_fiscalbr_inbox_documents SET
3160
+ default_chart_account_id = coalesce(p_default_chart_account_id, default_chart_account_id),
3161
+ default_location_id = coalesce(p_default_location_id, default_location_id),
3162
+ reviewed_at = now(), updated_at = now()
3163
+ WHERE id = v_doc.id;
3164
+
3165
+ FOR v_row IN SELECT * FROM jsonb_array_elements(coalesce(p_items, '[]'::jsonb)) LOOP
3166
+ UPDATE public.plg_fiscalbr_inbox_items i SET
3167
+ product_id = nullif(v_row->>'product_id', '')::uuid,
3168
+ entry_type = nullif(v_row->>'entry_type', ''),
3169
+ chart_account_id = nullif(v_row->>'chart_account_id', '')::uuid,
3170
+ stock_unit_id = nullif(v_row->>'stock_unit_id', '')::uuid,
3171
+ conversion_factor = coalesce(nullif(v_row->>'conversion_factor', '')::numeric, 1),
3172
+ cost_mode = coalesce(nullif(v_row->>'cost_mode', ''), 'novo'),
3173
+ shortage_quantity = nullif(v_row->>'shortage_quantity', '')::numeric,
3174
+ shortage_reason = nullif(v_row->>'shortage_reason', ''),
3175
+ shortage_note = nullif(v_row->>'shortage_note', ''),
3176
+ -- Quem escreveu. Ausente na chave \xE9 NULL, que \xE9 \xABuma pessoa escolheu\xBB \u2014 e \xE9
3177
+ -- o que acontece em todo salvamento comum, sem a IA ter passado por aqui.
3178
+ suggested_by = nullif(v_row->>'suggested_by', ''),
3179
+ suggestion_confidence = nullif(v_row->>'suggestion_confidence', '')::numeric,
3180
+ -- Derivadas, gravadas: \xE9 o que o operador viu.
3181
+ stock_quantity = i.quantity_commercial * coalesce(nullif(v_row->>'conversion_factor', '')::numeric, 1)
3182
+ - coalesce(nullif(v_row->>'shortage_quantity', '')::numeric, 0),
3183
+ unit_cost_new = CASE WHEN coalesce(nullif(v_row->>'conversion_factor', '')::numeric, 1) > 0
3184
+ THEN i.unit_price_commercial / coalesce(nullif(v_row->>'conversion_factor', '')::numeric, 1) END,
3185
+ -- Uma linha est\xE1 conciliada quando sabe o que \xE9. Despesa e servi\xE7o n\xE3o
3186
+ -- pedem produto: elas nunca v\xE3o virar estoque.
3187
+ reconciled = CASE
3188
+ WHEN nullif(v_row->>'entry_type', '') IN ('despesa','servico') THEN true
3189
+ ELSE nullif(v_row->>'product_id', '') IS NOT NULL
3190
+ END,
3191
+ updated_at = now()
3192
+ WHERE i.tenant_id = v_tenant AND i.inbox_document_id = v_doc.id
3193
+ AND i.id = (v_row->>'id')::uuid;
3194
+ v_done := v_done + 1;
3195
+ END LOOP;
3196
+
3197
+ IF p_duplicates IS NOT NULL THEN
3198
+ DELETE FROM public.plg_fiscalbr_inbox_duplicates
3199
+ WHERE tenant_id = v_tenant AND inbox_document_id = v_doc.id;
3200
+ INSERT INTO public.plg_fiscalbr_inbox_duplicates (
3201
+ tenant_id, inbox_document_id, duplicate_index, number, due_date, value,
3202
+ payment_type, barcode, pix_key, payment_method_id)
3203
+ SELECT v_tenant, v_doc.id, coalesce((d->>'index')::int, ord), d->>'number',
3204
+ nullif(d->>'due_date', '')::date, (d->>'value')::numeric,
3205
+ nullif(d->>'payment_type', ''), nullif(d->>'barcode', ''),
3206
+ nullif(d->>'pix_key', ''), nullif(d->>'payment_method_id', '')::uuid
3207
+ FROM jsonb_array_elements(p_duplicates) WITH ORDINALITY AS t(d, ord);
3208
+ END IF;
3209
+
3210
+ SELECT count(*) INTO v_total FROM public.plg_fiscalbr_inbox_items
3211
+ WHERE tenant_id = v_tenant AND inbox_document_id = v_doc.id;
3212
+
3213
+ UPDATE public.plg_fiscalbr_inbox_documents SET status = CASE
3214
+ WHEN (SELECT count(*) FROM public.plg_fiscalbr_inbox_items
3215
+ WHERE tenant_id = v_tenant AND inbox_document_id = v_doc.id AND reconciled) = v_total
3216
+ THEN 'accepted' ELSE 'partial' END
3217
+ WHERE id = v_doc.id;
3218
+
3219
+ RETURN jsonb_build_object('document_id', v_doc.id, 'saved', v_done, 'items', v_total,
3220
+ 'reconciled', (SELECT count(*) FROM public.plg_fiscalbr_inbox_items
3221
+ WHERE tenant_id = v_tenant AND inbox_document_id = v_doc.id AND reconciled));
3222
+ END $function$;
3223
+
3224
+ REVOKE ALL ON FUNCTION public.fiscalbr_save_inbox_decision(uuid, jsonb, uuid, uuid, jsonb) FROM PUBLIC;
3225
+ GRANT EXECUTE ON FUNCTION public.fiscalbr_save_inbox_decision(uuid, jsonb, uuid, uuid, jsonb) TO authenticated;
3226
+ `;
3227
+ var MIGRATION_014_A_DESCRICAO_DA_NOTA_TEM_LIXO_NO_MEIO = `-- ---------------------------------------------------------------------------
3228
+ -- A descri\xE7\xE3o da nota tem lixo no meio, e um token de lixo matava a busca.
3229
+ -- ---------------------------------------------------------------------------
3230
+ -- Caso real, primeira nota de verdade: o item \xAB20X35 AR VACUO\xBB voltou SEM
3231
+ -- candidato, e o produto \`EMBALAGEM VACUO 20X35\` est\xE1 cadastrado.
3232
+ --
3233
+ -- \`public.global_search\` exige que TODOS os tokens apare\xE7am no haystack \u2014 \xE9 a
3234
+ -- regra certa para gente digitando (\xABjo\xE3o silva\xBB n\xE3o deve trazer todos os jo\xE3os)
3235
+ -- e \xE9 a regra errada para descri\xE7\xE3o de NF-e. Os tokens de \`20X35 AR VACUO\` s\xE3o
3236
+ -- \`20x35\`, \`ar\`, \`vacuo\`; o produto \xE9 \`embalagem vacuo 20x35\`. Dois casam. \`ar\`
3237
+ -- n\xE3o aparece em lugar nenhum, e o AND derruba a linha inteira.
3238
+ --
3239
+ -- A segunda fase (\`word_similarity\`) tamb\xE9m n\xE3o salva: ela compara a frase
3240
+ -- inteira contra o haystack, e com um ter\xE7o da frase sendo ru\xEDdo a semelhan\xE7a
3241
+ -- fica abaixo do limiar.
3242
+ --
3243
+ -- Descri\xE7\xE3o de nota fiscal \xE9 assim por natureza: abreviada, em ordem pr\xF3pria, com
3244
+ -- sigla de unidade, micragem e palavra solta no meio. Ent\xE3o o que entra na busca
3245
+ -- deixa de ser a frase e passa a ser O QUE NELA DISTINGUE:
3246
+ --
3247
+ -- \u2460 tokens de 3 letras ou mais. \xABar\xBB, \xABkg\xBB, \xABun\xBB, \xABc/\xBB, \xABp/\xBB saem. \xAB20x35\xBB
3248
+ -- e \xABvacuo\xBB ficam \u2014 e s\xE3o eles que identificam o produto.
3249
+ -- \u2461 se mesmo assim n\xE3o vier nada, UMA \xFAltima tentativa com o token mais longo
3250
+ -- sozinho. \xC9 o recuo que acha \xABembalagem a v\xE1cuo 15 x 25\xBB a partir de
3251
+ -- \xABVACUO\xBB, para a pessoa escolher entre poucos em vez de nenhum.
3252
+ --
3253
+ -- N\xE3o se mexe em \`global_search\`: l\xE1 a regra est\xE1 certa para quem ela serve.
3254
+ -- ---------------------------------------------------------------------------
3255
+
3256
+ CREATE OR REPLACE FUNCTION public.fiscalbr_match_product_candidates(
3257
+ p_queries jsonb,
3258
+ p_supplier_cnpj text DEFAULT NULL,
3259
+ p_per_query integer DEFAULT 8
3260
+ ) RETURNS TABLE (
3261
+ ref text,
3262
+ product_id uuid,
3263
+ name text,
3264
+ sku text,
3265
+ unit text,
3266
+ cost numeric,
3267
+ score real,
3268
+ reason text
3269
+ )
3270
+ LANGUAGE plpgsql
3271
+ STABLE
3272
+ SET search_path TO 'public', 'extensions', 'pg_temp'
3273
+ AS $fn$
3274
+ DECLARE
3275
+ v_tenant uuid := (SELECT app.current_tenant_id());
3276
+ v_cnpj text := public.fayz_digits(coalesce(p_supplier_cnpj, ''));
3277
+ v_barcodes boolean := to_regclass('public.plg_inventory_product_barcodes') IS NOT NULL;
3278
+ v_map boolean := to_regclass('public.plg_fiscalbr_supplier_product_map') IS NOT NULL;
3279
+ v_search text;
3280
+ v_q record;
3281
+ v_found uuid;
3282
+ v_tokens text[];
3283
+ v_strong text[];
3284
+ v_busca text;
3285
+ v_recuo text;
3286
+ BEGIN
3287
+ IF v_tenant IS NULL THEN RETURN; END IF;
3288
+ IF p_queries IS NULL OR jsonb_typeof(p_queries) <> 'array' THEN RETURN; END IF;
3289
+ p_per_query := least(greatest(coalesce(p_per_query, 8), 1), 50);
3290
+
3291
+ v_search := CASE
3292
+ WHEN to_regprocedure('public.global_search(text,uuid,text[],integer,integer)') IS NOT NULL
3293
+ THEN 'public.global_search'
3294
+ WHEN to_regprocedure('public.fayz_global_search(text,uuid,text[],integer,integer)') IS NOT NULL
3295
+ THEN 'public.fayz_global_search'
3296
+ ELSE NULL
3297
+ END;
3298
+
3299
+ FOR v_q IN
3300
+ SELECT e->>'ref' AS ref,
3301
+ coalesce(e->>'text', '') AS text,
3302
+ public.fayz_digits(coalesce(e->>'ean', '')) AS ean,
3303
+ nullif(btrim(coalesce(e->>'code', '')), '') AS code
3304
+ FROM jsonb_array_elements(p_queries) AS e
3305
+ WHERE e->>'ref' IS NOT NULL
3306
+ LOOP
3307
+ v_found := NULL;
3308
+
3309
+ -- \u2460 O de-para do fornecedor. Aprendizado da casa, e ele ganha de tudo.
3310
+ IF v_map AND v_cnpj <> '' AND v_q.code IS NOT NULL THEN
3311
+ SELECT p.id INTO v_found
3312
+ FROM public.plg_fiscalbr_supplier_product_map m
3313
+ JOIN public.products p ON p.id = m.product_id
3314
+ WHERE m.supplier_cnpj = v_cnpj
3315
+ AND m.supplier_product_code = v_q.code
3316
+ AND p.is_active
3317
+ ORDER BY m.hit_count DESC NULLS LAST, m.last_used_at DESC NULLS LAST
3318
+ LIMIT 1;
3319
+
3320
+ IF v_found IS NOT NULL THEN
3321
+ RETURN QUERY
3322
+ SELECT v_q.ref, p.id, p.name, p.sku, p.unit, p.cost, 1.0::real, 'supplier_map'::text
3323
+ FROM public.products p WHERE p.id = v_found;
3324
+ CONTINUE;
3325
+ END IF;
3326
+ END IF;
3327
+
3328
+ -- \u2461 O c\xF3digo de barras. \xABSEM GTIN\xBB \xE9 o que a NF-e escreve quando n\xE3o h\xE1 \u2014 e
3329
+ -- \`fayz_digits\` o reduz a vazio, ent\xE3o o teste de comprimento j\xE1 o barra.
3330
+ IF length(v_q.ean) >= 8 THEN
3331
+ SELECT p.id INTO v_found
3332
+ FROM public.products p
3333
+ WHERE p.is_active
3334
+ AND (
3335
+ public.fayz_digits(coalesce(p.metadata->>'barcode', '')) = v_q.ean
3336
+ OR (v_barcodes AND EXISTS (
3337
+ SELECT 1 FROM public.plg_inventory_product_barcodes b
3338
+ WHERE b.product_id = p.id
3339
+ AND public.fayz_digits(coalesce(b.barcode, '')) = v_q.ean))
3340
+ )
3341
+ LIMIT 1;
3342
+
3343
+ IF v_found IS NOT NULL THEN
3344
+ RETURN QUERY
3345
+ SELECT v_q.ref, p.id, p.name, p.sku, p.unit, p.cost, 1.0::real, 'ean'::text
3346
+ FROM public.products p WHERE p.id = v_found;
3347
+ CONTINUE;
3348
+ END IF;
3349
+ END IF;
3350
+
3351
+ -- \u2462 Semelhan\xE7a de texto, com o que DISTINGUE em vez da frase inteira.
3352
+ CONTINUE WHEN v_search IS NULL;
3353
+
3354
+ v_tokens := array_remove(string_to_array(public.fayz_norm(v_q.text), ' '), '');
3355
+ SELECT array_agg(t ORDER BY length(t) DESC) INTO v_strong
3356
+ FROM unnest(v_tokens) t WHERE length(t) >= 3;
3357
+
3358
+ v_busca := array_to_string(coalesce(v_strong, v_tokens), ' ');
3359
+ -- O recuo \xE9 o token mais longo sozinho: numa descri\xE7\xE3o de nota ele \xE9 quase
3360
+ -- sempre o nome da coisa, e os outros s\xE3o medida, micragem ou conjun\xE7\xE3o.
3361
+ v_recuo := coalesce(v_strong[1], '');
3362
+
3363
+ CONTINUE WHEN length(v_busca) < 3;
3364
+
3365
+ RETURN QUERY EXECUTE format($q$
3366
+ WITH forte AS (
3367
+ SELECT g.record_id, g.score FROM %1$s($1, $2, ARRAY['product']::text[], $3, $3) g
3368
+ ),
3369
+ -- S\xF3 quando a busca forte n\xE3o achou nada: um recuo que rodasse sempre
3370
+ -- encheria a lista de parecidos e empurraria o certo para fora do teto.
3371
+ fraco AS (
3372
+ SELECT g.record_id, g.score * 0.7 AS score
3373
+ FROM %1$s($4, $2, ARRAY['product']::text[], $3, $3) g
3374
+ WHERE $4 <> '' AND $4 <> $1 AND NOT EXISTS (SELECT 1 FROM forte)
3375
+ )
3376
+ SELECT $5::text, p.id, p.name, p.sku, p.unit, p.cost, g.score::real, 'text'::text
3377
+ FROM (SELECT * FROM forte UNION ALL SELECT * FROM fraco) g
3378
+ JOIN public.products p ON p.id = g.record_id::uuid
3379
+ WHERE p.is_active
3380
+ ORDER BY g.score DESC
3381
+ $q$, v_search)
3382
+ USING v_busca, v_tenant, p_per_query, v_recuo, v_q.ref;
3383
+ END LOOP;
3384
+ END
3385
+ $fn$;
3386
+
3387
+ GRANT EXECUTE ON FUNCTION public.fiscalbr_match_product_candidates(jsonb, text, integer) TO authenticated;
3388
+
3389
+ COMMENT ON FUNCTION public.fiscalbr_match_product_candidates(jsonb, text, integer) IS
3390
+ 'Pr\xE9-sele\xE7\xE3o de produtos para a sugest\xE3o de IA da entrada de notas: de-para do fornecedor e EAN resolvem sozinhos (reason supplier_map/ean); o resto volta como candidatos de texto. A busca usa os tokens de 3+ letras da descri\xE7\xE3o, n\xE3o a frase \u2014 \xAB20X35 AR VACUO\xBB achava nada porque o AND de global_search exige o token \xABar\xBB, que n\xE3o existe em \xABembalagem vacuo 20x35\xBB.';
3391
+ `;
3392
+ var MIGRATION_015_O_MOVIMENTO_DIZ_QUE_VEIO_DA_NOTA = `-- ---------------------------------------------------------------------------
3393
+ -- O movimento de estoque diz, por escrito, que veio desta nota.
3394
+ -- ---------------------------------------------------------------------------
3395
+ -- A aceita\xE7\xE3o j\xE1 mandava a frase para \`inventory_receive\`, na chave \`notes\` do
3396
+ -- cabe\xE7alho \u2014 e ela nunca chegava: l\xE1 dentro o \`coalesce(v_ln.notes, \u2026)\` d\xE1 a vez
3397
+ -- \xE0 anota\xE7\xE3o DA LINHA, que aqui \xE9 sempre a descri\xE7\xE3o do item. A tela de
3398
+ -- Movimenta\xE7\xF5es ent\xE3o escrevia \xABSem motivo registrado\xBB sobre a entrada mais
3399
+ -- rastre\xE1vel que existe.
3400
+ --
3401
+ -- \`inventory/021\` abriu a chave \`reason\` no cabe\xE7alho. Esta migration passa a
3402
+ -- us\xE1-la, com a mesma frase que as linhas migradas do V1 carregam \u2014 para que a
3403
+ -- entrada de ontem e a de hoje se leiam igual no mesmo raz\xE3o.
3404
+ --
3405
+ -- S\xF3 a chamada muda. O resto do corpo \xE9 o da 009, palavra por palavra: esta
3406
+ -- fun\xE7\xE3o escreve estoque e t\xEDtulo na mesma transa\xE7\xE3o, e reescrev\xEA-la de mem\xF3ria
3407
+ -- \xE9 como se perde uma regra.
3408
+ -- ---------------------------------------------------------------------------
3409
+
3410
+ DO $m$
3411
+ DECLARE
3412
+ v_def text;
3413
+ v_antes text := E' ''ref'', ''NF-e '' || coalesce(v_doc.nfe_number, ''?''),\\n ''notes'', ''Entrada via DANFE NF-e '' || coalesce(v_doc.nfe_number, ''?'')),';
3414
+ v_depois text := E' ''ref'', ''NF-e '' || coalesce(v_doc.nfe_number, ''?''),\\n ''notes'', ''Entrada via DANFE NF-e '' || coalesce(v_doc.nfe_number, ''?''),\\n ''reason'', ''Entrada via DANFE NF-e '' || coalesce(v_doc.nfe_number, ''?'')),';
3415
+ BEGIN
3416
+ SELECT pg_get_functiondef(p.oid) INTO v_def
3417
+ FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace
3418
+ WHERE n.nspname = 'public' AND p.proname = 'fiscalbr_accept_inbox_document';
3419
+
3420
+ IF v_def IS NULL THEN
3421
+ RAISE NOTICE 'fiscalbr: accept ainda n\xE3o existe neste pool \u2014 009 roda antes e j\xE1 traz o reason';
3422
+ RETURN;
3423
+ END IF;
3424
+
3425
+ -- J\xE1 aplicada: sair sem tocar em nada \xE9 o que torna esta migration repet\xEDvel.
3426
+ IF position('''reason'', ''Entrada via DANFE' in v_def) > 0 THEN RETURN; END IF;
3427
+
3428
+ IF position(v_antes in v_def) = 0 THEN
3429
+ RAISE EXCEPTION 'fiscalbr/015: o corpo de fiscalbr_accept_inbox_document n\xE3o \xE9 o esperado \u2014 confira \xE0 m\xE3o em vez de deixar passar calado';
3430
+ END IF;
3431
+
3432
+ EXECUTE replace(v_def, v_antes, v_depois);
3433
+ END $m$;
3434
+ `;
3435
+ var MIGRATION_016_A_NOTA_GUARDA_O_PROPRIO_XML = `-- ---------------------------------------------------------------------------
3436
+ -- A nota guarda o pr\xF3prio XML, em vez de l\xEA-lo e jogar fora.
3437
+ -- ---------------------------------------------------------------------------
3438
+ -- O V1 abre o XML e o DANFE direto da lista de notas. A V2 mostrava os dois
3439
+ -- bot\xF5es desligados porque n\xE3o tinha o arquivo: das 889 notas deste cluster,
3440
+ -- ZERO t\xEAm \`document_id\`, zero t\xEAm bytes em \`documents\`, zero t\xEAm nada no
3441
+ -- metadata.
3442
+ --
3443
+ -- E n\xE3o \xE9 que o arquivo nunca tenha passado por aqui \u2014 ele passa INTEIRO pela
3444
+ -- m\xE3o do est\xE1gio: o navegador l\xEA o \`.xml\` que a pessoa arrastou, o parser extrai
3445
+ -- itens, parcelas e totais, e os bytes s\xE3o descartados no fim da fun\xE7\xE3o. A nota
3446
+ -- fiscal \xE9 documento com guarda legal de cinco anos, e a \xFAnica c\xF3pia ficava na
3447
+ -- pasta de downloads de quem importou.
3448
+ --
3449
+ -- \`xml_content text\` \xE9 o mesmo nome e o mesmo tipo do \`dfe_inbox.xml_content\` do
3450
+ -- V1 \u2014 a ponte da 003 j\xE1 citava essa coluna, e repetir o nome \xE9 o que faz uma
3451
+ -- futura migra\xE7\xE3o de dados ser um \`INSERT ... SELECT\` em vez de um de-para.
3452
+ --
3453
+ -- Guardar no banco e n\xE3o no bucket, de prop\xF3sito: s\xE3o 10\u201350 KB de texto por nota
3454
+ -- e s\xE3o o \xDANICO conte\xFAdo que torna a linha audit\xE1vel. Storage \xE9 outro sistema,
3455
+ -- com outra vida \xFAtil e outra pol\xEDtica de acesso, e um XML fiscal que vive em
3456
+ -- bucket \xE9 um XML que algu\xE9m apaga limpando bucket.
3457
+ --
3458
+ -- Isto N\xC3O preenche as 889 notas antigas: o que n\xE3o foi guardado n\xE3o se inventa.
3459
+ -- A tela desliga o bot\xE3o por nota, n\xE3o por feature.
3460
+ -- ---------------------------------------------------------------------------
3461
+
3462
+ ALTER TABLE public.plg_fiscalbr_inbox_documents
3463
+ ADD COLUMN IF NOT EXISTS xml_content text;
3464
+
3465
+ COMMENT ON COLUMN public.plg_fiscalbr_inbox_documents.xml_content IS
3466
+ 'O XML autorizado da NF-e, como chegou. Nulo nas notas importadas antes desta coluna existir \u2014 o que n\xE3o foi guardado n\xE3o se inventa.';
3467
+
3468
+ -- \u2500\u2500 O est\xE1gio passa a guardar o que j\xE1 tinha em m\xE3o \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3469
+ -- Patch sobre o corpo que est\xE1 no cluster, e n\xE3o reescrita: esta fun\xE7\xE3o insere
3470
+ -- documento, itens, duplicatas e pagamentos numa transa\xE7\xE3o s\xF3, e reescrev\xEA-la de
3471
+ -- mem\xF3ria \xE9 como se perde uma regra.
3472
+
3473
+ DO $m$
3474
+ DECLARE
3475
+ v_def text;
3476
+ v_cols_antes text := E' total_freight, total_insurance, total_other, additional_info, metadata)';
3477
+ v_cols_depois text := E' total_freight, total_insurance, total_other, additional_info, metadata, xml_content)';
3478
+ v_vals_antes text := E' jsonb_build_object(''source'', coalesce(p_note->>''source'', ''xml_upload'')))';
3479
+ v_vals_depois text := E' jsonb_build_object(''source'', coalesce(p_note->>''source'', ''xml_upload'')),' || E'\\n' ||
3480
+ E' nullif(p_note->>''xml'', ''''))';
3481
+ v_upd_antes text := E' total_value = EXCLUDED.total_value,';
3482
+ v_upd_depois text := E' total_value = EXCLUDED.total_value,' || E'\\n' ||
3483
+ E' -- Reimportar com o arquivo preenche o que faltava; reimportar sem ele' || E'\\n' ||
3484
+ E' -- n\xE3o apaga o que j\xE1 estava guardado.' || E'\\n' ||
3485
+ E' xml_content = coalesce(EXCLUDED.xml_content, public.plg_fiscalbr_inbox_documents.xml_content),';
3486
+ BEGIN
3487
+ SELECT pg_get_functiondef(p.oid) INTO v_def
3488
+ FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace
3489
+ WHERE n.nspname = 'public' AND p.proname = 'fiscalbr_stage_inbox_document';
3490
+
3491
+ IF v_def IS NULL THEN
3492
+ RAISE NOTICE 'fiscalbr/016: o est\xE1gio ainda n\xE3o existe neste pool \u2014 a 009 roda antes';
3493
+ RETURN;
3494
+ END IF;
3495
+
3496
+ -- J\xE1 aplicada: sair sem tocar em nada \xE9 o que torna esta migration repet\xEDvel.
3497
+ IF position('xml_content' in v_def) > 0 THEN RETURN; END IF;
3498
+
3499
+ IF position(v_cols_antes in v_def) = 0
3500
+ OR position(v_vals_antes in v_def) = 0
3501
+ OR position(v_upd_antes in v_def) = 0 THEN
3502
+ RAISE EXCEPTION 'fiscalbr/016: o corpo de fiscalbr_stage_inbox_document n\xE3o \xE9 o esperado \u2014 confira \xE0 m\xE3o em vez de deixar passar calado';
3503
+ END IF;
3504
+
3505
+ v_def := replace(v_def, v_cols_antes, v_cols_depois);
3506
+ v_def := replace(v_def, v_vals_antes, v_vals_depois);
3507
+ v_def := replace(v_def, v_upd_antes, v_upd_depois);
3508
+ EXECUTE v_def;
3509
+ END $m$;
3510
+ `;
3511
+ var MIGRATION_017_QUEM_EMITE_E_O_CNPJ_NAO_A_LOJA = `-- Quem emite \xE9 o CNPJ, n\xE3o a loja.
3512
+ --
3513
+ -- As abas "Certificado" e "Emiss\xE3o Fiscal" do cadastro de empresa do V1
3514
+ -- guardam, em companies, como aquela pessoa jur\xEDdica emite NFS-e e NFC-e e se
3515
+ -- recebe DANFes da SEFAZ. No V2 a pessoa jur\xEDdica \xE9
3516
+ -- public.company_legal_entities (base), e a filial chega nela por
3517
+ -- app.units.company_legal_entity_id (168). Esta tabela \xE9 uma linha por pessoa
3518
+ -- jur\xEDdica:
3519
+ --
3520
+ -- unidade \u2192 app.units.company_legal_entity_id \u2192 company_legal_entities.id
3521
+ -- \u2192 plg_fiscalbr_emitter_settings.legal_entity_id (UNIQUE)
3522
+ --
3523
+ -- Duas filiais com o mesmo CNPJ dividem a mesma configura\xE7\xE3o, como no V1.
3524
+ --
3525
+ -- O certificado A1 N\xC3O mora aqui. O .pfx e a senha v\xE3o direto ao provedor
3526
+ -- (PlugNotas) e o que fica \xE9 o recibo: id no provedor, titular e validade. O
3527
+ -- CSC da NFC-e \xE9 segredo \u2014 o navegador grava, mas n\xE3o l\xEA de volta: a coluna
3528
+ -- fica fora do GRANT SELECT e a tela v\xEA s\xF3 \`nfce_csc_token_set\`.
3529
+ --
3530
+ -- Plano aprovado em docs/port/empresa.schema.json. Idempotente.
3531
+
3532
+ CREATE TABLE IF NOT EXISTS public.plg_fiscalbr_emitter_settings (
3533
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
3534
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
3535
+ legal_entity_id uuid NOT NULL,
3536
+
3537
+ nfse_enabled boolean NOT NULL DEFAULT false,
3538
+ nfse_production boolean NOT NULL DEFAULT false,
3539
+ nfse_national boolean NOT NULL DEFAULT false,
3540
+ nfse_service_code_national text,
3541
+ nfse_service_code_municipal text,
3542
+ nfse_nbs_code text,
3543
+ nfse_cnae text,
3544
+ nfse_iss_rate numeric(5,2) CHECK (nfse_iss_rate IS NULL OR (nfse_iss_rate >= 0 AND nfse_iss_rate <= 100)),
3545
+ nfse_iss_taxation smallint,
3546
+ nfse_iss_exigibility smallint,
3547
+ nfse_ibscbs_cst text,
3548
+ nfse_ibscbs_classification text,
3549
+ nfse_simples_regime smallint,
3550
+ nfse_net_value boolean NOT NULL DEFAULT false,
3551
+ nfse_additional_info text,
3552
+ nfse_city_features jsonb,
3553
+
3554
+ nfce_enabled boolean NOT NULL DEFAULT false,
3555
+ nfce_production boolean NOT NULL DEFAULT false,
3556
+ nfce_series integer CHECK (nfce_series IS NULL OR nfce_series > 0),
3557
+ nfce_csc_id text,
3558
+ nfce_csc_token text,
3559
+ nfce_csc_token_set boolean GENERATED ALWAYS AS (nfce_csc_token IS NOT NULL AND nfce_csc_token <> '') STORED,
3560
+ nfce_generic_item jsonb,
3561
+
3562
+ nfe_enabled boolean NOT NULL DEFAULT false,
3563
+
3564
+ certificate_provider_id text,
3565
+ certificate_cn text,
3566
+ certificate_expires_at timestamptz,
3567
+ certificate_uploaded_at timestamptz,
3568
+
3569
+ dfe_sync_enabled boolean NOT NULL DEFAULT false,
3570
+ dfe_sync_cursor text,
3571
+ dfe_last_sync_at timestamptz,
3572
+ provider_synced_at timestamptz,
3573
+
3574
+ created_at timestamptz NOT NULL DEFAULT now(),
3575
+ updated_at timestamptz NOT NULL DEFAULT now(),
3576
+ UNIQUE (tenant_id, id)
3577
+ );
3578
+
3579
+ DO $$
3580
+ BEGIN
3581
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_fiscalbr_emitter_settings_legal_entity_fkey') THEN
3582
+ -- company_legal_entities \xE9 da base, n\xE3o de outro plugin: a FK f\xEDsica \xE9
3583
+ -- permitida (a regra do 000 pro\xEDbe FK entre PLUGINS).
3584
+ ALTER TABLE ONLY public.plg_fiscalbr_emitter_settings
3585
+ ADD CONSTRAINT plg_fiscalbr_emitter_settings_legal_entity_fkey
3586
+ FOREIGN KEY (legal_entity_id) REFERENCES public.company_legal_entities(id) ON DELETE CASCADE;
3587
+ END IF;
3588
+ END $$;
3589
+
3590
+ CREATE UNIQUE INDEX IF NOT EXISTS plg_fiscalbr_emitter_settings_legal_entity_uidx
3591
+ ON public.plg_fiscalbr_emitter_settings USING btree (legal_entity_id);
3592
+
3593
+ COMMENT ON TABLE public.plg_fiscalbr_emitter_settings IS
3594
+ 'V1: companies (abas Certificado + Emiss\xE3o Fiscal). Como uma pessoa jur\xEDdica emite NFS-e/NFC-e e recebe DANFes. Uma linha por CNPJ; a filial chega por app.units.company_legal_entity_id.';
3595
+ COMMENT ON COLUMN public.plg_fiscalbr_emitter_settings.legal_entity_id IS 'A chave que conecta: company_legal_entities.id. UNIQUE.';
3596
+ COMMENT ON COLUMN public.plg_fiscalbr_emitter_settings.nfse_production IS 'V1: companies.nfse_producao \u2014 false = homologa\xE7\xE3o.';
3597
+ COMMENT ON COLUMN public.plg_fiscalbr_emitter_settings.nfse_national IS 'V1: companies.nfse_nacional \u2014 padr\xE3o nacional \xD7 municipal.';
3598
+ COMMENT ON COLUMN public.plg_fiscalbr_emitter_settings.nfse_service_code_national IS 'V1: companies.nfse_codigo_tributacao_nacional';
3599
+ COMMENT ON COLUMN public.plg_fiscalbr_emitter_settings.nfse_service_code_municipal IS 'V1: companies.nfse_codigo_tributacao_municipio';
3600
+ COMMENT ON COLUMN public.plg_fiscalbr_emitter_settings.nfse_nbs_code IS 'V1: companies.nfse_codigo_nbs';
3601
+ COMMENT ON COLUMN public.plg_fiscalbr_emitter_settings.nfse_cnae IS 'V1: companies.nfse_cnae \u2014 CNAE do servi\xE7o, pode diferir do principal.';
3602
+ COMMENT ON COLUMN public.plg_fiscalbr_emitter_settings.nfse_iss_rate IS 'V1: companies.nfse_iss_aliquota (%)';
3603
+ COMMENT ON COLUMN public.plg_fiscalbr_emitter_settings.nfse_iss_taxation IS 'V1: companies.nfse_iss_tipo_tributacao';
3604
+ COMMENT ON COLUMN public.plg_fiscalbr_emitter_settings.nfse_iss_exigibility IS 'V1: companies.nfse_iss_exigibilidade';
3605
+ COMMENT ON COLUMN public.plg_fiscalbr_emitter_settings.nfse_simples_regime IS 'V1: companies.nfse_regime_apuracao_sn \u2014 s\xF3 para optante do Simples.';
3606
+ COMMENT ON COLUMN public.plg_fiscalbr_emitter_settings.nfse_net_value IS 'V1: companies.nfse_valor_liquido \u2014 emitir pelo valor l\xEDquido.';
3607
+ COMMENT ON COLUMN public.plg_fiscalbr_emitter_settings.nfse_city_features IS 'V1: companies.nfse_city_features \u2014 o que a prefeitura aceita, devolvido pelo provedor.';
3608
+ COMMENT ON COLUMN public.plg_fiscalbr_emitter_settings.nfce_csc_token IS 'V1: companies.nfce_csc_token \u2014 SEGREDO. Fora do GRANT SELECT de authenticated.';
3609
+ COMMENT ON COLUMN public.plg_fiscalbr_emitter_settings.nfce_generic_item IS 'V1: companies.nfce_item_generico \u2014 {ncm, cfop, csosn}.';
3610
+ COMMENT ON COLUMN public.plg_fiscalbr_emitter_settings.certificate_provider_id IS 'V1: companies.certificate_plugnotas_id \u2014 recibo; o .pfx n\xE3o fica no banco.';
3611
+ COMMENT ON COLUMN public.plg_fiscalbr_emitter_settings.dfe_sync_enabled IS 'V1: companies.dfe_sync_enabled \u2014 receber DANFes automaticamente.';
3612
+ COMMENT ON COLUMN public.plg_fiscalbr_emitter_settings.dfe_sync_cursor IS 'V1: companies.dfe_sync_cursor \u2014 NSU onde a busca na SEFAZ parou.';
3613
+
3614
+ ALTER TABLE public.plg_fiscalbr_emitter_settings ENABLE ROW LEVEL SECURITY;
3615
+ ALTER TABLE public.plg_fiscalbr_emitter_settings FORCE ROW LEVEL SECURITY;
3616
+
3617
+ -- O segredo fica fora do SELECT: grava-se, n\xE3o se l\xEA.
3618
+ REVOKE ALL ON TABLE public.plg_fiscalbr_emitter_settings FROM anon, authenticated;
3619
+ GRANT INSERT, UPDATE, DELETE ON TABLE public.plg_fiscalbr_emitter_settings TO authenticated;
3620
+ GRANT SELECT (
3621
+ id, tenant_id, legal_entity_id,
3622
+ nfse_enabled, nfse_production, nfse_national, nfse_service_code_national, nfse_service_code_municipal,
3623
+ nfse_nbs_code, nfse_cnae, nfse_iss_rate, nfse_iss_taxation, nfse_iss_exigibility, nfse_ibscbs_cst,
3624
+ nfse_ibscbs_classification, nfse_simples_regime, nfse_net_value, nfse_additional_info, nfse_city_features,
3625
+ nfce_enabled, nfce_production, nfce_series, nfce_csc_id, nfce_csc_token_set, nfce_generic_item,
3626
+ nfe_enabled, certificate_provider_id, certificate_cn, certificate_expires_at, certificate_uploaded_at,
3627
+ dfe_sync_enabled, dfe_sync_cursor, dfe_last_sync_at, provider_synced_at, created_at, updated_at
3628
+ ) ON public.plg_fiscalbr_emitter_settings TO authenticated;
3629
+ GRANT ALL ON TABLE public.plg_fiscalbr_emitter_settings TO service_role;
3630
+
3631
+ DROP POLICY IF EXISTS plg_fiscalbr_emitter_settings_authz_select ON public.plg_fiscalbr_emitter_settings;
3632
+ CREATE POLICY plg_fiscalbr_emitter_settings_authz_select ON public.plg_fiscalbr_emitter_settings
3633
+ FOR SELECT TO authenticated USING (
3634
+ tenant_id = (SELECT app.current_tenant_id())
3635
+ AND (SELECT app.has_permission('financial.read', NULL::uuid)));
3636
+
3637
+ DROP POLICY IF EXISTS plg_fiscalbr_emitter_settings_authz_write ON public.plg_fiscalbr_emitter_settings;
3638
+ CREATE POLICY plg_fiscalbr_emitter_settings_authz_write ON public.plg_fiscalbr_emitter_settings
3639
+ FOR ALL TO authenticated
3640
+ USING (
3641
+ tenant_id = (SELECT app.current_tenant_id())
3642
+ AND (SELECT app.has_permission('settings.manage', NULL::uuid)))
3643
+ WITH CHECK (
3644
+ tenant_id = (SELECT app.current_tenant_id())
3645
+ AND (SELECT app.has_permission('settings.manage', NULL::uuid))
3646
+ AND EXISTS (SELECT 1 FROM public.company_legal_entities le
3647
+ WHERE le.id = plg_fiscalbr_emitter_settings.legal_entity_id
3648
+ AND le.tenant_id = plg_fiscalbr_emitter_settings.tenant_id));
3649
+ `;
2835
3650
  var MIGRATIONS = [
2836
3651
  { id: "000_baseline", sql: MIGRATION_000_BASELINE },
2837
3652
  { id: "001_preserve_terminal_results_and_recover_leases", sql: MIGRATION_001_PRESERVE_TERMINAL_RESULTS_AND_RECOVER_LEASES },
@@ -2843,7 +3658,14 @@ var MIGRATIONS = [
2843
3658
  { id: "007_a_rls_do_documento_passa_a_ser_legivel", sql: MIGRATION_007_A_RLS_DO_DOCUMENTO_PASSA_A_SER_LEGIVEL },
2844
3659
  { id: "008_a_nota_recebida_guarda_a_decisao_de_entrada", sql: MIGRATION_008_A_NOTA_RECEBIDA_GUARDA_A_DECISAO_DE_ENTRADA },
2845
3660
  { id: "009_aceitar_a_nota_escreve_estoque_e_titulo_de_uma_vez", sql: MIGRATION_009_ACEITAR_A_NOTA_ESCREVE_ESTOQUE_E_TITULO_DE_UMA_VEZ },
2846
- { id: "010_a_data_de_emissao_e_instante_e_a_competencia_e_dia", sql: MIGRATION_010_A_DATA_DE_EMISSAO_E_INSTANTE_E_A_COMPETENCIA_E_DIA }
3661
+ { id: "010_a_data_de_emissao_e_instante_e_a_competencia_e_dia", sql: MIGRATION_010_A_DATA_DE_EMISSAO_E_INSTANTE_E_A_COMPETENCIA_E_DIA },
3662
+ { id: "011_o_imposto_do_produto_e_do_fiscal", sql: MIGRATION_011_O_IMPOSTO_DO_PRODUTO_E_DO_FISCAL },
3663
+ { id: "012_quem_e_o_produto_se_pergunta_ao_indice", sql: MIGRATION_012_QUEM_E_O_PRODUTO_SE_PERGUNTA_AO_INDICE },
3664
+ { id: "013_a_sugestao_sobrevive_a_fechar_a_janela", sql: MIGRATION_013_A_SUGESTAO_SOBREVIVE_A_FECHAR_A_JANELA },
3665
+ { id: "014_a_descricao_da_nota_tem_lixo_no_meio", sql: MIGRATION_014_A_DESCRICAO_DA_NOTA_TEM_LIXO_NO_MEIO },
3666
+ { id: "015_o_movimento_diz_que_veio_da_nota", sql: MIGRATION_015_O_MOVIMENTO_DIZ_QUE_VEIO_DA_NOTA },
3667
+ { id: "016_a_nota_guarda_o_proprio_xml", sql: MIGRATION_016_A_NOTA_GUARDA_O_PROPRIO_XML },
3668
+ { id: "017_quem_emite_e_o_cnpj_nao_a_loja", sql: MIGRATION_017_QUEM_EMITE_E_O_CNPJ_NAO_A_LOJA }
2847
3669
  ];
2848
3670
  var taxGroupEntity = {
2849
3671
  name: "Grupo fiscal",
@@ -3175,6 +3997,10 @@ function parseNfeXml(xml) {
3175
3997
  }
3176
3998
  function toStagePayload(note, options = {}) {
3177
3999
  return {
4000
+ // O ARQUIVO VIAJA JUNTO. Ele já está na mão aqui — o parser acabou de lê-lo —
4001
+ // e sem isto os bytes morriam no fim da função: nota fiscal tem guarda legal
4002
+ // de cinco anos e a única cópia ficava na pasta de downloads de quem importou.
4003
+ ...options.xml ? { xml: options.xml } : {},
3178
4004
  access_key: note.accessKey,
3179
4005
  nfe_number: note.nfeNumber,
3180
4006
  nfe_series: note.nfeSeries,
@@ -3249,7 +4075,7 @@ function sb() {
3249
4075
  var NOTE_COLUMNS = `
3250
4076
  id, access_key, nfe_number, nfe_series, issue_date, received_at, status,
3251
4077
  manifestation_status, manifested_at, manifestation_attempts,
3252
- nature_operation, supplier_name, supplier_cnpj, supplier_party_id,
4078
+ nature_operation, supplier_name, supplier_cnpj, supplier_party_id, xml_content,
3253
4079
  total_value, total_products, total_discount, total_freight,
3254
4080
  invoice_id, inventory_operation_id, stocked_at, reviewed_at,
3255
4081
  default_chart_account_id, default_location_id, unit_id, document_id, metadata,
@@ -3286,6 +4112,7 @@ function rowToNote(row) {
3286
4112
  operationId: row.inventory_operation_id ?? null,
3287
4113
  stockedAt: row.stocked_at ?? null,
3288
4114
  documentId: row.document_id ?? null,
4115
+ hasXml: Boolean(row.xml_content),
3289
4116
  unitId: row.unit_id ?? null
3290
4117
  };
3291
4118
  }
@@ -3312,10 +4139,11 @@ async function listInboxNotes(options = {}) {
3312
4139
  var ITEM_COLUMNS = `
3313
4140
  id, item_number, supplier_product_code, ean_barcode, description, ncm, cest, cfop,
3314
4141
  unit_commercial, quantity_commercial, unit_price_commercial, total_price, discount,
4142
+ unit_taxable, quantity_taxable,
3315
4143
  icms_cst, icms_csosn, icms_base, icms_rate, icms_value, pis_value, cofins_value, ipi_value,
3316
4144
  product_id, entry_type, chart_account_id, stock_unit_id, stock_quantity,
3317
4145
  conversion_factor, cost_mode, unit_cost_new, reconciled, stock_movement_id,
3318
- shortage_quantity, shortage_reason, shortage_note
4146
+ shortage_quantity, shortage_reason, shortage_note, suggested_by, suggestion_confidence
3319
4147
  `;
3320
4148
  function rowToItem(row) {
3321
4149
  const quantity = Number(row.quantity_commercial ?? 0);
@@ -3333,6 +4161,8 @@ function rowToItem(row) {
3333
4161
  quantity,
3334
4162
  unitPrice: Number(row.unit_price_commercial ?? 0),
3335
4163
  total: Number(row.total_price ?? 0),
4164
+ unitTaxable: row.unit_taxable ?? null,
4165
+ quantityTaxable: row.quantity_taxable == null ? null : Number(row.quantity_taxable),
3336
4166
  discount: row.discount == null ? null : Number(row.discount),
3337
4167
  icmsCst: row.icms_cst ?? null,
3338
4168
  icmsCsosn: row.icms_csosn ?? null,
@@ -3349,6 +4179,8 @@ function rowToItem(row) {
3349
4179
  costMode: row.cost_mode ?? "novo",
3350
4180
  unitCostNew: row.unit_cost_new == null ? null : Number(row.unit_cost_new),
3351
4181
  reconciled: Boolean(row.reconciled),
4182
+ suggestedBy: row.suggested_by === "ai" || row.suggested_by === "ean" || row.suggested_by === "supplier_map" ? row.suggested_by : null,
4183
+ suggestionConfidence: row.suggestion_confidence == null ? null : Number(row.suggestion_confidence),
3352
4184
  stockMovementId: row.stock_movement_id ?? null,
3353
4185
  shortageQuantity: row.shortage_quantity == null ? null : Number(row.shortage_quantity),
3354
4186
  shortageReason: row.shortage_reason ?? null,
@@ -3394,7 +4226,7 @@ async function getInboxNote(id) {
3394
4226
  async function stageNoteFromXml(xml, options = {}) {
3395
4227
  const parsed = parseNfeXml(xml);
3396
4228
  const { data, error } = await sb().rpc("fiscalbr_stage_inbox_document", {
3397
- p_note: toStagePayload(parsed, { unitId: options.unitId ?? null })
4229
+ p_note: toStagePayload(parsed, { unitId: options.unitId ?? null, xml })
3398
4230
  });
3399
4231
  if (error) throw error;
3400
4232
  const res = data ?? {};
@@ -3419,6 +4251,8 @@ async function saveNoteDecision(input) {
3419
4251
  stock_unit_id: i.stockUnitId ?? null,
3420
4252
  conversion_factor: i.conversionFactor ?? 1,
3421
4253
  cost_mode: i.costMode ?? "novo",
4254
+ suggested_by: i.suggestedBy ?? null,
4255
+ suggestion_confidence: i.suggestionConfidence ?? null,
3422
4256
  shortage_quantity: i.shortageQuantity ?? null,
3423
4257
  shortage_reason: i.shortageReason ?? null,
3424
4258
  shortage_note: i.shortageNote ?? null
@@ -3482,6 +4316,57 @@ async function searchProducts(term, limit = 20) {
3482
4316
  kind: p.kind ?? null
3483
4317
  }));
3484
4318
  }
4319
+ async function getInboxNoteXml(id) {
4320
+ const { data, error } = await sb().from("plg_fiscalbr_inbox_documents").select("xml_content").eq("id", id).maybeSingle();
4321
+ if (error) throw error;
4322
+ return data?.xml_content ?? null;
4323
+ }
4324
+ async function findInboxNoteByNumber(nfeNumber) {
4325
+ const n = (nfeNumber ?? "").replace(/\D/g, "");
4326
+ if (!n) return null;
4327
+ const { data, error } = await sb().from("plg_fiscalbr_inbox_documents").select("id").eq("nfe_number", n).limit(2);
4328
+ if (error) throw error;
4329
+ const linhas = data ?? [];
4330
+ return linhas.length === 1 ? linhas[0].id : null;
4331
+ }
4332
+ async function findInboxNoteByOperation(operationId) {
4333
+ if (!operationId) return null;
4334
+ const { data, error } = await sb().from("plg_fiscalbr_inbox_documents").select("id").eq("inventory_operation_id", operationId).limit(1).maybeSingle();
4335
+ if (error) throw error;
4336
+ return data?.id ?? null;
4337
+ }
4338
+ async function suggestNoteItems(documentId, reevaluate = false) {
4339
+ const { data, error } = await sb().functions.invoke("fiscalbr-suggest-note-items", {
4340
+ body: { documentId, reevaluate, tenantId: getActiveTenantId() ?? null }
4341
+ });
4342
+ if (error) {
4343
+ const corpo = await error.context?.json?.().catch(() => null);
4344
+ throw new Error(corpo?.error ?? error.message);
4345
+ }
4346
+ const res = data ?? {};
4347
+ if (res.ok === false) throw new Error(String(res.error ?? "A sugest\xE3o falhou."));
4348
+ return {
4349
+ applied: Number(res.applied ?? 0),
4350
+ typedOnly: Number(res.typedOnly ?? 0),
4351
+ bound: Number(res.bound ?? 0),
4352
+ boundByEan: Number(res.boundByEan ?? 0),
4353
+ newProductNames: Array.isArray(res.newProductNames) ? res.newProductNames.filter((n) => typeof n === "string") : [],
4354
+ warnings: Array.isArray(res.warnings) ? res.warnings.filter((w) => typeof w === "string") : []
4355
+ };
4356
+ }
4357
+ async function listProductsByIds(ids) {
4358
+ if (ids.length === 0) return [];
4359
+ const { data, error } = await sb().from("products").select("id, name, sku, unit, cost, kind").in("id", ids);
4360
+ if (error) throw error;
4361
+ return (data ?? []).map((p) => ({
4362
+ id: String(p.id),
4363
+ name: p.name ?? "",
4364
+ sku: p.sku ?? null,
4365
+ unit: p.unit ?? null,
4366
+ cost: p.cost == null ? null : Number(p.cost),
4367
+ kind: p.kind ?? null
4368
+ }));
4369
+ }
3485
4370
  async function listMeasurementUnits() {
3486
4371
  const { data, error } = await sb().from("plg_inventory_measurement_units").select("id, name, abbreviation, is_base").eq("is_active", true).order("name");
3487
4372
  if (error) throw error;
@@ -3711,13 +4596,13 @@ function InboundNotesView({ onOpenNote, onOpenManifest }) {
3711
4596
  ] }) : /* @__PURE__ */ jsxs("div", { children: [
3712
4597
  /* @__PURE__ */ jsx(Inbox, { className: "mx-auto h-8 w-8 text-muted-foreground", "aria-hidden": true }),
3713
4598
  /* @__PURE__ */ jsx("p", { className: "mt-2 text-sm font-medium", children: "Nenhuma nota de entrada" }),
3714
- /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "As notas chegam da SEFAZ pelo Manifesto, ou voc\xEA arrasta o XML aqui." })
4599
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "As notas chegam da SEFAZ pela fila de manifesta\xE7\xE3o, ou voc\xEA arrasta o XML aqui." })
3715
4600
  ] });
3716
4601
  return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
3717
4602
  /* @__PURE__ */ jsxs(PageHeaderActions, { children: [
3718
4603
  onOpenManifest && /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", onClick: onOpenManifest, children: [
3719
4604
  /* @__PURE__ */ jsx(Inbox, { className: "h-4 w-4 mr-1.5" }),
3720
- "Manifesto de Notas"
4605
+ "Fila da SEFAZ"
3721
4606
  ] }),
3722
4607
  /* @__PURE__ */ jsxs(Button, { size: "sm", onClick: () => fileInput.current?.click(), disabled: busy, children: [
3723
4608
  busy ? /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 mr-1.5 animate-spin" }) : /* @__PURE__ */ jsx(UploadCloud, { className: "h-4 w-4 mr-1.5" }),
@@ -3862,6 +4747,24 @@ function StockBadge({ stockedAt, status }) {
3862
4747
  // src/types.ts
3863
4748
  var STOCK_BEARING_ENTRY_TYPES = ["insumo", "revenda", "patrimonio"];
3864
4749
  var isStockBearing = (type) => STOCK_BEARING_ENTRY_TYPES.includes(type);
4750
+
4751
+ // src/lib/note-suggestion.ts
4752
+ var PACKAGING_COUNT = {
4753
+ MI: 1e3,
4754
+ MIL: 1e3,
4755
+ MLH: 1e3,
4756
+ MILHEIRO: 1e3,
4757
+ "MI/1000": 1e3,
4758
+ DZ: 12,
4759
+ DUZIA: 12,
4760
+ "D\xDAZIA": 12,
4761
+ CENTO: 100,
4762
+ CENT: 100
4763
+ };
4764
+ var norm = (u) => (u ?? "").trim().toUpperCase().replace(/\.$/, "");
4765
+ function packagingCount(commercialUnit) {
4766
+ return PACKAGING_COUNT[norm(commercialUnit)] ?? null;
4767
+ }
3865
4768
  var money2 = (v) => new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL" }).format(v || 0);
3866
4769
  var qty = (v) => new Intl.NumberFormat("pt-BR", { maximumFractionDigits: 4 }).format(v || 0);
3867
4770
  var day2 = (iso) => iso ? iso.slice(0, 10).split("-").reverse().join("/") : "\u2014";
@@ -3885,9 +4788,15 @@ var draftOf = (item) => ({
3885
4788
  stockUnitId: item.stockUnitId,
3886
4789
  conversionFactor: String(item.conversionFactor ?? 1),
3887
4790
  costMode: item.costMode === "media" ? "media" : "novo",
3888
- shortageQuantity: item.shortageQuantity == null ? "" : String(item.shortageQuantity)
4791
+ shortageQuantity: item.shortageQuantity == null ? "" : String(item.shortageQuantity),
4792
+ // Vem do SERVIDOR: é o que faz o selo sobreviver a recarregar a página.
4793
+ suggestedBy: item.suggestedBy,
4794
+ suggestionConfidence: item.suggestionConfidence
3889
4795
  });
3890
- function NoteReconcileView({ noteId, onBack, onOpenInvoice }) {
4796
+ function NoteReconcileView({ noteId, onBack, onOpenInvoice, chrome = "page" }) {
4797
+ const emModal = chrome === "modal";
4798
+ const sangria = emModal ? "-mx-6 px-6" : "-mx-4 px-4";
4799
+ const cabecalho = (titulo) => emModal ? null : /* @__PURE__ */ jsx(SubpageHeader, { title: titulo, onBack, parentLabel: "Manifesto de Notas" });
3891
4800
  const [note, setNote] = useState(null);
3892
4801
  const [loading, setLoading] = useState(true);
3893
4802
  const [drafts, setDrafts] = useState({});
@@ -3904,6 +4813,9 @@ function NoteReconcileView({ noteId, onBack, onOpenInvoice }) {
3904
4813
  const [registryWarning, setRegistryWarning] = useState(null);
3905
4814
  const [error, setError] = useState(null);
3906
4815
  const [accepted, setAccepted] = useState(null);
4816
+ const [baixandoXml, setBaixandoXml] = useState(false);
4817
+ const [suggesting, setSuggesting] = useState(false);
4818
+ const [suggestionNote, setSuggestionNote] = useState(null);
3907
4819
  const load = useCallback(async () => {
3908
4820
  setLoading(true);
3909
4821
  try {
@@ -3929,9 +4841,13 @@ function NoteReconcileView({ noteId, onBack, onOpenInvoice }) {
3929
4841
  setLocations(l);
3930
4842
  setRegistryWarning(failed.length ? `N\xE3o consegui carregar ${failed.join(", ")}. Estes seletores v\xE3o aparecer vazios.` : null);
3931
4843
  if (detail) {
3932
- setDrafts(Object.fromEntries(detail.items.map((i) => [i.id, draftOf(i)])));
3933
- const done = Object.fromEntries(detail.items.filter((i) => i.reconciled).map((i) => [i.id, true]));
3934
- setChecked((prev) => ({ ...prev, ...done }));
4844
+ const vinculados = [...new Set(detail.items.map((i) => i.productId).filter((id) => !!id))];
4845
+ const nomes = vinculados.length ? new Map((await listProductsByIds(vinculados).catch(() => [])).map((p) => [p.id, p.name])) : /* @__PURE__ */ new Map();
4846
+ setDrafts(Object.fromEntries(detail.items.map((i) => [
4847
+ i.id,
4848
+ { ...draftOf(i), productLabel: i.productId && nomes.get(i.productId) || "" }
4849
+ ])));
4850
+ setChecked(Object.fromEntries(detail.items.filter((i) => i.reconciled).map((i) => [i.id, true])));
3935
4851
  const pending = detail.items.find((i) => !i.reconciled);
3936
4852
  setOpenIds(pending ? [pending.id] : []);
3937
4853
  setInstalments(detail.instalments.map((d) => ({
@@ -3955,7 +4871,14 @@ function NoteReconcileView({ noteId, onBack, onOpenInvoice }) {
3955
4871
  useEffect(() => {
3956
4872
  void load();
3957
4873
  }, [load]);
3958
- const patch = (id, change) => setDrafts((prev) => ({ ...prev, [id]: { ...prev[id], ...change } }));
4874
+ const patch = (id, change) => setDrafts((prev) => ({
4875
+ ...prev,
4876
+ [id]: {
4877
+ ...prev[id],
4878
+ ..."suggestedBy" in change ? {} : { suggestedBy: null, suggestionConfidence: null },
4879
+ ...change
4880
+ }
4881
+ }));
3959
4882
  const accountOptions = useMemo(
3960
4883
  () => accounts.map((a) => ({ id: a.id, label: `${a.code} \xB7 ${a.name}` })),
3961
4884
  [accounts]
@@ -3968,10 +4891,10 @@ function NoteReconcileView({ noteId, onBack, onOpenInvoice }) {
3968
4891
  () => locations.map((l) => ({ id: l.id, label: l.name })),
3969
4892
  [locations]
3970
4893
  );
3971
- const decisions = useMemo(() => {
4894
+ const buildDecisions = useCallback((rascunhos) => {
3972
4895
  if (!note) return [];
3973
4896
  return note.items.map((item) => {
3974
- const d = drafts[item.id] ?? draftOf(item);
4897
+ const d = rascunhos[item.id] ?? draftOf(item);
3975
4898
  return {
3976
4899
  id: item.id,
3977
4900
  productId: isStockBearing(d.entryType) ? d.productId : null,
@@ -3981,16 +4904,21 @@ function NoteReconcileView({ noteId, onBack, onOpenInvoice }) {
3981
4904
  stockUnitId: d.stockUnitId,
3982
4905
  conversionFactor: Number(d.conversionFactor.replace(",", ".")) || 1,
3983
4906
  costMode: d.costMode,
4907
+ suggestedBy: d.suggestedBy,
4908
+ suggestionConfidence: d.suggestionConfidence,
3984
4909
  shortageQuantity: d.shortageQuantity === "" ? null : Number(d.shortageQuantity.replace(",", "."))
3985
4910
  };
3986
4911
  });
3987
- }, [note, drafts, defaultAccount]);
4912
+ }, [note, defaultAccount]);
4913
+ const decisions = useMemo(() => buildDecisions(drafts), [buildDecisions, drafts]);
3988
4914
  const stockLines = decisions.filter((d) => isStockBearing(d.entryType));
3989
4915
  const missingProduct = stockLines.filter((d) => !d.productId).length;
3990
4916
  const missingType = decisions.filter((d) => !d.entryType).length;
4917
+ const suspectFactor = (item, d) => packagingCount(item.unit) !== null && isStockBearing(d.entryType) && ((Number(d.conversionFactor.replace(",", ".")) || 1) === 1 || !d.stockUnitId);
4918
+ const unverifiedFactor = note ? note.items.filter((i) => suspectFactor(i, drafts[i.id] ?? draftOf(i))).length : 0;
3991
4919
  const instalmentSum = instalments.reduce((acc, d) => acc + (Number(d.amount) || 0), 0);
3992
4920
  const readOnly = Boolean(note?.stockedAt || note?.invoiceId);
3993
- const isReady = (d) => Boolean(d.entryType) && (!isStockBearing(d.entryType) || Boolean(d.productId));
4921
+ const isReady = (item, d) => Boolean(d.entryType) && (!isStockBearing(d.entryType) || Boolean(d.productId)) && !suspectFactor(item, d);
3994
4922
  const doneCount = note ? note.items.filter((i) => checked[i.id]).length : 0;
3995
4923
  const toggleItem = (id) => setOpenIds((prev) => prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]);
3996
4924
  const confirmItem = (id) => {
@@ -4001,6 +4929,106 @@ function NoteReconcileView({ noteId, onBack, onOpenInvoice }) {
4001
4929
  return next && !rest.includes(next.id) ? [...rest, next.id] : rest;
4002
4930
  });
4003
4931
  };
4932
+ const persistir = useCallback(async (rascunhos) => {
4933
+ if (!note) return;
4934
+ await saveNoteDecision({
4935
+ documentId: note.id,
4936
+ items: buildDecisions(rascunhos),
4937
+ defaultChartAccountId: defaultAccount,
4938
+ defaultLocationId: location,
4939
+ instalments
4940
+ });
4941
+ }, [note, buildDecisions, defaultAccount, location, instalments]);
4942
+ const pendingItems = useMemo(
4943
+ () => (note?.items ?? []).filter((item) => {
4944
+ const d = drafts[item.id] ?? draftOf(item);
4945
+ return !d.entryType || isStockBearing(d.entryType) && !d.productId;
4946
+ }),
4947
+ [note, drafts]
4948
+ );
4949
+ const machineItems = useMemo(
4950
+ () => (note?.items ?? []).filter((item) => (drafts[item.id] ?? draftOf(item)).suggestedBy !== null),
4951
+ [note, drafts]
4952
+ );
4953
+ const sugerir = useCallback(async (reavaliar2 = false) => {
4954
+ if (!note) return;
4955
+ setSuggesting(true);
4956
+ setSuggestionNote(null);
4957
+ setError(null);
4958
+ try {
4959
+ await persistir(drafts);
4960
+ const res = await suggestNoteItems(note.id, reavaliar2);
4961
+ await load();
4962
+ if (res.applied === 0 && res.bound === 0 && res.typedOnly === 0) {
4963
+ setSuggestionNote(res.warnings[0] ?? "N\xE3o achei nada para preencher nestes itens.");
4964
+ return;
4965
+ }
4966
+ const semIA = res.bound ? `Sem IA: ${res.boundByEan ? `${res.boundByEan} pelo c\xF3digo de barras` : ""}` + (res.boundByEan && res.bound - res.boundByEan ? " e " : "") + (res.bound - res.boundByEan ? `${res.bound - res.boundByEan} pelo cat\xE1logo deste fornecedor` : "") + "." : "";
4967
+ setSuggestionNote([
4968
+ semIA,
4969
+ res.applied ? `${res.applied} ${res.applied === 1 ? "item preenchido" : "itens preenchidos"} pela IA.` : "",
4970
+ res.typedOnly ? `${res.typedOnly} ${res.typedOnly === 1 ? "item ficou" : "itens ficaram"} sem produto no cat\xE1logo` + (res.newProductNames.length ? ` (${res.newProductNames.slice(0, 3).join(", ")})` : "") + " \u2014 escolha o produto ou cadastre antes de aceitar." : "",
4971
+ res.applied || res.bound ? "Salvo." : "",
4972
+ ...res.warnings
4973
+ ].filter(Boolean).join(" "));
4974
+ } catch (e) {
4975
+ setError(e instanceof Error ? e.message : String(e));
4976
+ } finally {
4977
+ setSuggesting(false);
4978
+ }
4979
+ }, [note, drafts, persistir, load]);
4980
+ const reavaliar = pendingItems.length === 0 && machineItems.length > 0;
4981
+ const botaoIA = readOnly || pendingItems.length === 0 && !reavaliar ? null : /* @__PURE__ */ jsx(
4982
+ AiActionButton,
4983
+ {
4984
+ size: "sm",
4985
+ className: "shrink-0",
4986
+ tone: reavaliar ? "soft" : "solid",
4987
+ busy: suggesting,
4988
+ busyLabel: "Pensando\u2026",
4989
+ onClick: () => {
4990
+ void sugerir(reavaliar);
4991
+ },
4992
+ children: reavaliar ? "Rodar IA novamente" : "Sugerir com IA"
4993
+ }
4994
+ );
4995
+ const moldura = (node, titulo, rodape2) => emModal ? /* @__PURE__ */ jsxs(Fragment, { children: [
4996
+ /* @__PURE__ */ jsxs(ModalHeader, { className: "shrink-0 flex-row items-center justify-between gap-3 space-y-0 border-b px-6 py-3 pr-14 text-left sm:flex-row", children: [
4997
+ /* @__PURE__ */ jsx(ModalTitle, { className: "min-w-0 truncate", children: titulo }),
4998
+ botaoIA
4999
+ ] }),
5000
+ /* @__PURE__ */ jsx(ModalBody, { className: "px-6 py-4", children: node }),
5001
+ rodape2 && /* @__PURE__ */ jsx(ModalFooter, { className: "shrink-0 flex-row items-center justify-between gap-3 space-x-0 border-t px-6 py-3 pr-16", children: rodape2 })
5002
+ ] }) : (
5003
+ // Em tela cheia não há moldura onde pendurar o rodapé: ele volta para dentro
5004
+ // do conteúdo, grudado na base da janela enquanto a página rola.
5005
+ /* @__PURE__ */ jsxs(Fragment, { children: [
5006
+ node,
5007
+ rodape2 && /* @__PURE__ */ jsx("div", { className: `sticky bottom-0 ${sangria} flex items-center justify-between gap-3 border-t bg-card/95 py-3 pr-16 backdrop-blur`, children: rodape2 })
5008
+ ] })
5009
+ );
5010
+ const baixarXml = useCallback(async () => {
5011
+ if (!note) return;
5012
+ setBaixandoXml(true);
5013
+ setError(null);
5014
+ try {
5015
+ const xml = await getInboxNoteXml(note.id);
5016
+ if (!xml) {
5017
+ setError("O arquivo desta nota n\xE3o est\xE1 guardado.");
5018
+ return;
5019
+ }
5020
+ const url = URL.createObjectURL(new Blob([xml], { type: "application/xml" }));
5021
+ const a = document.createElement("a");
5022
+ a.href = url;
5023
+ a.download = `${note.accessKey || note.number || "nfe"}.xml`;
5024
+ a.click();
5025
+ URL.revokeObjectURL(url);
5026
+ } catch (e) {
5027
+ setError(e instanceof Error ? e.message : String(e));
5028
+ } finally {
5029
+ setBaixandoXml(false);
5030
+ }
5031
+ }, [note]);
4004
5032
  const save = async () => {
4005
5033
  if (!note) return;
4006
5034
  setSaving(true);
@@ -4041,8 +5069,8 @@ function NoteReconcileView({ noteId, onBack, onOpenInvoice }) {
4041
5069
  }
4042
5070
  };
4043
5071
  if (loading) {
4044
- return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
4045
- /* @__PURE__ */ jsx(SubpageHeader, { title: "Concilia\xE7\xE3o", onBack, parentLabel: "Entrada de Notas" }),
5072
+ return moldura(/* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
5073
+ cabecalho("Concilia\xE7\xE3o"),
4046
5074
  /* @__PURE__ */ jsxs(Card, { className: "p-4", children: [
4047
5075
  /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-32" }),
4048
5076
  /* @__PURE__ */ jsx("div", { className: "mt-3 grid gap-x-6 gap-y-3 sm:grid-cols-2 lg:grid-cols-3", children: [0, 1, 2, 3, 4, 5].map((i) => /* @__PURE__ */ jsxs("div", { children: [
@@ -4066,26 +5094,64 @@ function NoteReconcileView({ noteId, onBack, onOpenInvoice }) {
4066
5094
  ] }),
4067
5095
  /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-20" })
4068
5096
  ] }, i)) })
4069
- ] });
5097
+ ] }), "Concilia\xE7\xE3o");
4070
5098
  }
4071
5099
  if (!note) {
4072
- return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
4073
- /* @__PURE__ */ jsx(SubpageHeader, { title: "Concilia\xE7\xE3o", onBack, parentLabel: "Entrada de Notas" }),
5100
+ return moldura(/* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
5101
+ cabecalho("Concilia\xE7\xE3o"),
4074
5102
  /* @__PURE__ */ jsx(Card, { className: "p-8 text-center text-sm", children: error ? /* @__PURE__ */ jsxs("span", { className: "text-destructive", children: [
4075
5103
  "N\xE3o consegui carregar a nota: ",
4076
5104
  error
4077
5105
  ] }) : /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "Nota n\xE3o encontrada." }) })
4078
- ] });
5106
+ ] }), "Concilia\xE7\xE3o");
4079
5107
  }
4080
- return /* @__PURE__ */ jsxs("div", { className: "space-y-4 pb-24", children: [
4081
- /* @__PURE__ */ jsx(
4082
- SubpageHeader,
5108
+ const rodape = readOnly ? null : /* @__PURE__ */ jsxs(Fragment, { children: [
5109
+ /* @__PURE__ */ jsxs(
5110
+ Button,
4083
5111
  {
4084
- title: `Concilia\xE7\xE3o \xB7 NF-e ${note.number}`,
4085
- onBack,
4086
- parentLabel: "Entrada de Notas"
5112
+ variant: "ghost",
5113
+ onClick: async () => {
5114
+ await rejectNote(note.id);
5115
+ onBack();
5116
+ },
5117
+ disabled: saving || accepting,
5118
+ children: [
5119
+ /* @__PURE__ */ jsx(X, { className: "mr-1.5 h-4 w-4" }),
5120
+ "Rejeitar"
5121
+ ]
4087
5122
  }
4088
5123
  ),
5124
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
5125
+ (missingType > 0 || missingProduct > 0) && /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
5126
+ unverifiedFactor > 0 && `${unverifiedFactor} com fator por conferir`,
5127
+ unverifiedFactor > 0 && (missingType > 0 || missingProduct > 0) && " \xB7 ",
5128
+ missingType > 0 && `${missingType} sem tipo`,
5129
+ missingType > 0 && missingProduct > 0 && " \xB7 ",
5130
+ missingProduct > 0 && `${missingProduct} sem produto`
5131
+ ] }),
5132
+ /* @__PURE__ */ jsxs(Button, { variant: "outline", onClick: () => {
5133
+ void save();
5134
+ }, disabled: saving || accepting, children: [
5135
+ saving && /* @__PURE__ */ jsx(Loader2, { className: "mr-1.5 h-4 w-4 animate-spin" }),
5136
+ "Salvar concilia\xE7\xE3o"
5137
+ ] }),
5138
+ /* @__PURE__ */ jsxs(
5139
+ Button,
5140
+ {
5141
+ onClick: () => {
5142
+ void accept();
5143
+ },
5144
+ disabled: accepting || saving || missingType > 0 || missingProduct > 0 || unverifiedFactor > 0,
5145
+ children: [
5146
+ accepting ? /* @__PURE__ */ jsx(Loader2, { className: "mr-1.5 h-4 w-4 animate-spin" }) : /* @__PURE__ */ jsx(Check, { className: "mr-1.5 h-4 w-4" }),
5147
+ "Aceitar"
5148
+ ]
5149
+ }
5150
+ )
5151
+ ] })
5152
+ ] });
5153
+ return moldura(/* @__PURE__ */ jsxs("div", { className: `space-y-4 ${emModal ? "" : "pb-24"}`, children: [
5154
+ cabecalho(`Concilia\xE7\xE3o \xB7 NF-e ${note.number}`),
4089
5155
  accepted && /* @__PURE__ */ jsxs(Card, { className: "border-success bg-success-soft p-4", children: [
4090
5156
  /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-success-soft-foreground", children: accepted.alreadyAccepted ? "Esta nota j\xE1 havia sido aceita \u2014 nada foi lan\xE7ado de novo." : `Aceita: ${accepted.stockLines} ${accepted.stockLines === 1 ? "linha" : "linhas"} de estoque e um t\xEDtulo de ${money2(accepted.invoiceTotal)} em ${accepted.instalments} ${accepted.instalments === 1 ? "parcela" : "parcelas"}.` }),
4091
5157
  accepted.invoiceId && onOpenInvoice && /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", className: "mt-2", onClick: () => onOpenInvoice(accepted.invoiceId), children: "Abrir a conta a pagar" })
@@ -4130,6 +5196,25 @@ function NoteReconcileView({ noteId, onBack, onOpenInvoice }) {
4130
5196
  /* @__PURE__ */ jsx(RefreshCw, { className: "mr-1.5 h-4 w-4" }),
4131
5197
  "Atualizar status"
4132
5198
  ] }),
5199
+ note.hasXml ? /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", disabled: baixandoXml, onClick: () => {
5200
+ void baixarXml();
5201
+ }, children: [
5202
+ baixandoXml ? /* @__PURE__ */ jsx(Loader2, { className: "mr-1.5 h-4 w-4 animate-spin" }) : /* @__PURE__ */ jsx(FileDown, { className: "mr-1.5 h-4 w-4" }),
5203
+ "Baixar XML"
5204
+ ] }) : /* @__PURE__ */ jsx(
5205
+ EmBreve,
5206
+ {
5207
+ label: "Baixar XML",
5208
+ o_que: "O arquivo desta nota n\xE3o foi guardado \u2014 ela entrou antes de o est\xE1gio passar a gravar o XML. Reimportar o .xml preenche"
5209
+ }
5210
+ ),
5211
+ /* @__PURE__ */ jsx(
5212
+ EmBreve,
5213
+ {
5214
+ label: "DANFE (PDF)",
5215
+ o_que: "O espelho impresso da nota. Tem layout fixado pela SEFAZ e vinha renderizado da PlugNotas no V1 \u2014 n\xE3o sai do XML sozinho"
5216
+ }
5217
+ ),
4133
5218
  /* @__PURE__ */ jsx(
4134
5219
  EmBreve,
4135
5220
  {
@@ -4218,6 +5303,19 @@ function NoteReconcileView({ noteId, onBack, onOpenInvoice }) {
4218
5303
  onClick: () => setOpenIds(openIds.length ? [] : note.items.map((i) => i.id)),
4219
5304
  children: openIds.length ? "Recolher todos" : "Abrir todos"
4220
5305
  }
5306
+ ),
5307
+ !emModal && botaoIA
5308
+ ] }),
5309
+ suggestionNote && /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2 rounded-md border border-magic/30 bg-magic-soft px-3 py-2", children: [
5310
+ /* @__PURE__ */ jsx("p", { className: "flex-1 text-xs text-magic-soft-foreground", children: suggestionNote }),
5311
+ /* @__PURE__ */ jsx(
5312
+ "button",
5313
+ {
5314
+ type: "button",
5315
+ className: "shrink-0 text-xs text-magic-soft-foreground/70 hover:underline",
5316
+ onClick: () => setSuggestionNote(null),
5317
+ children: "Fechar"
5318
+ }
4221
5319
  )
4222
5320
  ] }),
4223
5321
  note.items.map((item) => {
@@ -4229,7 +5327,7 @@ function NoteReconcileView({ noteId, onBack, onOpenInvoice }) {
4229
5327
  const bearing = isStockBearing(d.entryType);
4230
5328
  const open = openIds.includes(item.id);
4231
5329
  const done = Boolean(checked[item.id]);
4232
- const ready = isReady(d);
5330
+ const ready = isReady(item, d);
4233
5331
  const typeLabel = ENTRY_TYPES.find((t) => t.value === d.entryType)?.label;
4234
5332
  return /* @__PURE__ */ jsxs(Card, { className: "overflow-hidden", children: [
4235
5333
  /* @__PURE__ */ jsxs(
@@ -4260,7 +5358,8 @@ function NoteReconcileView({ noteId, onBack, onOpenInvoice }) {
4260
5358
  ] })
4261
5359
  ] }),
4262
5360
  /* @__PURE__ */ jsx("span", { className: "shrink-0 text-sm tabular-nums", children: money2(item.total) }),
4263
- /* @__PURE__ */ jsx(ItemState, { done, ready })
5361
+ d.suggestedBy && !done && /* @__PURE__ */ jsx(AuthorBadge, { draft: d }),
5362
+ /* @__PURE__ */ jsx(ItemState, { done: done && ready, ready })
4264
5363
  ]
4265
5364
  }
4266
5365
  ),
@@ -4363,6 +5462,28 @@ function NoteReconcileView({ noteId, onBack, onOpenInvoice }) {
4363
5462
  )
4364
5463
  ] })
4365
5464
  ] }),
5465
+ suspectFactor(item, d) && /* @__PURE__ */ jsx("div", { className: "rounded-md border border-warning bg-warning-soft px-3 py-2", children: /* @__PURE__ */ jsxs("p", { className: "text-xs text-warning-soft-foreground", children: [
5466
+ /* @__PURE__ */ jsx(AlertTriangle, { className: "mr-1 inline h-3.5 w-3.5", "aria-hidden": true }),
5467
+ "Este item vem em ",
5468
+ /* @__PURE__ */ jsx("strong", { children: item.unit }),
5469
+ ", que s\xE3o",
5470
+ " ",
5471
+ qty(packagingCount(item.unit) ?? 0),
5472
+ " por embalagem.",
5473
+ " ",
5474
+ !d.stockUnitId ? /* @__PURE__ */ jsxs(Fragment, { children: [
5475
+ "Escolha a ",
5476
+ /* @__PURE__ */ jsx("strong", { children: "unidade de estoque" }),
5477
+ " para confirmar o fator \u2014 o produto est\xE1 cadastrado sem unidade."
5478
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
5479
+ "O fator ainda \xE9 1: do jeito que est\xE1 entrariam ",
5480
+ qty(item.quantity),
5481
+ " ",
5482
+ "em vez de ",
5483
+ qty(item.quantity * (packagingCount(item.unit) ?? 1)),
5484
+ "."
5485
+ ] })
5486
+ ] }) }),
4366
5487
  /* @__PURE__ */ jsxs("div", { className: "rounded-md bg-muted/50 px-3 py-2", children: [
4367
5488
  /* @__PURE__ */ jsxs("div", { className: "flex items-baseline justify-between text-sm", children: [
4368
5489
  /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: "Qtd estoque (calc.)" }),
@@ -4583,51 +5704,18 @@ function NoteReconcileView({ noteId, onBack, onOpenInvoice }) {
4583
5704
  )
4584
5705
  ] })
4585
5706
  ] }),
4586
- error && /* @__PURE__ */ jsx(Card, { className: "border-destructive bg-destructive/5 p-4", children: /* @__PURE__ */ jsx("p", { className: "text-sm text-destructive", children: error }) }),
4587
- !readOnly && /* @__PURE__ */ jsxs("div", { className: "sticky bottom-0 -mx-4 flex items-center justify-between gap-3 border-t bg-card/95 px-4 py-3 pr-16 backdrop-blur", children: [
4588
- /* @__PURE__ */ jsxs(
4589
- Button,
4590
- {
4591
- variant: "ghost",
4592
- onClick: async () => {
4593
- await rejectNote(note.id);
4594
- onBack();
4595
- },
4596
- disabled: saving || accepting,
4597
- children: [
4598
- /* @__PURE__ */ jsx(X, { className: "mr-1.5 h-4 w-4" }),
4599
- "Rejeitar"
4600
- ]
4601
- }
4602
- ),
4603
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
4604
- (missingType > 0 || missingProduct > 0) && /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
4605
- missingType > 0 && `${missingType} sem tipo`,
4606
- missingType > 0 && missingProduct > 0 && " \xB7 ",
4607
- missingProduct > 0 && `${missingProduct} sem produto`
4608
- ] }),
4609
- /* @__PURE__ */ jsxs(Button, { variant: "outline", onClick: () => {
4610
- void save();
4611
- }, disabled: saving || accepting, children: [
4612
- saving && /* @__PURE__ */ jsx(Loader2, { className: "mr-1.5 h-4 w-4 animate-spin" }),
4613
- "Salvar concilia\xE7\xE3o"
4614
- ] }),
4615
- /* @__PURE__ */ jsxs(
4616
- Button,
4617
- {
4618
- onClick: () => {
4619
- void accept();
4620
- },
4621
- disabled: accepting || saving || missingType > 0 || missingProduct > 0,
4622
- children: [
4623
- accepting ? /* @__PURE__ */ jsx(Loader2, { className: "mr-1.5 h-4 w-4 animate-spin" }) : /* @__PURE__ */ jsx(Check, { className: "mr-1.5 h-4 w-4" }),
4624
- "Aceitar"
4625
- ]
4626
- }
4627
- )
4628
- ] })
4629
- ] })
4630
- ] });
5707
+ error && /* @__PURE__ */ jsx(Card, { className: "border-destructive bg-destructive/5 p-4", children: /* @__PURE__ */ jsx("p", { className: "text-sm text-destructive", children: error }) })
5708
+ ] }), `Concilia\xE7\xE3o \xB7 NF-e ${note.number}`, rodape);
5709
+ }
5710
+ function AuthorBadge({ draft }) {
5711
+ if (draft.suggestedBy === "ai") {
5712
+ const confira = draft.suggestionConfidence != null && draft.suggestionConfidence < 0.6;
5713
+ return /* @__PURE__ */ jsxs("span", { className: "inline-flex shrink-0 items-center rounded-full bg-magic-soft px-2 py-0.5 text-[11px] font-medium text-magic-soft-foreground", children: [
5714
+ "IA",
5715
+ confira ? " \xB7 confira" : ""
5716
+ ] });
5717
+ }
5718
+ return /* @__PURE__ */ jsx("span", { className: "inline-flex shrink-0 items-center rounded-full bg-info-soft px-2 py-0.5 text-[11px] font-medium text-info-soft-foreground", children: draft.suggestedBy === "ean" ? "EAN" : "fornecedor" });
4631
5719
  }
4632
5720
  function ItemState({ done, ready }) {
4633
5721
  if (done) {
@@ -4651,7 +5739,7 @@ var MANIFEST_TONE = {
4651
5739
  processando: "bg-info-soft text-info-soft-foreground",
4652
5740
  erro: "bg-destructive/10 text-destructive"
4653
5741
  };
4654
- function ManifestView({ onOpenNote }) {
5742
+ function ManifestView({ onOpenNote, onBack }) {
4655
5743
  const [notes, setNotes] = useState([]);
4656
5744
  const [loading, setLoading] = useState(true);
4657
5745
  const [tab, setTab] = useState("pendentes");
@@ -4687,6 +5775,7 @@ function ManifestView({ onOpenNote }) {
4687
5775
  ]
4688
5776
  }], [tab]);
4689
5777
  return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
5778
+ onBack && /* @__PURE__ */ jsx(SubpageHeader, { title: "Fila da SEFAZ", onBack, parentLabel: "Manifesto de Notas" }),
4690
5779
  /* @__PURE__ */ jsxs(Card, { className: "p-4", children: [
4691
5780
  /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
4692
5781
  /* @__PURE__ */ jsx(ShieldCheck, { className: "mt-0.5 h-5 w-5 text-muted-foreground", "aria-hidden": true }),
@@ -4755,13 +5844,17 @@ var pathOf = (view) => view.kind === "note" ? `${HASH_PREFIX}/note/${view.id}` :
4755
5844
  var STACK_MARK = { inboundNotes: true };
4756
5845
  function InboundNotesPage({ payableRoute } = {}) {
4757
5846
  const [view, setView] = useState(viewFromLocation);
5847
+ const [background, setBackground] = useState(
5848
+ () => viewFromLocation().kind === "manifest" ? "manifest" : "list"
5849
+ );
4758
5850
  const go = useCallback((next) => {
4759
5851
  setView(next);
5852
+ if (next.kind !== "note") setBackground(next.kind);
4760
5853
  if (typeof window === "undefined") return;
4761
5854
  const path = pathOf(next);
4762
5855
  if (window.location.hash === path) return;
4763
5856
  const empilhado = window.history.state?.inboundNotes;
4764
- if (next.kind === "list" && empilhado) {
5857
+ if (next.kind !== "note" && empilhado) {
4765
5858
  window.history.back();
4766
5859
  return;
4767
5860
  }
@@ -4770,7 +5863,11 @@ function InboundNotesPage({ payableRoute } = {}) {
4770
5863
  }, []);
4771
5864
  useEffect(() => {
4772
5865
  if (typeof window === "undefined") return;
4773
- const onPop = () => setView(viewFromLocation());
5866
+ const onPop = () => {
5867
+ const next = viewFromLocation();
5868
+ setView(next);
5869
+ if (next.kind !== "note") setBackground(next.kind);
5870
+ };
4774
5871
  window.addEventListener("popstate", onPop);
4775
5872
  window.addEventListener("hashchange", onPop);
4776
5873
  return () => {
@@ -4778,34 +5875,436 @@ function InboundNotesPage({ payableRoute } = {}) {
4778
5875
  window.removeEventListener("hashchange", onPop);
4779
5876
  };
4780
5877
  }, []);
4781
- if (view.kind === "note") {
4782
- return /* @__PURE__ */ jsx(
5878
+ const fecharNota = useCallback(() => go({ kind: background }), [go, background]);
5879
+ useEffect(() => {
5880
+ if (typeof window === "undefined") return;
5881
+ const op = window.location.hash.match(/[?&]operation=([0-9a-f-]{36})/i)?.[1];
5882
+ if (!op) return;
5883
+ let vivo = true;
5884
+ findInboxNoteByOperation(op).then((id) => {
5885
+ if (vivo && id) go({ kind: "note", id });
5886
+ }).catch(() => {
5887
+ });
5888
+ return () => {
5889
+ vivo = false;
5890
+ };
5891
+ }, [go]);
5892
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
5893
+ background === "manifest" ? /* @__PURE__ */ jsx(ManifestView, { onOpenNote: (id) => go({ kind: "note", id }), onBack: () => go({ kind: "list" }) }) : /* @__PURE__ */ jsx(
5894
+ InboundNotesView,
5895
+ {
5896
+ onOpenNote: (id) => go({ kind: "note", id }),
5897
+ onOpenManifest: () => go({ kind: "manifest" })
5898
+ }
5899
+ ),
5900
+ /* @__PURE__ */ jsx(Modal, { open: view.kind === "note", onOpenChange: (open) => {
5901
+ if (!open) fecharNota();
5902
+ }, children: view.kind === "note" && // `noPadding`: a conciliação desenha o próprio cabeçalho e o próprio
5903
+ // corpo — o título e o «Sugerir com IA» ficam fora da área que rola, e o
5904
+ // rodapé «Aceitar» precisa encostar nas bordas do modal.
5905
+ /* @__PURE__ */ jsx(ModalContent, { size: "5xl", noPadding: true, className: "sm:max-h-[90vh]", children: /* @__PURE__ */ jsx(
4783
5906
  NoteReconcileView,
4784
5907
  {
5908
+ chrome: "modal",
4785
5909
  noteId: view.id,
4786
- onBack: () => go({ kind: "list" }),
5910
+ onBack: fecharNota,
4787
5911
  onOpenInvoice: payableRoute ? (invoiceId) => {
4788
5912
  window.location.hash = payableRoute(invoiceId);
4789
5913
  } : void 0
4790
5914
  }
4791
- );
4792
- }
4793
- if (view.kind === "manifest") {
4794
- return /* @__PURE__ */ jsx("div", { className: "space-y-4", children: /* @__PURE__ */ jsx(ManifestView, { onOpenNote: (id) => go({ kind: "note", id }) }) });
4795
- }
4796
- return /* @__PURE__ */ jsx(
4797
- InboundNotesView,
4798
- {
4799
- onOpenNote: (id) => go({ kind: "note", id }),
4800
- onOpenManifest: () => go({ kind: "manifest" })
4801
- }
4802
- );
5915
+ ) }) })
5916
+ ] });
4803
5917
  }
4804
5918
  function makeInboundNotesRoute(options = {}) {
4805
5919
  return function InboundNotesRoute() {
4806
5920
  return /* @__PURE__ */ jsx(InboundNotesPage, { ...options });
4807
5921
  };
4808
5922
  }
5923
+ var COLUMNS = [
5924
+ "id",
5925
+ "legal_entity_id",
5926
+ "nfse_enabled",
5927
+ "nfse_production",
5928
+ "nfse_national",
5929
+ "nfse_service_code_national",
5930
+ "nfse_service_code_municipal",
5931
+ "nfse_nbs_code",
5932
+ "nfse_cnae",
5933
+ "nfse_iss_rate",
5934
+ "nfse_iss_taxation",
5935
+ "nfse_ibscbs_cst",
5936
+ "nfse_ibscbs_classification",
5937
+ "nfse_simples_regime",
5938
+ "nfse_net_value",
5939
+ "nfse_additional_info",
5940
+ "nfce_enabled",
5941
+ "nfce_production",
5942
+ "nfce_series",
5943
+ "nfce_csc_id",
5944
+ "nfce_csc_token_set",
5945
+ "nfce_generic_item",
5946
+ "certificate_cn",
5947
+ "certificate_expires_at",
5948
+ "certificate_uploaded_at",
5949
+ "dfe_sync_enabled",
5950
+ "dfe_last_sync_at",
5951
+ "provider_synced_at"
5952
+ ].join(", ");
5953
+ var sb2 = () => getSupabaseClientOptional();
5954
+ function useEmitterSettings(legalEntityId) {
5955
+ const [row, setRow] = React5.useState(null);
5956
+ const [loading, setLoading] = React5.useState(true);
5957
+ React5.useEffect(() => {
5958
+ if (!legalEntityId) {
5959
+ setLoading(false);
5960
+ return;
5961
+ }
5962
+ let alive = true;
5963
+ void sb2()?.from("plg_fiscalbr_emitter_settings").select(COLUMNS).eq("legal_entity_id", legalEntityId).maybeSingle().then(({ data, error }) => {
5964
+ if (!alive) return;
5965
+ if (error) toast.error("N\xE3o foi poss\xEDvel ler a configura\xE7\xE3o fiscal", { description: error.message });
5966
+ setRow(data ?? {});
5967
+ setLoading(false);
5968
+ });
5969
+ return () => {
5970
+ alive = false;
5971
+ };
5972
+ }, [legalEntityId]);
5973
+ const save = React5.useCallback(async (tenantId, patch) => {
5974
+ if (!legalEntityId) return false;
5975
+ const payload = { ...patch, tenant_id: tenantId, legal_entity_id: legalEntityId, updated_at: (/* @__PURE__ */ new Date()).toISOString() };
5976
+ const { data, error } = await sb2().from("plg_fiscalbr_emitter_settings").upsert(payload, { onConflict: "legal_entity_id" }).select(COLUMNS).single();
5977
+ if (error) {
5978
+ toast.error("N\xE3o foi poss\xEDvel salvar", { description: error.message });
5979
+ return false;
5980
+ }
5981
+ setRow(data);
5982
+ return true;
5983
+ }, [legalEntityId]);
5984
+ return { row, loading, save };
5985
+ }
5986
+ function NeedsLegalEntity({ item, what }) {
5987
+ return /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardContent, { className: "flex flex-col items-start gap-3 py-6", children: [
5988
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: "Falta o CNPJ desta unidade" }),
5989
+ /* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground", children: [
5990
+ what,
5991
+ " pertence \xE0 pessoa jur\xEDdica. Preencha a raz\xE3o social e o CNPJ em Dados da empresa para configurar aqui."
5992
+ ] }),
5993
+ /* @__PURE__ */ jsx(Button, { variant: "outline", onClick: () => adminNavigateTo(`/settings/locations/${item.id}/edit`), children: "Preencher CNPJ" })
5994
+ ] }) });
5995
+ }
5996
+ function LoadingCard() {
5997
+ return /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardContent, { className: "space-y-3 py-6", children: [
5998
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-5 w-48" }),
5999
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-10 w-full" }),
6000
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-10 w-full" })
6001
+ ] }) });
6002
+ }
6003
+ function ToggleRow({ title, description, checked, onChange, disabled, children }) {
6004
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-4", children: [
6005
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
6006
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: title }),
6007
+ description && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: description })
6008
+ ] }),
6009
+ /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [
6010
+ children,
6011
+ /* @__PURE__ */ jsx(Switch, { checked, disabled, onCheckedChange: onChange })
6012
+ ] })
6013
+ ] });
6014
+ }
6015
+ function Field2({ label, hint, children }) {
6016
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
6017
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: label }),
6018
+ children,
6019
+ hint && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: hint })
6020
+ ] });
6021
+ }
6022
+ function SummaryCard({ title, lines, badge, onConfigure }) {
6023
+ return /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardHeader, { className: "flex-row items-start justify-between gap-3 space-y-0", children: [
6024
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
6025
+ /* @__PURE__ */ jsx(CardTitle, { className: "text-base", children: title }),
6026
+ /* @__PURE__ */ jsx("dl", { className: "mt-2 grid gap-x-6 gap-y-1 text-sm", style: { gridTemplateColumns: "auto 1fr" }, children: lines.map((l) => /* @__PURE__ */ jsxs(React5.Fragment, { children: [
6027
+ /* @__PURE__ */ jsx("dt", { className: "text-muted-foreground", children: l.label }),
6028
+ /* @__PURE__ */ jsx("dd", { className: "text-foreground", children: l.value })
6029
+ ] }, l.label)) })
6030
+ ] }),
6031
+ /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [
6032
+ badge && /* @__PURE__ */ jsx(
6033
+ Badge,
6034
+ {
6035
+ variant: badge.tone === "bad" ? "destructive" : "outline",
6036
+ className: badge.tone === "ok" ? "border-emerald-500/40 bg-emerald-500/10 text-emerald-700 dark:text-emerald-400" : badge.tone === "muted" ? "text-muted-foreground" : void 0,
6037
+ children: badge.text
6038
+ }
6039
+ ),
6040
+ /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: onConfigure, children: "Configurar" })
6041
+ ] })
6042
+ ] }) });
6043
+ }
6044
+ var fmtDateTime = (v) => v ? new Date(v).toLocaleString("pt-BR") : null;
6045
+ function UnitCertificateWidget({ item }) {
6046
+ const { row, loading, save } = useEmitterSettings(item?.legalEntityId);
6047
+ const [open, setOpen] = React5.useState(false);
6048
+ if (!item?.id) return null;
6049
+ if (!item.legalEntityId) return /* @__PURE__ */ jsx(NeedsLegalEntity, { item, what: "O certificado digital" });
6050
+ if (loading || !row) return /* @__PURE__ */ jsx(LoadingCard, {});
6051
+ const expires = row.certificate_expires_at ? new Date(row.certificate_expires_at) : null;
6052
+ const expired = expires ? expires.getTime() < Date.now() : false;
6053
+ const hasCertificate = !!row.certificate_uploaded_at;
6054
+ if (!open) {
6055
+ return /* @__PURE__ */ jsx(
6056
+ SummaryCard,
6057
+ {
6058
+ title: "Certificado digital e DANFes",
6059
+ lines: [
6060
+ { label: "Certificado A1", value: hasCertificate ? [row.certificate_cn, expires ? `v\xE1lido at\xE9 ${expires.toLocaleDateString("pt-BR")}` : null].filter(Boolean).join(" \xB7 ") || "Enviado" : "Nenhum certificado enviado" },
6061
+ { label: "Receber DANFes", value: row.dfe_sync_enabled ? `Ligado \xB7 \xFAltima sincroniza\xE7\xE3o ${fmtDateTime(row.dfe_last_sync_at) ?? "nunca"}` : "Desligado" }
6062
+ ],
6063
+ badge: hasCertificate ? { text: expired ? "Vencido" : "Ativo", tone: expired ? "bad" : "ok" } : { text: "Sem certificado", tone: "muted" },
6064
+ onConfigure: () => setOpen(true)
6065
+ }
6066
+ );
6067
+ }
6068
+ return /* @__PURE__ */ jsxs(Card, { children: [
6069
+ /* @__PURE__ */ jsxs(CardHeader, { className: "flex-row items-start justify-between gap-3 space-y-0", children: [
6070
+ /* @__PURE__ */ jsx(CardTitle, { className: "text-base", children: "Certificado digital e DANFes" }),
6071
+ /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: () => setOpen(false), children: "Fechar" })
6072
+ ] }),
6073
+ /* @__PURE__ */ jsxs(CardContent, { className: "space-y-5", children: [
6074
+ /* @__PURE__ */ jsx(
6075
+ ToggleRow,
6076
+ {
6077
+ title: "Receber DANFes automaticamente",
6078
+ description: "Busca por hora as NF-e emitidas contra o CNPJ desta empresa.",
6079
+ checked: !!row.dfe_sync_enabled,
6080
+ onChange: (v) => void save(item.tenantId, { dfe_sync_enabled: v }).then((ok) => ok && toast.success(v ? "Recep\xE7\xE3o ligada" : "Recep\xE7\xE3o desligada"))
6081
+ }
6082
+ ),
6083
+ /* @__PURE__ */ jsxs("div", { className: "rounded-md border bg-muted/40 p-3 text-sm", children: [
6084
+ /* @__PURE__ */ jsxs("p", { className: "mb-1.5 flex items-center gap-1.5 font-medium", children: [
6085
+ /* @__PURE__ */ jsx(Info, { className: "h-4 w-4" }),
6086
+ " Como funciona"
6087
+ ] }),
6088
+ /* @__PURE__ */ jsxs("ul", { className: "ml-5 list-disc space-y-1 text-muted-foreground", children: [
6089
+ /* @__PURE__ */ jsxs("li", { children: [
6090
+ /* @__PURE__ */ jsx("strong", { className: "text-foreground", children: "O que faz:" }),
6091
+ " consulta a SEFAZ e lista as NF-e emitidas contra o CNPJ, para voc\xEA aceitar ou rejeitar e gerar entrada de estoque ou conta a pagar."
6092
+ ] }),
6093
+ /* @__PURE__ */ jsxs("li", { children: [
6094
+ /* @__PURE__ */ jsx("strong", { className: "text-foreground", children: "Requer certificado digital A1" }),
6095
+ " (.pfx) v\xE1lido desta empresa. O arquivo vai direto ao servi\xE7o fiscal e n\xE3o fica armazenado no nosso sistema."
6096
+ ] }),
6097
+ /* @__PURE__ */ jsxs("li", { children: [
6098
+ /* @__PURE__ */ jsx("strong", { className: "text-foreground", children: "N\xE3o faz manifesta\xE7\xE3o na SEFAZ" }),
6099
+ " (ci\xEAncia, confirma\xE7\xE3o, desconhecimento) nem emite NF-e."
6100
+ ] })
6101
+ ] })
6102
+ ] }),
6103
+ /* @__PURE__ */ jsxs("div", { className: "rounded-md border p-4 space-y-4", children: [
6104
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-3", children: [
6105
+ /* @__PURE__ */ jsxs("div", { children: [
6106
+ /* @__PURE__ */ jsxs("p", { className: "flex items-center gap-1.5 text-sm font-medium", children: [
6107
+ /* @__PURE__ */ jsx(ShieldCheck, { className: "h-4 w-4" }),
6108
+ " Certificado digital A1"
6109
+ ] }),
6110
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: hasCertificate ? [row.certificate_cn, expires ? `v\xE1lido at\xE9 ${expires.toLocaleDateString("pt-BR")}` : null].filter(Boolean).join(" \xB7 ") : "Nenhum certificado enviado" })
6111
+ ] }),
6112
+ hasCertificate && /* @__PURE__ */ jsx(Badge, { variant: expired ? "destructive" : "secondary", children: expired ? "Vencido" : "Ativo" })
6113
+ ] }),
6114
+ /* @__PURE__ */ jsx("div", { className: "fz-container", children: /* @__PURE__ */ jsxs("div", { className: "fz-fields fz-fields-2 gap-4", children: [
6115
+ /* @__PURE__ */ jsx(Field2, { label: "Arquivo (.pfx)", children: /* @__PURE__ */ jsx(Input, { type: "file", accept: ".pfx,.p12", disabled: true }) }),
6116
+ /* @__PURE__ */ jsx(Field2, { label: "Senha do certificado", children: /* @__PURE__ */ jsx(Input, { type: "password", disabled: true }) })
6117
+ ] }) }),
6118
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "O envio do certificado ainda n\xE3o est\xE1 dispon\xEDvel nesta vers\xE3o: falta a fun\xE7\xE3o no servidor que repassa o arquivo ao PlugNotas sem guard\xE1-lo." })
6119
+ ] }),
6120
+ /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
6121
+ "\xDAltima sincroniza\xE7\xE3o: ",
6122
+ fmtDateTime(row.dfe_last_sync_at) ?? "nunca"
6123
+ ] })
6124
+ ] })
6125
+ ] });
6126
+ }
6127
+ var ISS_TAXATION = [
6128
+ { value: "6", label: "Tribut\xE1vel no munic\xEDpio" },
6129
+ { value: "7", label: "Tribut\xE1vel fora do munic\xEDpio" },
6130
+ { value: "1", label: "Isento" },
6131
+ { value: "2", label: "Imune" }
6132
+ ];
6133
+ var SIMPLES_REGIME = [
6134
+ { value: "auto", label: "Autom\xE1tico (1 \u2014 tudo pelo Simples; excesso \u2192 3)" },
6135
+ { value: "1", label: "1 \u2014 Federais e ISS pelo Simples (DAS)" },
6136
+ { value: "2", label: "2 \u2014 Federais pelo Simples; ISS pela NFS-e (lei municipal)" },
6137
+ { value: "3", label: "3 \u2014 Federais e ISS fora do Simples (sublimite)" }
6138
+ ];
6139
+ function EnvSelect({ value, onChange, disabled }) {
6140
+ return /* @__PURE__ */ jsxs(Select, { value: value ? "prod" : "hom", onValueChange: (v) => onChange(v === "prod"), disabled, children: [
6141
+ /* @__PURE__ */ jsx(SelectTrigger, { className: "h-8 w-36", children: /* @__PURE__ */ jsx(SelectValue, {}) }),
6142
+ /* @__PURE__ */ jsxs(SelectContent, { children: [
6143
+ /* @__PURE__ */ jsx(SelectItem, { value: "hom", children: "Homologa\xE7\xE3o" }),
6144
+ /* @__PURE__ */ jsx(SelectItem, { value: "prod", children: "Produ\xE7\xE3o" })
6145
+ ] })
6146
+ ] });
6147
+ }
6148
+ function UnitFiscalWidget({ item }) {
6149
+ const { row, loading, save } = useEmitterSettings(item?.legalEntityId);
6150
+ const [draft, setDraft] = React5.useState({});
6151
+ const [cscToken, setCscToken] = React5.useState("");
6152
+ const [saving, setSaving] = React5.useState(false);
6153
+ const [open, setOpen] = React5.useState(false);
6154
+ React5.useEffect(() => {
6155
+ if (row) setDraft(row);
6156
+ }, [row]);
6157
+ if (!item?.id) return null;
6158
+ if (!item.legalEntityId) return /* @__PURE__ */ jsx(NeedsLegalEntity, { item, what: "A emiss\xE3o fiscal" });
6159
+ if (loading || !row) return /* @__PURE__ */ jsx(LoadingCard, {});
6160
+ const set = (patch) => setDraft((d) => ({ ...d, ...patch }));
6161
+ const generic = draft.nfce_generic_item ?? {};
6162
+ const setGeneric = (patch) => set({ nfce_generic_item: { ...generic, ...patch } });
6163
+ const isSimples = (item.taxRegime ?? "").startsWith("simples");
6164
+ const checks = [
6165
+ { ok: !!row.certificate_uploaded_at && !(row.certificate_expires_at && Date.parse(row.certificate_expires_at) < Date.now()), label: "Certificado A1 v\xE1lido" },
6166
+ // Only what the switched-on documents need: NFS-e off does not block a restaurant that issues NFC-e.
6167
+ ...draft.nfse_enabled ? [
6168
+ { ok: !!draft.nfse_service_code_national, label: "C\xF3digo de tributa\xE7\xE3o do servi\xE7o" },
6169
+ { ok: draft.nfse_iss_rate != null && draft.nfse_iss_rate !== "", label: "Al\xEDquota de ISS" }
6170
+ ] : [],
6171
+ ...draft.nfce_enabled ? [
6172
+ { ok: !!item.stateRegistration && !!draft.nfce_csc_id && (!!row.nfce_csc_token_set || !!cscToken), label: "IE + CSC (NFC-e)" }
6173
+ ] : [],
6174
+ { ok: !!row.provider_synced_at, label: "Empresa registrada para emitir" }
6175
+ ];
6176
+ const ready = (draft.nfse_enabled || draft.nfce_enabled) && checks.every((c) => c.ok);
6177
+ const onSave = async () => {
6178
+ setSaving(true);
6179
+ const {
6180
+ id: _id,
6181
+ legal_entity_id: _le,
6182
+ nfce_csc_token_set: _t,
6183
+ certificate_cn: _cn,
6184
+ certificate_expires_at: _e,
6185
+ certificate_uploaded_at: _u,
6186
+ dfe_last_sync_at: _d,
6187
+ provider_synced_at: _p,
6188
+ ...patch
6189
+ } = draft;
6190
+ const issRate = patch.nfse_iss_rate === "" || patch.nfse_iss_rate == null ? null : Number(String(patch.nfse_iss_rate).replace(",", "."));
6191
+ const ok = await save(item.tenantId, {
6192
+ ...patch,
6193
+ nfse_iss_rate: Number.isFinite(issRate) ? issRate : null,
6194
+ nfse_iss_taxation: patch.nfse_iss_taxation ? Number(patch.nfse_iss_taxation) : null,
6195
+ nfse_simples_regime: patch.nfse_simples_regime && patch.nfse_simples_regime !== "auto" ? Number(patch.nfse_simples_regime) : null,
6196
+ nfce_series: patch.nfce_series ? Number(patch.nfce_series) : null,
6197
+ ...cscToken ? { nfce_csc_token: cscToken } : {}
6198
+ });
6199
+ setSaving(false);
6200
+ if (ok) {
6201
+ setCscToken("");
6202
+ setOpen(false);
6203
+ toast.success("Configura\xE7\xE3o fiscal salva");
6204
+ }
6205
+ };
6206
+ if (!open) {
6207
+ const env = (prod) => prod ? "Produ\xE7\xE3o" : "Homologa\xE7\xE3o";
6208
+ return /* @__PURE__ */ jsx(
6209
+ SummaryCard,
6210
+ {
6211
+ title: "Emiss\xE3o fiscal (NFS-e / NFC-e)",
6212
+ lines: [
6213
+ { label: "NFS-e", value: row.nfse_enabled ? `Ligada \xB7 ${env(!!row.nfse_production)}` : "Desligada" },
6214
+ { label: "NFC-e", value: row.nfce_enabled ? `Ligada \xB7 ${env(!!row.nfce_production)}${row.nfce_series ? ` \xB7 s\xE9rie ${row.nfce_series}` : ""}` : "Desligada" }
6215
+ ],
6216
+ badge: ready ? { text: "Pronto para emitir", tone: "ok" } : { text: "Configura\xE7\xE3o incompleta", tone: "muted" },
6217
+ onConfigure: () => setOpen(true)
6218
+ }
6219
+ );
6220
+ }
6221
+ return /* @__PURE__ */ jsxs(Card, { children: [
6222
+ /* @__PURE__ */ jsxs(CardHeader, { className: "flex-row items-start justify-between gap-3 space-y-0", children: [
6223
+ /* @__PURE__ */ jsxs("div", { children: [
6224
+ /* @__PURE__ */ jsx(CardTitle, { className: "text-base", children: "Emiss\xE3o fiscal (NFS-e / NFC-e)" }),
6225
+ /* @__PURE__ */ jsxs(CardDescription, { children: [
6226
+ item.legalName ?? "Pessoa jur\xEDdica",
6227
+ item.cnpj ? ` \xB7 CNPJ ${maskCpfCnpj(item.cnpj)}` : ""
6228
+ ] })
6229
+ ] }),
6230
+ /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [
6231
+ /* @__PURE__ */ jsx(Badge, { variant: ready ? "default" : "secondary", children: ready ? "Pronto para emitir" : "Configura\xE7\xE3o incompleta" }),
6232
+ /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: () => {
6233
+ setDraft(row);
6234
+ setCscToken("");
6235
+ setOpen(false);
6236
+ }, children: "Cancelar" })
6237
+ ] })
6238
+ ] }),
6239
+ /* @__PURE__ */ jsxs(CardContent, { className: "space-y-5", children: [
6240
+ /* @__PURE__ */ jsxs("section", { className: "space-y-4 rounded-md border p-4", children: [
6241
+ /* @__PURE__ */ jsx(
6242
+ ToggleRow,
6243
+ {
6244
+ title: "NFS-e \u2014 Nota de Servi\xE7o",
6245
+ description: "Padr\xE3o nacional (RJ, Vit\xF3ria e demais aderentes)",
6246
+ checked: !!draft.nfse_enabled,
6247
+ onChange: (v) => set({ nfse_enabled: v }),
6248
+ children: /* @__PURE__ */ jsx(EnvSelect, { value: !!draft.nfse_production, onChange: (v) => set({ nfse_production: v }), disabled: !draft.nfse_enabled })
6249
+ }
6250
+ ),
6251
+ draft.nfse_enabled && /* @__PURE__ */ jsxs("div", { className: "fz-container space-y-4", children: [
6252
+ /* @__PURE__ */ jsxs("div", { className: "fz-fields fz-fields-2 gap-4", children: [
6253
+ /* @__PURE__ */ jsx(Field2, { label: "C\xF3d. tributa\xE7\xE3o nacional", children: /* @__PURE__ */ jsx(Input, { value: draft.nfse_service_code_national ?? "", placeholder: "ex.: 06.02.01", onChange: (e) => set({ nfse_service_code_national: e.target.value }) }) }),
6254
+ /* @__PURE__ */ jsx(Field2, { label: "C\xF3d. tributa\xE7\xE3o municipal", children: /* @__PURE__ */ jsx(Input, { value: draft.nfse_service_code_municipal ?? "", placeholder: "ex.: 001", onChange: (e) => set({ nfse_service_code_municipal: e.target.value }) }) }),
6255
+ /* @__PURE__ */ jsx(Field2, { label: "C\xF3digo NBS", children: /* @__PURE__ */ jsx(Input, { value: draft.nfse_nbs_code ?? "", placeholder: "ex.: 1.2602.90.00", onChange: (e) => set({ nfse_nbs_code: e.target.value }) }) }),
6256
+ /* @__PURE__ */ jsx(Field2, { label: "CNAE do servi\xE7o", children: /* @__PURE__ */ jsx(Input, { value: draft.nfse_cnae ?? "", placeholder: "ex.: 9602-5/02", onChange: (e) => set({ nfse_cnae: e.target.value }) }) }),
6257
+ /* @__PURE__ */ jsx(Field2, { label: "Al\xEDquota ISS (%)", children: /* @__PURE__ */ jsx(Input, { inputMode: "decimal", value: draft.nfse_iss_rate ?? "", placeholder: "ex.: 5", onChange: (e) => set({ nfse_iss_rate: e.target.value }) }) }),
6258
+ /* @__PURE__ */ jsx(Field2, { label: "Tributa\xE7\xE3o ISS", children: /* @__PURE__ */ jsxs(Select, { value: draft.nfse_iss_taxation != null ? String(draft.nfse_iss_taxation) : void 0, onValueChange: (v) => set({ nfse_iss_taxation: v }), children: [
6259
+ /* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "Selecione" }) }),
6260
+ /* @__PURE__ */ jsx(SelectContent, { children: ISS_TAXATION.map((o) => /* @__PURE__ */ jsx(SelectItem, { value: o.value, children: o.label }, o.value)) })
6261
+ ] }) }),
6262
+ /* @__PURE__ */ jsx(Field2, { label: "IBS/CBS \u2014 CST", children: /* @__PURE__ */ jsx(Input, { value: draft.nfse_ibscbs_cst ?? "", placeholder: "ex.: 000", onChange: (e) => set({ nfse_ibscbs_cst: e.target.value }) }) }),
6263
+ /* @__PURE__ */ jsx(Field2, { label: "IBS/CBS \u2014 Classifica\xE7\xE3o", children: /* @__PURE__ */ jsx(Input, { value: draft.nfse_ibscbs_classification ?? "", placeholder: "ex.: 000001", onChange: (e) => set({ nfse_ibscbs_classification: e.target.value }) }) }),
6264
+ isSimples && /* @__PURE__ */ jsx(Field2, { label: "Apura\xE7\xE3o dos tributos no Simples", children: /* @__PURE__ */ jsxs(Select, { value: draft.nfse_simples_regime != null ? String(draft.nfse_simples_regime) : "auto", onValueChange: (v) => set({ nfse_simples_regime: v }), children: [
6265
+ /* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, {}) }),
6266
+ /* @__PURE__ */ jsx(SelectContent, { children: SIMPLES_REGIME.map((o) => /* @__PURE__ */ jsx(SelectItem, { value: o.value, children: o.label }, o.value)) })
6267
+ ] }) })
6268
+ ] }),
6269
+ /* @__PURE__ */ jsx(ToggleRow, { title: "Emitir pelo valor l\xEDquido", checked: !!draft.nfse_net_value, onChange: (v) => set({ nfse_net_value: v }) }),
6270
+ /* @__PURE__ */ jsx(Field2, { label: "Informa\xE7\xF5es complementares da nota", children: /* @__PURE__ */ jsx(Input, { value: draft.nfse_additional_info ?? "", placeholder: "ex.: Totais Aproximados dos Tributos cfe. Lei n\xBA 12.741/2012\u2026", onChange: (e) => set({ nfse_additional_info: e.target.value }) }) })
6271
+ ] })
6272
+ ] }),
6273
+ /* @__PURE__ */ jsxs("section", { className: "space-y-4 rounded-md border p-4", children: [
6274
+ /* @__PURE__ */ jsx(
6275
+ ToggleRow,
6276
+ {
6277
+ title: "NFC-e \u2014 Cupom do Consumidor",
6278
+ description: "Produtos / comanda",
6279
+ checked: !!draft.nfce_enabled,
6280
+ onChange: (v) => set({ nfce_enabled: v }),
6281
+ children: /* @__PURE__ */ jsx(EnvSelect, { value: !!draft.nfce_production, onChange: (v) => set({ nfce_production: v }), disabled: !draft.nfce_enabled })
6282
+ }
6283
+ ),
6284
+ draft.nfce_enabled && /* @__PURE__ */ jsxs("div", { className: "fz-container space-y-4", children: [
6285
+ /* @__PURE__ */ jsxs("div", { className: "fz-fields fz-fields-2 gap-4", children: [
6286
+ /* @__PURE__ */ jsx(Field2, { label: "S\xE9rie", children: /* @__PURE__ */ jsx(Input, { inputMode: "numeric", value: draft.nfce_series ?? "", onChange: (e) => set({ nfce_series: e.target.value.replace(/\D/g, "") }) }) }),
6287
+ /* @__PURE__ */ jsx(Field2, { label: "CSC \u2014 ID", children: /* @__PURE__ */ jsx(Input, { value: draft.nfce_csc_id ?? "", onChange: (e) => set({ nfce_csc_id: e.target.value }) }) })
6288
+ ] }),
6289
+ /* @__PURE__ */ jsx(Field2, { label: "CSC \u2014 Token", hint: "O CSC (id num\xE9rico + token) \xE9 gerado no portal da SEFAZ do estado \u2014 n\xE3o \xE9 o login de nenhum sistema.", children: /* @__PURE__ */ jsx(Input, { type: "password", value: cscToken, placeholder: row.nfce_csc_token_set ? "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022 (configurado \u2014 digite para trocar)" : "", onChange: (e) => setCscToken(e.target.value) }) }),
6290
+ /* @__PURE__ */ jsxs("div", { className: "fz-fields fz-fields-2 gap-4", children: [
6291
+ /* @__PURE__ */ jsx(Field2, { label: "Item gen\xE9rico \u2014 NCM", children: /* @__PURE__ */ jsx(Input, { value: generic.ncm ?? "", onChange: (e) => setGeneric({ ncm: e.target.value }) }) }),
6292
+ /* @__PURE__ */ jsx(Field2, { label: "Item gen\xE9rico \u2014 CFOP", children: /* @__PURE__ */ jsx(Input, { value: generic.cfop ?? "", onChange: (e) => setGeneric({ cfop: e.target.value }) }) }),
6293
+ /* @__PURE__ */ jsx(Field2, { label: "Item gen\xE9rico \u2014 CSOSN/CST", children: /* @__PURE__ */ jsx(Input, { value: generic.csosn ?? "", onChange: (e) => setGeneric({ csosn: e.target.value }) }) })
6294
+ ] })
6295
+ ] })
6296
+ ] }),
6297
+ /* @__PURE__ */ jsx("ul", { className: "flex flex-wrap gap-2", children: checks.map((c) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(Badge, { variant: "outline", className: c.ok ? "gap-1 border-emerald-500/40 text-emerald-700 dark:text-emerald-400" : "gap-1 text-muted-foreground", children: [
6298
+ c.ok ? /* @__PURE__ */ jsx(Check, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(X, { className: "h-3 w-3" }),
6299
+ c.label
6300
+ ] }) }, c.label)) }),
6301
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-end gap-3", children: [
6302
+ /* @__PURE__ */ jsx("p", { className: "mr-auto text-xs text-muted-foreground", children: "Registrar a empresa no emissor ainda n\xE3o est\xE1 dispon\xEDvel nesta vers\xE3o \u2014 a configura\xE7\xE3o fica salva para quando estiver." }),
6303
+ /* @__PURE__ */ jsx(Button, { onClick: () => void onSave(), disabled: saving, children: saving ? "Salvando\u2026" : "Salvar configura\xE7\xE3o" })
6304
+ ] })
6305
+ ] })
6306
+ ] });
6307
+ }
4809
6308
 
4810
6309
  // src/lib/financial-provider.ts
4811
6310
  function fiscalReader(provider) {
@@ -4813,13 +6312,13 @@ function fiscalReader(provider) {
4813
6312
  return provider.getNotaById.bind(provider);
4814
6313
  }
4815
6314
  async function committed(read, id) {
4816
- const document = await read(id);
4817
- if (!document) throw new Error("Fiscal document was not found after the server operation.");
4818
- return document;
6315
+ const document2 = await read(id);
6316
+ if (!document2) throw new Error("Fiscal document was not found after the server operation.");
6317
+ return document2;
4819
6318
  }
4820
- function resultError(result, document, fallback) {
6319
+ function resultError(result, document2, fallback) {
4821
6320
  if (result.status !== "error") return;
4822
- throw new Error(result.message?.trim() || document.errorMessage?.trim() || fallback);
6321
+ throw new Error(result.message?.trim() || document2.errorMessage?.trim() || fallback);
4823
6322
  }
4824
6323
  function cancellationPending(metadata) {
4825
6324
  const provider = metadata?.plugnotas;
@@ -4841,11 +6340,11 @@ function createPlugNotasFinancialProvider(options) {
4841
6340
  const { base, gateway } = options;
4842
6341
  const issueNota = async (id) => {
4843
6342
  const read = fiscalReader(base);
4844
- const document = await read(id);
4845
- if (!document) throw new Error("Fiscal document not found.");
4846
- if (document.model !== "nfce") throw new Error("PlugNotas currently authorizes NFC-e only.");
4847
- if (!document.orderId?.trim()) throw new Error("PlugNotas NFC-e must be created from an order.");
4848
- if (!["draft", "error"].includes(document.status)) return document;
6343
+ const document2 = await read(id);
6344
+ if (!document2) throw new Error("Fiscal document not found.");
6345
+ if (document2.model !== "nfce") throw new Error("PlugNotas currently authorizes NFC-e only.");
6346
+ if (!document2.orderId?.trim()) throw new Error("PlugNotas NFC-e must be created from an order.");
6347
+ if (!["draft", "error"].includes(document2.status)) return document2;
4849
6348
  const result = await gateway.issue({ notaId: id, idempotencyKey: `nfce:${id}` });
4850
6349
  const saved = await committed(read, id);
4851
6350
  resultError(result, saved, "PlugNotas could not issue this NFC-e.");
@@ -4853,9 +6352,9 @@ function createPlugNotasFinancialProvider(options) {
4853
6352
  };
4854
6353
  const refreshNota = async (id) => {
4855
6354
  const read = fiscalReader(base);
4856
- const document = await read(id);
4857
- if (!document) throw new Error("Fiscal document not found.");
4858
- if (!["processing", "error"].includes(document.status) && !cancellationPending(document.metadata)) return document;
6355
+ const document2 = await read(id);
6356
+ if (!document2) throw new Error("Fiscal document not found.");
6357
+ if (!["processing", "error"].includes(document2.status) && !cancellationPending(document2.metadata)) return document2;
4859
6358
  const result = await gateway.refresh({ notaId: id });
4860
6359
  const saved = await committed(read, id);
4861
6360
  resultError(result, saved, "PlugNotas could not refresh this NFC-e.");
@@ -4863,10 +6362,10 @@ function createPlugNotasFinancialProvider(options) {
4863
6362
  };
4864
6363
  const cancelNota = async (id, reason) => {
4865
6364
  const read = fiscalReader(base);
4866
- const document = await read(id);
4867
- if (!document) throw new Error("Fiscal document not found.");
4868
- if (document.status !== "issued") throw new Error("Only an issued NFC-e can be cancelled.");
4869
- if (cancellationPending(document.metadata)) throw new Error("NFC-e cancellation is already in progress.");
6365
+ const document2 = await read(id);
6366
+ if (!document2) throw new Error("Fiscal document not found.");
6367
+ if (document2.status !== "issued") throw new Error("Only an issued NFC-e can be cancelled.");
6368
+ if (cancellationPending(document2.metadata)) throw new Error("NFC-e cancellation is already in progress.");
4870
6369
  const normalized = reason?.trim() ?? "";
4871
6370
  if (normalized.length < 15 || normalized.length > 255) {
4872
6371
  throw new Error("Cancellation reason must contain between 15 and 255 characters.");
@@ -4878,12 +6377,12 @@ function createPlugNotasFinancialProvider(options) {
4878
6377
  };
4879
6378
  const getNotaArtifactUrl = async (id, kind) => {
4880
6379
  const read = fiscalReader(base);
4881
- const document = await read(id);
4882
- if (!document) throw new Error("Fiscal document not found.");
4883
- if (!["issued", "cancelled"].includes(document.status)) {
6380
+ const document2 = await read(id);
6381
+ if (!document2) throw new Error("Fiscal document not found.");
6382
+ if (!["issued", "cancelled"].includes(document2.status)) {
4884
6383
  throw new Error("Fiscal artifacts are available only after authorization.");
4885
6384
  }
4886
- if (kind === "cancellation_xml" && document.status !== "cancelled") {
6385
+ if (kind === "cancellation_xml" && document2.status !== "cancelled") {
4887
6386
  throw new Error("Cancellation XML is available only after cancellation.");
4888
6387
  }
4889
6388
  return artifactUrl((await gateway.artifact({ notaId: id, kind })).url);
@@ -4955,10 +6454,14 @@ function ReturnedNotesView({ onBack } = {}) {
4955
6454
  );
4956
6455
  }
4957
6456
  function inboundNotesInventoryView(options = {}) {
6457
+ registerDocumentOpener(NFE_OPENER);
4958
6458
  return {
4959
6459
  id: "inbound-notes",
4960
6460
  label: "Notas Fiscais",
4961
6461
  icon: "FileText",
6462
+ // O nome do V1 para a fila de notas de compra. «Entrada» era o nome genérico
6463
+ // que o Estoque dá a qualquer tela emprestada, e dizia menos do que a tela faz.
6464
+ defaultChildLabel: "Manifesto de Notas",
4962
6465
  render: () => /* @__PURE__ */ jsx(InboundNotesPage, { ...options }),
4963
6466
  // A ordem é a do V1: Grupos Fiscais vem antes das duas de emissão, que é
4964
6467
  // onde ele é usado.
@@ -4976,6 +6479,60 @@ function inboundNotesInventoryView(options = {}) {
4976
6479
  ]
4977
6480
  };
4978
6481
  }
6482
+ function InboundNoteDocumentModal({ movement, onClose }) {
6483
+ const [noteId, setNoteId] = React5.useState(null);
6484
+ const [erro, setErro] = React5.useState(null);
6485
+ React5.useEffect(() => {
6486
+ let vivo = true;
6487
+ const numero = nfeNumberOf(movement);
6488
+ const busca = movement.operationId ? findInboxNoteByOperation(movement.operationId).then((id) => id ?? (numero ? findInboxNoteByNumber(numero) : null)) : numero ? findInboxNoteByNumber(numero) : Promise.resolve(null);
6489
+ busca.then((id) => {
6490
+ if (!vivo) return;
6491
+ if (id) setNoteId(id);
6492
+ else setErro("N\xE3o achei a nota desta entrada nesta conta.");
6493
+ }).catch((e) => {
6494
+ if (vivo) setErro(e instanceof Error ? e.message : String(e));
6495
+ });
6496
+ return () => {
6497
+ vivo = false;
6498
+ };
6499
+ }, [movement.operationId, movement.documentNumber, movement.reason]);
6500
+ if (erro) {
6501
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
6502
+ /* @__PURE__ */ jsx(ModalHeader, { className: "shrink-0 border-b px-6 py-3 pr-14 text-left", children: /* @__PURE__ */ jsx(ModalTitle, { children: "Nota de entrada" }) }),
6503
+ /* @__PURE__ */ jsx(ModalBody, { className: "px-6 py-8", children: /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: erro }) })
6504
+ ] });
6505
+ }
6506
+ if (!noteId) {
6507
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
6508
+ /* @__PURE__ */ jsx(ModalHeader, { className: "shrink-0 border-b px-6 py-3 pr-14 text-left", children: /* @__PURE__ */ jsx(ModalTitle, { children: "Nota de entrada" }) }),
6509
+ /* @__PURE__ */ jsxs(ModalBody, { className: "space-y-3 px-6 py-4", children: [
6510
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-48" }),
6511
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-24 w-full" }),
6512
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-24 w-full" })
6513
+ ] })
6514
+ ] });
6515
+ }
6516
+ return /* @__PURE__ */ jsx(NoteReconcileView, { chrome: "modal", noteId, onBack: onClose });
6517
+ }
6518
+ function inboundNoteDocumentModal(movement, close) {
6519
+ if (movement.documentType !== "nfe" || !movement.operationId) return void 0;
6520
+ return /* @__PURE__ */ jsx(InboundNoteDocumentModal, { movement, onClose: close });
6521
+ }
6522
+ var nfeNumberOf = (ref) => (ref.documentNumber ?? ref.reason ?? "").match(/NF-e\s*([0-9]{2,})/i)?.[1] ?? null;
6523
+ var NFE_OPENER = {
6524
+ documentType: "nfe",
6525
+ // O razão é APPEND-ONLY — um gatilho recusa `UPDATE`, e com razão — então as
6526
+ // 3.088 linhas que a importação do V1 trouxe sem `document_type` nunca vão
6527
+ // ganhá-lo. O que elas têm é «Entrada via DANFE NF-e 10592» no motivo, frase
6528
+ // que este mesmo sistema escreveu. Reconhecê-la é ler o que se escreveu.
6529
+ claims: (ref) => Boolean(nfeNumberOf(ref)),
6530
+ // Const de módulo e não arrow inline: o registro compara por identidade para
6531
+ // distinguir recarga quente de dois plugins disputando o mesmo tipo, e uma
6532
+ // função nova a cada chamada pareceria disputa.
6533
+ render: (ref, close) => /* @__PURE__ */ jsx(InboundNoteDocumentModal, { movement: ref, onClose: close })
6534
+ };
6535
+ registerDocumentOpener(NFE_OPENER);
4979
6536
 
4980
6537
  // src/index.ts
4981
6538
  function createPlugNotasPlugin(options = {}) {
@@ -5013,6 +6570,22 @@ function createPlugNotasPlugin(options = {}) {
5013
6570
  // O grupo fiscal é CADASTRO, e por isso entra como registry e não como
5014
6571
  // rota própria: ele aparece onde os outros cadastros do produto aparecem.
5015
6572
  registries: fiscalRegistries,
6573
+ widgets: [
6574
+ {
6575
+ id: "fiscal-br-unit-certificate",
6576
+ zone: UNIT_DETAIL_ZONES.certificate,
6577
+ component: UnitCertificateWidget,
6578
+ order: 0,
6579
+ permission: { feature: "financial", action: "read" }
6580
+ },
6581
+ {
6582
+ id: "fiscal-br-unit-fiscal",
6583
+ zone: UNIT_DETAIL_ZONES.fiscal,
6584
+ component: UnitFiscalWidget,
6585
+ order: 0,
6586
+ permission: { feature: "financial", action: "read" }
6587
+ }
6588
+ ],
5016
6589
  connectors: [createPlugNotasConnector(options)],
5017
6590
  functions: [{
5018
6591
  slug: "plugnotas-bridge",
@@ -5084,6 +6657,6 @@ function createPlugNotasPlugin(options = {}) {
5084
6657
  });
5085
6658
  }
5086
6659
 
5087
- export { InboundNotesPage, InboundNotesView, IssuedNotesView, ManifestView, NfeParseError, NoteReconcileView, PLUGNOTAS_CONNECTOR_ID, PLUGNOTAS_FIELDS, PLUGNOTAS_SETTINGS, ReturnedNotesView, STOCK_BEARING_ENTRY_TYPES, TaxGroupsView, acceptNote, createPlugNotasConnector, createPlugNotasFinancialProvider, createPlugNotasPlugin, getInboxNote, inboundNotesInventoryView, isStockBearing, isValidCnpj, listChartAccounts, listInboxNotes, listMeasurementUnits, listStockLocations, listSupplierMap, markPlugNotasEmitterReady, onlyDigits, parseNfeXml, paymentLabel, paymentTypeFromCode, rejectNote, saveNoteDecision, searchProducts, stageNoteFromXml, toStagePayload };
6660
+ export { InboundNoteDocumentModal, InboundNotesPage, InboundNotesView, IssuedNotesView, ManifestView, NfeParseError, NoteReconcileView, PLUGNOTAS_CONNECTOR_ID, PLUGNOTAS_FIELDS, PLUGNOTAS_SETTINGS, ReturnedNotesView, STOCK_BEARING_ENTRY_TYPES, TaxGroupsView, acceptNote, createPlugNotasConnector, createPlugNotasFinancialProvider, createPlugNotasPlugin, findInboxNoteByNumber, findInboxNoteByOperation, getInboxNote, getInboxNoteXml, inboundNoteDocumentModal, inboundNotesInventoryView, isStockBearing, isValidCnpj, listChartAccounts, listInboxNotes, listMeasurementUnits, listProductsByIds, listStockLocations, listSupplierMap, markPlugNotasEmitterReady, onlyDigits, parseNfeXml, paymentLabel, paymentTypeFromCode, rejectNote, saveNoteDecision, searchProducts, stageNoteFromXml, suggestNoteItems, toStagePayload };
5088
6661
  //# sourceMappingURL=index.js.map
5089
6662
  //# sourceMappingURL=index.js.map