@fayz-ai/plugin-fiscal-br 0.3.5 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,9 +1,10 @@
1
- import { recordRailFields, RECORD_GROUP, activeFacet, connectionSettings, connectionStatus, definePlugin, createConnectionStore, settingsStatus, getSupabaseClientOptional } from '@fayz-ai/core';
2
- import { useState, useRef, useCallback, useEffect, useMemo } from 'react';
3
- import { Inbox, Loader2, UploadCloud, Trash2, FileDown, Gift, RefreshCw, Check, Plus, AlertTriangle, X, ShieldCheck, FileText, ExternalLink } from 'lucide-react';
4
- import { PageHeaderActions, Button, Card, ListFilterBar, Skeleton, SubpageHeader, EmBreve, SearchSelect, Input, DatePicker, Hint, 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 { jsxs, jsx, Fragment } from 'react/jsx-runtime';
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
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
7
8
 
8
9
  // src/index.ts
9
10
  var PLUGNOTAS_CONNECTOR_ID = "plugnotas";
@@ -2831,6 +2832,821 @@ END $function$;
2831
2832
  REVOKE ALL ON FUNCTION public.fiscalbr_accept_inbox_document(uuid, uuid) FROM PUBLIC;
2832
2833
  GRANT EXECUTE ON FUNCTION public.fiscalbr_accept_inbox_document(uuid, uuid) TO authenticated;
2833
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
+ `;
2834
3650
  var MIGRATIONS = [
2835
3651
  { id: "000_baseline", sql: MIGRATION_000_BASELINE },
2836
3652
  { id: "001_preserve_terminal_results_and_recover_leases", sql: MIGRATION_001_PRESERVE_TERMINAL_RESULTS_AND_RECOVER_LEASES },
@@ -2842,7 +3658,14 @@ var MIGRATIONS = [
2842
3658
  { id: "007_a_rls_do_documento_passa_a_ser_legivel", sql: MIGRATION_007_A_RLS_DO_DOCUMENTO_PASSA_A_SER_LEGIVEL },
2843
3659
  { id: "008_a_nota_recebida_guarda_a_decisao_de_entrada", sql: MIGRATION_008_A_NOTA_RECEBIDA_GUARDA_A_DECISAO_DE_ENTRADA },
2844
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 },
2845
- { 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 }
2846
3669
  ];
2847
3670
  var taxGroupEntity = {
2848
3671
  name: "Grupo fiscal",
@@ -3174,6 +3997,10 @@ function parseNfeXml(xml) {
3174
3997
  }
3175
3998
  function toStagePayload(note, options = {}) {
3176
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 } : {},
3177
4004
  access_key: note.accessKey,
3178
4005
  nfe_number: note.nfeNumber,
3179
4006
  nfe_series: note.nfeSeries,
@@ -3248,7 +4075,7 @@ function sb() {
3248
4075
  var NOTE_COLUMNS = `
3249
4076
  id, access_key, nfe_number, nfe_series, issue_date, received_at, status,
3250
4077
  manifestation_status, manifested_at, manifestation_attempts,
3251
- nature_operation, supplier_name, supplier_cnpj, supplier_party_id,
4078
+ nature_operation, supplier_name, supplier_cnpj, supplier_party_id, xml_content,
3252
4079
  total_value, total_products, total_discount, total_freight,
3253
4080
  invoice_id, inventory_operation_id, stocked_at, reviewed_at,
3254
4081
  default_chart_account_id, default_location_id, unit_id, document_id, metadata,
@@ -3285,12 +4112,16 @@ function rowToNote(row) {
3285
4112
  operationId: row.inventory_operation_id ?? null,
3286
4113
  stockedAt: row.stocked_at ?? null,
3287
4114
  documentId: row.document_id ?? null,
4115
+ hasXml: Boolean(row.xml_content),
3288
4116
  unitId: row.unit_id ?? null
3289
4117
  };
3290
4118
  }
