📄 SKILL.md

← Vault

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

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