feat(web+api): ficha do cliente — contatos, CTR, NF-e, pedidos e produtos

- Contatos: grid de cards 3 colunas, botão "Novo Contato" no topo da página
- Sync ERP↔SAR corrigido: vw_contatos aceita formato COR#<id>, trigger normaliza id_empresa (9001→1)
- CTR + NF-e: layout 50/50 — lista de títulos abertos com badge vencido/a vencer e lista de notas com botão copiar chave NF-e
- Histórico de pedidos: UNION SAR+ERP, top 5 + modal "Ver todos"
- Produtos mais comprados: top 5 com último preço + modal "Ver todos"
- Novos endpoints: ctr-list, notas, orders-history, top-produtos
- AppShell: overflow-x travado, sem scroll horizontal na aplicação

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-25 14:13:21 +00:00
parent a2bab75bad
commit 400fcb3360
23 changed files with 2532 additions and 356 deletions

View File

@@ -505,14 +505,18 @@ FROM gestao.marca;
-- -----------------------------------------------------------------------------
-- Contatos vinculados a clientes (gestao.contato)
-- id_entidade = id_corrent::text (CHAR 20, auto-pad)
-- Usado para envio via WhatsApp / ChatWoot futuramente
-- O ERP grava id_entidade como 'COR#<id_corrent>' (formato nativo).
-- O SAR legado gravava apenas o número puro. Ambos os formatos são aceitos.
-- -----------------------------------------------------------------------------
CREATE OR REPLACE VIEW sar.vw_contatos AS
SELECT
c.id_contato,
c.id_empresa,
TRIM(c.id_entidade)::integer AS id_corrent,
CASE
WHEN TRIM(c.id_entidade) ~ '^COR#\d+$'
THEN SUBSTRING(TRIM(c.id_entidade) FROM 5)::integer
ELSE TRIM(c.id_entidade)::integer
END AS id_corrent,
c.id_chatwoot,
TRIM(c.nome) AS nome,
TRIM(c.empresa) AS empresa,
@@ -530,9 +534,13 @@ SELECT
TRIM(cr.razao::text) AS razao_cliente,
cr.cod_vendedor
FROM gestao.contato c
LEFT JOIN sig.corrent cr
ON cr.id_corrent = TRIM(c.id_entidade)::integer
WHERE TRIM(c.id_entidade) ~ '^\d+$';
LEFT JOIN sig.corrent cr ON cr.id_corrent = CASE
WHEN TRIM(c.id_entidade) ~ '^COR#\d+$'
THEN SUBSTRING(TRIM(c.id_entidade) FROM 5)::integer
ELSE TRIM(c.id_entidade)::integer
END
WHERE TRIM(c.id_entidade) ~ '^\d+$'
OR TRIM(c.id_entidade) ~ '^COR#\d+$';
-- =============================================================================
-- PARTE 2 — TABELAS DE ESCRITA DO SAR

View File

@@ -489,8 +489,14 @@ CREATE INDEX IF NOT EXISTS idx_sar_ctnov_sync ON sar.contatos_novos(sincroniz
CREATE OR REPLACE FUNCTION sar.fn_sync_contato_to_erp()
RETURNS TRIGGER LANGUAGE plpgsql AS $fn$
DECLARE
v_id_contato INTEGER;
v_id_contato INTEGER;
v_id_empresa INTEGER;
BEGIN
-- Normaliza empresa fiscal (9001) → empresa gerencial (1), padrão do ERP
v_id_empresa := CASE WHEN NEW.id_empresa > 9000
THEN NEW.id_empresa - 9000
ELSE NEW.id_empresa END;
BEGIN
INSERT INTO gestao.contato (
id_empresa, id_entidade,
@@ -499,8 +505,8 @@ BEGIN
telefone, ramal, celular, whatsapp, email,
ativo, anotacoes
) VALUES (
NEW.id_empresa,
NEW.id_corrent::text,
v_id_empresa,
'COR#' || NEW.id_corrent::text, -- formato nativo do ERP
COALESCE(LEFT(NEW.nome, 100), ''),
LEFT(NEW.empresa, 100),
LEFT(NEW.cargo, 100),