3291
4119
  async function listInboxNotes(options = {}) {
3292
- const { filter = "all", search, limit = 200 } = options;
4120
+ const { filter = "all", search, unitId, from, to, limit = 200 } = options;
3293
4121
  let query = sb().from("plg_fiscalbr_inbox_documents").select(NOTE_COLUMNS);
4122
+ if (unitId) query = query.eq("unit_id", unitId);
4123
+ if (from) query = query.or(`issue_date.gte.${from},issue_date.is.null`);
4124
+ if (to) query = query.or(`issue_date.lte.${to},issue_date.is.null`);
3294
4125
  if (filter === "stocked") query = query.not("stocked_at", "is", null);
3295
4126
  if (filter === "pending") query = query.is("stocked_at", null);
3296
4127
  if (filter === "rejected") query = query.eq("status", "rejected");
@@ -3308,10 +4139,11 @@ async function listInboxNotes(options = {}) {
3308
4139
  var ITEM_COLUMNS = `
3309
4140
  id, item_number, supplier_product_code, ean_barcode, description, ncm, cest, cfop,
3310
4141
  unit_commercial, quantity_commercial, unit_price_commercial, total_price, discount,
4142
+ unit_taxable, quantity_taxable,
3311
4143
  icms_cst, icms_csosn, icms_base, icms_rate, icms_value, pis_value, cofins_value, ipi_value,
3312
4144
  product_id, entry_type, chart_account_id, stock_unit_id, stock_quantity,
3313
4145
  conversion_factor, cost_mode, unit_cost_new, reconciled, stock_movement_id,
3314
- shortage_quantity, shortage_reason, shortage_note
4146
+ shortage_quantity, shortage_reason, shortage_note, suggested_by, suggestion_confidence
3315
4147
  `;
3316
4148
  function rowToItem(row) {
3317
4149
  const quantity = Number(row.quantity_commercial ?? 0);
@@ -3329,6 +4161,8 @@ function rowToItem(row) {
3329
4161
  quantity,
3330
4162
  unitPrice: Number(row.unit_price_commercial ?? 0),
3331
4163
  total: Number(row.total_price ?? 0),
4164
+ unitTaxable: row.unit_taxable ?? null,
4165
+ quantityTaxable: row.quantity_taxable == null ? null : Number(row.quantity_taxable),
3332
4166
  discount: row.discount == null ? null : Number(row.discount),
3333
4167
  icmsCst: row.icms_cst ?? null,
3334
4168
  icmsCsosn: row.icms_csosn ?? null,
@@ -3345,6 +4179,8 @@ function rowToItem(row) {
3345
4179
  costMode: row.cost_mode ?? "novo",
3346
4180
  unitCostNew: row.unit_cost_new == null ? null : Number(row.unit_cost_new),
3347
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),
3348
4184
  stockMovementId: row.stock_movement_id ?? null,
3349
4185
  shortageQuantity: row.shortage_quantity == null ? null : Number(row.shortage_quantity),
3350
4186
  shortageReason: row.shortage_reason ?? null,
@@ -3390,7 +4226,7 @@ async function getInboxNote(id) {
3390
4226
  async function stageNoteFromXml(xml, options = {}) {
3391
4227
  const parsed = parseNfeXml(xml);
3392
4228
  const { data, error } = await sb().rpc("fiscalbr_stage_inbox_document", {
3393
- p_note: toStagePayload(parsed, { unitId: options.unitId ?? null })
4229
+ p_note: toStagePayload(parsed, { unitId: options.unitId ?? null, xml })
3394
4230
  });
3395
4231
  if (error) throw error;
3396
4232
  const res = data ?? {};
@@ -3415,6 +4251,8 @@ async function saveNoteDecision(input) {
3415
4251
  stock_unit_id: i.stockUnitId ?? null,
3416
4252
  conversion_factor: i.conversionFactor ?? 1,
3417
4253
  cost_mode: i.costMode ?? "novo",
4254
+ suggested_by: i.suggestedBy ?? null,
4255
+ suggestion_confidence: i.suggestionConfidence ?? null,
3418
4256
  shortage_quantity: i.shortageQuantity ?? null,
3419
4257
  shortage_reason: i.shortageReason ?? null,
3420
4258
  shortage_note: i.shortageNote ?? null
@@ -3478,24 +4316,77 @@ async function searchProducts(term, limit = 20) {
3478
4316
  kind: p.kind ?? null
3479
4317
  }));
3480
4318
  }
3481
- async function listMeasurementUnits() {
3482
- const { data, error } = await sb().from("plg_inventory_measurement_units").select("id, name, abbreviation, is_base").eq("is_active", true).order("name");
4319
+ async function getInboxNoteXml(id) {
4320
+ const { data, error } = await sb().from("plg_fiscalbr_inbox_documents").select("xml_content").eq("id", id).maybeSingle();
3483
4321
  if (error) throw error;
3484
- return (data ?? []).map((u) => ({
3485
- id: String(u.id),
3486
- name: u.name ?? "",
3487
- abbreviation: u.abbreviation ?? "",
3488
- isBase: Boolean(u.is_base)
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
+ }
4370
+ async function listMeasurementUnits() {
4371
+ const { data, error } = await sb().from("plg_inventory_measurement_units").select("id, name, abbreviation, is_base").eq("is_active", true).order("name");
4372
+ if (error) throw error;
4373
+ return (data ?? []).map((u) => ({
4374
+ id: String(u.id),
4375
+ name: u.name ?? "",
4376
+ abbreviation: u.abbreviation ?? "",
4377
+ isBase: Boolean(u.is_base)
3489
4378
  }));
3490
4379
  }
3491
4380
  async function listChartAccounts() {
3492
- const { data, error } = await sb().from("plg_financial_chart_of_accounts").select("id, code, name, kind, is_active").order("code");
4381
+ const { data, error } = await sb().from("plg_financial_chart_of_accounts").select("id, code, name, account_type, node_type, parent_id, is_active").order("code");
3493
4382
  if (error) throw error;
3494
- return (data ?? []).filter((a) => a.is_active !== false).map((a) => ({
4383
+ const rows = (data ?? []).filter((a) => a.is_active !== false);
4384
+ const parents = new Set(rows.map((a) => a.parent_id).filter(Boolean).map(String));
4385
+ return rows.filter((a) => a.node_type !== "group" && !parents.has(String(a.id))).map((a) => ({
3495
4386
  id: String(a.id),
3496
4387
  code: a.code ?? "",
3497
4388
  name: a.name ?? "",
3498
- kind: a.kind ?? null
4389
+ kind: a.account_type ?? null
3499
4390
  }));
3500
4391
  }
3501
4392
  async function listStockLocations() {
@@ -3521,22 +4412,53 @@ var cnpj = (digits2) => {
3521
4412
  if (d.length !== 14) return digits2 ?? "\u2014";
3522
4413
  return `${d.slice(0, 2)}.${d.slice(2, 5)}.${d.slice(5, 8)}/${d.slice(8, 12)}-${d.slice(12)}`;
3523
4414
  };
4415
+ var CAMPO = "h-9 w-full rounded-md border border-input bg-background px-2 text-sm";
4416
+ var STATE_OPTIONS = [
4417
+ { value: "all", label: "Todos" },
4418
+ { value: "pending", label: "Estoque pendente" },
4419
+ { value: "stocked", label: "Estocado" },
4420
+ { value: "to_manifest", label: "A manifestar" },
4421
+ { value: "rejected", label: "Recusada" }
4422
+ ];
4423
+ var isoDay = (d) => d.toISOString().slice(0, 10);
3524
4424
  function InboundNotesView({ onOpenNote, onOpenManifest }) {
3525
4425
  const [notes, setNotes] = useState([]);
3526
4426
  const [loading, setLoading] = useState(true);
3527
- const [filter, setFilter] = useState("all");
4427
+ const [search, setSearch] = useState("");
3528
4428
  const [busy, setBusy] = useState(false);
3529
4429
  const [report, setReport] = useState(null);
3530
4430
  const [dragging, setDragging] = useState(false);
3531
4431
  const fileInput = useRef(null);
4432
+ const unidades = useUnitFilter({ scope: "list" });
4433
+ const { draft, applied, patch, setDraft, dirty, apply, reset } = useFilterDraft({
4434
+ filter: "all",
4435
+ unitId: unidades.value ?? "",
4436
+ // Sem período por padrão: o V1 abre em «Todos os registros», e uma janela
4437
+ // de 30 dias esconderia justamente a nota velha que nunca foi estocada.
4438
+ from: "",
4439
+ to: ""
4440
+ });
4441
+ const ancora = unidades.value ?? "";
4442
+ const ancoraAnterior = useRef(ancora);
4443
+ useEffect(() => {
4444
+ if (ancoraAnterior.current === ancora) return;
4445
+ ancoraAnterior.current = ancora;
4446
+ setDraft((atual) => ({ ...atual, unitId: ancora }));
4447
+ apply();
4448
+ }, [ancora, setDraft, apply]);
3532
4449
  const reload = useCallback(async () => {
3533
4450
  setLoading(true);
3534
4451
  try {
3535
- setNotes(await listInboxNotes({ filter }));
4452
+ setNotes(await listInboxNotes({
4453
+ filter: applied.filter,
4454
+ unitId: applied.unitId || void 0,
4455
+ from: applied.from || void 0,
4456
+ to: applied.to || void 0
4457
+ }));
3536
4458
  } finally {
3537
4459
  setLoading(false);
3538
4460
  }
3539
- }, [filter]);
4461
+ }, [applied]);
3540
4462
  useEffect(() => {
3541
4463
  void reload();
3542
4464
  }, [reload]);
@@ -3570,24 +4492,117 @@ function InboundNotesView({ onOpenNote, onOpenManifest }) {
3570
4492
  await rejectNote(note.id, "Recusada na lista de entrada");
3571
4493
  await reload();
3572
4494
  }, [reload]);
3573
- const filters = useMemo(() => [{
3574
- id: "estado",
3575
- label: "Estado",
3576
- allLabel: "Todos",
3577
- value: filter === "all" ? void 0 : filter,
3578
- onChange: (value) => setFilter(value ?? "all"),
3579
- options: [
3580
- { value: "pending", label: "Estoque pendente" },
3581
- { value: "stocked", label: "Estocado" },
3582
- { value: "to_manifest", label: "A manifestar" },
3583
- { value: "rejected", label: "Recusada" }
3584
- ]
3585
- }], [filter]);
4495
+ const activeCount = (applied.filter === "all" ? 0 : 1) + (applied.unitId ? 1 : 0) + (applied.from || applied.to ? 1 : 0);
4496
+ const limpar = useCallback(() => {
4497
+ reset({ filter: "all", unitId: ancora, from: "", to: "" });
4498
+ }, [reset, ancora]);
4499
+ const setFrom = (from) => patch({ from, to: draft.to && from > draft.to ? from : draft.to });
4500
+ const setTo = (to) => patch({ to, from: draft.from && to < draft.from ? to : draft.from });
4501
+ const query = search.trim().toLowerCase();
4502
+ const shown = useMemo(() => {
4503
+ if (!query) return notes;
4504
+ const digits2 = query.replace(/\D/g, "");
4505
+ return notes.filter((note) => {
4506
+ if (`${note.number} ${note.series} ${note.supplierName}`.toLowerCase().includes(query)) return true;
4507
+ if (digits2.length < 2) return false;
4508
+ if ((note.supplierCnpj ?? "").replace(/\D/g, "").includes(digits2)) return true;
4509
+ if ((note.accessKey ?? "").includes(digits2)) return true;
4510
+ return day(note.issueDate).replace(/\D/g, "").includes(digits2);
4511
+ });
4512
+ }, [notes, query]);
4513
+ const columns = useMemo(() => [
4514
+ {
4515
+ id: "issueDate",
4516
+ header: "Emiss\xE3o",
4517
+ accessorFn: (note) => note.issueDate ?? "",
4518
+ cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "tabular-nums text-muted-foreground", children: day(row.original.issueDate) })
4519
+ },
4520
+ {
4521
+ id: "number",
4522
+ header: "N\xBA",
4523
+ accessorFn: (note) => Number(note.number) || note.number,
4524
+ cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "tabular-nums font-medium", children: row.original.number })
4525
+ },
4526
+ {
4527
+ id: "supplier",
4528
+ header: "Fornecedor",
4529
+ accessorFn: (note) => note.supplierName,
4530
+ cell: ({ row }) => /* @__PURE__ */ jsxs("div", { children: [
4531
+ /* @__PURE__ */ jsx("p", { className: "font-medium leading-tight", children: row.original.supplierName || "\u2014" }),
4532
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground tabular-nums", children: cnpj(row.original.supplierCnpj) })
4533
+ ] })
4534
+ },
4535
+ {
4536
+ id: "items",
4537
+ header: () => /* @__PURE__ */ jsx("span", { className: "block text-right", children: "Itens" }),
4538
+ accessorFn: (note) => note.itemCount,
4539
+ cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "block text-right tabular-nums text-muted-foreground", children: row.original.itemCount })
4540
+ },
4541
+ {
4542
+ id: "reconciled",
4543
+ header: () => /* @__PURE__ */ jsx("span", { className: "block text-center", children: "Concilia\xE7\xE3o" }),
4544
+ accessorFn: (note) => note.itemCount > 0 ? note.reconciledCount / note.itemCount : -1,
4545
+ cell: ({ row }) => /* @__PURE__ */ jsx("div", { className: "text-center", children: /* @__PURE__ */ jsx(ReconcileBadge, { done: row.original.reconciledCount, total: row.original.itemCount }) })
4546
+ },
4547
+ {
4548
+ id: "total",
4549
+ header: () => /* @__PURE__ */ jsx("span", { className: "block text-right", children: "Valor" }),
4550
+ accessorFn: (note) => note.total,
4551
+ cell: ({ row }) => {
4552
+ const note = row.original;
4553
+ return /* @__PURE__ */ jsxs("div", { className: "text-right tabular-nums", children: [
4554
+ money(note.total),
4555
+ note.instalmentCount > 0 && Math.abs(note.instalmentTotal - note.total) > 0.01 && /* @__PURE__ */ jsxs("p", { className: "text-[11px] text-warning-soft-foreground", children: [
4556
+ "parcelas: ",
4557
+ money(note.instalmentTotal)
4558
+ ] })
4559
+ ] });
4560
+ }
4561
+ },
4562
+ {
4563
+ id: "stock",
4564
+ header: () => /* @__PURE__ */ jsx("span", { className: "block text-center", children: "Estoque" }),
4565
+ accessorFn: (note) => note.stockedAt ? 2 : note.status === "rejected" ? 0 : 1,
4566
+ cell: ({ row }) => /* @__PURE__ */ jsx("div", { className: "text-center", children: /* @__PURE__ */ jsx(StockBadge, { stockedAt: row.original.stockedAt, status: row.original.status }) })
4567
+ },
4568
+ {
4569
+ id: "actions",
4570
+ header: () => /* @__PURE__ */ jsx("span", { className: "block text-right", children: "A\xE7\xF5es" }),
4571
+ enableSorting: false,
4572
+ cell: ({ row }) => {
4573
+ const note = row.original;
4574
+ return /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-1", children: [
4575
+ /* @__PURE__ */ jsx(Button, { size: "sm", variant: note.stockedAt ? "ghost" : "outline", onClick: () => onOpenNote(note.id), children: note.stockedAt ? "Ver" : "Conciliar" }),
4576
+ !note.stockedAt && !note.invoiceId && /* @__PURE__ */ jsx(
4577
+ Button,
4578
+ {
4579
+ size: "sm",
4580
+ variant: "ghost",
4581
+ "aria-label": `Recusar a nota ${note.number}`,
4582
+ onClick: () => {
4583
+ void remove(note);
4584
+ },
4585
+ children: /* @__PURE__ */ jsx(Trash2, { className: "h-4 w-4" })
4586
+ }
4587
+ )
4588
+ ] });
4589
+ }
4590
+ }
4591
+ ], [onOpenNote, remove]);
4592
+ const emptyMessage = query || activeCount > 0 ? /* @__PURE__ */ jsxs("div", { children: [
4593
+ /* @__PURE__ */ jsx(Inbox, { className: "mx-auto h-8 w-8 text-muted-foreground", "aria-hidden": true }),
4594
+ /* @__PURE__ */ jsx("p", { className: "mt-2 text-sm font-medium", children: "Nenhuma nota encontrada" }),
4595
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "Tente outro n\xFAmero, fornecedor ou CNPJ." })
4596
+ ] }) : /* @__PURE__ */ jsxs("div", { children: [
4597
+ /* @__PURE__ */ jsx(Inbox, { className: "mx-auto h-8 w-8 text-muted-foreground", "aria-hidden": true }),
4598
+ /* @__PURE__ */ jsx("p", { className: "mt-2 text-sm font-medium", children: "Nenhuma nota de entrada" }),
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." })
4600
+ ] });
3586
4601
  return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
