name: comercialrs-sales-crm-view
description: Criar view sales_crm_view no Supabase Cloud unindo crm_leads_duplicate + sales manuais. Frontend lĂȘ da view; SaleDetailDialog protege CRM de edits/deletes. Inclui SQL, patches Sales.tsx/SaleDetailDialog, pitfalls.
trigger: criar view sales_crm_view crm_leads_duplicate sales comercialrs
ComercialRS â sales_crm_view (CRM + Manuais)
Contexto
- -
crm_leads_duplicate= tabela fonte do Painel do Corretor (trigger externo alimenta) - -
sales= tabela local com copies do CRM (source='crm') + manuais (source='manual') - - Objetivo: view UNION das duas fontes, frontend lendo da view em vez de
salesdireto
SQL â View
`sql
CREATE OR REPLACE VIEW public.sales_crm_view AS
-- CRM: crm_leads_duplicate fonte, LEFT JOIN sales para sync meta (notes, link)
SELECT
(CASE WHEN s.id IS NOT NULL THEN s.id::text ELSE 'crm-' || c.id_internal END) AS id,
('crm-lead-' || c."Id") AS external_ref,
c."Nome" AS title,
COALESCE(c."Vendedor", '') AS seller,
NULL AS seller_email,
COALESCE(c."Contrato", '') AS policy_number,
COALESCE(c."Produto.Nome", '') AS product,
COALESCE(c."Valor" :: numeric, 0) AS value,
CASE
WHEN c."Etapa" IN ('Novos Leads', 'Qualificação', 'Reunião agendada') THEN 'emissao'
WHEN c."Etapa" IN ('Remarketing ', 'Analise da Seguradora') THEN 'analise_seguradora'
WHEN c."Etapa" = 'Proposta Enviada p/cliente' THEN 'aguardando_pagamento'
WHEN c."Etapa" IN ('NegĂłcios Fechado', 'Fechado') THEN 'pago'
WHEN c."Etapa" IN ('Sem Interresse ', 'Declinado') THEN 'declinado'
ELSE 'emissao'
END AS status,
'crm' AS source,
CASE WHEN c."Fechamento" IS NOT NULL THEN c."Fechamento"
WHEN c."Etapa" IN ('NegĂłcios Fechado', 'Fechado') THEN c."CriadoEm"
ELSE NULL END AS paid_at,
c."CriadoEm" AS created_at,
c."CriadoEm" AS emitted_at,
s.synced_at, s.notes, s.link, s.deleted_at,
c."Id" AS crm_id,
c.id_internal AS crm_internal_id,
s.id AS sales_id
FROM crm_leads_duplicate c
LEFT JOIN (
SELECT DISTINCT ON (external_ref) *
FROM sales
WHERE source = 'crm' AND deleted_at IS NULL
ORDER BY external_ref, updated_at DESC NULLS LAST
) s ON s.external_ref = ('crm-lead-' || c."Id")
UNION ALL
SELECT
s.id::text, COALESCE(s.external_ref, ''), s.title,
COALESCE(s.seller, ''), s.seller_email,
COALESCE(s.policy_number, ''), COALESCE(s.product, ''),
s.value, s.status, s.source,
s.paid_at, s.created_at, s.emitted_at, s.synced_at,
s.notes, s.link, s.deleted_at,
NULL AS crm_id, NULL AS crm_internal_id, s.id AS sales_id
FROM sales s
WHERE s.source = 'manual' AND s.deleted_at IS NULL;
`
Colunas de crm_leads_duplicate (schema real)
`
Contato.Avatar, Contato.Cidade, Contato.Documento, Contato.Email,
Contato.Id, Contato.Nome, Contato.Telefones, Contrato, CriadoEm,
Etapa, Etiquetas, Fechamento, Id, Nome, Observacao,
Produto.Nome, Produto.Ramo, ProdutoId, Valor, Vendedor,
custom_data (jsonb), id_internal (uuid), source
`
CUIDADO: policyname aparece em information_schema mas nao existe. Provavelmente dado
da apolice affinity esta em custom_data (jsonb) ou outra coluna â confirmar com usuario.
Frontend â Sales.tsx
`typescript
type SaleRow = {
// ... campos originais ...
crm_id?: string | null;
crm_internal_id?: string | null;
sales_id?: string | null;
};
function rowToSale(r: SaleRow): Sale {
return {
// ... mapeamento existente ...
crmId: r.crm_id ?? undefined,
crmInternalId: r.crm_internal_id ?? undefined,
salesId: r.sales_id ?? undefined,
};
}
// fetchSales: view no lugar da tabela
const { data } = await supabase
.from('sales_crm_view')
.select('*')
.is('deleted_at', null) // NOTE: deleted_at nao existe na view (sao filtrados no SQL)
// na verdade: filtro no SQL ja filtra deleted_at, mas a view retorna null
// .is('deleted_at', null) // remover se nao necessario
// handleSaleMove: usa salesId para update no banco
const targetId = sale?.salesId || saleId;
`
SaleDetailDialog â protecao CRM
`typescript
// handleSave â edicao
if (sale.source === 'crm') {
toast.error('Vendas do CRM sĂŁo somente leitura. Edite no Painel do Corretor.');
return;
}
// handleDelete â exclusao
if (sale.source === 'crm') {
toast.error('Vendas do CRM nĂŁo podem ser excluĂdas aqui.');
return;
}
// handleSaveNotes â anotacoes
if (sale.source === 'crm'! && !sale.salesId) {
toast.error('Esta venda do CRM ainda nĂŁo foi sincronizada.');
return;
}
const targetId = sale.salesId || sale.id; // salesId para CRM syncado
`
Pitfalls
1. supabase db query --linked output: CLI imprime banner ASCII que polui output JSON.
Usar --output json + grep -v "Initialising\|version\|A new\|â"
2. Fechamento empty string: c."Fechamento" <> '' nao funciona com tipo timestamptz.
Usar so c."Fechamento" IS NOT NULL
3. Deduplicacao CRM na view: multiplos syncs criam multiplos sales com mesmo
external_ref. Usar SELECT DISTINCT ON (external_ref) no subselect
4. id da view para CRM: usa sales.id se existir (ja syncado) ou 'crm-' || id_internal
(nunca syncado). Frontend usa id p/ display, salesId p/ updates
5. information_schema mente: policyname aparece em information_schema.columns
mas a coluna real nao existe. Sempre verificar com query real antes de usar