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:
@@ -7,7 +7,10 @@ import type {
|
||||
ClientListResponse,
|
||||
ClientNovoResult,
|
||||
ClientSummary,
|
||||
Contato,
|
||||
ContatoResult,
|
||||
CreateClientNovo,
|
||||
CreateContato,
|
||||
} from '@sar/api-interface';
|
||||
import type { WorkspaceClsStore } from '../workspace/workspace.types';
|
||||
|
||||
@@ -248,6 +251,119 @@ export class ClientsService {
|
||||
};
|
||||
}
|
||||
|
||||
async listContacts(idCorrent: number): Promise<Contato[]> {
|
||||
const prisma = this.cls.get('prisma');
|
||||
if (!prisma) throw new Error('prisma não disponível no CLS');
|
||||
|
||||
interface Row {
|
||||
id_contato: number;
|
||||
id_empresa: number;
|
||||
id_corrent: number;
|
||||
id_chatwoot: number | null;
|
||||
nome: string;
|
||||
empresa: string | null;
|
||||
cargo: string | null;
|
||||
departamento: string | null;
|
||||
dt_aniversario: string | null;
|
||||
telefone: string | null;
|
||||
ramal: string | null;
|
||||
celular: string | null;
|
||||
whatsapp: string | null;
|
||||
email: string | null;
|
||||
ativo: number;
|
||||
anotacoes: string | null;
|
||||
nome_cliente: string | null;
|
||||
razao_cliente: string | null;
|
||||
cod_vendedor: number | null;
|
||||
}
|
||||
|
||||
const rows = await prisma.$queryRawUnsafe<Row[]>(`
|
||||
SELECT id_contato, id_empresa, id_corrent, id_chatwoot,
|
||||
nome, empresa, cargo, departamento,
|
||||
dt_aniversario::text, telefone, ramal, celular, whatsapp, email,
|
||||
ativo, anotacoes, nome_cliente, razao_cliente, cod_vendedor
|
||||
FROM sar.vw_contatos
|
||||
WHERE id_corrent = ${idCorrent} AND ativo = 1
|
||||
ORDER BY nome
|
||||
`);
|
||||
|
||||
return rows.map((r) => ({
|
||||
idContato: Number(r.id_contato),
|
||||
idEmpresa: Number(r.id_empresa),
|
||||
idCorrent: Number(r.id_corrent),
|
||||
idChatwoot: r.id_chatwoot !== null ? Number(r.id_chatwoot) : null,
|
||||
nome: r.nome,
|
||||
empresa: r.empresa,
|
||||
cargo: r.cargo,
|
||||
departamento: r.departamento,
|
||||
dtAniversario: r.dt_aniversario,
|
||||
telefone: r.telefone,
|
||||
ramal: r.ramal,
|
||||
celular: r.celular,
|
||||
whatsapp: r.whatsapp,
|
||||
email: r.email,
|
||||
ativo: Number(r.ativo),
|
||||
anotacoes: r.anotacoes,
|
||||
nomeCliente: r.nome_cliente,
|
||||
razaoCliente: r.razao_cliente,
|
||||
codVendedor: r.cod_vendedor !== null ? Number(r.cod_vendedor) : null,
|
||||
}));
|
||||
}
|
||||
|
||||
async createContato(idCorrent: number, dto: CreateContato): Promise<ContatoResult> {
|
||||
const prisma = this.cls.get('prisma');
|
||||
if (!prisma) throw new Error('prisma não disponível no CLS');
|
||||
const idEmpresa = this.cls.get('idEmpresa');
|
||||
const userId = this.cls.get('userId');
|
||||
const codVendedor = userId ? parseInt(userId, 10) : 0;
|
||||
|
||||
interface Row {
|
||||
id: string;
|
||||
}
|
||||
const esc = (s: string) => s.replace(/'/g, "''");
|
||||
const opt = (v: string | undefined) => (v ? `'${esc(v)}'` : 'NULL');
|
||||
|
||||
const insertRows = await prisma.$queryRawUnsafe<Row[]>(`
|
||||
INSERT INTO sar.contatos_novos (
|
||||
id_empresa, cod_vendedor, id_corrent,
|
||||
nome, empresa, cargo, departamento,
|
||||
dt_aniversario, telefone, ramal, celular, whatsapp, email, anotacoes
|
||||
) VALUES (
|
||||
${idEmpresa}, ${codVendedor}, ${idCorrent},
|
||||
'${esc(dto.nome)}',
|
||||
${opt(dto.empresa)}, ${opt(dto.cargo)}, ${opt(dto.departamento)},
|
||||
${dto.dtAniversario ? `'${dto.dtAniversario}'` : 'NULL'},
|
||||
${opt(dto.telefone)}, ${opt(dto.ramal)}, ${opt(dto.celular)},
|
||||
${opt(dto.whatsapp)}, ${opt(dto.email)}, ${opt(dto.anotacoes)}
|
||||
)
|
||||
RETURNING id::text
|
||||
`);
|
||||
|
||||
const id = insertRows[0]?.id;
|
||||
if (!id) throw new Error('Falha ao inserir contato');
|
||||
|
||||
interface ResultRow {
|
||||
id: string;
|
||||
id_contato_erp: number | null;
|
||||
sincronizado: boolean;
|
||||
erro_sync: string | null;
|
||||
}
|
||||
const rows = await prisma.$queryRawUnsafe<ResultRow[]>(`
|
||||
SELECT id::text, id_contato_erp, sincronizado, erro_sync
|
||||
FROM sar.contatos_novos WHERE id = '${id}'
|
||||
`);
|
||||
|
||||
const r = rows[0];
|
||||
if (!r) throw new Error('Falha ao recuperar contato inserido');
|
||||
|
||||
return {
|
||||
id: r.id,
|
||||
idContatoErp: r.id_contato_erp !== null ? Number(r.id_contato_erp) : null,
|
||||
sincronizado: r.sincronizado,
|
||||
erroSync: r.erro_sync,
|
||||
};
|
||||
}
|
||||
|
||||
async findOne(idCliente: number): Promise<ClientDetail> {
|
||||
const prisma = this.cls.get('prisma');
|
||||
if (!prisma) throw new Error('prisma não disponível no CLS');
|
||||
|
||||
Reference in New Issue
Block a user