3587
4602
  /* @__PURE__ */ jsxs(PageHeaderActions, { children: [
3588
4603
  onOpenManifest && /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", onClick: onOpenManifest, children: [
3589
4604
  /* @__PURE__ */ jsx(Inbox, { className: "h-4 w-4 mr-1.5" }),
3590
- "Manifesto de Notas"
4605
+ "Fila da SEFAZ"
3591
4606
  ] }),
3592
4607
  /* @__PURE__ */ jsxs(Button, { size: "sm", onClick: () => fileInput.current?.click(), disabled: busy, children: [
3593
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" }),
@@ -3645,56 +4660,64 @@ function InboundNotesView({ onOpenNote, onOpenManifest }) {
3645
4660
  ] }, r.file)),
3646
4661
  /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: () => setReport(null), children: "Fechar" })
3647
4662
  ] }),
3648
- /* @__PURE__ */ jsx(ListFilterBar, { filters, onClear: () => setFilter("all"), activeCount: filter === "all" ? 0 : 1 }),
3649
- loading ? /* @__PURE__ */ jsx("div", { className: "space-y-2", children: [0, 1, 2].map((i) => /* @__PURE__ */ jsx(Skeleton, { className: "h-14 w-full" }, i)) }) : notes.length === 0 ? /* @__PURE__ */ jsxs(Card, { className: "p-10 text-center", children: [
3650
- /* @__PURE__ */ jsx(Inbox, { className: "mx-auto h-8 w-8 text-muted-foreground", "aria-hidden": true }),
3651
- /* @__PURE__ */ jsx("p", { className: "mt-2 text-sm font-medium", children: "Nenhuma nota de entrada" }),
3652
- /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "As notas chegam da SEFAZ pelo Manifesto, ou voc\xEA arrasta o XML aqui." })
3653
- ] }) : /* @__PURE__ */ jsx(Card, { className: "overflow-hidden", children: /* @__PURE__ */ jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxs("table", { className: "w-full text-sm", children: [
3654
- /* @__PURE__ */ jsx("thead", { className: "border-b bg-muted/40 text-xs text-muted-foreground", children: /* @__PURE__ */ jsxs("tr", { children: [
3655
- /* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-left font-medium", children: "Emiss\xE3o" }),
3656
- /* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-left font-medium", children: "N\xBA" }),
3657
- /* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-left font-medium", children: "Fornecedor" }),
3658
- /* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-right font-medium", children: "Itens" }),
3659
- /* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-center font-medium", children: "Concilia\xE7\xE3o" }),
3660
- /* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-right font-medium", children: "Valor" }),
3661
- /* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-center font-medium", children: "Estoque" }),
3662
- /* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-right font-medium", children: "A\xE7\xF5es" })
3663
- ] }) }),
3664
- /* @__PURE__ */ jsx("tbody", { children: notes.map((note) => /* @__PURE__ */ jsxs("tr", { className: "border-b last:border-0 hover:bg-muted/30", children: [
3665
- /* @__PURE__ */ jsx("td", { className: "px-3 py-2 tabular-nums text-muted-foreground", children: day(note.issueDate) }),
3666
- /* @__PURE__ */ jsx("td", { className: "px-3 py-2 tabular-nums font-medium", children: note.number }),
3667
- /* @__PURE__ */ jsxs("td", { className: "px-3 py-2", children: [
3668
- /* @__PURE__ */ jsx("p", { className: "font-medium leading-tight", children: note.supplierName || "\u2014" }),
3669
- /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground tabular-nums", children: cnpj(note.supplierCnpj) })
3670
- ] }),
3671
- /* @__PURE__ */ jsx("td", { className: "px-3 py-2 text-right tabular-nums text-muted-foreground", children: note.itemCount }),
3672
- /* @__PURE__ */ jsx("td", { className: "px-3 py-2 text-center", children: /* @__PURE__ */ jsx(ReconcileBadge, { done: note.reconciledCount, total: note.itemCount }) }),
3673
- /* @__PURE__ */ jsxs("td", { className: "px-3 py-2 text-right tabular-nums", children: [
3674
- money(note.total),
3675
- note.instalmentCount > 0 && Math.abs(note.instalmentTotal - note.total) > 0.01 && /* @__PURE__ */ jsxs("p", { className: "text-[11px] text-warning-soft-foreground", children: [
3676
- "parcelas: ",
3677
- money(note.instalmentTotal)
3678
- ] })
3679
- ] }),
3680
- /* @__PURE__ */ jsx("td", { className: "px-3 py-2 text-center", children: /* @__PURE__ */ jsx(StockBadge, { stockedAt: note.stockedAt, status: note.status }) }),
3681
- /* @__PURE__ */ jsx("td", { className: "px-3 py-2", children: /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-1", children: [
3682
- /* @__PURE__ */ jsx(Button, { size: "sm", variant: note.stockedAt ? "ghost" : "outline", onClick: () => onOpenNote(note.id), children: note.stockedAt ? "Ver" : "Conciliar" }),
3683
- !note.stockedAt && !note.invoiceId && /* @__PURE__ */ jsx(
3684
- Button,
4663
+ /* @__PURE__ */ jsxs(
4664
+ FilterPanel,
4665
+ {
4666
+ title: "Filtros",
4667
+ dirty,
4668
+ onApply: apply,
4669
+ applyLabel: "Buscar",
4670
+ pendingTitle: "Filtros n\xE3o aplicados",
4671
+ pendingHint: "A lista ainda mostra o recorte anterior.",
4672
+ onClear: activeCount > 0 ? limpar : void 0,
4673
+ clearLabel: "Limpar",
4674
+ busy: loading,
4675
+ children: [
4676
+ unidades.visible && /* @__PURE__ */ jsx(FilterPanelField, { label: unidades.label, children: /* @__PURE__ */ jsxs(
4677
+ Select,
3685
4678
  {
3686
- size: "sm",
3687
- variant: "ghost",
3688
- "aria-label": `Recusar a nota ${note.number}`,
3689
- onClick: () => {
3690
- void remove(note);
3691
- },
3692
- children: /* @__PURE__ */ jsx(Trash2, { className: "h-4 w-4" })
4679
+ value: draft.unitId || "__all__",
4680
+ onValueChange: (v) => patch({ unitId: v === "__all__" ? "" : v }),
4681
+ children: [
4682
+ /* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, {}) }),
4683
+ /* @__PURE__ */ jsxs(SelectContent, { children: [
4684
+ /* @__PURE__ */ jsx(SelectItem, { value: "__all__", children: unidades.allLabel }),
4685
+ unidades.options.map((u) => /* @__PURE__ */ jsx(SelectItem, { value: u.id, children: u.name }, u.id))
4686
+ ] })
4687
+ ]
3693
4688
  }
3694
- )
3695
- ] }) })
3696
- ] }, note.id)) })
3697
- ] }) }) })
4689
+ ) }),
4690
+ /* @__PURE__ */ jsx(FilterPanelField, { label: "Estado", children: /* @__PURE__ */ jsxs(Select, { value: draft.filter, onValueChange: (v) => patch({ filter: v }), children: [
4691
+ /* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, {}) }),
4692
+ /* @__PURE__ */ jsx(SelectContent, { children: STATE_OPTIONS.map((o) => /* @__PURE__ */ jsx(SelectItem, { value: o.value, children: o.label }, o.value)) })
4693
+ ] }) }),
4694
+ /* @__PURE__ */ jsx(FilterPanelField, { label: "Emiss\xE3o de", children: /* @__PURE__ */ jsx("input", { type: "date", value: draft.from, max: isoDay(/* @__PURE__ */ new Date()), onChange: (e) => setFrom(e.target.value), className: CAMPO }) }),
4695
+ /* @__PURE__ */ jsx(FilterPanelField, { label: "Emiss\xE3o at\xE9", children: /* @__PURE__ */ jsx("input", { type: "date", value: draft.to, max: isoDay(/* @__PURE__ */ new Date()), onChange: (e) => setTo(e.target.value), className: CAMPO }) })
4696
+ ]
4697
+ }
4698
+ ),
4699
+ /* @__PURE__ */ jsxs(Card, { className: "overflow-hidden", children: [
4700
+ /* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center gap-2 border-b border-border px-3 py-2", children: /* @__PURE__ */ jsx("div", { className: "min-w-[10rem] flex-1", children: /* @__PURE__ */ jsx(
4701
+ SearchField,
4702
+ {
4703
+ value: search,
4704
+ onChange: setSearch,
4705
+ placeholder: "Buscar por n\xFAmero, fornecedor, CNPJ\u2026",
4706
+ "data-testid": "inbound-notes-search"
4707
+ }
4708
+ ) }) }),
4709
+ /* @__PURE__ */ jsx(
4710
+ DataTable,
4711
+ {
4712
+ variant: "bare",
4713
+ columns,
4714
+ data: loading ? [] : shown,
4715
+ loading,
4716
+ getRowId: (note) => note.id,
4717
+ emptyMessage
4718
+ }
4719
+ )
4720
+ ] })
3698
4721
  ] });
3699
4722
  }
