feat(web+api): contatos vinculados a clientes com sync ERP e base WhatsApp

DB:
- sar.vw_contatos: view sobre gestao.contato com join sig.corrent
  (id_entidade CHAR20 = id_corrent; cod_vendedor para filtro por rep)
- sar.contatos_novos: staging com nome, cargo, departamento, telefone,
  celular, whatsapp, email, dt_aniversario, anotacoes
- trg_contato_novo: AFTER INSERT → gestao.contato; grava id_contato_erp

API:
- GET  /clients/:id/contacts  — lista contatos ativos do cliente
- POST /clients/:id/contacts  — cria via staging; retorna { idContatoErp, sincronizado }

Web:
- ClientContacts: tabela de contatos no detalhe do cliente
- Links diretos WhatsApp (wa.me) + mailto por contato
- Modal "Novo Contato" com todos os campos; feedback de código ERP no sucesso
- whatsapp é o campo principal para envio futuro via ChatWoot

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-24 17:25:48 +00:00
parent 236dd69eba
commit 34368e8006
8 changed files with 574 additions and 0 deletions

View File

@@ -63,6 +63,54 @@ export const ClientListResponseSchema = z.object({
});
export type ClientListResponse = z.infer<typeof ClientListResponseSchema>;
// ─── Contato ─────────────────────────────────────────────────────────────────
export const ContatoSchema = z.object({
idContato: z.number().int(),
idEmpresa: z.number().int(),
idCorrent: z.number().int(),
idChatwoot: z.number().int().nullable(),
nome: z.string(),
empresa: z.string().nullable(),
cargo: z.string().nullable(),
departamento: z.string().nullable(),
dtAniversario: z.string().nullable(),
telefone: z.string().nullable(),
ramal: z.string().nullable(),
celular: z.string().nullable(),
whatsapp: z.string().nullable(),
email: z.string().nullable(),
ativo: z.number().int(),
anotacoes: z.string().nullable(),
nomeCliente: z.string().nullable(),
razaoCliente: z.string().nullable(),
codVendedor: z.number().int().nullable(),
});
export type Contato = z.infer<typeof ContatoSchema>;
export const CreateContatoSchema = z.object({
nome: z.string().min(1).max(100),
empresa: z.string().max(100).optional(),
cargo: z.string().max(100).optional(),
departamento: z.string().max(100).optional(),
dtAniversario: z.string().optional(),
telefone: z.string().max(20).optional(),
ramal: z.string().max(10).optional(),
celular: z.string().max(20).optional(),
whatsapp: z.string().max(20).optional(),
email: z.string().max(150).optional(),
anotacoes: z.string().optional(),
});
export type CreateContato = z.infer<typeof CreateContatoSchema>;
export const ContatoResultSchema = z.object({
id: z.string().uuid(),
idContatoErp: z.number().int().nullable(),
sincronizado: z.boolean(),
erroSync: z.string().nullable(),
});
export type ContatoResult = z.infer<typeof ContatoResultSchema>;
// ─── Municipio ───────────────────────────────────────────────────────────────
export const MunicipioSchema = z.object({