3700
4723
  function ReconcileBadge({ done, total }) {
@@ -3724,6 +4747,24 @@ function StockBadge({ stockedAt, status }) {
3724
4747
  // src/types.ts
3725
4748
  var STOCK_BEARING_ENTRY_TYPES = ["insumo", "revenda", "patrimonio"];
3726
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
+ }
3727
4768
  var money2 = (v) => new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL" }).format(v || 0);
3728
4769
  var qty = (v) => new Intl.NumberFormat("pt-BR", { maximumFractionDigits: 4 }).format(v || 0);
3729
4770
  var day2 = (iso) => iso ? iso.slice(0, 10).split("-").reverse().join("/") : "\u2014";
@@ -3747,9 +4788,15 @@ var draftOf = (item) => ({
3747
4788
  stockUnitId: item.stockUnitId,
3748
4789
  conversionFactor: String(item.conversionFactor ?? 1),
3749
4790
  costMode: item.costMode === "media" ? "media" : "novo",
3750
- 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
3751
4795
  });
3752
- 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" });
3753
4800
  const [note, setNote] = useState(null);
3754
4801
  const [loading, setLoading] = useState(true);
3755
4802
  const [drafts, setDrafts] = useState({});
@@ -3761,23 +4808,48 @@ function NoteReconcileView({ noteId, onBack, onOpenInvoice }) {
3761
4808
  const [locations, setLocations] = useState([]);
3762
4809
  const [saving, setSaving] = useState(false);
3763
4810
  const [accepting, setAccepting] = useState(false);
4811
+ const [openIds, setOpenIds] = useState([]);
4812
+ const [checked, setChecked] = useState({});
4813
+ const [registryWarning, setRegistryWarning] = useState(null);
3764
4814
  const [error, setError] = useState(null);
3765
4815
  const [accepted, setAccepted] = useState(null);
4816
+ const [baixandoXml, setBaixandoXml] = useState(false);
4817
+ const [suggesting, setSuggesting] = useState(false);
4818
+ const [suggestionNote, setSuggestionNote] = useState(null);
3766
4819
  const load = useCallback(async () => {
3767
4820
  setLoading(true);
3768
4821
  try {
3769
4822
  const detail = await getInboxNote(noteId);
3770
4823
  setNote(detail);
4824
+ const failed = [];
3771
4825
  const [u, a, l] = await Promise.all([
3772
- listMeasurementUnits().catch(() => []),
3773
- listChartAccounts().catch(() => []),
3774
- listStockLocations().catch(() => [])
4826
+ listMeasurementUnits().catch(() => {
4827
+ failed.push("unidades");
4828
+ return [];
4829
+ }),
4830
+ listChartAccounts().catch(() => {
4831
+ failed.push("plano de contas");
4832
+ return [];
4833
+ }),
4834
+ listStockLocations().catch(() => {
4835
+ failed.push("locais de estoque");
4836
+ return [];
4837
+ })
3775
4838
  ]);
3776
4839
  setUnits(u);
3777
4840
  setAccounts(a);
3778
4841
  setLocations(l);
4842
+ setRegistryWarning(failed.length ? `N\xE3o consegui carregar ${failed.join(", ")}. Estes seletores v\xE3o aparecer vazios.` : null);
3779
4843
  if (detail) {
3780
- setDrafts(Object.fromEntries(detail.items.map((i) => [i.id, draftOf(i)])));
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])));
4851
+ const pending = detail.items.find((i) => !i.reconciled);
4852
+ setOpenIds(pending ? [pending.id] : []);
3781
4853
  setInstalments(detail.instalments.map((d) => ({
3782
4854
  index: d.index,
3783
4855
  number: d.number,
@@ -3799,7 +4871,14 @@ function NoteReconcileView({ noteId, onBack, onOpenInvoice }) {
3799
4871
  useEffect(() => {
3800
4872
  void load();
3801
4873
  }, [load]);
3802
- 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
+ }));
3803
4882
  const accountOptions = useMemo(
3804
4883
  () => accounts.map((a) => ({ id: a.id, label: `${a.code} \xB7 ${a.name}` })),
3805
4884
  [accounts]
@@ -3812,10 +4891,10 @@ function NoteReconcileView({ noteId, onBack, onOpenInvoice }) {
3812
4891
  () => locations.map((l) => ({ id: l.id, label: l.name })),
3813
4892
  [locations]
3814
4893
  );
3815
- const decisions = useMemo(() => {
4894
+ const buildDecisions = useCallback((rascunhos) => {
3816
4895
  if (!note) return [];
3817
4896
  return note.items.map((item) => {
3818
- const d = drafts[item.id] ?? draftOf(item);
4897
+ const d = rascunhos[item.id] ?? draftOf(item);
3819
4898
  return {
3820
4899
  id: item.id,
3821
4900
  productId: isStockBearing(d.entryType) ? d.productId : null,
@@ -3825,15 +4904,131 @@ function NoteReconcileView({ noteId, onBack, onOpenInvoice }) {
3825
4904
  stockUnitId: d.stockUnitId,
3826
4905
  conversionFactor: Number(d.conversionFactor.replace(",", ".")) || 1,
3827
4906
  costMode: d.costMode,
4907
+ suggestedBy: d.suggestedBy,
4908
+ suggestionConfidence: d.suggestionConfidence,
3828
4909
  shortageQuantity: d.shortageQuantity === "" ? null : Number(d.shortageQuantity.replace(",", "."))
3829
4910
  };
3830
4911
  });
3831
- }, [note, drafts, defaultAccount]);
4912
+ }, [note, defaultAccount]);
4913
+ const decisions = useMemo(() => buildDecisions(drafts), [buildDecisions, drafts]);
3832
4914
  const stockLines = decisions.filter((d) => isStockBearing(d.entryType));
3833
4915
  const missingProduct = stockLines.filter((d) => !d.productId).length;
3834
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;
3835
4919
  const instalmentSum = instalments.reduce((acc, d) => acc + (Number(d.amount) || 0), 0);
3836
4920
  const readOnly = Boolean(note?.stockedAt || note?.invoiceId);
4921
+ const isReady = (item, d) => Boolean(d.entryType) && (!isStockBearing(d.entryType) || Boolean(d.productId)) && !suspectFactor(item, d);
4922
+ const doneCount = note ? note.items.filter((i) => checked[i.id]).length : 0;
4923
+ const toggleItem = (id) => setOpenIds((prev) => prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]);
4924
+ const confirmItem = (id) => {
4925
+ setChecked((prev) => ({ ...prev, [id]: true }));
4926
+ setOpenIds((prev) => {
4927
+ const rest = prev.filter((x) => x !== id);
4928
+ const next = note?.items.find((i) => i.id !== id && !checked[i.id]);
4929
+ return next && !rest.includes(next.id) ? [...rest, next.id] : rest;
4930
+ });
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]);
3837
5032
  const save = async () => {
3838
5033
  if (!note) return;
3839
5034
  setSaving(true);
@@ -3874,30 +5069,89 @@ function NoteReconcileView({ noteId, onBack, onOpenInvoice }) {
3874
5069
  }
3875
5070
  };
3876
5071
  if (loading) {
3877
- return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
3878
- /* @__PURE__ */ jsx(SubpageHeader, { title: "Concilia\xE7\xE3o", onBack, parentLabel: "Entrada de Notas" }),
3879
- /* @__PURE__ */ jsx(Skeleton, { className: "h-24 w-full" }),
3880
- /* @__PURE__ */ jsx(Skeleton, { className: "h-48 w-full" })
3881
- ] });
5072
+ return moldura(/* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
5073
+ cabecalho("Concilia\xE7\xE3o"),
5074
+ /* @__PURE__ */ jsxs(Card, { className: "p-4", children: [
5075
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-32" }),
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: [
5077
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-3 w-20" }),
5078
+ /* @__PURE__ */ jsx(Skeleton, { className: "mt-1.5 h-4 w-36" })
5079
+ ] }, i)) })
5080
+ ] }),
5081
+ /* @__PURE__ */ jsxs(Card, { className: "p-4", children: [
5082
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-36" }),
5083
+ /* @__PURE__ */ jsx("div", { className: "mt-3 grid gap-4 sm:grid-cols-2", children: [0, 1].map((i) => /* @__PURE__ */ jsxs("div", { children: [
5084
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-3 w-28" }),
5085
+ /* @__PURE__ */ jsx(Skeleton, { className: "mt-1.5 h-9 w-full" })
5086
+ ] }, i)) })
5087
+ ] }),
5088
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-24" }),
5089
+ /* @__PURE__ */ jsx("div", { className: "space-y-2", children: [0, 1, 2].map((i) => /* @__PURE__ */ jsxs(Card, { className: "flex items-center gap-3 px-4 py-3", children: [
5090
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-4 rounded-full" }),
5091
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
5092
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-2/3" }),
5093
+ /* @__PURE__ */ jsx(Skeleton, { className: "mt-1.5 h-3 w-1/3" })
5094
+ ] }),
5095
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-20" })
5096
+ ] }, i)) })
5097
+ ] }), "Concilia\xE7\xE3o");
3882
5098
  }
3883
5099
  if (!note) {
3884
- return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
3885
- /* @__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"),
3886
5102
  /* @__PURE__ */ jsx(Card, { className: "p-8 text-center text-sm", children: error ? /* @__PURE__ */ jsxs("span", { className: "text-destructive", children: [
3887
5103
  "N\xE3o consegui carregar a nota: ",
3888
5104
  error
3889
5105
  ] }) : /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "Nota n\xE3o encontrada." }) })
3890
- ] });
5106
+ ] }), "Concilia\xE7\xE3o");
3891
5107
  }
3892
- return /* @__PURE__ */ jsxs("div", { className: "space-y-4 pb-24", children: [
3893
- /* @__PURE__ */ jsx(
3894
- SubpageHeader,
5108
+ const rodape = readOnly ? null : /* @__PURE__ */ jsxs(Fragment, { children: [
5109
+ /* @__PURE__ */ jsxs(
5110
+ Button,
3895
5111
  {
3896
- title: `Concilia\xE7\xE3o \xB7 NF-e ${note.number}`,
3897
- onBack,
3898
- 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
+ ]
3899
5122
  }
3900
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}`),
3901
5155
  accepted && /* @__PURE__ */ jsxs(Card, { className: "border-success bg-success-soft p-4", children: [
3902
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"}.` }),
3903
5157
  accepted.invoiceId && onOpenInvoice && /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", className: "mt-2", onClick: () => onOpenInvoice(accepted.invoiceId), children: "Abrir a conta a pagar" })
@@ -3942,6 +5196,25 @@ function NoteReconcileView({ noteId, onBack, onOpenInvoice }) {
3942
5196
  /* @__PURE__ */ jsx(RefreshCw, { className: "mr-1.5 h-4 w-4" }),
3943
5197
  "Atualizar status"
3944
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
+ ),
3945
5218
  /* @__PURE__ */ jsx(
3946
5219
  EmBreve,
3947
5220
  {
@@ -3954,6 +5227,10 @@ function NoteReconcileView({ noteId, onBack, onOpenInvoice }) {
3954
5227
  ] }),
3955
5228
  /* @__PURE__ */ jsxs(Card, { className: "p-4", children: [
3956
5229
  /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: "Padr\xF5es desta nota" }),
5230
+ registryWarning && /* @__PURE__ */ jsx("div", { className: "mt-2 rounded-md border border-warning bg-warning-soft px-3 py-2", children: /* @__PURE__ */ jsxs("p", { className: "text-xs text-warning-soft-foreground", children: [
5231
+ /* @__PURE__ */ jsx(AlertTriangle, { className: "mr-1 inline h-3.5 w-3.5", "aria-hidden": true }),
5232
+ registryWarning
5233
+ ] }) }),
3957
5234
  /* @__PURE__ */ jsxs("div", { className: "mt-3 grid gap-4 sm:grid-cols-2", children: [
3958
5235
  /* @__PURE__ */ jsxs("label", { className: "block", children: [
3959
5236
  /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Plano de contas padr\xE3o" }),
@@ -3968,6 +5245,7 @@ function NoteReconcileView({ noteId, onBack, onOpenInvoice }) {
3968
5245
  placeholder: "Escolher plano de contas",
3969
5246
  noResultsLabel: "Nenhum plano de contas",
3970
5247
  searchingLabel: "Buscando\u2026",
5248
+ clearLabel: "Limpar",
3971
5249
  disabled: readOnly
3972
5250
  }
3973
5251
  ),
@@ -3986,6 +5264,7 @@ function NoteReconcileView({ noteId, onBack, onOpenInvoice }) {
3986
5264
  placeholder: "Escolher local",
3987
5265
  noResultsLabel: "Nenhum local de estoque",
3988
5266
  searchingLabel: "Buscando\u2026",
5267
+ clearLabel: "Limpar",
3989
5268
  disabled: readOnly
3990
5269
  }
3991
5270
  ),
@@ -3993,14 +5272,51 @@ function NoteReconcileView({ noteId, onBack, onOpenInvoice }) {
3993
5272
  ] })
3994
5273
  ] })
3995
5274
  ] }),
3996
- /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
3997
- /* @__PURE__ */ jsxs("h3", { className: "text-sm font-semibold", children: [
3998
- "Itens ",
3999
- /* @__PURE__ */ jsxs("span", { className: "font-normal text-muted-foreground", children: [
4000
- "(",
5275
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
5276
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [
5277
+ /* @__PURE__ */ jsxs("h3", { className: "text-sm font-semibold", children: [
5278
+ "Itens ",
5279
+ /* @__PURE__ */ jsxs("span", { className: "font-normal text-muted-foreground", children: [
5280
+ "(",
5281
+ note.items.length,
5282
+ ")"
5283
+ ] })
5284
+ ] }),
5285
+ /* @__PURE__ */ jsx("div", { className: "h-1.5 min-w-[6rem] flex-1 overflow-hidden rounded-full bg-muted", children: /* @__PURE__ */ jsx(
5286
+ "div",
5287
+ {
5288
+ className: "h-full rounded-full bg-primary transition-all",
5289
+ style: { width: `${note.items.length ? doneCount / note.items.length * 100 : 0}%` }
5290
+ }
5291
+ ) }),
5292
+ /* @__PURE__ */ jsxs("span", { className: "text-xs tabular-nums text-muted-foreground", children: [
5293
+ doneCount,
5294
+ "/",
4001
5295
  note.items.length,
4002
- ")"
4003
- ] })
5296
+ " conferidos"
5297
+ ] }),
5298
+ /* @__PURE__ */ jsx(
5299
+ Button,
5300
+ {
5301
+ variant: "ghost",
5302
+ size: "sm",
5303
+ onClick: () => setOpenIds(openIds.length ? [] : note.items.map((i) => i.id)),
5304
+ children: openIds.length ? "Recolher todos" : "Abrir todos"
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
+ }
5319
+ )
4004
5320
  ] }),
4005
5321
  note.items.map((item) => {
4006
5322
  const d = drafts[item.id] ?? draftOf(item);
@@ -4009,184 +5325,264 @@ function NoteReconcileView({ noteId, onBack, onOpenInvoice }) {
4009
5325
  const stockQty = item.quantity * factor - shortage;
4010
5326
  const unitCost = factor > 0 ? item.unitPrice / factor : item.unitPrice;
4011
5327
  const bearing = isStockBearing(d.entryType);
4012
- return /* @__PURE__ */ jsx(Card, { className: "p-4", children: /* @__PURE__ */ jsxs("div", { className: "grid gap-4 lg:grid-cols-2", children: [
4013
- /* @__PURE__ */ jsxs("div", { children: [
4014
- /* @__PURE__ */ jsx("p", { className: "text-[11px] font-semibold uppercase tracking-wide text-muted-foreground", children: "Item na nota" }),
4015
- /* @__PURE__ */ jsxs("p", { className: "mt-1 font-medium leading-tight", children: [
4016
- item.itemNumber,
4017
- ". ",
4018
- item.description
4019
- ] }),
4020
- /* @__PURE__ */ jsxs("dl", { className: "mt-2 grid grid-cols-2 gap-x-4 gap-y-1 text-xs sm:grid-cols-4", children: [
4021
- /* @__PURE__ */ jsx(Field, { small: true, label: "Qtd", value: `${qty(item.quantity)} ${item.unit ?? ""}` }),
4022
- /* @__PURE__ */ jsx(Field, { small: true, label: "Vl. unit.", value: money2(item.unitPrice) }),
4023
- /* @__PURE__ */ jsx(Field, { small: true, label: "Total", value: money2(item.total) }),
4024
- /* @__PURE__ */ jsx(Field, { small: true, label: "C\xF3d", value: item.supplierProductCode || "\u2014", mono: true }),
4025
- /* @__PURE__ */ jsx(Field, { small: true, label: "EAN", value: item.eanBarcode ?? "\u2014", mono: true }),
4026
- /* @__PURE__ */ jsx(Field, { small: true, label: "NCM", value: item.ncm ?? "\u2014", mono: true }),
4027
- /* @__PURE__ */ jsx(Field, { small: true, label: "CFOP", value: item.cfop ?? "\u2014", mono: true }),
4028
- /* @__PURE__ */ jsx(Field, { small: true, label: "CST/CSOSN", value: item.icmsCst ?? item.icmsCsosn ?? "\u2014", mono: true })
4029
- ] })
4030
- ] }),
4031
- /* @__PURE__ */ jsxs("div", { className: "space-y-3 lg:border-l lg:pl-4", children: [
4032
- /* @__PURE__ */ jsx("p", { className: "text-[11px] font-semibold uppercase tracking-wide text-muted-foreground", children: "Item no estoque" }),
4033
- /* @__PURE__ */ jsxs("label", { className: "block", children: [
4034
- /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Tipo do registro" }),
4035
- /* @__PURE__ */ jsx("div", { className: "mt-1 flex flex-wrap gap-1", children: ENTRY_TYPES.map((t) => /* @__PURE__ */ jsxs(
4036
- "button",
4037
- {
4038
- type: "button",
4039
- disabled: readOnly,
4040
- onClick: () => patch(item.id, { entryType: t.value }),
4041
- className: `rounded-full border px-2.5 py-1 text-[11px] transition-colors disabled:opacity-60 ${d.entryType === t.value ? "border-primary bg-primary text-primary-foreground" : "border-input hover:bg-muted"}`,
4042
- children: [
4043
- t.label,
4044
- t.hint && /* @__PURE__ */ jsxs("span", { className: "ml-1 opacity-70", children: [
4045
- "\xB7 ",
4046
- t.hint
4047
- ] })
4048
- ]
4049
- },
4050
- t.value
4051
- )) })
4052
- ] }),
4053
- bearing && /* @__PURE__ */ jsxs(Fragment, { children: [
4054
- /* @__PURE__ */ jsxs("label", { className: "block", children: [
4055
- /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Produto vinculado" }),
5328
+ const open = openIds.includes(item.id);
5329
+ const done = Boolean(checked[item.id]);
5330
+ const ready = isReady(item, d);
5331
+ const typeLabel = ENTRY_TYPES.find((t) => t.value === d.entryType)?.label;
5332
+ return /* @__PURE__ */ jsxs(Card, { className: "overflow-hidden", children: [
5333
+ /* @__PURE__ */ jsxs(
5334
+ "button",
5335
+ {
5336
+ type: "button",
5337
+ onClick: () => toggleItem(item.id),
5338
+ "aria-expanded": open,
5339
+ className: "flex w-full items-center gap-3 px-4 py-3 text-left transition-colors hover:bg-muted/30",
5340
+ children: [
4056
5341
  /* @__PURE__ */ jsx(
4057
- SearchSelect,
5342
+ ChevronRight,
4058
5343
  {
4059
- value: d.productId ?? void 0,
4060
- displayValue: d.productLabel || void 0,
4061
- onChange: (id, option) => patch(item.id, { productId: id || null, productLabel: option?.label ?? "" }),
4062
- onSearch: async (term) => {
4063
- const found = await searchProducts(term || item.description.slice(0, 24));
4064
- return found.map((prod) => ({
4065
- id: prod.id,
4066
- label: prod.name,
4067
- subtitle: prod.sku ?? void 0
4068
- }));
4069
- },
4070
- minChars: 0,
4071
- placeholder: "Buscar produto do cat\xE1logo",
4072
- noResultsLabel: "Nenhum produto encontrado",
4073
- searchingLabel: "Buscando\u2026",
4074
- disabled: readOnly
5344
+ className: `h-4 w-4 shrink-0 text-muted-foreground transition-transform ${open ? "rotate-90" : ""}`,
5345
+ "aria-hidden": true
4075
5346
  }
4076
- )
5347
+ ),
5348
+ /* @__PURE__ */ jsx("span", { className: "w-5 shrink-0 text-xs tabular-nums text-muted-foreground", children: item.itemNumber }),
5349
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
5350
+ /* @__PURE__ */ jsx("span", { className: "block truncate text-sm font-medium leading-tight", children: item.description }),
5351
+ /* @__PURE__ */ jsxs("span", { className: "block truncate text-xs text-muted-foreground", children: [
5352
+ qty(item.quantity),
5353
+ " ",
5354
+ item.unit ?? "",
5355
+ " \xB7 ",
5356
+ money2(item.unitPrice),
5357
+ typeLabel ? ` \xB7 ${typeLabel}` : " \xB7 sem tipo"
5358
+ ] })
5359
+ ] }),
5360
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 text-sm tabular-nums", children: money2(item.total) }),
5361
+ d.suggestedBy && !done && /* @__PURE__ */ jsx(AuthorBadge, { draft: d }),
5362
+ /* @__PURE__ */ jsx(ItemState, { done: done && ready, ready })
5363
+ ]
5364
+ }
5365
+ ),
5366
+ open && /* @__PURE__ */ jsxs("div", { className: "grid gap-4 border-t p-4 lg:grid-cols-2", children: [
5367
+ /* @__PURE__ */ jsxs("div", { children: [
5368
+ /* @__PURE__ */ jsx("p", { className: "text-[11px] font-semibold uppercase tracking-wide text-muted-foreground", children: "Item na nota" }),
5369
+ /* @__PURE__ */ jsxs("p", { className: "mt-1 font-medium leading-tight", children: [
5370
+ item.itemNumber,
5371
+ ". ",
5372
+ item.description
4077
5373
  ] }),
4078
- /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-3", children: [
5374
+ /* @__PURE__ */ jsxs("dl", { className: "mt-2 grid grid-cols-2 gap-x-4 gap-y-1 text-xs sm:grid-cols-4", children: [
5375
+ /* @__PURE__ */ jsx(Field, { small: true, label: "Qtd", value: `${qty(item.quantity)} ${item.unit ?? ""}` }),
5376
+ /* @__PURE__ */ jsx(Field, { small: true, label: "Vl. unit.", value: money2(item.unitPrice) }),
5377
+ /* @__PURE__ */ jsx(Field, { small: true, label: "Total", value: money2(item.total) }),
5378
+ /* @__PURE__ */ jsx(Field, { small: true, label: "C\xF3d", value: item.supplierProductCode || "\u2014", mono: true }),
5379
+ /* @__PURE__ */ jsx(Field, { small: true, label: "EAN", value: item.eanBarcode ?? "\u2014", mono: true }),
5380
+ /* @__PURE__ */ jsx(Field, { small: true, label: "NCM", value: item.ncm ?? "\u2014", mono: true }),
5381
+ /* @__PURE__ */ jsx(Field, { small: true, label: "CFOP", value: item.cfop ?? "\u2014", mono: true }),
5382
+ /* @__PURE__ */ jsx(Field, { small: true, label: "CST/CSOSN", value: item.icmsCst ?? item.icmsCsosn ?? "\u2014", mono: true })
5383
+ ] })
5384
+ ] }),
5385
+ /* @__PURE__ */ jsxs("div", { className: "space-y-3 lg:border-l lg:pl-4", children: [
5386
+ /* @__PURE__ */ jsx("p", { className: "text-[11px] font-semibold uppercase tracking-wide text-muted-foreground", children: "Item no estoque" }),
5387
+ /* @__PURE__ */ jsxs("label", { className: "block", children: [
5388
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Tipo do registro" }),
5389
+ /* @__PURE__ */ jsx("div", { className: "mt-1 flex flex-wrap gap-1", children: ENTRY_TYPES.map((t) => /* @__PURE__ */ jsxs(
5390
+ "button",
5391
+ {
5392
+ type: "button",
5393
+ disabled: readOnly,
5394
+ onClick: () => patch(item.id, { entryType: t.value }),
5395
+ className: `rounded-full border px-2.5 py-1 text-[11px] transition-colors disabled:opacity-60 ${d.entryType === t.value ? "border-primary bg-primary text-primary-foreground" : "border-input hover:bg-muted"}`,
5396
+ children: [
5397
+ t.label,
5398
+ t.hint && /* @__PURE__ */ jsxs("span", { className: "ml-1 opacity-70", children: [
5399
+ "\xB7 ",
5400
+ t.hint
5401
+ ] })
5402
+ ]
5403
+ },
5404
+ t.value
5405
+ )) })
5406
+ ] }),
5407
+ bearing && /* @__PURE__ */ jsxs(Fragment, { children: [
4079
5408
  /* @__PURE__ */ jsxs("label", { className: "block", children: [
4080
- /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Fator de convers\xE3o" }),
5409
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Produto vinculado" }),
4081
5410
  /* @__PURE__ */ jsx(
4082
- Input,
5411
+ SearchSelect,
4083
5412
  {
4084
- value: d.conversionFactor,
4085
- onChange: (e) => patch(item.id, { conversionFactor: e.target.value }),
4086
- inputMode: "decimal",
5413
+ value: d.productId ?? void 0,
5414
+ displayValue: d.productLabel || void 0,
5415
+ onChange: (id, option) => patch(item.id, { productId: id || null, productLabel: option?.label ?? "" }),
5416
+ onSearch: async (term) => {
5417
+ const found = await searchProducts(term || item.description.slice(0, 24));
5418
+ return found.map((prod) => ({
5419
+ id: prod.id,
5420
+ label: prod.name,
5421
+ subtitle: prod.sku ?? void 0
5422
+ }));
5423
+ },
5424
+ minChars: 0,
5425
+ placeholder: "Buscar produto do cat\xE1logo",
5426
+ noResultsLabel: "Nenhum produto encontrado",
5427
+ searchingLabel: "Buscando\u2026",
5428
+ clearLabel: "Limpar",
4087
5429
  disabled: readOnly
4088
5430
  }
4089
5431
  )
4090
5432
  ] }),
4091
- /* @__PURE__ */ jsxs("label", { className: "block", children: [
4092
- /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Unidade de estoque" }),
5433
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-3", children: [
5434
+ /* @__PURE__ */ jsxs("label", { className: "block", children: [
5435
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Fator de convers\xE3o" }),
5436
+ /* @__PURE__ */ jsx(
5437
+ Input,
5438
+ {
5439
+ value: d.conversionFactor,
5440
+ onChange: (e) => patch(item.id, { conversionFactor: e.target.value }),
5441
+ inputMode: "decimal",
5442
+ disabled: readOnly
5443
+ }
5444
+ )
5445
+ ] }),
5446
+ /* @__PURE__ */ jsxs("label", { className: "block", children: [
5447
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Unidade de estoque" }),
5448
+ /* @__PURE__ */ jsx(
5449
+ SearchSelect,
5450
+ {
5451
+ onSearch: localSearch(unitOptions),
5452
+ value: d.stockUnitId ?? void 0,
5453
+ displayValue: labelOf(unitOptions, d.stockUnitId),
5454
+ onChange: (id) => patch(item.id, { stockUnitId: id || null }),
5455
+ minChars: 0,
5456
+ placeholder: "Unidade",
5457
+ noResultsLabel: "Nenhuma unidade",
5458
+ searchingLabel: "Buscando\u2026",
5459
+ clearLabel: "Limpar",
5460
+ disabled: readOnly
5461
+ }
5462
+ )
5463
+ ] })
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
+ ] }) }),
5487
+ /* @__PURE__ */ jsxs("div", { className: "rounded-md bg-muted/50 px-3 py-2", children: [
5488
+ /* @__PURE__ */ jsxs("div", { className: "flex items-baseline justify-between text-sm", children: [
5489
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: "Qtd estoque (calc.)" }),
5490
+ /* @__PURE__ */ jsx("span", { className: "font-semibold tabular-nums", children: qty(stockQty) })
5491
+ ] }),
5492
+ /* @__PURE__ */ jsxs("div", { className: "flex items-baseline justify-between text-sm", children: [
5493
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: "Novo custo unit\xE1rio" }),
5494
+ /* @__PURE__ */ jsx("span", { className: "tabular-nums", children: money2(unitCost) })
5495
+ ] })
5496
+ ] }),
5497
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
4093
5498
  /* @__PURE__ */ jsx(
4094
- SearchSelect,
5499
+ EmBreve,
4095
5500
  {
4096
- onSearch: localSearch(unitOptions),
4097
- value: d.stockUnitId ?? void 0,
4098
- displayValue: labelOf(unitOptions, d.stockUnitId),
4099
- onChange: (id) => patch(item.id, { stockUnitId: id || null }),
4100
- minChars: 0,
4101
- placeholder: "Unidade",
4102
- noResultsLabel: "Nenhuma unidade",
4103
- searchingLabel: "Buscando\u2026",
4104
- disabled: readOnly
5501
+ label: "Criar produto daqui",
5502
+ o_que: "Cadastrar o produto a partir desta linha da nota, com nome, c\xF3digo e unidade j\xE1 preenchidos",
5503
+ quantas: "ela criou 33 produtos no V1 no meio de importa\xE7\xF5es"
5504
+ }
5505
+ ),
5506
+ /* @__PURE__ */ jsx(
5507
+ EmBreve,
5508
+ {
5509
+ label: "Lote e validade",
5510
+ o_que: "Informar lote e vencimento desta entrada \u2014 a coluna j\xE1 existe no movimento de estoque",
5511
+ quantas: "usado 5 vezes no V1; numa churrascaria \xE9 validade de carne"
4105
5512
  }
4106
5513
  )
4107
- ] })
4108
- ] }),
4109
- /* @__PURE__ */ jsxs("div", { className: "rounded-md bg-muted/50 px-3 py-2", children: [
4110
- /* @__PURE__ */ jsxs("div", { className: "flex items-baseline justify-between text-sm", children: [
4111
- /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: "Qtd estoque (calc.)" }),
4112
- /* @__PURE__ */ jsx("span", { className: "font-semibold tabular-nums", children: qty(stockQty) })
4113
5514
  ] }),
4114
- /* @__PURE__ */ jsxs("div", { className: "flex items-baseline justify-between text-sm", children: [
4115
- /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: "Novo custo unit\xE1rio" }),
4116
- /* @__PURE__ */ jsx("span", { className: "tabular-nums", children: money2(unitCost) })
5515
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-3", children: [
5516
+ /* @__PURE__ */ jsxs("label", { className: "block", children: [
5517
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Modo de custo" }),
5518
+ /* @__PURE__ */ jsx("div", { className: "mt-1 flex gap-1", children: ["novo", "media"].map((mode) => /* @__PURE__ */ jsx(
5519
+ "button",
5520
+ {
5521
+ type: "button",
5522
+ disabled: readOnly,
5523
+ onClick: () => patch(item.id, { costMode: mode }),
5524
+ className: `flex-1 rounded-md border px-2 py-1 text-[11px] disabled:opacity-60 ${d.costMode === mode ? "border-primary bg-primary/10" : "border-input hover:bg-muted"}`,
5525
+ children: mode === "novo" ? "Novo" : "M\xE9dia"
5526
+ },
5527
+ mode
5528
+ )) })
5529
+ ] }),
5530
+ /* @__PURE__ */ jsxs("label", { className: "block", children: [
5531
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Falta (qtd)" }),
5532
+ /* @__PURE__ */ jsx(
5533
+ Input,
5534
+ {
5535
+ value: d.shortageQuantity,
5536
+ onChange: (e) => patch(item.id, { shortageQuantity: e.target.value }),
5537
+ inputMode: "decimal",
5538
+ placeholder: "0",
5539
+ disabled: readOnly
5540
+ }
5541
+ )
5542
+ ] })
4117
5543
  ] })
4118
5544
  ] }),
4119
- /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
5545
+ /* @__PURE__ */ jsxs("label", { className: "block", children: [
5546
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Plano de contas" }),
4120
5547
  /* @__PURE__ */ jsx(
4121
- EmBreve,
5548
+ SearchSelect,
4122
5549
  {
4123
- label: "Criar produto daqui",
4124
- o_que: "Cadastrar o produto a partir desta linha da nota, com nome, c\xF3digo e unidade j\xE1 preenchidos",
4125
- quantas: "ela criou 33 produtos no V1 no meio de importa\xE7\xF5es"
5550
+ onSearch: localSearch(accountOptions),
5551
+ value: d.chartAccountId ?? defaultAccount ?? void 0,
5552
+ displayValue: labelOf(accountOptions, d.chartAccountId ?? defaultAccount),
5553
+ onChange: (id) => patch(item.id, { chartAccountId: id || null }),
5554
+ minChars: 0,
5555
+ placeholder: "Herda o padr\xE3o da nota",
5556
+ noResultsLabel: "Nenhum plano de contas",
5557
+ searchingLabel: "Buscando\u2026",
5558
+ clearLabel: "Limpar",
5559
+ disabled: readOnly
4126
5560
  }
4127
- ),
4128
- /* @__PURE__ */ jsx(
4129
- EmBreve,
5561
+ )
5562
+ ] }),
5563
+ item.stockMovementId && /* @__PURE__ */ jsxs("p", { className: "text-[11px] text-success-soft-foreground", children: [
5564
+ /* @__PURE__ */ jsx(Check, { className: "mr-1 inline h-3 w-3", "aria-hidden": true }),
5565
+ "Esta linha j\xE1 deu entrada no estoque."
5566
+ ] }),
5567
+ !readOnly && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-end gap-3 border-t pt-3", children: [
5568
+ !ready && /* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground", children: !d.entryType ? "Escolha o tipo do registro" : "Vincule o produto" }),
5569
+ /* @__PURE__ */ jsxs(
5570
+ Button,
4130
5571
  {
4131
- label: "Lote e validade",
4132
- o_que: "Informar lote e vencimento desta entrada \u2014 a coluna j\xE1 existe no movimento de estoque",
4133
- quantas: "usado 5 vezes no V1; numa churrascaria \xE9 validade de carne"
5572
+ size: "sm",
5573
+ variant: done ? "ghost" : "outline",
5574
+ disabled: !ready,
5575
+ onClick: () => done ? setChecked((prev) => ({ ...prev, [item.id]: false })) : confirmItem(item.id),
5576
+ children: [
5577
+ /* @__PURE__ */ jsx(Check, { className: "mr-1.5 h-4 w-4" }),
5578
+ done ? "Conferido" : "Conferir item"
5579
+ ]
4134
5580
  }
4135
5581
  )
4136
- ] }),
4137
- /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-3", children: [
4138
- /* @__PURE__ */ jsxs("label", { className: "block", children: [
4139
- /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Modo de custo" }),
4140
- /* @__PURE__ */ jsx("div", { className: "mt-1 flex gap-1", children: ["novo", "media"].map((mode) => /* @__PURE__ */ jsx(
4141
- "button",
4142
- {
4143
- type: "button",
4144
- disabled: readOnly,
4145
- onClick: () => patch(item.id, { costMode: mode }),
4146
- className: `flex-1 rounded-md border px-2 py-1 text-[11px] disabled:opacity-60 ${d.costMode === mode ? "border-primary bg-primary/10" : "border-input hover:bg-muted"}`,
4147
- children: mode === "novo" ? "Novo" : "M\xE9dia"
4148
- },
4149
- mode
4150
- )) })
4151
- ] }),
4152
- /* @__PURE__ */ jsxs("label", { className: "block", children: [
4153
- /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Falta (qtd)" }),
4154
- /* @__PURE__ */ jsx(
4155
- Input,
4156
- {
4157
- value: d.shortageQuantity,
4158
- onChange: (e) => patch(item.id, { shortageQuantity: e.target.value }),
4159
- inputMode: "decimal",
4160
- placeholder: "0",
4161
- disabled: readOnly
4162
- }
4163
- )
4164
- ] })
4165
5582
  ] })
4166
- ] }),
4167
- /* @__PURE__ */ jsxs("label", { className: "block", children: [
4168
- /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "Plano de contas" }),
4169
- /* @__PURE__ */ jsx(
4170
- SearchSelect,
4171
- {
4172
- onSearch: localSearch(accountOptions),
4173
- value: d.chartAccountId ?? defaultAccount ?? void 0,
4174
- displayValue: labelOf(accountOptions, d.chartAccountId ?? defaultAccount),
4175
- onChange: (id) => patch(item.id, { chartAccountId: id || null }),
4176
- minChars: 0,
4177
- placeholder: "Herda o padr\xE3o da nota",
4178
- noResultsLabel: "Nenhum plano de contas",
4179
- searchingLabel: "Buscando\u2026",
4180
- disabled: readOnly
4181
- }
4182
- )
4183
- ] }),
4184
- item.stockMovementId && /* @__PURE__ */ jsxs("p", { className: "text-[11px] text-success-soft-foreground", children: [
4185
- /* @__PURE__ */ jsx(Check, { className: "mr-1 inline h-3 w-3", "aria-hidden": true }),
4186
- "Esta linha j\xE1 deu entrada no estoque."
4187
5583
  ] })
4188
5584
  ] })
4189
- ] }) }, item.id);
5585
+ ] }, item.id);
4190
5586
  })
4191
5587
  ] }),
4192
5588
  /* @__PURE__ */ jsxs(Card, { className: "p-4", children: [
@@ -4308,51 +5704,27 @@ function NoteReconcileView({ noteId, onBack, onOpenInvoice }) {
4308
5704
  )
4309
5705
  ] })
4310
5706
  ] }),
4311
- error && /* @__PURE__ */ jsx(Card, { className: "border-destructive bg-destructive/5 p-4", children: /* @__PURE__ */ jsx("p", { className: "text-sm text-destructive", children: error }) }),
4312
- !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: [
4313
- /* @__PURE__ */ jsxs(
4314
- Button,
4315
- {
4316
- variant: "ghost",
4317
- onClick: async () => {
4318
- await rejectNote(note.id);
4319
- onBack();
4320
- },
4321
- disabled: saving || accepting,
4322
- children: [
4323
- /* @__PURE__ */ jsx(X, { className: "mr-1.5 h-4 w-4" }),
4324
- "Rejeitar"
4325
- ]
4326
- }
4327
- ),
4328
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
4329
- (missingType > 0 || missingProduct > 0) && /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
4330
- missingType > 0 && `${missingType} sem tipo`,
4331
- missingType > 0 && missingProduct > 0 && " \xB7 ",
4332
- missingProduct > 0 && `${missingProduct} sem produto`
4333
- ] }),
4334
- /* @__PURE__ */ jsxs(Button, { variant: "outline", onClick: () => {
4335
- void save();
4336
- }, disabled: saving || accepting, children: [
4337
- saving && /* @__PURE__ */ jsx(Loader2, { className: "mr-1.5 h-4 w-4 animate-spin" }),
4338
- "Salvar concilia\xE7\xE3o"
4339
- ] }),
4340
- /* @__PURE__ */ jsxs(
4341
- Button,
4342
- {
4343
- onClick: () => {
4344
- void accept();
4345
- },
4346
- disabled: accepting || saving || missingType > 0 || missingProduct > 0,
4347
- children: [
4348
- 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" }),
4349
- "Aceitar"
4350
- ]
4351
- }
4352
- )
4353
- ] })
4354
- ] })
4355
- ] });
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" });
5719
+ }
5720
+ function ItemState({ done, ready }) {
5721
+ if (done) {
5722
+ return /* @__PURE__ */ jsxs("span", { className: "inline-flex shrink-0 items-center rounded-full bg-success-soft px-2 py-0.5 text-[11px] font-medium text-success-soft-foreground", children: [
5723
+ /* @__PURE__ */ jsx(Check, { className: "mr-1 h-3 w-3", "aria-hidden": true }),
5724
+ "Conferido"
5725
+ ] });
5726
+ }
5727
+ return /* @__PURE__ */ jsx("span", { className: `inline-flex shrink-0 items-center rounded-full px-2 py-0.5 text-[11px] font-medium ${ready ? "bg-muted text-muted-foreground" : "bg-warning-soft text-warning-soft-foreground"}`, children: ready ? "A conferir" : "Falta decidir" });
4356
5728
  }
4357
5729
  function Field({ label, value, mono, small }) {
4358
5730
  return /* @__PURE__ */ jsxs("div", { children: [
@@ -4367,7 +5739,7 @@ var MANIFEST_TONE = {
4367
5739
  processando: "bg-info-soft text-info-soft-foreground",
4368
5740
  erro: "bg-destructive/10 text-destructive"
4369
5741
  };
4370
- function ManifestView({ onOpenNote }) {
5742
+ function ManifestView({ onOpenNote, onBack }) {
4371
5743
  const [notes, setNotes] = useState([]);
4372
5744
  const [loading, setLoading] = useState(true);
4373
5745
  const [tab, setTab] = useState("pendentes");
@@ -4403,6 +5775,7 @@ function ManifestView({ onOpenNote }) {
4403
5775
  ]
4404
5776
  }], [tab]);
4405
5777
  return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
5778
+ onBack && /* @__PURE__ */ jsx(SubpageHeader, { title: "Fila da SEFAZ", onBack, parentLabel: "Manifesto de Notas" }),
4406
5779
  /* @__PURE__ */ jsxs(Card, { className: "p-4", children: [
4407
5780
  /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
4408
5781
  /* @__PURE__ */ jsx(ShieldCheck, { className: "mt-0.5 h-5 w-5 text-muted-foreground", "aria-hidden": true }),
@@ -4468,17 +5841,33 @@ function viewFromLocation() {
4468
5841
  return { kind: "list" };
4469
5842
  }
4470
5843
  var pathOf = (view) => view.kind === "note" ? `${HASH_PREFIX}/note/${view.id}` : view.kind === "manifest" ? `${HASH_PREFIX}/manifest` : HASH_PREFIX;
5844
+ var STACK_MARK = { inboundNotes: true };
4471
5845
  function InboundNotesPage({ payableRoute } = {}) {
4472
5846
  const [view, setView] = useState(viewFromLocation);
5847
+ const [background, setBackground] = useState(
5848
+ () => viewFromLocation().kind === "manifest" ? "manifest" : "list"
5849
+ );
4473
5850
  const go = useCallback((next) => {
4474
5851
  setView(next);
4475
- if (typeof window !== "undefined") {
4476
- window.history.replaceState(null, "", pathOf(next));
5852
+ if (next.kind !== "note") setBackground(next.kind);
5853
+ if (typeof window === "undefined") return;
5854
+ const path = pathOf(next);
5855
+ if (window.location.hash === path) return;
5856
+ const empilhado = window.history.state?.inboundNotes;
5857
+ if (next.kind !== "note" && empilhado) {
5858
+ window.history.back();
5859
+ return;
4477
5860
  }
5861
+ if (next.kind === "list") window.history.replaceState(null, "", path);
5862
+ else window.history.pushState(STACK_MARK, "", path);
4478
5863
  }, []);
4479
5864
  useEffect(() => {
4480
5865
  if (typeof window === "undefined") return;
4481
- const onPop = () => setView(viewFromLocation());
5866
+ const onPop = () => {
5867
+ const next = viewFromLocation();
5868
+ setView(next);
5869
+ if (next.kind !== "note") setBackground(next.kind);
5870
+ };
4482
5871
  window.addEventListener("popstate", onPop);
4483
5872
  window.addEventListener("hashchange", onPop);
4484
5873
  return () => {
@@ -4486,34 +5875,436 @@ function InboundNotesPage({ payableRoute } = {}) {
4486
5875
  window.removeEventListener("hashchange", onPop);
4487
5876
  };
4488
5877
  }, []);
4489
- if (view.kind === "note") {
4490
- 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(
4491
5906
  NoteReconcileView,
4492
5907
  {
5908
+ chrome: "modal",
4493
5909
  noteId: view.id,
4494
- onBack: () => go({ kind: "list" }),
5910
+ onBack: fecharNota,
4495
5911
  onOpenInvoice: payableRoute ? (invoiceId) => {
4496
5912
  window.location.hash = payableRoute(invoiceId);
4497
5913
  } : void 0
4498
5914
  }
4499
- );
4500
- }
4501
- if (view.kind === "manifest") {
4502
- return /* @__PURE__ */ jsx("div", { className: "space-y-4", children: /* @__PURE__ */ jsx(ManifestView, { onOpenNote: (id) => go({ kind: "note", id }) }) });
4503
- }
4504
- return /* @__PURE__ */ jsx(
4505
- InboundNotesView,
4506
- {
4507
- onOpenNote: (id) => go({ kind: "note", id }),
4508
- onOpenManifest: () => go({ kind: "manifest" })
4509
- }
4510
- );
5915
+ ) }) })
5916
+ ] });
4511
5917
  }
4512
5918
  function makeInboundNotesRoute(options = {}) {
4513
5919
  return function InboundNotesRoute() {
4514
5920
  return /* @__PURE__ */ jsx(InboundNotesPage, { ...options });
4515
5921
  };
4516
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
+ }
4517
6308
 
4518
6309
  // src/lib/financial-provider.ts
4519
6310
  function fiscalReader(provider) {
@@ -4521,13 +6312,13 @@ function fiscalReader(provider) {
4521
6312
  return provider.getNotaById.bind(provider);
4522
6313
  }
4523
6314
  async function committed(read, id) {
4524
- const document = await read(id);
4525
- if (!document) throw new Error("Fiscal document was not found after the server operation.");
4526
- 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;
4527
6318
  }
4528
- function resultError(result, document, fallback) {
6319
+ function resultError(result, document2, fallback) {
4529
6320
  if (result.status !== "error") return;
4530
- throw new Error(result.message?.trim() || document.errorMessage?.trim() || fallback);
6321
+ throw new Error(result.message?.trim() || document2.errorMessage?.trim() || fallback);
4531
6322
  }
4532
6323
  function cancellationPending(metadata) {
4533
6324
  const provider = metadata?.plugnotas;
@@ -4549,11 +6340,11 @@ function createPlugNotasFinancialProvider(options) {
4549
6340
  const { base, gateway } = options;
4550
6341
  const issueNota = async (id) => {
4551
6342
  const read = fiscalReader(base);
4552
- const document = await read(id);
4553
- if (!document) throw new Error("Fiscal document not found.");
4554
- if (document.model !== "nfce") throw new Error("PlugNotas currently authorizes NFC-e only.");
4555
- if (!document.orderId?.trim()) throw new Error("PlugNotas NFC-e must be created from an order.");
4556
- 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;
4557
6348
  const result = await gateway.issue({ notaId: id, idempotencyKey: `nfce:${id}` });
4558
6349
  const saved = await committed(read, id);
4559
6350
  resultError(result, saved, "PlugNotas could not issue this NFC-e.");
@@ -4561,9 +6352,9 @@ function createPlugNotasFinancialProvider(options) {
4561
6352
  };
4562
6353
  const refreshNota = async (id) => {
4563
6354
  const read = fiscalReader(base);
4564
- const document = await read(id);
4565
- if (!document) throw new Error("Fiscal document not found.");
4566
- 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;
4567
6358
  const result = await gateway.refresh({ notaId: id });
4568
6359
  const saved = await committed(read, id);
4569
6360
  resultError(result, saved, "PlugNotas could not refresh this NFC-e.");
@@ -4571,10 +6362,10 @@ function createPlugNotasFinancialProvider(options) {
4571
6362
  };
4572
6363
  const cancelNota = async (id, reason) => {
4573
6364
  const read = fiscalReader(base);
4574
- const document = await read(id);
4575
- if (!document) throw new Error("Fiscal document not found.");
4576
- if (document.status !== "issued") throw new Error("Only an issued NFC-e can be cancelled.");
4577
- 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.");
4578
6369
  const normalized = reason?.trim() ?? "";
4579
6370
  if (normalized.length < 15 || normalized.length > 255) {
4580
6371
  throw new Error("Cancellation reason must contain between 15 and 255 characters.");
@@ -4586,12 +6377,12 @@ function createPlugNotasFinancialProvider(options) {
4586
6377
  };
4587
6378
  const getNotaArtifactUrl = async (id, kind) => {
4588
6379
  const read = fiscalReader(base);
4589
- const document = await read(id);
4590
- if (!document) throw new Error("Fiscal document not found.");
4591
- 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)) {
4592
6383
  throw new Error("Fiscal artifacts are available only after authorization.");
4593
6384
  }
4594
- if (kind === "cancellation_xml" && document.status !== "cancelled") {
6385
+ if (kind === "cancellation_xml" && document2.status !== "cancelled") {
4595
6386
  throw new Error("Cancellation XML is available only after cancellation.");
4596
6387
  }
4597
6388
  return artifactUrl((await gateway.artifact({ notaId: id, kind })).url);
@@ -4663,10 +6454,14 @@ function ReturnedNotesView({ onBack } = {}) {
4663
6454
  );
4664
6455
  }
4665
6456
  function inboundNotesInventoryView(options = {}) {
6457
+ registerDocumentOpener(NFE_OPENER);
4666
6458
  return {
4667
6459
  id: "inbound-notes",
4668
6460
  label: "Notas Fiscais",
4669
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",
4670
6465
  render: () => /* @__PURE__ */ jsx(InboundNotesPage, { ...options }),
4671
6466
  // A ordem é a do V1: Grupos Fiscais vem antes das duas de emissão, que é
4672
6467
  // onde ele é usado.
@@ -4684,6 +6479,60 @@ function inboundNotesInventoryView(options = {}) {
4684
6479
  ]
4685
6480
  };
4686
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);
4687
6536
 
4688
6537
  // src/index.ts
4689
6538
  function createPlugNotasPlugin(options = {}) {
@@ -4721,6 +6570,22 @@ function createPlugNotasPlugin(options = {}) {
4721
6570
  // O grupo fiscal é CADASTRO, e por isso entra como registry e não como
4722
6571
  // rota própria: ele aparece onde os outros cadastros do produto aparecem.
4723
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
+ ],
4724
6589
  connectors: [createPlugNotasConnector(options)],
4725
6590
  functions: [{
4726
6591
  slug: "plugnotas-bridge",
@@ -4792,6 +6657,6 @@ function createPlugNotasPlugin(options = {}) {
4792
6657
  });
4793
6658
  }
4794
6659
 
4795
- 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 };
4796
6661
  //# sourceMappingURL=index.js.map
4797
6662
  //# sourceMappingURL=index.js.map