﻿// Mock data for Horos dashboard

const SEGMENTS = [
  { id: 'all', label: 'Todos' },
  { id: 'imobiliario', label: 'Imobiliário' },
  { id: 'seguros', label: 'Seguros' },
  { id: 'veiculos', label: 'Veículos' },
  { id: 'credito', label: 'Crédito' },
  { id: 'consorcios', label: 'Consórcios' },
];

// Colunas que representam leads "perdidos/arquivados" (não contam como ativos)
const ARCHIVED_COLS = new Set(['arquivados', 'arquivados_atend', 'arquivado_aprovado', 'serasa']);

// Mapeamento coluna Kanban → status CRM (compartilhado por dashboard, crm, clientes)
const COL_TO_STATUS = {
  inbox: 'qualifying',
  contato1: 'qualifying',      contato2: 'qualifying',      contato3: 'qualifying',
  contato4: 'qualifying',      contato5: 'qualifying',      contato6: 'qualifying',
  contato7: 'qualifying',
  aguardando_docs: 'engaged',  aguardando_agenda: 'engaged', agendado: 'engaged',
  em_conversa: 'engaged',
  em_analise: 'proposal',      aprovado_final: 'proposal',
  arquivados: 'lost',          arquivados_atend: 'lost',    arquivado_aprovado: 'lost',
  serasa: 'lost',
  fechado: 'won',
};

const COLUMNS = [
  { id: 'inbox',                title: 'Inbox',                       color: '#7EC8FF' },
  { id: 'contato1',             title: '1º Contato',                  color: '#1F93FF' },
  { id: 'contato2',             title: '2º Contato',                  color: '#60a5fa' },
  { id: 'contato3',             title: '3º Contato',                  color: '#38bdf8' },
  { id: 'contato4',             title: '4º Contato',                  color: '#0ea5e9' },
  { id: 'contato5',             title: '5º Contato',                  color: '#22d3ee' },
  { id: 'contato6',             title: '6º Contato',                  color: '#2dd4bf' },
  { id: 'contato7',             title: '7º Contato',                  color: '#34d399' },
  { id: 'em_conversa',          title: 'Em conversa',                 color: '#06b6d4' },
  { id: 'aguardando_docs',      title: 'Aguardando Documentação',     color: '#fbbf24' },
  { id: 'aguardando_agenda',    title: 'Aguardando Agendamento',      color: '#f59e0b' },
  { id: 'em_analise',           title: 'Em Análise',                  color: '#fb923c' },
  { id: 'agendado',             title: 'Agendado',                    color: '#f97316' },
  { id: 'arquivados',           title: 'Arquivados',                  color: '#94a3b8' },
  { id: 'arquivados_atend',     title: 'Arquivados em Atendimento',   color: '#64748b' },
  { id: 'arquivado_aprovado',   title: 'Arquivado Aprovado',          color: '#a78bfa' },
  { id: 'serasa',               title: 'Serasa/Bacen',                color: '#ef4444' },
  { id: 'aprovado_final',       title: 'Aprovado',                    color: '#10b981' },
  { id: 'fechado',              title: 'Fechado',                     color: '#34d399' },
];

// Progress % by kanban column position — shared by Dashboard and CRM
const COL_PROGRESS = {};
COLUMNS.forEach((c, i) => { COL_PROGRESS[c.id] = Math.round((i / (COLUMNS.length - 1)) * 100); });

const AVATAR_PALETTES = [
  ['#f59e0b', '#ef4444'],
  ['#1F93FF', '#3b82f6'],
  ['#10b981', '#06b6d4'],
  ['#8b5cf6', '#ec4899'],
  ['#f97316', '#eab308'],
  ['#14b8a6', '#3b82f6'],
  ['#ec4899', '#f43f5e'],
  ['#22d3ee', '#1F93FF'],
];

// Labels dos documentos do lead (espelha DOC_CATEGORIES em clientes-editar.jsx)
const LEAD_DOC_LABELS = {
  rg: 'RG (frente e verso)', cnh: 'CNH', certidao: 'Certidão nasc./casamento',
  holerite_1: 'Holerite — mês atual', holerite_2: 'Holerite — mês anterior',
  holerite_3: 'Holerite — há 2 meses', ctps: 'Carteira de Trabalho',
  ir: 'Declaração de IR', fgts: 'Extrato FGTS', decore: 'Decore (autônomos)',
  extrato: 'Extrato bancário', comp_res: 'Comprovante de residência',
  analise_credito: 'Análise de crédito CCA', carta_aprovacao: 'Carta de aprovação',
};

// Converte lead.docs JSONB → array [{ label, ok }] para o Customer360
function buildCreditDocs(rawDocs) {
  if (!rawDocs || typeof rawDocs !== 'object') return null;
  const list = Object.entries(rawDocs)
    .filter(([k, v]) => LEAD_DOC_LABELS[k] && v && v.status && v.status !== 'empty')
    .map(([k, v]) => ({ label: LEAD_DOC_LABELS[k], ok: v.status === 'approved' }));
  return list.length > 0 ? list : null;
}

// Normalizes a Supabase lead (flat shape) into the richer card shape.
// Always re-processes — never short-circuits — so updates via horos:lead-updated
// always reflect the latest JSONB data.
function normalizeLead(l) {
  const hash = [...(l.id || l.name || '')].reduce((a, c) => a + c.charCodeAt(0), 0);
  const palette = l.palette || AVATAR_PALETTES[hash % AVATAR_PALETTES.length];
  let last = l.last || '—';
  const ts = l.updated_at;
  if (ts) {
    const diff = Math.floor((Date.now() - new Date(ts)) / 60000);
    if (diff < 60) last = `há ${diff}min`;
    else if (diff < 1440) last = `há ${Math.floor(diff / 60)}h`;
    else last = `há ${Math.floor(diff / 1440)}d`;
  }
  const d = l.data || {};
  return {
    ...l,
    palette,
    progress: COL_PROGRESS[l.col] ?? 0,
    wa: l.wa || false,
    last,
    tags: l.tags || [],
    broker:      d.corretor           || l.broker || '',
    renda:       d.rendaIndividual    ? `R$ ${d.rendaIndividual}/mês` : null,
    region:      d.regiaoDesejada     || (l.cat && l.cat !== l.name ? l.cat : null),
    interesse:   d.categoriaInteresse || l.cat    || null,
    observacoes: d.obsInteresses      || null,
    creditDocs:  buildCreditDocs(l.docs),
  };
}

// Só letras/números: `s[0]` em nome com emoji pegava meio caractere (surrogate
// solto), o JSON ficava inválido e o banco recusava (PGRST102 no "Adicionar como Lead").
const initials = (n) => {
  const words = String(n || '').split(/\s+/)
    .map(w => (w.match(/[\p{L}\p{N}]/u) || [''])[0])
    .filter(Boolean);
  return words.slice(0, 2).join('').toUpperCase();
};

const fmtCurrency = (v) => {
  if (v >= 1000000) return `${(v/1000000).toFixed(2).replace('.', ',')}M`;
  if (v >= 1000) return `${(v/1000).toFixed(0)}k`;
  return v.toString();
};

// Palavra da interface conforme o ramo da empresa (horos-segments.js): segTerm('Agent') → "Corretor" no
// imobiliário, "Consultor" no consórcio… · 'company' → imobiliária/empresa · 'product' → imóvel/cota/curso
const segTerm = (key, fallback) => (window.horosTerm ? window.horosTerm(key) : (fallback || key));
window.segTerm = segTerm;

const fmtPhone = (v) => {
  if (!v || v === '—') return v || '—';
  let d = v.replace(/\D/g, '');
  if (d.length > 11) {
    const take = d.length === 12 ? 10 : 11;
    d = d.slice(d.length - take);
  }
  d = d.slice(0, 11);
  if (d.length === 0) return '—';
  if (d.length <= 2) return `(${d})`;
  if (d.length <= 6) return `(${d.slice(0,2)}) ${d.slice(2)}`;
  if (d.length <= 10) return `(${d.slice(0,2)}) ${d.slice(2,6)}-${d.slice(6)}`;
  return `(${d.slice(0,2)}) ${d.slice(2,7)}-${d.slice(7)}`;
};


const _DEMO_CLIENTS = [
  {
    id: 'C01',
    name: 'Helena Duarte', email: 'helena.duarte@gmail.com',
    phone: '+55 11 98765-4321', seg: 'Imobiliário', cat: 'Apto frente-mar',
    status: 'proposal', score: 93, action: 'Enviar contrato', when: 'Hoje · 14:00', urgency: 'due-now',
    palette: ['#ec4899', '#f43f5e'], initials: 'HD',
    temp: 'hot', value: 1180000, progress: 85, last: 'há 4h',
    broker: 'João Mendes', renda: 'R$ 32.000/mês', region: 'Guarujá / Santos',
    proxAcao: 'Enviar contrato para assinatura',
    proxAcaoWhen: 'Hoje · 14:00', proxAcaoUrgency: 'now',
    observacoes: 'Cliente VIP. Quer frente ao mar, andar alto. Marido viaja muito — decisão é dela.',
    creditScore: 870, creditTier: 'Muito Bom',
    creditFaixa: 'R$ 25k–40k/mês', creditCompromisso: '18% da renda',
    creditDocs: [
      { label: 'RG / CNH', ok: true },
      { label: 'Comprovante de renda (3 meses)', ok: true },
      { label: 'Extrato bancário (6 meses)', ok: true },
      { label: 'Declaração IR 2024', ok: false },
    ],
    messages: [
      { from: 'client', text: 'Bom dia! Podemos ver o contrato hoje à tarde?', time: '09:12' },
      { from: 'me', text: 'Claro, Helena! Vou enviar até as 14h. Está tudo certo com a documentação?', time: '09:45' },
      { from: 'client', text: 'Perfeito. Só falta a declaração de IR que vou buscar amanhã.', time: '10:03' },
      { from: 'ia', text: 'Sugestão IA: Helena tem score 93 e 18% de comprometimento de renda. Aprovação bancária é praticamente certa. Priorize o envio do contrato hoje.', time: '10:05' },
    ],
    visits: [
      { date: '22 Mai', time: '10:00', status: 'realizada', agent: 'João Mendes', note: 'Cliente adorou a vista do 12º andar. Pediu para verificar barulho na varanda.' },
      { date: '28 Mai', time: '14:30', status: 'agendada', agent: 'João Mendes', note: 'Segunda visita com o marido.' },
    ],
    timeline: [
      { kind: 'client', time: 'Hoje · 09:12', label: 'Cliente enviou mensagem via WhatsApp perguntando sobre o contrato.' },
      { kind: 'broker', time: 'Hoje · 09:45', label: 'Corretor confirmou envio do contrato para as 14h.' },
      { kind: 'ai', time: 'Hoje · 10:05', label: 'IA identificou alta probabilidade de fechamento. Score subiu de 89 → 93.' },
      { kind: 'broker', time: '22 Mai · 10:00', label: 'Visita realizada ao imóvel. Feedback positivo registrado.' },
      { kind: 'system', time: '20 Mai · 18:00', label: 'Lead avançou de Reunião → Proposta automaticamente.' },
    ],
  },
  {
    id: 'C02',
    name: 'Lucas Pereira', email: 'lucas@startup.co',
    phone: '+55 21 97654-3210', seg: 'Crédito PJ', cat: 'Financ. R$ 1.8M',
    status: 'engaged', score: 91, action: 'Reunião com sócios', when: 'Amanhã · 10:30', urgency: 'due-soon',
    palette: ['#1F93FF', '#3b82f6'], initials: 'LP',
    temp: 'hot', value: 1800000, progress: 72, last: 'há 1h',
    broker: 'Ana Paula Silva', renda: 'R$ 180k/mês (PJ)', region: 'Rio de Janeiro · Centro',
    proxAcao: 'Reunião com sócios para aprovação',
    proxAcaoWhen: 'Amanhã · 10:30', proxAcaoUrgency: 'soon',
    observacoes: 'Startup de logística em fase de expansão. Decisão colegiada — precisa de 3 sócios presentes.',
    creditScore: 920, creditTier: 'Excelente',
    creditFaixa: 'R$ 100k–250k/mês (PJ)', creditCompromisso: '12% da receita',
    creditDocs: [
      { label: 'CNPJ / Contrato social', ok: true },
      { label: 'Balanço patrimonial 2024', ok: true },
      { label: 'DRE últimos 12 meses', ok: true },
      { label: 'Extrato conta PJ (6 meses)', ok: true },
      { label: 'Garantias reais', ok: false },
    ],
    messages: [
      { from: 'me', text: 'Lucas, confirmado para amanhã às 10:30 na sede?', time: '14:20' },
      { from: 'client', text: 'Confirmado! Os três sócios estarão presentes.', time: '14:55' },
      { from: 'ia', text: 'Sugestão IA: Prepare uma apresentação com simulação de parcelas e projeção de retorno para convencer os sócios avessos a risco.', time: '15:00' },
    ],
    visits: [],
    timeline: [
      { kind: 'client', time: 'Hoje · 14:55', label: 'Lucas confirmou presença de todos os sócios na reunião de amanhã.' },
      { kind: 'ai', time: 'Hoje · 15:00', label: 'IA preparou briefing de reunião com simulações de crédito.' },
      { kind: 'broker', time: 'Ontem · 09:00', label: 'Reunião inicial realizada. Interesse alto em R$ 1.8M para expansão logística.' },
      { kind: 'system', time: '25 Mai · 11:00', label: 'Lead entrou no funil pela campanha Google Ads "Crédito PJ".' },
    ],
  },
  {
    id: 'C03',
    name: 'Camila Reis', email: 'camila.reis@me.com',
    phone: '+55 11 99988-7766', seg: 'Imobiliário', cat: 'Cobertura duplex',
    status: 'engaged', score: 88, action: 'Follow-up visita', when: 'Hoje · 17:00', urgency: 'due-now',
    palette: ['#8b5cf6', '#ec4899'], initials: 'CR',
    temp: 'hot', value: 2100000, progress: 68, last: 'há 3h',
    broker: 'João Mendes', renda: 'R$ 55.000/mês', region: 'Itaim Bibi · São Paulo',
    proxAcao: 'Follow-up pós-visita',
    proxAcaoWhen: 'Hoje · 17:00', proxAcaoUrgency: 'now',
    observacoes: 'Profissional liberal (médica). Quer privacidade e espaço. Cobertura com piscina é prioridade.',
    creditScore: 850, creditTier: 'Muito Bom',
    creditFaixa: 'R$ 45k–65k/mês', creditCompromisso: '22% da renda',
    creditDocs: [
      { label: 'RG / CRM médico', ok: true },
      { label: 'Comprovante de renda (6 meses)', ok: true },
      { label: 'Declaração IR 2024', ok: true },
      { label: 'Matrícula do imóvel atual', ok: false },
    ],
    messages: [
      { from: 'client', text: 'A cobertura é incrível! Mas quero comparar com outra que vi no Jardins.', time: '13:30' },
      { from: 'ia', text: 'Sugestão IA: Envie comparativo direto mostrando que o Itaim tem 40% mais valorização histórica que o Jardins no mesmo ticket.', time: '13:32' },
      { from: 'me', text: 'Camila, fiz o comparativo — o Itaim valorizou 42% nos últimos 5 anos contra 28% no Jardins. Falo às 17h?', time: '14:00' },
    ],
    visits: [
      { date: '27 Mai', time: '11:00', status: 'realizada', agent: 'João Mendes', note: 'Visita à cobertura duplex. Cliente ficou 2h explorando os ambientes. Muito positiva.' },
    ],
    timeline: [
      { kind: 'client', time: 'Hoje · 13:30', label: 'Camila expressou interesse mas quer comparar com imóvel no Jardins.' },
      { kind: 'ai', time: 'Hoje · 13:32', label: 'IA gerou comparativo de valorização Itaim vs Jardins para o corretor.' },
      { kind: 'broker', time: 'Hoje · 11:00', label: 'Visita realizada. Cliente avaliou positivamente.' },
      { kind: 'system', time: '24 Mai · 16:00', label: 'Lead evoluiu de Contato → Reunião após confirmar visita.' },
    ],
  },
  {
    id: 'C04',
    name: 'Tiago Vieira', email: 'tiago.v@outlook.com',
    phone: '+55 31 98123-4567', seg: 'Imobiliário', cat: 'Casa em condomínio',
    status: 'qualifying', score: 84, action: 'Agendar visita', when: 'Quinta · 09:00', urgency: '',
    palette: ['#10b981', '#06b6d4'], initials: 'TV',
    temp: 'hot', value: 1450000, progress: 45, last: 'há 1 dia',
    broker: 'Fernanda Costa', renda: 'R$ 28.000/mês', region: 'Nova Lima · MG',
    proxAcao: 'Agendar visita ao condomínio',
    proxAcaoWhen: 'Quinta · 09:00', proxAcaoUrgency: '',
    observacoes: 'Família com 2 filhos pequenos. Prioriza segurança e área de lazer. Transferência corporativa de BH.',
    creditScore: 790, creditTier: 'Bom',
    creditFaixa: 'R$ 22k–32k/mês', creditCompromisso: '28% da renda',
    creditDocs: [
      { label: 'RG / CPF', ok: true },
      { label: 'Comprovante de renda', ok: true },
      { label: 'Extrato bancário (3 meses)', ok: false },
      { label: 'Declaração IR 2024', ok: false },
    ],
    messages: [
      { from: 'me', text: 'Tiago, consegui marcar a visita para quinta às 9h no Alphaville.', time: '16:00' },
      { from: 'client', text: 'Ótimo! Minha esposa também vai. Tem área de lazer para crianças?', time: '16:42' },
      { from: 'me', text: 'Tem playground, piscina infantil e campo society. Vou te enviar o catálogo.', time: '17:00' },
    ],
    visits: [
      { date: '29 Mai', time: '09:00', status: 'agendada', agent: 'Fernanda Costa', note: 'Primeira visita com família. Foco em área de lazer e segurança.' },
    ],
    timeline: [
      { kind: 'client', time: 'Hoje · 16:42', label: 'Tiago confirmou visita com a esposa para quinta.' },
      { kind: 'broker', time: 'Hoje · 16:00', label: 'Visita agendada para quinta-feira às 09:00.' },
      { kind: 'system', time: '26 Mai · 10:00', label: 'Lead criado via formulário no site — pesquisa "casa condomínio BH".' },
    ],
  },
  {
    id: 'C05',
    name: 'Mariana Freitas', email: 'mari.freitas@gmail.com',
    phone: '+55 47 99654-1122', seg: 'Imobiliário', cat: 'Apartamento 2Q',
    status: 'won', score: 96, action: 'Pós-venda · NPS', when: 'Sex · 11:00', urgency: '',
    palette: ['#f59e0b', '#ef4444'], initials: 'MF',
    temp: 'hot', value: 520000, progress: 100, last: 'há 2 dias',
    broker: 'Fernanda Costa', renda: 'R$ 12.000/mês', region: 'Balneário Camboriú',
    proxAcao: 'Enviar pesquisa NPS pós-venda',
    proxAcaoWhen: 'Sex · 11:00', proxAcaoUrgency: '',
    observacoes: 'Primeiro imóvel. Muito satisfeita com o atendimento. Potencial para indicar novos clientes.',
    creditScore: 810, creditTier: 'Muito Bom',
    creditFaixa: 'R$ 10k–15k/mês', creditCompromisso: '24% da renda',
    creditDocs: [
      { label: 'RG / CPF', ok: true },
      { label: 'Comprovante de renda', ok: true },
      { label: 'Extrato bancário (6 meses)', ok: true },
      { label: 'Declaração IR 2024', ok: true },
      { label: 'Contrato assinado', ok: true },
    ],
    messages: [
      { from: 'client', text: 'Fernanda, obrigada por tudo! Estou muito feliz com o apartamento!', time: '10:15' },
      { from: 'me', text: 'A alegria é toda nossa, Mariana! Vou te enviar a pesquisa de satisfação.', time: '10:30' },
      { from: 'ia', text: 'Sugestão IA: Mariana tem NPS potencial 10. Peça indicação ativa após o NPS — 63% de chance de referência.', time: '10:32' },
    ],
    visits: [
      { date: '10 Mai', time: '14:00', status: 'realizada', agent: 'Fernanda Costa', note: 'Primeira visita. Gostou muito da planta e da localização.' },
      { date: '15 Mai', time: '10:00', status: 'realizada', agent: 'Fernanda Costa', note: 'Segunda visita com a mãe. Decisão de compra confirmada.' },
    ],
    timeline: [
      { kind: 'client', time: 'Hoje · 10:15', label: 'Mariana enviou mensagem agradecendo o atendimento.' },
      { kind: 'ai', time: 'Hoje · 10:32', label: 'IA identificou Mariana como candidata a referência. Enviar NPS esta semana.' },
      { kind: 'system', time: '24 Mai · 09:00', label: 'Negócio marcado como Ganho. Contrato assinado digitalmente.' },
      { kind: 'broker', time: '15 Mai · 10:00', label: 'Segunda visita realizada. Compra confirmada.' },
    ],
  },
  {
    id: 'C06',
    name: 'Júlia Carvalho', email: 'julia.c@empresa.br',
    phone: '+55 11 98765-1010', seg: 'Crédito', cat: 'Refinanciamento',
    status: 'proposal', score: 81, action: 'Análise documentos', when: 'Hoje · 18:30', urgency: 'due-now',
    palette: ['#22d3ee', '#1F93FF'], initials: 'JC',
    temp: 'hot', value: 620000, progress: 60, last: 'há 6h',
    broker: 'Pedro Alves', renda: 'R$ 18.000/mês', region: 'Campinas · SP',
    proxAcao: 'Análise dos documentos pendentes',
    proxAcaoWhen: 'Hoje · 18:30', proxAcaoUrgency: 'now',
    observacoes: 'Quer reduzir taxa do financiamento atual de 12% para ~8%. Tem imóvel quitado como garantia.',
    creditScore: 755, creditTier: 'Bom',
    creditFaixa: 'R$ 15k–22k/mês', creditCompromisso: '31% da renda',
    creditDocs: [
      { label: 'RG / CPF', ok: true },
      { label: 'Comprovante de renda', ok: true },
      { label: 'Matrícula do imóvel (garantia)', ok: true },
      { label: 'Extrato bancário (6 meses)', ok: false },
      { label: 'Certidão negativa de débitos', ok: false },
    ],
    messages: [
      { from: 'client', text: 'Pedro, consegui a matrícula do imóvel. O extrato ainda não está pronto.', time: '08:00' },
      { from: 'me', text: 'Ótimo, Júlia! O extrato é o que o banco mais pede. Consegue até hoje?', time: '08:30' },
      { from: 'client', text: 'Vou tentar no app do banco agora.', time: '08:45' },
    ],
    visits: [],
    timeline: [
      { kind: 'client', time: 'Hoje · 08:00', label: 'Júlia enviou matrícula do imóvel. Extrato bancário ainda pendente.' },
      { kind: 'broker', time: 'Hoje · 08:30', label: 'Corretor solicitou extrato bancário com urgência.' },
      { kind: 'ai', time: '26 Mai · 15:00', label: 'IA calculou economia de R$ 1.240/mês com refinanciamento a 8,1% a.a.' },
      { kind: 'system', time: '25 Mai · 10:00', label: 'Proposta de refinanciamento gerada automaticamente pelo sistema.' },
    ],
  },
  {
    id: 'C07',
    name: 'Diogo Almeida', email: 'diogo.a@protonmail.com',
    phone: '+55 81 99221-3344', seg: 'Consórcio', cat: 'Imóvel R$ 500k',
    status: 'qualifying', score: 71, action: 'Apresentar carta', when: 'Qua · 15:00', urgency: 'due-soon',
    palette: ['#14b8a6', '#3b82f6'], initials: 'DA',
    temp: 'warm', value: 500000, progress: 35, last: 'há 2 dias',
    broker: 'Ana Paula Silva', renda: 'R$ 9.500/mês', region: 'Recife · PE',
    proxAcao: 'Apresentar carta de consórcio e simulação',
    proxAcaoWhen: 'Qua · 15:00', proxAcaoUrgency: 'soon',
    observacoes: 'Comparando consórcio vs financiamento. Prefere não pagar juros. Tem R$ 80k de entrada disponível.',
    creditScore: 680, creditTier: 'Regular',
    creditFaixa: 'R$ 7k–12k/mês', creditCompromisso: '35% da renda',
    creditDocs: [
      { label: 'RG / CPF', ok: true },
      { label: 'Comprovante de renda', ok: false },
      { label: 'Extrato bancário', ok: false },
    ],
    messages: [
      { from: 'me', text: 'Diogo, consegui um grupo com parcela de R$ 1.200/mês para R$ 500k. Podemos conversar na quarta?', time: '15:00' },
      { from: 'client', text: 'Pode ser. Mas quero entender a diferença real pro financiamento.', time: '15:30' },
      { from: 'ia', text: 'Sugestão IA: Prepare comparativo mostrando economia de ~R$ 280k em juros no consórcio vs financiamento em 20 anos.', time: '15:32' },
    ],
    visits: [],
    timeline: [
      { kind: 'broker', time: 'Hoje · 15:00', label: 'Reunião agendada para quarta-feira com proposta de consórcio.' },
      { kind: 'ai', time: 'Hoje · 15:32', label: 'IA preparou comparativo consórcio vs financiamento.' },
      { kind: 'system', time: '25 Mai · 08:00', label: 'Lead entrou pelo Instagram Ads — "consórcio imóvel sem juros".' },
    ],
  },
  {
    id: 'C08',
    name: 'Bruno Tavares', email: 'bruno.t@gmail.com',
    phone: '+55 51 99887-1122', seg: 'Veículos', cat: 'Pickup 4x4',
    status: 'engaged', score: 67, action: 'Test drive', when: 'Sáb · 10:00', urgency: '',
    palette: ['#f97316', '#eab308'], initials: 'BT',
    temp: 'warm', value: 285000, progress: 42, last: 'há 1 dia',
    broker: 'Pedro Alves', renda: 'R$ 14.000/mês', region: 'Porto Alegre · RS',
    proxAcao: 'Test drive agendado',
    proxAcaoWhen: 'Sáb · 10:00', proxAcaoUrgency: '',
    observacoes: 'Empresário rural. Precisa de pickup robusta para fazenda. Avalia Hilux vs S10 vs Ranger.',
    creditScore: 710, creditTier: 'Bom',
    creditFaixa: 'R$ 10k–18k/mês', creditCompromisso: '19% da renda',
    creditDocs: [
      { label: 'CNH / RG', ok: true },
      { label: 'Comprovante de renda', ok: true },
      { label: 'Extrato bancário', ok: false },
    ],
    messages: [
      { from: 'client', text: 'Pedro, qual pickup tem o melhor torque para terreno acidentado?', time: '11:00' },
      { from: 'me', text: 'Bruno, a Hilux GR-Sport tem torque de 500Nm e 4x4 com diferencial blocável — ideal para fazenda. Test drive no sábado vai te convencer!', time: '11:30' },
      { from: 'client', text: 'Perfeito. Confirmo o sábado às 10h.', time: '12:00' },
    ],
    visits: [
      { date: '31 Mai', time: '10:00', status: 'agendada', agent: 'Pedro Alves', note: 'Test drive Hilux GR-Sport + S10 High Country. Cliente quer comparar os dois.' },
    ],
    timeline: [
      { kind: 'client', time: 'Hoje · 12:00', label: 'Bruno confirmou test drive para sábado às 10h.' },
      { kind: 'broker', time: 'Hoje · 11:30', label: 'Corretor recomendou Hilux GR-Sport como principal opção.' },
      { kind: 'system', time: '26 Mai · 09:00', label: 'Lead entrou via WhatsApp Business — "pickup 4x4 RS".' },
    ],
  },
];

// ── Score engine (compartilhado: clientes-editar, new-lead-modal, kanban) ────
const SCORE_RULES = [
  // Contato básico (30 pts)
  { pts: 10, test: f => !!f.nome?.trim() },
  { pts: 10, test: f => !!f.telefone?.trim() },
  { pts: 10, test: f => !!f.email?.trim() },
  // Dados pessoais (20 pts)
  { pts:  5, test: f => !!f.cpf?.trim() },
  { pts:  4, test: f => !!f.rg?.trim() },
  { pts:  4, test: f => !!f.dataNascimento?.trim() },
  { pts:  4, test: f => !!f.estadoCivil?.trim() },
  { pts:  3, test: f => !!f.sexo?.trim() },
  // Endereço (10 pts)
  { pts:  4, test: f => !!f.cep?.trim() },
  { pts:  3, test: f => !!f.rua?.trim() && !!f.numero?.trim() },
  { pts:  3, test: f => !!f.cidade?.trim() && !!f.estado?.trim() },
  // Profissional / Renda (20 pts)
  { pts:  5, test: f => !!f.profissao?.trim() },
  { pts:  5, test: f => !!f.empresa?.trim() },
  { pts:  7, test: f => !!f.rendaIndividual?.trim() },
  { pts:  3, test: f => !!f.tempoEmpresa?.trim() },
  // Interesse (10 pts)
  { pts:  3, test: f => !!f.categoriaInteresse?.trim() },
  { pts:  3, test: f => !!f.regiaoDesejada?.trim() },
  { pts:  2, test: f => !!f.valorPretendido?.trim() },
  { pts:  2, test: f => !!f.urgencia?.trim() },
  // Crédito (10 pts)
  { pts:  5, test: f => f.statusCredito === 'aprovado' },
  { pts:  3, test: f => !!f.valorAprovado?.trim() && !!f.banco?.trim() },
  { pts:  2, test: f => !!f.prazo?.trim() },
  // Bônus temperatura
  { pts:  5, test: f => f.temp === 'hot' },
  { pts:  2, test: f => f.temp === 'warm' },
  // Bônus docs
  { pts:  5, test: (f, d) => Object.values(d||{}).some(x => x?.status === 'approved') },
  { pts:  3, test: (f, d) => Object.values(d||{}).filter(x => x?.status === 'approved').length >= 3 },
];

function calcScore(form, docs) {
  if (!form) return 0;
  return Math.min(100, Math.round(SCORE_RULES.reduce((sum, r) => sum + (r.test(form, docs) ? r.pts : 0), 0)));
}

// ── Kanban column persistence ──────────────────────────────────────────────────
let _kcCache = { raw: undefined, cols: COLUMNS };
function loadKanbanCols() {
  const userId = window.HOROS_USER?.id || 'default';
  try {
    const saved = localStorage.getItem('horos-kanban-' + userId);
    if (saved === _kcCache.raw) return _kcCache.cols;
    let cols = COLUMNS;
    if (saved) {
      const parsed = JSON.parse(saved);
      if (Array.isArray(parsed) && parsed.length > 0) cols = parsed;
    }
    _kcCache = { raw: saved, cols };
    return cols;
  } catch {}
  return COLUMNS;
}

function _applyKanbanCols(cols) {
  const userId = window.HOROS_USER?.id || 'default';
  try { localStorage.setItem('horos-kanban-' + userId, JSON.stringify(cols)); } catch {}
  cols.forEach((c, i) => { COL_PROGRESS[c.id] = Math.round(i / Math.max(1, cols.length - 1) * 100); });
  window.dispatchEvent(new CustomEvent('horos:columns-updated', { detail: cols }));
}

// Salva local (todas as telas abertas atualizam via 'horos:columns-updated') + no banco
// (profiles.kanban_columns), para as etapas valerem em qualquer tela/dispositivo.
// RPC save_my_kanban/get_my_kanban: assistente vinculada usa (e edita) o funil do corretor dela.
async function saveKanbanCols(cols) {
  _applyKanbanCols(cols);
  const sb = window.HOROS_SB, userId = window.HOROS_USER?.id;
  if (!sb || !userId) return;
  const { error } = await sb.rpc('save_my_kanban', { p_cols: cols });
  if (error) console.warn('[kanban] erro ao salvar etapas:', error.message);
}

// Na abertura de qualquer tela: traz as etapas salvas no banco
(async () => {
  try {
    await window.HOROS_WORKSPACE_PROMISE;
    const sb = window.HOROS_SB, userId = window.HOROS_USER?.id;
    if (!sb || !userId) return;
    const { data: remote } = await sb.rpc('get_my_kanban');
    const local = loadKanbanCols();
    if (Array.isArray(remote) && remote.length > 0) {
      if (JSON.stringify(remote) !== JSON.stringify(local)) _applyKanbanCols(remote);
    } else if (local !== COLUMNS) {
      // Funil personalizado só no navegador → sobe para o banco
      await sb.rpc('save_my_kanban', { p_cols: local });
    }
  } catch (e) { console.warn('[kanban] sync etapas:', e.message); }
})();

// Outra aba salvou o funil → atualiza esta
window.addEventListener('storage', (e) => {
  if (!e.key || !e.key.startsWith('horos-kanban-') || !e.newValue) return;
  try {
    const cols = JSON.parse(e.newValue);
    if (Array.isArray(cols) && cols.length) window.dispatchEvent(new CustomEvent('horos:columns-updated', { detail: cols }));
  } catch {}
});

// ── Vários funis (Kanbans) por pessoa ──────────────────────────────────────────
// Principal = profiles.kanban_columns (loadKanbanCols); extras em profiles.kanban_pipelines.
// Lead: pipeline_id nulo = principal ('main' no front). RPCs get/save_my_pipelines, move_lead_pipeline.
const _plKey = () => 'horos-pipelines-' + (window.HOROS_USER?.id || 'default');
let _plCache = { raw: undefined, val: null };
function _plStored() {
  try {
    const raw = localStorage.getItem(_plKey());
    if (raw === _plCache.raw) return _plCache.val;
    const p = raw ? JSON.parse(raw) : null;
    const val = { mainName: (p && p.mainName) || '', pipelines: Array.isArray(p && p.pipelines) ? p.pipelines : [], entry: (p && p.entry) || null };
    _plCache = { raw, val };
    return val;
  } catch { return { mainName: '', pipelines: [], entry: null }; }
}
function loadPipelines() {
  const s = _plStored();
  return { main: loadKanbanCols(), mainName: s.mainName || 'CRM 1', pipelines: s.pipelines, entry: s.entry };
}
function _applyPipelines(v) {
  try { localStorage.setItem(_plKey(), JSON.stringify({ mainName: v.mainName || '', pipelines: v.pipelines || [], entry: v.entry || null })); } catch {}
  window.dispatchEvent(new CustomEvent('horos:pipelines-updated', { detail: loadPipelines() }));
}
async function savePipelines(pipelines, entry, mainName) {
  const prev = _plStored();
  _applyPipelines({ pipelines, entry: entry === 'main' ? null : entry, mainName });
  const sb = window.HOROS_SB;
  if (!sb) return {};
  const { error } = await sb.rpc('save_my_pipelines', {
    p_pipelines: pipelines, p_entry: entry === 'main' ? null : (entry || null), p_main_name: mainName || null,
  });
  if (error) { _applyPipelines(prev); return { error: error.message }; }
  return {};
}
function usePipelines() {
  const [v, setV] = React.useState(() => loadPipelines());
  React.useEffect(() => {
    const on = () => setV(loadPipelines());
    window.addEventListener('horos:pipelines-updated', on);
    window.addEventListener('horos:columns-updated', on);
    return () => { window.removeEventListener('horos:pipelines-updated', on); window.removeEventListener('horos:columns-updated', on); };
  }, []);
  return v;
}
// 'main' / nulo / funil que não existe (apagado) → principal
function pipelineCols(pid) {
  if (pid && pid !== 'main') {
    const p = _plStored().pipelines.find(x => x.id === pid);
    if (p && Array.isArray(p.cols) && p.cols.length) return p.cols;
  }
  return loadKanbanCols();
}
// Lead visto pelo rodízio (corretor anterior) fica no principal de quem vê
function leadPipeline(lead) {
  const pid = lead && !lead._prevHolder && lead.pipeline_id;
  return pid && _plStored().pipelines.some(p => p.id === pid) ? pid : 'main';
}
function kanbanColTitle(colId, pid) {
  if (!colId) return '';
  const find = (cols) => (cols || []).find(c => c.id === colId);
  const hit = (pid && pid !== 'main' && find(pipelineCols(pid))) || find(loadKanbanCols())
    || _plStored().pipelines.map(p => find(p.cols)).find(Boolean) || find(COLUMNS);
  return (hit && hit.title) || colId;
}
const newPipelineId = () => 'p_' + Array.from({ length: 8 }, () => 'abcdefghijklmnopqrstuvwxyz0123456789'[Math.floor(Math.random() * 36)]).join('');
const defaultPipelineCols = () => COLUMNS.map(c => ({ ...c }));
async function horosMoveLeadsPipeline(ids, pid, col = 'inbox') {
  const sb = window.HOROS_SB;
  if (!sb || !ids || !ids.length) return 0;
  const { data, error } = await sb.rpc('move_lead_pipeline', { p_ids: ids, p_pipeline: pid === 'main' ? null : pid, p_col: col });
  if (error) throw new Error(error.message);
  // O banco devolve só os ids que mudaram (lead de outra pessoa fica de fora)
  const moved = Array.isArray(data) ? data : [];
  const target = pipelineCols(pid);
  const realCol = target.some(c => c.id === col) ? col : 'inbox';
  const pipeline_id = pid === 'main' ? null : pid;
  const set = new Set(moved);
  // lead vendido indo para a Entrada continua vendido (mesma regra do banco)
  const colOf = (l) => (realCol === 'inbox' && l && l.col === 'fechado' && target.some(c => c.id === 'fechado')) ? 'fechado' : realCol;
  const byId = new Map((window.LEADS || []).map(l => [l.id, l]));
  if (Array.isArray(window.LEADS)) window.LEADS = window.LEADS.map(l => set.has(l.id) ? { ...l, pipeline_id, col: colOf(l) } : l);
  moved.forEach(id => window.dispatchEvent(new CustomEvent('horos:lead-updated', { detail: { id, col: colOf(byId.get(id)), pipeline_id, updated_at: new Date().toISOString() } })));
  return moved.length;
}
// Na abertura: traz os funis do banco
(async () => {
  try {
    await window.HOROS_WORKSPACE_PROMISE;
    const sb = window.HOROS_SB;
    if (!sb || !window.HOROS_USER?.id) return;
    const { data } = await sb.rpc('get_my_pipelines');
    if (!data) return;
    const next = { mainName: data.main_name || '', pipelines: Array.isArray(data.pipelines) ? data.pipelines : [], entry: data.entry || null };
    const cur = _plStored();
    if (JSON.stringify(next) !== JSON.stringify({ mainName: cur.mainName, pipelines: cur.pipelines, entry: cur.entry })) _applyPipelines(next);
  } catch (e) { console.warn('[kanban] sync funis:', e.message); }
})();
window.addEventListener('storage', (e) => {
  if (e.key === _plKey()) window.dispatchEvent(new CustomEvent('horos:pipelines-updated', { detail: loadPipelines() }));
});

// ── Texto da IA formatado (chat da IA, janelinha da IA, Assistente do Dashboard, relatório IA) ──
// **negrito**, *itálico*, `código`, links, "# Título", listas "- " / "• " / "* " e "1." (numeradas),
// linha vazia = novo parágrafo, quebra simples = nova linha. CSS .hrt-* em styles.css.
// Nome de cliente citado pela IA (refs = [{ id, name }] do broker-chat) vira link para a ficha
function _hrtNames(s, refs, kp) {
  if (!refs || !refs.length || !s) return s;
  const esc = x => x.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  const re = new RegExp(`(?<![\\p{L}\\p{N}])(${refs.map(r => esc(r.name)).sort((a, b) => b.length - a.length).join('|')})(?![\\p{L}\\p{N}])`, 'giu');
  const parts = String(s).split(re);
  if (parts.length === 1) return s;
  return parts.map((p, i) => {
    if (i % 2 === 0) return p;
    const ref = refs.find(r => r.name.toLowerCase() === p.toLowerCase());
    if (!ref) return p;
    return <a key={`${kp}-n${i}`} className="hrt-lead" href={`client.html?id=${encodeURIComponent(ref.id)}`}
      onClick={e => { if (window.horosOpenLeadSheet) { e.preventDefault(); window.horosOpenLeadSheet(ref.id); } }}>{p}</a>;
  });
}

function _hrtInline(s, kp, refs) {
  const out = [];
  const re = /(\*\*[^*\n]+?\*\*|__[^_\n]+?__|`[^`\n]+`|(?<![\w*])\*[^*\s][^*\n]*?\*(?![\w*])|https?:\/\/[^\s<>()]+[^\s<>().,;:!?"'])/g;
  let last = 0, m, i = 0;
  const str = String(s);
  while ((m = re.exec(str))) {
    if (m.index > last) out.push(_hrtNames(str.slice(last, m.index), refs, `${kp}-t${i}`));
    const t = m[0], k = `${kp}-${i++}`;
    if (t.startsWith('**') || t.startsWith('__')) out.push(<strong key={k}>{_hrtInline(t.slice(2, -2), k, refs)}</strong>);
    else if (t.startsWith('`')) out.push(<code key={k} className="hrt-code">{t.slice(1, -1)}</code>);
    else if (t.startsWith('*')) out.push(<em key={k}>{_hrtNames(t.slice(1, -1), refs, k)}</em>);
    else out.push(<a key={k} href={t} target="_blank" rel="noopener noreferrer">{t.replace(/^https?:\/\/(www\.)?/, '').slice(0, 48)}</a>);
    last = m.index + t.length;
  }
  if (last < str.length) out.push(_hrtNames(str.slice(last), refs, `${kp}-end`));
  return out;
}

function HorosRichText({ text, className, refs }) {
  const blocks = [];
  String(text || '').replace(/\r/g, '').split('\n').forEach(raw => {
    const line = raw.trim();
    const last = blocks[blocks.length - 1];
    if (!line) { if (last) last.closed = true; return; }
    if (/^([-*_])\1{2,}$/.test(line)) { blocks.push({ type: 'hr' }); return; }
    const h = line.match(/^#{1,4}\s+(.*)$/) || line.match(/^\*\*([^*]+)\*\*:?$/);
    if (h) { blocks.push({ type: 'h', text: h[1].replace(/:$/, '') }); return; }
    const ul = line.match(/^[-•*]\s+(.*)$/);
    const ol = line.match(/^(\d+)[.)]\s+(.*)$/);
    if (ul || ol) {
      const type = ul ? 'ul' : 'ol';
      const item = ul ? ul[1] : ol[2];
      if (last && last.type === type) { last.items.push(item); last.closed = false; }
      else blocks.push({ type, items: [item], start: ol ? parseInt(ol[1], 10) : 1 });
      return;
    }
    // linha comum logo abaixo de um item de lista (sem linha vazia) continua o item
    if (last && (last.type === 'ul' || last.type === 'ol') && !last.closed) {
      last.items[last.items.length - 1] += '\n' + line; return;
    }
    if (last && last.type === 'p' && !last.closed) last.lines.push(line);
    else blocks.push({ type: 'p', lines: [line] });
  });
  const lines = (s, k) => String(s).split('\n').map((l, j) => <React.Fragment key={j}>{j > 0 && <br />}{_hrtInline(l, `${k}-${j}`, refs)}</React.Fragment>);
  return (
    <div className={`hrt${className ? ' ' + className : ''}`}>
      {blocks.map((b, i) => {
        if (b.type === 'hr') return <hr key={i} className="hrt-hr" />;
        if (b.type === 'h')  return <div key={i} className="hrt-h">{_hrtInline(b.text, `h${i}`, refs)}</div>;
        if (b.type === 'ul') return <ul key={i} className="hrt-ul">{b.items.map((it, j) => <li key={j}>{lines(it, `u${i}-${j}`)}</li>)}</ul>;
        if (b.type === 'ol') return <ol key={i} className="hrt-ol" start={b.start} style={{ '--hrt-start': b.start - 1 }}>{b.items.map((it, j) => <li key={j}>{lines(it, `o${i}-${j}`)}</li>)}</ol>;
        return <p key={i} className="hrt-p">{lines(b.lines.join('\n'), `p${i}`)}</p>;
      })}
    </div>
  );
}
window.HorosRichText = HorosRichText;

// Hook: etapas atuais do funil, sempre atualizadas
// Plano individual (Pro/Max) → sem recursos de equipe. Atualiza quando o workspace chega (auth-guard.js)
function useHorosSolo() {
  const [solo, setSolo] = React.useState(() => !!(window.horosIsSolo && window.horosIsSolo()));
  React.useEffect(() => {
    const on = () => setSolo(!!(window.horosIsSolo && window.horosIsSolo()));
    window.addEventListener('horos:plan-scope', on);
    on();
    return () => window.removeEventListener('horos:plan-scope', on);
  }, []);
  return solo;
}
window.useHorosSolo = useHorosSolo;
// Plano Distribuição (auth-guard.js horosIsDist): só Leads recebidos, Equipe, Integrações e Configurações
function useHorosDist() {
  const [dist, setDist] = React.useState(() => !!(window.horosIsDist && window.horosIsDist()));
  React.useEffect(() => {
    const on = () => setDist(!!(window.horosIsDist && window.horosIsDist()));
    window.addEventListener('horos:plan-scope', on);
    on();
    return () => window.removeEventListener('horos:plan-scope', on);
  }, []);
  return dist;
}
window.useHorosDist = useHorosDist;

// ── Recursos por plano (migration 20261006_plan_features.sql) ────────────────
// tier 'max' = Max, Enterprise, assento Premium do Team e isentos → copiloto sem limite, automações,
// disparos em massa, relatório com IA e relatórios rápidos. tier 'pro' = Pro e assento Standard
// (copiloto com 50 sugestões de teste). O servidor também confere (triggers + APIs); aqui é só a tela.
// Enquanto não carregou, vale o último resultado guardado; sem nada guardado, libera (não pisca bloqueio).
let _planFetch = null;
const _planKey = () => 'horos-plan-features:' + (window.HOROS_USER?.id || '');
function horosPlan() {
  if (window.HOROS_PLAN) return window.HOROS_PLAN;
  try { const c = JSON.parse(localStorage.getItem(_planKey()) || 'null'); if (c) return c; } catch {}
  return { tier: 'max', pending: true };
}
function horosLoadPlan(force) {
  if (_planFetch && !force) return _planFetch;
  const sb = window.HOROS_SB;
  if (!sb) return Promise.resolve(horosPlan());
  _planFetch = sb.rpc('my_plan_features').then(({ data, error }) => {
    if (error || !data) { _planFetch = null; return horosPlan(); }
    window.HOROS_PLAN = data;
    try { localStorage.setItem(_planKey(), JSON.stringify(data)); } catch {}
    window.dispatchEvent(new CustomEvent('horos:plan-features', { detail: data }));
    return data;
  }).catch(() => { _planFetch = null; return horosPlan(); });
  return _planFetch;
}
function useHorosPlan() {
  const [plan, setPlan] = React.useState(horosPlan);
  React.useEffect(() => {
    const on = (e) => setPlan(e.detail || horosPlan());
    window.addEventListener('horos:plan-features', on);
    horosLoadPlan();
    return () => window.removeEventListener('horos:plan-features', on);
  }, []);
  return plan;
}
const horosIsMax = (p) => (p || horosPlan()).tier !== 'pro';
// Para onde mandar quem quer liberar: quem cuida da assinatura escolhe o plano; no Team, assento Premium
function horosUpgradeInfo(p) {
  p = p || horosPlan();
  if (p.plan === 'team') {
    return p.can_assign
      ? { text: 'Dê um assento Premium para quem vai usar (tela Equipe) ou compre mais assentos Premium na Assinatura.', cta: 'Abrir Equipe', href: 'team.html' }
      : { text: 'Seu assento é Standard. Peça um assento Premium a quem cuida da assinatura da empresa.', cta: null, href: null };
  }
  return { text: 'Faça upgrade para o plano Max para liberar.', cta: 'Ver o plano Max', href: 'profile.html?aba=assinatura&upgrade=1' };
}
// Aviso padrão de recurso do Max (design system: cartão neutro com traço azul)
function HorosPlanLock({ title, desc, compact }) {
  const p = useHorosPlan();
  const up = horosUpgradeInfo(p);
  return (
    <div className={`hpl${compact ? ' hpl-compact' : ''}`}>
      <span className="hpl-ic"><Icon name="lock" size={compact ? 15 : 18} /></span>
      <div className="hpl-txt">
        <b>{title || 'Disponível no plano Max'}</b>
        <span>{desc ? desc + ' ' : ''}{up.text}</span>
      </div>
      {up.cta && <a className="hpl-btn" href={up.href}>{up.cta}</a>}
    </div>
  );
}
// Selo "Max" com cadeado: no Pro o recurso continua visível, avisando que precisa de upgrade
function HorosMaxTag() {
  if (horosIsMax(useHorosPlan())) return null;
  return <span className="hmx-tag" title="Disponível no plano Max"><Icon name="lock" size={10} stroke={2.2} />Max</span>;
}
// Tela trancada: o pai recebe a classe "hpg-on" (conteúdo esmaecido, sem clique) e este cartão fica por cima
function HorosPlanOverlay({ title, desc }) {
  return <div className="hpg-over"><HorosPlanLock title={title} desc={desc} /></div>;
}
Object.assign(window, { horosPlan, horosLoadPlan, useHorosPlan, horosIsMax, horosUpgradeInfo, HorosPlanLock, HorosMaxTag, HorosPlanOverlay });

// Ramo de atuação da empresa (workspaces.segment) — muda o texto das IAs. Cache em localStorage('horos-ai-segment'),
// confere no banco 1× por carregamento (get_workspace_ai); mudança dispara 'horos:segment'.
let _segFetch = null;
function horosSegment() { try { return localStorage.getItem('horos-ai-segment') || 'imobiliario'; } catch { return 'imobiliario'; } }
function horosSetSegment(seg) {
  try { localStorage.setItem('horos-ai-segment', seg); } catch {}
  window.dispatchEvent(new CustomEvent('horos:segment', { detail: seg }));
}
function useHorosSegment() {
  const [seg, setSeg] = React.useState(horosSegment);
  React.useEffect(() => {
    const on = (e) => setSeg(e.detail || horosSegment());
    window.addEventListener('horos:segment', on);
    if (!_segFetch && window.HOROS_SB) {
      _segFetch = window.HOROS_SB.rpc('get_workspace_ai').then(({ data }) => {
        if (data?.segment && data.segment !== horosSegment()) horosSetSegment(data.segment);
      }).catch(() => { _segFetch = null; });
    }
    return () => window.removeEventListener('horos:segment', on);
  }, []);
  return seg;
}
Object.assign(window, { horosSegment, horosSetSegment, useHorosSegment });

// pid = funil ('main'/sem argumento = principal)
function useKanbanCols(pid) {
  const [cols, setCols] = React.useState(() => pipelineCols(pid));
  React.useEffect(() => {
    setCols(pipelineCols(pid));
    const on = () => setCols(pipelineCols(pid));
    window.addEventListener('horos:columns-updated', on);
    window.addEventListener('horos:pipelines-updated', on);
    return () => { window.removeEventListener('horos:columns-updated', on); window.removeEventListener('horos:pipelines-updated', on); };
  }, [pid]);
  return cols;
}

// Colunas que funcionam como "arquivo" dentro do Kanban
const ARCHIVE_COLS = new Set(['arquivados', 'arquivados_atend', 'arquivado_aprovado']);

// Lead arquivado não mostra nem soma valores (R$, em negociação) — pedido do usuário 2026-10-03.
// Arquivo = etapas fixas acima OU etapa criada pelo usuário com nome "Arquiv…" (mesma regra do banco: lead_is_archive_col)
let _archColsMemo = { cols: null, set: null };
// Etapas de arquivo de TODA a imobiliária (RPC workspace_archive_cols): um lead pode estar numa etapa
// que só existe no funil de outro corretor (ex.: gestor moveu para o "Arquivado" dele)
let _wsArchCols = (() => { try { return new Set(JSON.parse(localStorage.getItem('horos-ws-archive-cols') || '[]')); } catch { return new Set(); } })();
(async function _loadWsArchCols() {
  for (let i = 0; i < 40 && !window.HOROS_SB; i++) await new Promise(r => setTimeout(r, 250));
  if (!window.HOROS_SB) return;
  const { data } = await window.HOROS_SB.rpc('workspace_archive_cols').then(r => r, () => ({}));
  if (Array.isArray(data)) {
    _wsArchCols = new Set(data);
    try { localStorage.setItem('horos-ws-archive-cols', JSON.stringify(data)); } catch {}
  }
})();
function isArchivedCol(col) {
  if (!col) return false;
  if (ARCHIVE_COLS.has(col) || _wsArchCols.has(col)) return true;
  try {
    const cols = loadKanbanCols() || [], pls = _plStored().pipelines;
    if (_archColsMemo.cols !== cols || _archColsMemo.pls !== pls) {
      const all = cols.concat(...pls.map(p => p.cols || []));
      _archColsMemo = { cols, pls, set: new Set(all.filter(c => /^\s*arquiv/i.test(c.title || '')).map(c => c.id)) };
    }
    return _archColsMemo.set.has(col);
  } catch { return false; }
}
const isArchivedLead = (l) => !!(l && (l.archived || isArchivedCol(l.col)));
// valor que pode aparecer/somar: 0 para arquivado
const leadMoney = (l) => (isArchivedLead(l) ? 0 : (Number(l && l.value) || 0));

// ── Rodízio: etapa de cada corretor (lead_assignments) ──────────────────────
// Corretor: leads que já passaram por ele aparecem na etapa DELE (+ "Agora com X").
// Gerente+: cada lead aparece uma vez, com a etapa de cada corretor que já o teve.
const MANAGER_ROLES = new Set(['ceo', 'diretor', 'super', 'gerente']);
async function _rpcAll(fn) {
  const sb = window.HOROS_SB, PAGE = 1000;
  let all = [];
  for (let p = 0; p < 100; p++) {
    const { data, error } = await sb.rpc(fn).range(p * PAGE, (p + 1) * PAGE - 1);
    if (error || !data) break;
    all = all.concat(data);
    if (data.length < PAGE) break;
  }
  return all;
}
async function enrichLeadStages(leads) {
  if (!window.HOROS_SB || !Array.isArray(leads) || !leads.length) return leads;
  const ws = window.HOROS_WORKSPACE || await window.HOROS_WORKSPACE_PROMISE;
  // lead_role: gestor que também atua como corretor vê os leads como corretor
  const role = ws?.lead_role || ws?.role || 'corretor';
  try {
    if (MANAGER_ROLES.has(role)) {
      const rows = await _rpcAll('lead_stage_breakdown');
      if (!rows.length) return leads;
      const map = new Map(rows.map(r => [r.lead_id, r.stages]));
      return leads.map(l => map.has(l.id) ? { ...l, stages: map.get(l.id) } : l);
    }
    const rows = await _rpcAll('my_lead_stages');
    if (!rows.length) return leads;
    const map = new Map(rows.map(r => [r.lead_id, r]));
    return leads.map(l => {
      const r = map.get(l.id);
      return r ? { ...l, col: r.col, pipeline_id: null, _prevHolder: r.holder_name || `outro ${segTerm('agent', 'corretor')}` } : l;
    });
  } catch (e) {
    console.warn('[stages]', e.message);
    return leads;
  }
}

// Card do Kanban: etapa de cada corretor (gerente) ou "Agora com X" (corretor anterior)
function LeadStages({ lead }) {
  if (lead._prevHolder) {
    return (
      <div className="lead-stages-prev" title={`Este lead passou para outro ${segTerm('agent', 'corretor')}; aqui fica a sua etapa`}>
        <Icon name="user" size={10} stroke={2} /> Agora com {lead._prevHolder.split(' ')[0]}
      </div>
    );
  }
  const st = (lead.stages || []).filter(s => !s.archived);
  if (st.length < 2) return null;
  const cols = loadKanbanCols();
  const title = (c) => cols.find(k => k.id === c)?.title || c;
  return (
    <div className="lead-stages" onClick={e => e.stopPropagation()}>
      <div className="lead-stages-head">Etapa por {segTerm('agent', 'corretor')}</div>
      {st.map(s => (
        <div key={s.user_id} className={`lead-stages-row${s.current ? ' current' : ''}`} title={s.current ? 'Responsável atual' : ''}>
          <span className="lead-stages-name">{(s.name || segTerm('Agent', 'Corretor')).split(' ')[0]}</span>
          <span className="lead-stages-col">{title(s.col)}</span>
        </div>
      ))}
    </div>
  );
}

// ── Card do Kanban (visual "kanban-board": chips · título · nota · divisória · rodapé) ──
// Usado pelos dois Kanbans (Dashboard e CRM). Etiquetas do WhatsApp, documentos e etapa
// por corretor continuam no card. `footRight` troca o avatar do lead (ex.: botão de responsável no CRM).
const KBX_TEMP = {
  hot:  { label: 'Quente', icon: 'flame' },
  warm: { label: 'Morno',  icon: 'drop' },
  cold: { label: 'Frio',   icon: 'snow' },
};

function KbxRing({ value }) {
  const v = Math.max(0, Math.min(100, Math.round(Number(value) || 0)));
  const r = 6, c = 2 * Math.PI * r;
  return (
    <span className={`kbx-ring${v >= 100 ? ' done' : ''}`} title={`${v}% do funil`}>
      <svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true">
        <circle cx="8" cy="8" r={r} fill="none" strokeWidth="2" className="kbx-ring-bg" />
        <circle cx="8" cy="8" r={r} fill="none" strokeWidth="2" strokeLinecap="round" className="kbx-ring-fg"
          strokeDasharray={c} strokeDashoffset={c - (c * v) / 100} />
      </svg>
      {v}%
    </span>
  );
}

function KbxAvatar({ lead }) {
  const pic = lead.pic || lead.profilePic || (lead.data && lead.data.profilePic);
  const [broken, setBroken] = React.useState(false);
  return (
    <span className="kbx-avatar" title={lead.name}>
      {pic && !broken
        ? <img src={pic} alt="" draggable={false} onError={() => setBroken(true)} />
        : <span>{lead.initials || initials(lead.name || '?')}</span>}
    </span>
  );
}

// Selo de origem — aparece para qualquer corretor que tenha o lead (inclusive depois de repassado).
// origem = leads.data.origemLead (ou lead.origem da carga leve); canal 'whatsapp' = anúncio que abriu conversa.
// 2026-10-11 (pedido do usuário): vale para TODAS as integrações, não só o Meta. Mesmos nomes de
// lead_origin_label() no banco (usado nos avisos de repasse).
const LEAD_ORIGINS = {
  meta_ads:    { label: 'Meta Ads',          logo: 'logos/meta.svg',            integ: true, cls: 'meta' },
  google_ads:  { label: 'Google Ads',        logo: 'logos/google-ads.svg',      integ: true },
  tiktok_ads:  { label: 'TikTok Ads',        logo: 'logos/tiktok.svg',          integ: true },
  formulario:  { label: 'Formulário do site', icon: 'sheet',                    integ: true },
  site:        { label: 'Site',              icon: 'link',                      integ: true },
  api:         { label: 'API',               logo: 'favicon.svg',               integ: true },
  zapier:      { label: 'Zapier',            logo: 'logos/zapier.png',          integ: true },
  make:        { label: 'Make',              logo: 'logos/make.png',            integ: true },
  zap:         { label: 'ZAP Imóveis',       logo: 'logos/zap.png',             integ: true },
  vivareal:    { label: 'VivaReal',          logo: 'logos/vivareal.png',        integ: true },
  imovelweb:   { label: 'Imovelweb',         logo: 'logos/imovelweb.png',       integ: true },
  chavesnamao: { label: 'Chaves na Mão',     logo: 'logos/chavesnamao.png',     integ: true },
  webmotors:   { label: 'WebMotors',         icon: 'link',                      integ: true },
  whatsapp:    { label: 'WhatsApp',          logo: 'logos/whatsapp.svg' },
  importacao:  { label: 'Importação',        icon: 'sheet' },
  campanha:    { label: 'Campanha',          icon: 'target' },
  lista:       { label: 'Lista',             icon: 'layers' },
  conhecido:   { label: 'Conhecido',         icon: 'user' },
  indicacao:   { label: 'Indicação',         icon: 'userPlus' },
  feirao:      { label: 'Feirão / Eventos',  icon: 'cal' },
  acao_rua:    { label: 'Ação de rua',       icon: 'mapPin' },
  imagem_ia:   { label: 'Imagem IA',         icon: 'sparkles' },
  outros:      { label: 'Outros',            icon: 'layers' },
  manual:      { label: 'Cadastro manual',   icon: 'userPlus' },
};
function leadOriginOf(lead) {
  if (!lead) return null;
  return lead.origem || (lead.data && lead.data.origemLead) || null;
}
function leadOriginInfo(o) {
  const key = o || 'manual';
  // No imobiliário "Lista" e "Importação" são a mesma coisa (planilha/lista de contatos) — um nome só
  if ((key === 'importacao' || key === 'lista') && horosSegment() === 'imobiliario') return { ...LEAD_ORIGINS.lista, label: 'Lista' };
  return LEAD_ORIGINS[key] || { label: String(key).replace(/_/g, ' ').replace(/^./, c => c.toUpperCase()), icon: 'layers' };
}
// Origens que a pessoa pode escolher à mão (Novo lead, ficha). As de integração (Meta, Google, site,
// formulário, API, portais…) só entram sozinhas, quando o lead chega por elas — escolher à mão
// fazia aparecer "Site" no relatório de quem nunca conectou site (2026-10-14).
function leadOriginOptions(current) {
  const imob = horosSegment() === 'imobiliario';
  const keys = ['whatsapp', imob ? 'lista' : 'importacao', 'campanha', 'conhecido', 'indicacao', 'feirao', 'acao_rua', 'outros'];
  const opts = keys.map(k => ({ value: k, label: k === 'importacao' ? 'Importação de planilha' : leadOriginInfo(k).label }));
  const cur = current || '';
  if (cur && !opts.some(o => o.value === cur)) {
    const info = leadOriginInfo(cur);
    const label = cur === 'importacao' && imob ? 'Lista (importada)' : info.integ ? `${info.label} (automático)` : info.label;
    opts.unshift({ value: cur, label });
  }
  return opts;
}
// Nome da origem no relatório: o mesmo do resto do app. Sem origem = criado no WhatsApp (id "WA…") ou cadastro manual
function leadOriginReportName(raw, leadId) {
  const key = raw || (String(leadId || '').startsWith('WA') ? 'whatsapp' : 'manual');
  return leadOriginInfo(key).label;
}
function leadHandoffOf(lead) {
  if (!lead) return null;
  const r = lead.repasse || (lead.data && lead.data.repasse);
  return r && typeof r === 'object' ? r : null;
}
const LEAD_HANDOFF_MODE = { rodizio: 'pela fila da equipe', transferencia: 'repassado', automatico: 'automático' };

// compact = card do Kanban: mostra integrações sempre e as demais origens só se o lead foi repassado
function LeadOriginBadge({ lead, origem, compact }) {
  const o = origem || leadOriginOf(lead);
  const info = leadOriginInfo(o);
  const handoff = leadHandoffOf(lead);
  if (compact && !info.integ && !handoff) return null;
  if (!compact && !o && !handoff) return null;
  const d = (lead && lead.data) || {};
  const canal = (lead && lead.origem_canal) || d.origemCanal;
  let extra = '', title = `Origem: ${info.label}`;
  if (o === 'meta_ads') {
    const via = canal === 'whatsapp' ? 'pelo WhatsApp' : d.metaFormId || d.metaLeadId ? 'formulário' : '';
    const app = d.metaAd && d.metaAd.app ? (d.metaAd.app === 'instagram' ? 'Instagram' : d.metaAd.app === 'facebook' ? 'Facebook' : '') : '';
    extra = app || (canal === 'whatsapp' ? 'WhatsApp' : '');
    title = ['Lead do Meta Ads', app, via, d.metaAd && d.metaAd.title ? `anúncio "${d.metaAd.title}"` : ''].filter(Boolean).join(' · ');
  } else if (o === 'google_ads' && d.googleTest) {
    extra = 'teste';
  }
  if (handoff) {
    const who = handoff.porNome || handoff.deNome;
    title += ` · ${handoff.modo === 'rodizio' ? 'distribuído pela fila' : 'repassado'}${who ? ` por ${who}` : ''}`;
  }
  return (
    <span className={`lead-origin ${info.cls || (info.integ ? 'integ' : 'plain')}${compact ? ' compact' : ''}`} title={title}>
      {o === 'meta_ads'
        ? <svg viewBox="0 0 24 24" width="12" height="12" aria-hidden="true"><path fill="currentColor" d="M6.9 6.2c-2.6 0-4.4 3.3-4.4 6.5 0 2.3 1 4 2.9 4 1.5 0 2.6-1 4.2-3.6l1.5-2.4 1.1 1.8c1.8 3 3 4.2 4.9 4.2 2.2 0 3.4-1.9 3.4-4.6 0-3.6-1.9-6-4.3-6-1.5 0-2.7.9-4 2.7-1.3-1.8-2.4-2.6-5.3-2.6Zm-.2 1.9c1.4 0 2.1.6 3.6 2.6l-1.3 2c-1.5 2.4-2.1 3-3 3s-1.5-.9-1.5-2.5c0-2.6 1-5.1 2.2-5.1Zm9.7.1c1.6 0 2.6 2 2.6 4.4 0 1.6-.6 2.6-1.6 2.6s-1.6-.7-3.1-3.2l-1-1.6c1.2-1.6 2-2.2 3.1-2.2Z"/></svg>
        : info.logo ? <img src={info.logo} alt="" width="12" height="12" />
        : <Icon name={info.icon || 'layers'} size={12} stroke={1.9} />}
      {info.label}{!compact && extra ? ` · ${extra}` : ''}
    </span>
  );
}

// "Veio de onde" do repasse: Horos 360, painel de Mensagens e ficha.
// Ex.: "Distribuído pela fila da equipe Imob · antes com Wanderson Cruz · 10/10 14:32"
function LeadHandoffNote({ lead, className }) {
  const r = leadHandoffOf(lead);
  if (!r) return null;
  const when = r.em ? new Date(r.em).toLocaleString('pt-BR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }) : '';
  const head = r.modo === 'rodizio'
    ? `Distribuído pela fila${r.equipe ? ` da equipe ${r.equipe}` : ''}`
    : r.modo === 'automatico' ? 'Repassado automaticamente'
    : `Repassado${r.porNome ? ` por ${r.porNome}` : ''}`;
  const parts = [head];
  if (r.deNome && r.deNome !== r.porNome) parts.push(`antes com ${r.deNome}`);
  if (r.paraNome) parts.push(`para ${r.paraNome}`);
  if (when) parts.push(when);
  return (
    <span className={`lead-handoff${className ? ' ' + className : ''}`} title={parts.join(' · ')}>
      <Icon name="arrRightS" size={12} stroke={2} />{parts.join(' · ')}
    </span>
  );
}
// Origem + repasse completos de um lead (o lead que chega na tela pode vir da carga leve ou de uma
// conversa, sem esses campos). Lê só os 4 campos do banco e junta no objeto; recarrega em horos:lead-updated.
function useLeadOrigin(lead) {
  const id = lead && lead.id;
  const [extra, setExtra] = React.useState(null);
  React.useEffect(() => {
    if (!id || !window.HOROS_SB) { setExtra(null); return; }
    let alive = true;
    const load = () => window.HOROS_SB.from('leads')
      .select('origem:data->>origemLead,origem_canal:data->>origemCanal,repasse:data->repasse,metaAd:data->metaAd')
      .eq('id', id).maybeSingle()
      .then(({ data }) => { if (alive && data) setExtra(data); }, () => {});
    load();
    const on = (e) => { if (!e.detail || e.detail.id === id) load(); };
    window.addEventListener('horos:lead-updated', on);
    return () => { alive = false; window.removeEventListener('horos:lead-updated', on); };
  }, [id]);
  if (!lead || !extra) return lead;
  return { ...lead, origem: extra.origem || lead.origem, origem_canal: extra.origem_canal || lead.origem_canal,
    repasse: extra.repasse || lead.repasse,
    data: { ...(lead.data || {}), ...(extra.metaAd ? { metaAd: extra.metaAd } : {}), ...(extra.origem_canal ? { origemCanal: extra.origem_canal } : {}) } };
}
window.useLeadOrigin = useLeadOrigin;
window.LeadOriginBadge = LeadOriginBadge;
window.LeadHandoffNote = LeadHandoffNote;
window.leadOriginOf = leadOriginOf;
window.leadOriginInfo = leadOriginInfo;
window.leadOriginOptions = leadOriginOptions;
window.leadOriginReportName = leadOriginReportName;
window.leadHandoffOf = leadHandoffOf;

// Tempo desde `ts`: menos de 24 h → em horas (ou minutos); depois → dias. Pedido do usuário 2026-10-03.
function kbSince(ts) {
  if (!ts) return null;
  const min = Math.max(0, Math.floor((Date.now() - new Date(ts)) / 60000));
  if (min < 60) return { short: min < 1 ? 'agora' : `${min} min`, hours: true, days: 0 };
  if (min < 1440) { const h = Math.floor(min / 60); return { short: `${h} h`, hours: true, days: 0 }; }
  const d = Math.floor(min / 1440);
  return { short: `${d}d`, hours: false, days: d };
}
const kbDate = (ts) => {
  if (!ts) return '';
  const s = kbSince(ts);
  if (s.hours) return s.short === 'agora' ? 'agora' : `há ${s.short}`;
  return new Date(ts).toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit', year: 'numeric' });
};

// Botão do WhatsApp no card do Kanban: abre uma janelinha, escreve e envia pelo WhatsApp do Horos
function KbWaQuick({ lead }) {
  const [open, setOpen] = React.useState(false);
  const [text, setText] = React.useState('');
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState('');
  const first = String(lead.name || '').trim().split(/\s+/)[0] || '';
  const [qrs, setQrs] = React.useState(null);
  const [qrFiles, setQrFiles] = React.useState([]);   // anexos da resposta rápida escolhida
  React.useEffect(() => {
    if (!open || qrs) return;
    window.HOROS_SB.from('quick_replies').select('*').order('created_at').then(({ data }) => setQrs(data || []), () => setQrs([]));
  }, [open]);
  const pickQr = async (r) => {
    setErr('');
    setText(String(r.message || '').replace(/\{primeiro_nome\}/g, first).replace(/\{nome\}/g, lead.name || ''));
    const media = Array.isArray(r.media) ? r.media : [];
    try {
      const files = [];
      for (const m of media) {
        const { data, error } = await window.HOROS_SB.storage.from('quick-replies').download(m.path);
        if (error || !data) throw new Error(m.fileName || 'arquivo');
        const f = new File([data], m.fileName || 'arquivo', { type: m.mimetype || data.type || 'application/octet-stream' });
        f.__qrRef = { bucket: 'quick-replies', path: m.path };
        files.push(f);
      }
      setQrFiles(files);
    } catch (e) { setQrFiles([]); setErr('Não foi possível carregar o anexo: ' + e.message); }
  };
  const send = async () => {
    if ((!text.trim() && !qrFiles.length) || busy) return;
    setBusy(true); setErr('');
    const e = await horosSendWaText(lead.phone, lead.name, text.trim(), qrFiles);
    setBusy(false);
    if (e) { setErr(e); return; }
    setOpen(false); setText(''); setQrFiles([]);
    window.dispatchEvent(new CustomEvent('horos:toast', { detail: { title: 'Mensagem enviada', body: lead.name } }));
    window.dispatchEvent(new CustomEvent('horos:tour-check', { detail: { step: 'message' } }));
  };
  return (
    <>
      <button type="button" className="kbx-wa" title="Enviar mensagem no WhatsApp" disabled={!lead.phone}
        onMouseDown={e => e.stopPropagation()} onClick={e => { e.stopPropagation(); setOpen(true); }}>
        <img src="logos/whatsapp.svg" alt="" width="15" height="15" /><span>Mensagem</span>
      </button>
      {open && ReactDOM.createPortal(
        <div className="arq-ov" style={{ zIndex: 10001 }} onMouseDown={e => { e.stopPropagation(); if (e.target === e.currentTarget && !busy) setOpen(false); }} onClick={e => e.stopPropagation()}>
          <div className="arq-modal" role="dialog" aria-label="Mensagem no WhatsApp">
            <div className="arq-head">
              <span className="arq-ic"><img src="logos/whatsapp.svg" alt="" width="22" height="22" /></span>
              <div style={{ minWidth: 0 }}><h3>Mensagem para {first || 'o lead'}</h3><p>{fmtPhone(lead.phone)}</p></div>
              <button className="arq-x" onClick={() => setOpen(false)} aria-label="Fechar"><Icon name="close" size={16} /></button>
            </div>
            {qrs && qrs.length > 0 && (
              <div className="kbx-wa-qr" aria-label="Respostas rápidas">
                {qrs.map(r => <button key={r.id} type="button" className="kbx-wa-chip" title={r.message} onClick={() => pickQr(r)}>/{r.shortcut}</button>)}
              </div>
            )}
            {qrFiles.length > 0 && <div className="kbx-wa-files">{qrFiles.length} {qrFiles.length === 1 ? 'anexo' : 'anexos'}: {qrFiles.map(f => f.name).join(', ')}
              <button type="button" onClick={() => setQrFiles([])} aria-label="Tirar anexos">×</button></div>}
            <label className="arq-note">
              <textarea autoFocus rows={4} maxLength={2000} value={text} placeholder={`Olá ${first}, tudo bem?`}
                onChange={e => setText(e.target.value)}
                onKeyDown={e => { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) send(); }} />
            </label>
            {err && <div className="arq-err">{err}</div>}
            <div className="arq-foot">
              <button className="arq-btn ghost" onClick={() => setOpen(false)} disabled={busy}>Cancelar</button>
              <button className="arq-btn" onClick={send} disabled={(!text.trim() && !qrFiles.length) || busy}>{busy ? 'Enviando…' : 'Enviar'}</button>
            </div>
          </div>
        </div>, document.body)}
    </>
  );
}

function KbLeadCardBody({ lead, money, selectMode, footRight, children }) {
  const t = KBX_TEMP[lead.temp] || KBX_TEMP.warm;
  const stage = kbSince(lead.updated_at);
  const days = stage ? stage.days : null;
  const archived = isArchivedLead(lead);
  const fmtMoney = money || ((v) => fmtCurrency(v));
  const value = !archived && Number(lead.value) > 0 ? `R$ ${fmtMoney(lead.value)}` : '';
  const region = lead.region || (lead.cat && lead.cat !== '—' ? lead.cat : '');
  const email = lead.email && lead.email !== '—' ? lead.email : '';
  const progress = lead.progress ?? (typeof COL_PROGRESS !== 'undefined' ? COL_PROGRESS[lead.col] : 0) ?? 0;
  const handoff = leadHandoffOf(lead);
  return (
    <>
      <div className="kbx-chips">
        <span className={`kbx-chip kbx-temp t-${lead.temp || 'warm'}`}><Icon name={t.icon} size={12} stroke={2.1} />{t.label}</span>
        {lead.score != null && <span className="kbx-chip kbx-score" title="Score do lead">{lead.score} pts</span>}
        {value && <span className="kbx-chip kbx-money" title="Valor">{value}</span>}
        <LeadOriginBadge lead={lead} compact />
      </div>
      <p className="kbx-title">{lead.name}</p>
      <ul className="kbx-info">
        <li title="Telefone"><Icon name="phone" size={12} stroke={1.9} /><span>{lead.phone ? fmtPhone(lead.phone) : <i>Sem telefone</i>}</span></li>
        <li title={email || 'E-mail'}><Icon name="mail" size={12} stroke={1.9} /><span>{email || <i>Sem e-mail</i>}</span></li>
        <li title="Região de interesse"><Icon name="mapPin" size={12} stroke={1.9} /><span>{region || <i>Região não informada</i>}</span></li>
        {lead.created_at && (
          <li title={`Entrou em ${new Date(lead.created_at).toLocaleString('pt-BR')}`}><Icon name="cal" size={12} stroke={1.9} /><span>Entrou {kbDate(lead.created_at)}</span></li>
        )}
        {handoff && (handoff.porNome || handoff.deNome || handoff.modo === 'rodizio') && (
          <li title={`${handoff.modo === 'rodizio' ? 'Distribuído pela fila' : 'Repassado'}${handoff.deNome ? ` · antes com ${handoff.deNome}` : ''}${handoff.em ? ` · ${new Date(handoff.em).toLocaleString('pt-BR')}` : ''}`}>
            <Icon name="arrRightS" size={12} stroke={1.9} />
            <span>{handoff.modo === 'rodizio'
              ? `Pela fila${handoff.deNome ? ` · de ${handoff.deNome.split(' ')[0]}` : ''}`
              : `Repassado${(handoff.porNome || handoff.deNome) ? ` por ${(handoff.porNome || handoff.deNome).split(' ')[0]}` : ''}`}
              {handoff.em ? ` · ${kbDate(handoff.em)}` : ''}</span>
          </li>
        )}
      </ul>
      {children}
      {!selectMode && <KbWaLabels lead={lead} />}
      {lead.col === 'aguardando_docs' && <DocStrip docs={lead.docs} />}
      <LeadStages lead={lead} />
      <div className="kbx-div" />
      <div className="kbx-foot">
        <div className="kbx-foot-l">
          {stage && (
            <span className={`kbx-due${days > 21 ? ' late' : days > 7 ? ' soon' : ''}`} title={`Nesta etapa desde ${new Date(lead.updated_at).toLocaleString('pt-BR')}`}>
              <Icon name="clock" size={12} stroke={2} />{stage.short} na etapa
            </span>
          )}
          <KbxRing value={progress} />
        </div>
        {footRight || <KbxAvatar lead={lead} />}
      </div>
      {!selectMode && <KbWaQuick lead={lead} />}
    </>
  );
}

// ── Arrastar cards do Kanban (framer-motion via CDN → window.Motion) ──────────
// Igual ao componente "kanban-board": o card sai da lista e flutua seguindo o mouse,
// o espaço que ele deixou fecha, abre um espaço onde ele vai cair e os outros cards
// deslizam para o lugar. Começa só depois de mover 5px (clique continua abrindo o lead).
// Esc cancela. Toque (celular) não arrasta — lá ficam os botões Voltar/Avançar.
const KB_LIFT = { type: 'spring', stiffness: 520, damping: 34, mass: 0.7 };
const KB_FLOW = { type: 'spring', stiffness: 420, damping: 36, mass: 0.9 };
let _kbDragEndedAt = 0;
const kbDragJustEnded = () => Date.now() - _kbDragEndedAt < 250;
const _kbMV = () => { let v = 0; return { set: (n) => { v = n; }, get: () => v }; };

function useKbDrag(onDrop) {
  const M = window.Motion;
  const x = M ? M.useMotionValue(0) : React.useRef(_kbMV()).current;
  const y = M ? M.useMotionValue(0) : React.useRef(_kbMV()).current;
  const [drag, setDrag] = React.useState(null);     // { id, lead, fromCol, fromIndex, w, h, offX, offY }
  const [over, setOver] = React.useState(null);
  const [settling, setSettling] = React.useState(false);
  const dragRef = React.useRef(null);
  const overRef = React.useRef(null);
  const scrollRef = React.useRef(null);
  const autoRef = React.useRef(0);
  const dropRef = React.useRef(onDrop);
  dropRef.current = onDrop;

  const colAt = (cx) => {
    const root = scrollRef.current || document;
    let inside = null, nearest = null, best = Infinity;
    root.querySelectorAll('[data-col]').forEach(el => {
      const r = el.getBoundingClientRect();
      if (cx >= r.left && cx <= r.right) inside = el.dataset.col;
      const dx = Math.abs(cx - (r.left + r.width / 2));
      if (dx < best) { best = dx; nearest = el.dataset.col; }
    });
    return inside || nearest;
  };

  const finish = React.useCallback((commit) => {
    const d = dragRef.current, o = overRef.current;
    dragRef.current = null; overRef.current = null; autoRef.current = 0;
    document.body.classList.remove('kb-dragging');
    _kbDragEndedAt = Date.now();
    setDrag(null); setOver(null);
    setSettling(true);
    setTimeout(() => setSettling(false), 700);
    if (commit && d && o && o !== d.fromCol && dropRef.current) dropRef.current(d.id, o);
  }, []);

  React.useEffect(() => {
    if (!drag) return;
    const mv = (e) => {
      const d = dragRef.current; if (!d) return;
      x.set(e.clientX - d.offX); y.set(e.clientY - d.offY);
      const c = colAt(e.clientX);
      if (c && c !== overRef.current) { overRef.current = c; setOver(c); }
      const sc = scrollRef.current;
      if (sc) {
        const r = sc.getBoundingClientRect(), edge = 72;
        autoRef.current = e.clientX < r.left + edge ? -14 : e.clientX > r.right - edge ? 14 : 0;
      }
    };
    const up = () => finish(true);
    const key = (e) => { if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); finish(false); } };
    window.addEventListener('pointermove', mv);
    window.addEventListener('pointerup', up);
    window.addEventListener('pointercancel', up);
    window.addEventListener('keydown', key, true);
    let raf;
    const tick = () => { if (autoRef.current && scrollRef.current) scrollRef.current.scrollLeft += autoRef.current; raf = requestAnimationFrame(tick); };
    raf = requestAnimationFrame(tick);
    return () => {
      window.removeEventListener('pointermove', mv);
      window.removeEventListener('pointerup', up);
      window.removeEventListener('pointercancel', up);
      window.removeEventListener('keydown', key, true);
      cancelAnimationFrame(raf);
    };
  }, [drag, finish]);

  const onCardPointerDown = React.useCallback((e, lead) => {
    if (e.button !== 0 || e.pointerType === 'touch' || dragRef.current) return;
    if (e.target.closest('button, a, input, textarea, select, [data-no-drag]')) return;
    const el = e.currentTarget;
    const sx = e.clientX, sy = e.clientY;
    const stop = () => { window.removeEventListener('pointermove', pm); window.removeEventListener('pointerup', stop); };
    const pm = (ev) => {
      if (Math.hypot(ev.clientX - sx, ev.clientY - sy) < 5) return;
      stop();
      const r = el.getBoundingClientRect();
      const colEl = el.closest('[data-col]');
      scrollRef.current = colEl ? colEl.closest('.kanban') : null;
      const cards = colEl ? Array.from(colEl.querySelectorAll('[data-lead-id]')) : [];
      const st = { id: lead.id, lead, fromCol: lead.col, fromIndex: Math.max(0, cards.indexOf(el)), w: r.width, h: r.height, offX: sx - r.left, offY: sy - r.top };
      x.set(ev.clientX - st.offX); y.set(ev.clientY - st.offY);
      dragRef.current = st; overRef.current = lead.col;
      if (window.getSelection) window.getSelection().removeAllRanges();
      document.body.classList.add('kb-dragging');
      setOver(lead.col); setDrag(st);
    };
    window.addEventListener('pointermove', pm);
    window.addEventListener('pointerup', stop);
  }, []);

  return { drag, over, x, y, layoutOn: !!drag || settling, onCardPointerDown };
}

// Índice do espaço aberto numa coluna (-1 = nenhum): na própria coluna fica onde o card estava; em outra, no topo
function kbPlaceholderIndex(dnd, colId) {
  if (!dnd.drag || dnd.over !== colId) return -1;
  return dnd.drag.fromCol === colId ? dnd.drag.fromIndex : 0;
}

function KbPlaceholder({ height }) {
  const M = window.Motion;
  if (!M) return <div className="kbx-ph" style={{ height }} aria-hidden="true" />;
  return (
    <M.motion.div className="kbx-ph" aria-hidden="true" style={{ height }} layout
      initial={{ opacity: 0, scaleY: 0.7 }} animate={{ opacity: 1, scaleY: 1 }} transition={KB_FLOW} />
  );
}

// Envolve cada card: com o arrastar ativo, os outros deslizam para o novo lugar (layout)
function KbItem({ layoutOn, children }) {
  const M = window.Motion;
  if (!M) return children;
  return <M.motion.div layout={layoutOn ? 'position' : false} transition={KB_FLOW}>{children}</M.motion.div>;
}

// Celular: faixa com as etapas do funil acima do Kanban — mostra em qual etapa
// você está e pula direto para outra com um toque (deslizar coluna a coluna era difícil)
function KbMobileStages({ columns, counts, scrollRef }) {
  const mobile = window.useHorosMobile ? window.useHorosMobile() : false;
  const [active, setActive] = React.useState(0);
  const stripRef = React.useRef(null);
  const colEls = () => {
    const k = scrollRef.current;
    return k ? Array.from(k.querySelectorAll('.kanban-cols > .kcol')) : [];
  };
  React.useEffect(() => {
    const k = scrollRef.current;
    if (!mobile || !k) return;
    let raf = 0;
    const calc = () => {
      raf = 0;
      const base = k.getBoundingClientRect().left + 12;
      let best = 0, dist = Infinity;
      colEls().forEach((el, i) => {
        const d = Math.abs(el.getBoundingClientRect().left - base);
        if (d < dist) { dist = d; best = i; }
      });
      setActive(best);
    };
    const on = () => { if (!raf) raf = requestAnimationFrame(calc); };
    k.addEventListener('scroll', on, { passive: true });
    calc();
    return () => { k.removeEventListener('scroll', on); if (raf) cancelAnimationFrame(raf); };
  }, [mobile, columns.length]);
  // mantém a etapa atual visível na faixa
  React.useEffect(() => {
    const s = stripRef.current;
    const chip = s && s.children[active];
    if (!chip) return;
    const left = chip.offsetLeft - (s.clientWidth - chip.offsetWidth) / 2;
    s.scrollTo({ left: Math.max(0, left), behavior: 'smooth' });
  }, [active]);
  if (!mobile || !columns.length) return null;
  const go = (i) => {
    const k = scrollRef.current;
    const el = colEls()[Math.max(0, Math.min(columns.length - 1, i))];
    if (!k || !el) return;
    const left = k.scrollLeft + el.getBoundingClientRect().left - k.getBoundingClientRect().left - 12;
    k.scrollTo({ left, behavior: 'smooth' });
  };
  return (
    <div className="kb-mstages">
      <button className="kb-mstages-arrow" aria-label="Etapa anterior" disabled={active <= 0} onClick={() => go(active - 1)}>
        <Icon name="arrLeft" size={16} />
      </button>
      <div className="kb-mstages-strip" ref={stripRef}>
        {columns.map((c, i) => (
          <button key={c.id} className={'kb-mstage' + (i === active ? ' on' : '')} onClick={() => go(i)}>
            <span className="kb-mstage-dot" style={{ background: c.color }} />
            <span className="kb-mstage-name">{c.title}</span>
            <span className="kb-mstage-n">{(counts && counts[c.id]) || 0}</span>
          </button>
        ))}
      </div>
      <button className="kb-mstages-arrow" aria-label="Próxima etapa" disabled={active >= columns.length - 1} onClick={() => go(active + 1)}>
        <Icon name="arrRight" size={16} />
      </button>
    </div>
  );
}

// Lista de cards de uma coluna com o espaço aberto no lugar certo
function KbColCards({ dnd, colId, items, renderCard, empty }) {
  const list = dnd.drag ? items.filter(l => l.id !== dnd.drag.id) : items;
  const ph = kbPlaceholderIndex(dnd, colId);
  const phEl = ph > -1 ? <KbPlaceholder key="__ph" height={dnd.drag.h} /> : null;
  const out = [];
  list.forEach((l, i) => {
    if (i === ph) out.push(phEl);
    out.push(<KbItem key={l.id} layoutOn={dnd.layoutOn}>{renderCard(l)}</KbItem>);
  });
  if (ph > -1 && ph >= list.length) out.push(phEl);
  if (!out.length) return empty || null;
  return out;
}

// O card "no ar", seguindo o mouse
function KbDragOverlay({ dnd, render }) {
  const M = window.Motion;
  if (!M) return null;
  return ReactDOM.createPortal(
    <M.AnimatePresence>
      {dnd.drag && (
        <M.motion.div key={dnd.drag.id} className="kbx-float"
          style={{ x: dnd.x, y: dnd.y, width: dnd.drag.w }}
          initial={{ scale: 1, rotate: 0 }} animate={{ scale: 1.03, rotate: 1.6 }}
          exit={{ scale: 1, rotate: 0, opacity: 0 }} transition={KB_LIFT}>
          {render(dnd.drag.lead)}
        </M.motion.div>
      )}
    </M.AnimatePresence>,
    document.body,
  );
}

// Corretores que podem receber leads no rodízio (gerente: só a própria equipe)
function useTransferTargets() {
  const [list, setList] = React.useState([]);
  React.useEffect(() => {
    const ws = window.HOROS_WORKSPACE || {};
    if (!MANAGER_ROLES.has(ws.role) || !window.HOROS_SB) return;
    window.HOROS_SB.rpc('list_workspace_members').then(({ data }) => {
      if (!data) return;
      // Gestor que também atua como corretor (works_as_broker) recebe leads — inclusive ele mesmo
      setList(data.filter(m => (['corretor', 'assistente'].includes(m.role) || m.works_as_broker) && (ws.role !== 'gerente' || m.team_id === ws.team_id)));
    });
  }, []);
  return list;
}

// ── Seleção em massa do Kanban (Dashboard + CRM) ─────────────────────────────
// Clique marca/desmarca · Shift+clique marca o intervalo na mesma coluna ·
// Ctrl/Cmd+A marca todos os visíveis · Esc sai do modo seleção
function useKanbanSelection(visible) {
  const [selectMode, setSelectMode]   = React.useState(false);
  const [selectedIds, setSelectedIds] = React.useState(() => new Set());
  const anchorRef = React.useRef(null);
  const visRef = React.useRef(visible || []);
  if (visible) visRef.current = visible;

  const exitSelect = React.useCallback(() => {
    setSelectMode(false); setSelectedIds(new Set()); anchorRef.current = null;
  }, []);

  const toggleSelect = React.useCallback((id, shift) => {
    const list = visRef.current || [];
    const anchor = anchorRef.current;
    setSelectedIds(prev => {
      const n = new Set(prev);
      if (shift && anchor && anchor !== id) {
        const a = list.find(l => l.id === anchor), b = list.find(l => l.id === id);
        if (a && b && a.col === b.col) {
          const col = list.filter(l => l.col === b.col);
          const i1 = col.findIndex(l => l.id === anchor), i2 = col.findIndex(l => l.id === id);
          const [from, to] = i1 < i2 ? [i1, i2] : [i2, i1];
          const on = n.has(anchor);
          col.slice(from, to + 1).forEach(l => on ? n.add(l.id) : n.delete(l.id));
          return n;
        }
      }
      n.has(id) ? n.delete(id) : n.add(id);
      return n;
    });
    anchorRef.current = id;
  }, []);

  React.useEffect(() => {
    if (!selectMode) return;
    const onKey = (e) => {
      const ae = document.activeElement;
      if (ae && /^(INPUT|TEXTAREA|SELECT)$/.test(ae.tagName)) return;
      if (e.key === 'Escape') { exitSelect(); return; }
      if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'a') {
        e.preventDefault();
        setSelectedIds(new Set((visRef.current || []).map(l => l.id)));
      }
    };
    window.addEventListener('keydown', onKey);
    document.body.classList.add('kb-selecting');
    return () => { window.removeEventListener('keydown', onKey); document.body.classList.remove('kb-selecting'); };
  }, [selectMode, exitSelect]);

  return { selectMode, setSelectMode, selectedIds, setSelectedIds, toggleSelect, exitSelect, visRef };
}

// ── Busca por etapa (lupa no cabeçalho de cada coluna do Kanban) ──────────────
const _kbNorm = (t) => String(t || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
function kbLeadMatches(l, q) {
  const n = _kbNorm(q).trim();
  if (!n) return true;
  const digits = n.replace(/\D/g, '');
  if (digits.length >= 3 && String(l.phone || '').replace(/\D/g, '').includes(digits)) return true;
  return n.split(/\s+/).every(w =>
    _kbNorm(l.name).includes(w) || _kbNorm(l.email).includes(w) || _kbNorm(l.cat).includes(w) || _kbNorm(l.broker).includes(w));
}

// ── Ficha do cliente na metade da tela — em TODAS as telas (pedido do usuário 2026-10-03) ──
// horosOpenLeadSheet(id): abre client.html?id=&embed=1 num painel de 52% à direita (Esc fecha,
// botão para tela cheia). A ficha embutida manda 'horos:embed-close' (Voltar/Salvar e Fechar) e 'horos:lead-updated'.
function HorosLeadSheet({ leadId, tab, onClose }) {
  React.useEffect(() => {
    const onKey = (e) => { if (e.key === 'Escape') { e.stopPropagation(); onClose(); } };
    const onMsg = (e) => {
      if (e.origin !== location.origin || !e.data) return;
      if (e.data.type === 'horos:embed-close') onClose();
      if (e.data.type === 'horos:lead-updated') window.dispatchEvent(new CustomEvent('horos:lead-updated', { detail: e.data.detail }));
    };
    window.addEventListener('keydown', onKey, true);
    window.addEventListener('message', onMsg);
    const prev = document.body.style.overflow;
    document.body.style.overflow = 'hidden';
    return () => {
      window.removeEventListener('keydown', onKey, true);
      window.removeEventListener('message', onMsg);
      document.body.style.overflow = prev;
    };
  }, [onClose]);
  return (
    <>
      <div className="cli-sheet-overlay" onClick={onClose} />
      <aside className="cli-sheet" role="dialog" aria-label="Ficha do cliente">
        <div className="cli-sheet-tools">
          <button className="cli-sheet-btn" onClick={onClose} title="Fechar (Esc)"><Icon name="close" size={15} /></button>
          <a className="cli-sheet-btn" href={`client.html?id=${encodeURIComponent(leadId)}${tab ? `&tab=${encodeURIComponent(tab)}` : ''}`} title="Abrir em tela cheia"><Icon name="expand" size={14} /></a>
        </div>
        <iframe className="cli-sheet-frame" src={`client.html?id=${encodeURIComponent(leadId)}&embed=1${tab ? `&tab=${encodeURIComponent(tab)}` : ''}`} title="Ficha do cliente" />
      </aside>
    </>
  );
}
let _horosSheetRoot = null;
// opts.tab = aba da ficha que já abre (ex.: 'contratos' — tela Financeiro)
function horosOpenLeadSheet(leadId, opts) {
  if (!leadId) return;
  // dentro da própria ficha embutida: pede para a página de fora abrir
  if (window.self !== window.top && window.top.horosOpenLeadSheet) { window.top.horosOpenLeadSheet(leadId, opts); return; }
  let host = document.getElementById('horos-lead-sheet');
  if (!host) {
    host = document.createElement('div');
    host.id = 'horos-lead-sheet';
    document.body.appendChild(host);
    _horosSheetRoot = ReactDOM.createRoot(host);
  }
  const close = () => _horosSheetRoot && _horosSheetRoot.render(null);
  _horosSheetRoot.render(<HorosLeadSheet key={leadId + ':' + Date.now()} leadId={leadId} tab={opts?.tab} onClose={close} />);
}
window.horosOpenLeadSheet = horosOpenLeadSheet;
window.HorosLeadSheet = HorosLeadSheet;

// ── Ir até um lead no Kanban ──────────────────────────────────────────────────
// Página da coluna (100 por página) em que o lead está, na ordem em que o Kanban lista.
function kbLeadPage(list, lead, pageSize) {
  const idx = list.filter(l => l.col === lead.col).findIndex(l => l.id === lead.id);
  return idx < 0 ? 1 : Math.floor(idx / pageSize) + 1;
}
// Rola o Kanban até o card (horizontal: coluna no centro; vertical: card no centro) e destaca.
// Tenta por até ~3s enquanto o card não aparece no DOM.
function kbScrollToCard(id, tries = 20) {
  return new Promise(resolve => {
    const attempt = (n) => {
      const el = document.querySelector(`.kanban [data-lead-id="${CSS.escape(String(id))}"]`);
      if (!el) { if (n > 0) setTimeout(() => attempt(n - 1), 150); else resolve(false); return; }
      const container = el.closest('.kanban');
      if (container) {
        const pivot = el.closest('[data-col]') || el.closest('.kcol') || el;
        const cr = container.getBoundingClientRect(), pr = pivot.getBoundingClientRect();
        const left = container.scrollLeft + (pr.left - cr.left) - (container.clientWidth / 2) + (pivot.offsetWidth / 2);
        container.scrollLeft = Math.max(0, left);
      }
      requestAnimationFrame(() => el.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' }));
      el.classList.remove('crm-lead-pulse'); void el.offsetWidth;
      el.classList.add('crm-lead-pulse');
      setTimeout(() => el.classList.remove('crm-lead-pulse'), 2500);
      resolve(true);
    };
    setTimeout(() => attempt(tries), 60);
  });
}
// Leva ao lead: no Kanban da própria tela (se houver, via window.horosKanbanFocus) ou no CRM.
async function horosGoToLead(id) {
  if (!id) return;
  if (typeof window.horosKanbanFocus === 'function' && await window.horosKanbanFocus(id)) return;
  try { sessionStorage.setItem('horos:highlight-lead', id); } catch {}
  window.location.href = 'crm.html';
}

function KbColSearchBtn({ open, active, onToggle }) {
  return (
    <button className={`kcol-search-btn${open ? ' open' : ''}${active ? ' active' : ''}`} onClick={onToggle}
      title={open ? 'Fechar busca' : 'Buscar nesta etapa'}>
      <Icon name="search" size={12} stroke={2.2} />
    </button>
  );
}

function KbColSearchInput({ value, onChange, onClose, shown, total }) {
  return (
    <div className="kcol-search">
      <Icon name="search" size={12} stroke={2} />
      <input autoFocus value={value} placeholder="Nome, telefone, e-mail…"
        onChange={e => onChange(e.target.value)}
        onKeyDown={e => { if (e.key === 'Escape') { e.stopPropagation(); onClose(); } }} />
      {value && <span className="kcol-search-n">{shown}/{total}</span>}
      <button onClick={onClose} title="Limpar e fechar"><Icon name="close" size={11} stroke={2.4} /></button>
    </div>
  );
}

// Caixinha de 3 estados no cabeçalho da coluna (nenhum / alguns / todos)
function KbColCheck({ items, selectedIds, setSelectedIds }) {
  const n = items.reduce((s, l) => s + (selectedIds.has(l.id) ? 1 : 0), 0);
  const state = n === 0 ? 'none' : n === items.length ? 'all' : 'some';
  const toggle = () => setSelectedIds(prev => {
    const s = new Set(prev);
    items.forEach(l => state === 'all' ? s.delete(l.id) : s.add(l.id));
    return s;
  });
  return (
    <button className={`kb-colcheck ${state}`} onClick={toggle}
      title={state === 'all' ? 'Desmarcar a etapa' : 'Selecionar a etapa inteira'}>
      <span className="kb-colcheck-box">
        {state === 'all' && <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3.5" strokeLinecap="round" strokeLinejoin="round"><polyline points="20 6 9 17 4 12" /></svg>}
        {state === 'some' && <span className="kb-colcheck-dash" />}
      </span>
      {n > 0 ? <span className="kb-colcheck-n">{n}/{items.length}</span> : <span className="kb-colcheck-n muted">Todos</span>}
    </button>
  );
}

// Menu que abre para cima a partir da barra (fecha ao clicar fora / Esc)
function KbPopover({ open, onClose, children, align = 'left', width = 260 }) {
  const ref = React.useRef(null);
  React.useEffect(() => {
    if (!open) return;
    const down = (e) => { if (ref.current && !ref.current.contains(e.target) && !e.target.closest?.('.kb-bar-trigger')) onClose(); };
    const key = (e) => { if (e.key === 'Escape') { e.stopPropagation(); onClose(); } };
    document.addEventListener('mousedown', down);
    window.addEventListener('keydown', key, true);
    return () => { document.removeEventListener('mousedown', down); window.removeEventListener('keydown', key, true); };
  }, [open]);
  if (!open) return null;
  return <div ref={ref} className={`kb-pop ${align}`} style={{ width }}>{children}</div>;
}

const _kbIni = (n) => (n || '?').trim().split(/\s+/).map(p => p[0]).join('').slice(0, 2).toUpperCase();
const _KB_AV = ['#6366f1','#8b5cf6','#ec4899','#ef4444','#f97316','#22c55e','#14b8a6','#3b82f6'];
const _kbAvColor = (n) => { let h = 0; for (const c of (n || '')) h = (h * 31 + c.charCodeAt(0)) & 0xffff; return _KB_AV[h % _KB_AV.length]; };

// Barra flutuante de ações (aparece no rodapé enquanto o modo seleção está ativo)
// Mensagem para os selecionados do Kanban (2026-10-23): vira um disparo em massa no ritmo seguro
// (mesmo motor dos Disparos: fila, intervalo, limite diário, número checado; Pro e Max).
function KbBulkMessageModal({ leads, onClose }) {
  const isMax = useHorosPlan().tier !== 'dist';
  const [text, setText] = React.useState('');
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState('');
  const ref = React.useRef(null);
  const rows = React.useMemo(() => {
    const seen = new Set();
    return (leads || []).map(l => ({ phone: l.phone, name: l.name })).filter(r => {
      const d = String(r.phone || '').replace(/\D/g, '').replace(/^55(?=\d{10,11}$)/, '');
      if (d.length < 10 || seen.has(d)) return false;
      seen.add(d); return true;
    });
  }, [leads]);
  const skipped = (leads || []).length - rows.length;
  React.useEffect(() => {
    const k = e => { if (e.key === 'Escape' && !busy) onClose(); };
    window.addEventListener('keydown', k, true);
    return () => window.removeEventListener('keydown', k, true);
  }, [busy, onClose]);
  const addVar = (v) => {
    const ta = ref.current, s = ta ? ta.selectionStart : text.length, e = ta ? ta.selectionEnd : text.length;
    setText(text.slice(0, s) + v + text.slice(e));
    requestAnimationFrame(() => { if (ta) { ta.focus(); ta.selectionStart = ta.selectionEnd = s + v.length; } });
  };
  const send = async () => {
    if (!text.trim() || !rows.length || busy) return;
    setBusy(true); setErr('');
    const { error } = await window.HOROS_SB.rpc('create_broadcast', {
      p_name: `Mensagem do funil ${new Date().toLocaleDateString('pt-BR')} (${rows.length})`,
      p_message: text.trim(), p_media: null, p_audience: { source: 'list', rows },
      p_min_delay: 45, p_max_delay: 120, p_daily_limit: 200, p_window_start: 8, p_window_end: 20,
      p_scheduled_at: null, p_after_col: null,
    });
    if (error) { setErr(error.message || 'Não foi possível criar o envio.'); setBusy(false); return; }
    window.dispatchEvent(new CustomEvent('horos:toast', { detail: { title: 'Mensagem na fila', body: `${rows.length} contatos vão receber, um a um, com ritmo seguro. Acompanhe em Disparos.` } }));
    onClose();
  };
  return ReactDOM.createPortal(
    <div className="arq-ov" onMouseDown={e => { if (e.target === e.currentTarget && !busy) onClose(); }}>
      <div className="arq-modal" role="dialog" aria-label="Mensagem para vários contatos">
        <div className="arq-head">
          <span className="arq-ic"><Icon name="send" size={20} /></span>
          <div style={{ minWidth: 0 }}><h3>Mensagem para {rows.length} {rows.length === 1 ? 'contato' : 'contatos'}</h3>
            <p>Sai pelo seu WhatsApp, um por vez, com intervalo de 45 a 120 s (8h às 20h).</p></div>
          <button className="arq-x" onClick={onClose} disabled={busy} aria-label="Fechar"><Icon name="close" size={16} /></button>
        </div>
        {!isMax ? (
          <HorosPlanLock title="Envio em massa não faz parte do plano Distribuição" desc="Escreva uma vez e envie para todos os selecionados." />
        ) : (
          <>
            <label className="arq-note"><span>Mensagem</span>
              <textarea ref={ref} rows={5} maxLength={1000} autoFocus value={text} onChange={e => setText(e.target.value)}
                placeholder="Oi {primeiro_nome}, tudo bem? …" /></label>
            <div style={{ display: 'flex', gap: 6, margin: '6px 0' }}>
              {['{primeiro_nome}', '{nome}'].map(v => <button key={v} type="button" className="arq-btn ghost" style={{ height: 28, padding: '0 10px' }} onClick={() => addVar(v)}>{v}</button>)}
            </div>
            {skipped > 0 && <p className="arq-hint">{skipped} {skipped === 1 ? 'selecionado ficou' : 'selecionados ficaram'} de fora (sem telefone válido ou repetido).</p>}
            {err && <div className="arq-err">{err}</div>}
          </>
        )}
        <div className="arq-foot">
          <button type="button" className="arq-btn ghost" onClick={onClose} disabled={busy}>Cancelar</button>
          {isMax && <button type="button" className="arq-btn" onClick={send} disabled={!text.trim() || !rows.length || busy}>{busy ? 'Enviando…' : `Enviar para ${rows.length}`}</button>}
        </div>
      </div>
    </div>, document.body);
}

function KanbanBulkBar({ count, total, columns, selectedLeads, onMove, onArchive, onSelectAll, onClearSel, onExit, busy, onTransfer, pipelineId, onMovePipeline }) {
  const solo = useHorosSolo();
  const pipes = usePipelines();
  const otherPipes = [{ id: 'main', name: pipes.mainName }, ...pipes.pipelines].filter(p => p.id !== (pipelineId || 'main'));
  const [menu, setMenu]       = React.useState(null); // 'move' | 'transfer' | 'archive'
  const [msgOpen, setMsgOpen] = React.useState(false);
  const [restart, setRestart] = React.useState(true);
  const [q, setQ]             = React.useState('');
  const brokers = useTransferTargets();
  const none = count === 0;
  React.useEffect(() => { if (none) setMenu(null); }, [none]);

  // quantos dos selecionados estão em cada etapa (mostra no menu "Mover")
  const perCol = React.useMemo(() => {
    const m = {};
    (selectedLeads || []).forEach(l => { m[l.col] = (m[l.col] || 0) + 1; });
    return m;
  }, [selectedLeads]);

  // Etiquetas do WhatsApp em massa
  const waStore = useKbWaLabels();
  const waCatalog = waStore.catalog || [];
  const [labelRun, setLabelRun] = React.useState(null);   // { done, total }
  const [labelMsg, setLabelMsg] = React.useState('');
  const [labelAsk, setLabelAsk] = React.useState(null);   // { label, add, n } — confirmação antes de aplicar
  React.useEffect(() => { if (menu !== 'label') setLabelAsk(null); }, [menu]);
  const labelCount = {};
  if (menu === 'label') {
    (selectedLeads || []).forEach(l => {
      const conv = kbWaPhoneKeys(l.phone).map(k => waStore.byPhone.get(k)).find(Boolean);
      (conv?.labels || []).forEach(id => { labelCount[id] = (labelCount[id] || 0) + 1; });
    });
  }
  const runLabel = async (label, add) => {
    setMenu(null); setLabelMsg('');
    const r = await kbWaBulkLabel(selectedLeads || [], label.label_id, add, (done, total) => setLabelRun({ done, total }));
    setLabelRun(null);
    const verb = add ? 'colocada em' : 'tirada de';
    setLabelMsg(r.total === 0
      ? (add ? `Todos já têm "${label.name}".` : `Nenhum tem "${label.name}".`)
      : `"${label.name}" ${verb} ${r.ok} de ${r.total}` + (r.fail ? ` · ${r.fail} falharam${r.err ? ` (${r.err})` : ''}` : ''));
    setTimeout(() => setLabelMsg(''), 6000);
  };

  const [region, setRegion] = React.useState('');
  const runRegion = async () => {
    const ids = (selectedLeads || []).map(l => l.id);
    setMenu(null);
    const { data, error } = await window.HOROS_SB.rpc('set_leads_region', { p_ids: ids, p_region: region });
    const ok = !error && data?.ok;
    if (ok) {
      const v = region.trim();
      const set = new Set(ids);
      if (Array.isArray(window.LEADS)) window.LEADS = window.LEADS.map(l => set.has(l.id)
        ? { ...l, cat: v, region: v || null, data: { ...(l.data || {}), regiaoDesejada: v } } : l);
      window.dispatchEvent(new CustomEvent('horos:leads-refreshed'));
    }
    window.dispatchEvent(new CustomEvent('horos:toast', { detail: ok
      ? { title: 'Região atualizada', body: `${data.updated} ${data.updated === 1 ? 'lead' : 'leads'}` }
      : { title: 'Não foi possível atualizar a região', body: error?.message || '', type: 'error' } }));
  };
  const run = async (fn) => { setMenu(null); await fn(); };
  const brokersF = brokers.filter(b => !q || (b.name || '').toLowerCase().includes(q.toLowerCase()));

  return (
    <div className="kb-bar-wrap">
      <div className={`kb-bar${busy ? ' busy' : ''}`} role="toolbar" aria-label="Ações em massa">
        <div className="kb-bar-count">
          <span className="kb-bar-num">{count}</span>
          <span className="kb-bar-lbl">{count === 1 ? 'selecionado' : 'selecionados'}</span>
          {count > 0 && <button className="kb-bar-x" onClick={onClearSel} title="Desmarcar todos"><Icon name="close" size={11} stroke={2.4} /></button>}
        </div>

        {count < total && (
          <button className="kb-bar-link" onClick={onSelectAll} disabled={busy}>
            Selecionar todos <span>({total})</span>
          </button>
        )}

        <span className="kb-bar-sep" />

        <div className="kb-bar-item">
          <button className={`kb-bar-btn kb-bar-trigger${menu === 'move' ? ' on' : ''}`} disabled={busy || none}
            onClick={() => setMenu(m => m === 'move' ? null : 'move')}>
            <Icon name="arrRight" size={14} stroke={2} /> Mover para
            <svg className="kb-caret" width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"><polyline points="6 15 12 9 18 15" /></svg>
          </button>
          <KbPopover open={menu === 'move'} onClose={() => setMenu(null)}>
            <div className="kb-pop-head">Mover {count} {count === 1 ? 'lead' : 'leads'} para…</div>
            <div className="kb-pop-list">
              {columns.map(c => (
                <button key={c.id} className="kb-pop-row" onClick={() => run(() => onMove(c.id))}
                  disabled={perCol[c.id] === count}>
                  <span className="kb-pop-dot" style={{ background: c.color }} />
                  <span className="kb-pop-name">{c.title}</span>
                  {perCol[c.id] ? <span className="kb-pop-meta">{perCol[c.id] === count ? 'já estão aqui' : `${perCol[c.id]} já aqui`}</span> : null}
                </button>
              ))}
            </div>
          </KbPopover>
        </div>

        {onMovePipeline && pipes.pipelines.length > 0 && (
          <div className="kb-bar-item">
            <button className={`kb-bar-btn kb-bar-trigger${menu === 'pipeline' ? ' on' : ''}`} disabled={busy || none}
              onClick={() => setMenu(m => m === 'pipeline' ? null : 'pipeline')}>
              <Icon name="layers" size={14} stroke={2} /> Mover para funil
              <svg className="kb-caret" width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"><polyline points="6 15 12 9 18 15" /></svg>
            </button>
            <KbPopover open={menu === 'pipeline'} onClose={() => setMenu(null)}>
              <div className="kb-pop-head">Mover {count} {count === 1 ? 'lead' : 'leads'} para o funil…</div>
              <div className="kb-pop-list">
                {otherPipes.map(p => (
                  <button key={p.id} className="kb-pop-row" onClick={() => run(() => onMovePipeline(p.id))}>
                    <Icon name="layers" size={13} />
                    <span className="kb-pop-name">{p.name}</span>
                    <span className="kb-pop-meta">entra na Entrada</span>
                  </button>
                ))}
              </div>
            </KbPopover>
          </div>
        )}

        {onTransfer && !solo && brokers.length > 0 && (
          <div className="kb-bar-item">
            <button className={`kb-bar-btn kb-bar-trigger${menu === 'transfer' ? ' on' : ''}`} disabled={busy || none}
              onClick={() => { setQ(''); setMenu(m => m === 'transfer' ? null : 'transfer'); }}>
              <Icon name="user" size={14} stroke={2} /> Passar para
              <svg className="kb-caret" width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"><polyline points="6 15 12 9 18 15" /></svg>
            </button>
            <KbPopover open={menu === 'transfer'} onClose={() => setMenu(null)} width={280}>
              <div className="kb-pop-head">Passar {count} {count === 1 ? 'lead' : 'leads'} para…</div>
              {brokers.length > 6 && (
                <input className="kb-pop-search" autoFocus placeholder={`Buscar ${segTerm('agent', 'corretor')}…`} value={q} onChange={e => setQ(e.target.value)} />
              )}
              <div className="kb-pop-list">
                {brokersF.map(b => (
                  <button key={b.user_id} className="kb-pop-row" onClick={() => run(() => onTransfer(b.user_id, restart))}>
                    {b.avatar_url
                      ? <img className="kb-pop-av" src={b.avatar_url} alt="" />
                      : <span className="kb-pop-av" style={{ background: _kbAvColor(b.name) }}>{_kbIni(b.name)}</span>}
                    <span className="kb-pop-name">{b.name}</span>
                    {b.team_name && <span className="kb-pop-meta">{b.team_name}</span>}
                  </button>
                ))}
                {brokersF.length === 0 && <div className="kb-pop-empty">Nenhum {segTerm('agent', 'corretor')} encontrado</div>}
              </div>
              <label className="kb-pop-foot" title="Quem já teve o lead volta para a etapa em que parou">
                <span className={`kb-switch${restart ? ' on' : ''}`} onClick={() => setRestart(v => !v)}><span /></span>
                <span onClick={() => setRestart(v => !v)}>Quem nunca teve o lead começa no <b>Inbox</b></span>
              </label>
            </KbPopover>
          </div>
        )}

        <div className="kb-bar-item">
          <button className={`kb-bar-btn kb-bar-trigger${menu === 'region' ? ' on' : ''}`} disabled={busy || none}
            onClick={() => { setRegion(''); setMenu(m => m === 'region' ? null : 'region'); }}>
            <Icon name="mapPin" size={14} stroke={2} /> Região
            <svg className="kb-caret" width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"><polyline points="6 15 12 9 18 15" /></svg>
          </button>
          <KbPopover open={menu === 'region'} onClose={() => setMenu(null)} width={270}>
            <div className="kb-pop-head">Região de {count} {count === 1 ? 'lead' : 'leads'}</div>
            <input className="kb-pop-search" autoFocus maxLength={80} placeholder="Bairro ou região (vazio limpa)" value={region}
              onChange={e => setRegion(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') runRegion(); }} />
            <div className="kb-pop-confirm-btns">
              <button className="kb-bar-btn ghost" onClick={() => setMenu(null)}>Cancelar</button>
              <button className="kb-bar-btn solid" onClick={runRegion}>Aplicar</button>
            </div>
          </KbPopover>
        </div>

        {waCatalog.length > 0 && (
          <div className="kb-bar-item">
            <button className={`kb-bar-btn kb-bar-trigger${menu === 'label' ? ' on' : ''}`} disabled={busy || none || !!labelRun}
              onClick={() => { setLabelMsg(''); setMenu(m => m === 'label' ? null : 'label'); }}>
              {labelRun ? <><span className="kb-spin" /> {labelRun.done}/{labelRun.total}</> : <><Icon name="tag" size={14} stroke={2} /> Etiquetar</>}
              <svg className="kb-caret" width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"><polyline points="6 15 12 9 18 15" /></svg>
            </button>
            <KbPopover open={menu === 'label'} onClose={() => setMenu(null)} width={270}>
              {labelAsk ? (
                <div className="kb-pop-confirm">
                  <div className="kb-pop-confirm-title">
                    {labelAsk.add ? 'Colocar' : 'Excluir'} <span style={{ color: kbWaColor(labelAsk.label.color) }}>{labelAsk.label.name}</span> {labelAsk.add ? 'em' : 'de'} {labelAsk.n} {labelAsk.n === 1 ? 'lead' : 'leads'}?
                  </div>
                  <div className="kb-pop-confirm-text">
                    {labelAsk.add ? 'A etiqueta é colocada no WhatsApp de cada contato.' : 'A etiqueta é tirada do WhatsApp de cada contato.'}
                  </div>
                  <div className="kb-pop-confirm-btns">
                    <button className="kb-bar-btn ghost" onClick={() => setLabelAsk(null)}>Cancelar</button>
                    <button className={`kb-bar-btn ${labelAsk.add ? 'solid' : 'solid-danger'}`} onClick={() => runLabel(labelAsk.label, labelAsk.add)}>
                      {labelAsk.add ? 'Confirmar' : 'Excluir'}
                    </button>
                  </div>
                </div>
              ) : <>
              <div className="kb-pop-head">Etiqueta do WhatsApp em {count} {count === 1 ? 'lead' : 'leads'}</div>
              <div className="kb-pop-list">
                {waCatalog.map(l => {
                  const id = String(l.label_id);
                  const n = labelCount[id] || 0;
                  const all = n === count;
                  return (
                    <button key={id} className="kb-pop-row" onClick={() => setLabelAsk({ label: l, add: !all, n: all ? n : count - n })}
                      title={all ? 'Clique para tirar de todos' : 'Clique para colocar em todos'}>
                      <span className="kb-pop-dot" style={{ background: kbWaColor(l.color) }} />
                      <span className="kb-pop-name">{l.name}</span>
                      <span className="kb-pop-meta">{all ? <Icon name="check" size={13} stroke={2.4} /> : n > 0 ? `${n}/${count}` : ''}</span>
                    </button>
                  );
                })}
              </div>
              <div className="kb-pop-note">Etiqueta em todos → clique tira de todos. Senão, coloca em quem falta.</div>
              </>}
            </KbPopover>
            {labelMsg && !labelRun && <div className="kb-bar-toast" onClick={() => setLabelMsg('')}>{labelMsg}</div>}
          </div>
        )}

        <div className="kb-bar-item">
          <button className="kb-bar-btn" disabled={busy || none} onClick={() => { setMenu(null); setMsgOpen(true); }}>
            <Icon name="send" size={14} stroke={2} /> Mensagem
          </button>
          {msgOpen && <KbBulkMessageModal leads={selectedLeads || []} onClose={() => setMsgOpen(false)} />}
        </div>

        <div className="kb-bar-item">
          <button className="kb-bar-btn danger" disabled={busy || none}
            onClick={() => { setMenu(null); onArchive(); }}>
            <Icon name="trash" size={14} stroke={2} /> Arquivar
          </button>
        </div>

        <span className="kb-bar-sep" />

        <button className="kb-bar-btn done" onClick={onExit} disabled={busy || !!labelRun} title="Sair do modo seleção (Esc)">
          {busy ? <><span className="kb-spin" /> Aplicando…</> : 'Concluir'}
        </button>
      </div>
      <div className="kb-bar-hint">Clique nos cards para marcar · <b>Shift</b>+clique marca um intervalo · <b>Ctrl+A</b> todos · <b>Esc</b> sai</div>
    </div>
  );
}

// Executa uma RPC por lead em paralelo (lotes de 20); devolve os ids que deram certo
async function runBulkLeadRpc(ids, rpc, params) {
  const sb = window.HOROS_SB;
  const ok = [];
  for (let i = 0; i < ids.length; i += 20) {
    const chunk = ids.slice(i, i + 20);
    const res = await Promise.all(chunk.map(id => sb.rpc(rpc, { p_id: id, ...params })));
    res.forEach((r, j) => { if (!r.error && r.data) ok.push(chunk[j]); });
  }
  return ok;
}

// ── Arquivar com motivo (2026-10-07, pedido do usuário) ─────────────────────────────────────
// Toda tela que arquiva chama horosArchiveLeads(ids, { names }) → abre a janela do motivo, roda
// archive_lead(p_id, p_reason, p_note) e devolve os ids arquivados ([] se cancelar). O motivo fica em
// leads.data.arquivo; gerente/super veem os arquivados em blocos por motivo (Clientes → Arquivados).
const ARCHIVE_REASONS = [
  { id: 'sem_resposta',     label: 'Não responde',            desc: 'Várias tentativas sem retorno' },
  { id: 'sem_interesse',    label: 'Sem interesse',           desc: 'Desistiu ou não quer mais' },
  { id: 'sem_credito',      label: 'Sem crédito / renda',     desc: 'Não aprovou ou renda insuficiente' },
  { id: 'comprou_outro',    label: 'Comprou com outro',       desc: 'Fechou com concorrente' },
  { id: 'adiou',            label: 'Vai comprar mais tarde',  desc: 'Pediu para falar depois' },
  { id: 'fora_perfil',      label: 'Fora do perfil',          desc: 'Região, valor ou produto não atende' },
  { id: 'contato_invalido', label: 'Contato inválido',        desc: 'Número errado ou inexistente' },
  { id: 'duplicado',        label: 'Duplicado',               desc: 'Já existe outro cadastro' },
  { id: 'outro',            label: 'Outro motivo',            desc: 'Descreva na observação' },
];
// Padrão local até o banco responder (2026-10-23: a lista passou a ser editável por imobiliária,
// tabela archive_reasons; `recoverable` = o lead arquivado com esse motivo pode voltar ao funil).
ARCHIVE_REASONS.forEach(r => { r.recoverable = false; });
const ARCHIVE_REASON_LABEL = Object.fromEntries(ARCHIVE_REASONS.map(r => [r.id, r.label]));
ARCHIVE_REASON_LABEL._none = 'Sem motivo informado';
ARCHIVE_REASON_LABEL._col = 'Movido para etapa de arquivo';
const archiveReasonLabel = (id) => ARCHIVE_REASON_LABEL[id] || id || ARCHIVE_REASON_LABEL._none;
// Motivo sem linha na tabela (antigo, apagado, _none, _col) segue a regra antiga: gestor recupera.
const archiveReasonRecoverable = (id) => {
  const r = ARCHIVE_REASONS.find(x => x.id === id);
  return r ? !!r.recoverable : true;
};
let _arqLoad = null;
// Carrega os motivos do banco para dentro de ARCHIVE_REASONS (mesmo array, in place). Devolve { can_edit }.
function horosLoadArchiveReasons(force) {
  if (_arqLoad && !force) return _arqLoad;
  const sb = window.HOROS_SB;
  _arqLoad = (sb ? sb.rpc('list_archive_reasons') : Promise.resolve({})).then(({ data }) => {
    if (data && Array.isArray(data.rows) && data.rows.length) {
      ARCHIVE_REASONS.splice(0, ARCHIVE_REASONS.length, ...data.rows.map(r => ({
        id: r.id, label: r.label, desc: r.descr || '', recoverable: !!r.recoverable })));
      Object.keys(ARCHIVE_REASON_LABEL).forEach(k => { if (k[0] !== '_') delete ARCHIVE_REASON_LABEL[k]; });
      ARCHIVE_REASONS.forEach(r => { ARCHIVE_REASON_LABEL[r.id] = r.label; });
      window.dispatchEvent(new CustomEvent('horos:archive-reasons'));
    }
    return { can_edit: !!data?.can_edit };
  }, () => ({ can_edit: false }));
  return _arqLoad;
}
function useArchiveReasons() {
  const [, bump] = React.useState(0);
  const [canEdit, setCanEdit] = React.useState(false);
  React.useEffect(() => {
    const h = () => bump(n => n + 1);
    window.addEventListener('horos:archive-reasons', h);
    horosLoadArchiveReasons().then(r => setCanEdit(!!r.can_edit));
    return () => window.removeEventListener('horos:archive-reasons', h);
  }, []);
  return { reasons: ARCHIVE_REASONS, canEdit };
}

// Gestor: adicionar, editar e apagar motivos; escolher quais deixam o lead voltar ao funil
function ArchiveReasonsManager({ onClose }) {
  const { reasons } = useArchiveReasons();
  const [edit, setEdit] = React.useState(null);   // { id|null, label, descr, recoverable }
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState('');
  const save = async () => {
    if (!edit.label.trim() || busy) return;
    setBusy(true); setErr('');
    const { data, error } = await window.HOROS_SB.rpc('save_archive_reason',
      { p_id: edit.id, p_label: edit.label, p_descr: edit.descr, p_recoverable: edit.recoverable });
    if (error || !data?.ok) { setErr('Não foi possível salvar.'); setBusy(false); return; }
    await horosLoadArchiveReasons(true);
    setEdit(null); setBusy(false);
  };
  const del = async (r) => {
    if (busy || !window.confirm(`Apagar o motivo "${r.label}"? Leads já arquivados com ele continuam arquivados.`)) return;
    setBusy(true); setErr('');
    const { data, error } = await window.HOROS_SB.rpc('delete_archive_reason', { p_id: r.id });
    if (error || !data?.ok) setErr(data?.error === 'protected' ? 'Este motivo não pode ser apagado.' : 'Não foi possível apagar.');
    else await horosLoadArchiveReasons(true);
    setBusy(false);
  };
  const row = (r) => (
    <div key={r.id} className="arq-mgr-row">
      <span className="arq-opt-txt"><b>{r.label}</b><span>{r.desc}</span></span>
      <span className={`arq-tag ${r.recoverable ? 'rec' : 'fin'}`}>{r.recoverable ? 'Pode voltar' : 'Definitivo'}</span>
      <button className="icon-btn" title="Editar" onClick={() => setEdit({ id: r.id, label: r.label, descr: r.desc, recoverable: r.recoverable })}><Icon name="pencil" size={14} /></button>
      {r.id !== 'outro' && <button className="icon-btn" title="Apagar" onClick={() => del(r)}><Icon name="trash" size={14} /></button>}
    </div>);
  return ReactDOM.createPortal(
    <div className="arq-ov" style={{ zIndex: 10001 }} onMouseDown={e => { if (e.target === e.currentTarget && !busy) onClose(); }}>
      <div className="arq-modal" role="dialog" aria-label="Motivos de arquivamento">
        <div className="arq-head">
          <span className="arq-ic"><Icon name="archive" size={20} /></span>
          <div style={{ minWidth: 0 }}><h3>Motivos de arquivamento</h3>
            <p>Escolha quais motivos deixam o contato <b>voltar ao funil</b> e quais são definitivos.</p></div>
          <button className="arq-x" onClick={onClose} aria-label="Fechar"><Icon name="close" size={16} /></button>
        </div>
        {edit ? (
          <div className="arq-mgr-form">
            <label className="arq-note"><span>Nome</span>
              <input autoFocus maxLength={60} value={edit.label} onChange={e => setEdit({ ...edit, label: e.target.value })} placeholder="Ex.: Arquivado Serasa" /></label>
            <label className="arq-note"><span>Descrição (opcional)</span>
              <input maxLength={120} value={edit.descr || ''} onChange={e => setEdit({ ...edit, descr: e.target.value })} /></label>
            <label className="arq-mgr-check"><input type="checkbox" checked={edit.recoverable} onChange={e => setEdit({ ...edit, recoverable: e.target.checked })} />
              Pode voltar ao funil (gerente ou superintendente recupera)</label>
            {err && <div className="arq-err">{err}</div>}
            <div className="arq-foot">
              <button className="arq-btn ghost" onClick={() => { setEdit(null); setErr(''); }} disabled={busy}>Voltar</button>
              <button className="arq-btn" onClick={save} disabled={!edit.label.trim() || busy}>{busy ? 'Salvando…' : 'Salvar'}</button>
            </div>
          </div>
        ) : (
          <>
            <div className="arq-mgr-list">{reasons.map(row)}</div>
            {err && <div className="arq-err">{err}</div>}
            <div className="arq-foot">
              <button className="arq-btn ghost" onClick={onClose}>Fechar</button>
              <button className="arq-btn" onClick={() => setEdit({ id: null, label: '', descr: '', recoverable: true })}>+ Novo motivo</button>
            </div>
          </>
        )}
      </div>
    </div>, document.body);
}

function ArchiveReasonModal({ count, names, onCancel, onConfirm }) {
  const [reason, setReason] = React.useState('');
  const [note, setNote] = React.useState('');
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState('');
  const needNote = reason === 'outro' && !note.trim();
  const { reasons, canEdit } = useArchiveReasons();
  const [mgr, setMgr] = React.useState(false);
  const picked = reasons.find(r => r.id === reason);
  React.useEffect(() => {
    const k = e => { if (e.key === 'Escape' && !busy && !mgr) onCancel(); };
    window.addEventListener('keydown', k, true);
    return () => window.removeEventListener('keydown', k, true);
  }, [busy, onCancel, mgr]);
  const go = async () => {
    if (!reason || needNote || busy) return;
    setBusy(true); setErr('');
    const e = await onConfirm(reason, note.trim());
    if (e) { setErr(e); setBusy(false); }
  };
  const who = count === 1 ? (names?.[0] || 'este lead') : `${count} leads`;
  return ReactDOM.createPortal(
    <div className="arq-ov" onMouseDown={e => { if (e.target === e.currentTarget && !busy) onCancel(); }}>
      <div className="arq-modal" role="dialog" aria-label="Arquivar lead">
        <div className="arq-head">
          <span className="arq-ic"><Icon name="archive" size={20} /></span>
          <div style={{ minWidth: 0 }}>
            <h3>Arquivar {count === 1 ? 'lead' : `${count} leads`}</h3>
            <p>Por que <b>{who}</b> {count === 1 ? 'está saindo' : 'estão saindo'} do funil?</p>
          </div>
          <button className="arq-x" onClick={onCancel} disabled={busy} aria-label="Fechar"><Icon name="close" size={16} /></button>
        </div>
        {[[true, 'Pode voltar ao funil'], [false, 'Definitivo']].map(([rec, title]) => {
          const list = reasons.filter(r => !!r.recoverable === rec);
          return list.length ? (
            <React.Fragment key={title}>
              <div className="arq-sec">{title}</div>
              <div className="arq-grid" role="radiogroup">
                {list.map(r => (
                  <button key={r.id} type="button" role="radio" aria-checked={reason === r.id}
                    className={`arq-opt${reason === r.id ? ' on' : ''}`} onClick={() => setReason(r.id)}>
                    <span className="arq-dot" />
                    <span className="arq-opt-txt"><b>{r.label}</b><span>{r.desc}</span></span>
                  </button>
                ))}
              </div>
            </React.Fragment>) : null;
        })}
        {canEdit && <button type="button" className="arq-mgr-link" onClick={() => setMgr(true)}>Editar motivos</button>}
        {mgr && <ArchiveReasonsManager onClose={() => setMgr(false)} />}
        <label className="arq-note">
          <span>Observação {reason === 'outro' ? '(obrigatória)' : '(opcional)'}</span>
          <textarea rows={2} maxLength={500} value={note} onChange={e => setNote(e.target.value)}
            placeholder="Ex.: comprou na construtora X, pediu para ligar em março…" />
        </label>
        <p className="arq-hint">{picked && !picked.recoverable
          ? 'Motivo definitivo: o lead fica guardado em Clientes → Arquivados e não volta ao funil.'
          : 'O lead sai do funil e fica guardado com o motivo em Clientes → Arquivados. Gerente e superintendente podem recuperar.'}</p>
        {err && <div className="arq-err">{err}</div>}
        <div className="arq-foot">
          <button type="button" className="arq-btn ghost" onClick={onCancel} disabled={busy}>Cancelar</button>
          <button type="button" className="arq-btn danger" onClick={go} disabled={!reason || needNote || busy}>
            {busy ? 'Arquivando…' : 'Arquivar'}
          </button>
        </div>
      </div>
    </div>, document.body);
}

// Pergunta o motivo e arquiva. Devolve os ids arquivados ([] se cancelou ou nada deu certo).
function horosArchiveLeads(ids, opts = {}) {
  ids = (ids || []).filter(Boolean);
  if (!ids.length) return Promise.resolve([]);
  return new Promise(resolve => {
    const host = document.createElement('div');
    document.body.appendChild(host);
    const root = ReactDOM.createRoot(host);
    const close = (res) => { root.unmount(); host.remove(); resolve(res); };
    root.render(
      <ArchiveReasonModal count={ids.length} names={opts.names}
        onCancel={() => close([])}
        onConfirm={async (reason, note) => {
          const ok = await runBulkLeadRpc(ids, 'archive_lead', { p_reason: reason, p_note: note || null });
          if (!ok.length) return 'Não foi possível arquivar. Verifique se você tem permissão sobre esse lead.';
          if (ok.length < ids.length) {
            window.HorosNotif?.notify({ title: 'Alguns leads não foram arquivados', body: `${ids.length - ok.length} lead(s) sem permissão.`, module: 'system', type: 'info', priority: 'low' });
          }
          window.dispatchEvent(new CustomEvent('horos:lead-archived', { detail: { ids: ok, reason } }));
          close(ok);
          return null;
        }} />
    );
  });
}

// Venda registrada ao mover para "Fechado" (2026-11-09): abas Minha Casa Minha Vida (venda) e Aluguel →
// RPC register_sale grava no lead (data.venda) e lança a comissão do vendedor + o que a imobiliária tem a receber
// (comissão e, no aluguel, a taxa de administração de cada mês) no Financeiro.
const _saleBRL = (n) => Number(n || 0).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' });
const _saleMask = (s) => {
  const d = String(s || '').replace(/\D/g, '').slice(0, 13);
  return d ? (Number(d) / 100).toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : '';
};
const _saleNum = (s) => Number(String(s || '').replace(/\./g, '').replace(',', '.')) || 0;
const _saleMoney = (n) => n ? _saleMask(Math.round(Number(n) * 100)) : '';
const _salePct = (n) => n == null || n === '' ? '' : String(n).replace('.', ',');
const _saleDate = (iso) => iso ? new Date(iso + 'T12:00:00').toLocaleDateString('pt-BR') : '';
// mesma chave do banco (lead_region_key): sem acento, minúscula, espaços únicos
const _saleKey = (s) => String(s || '').normalize('NFD').replace(/[̀-ͯ]/g, '').trim().replace(/\s+/g, ' ').toLowerCase();
const _saleAddDays =(iso, d) => { const t = new Date(iso + 'T12:00:00'); t.setDate(t.getDate() + d); return t.toLocaleDateString('sv-SE'); };

// "fechado" ou etapa depois dele no funil do lead (ex.: "Comprou") — mesma regra de lead_is_won_col no banco
function horosIsWonCol(col, pid) {
  if (col === 'fechado') return true;
  const cols = pipelineCols(pid || 'main') || [];
  const iF = cols.findIndex(c => c.id === 'fechado');
  const iC = cols.findIndex(c => c.id === col);
  return iF >= 0 && iC > iF && !isArchivedCol(col);
}
window.horosIsWonCol = horosIsWonCol;

function SaleModal({ leadId, onClose }) {
  const today = new Date().toLocaleDateString('sv-SE');
  const [info, setInfo] = React.useState(null);
  const [kind, setKind] = React.useState(() => { try { return localStorage.getItem('horos-sale-kind') || 'venda'; } catch { return 'venda'; } });
  const [f, setF] = React.useState({ vgv: '', rent: '', months: '30', admin: '', start: today, guarantee: '',
    project: '', payer: '', sign: today, due: '', rate: '', share: '', bonus: '' });
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState('');
  const [saveProd, setSaveProd] = React.useState(false);
  const imob =window.horosSegHas ? window.horosSegHas('properties') : true;
  React.useEffect(() => {
    window.HOROS_SB.rpc('get_sale_info', { p_lead: leadId }).then(({ data }) => {
      if (!data || data.error) { onClose(false); return; }
      setInfo(data);
      const v = data.venda || {};
      if (v.tipo) setKind(v.tipo);
      setF(p => ({ ...p,
        vgv: _saleMoney(v.vgv || (v.tipo !== 'aluguel' ? data.value : 0)), rent: _saleMoney(v.aluguel),
        months: v.prazo_meses ? String(v.prazo_meses) : p.months, admin: _salePct(v.taxa_adm), start: v.inicio || p.start,
        guarantee: v.garantia || '', project: v.projeto || '', payer: v.construtora || v.proprietario || '',
        sign: v.assinatura || p.sign, due: v.previsao_repasse || '', bonus: _saleMoney(v.premio),
        rate: _salePct(v.pct_imobiliaria), share: _salePct(v.pct_corretor) }));
    }, () => onClose(false));
  }, [leadId]);
  React.useEffect(() => {
    const k = e => { if (e.key === 'Escape' && !busy) onClose(false); };
    window.addEventListener('keydown', k, true);
    return () => window.removeEventListener('keydown', k, true);
  }, [busy]);
  if (!info) return null;
  const rent = kind === 'aluguel' && imob;
  const money = new Set(['vgv', 'rent', 'bonus']);
  const set = (k) => (e) => setF(p => ({ ...p, [k]: money.has(k) ? _saleMask(e.target.value) : e.target.value }));
  const pickKind = (k) => { setKind(k); setErr(''); try { localStorage.setItem('horos-sale-kind', k); } catch {} };
  const base = _saleNum(rent ? f.rent : f.vgv);
  // % de cada venda: regra da imobiliária → produto (empreendimento) → gestão venda a venda (trava para o corretor);
  // o corretor só mexe no % que o produto não fixou — mesma ordem do register_sale
  const mgr = info.can_edit_rates;
  const v0 = info.venda || {};
  const locked = !mgr && v0.travado && v0.tipo === (rent ? 'aluguel' : 'venda');
  const prod = !rent && f.project.trim() ? (info.products || []).find(p => p.key === _saleKey(f.project)) : null;
  const canRate = mgr || (!locked && prod?.rate == null);
  const canShare = mgr || (!locked && prod?.seller_share == null);
  const defRate = prod?.rate ?? (rent ? info.rent_rate : info.rate);
  const defShare = prod?.seller_share ?? info.seller_share;
  const rate = (canRate || locked) && f.rate !== '' ? _saleNum(f.rate) : defRate;
  const share = (canShare || locked) && f.share !== '' ? _saleNum(f.share) : defShare;
  const bonus = _saleNum(f.bonus);
  const gross = base * rate / 100 + bonus;
  const tax = gross * (info.tax_rate || 0) / 100;
  const net = gross - tax;
  const seller = net * share / 100;
  const fee = rent ? base * _saleNum(f.admin) / 100 : 0;
  const months = Math.max(0, Math.min(60, parseInt(f.months, 10) || 0));
  const who = info.seller_is_me ? 'você' : (info.seller || 'quem vendeu');
  const save = async () => {
    if (base <= 0 || busy) return;
    setBusy(true); setErr('');
    const p = { kind: rent ? 'aluguel' : 'venda', project: f.project.trim(), payer: f.payer.trim(), sign_date: f.sign || null,
      due: f.due || null, bonus,
      ...(canRate && f.rate !== '' ? { rate } : {}), ...(canShare && f.share !== '' ? { seller_share: share } : {}),
      ...(mgr && saveProd && !rent && f.project.trim() ? { save_product: true, rate, seller_share: share } : {}),
      ...(rent ? { rent: base, months, admin_rate: _saleNum(f.admin), start_date: f.start || null, guarantee: f.guarantee } : { vgv: base }) };
    const { data, error } = await window.HOROS_SB.rpc('register_sale', { p_lead: leadId, p_data: p });
    if (error || data?.error) { setErr(data?.error || 'Não foi possível registrar. Tente de novo.'); setBusy(false); return; }
    window.dispatchEvent(new CustomEvent('horos:lead-updated', { detail: { id: leadId, value: base } }));
    window.dispatchEvent(new CustomEvent('horos:toast', { detail: { module: 'system', title: rent ? 'Aluguel registrado' : 'Venda registrada',
      body: data.commission ? `Comissão de ${_saleBRL(data.amount)} lançada no Financeiro.` : 'Dados salvos na ficha.' } }));
    onClose(true);
  };
  const lockWhy = locked ? 'Definido pela gestão nesta venda' : prod ? `Definido para o produto ${prod.name}` : '';
  const pctInput = (k, def, can) => (<>
    <input inputMode="decimal" value={can || locked ? f[k] : _salePct(def)} placeholder={_salePct(def)}
      onChange={set(k)} disabled={!can} title={can ? '' : lockWhy} />
    {!can && lockWhy && <small className="sale-hint">{lockWhy}</small>}
  </>);
  return ReactDOM.createPortal(
    <div className="lem-ov" onMouseDown={e => { if (e.target === e.currentTarget && !busy) onClose(false); }}>
      <div className="lem-box sale-box" role="dialog" aria-label="Registrar venda">
        <div className="lem-head"><b>{rent ? 'Registrar aluguel' : 'Registrar venda'} · {info.name}</b>
          <button className="lem-x" onClick={() => onClose(false)} disabled={busy} aria-label="Fechar"><Icon name="close" size={16} /></button>
        </div>
        <div className="lem-body">
          {imob && (
            <div className="sale-tabs" role="tablist">
              {[['venda', 'Minha Casa Minha Vida'], ['aluguel', 'Aluguel']].map(([k, l]) => (
                <button key={k} type="button" role="tab" aria-selected={kind === k} className={kind === k ? 'on' : ''} onClick={() => pickKind(k)}>{l}</button>
              ))}
            </div>
          )}
          <div className="sale-grid">
            {rent ? (<>
              <label className="lem-field"><span>Valor do aluguel (R$/mês) *</span>
                <input inputMode="numeric" value={f.rent} onChange={set('rent')} placeholder="0,00" autoFocus /></label>
              <label className="lem-field"><span>Imóvel</span>
                <input value={f.project} onChange={set('project')} maxLength={160} placeholder="Ex.: Rua das Flores, 120 — apto 31" /></label>
              <label className="lem-field"><span>Proprietário (paga a comissão)</span>
                <input value={f.payer} onChange={set('payer')} maxLength={120} /></label>
              <label className="lem-field"><span>Garantia</span>
                <select value={f.guarantee} onChange={set('guarantee')}>
                  <option value="">—</option>
                  {['Fiador', 'Caução', 'Seguro-fiança', 'Título de capitalização', 'Sem garantia'].map(g => <option key={g}>{g}</option>)}
                </select></label>
              <label className="lem-field"><span>Data de assinatura do contrato</span>
                <input type="date" value={f.sign} max={today} onChange={set('sign')} /></label>
              <label className="lem-field"><span>Início da locação</span>
                <input type="date" value={f.start} onChange={set('start')} /></label>
              <label className="lem-field"><span>Prazo (meses)</span>
                <input inputMode="numeric" value={f.months} onChange={e => setF(p => ({ ...p, months: e.target.value.replace(/\D/g, '').slice(0, 2) }))} /></label>
              <label className="lem-field"><span>Taxa de administração (% ao mês)</span>
                <input inputMode="decimal" value={f.admin} onChange={set('admin')} placeholder="Ex.: 10 · vazio se não administra" /></label>
            </>) : (<>
              <label className="lem-field"><span>VGV — valor da venda (R$) *</span>
                <input inputMode="numeric" value={f.vgv} onChange={set('vgv')} placeholder="0,00" autoFocus /></label>
              <label className="lem-field"><span>{imob ? 'Projeto / empreendimento' : 'Projeto / produto'}</span>
                <input value={f.project} onChange={set('project')} maxLength={160} list="sale-products" placeholder={imob ? 'Ex.: Residencial Jardins' : ''} />
                <datalist id="sale-products">{(info.products || []).map(p => <option key={p.key} value={p.name} />)}</datalist></label>
              <label className="lem-field"><span>{imob ? 'Construtora' : 'Quem paga a comissão'}</span>
                <input value={f.payer} onChange={set('payer')} maxLength={120} /></label>
              <label className="lem-field"><span>Data de assinatura</span>
                <input type="date" value={f.sign} max={today} onChange={set('sign')} />
                {f.sign && <small className="sale-hint">Venda confirmada em {_saleDate(_saleAddDays(f.sign, 7))} (7 dias após a assinatura)</small>}</label>
            </>)}
            <label className="lem-field"><span>{rent ? '% de intermediação (sobre o aluguel)' : '% comissão da imobiliária'}</span>{pctInput('rate', defRate, canRate)}</label>
            <label className="lem-field"><span>% do corretor (da comissão líquida)</span>{pctInput('share', defShare, canShare)}</label>
            {mgr && !rent && f.project.trim() && (
              <label className="sale-check"><input type="checkbox" checked={saveProd} onChange={e => setSaveProd(e.target.checked)} />
                <span>Usar sempre estes % para <b>{f.project.trim()}</b>{prod ? ' (substitui a regra atual do produto)' : ''}</span></label>
            )}
            <label className="lem-field"><span>Prêmio (R$)</span>
              <input inputMode="numeric" value={f.bonus} onChange={set('bonus')} placeholder="0,00" /></label>
            <label className="lem-field"><span>Previsão de repasse</span>
              <input type="date" value={f.due} onChange={set('due')} /></label>
          </div>
          {info.commission && base > 0 && (
            <div className="sale-sum">
              <div><span>Comissão{bonus > 0 ? ' + prêmio' : ''}</span><b>{_saleBRL(gross)}</b></div>
              <div><span>Imposto ({_salePct(info.tax_rate || 0)}%, pré-definido)</span><b>− {_saleBRL(tax)}</b></div>
              <div><span>Líquido da {segTerm('company', 'empresa')}</span><b>{_saleBRL(net - seller)}</b></div>
              <div><span>Comissão {info.seller_is_me ? 'sua' : `de ${who}`} ({_salePct(share)}%)</span><b>{_saleBRL(seller)}</b></div>
              {fee > 0 && months > 0 && <div><span>Taxa de administração</span><b>{_saleBRL(fee)}/mês × {months}</b></div>}
              <p>Vai para Financeiro → Comissões (a pagar {info.seller_is_me ? 'para você' : `para ${who}`} e a receber {rent ? 'do proprietário' : 'da construtora'}).{mgr ? '' : locked ? ' Percentuais definidos pela gestão nesta venda.' : prod ? ` Percentuais do produto ${prod.name}${canRate || canShare ? '; o que ele não fixou você ajusta' : ''}.` : ' Sem regra para este produto: confira os percentuais combinados.'}</p>
            </div>
          )}
          {err && <div className="lem-err">{err}</div>}
          <div className="lem-foot">
            <button className="lem-btn" onClick={() => onClose(false)} disabled={busy}>Depois</button>
            <button className="lem-btn lem-btn--primary" onClick={save} disabled={busy || base <= 0}>{busy ? 'Salvando…' : (rent ? 'Registrar aluguel' : 'Registrar venda')}</button>
          </div>
        </div>
      </div>
    </div>, document.body);
}

// Abre a janela de venda (um lead por vez). Chamar depois que o lead foi para "fechado".
let _saleQueue = Promise.resolve();
function horosAskSale(leadIds) {
  for (const id of [].concat(leadIds || []).filter(Boolean)) {
    _saleQueue = _saleQueue.then(() => new Promise(resolve => {
      const host = document.createElement('div');
      document.body.appendChild(host);
      const root = ReactDOM.createRoot(host);
      root.render(<SaleModal leadId={id} onClose={(ok) => { root.unmount(); host.remove(); resolve(ok); }} />);
    }));
  }
  return _saleQueue;
}
window.horosAskSale = horosAskSale;

const KANBAN_PALETTE = [
  '#1F93FF','#60B8FF','#60a5fa','#38bdf8','#06b6d4','#2dd4bf',
  '#34d399','#10b981','#fbbf24','#f97316','#ef4444','#ec4899',
  '#a78bfa','#f43f5e','#94a3b8','#64748b',
];

function KanbanEditModal({ columns, leads, onSave, onClose }) {
  const [cols, setCols] = React.useState(() => columns.map(c => ({ ...c })));
  const [pickerFor, setPickerFor] = React.useState(null);
  const [saving, setSaving] = React.useState(false);

  const leadCount = (id) => (leads || []).filter(l => l.col === id).length;
  const update = (id, key, val) => setCols(p => p.map(c => c.id === id ? { ...c, [key]: val } : c));
  const moveUp = (i) => { if (i === 0 || cols[i].id === 'inbox' || cols[i-1]?.id === 'inbox') return; setCols(p => { const a = [...p]; [a[i-1],a[i]]=[a[i],a[i-1]]; return a; }); };
  const moveDown = (i) => setCols(p => { if (i >= p.length-1 || p[i].id === 'inbox') return p; const a = [...p]; [a[i],a[i+1]]=[a[i+1],a[i]]; return a; });
  const del = (id) => { if (cols.length > 1) setCols(p => p.filter(c => c.id !== id)); };

  // Arrastar pela alça ⋮⋮ — o Inbox é fixo: não se arrasta nem se solta nada acima dele
  const [armed, setArmed]     = React.useState(null);   // id com a alça pressionada
  const [dragId, setDragId]   = React.useState(null);
  const [dropAt, setDropAt]   = React.useState(null);   // índice onde vai entrar
  const minIdx = cols[0]?.id === 'inbox' ? 1 : 0;
  React.useEffect(() => {
    if (!armed || dragId) return;
    const up = () => setArmed(null);
    window.addEventListener('mouseup', up);
    return () => window.removeEventListener('mouseup', up);
  }, [armed, dragId]);
  const onRowDragOver = (e, idx) => {
    if (!dragId) return;
    e.preventDefault();
    const r = e.currentTarget.getBoundingClientRect();
    const at = Math.max(minIdx, e.clientY < r.top + r.height / 2 ? idx : idx + 1);
    if (at !== dropAt) setDropAt(at);
  };
  const finishDrag = () => {
    if (dragId && dropAt != null) {
      setCols(p => {
        const from = p.findIndex(c => c.id === dragId);
        if (from < 0) return p;
        const a = [...p];
        const [item] = a.splice(from, 1);
        a.splice(dropAt > from ? dropAt - 1 : dropAt, 0, item);
        return a;
      });
    }
    setDragId(null); setDropAt(null); setArmed(null);
  };
  const add = () => setCols(p => [...p, { id: 'col_' + Date.now(), title: 'Nova Etapa', color: '#1F93FF' }]);

  const deletedIds = columns.filter(c => !cols.find(n => n.id === c.id)).map(c => c.id);
  const affectedLeads = (leads || []).filter(l => deletedIds.includes(l.col)).length;

  const handleSave = async () => {
    if (saving || cols.length === 0) return;
    setSaving(true);
    await onSave(cols);
    setSaving(false);
  };

  return (
    <div className="kem-overlay" onClick={e => e.target === e.currentTarget && onClose()}>
      <div className="kem-box">

        <div className="kem-head">
          <div>
            <div className="kem-title">Editar Funil</div>
            <div className="kem-sub">
              <span className="kem-sub-grip">
                <svg width="10" height="13" viewBox="0 0 12 16" fill="currentColor"><circle cx="3.5" cy="3" r="1.4"/><circle cx="8.5" cy="3" r="1.4"/><circle cx="3.5" cy="8" r="1.4"/><circle cx="8.5" cy="8" r="1.4"/><circle cx="3.5" cy="13" r="1.4"/><circle cx="8.5" cy="13" r="1.4"/></svg>
              </span>
              Segure e arraste para reordenar as etapas
            </div>
          </div>
          <button className="kem-close" onClick={onClose}><Icon name="close" size={16} /></button>
        </div>

        <div className="kem-list">
          {cols.map((col, idx) => {
            const cnt = leadCount(col.id);
            const fixed = col.id === 'inbox';
            const lineTop = dragId && dropAt === idx;
            const lineBottom = dragId && dropAt === cols.length && idx === cols.length - 1;
            const upOff = idx === 0 || fixed || cols[idx-1]?.id === 'inbox';
            const downOff = idx === cols.length - 1 || fixed;
            const delOff = cols.length <= 1 || fixed;
            return (
              <div key={col.id}
                className={`kem-row${dragId === col.id ? ' dragging' : ''}${lineTop ? ' drop-top' : ''}${lineBottom ? ' drop-bottom' : ''}${dragId ? ' drag-active' : ''}`}
                draggable={!fixed && armed === col.id}
                onDragStart={e => { e.dataTransfer.effectAllowed = 'move'; try { e.dataTransfer.setData('text/plain', col.id); } catch {} setDragId(col.id); setPickerFor(null); }}
                onDragOver={e => onRowDragOver(e, idx)}
                onDrop={e => { e.preventDefault(); finishDrag(); }}
                onDragEnd={finishDrag}>

                <span className={`kem-grip${fixed ? ' fixed' : ''}${dragId ? ' grabbing' : ''}`}
                  title={fixed ? 'Etapa fixa' : 'Arraste para reordenar'}
                  onMouseDown={() => !fixed && setArmed(col.id)}
                  onMouseUp={() => { if (!dragId) setArmed(null); }}>
                  <svg width="14" height="18" viewBox="0 0 12 16" fill="currentColor" aria-hidden="true">
                    <circle cx="3.5" cy="3" r="1.4"/><circle cx="8.5" cy="3" r="1.4"/>
                    <circle cx="3.5" cy="8" r="1.4"/><circle cx="8.5" cy="8" r="1.4"/>
                    <circle cx="3.5" cy="13" r="1.4"/><circle cx="8.5" cy="13" r="1.4"/>
                  </svg>
                </span>

                <div style={{ position:'relative', flexShrink:0 }}>
                  <button className="kem-color" onClick={() => setPickerFor(pickerFor === col.id ? null : col.id)} style={{ background: col.color }} />
                  {pickerFor === col.id && (
                    <div className="kem-palette">
                      {KANBAN_PALETTE.map(c => (
                        <button key={c} onClick={() => { update(col.id,'color',c); setPickerFor(null); }}
                          className={`kem-swatch${col.color === c ? ' on' : ''}`} style={{ background: c }} />
                      ))}
                    </div>
                  )}
                </div>

                <input className="kem-input" value={col.title} readOnly={fixed}
                  onChange={e => !fixed && update(col.id,'title',e.target.value)} />

                {fixed && <span className="kem-badge fixed">fixo</span>}
                {!fixed && cnt > 0 && <span className="kem-badge">{cnt}</span>}

                <button className="kem-arrow" onClick={() => moveUp(idx)} disabled={upOff}>▲</button>
                <button className="kem-arrow" onClick={() => moveDown(idx)} disabled={downOff}>▼</button>

                <button className="kem-del" onClick={() => del(col.id)} disabled={delOff}
                  title={fixed ? 'Etapa fixa — não pode ser removida' : cnt > 0 ? `${cnt} lead(s) serão movidos para "${cols[0]?.title}"` : 'Remover etapa'}>
                  <Icon name="close" size={14} />
                </button>
              </div>
            );
          })}

          <button className="kem-add" onClick={add}>
            <svg width="11" height="11" viewBox="0 0 12 12" fill="none"><path d="M6 1v10M1 6h10" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/></svg>
            Nova etapa
          </button>
        </div>

        <div className="kem-foot">
          {affectedLeads > 0 && (
            <span className="kem-warn"><Icon name="alert" size={12} />{affectedLeads} lead(s) serão movidos para "{cols[0]?.title}"</span>
          )}
          <button className="kem-btn" onClick={onClose}>Cancelar</button>
          <button className="kem-btn primary" onClick={handleSave} disabled={saving}>
            {saving ? 'Salvando…' : 'Salvar'}
          </button>
        </div>
      </div>
    </div>
  );
}

// ── Pipeline stages (agrupados para exibição compacta nos cards) ─────────────
const PIPE_STAGES = [
  { key: 'entrada',  label: 'Entrada',  cols: ['inbox'] },
  { key: 'contatos', label: 'Contatos', cols: ['primeiro_contato','contato1','contato2','contato3','contato4','contato5','contato6','contato7','em_conversa'] },
  { key: 'docs',     label: 'Docs',     cols: ['aguardando_docs'] },
  { key: 'agenda',   label: 'Agenda',   cols: ['aguardando_agenda','agendado'] },
  { key: 'analise',  label: 'Análise',  cols: ['em_analise','aprovado','aprovado_final','arquivado_aprovado'] },
  { key: 'fechado',  label: 'Fechado',  cols: ['fechado'] },
];
const STAGE_IDX = {};
PIPE_STAGES.forEach((s, i) => s.cols.forEach(c => { STAGE_IDX[c] = i; }));

// Fase do pipeline para qualquer etapa — inclusive as criadas pelo usuário,
// que herdam a fase da etapa conhecida mais próxima antes dela no funil.
function stageIdxOf(col) {
  if (STAGE_IDX[col] !== undefined) return STAGE_IDX[col];
  const cols = loadKanbanCols();
  const pos = cols.findIndex(c => c.id === col);
  if (pos < 0 || ARCHIVE_COLS.has(col)) return -1;
  for (let i = pos - 1; i >= 0; i--) {
    if (STAGE_IDX[cols[i].id] !== undefined) return STAGE_IDX[cols[i].id];
  }
  return 0;
}

function PipelineStrip({ col }) {
  const cur = stageIdxOf(col);
  if (cur < 0) return null;
  return (
    <div className="crm-pipe-strip">
      <div className="crm-pipe-bars">
        {PIPE_STAGES.map((s, i) => (
          <div key={s.key}
               className={`crm-pipe-seg${i < cur ? ' past' : i === cur ? ' current' : ''}`}
               title={s.label} />
        ))}
      </div>
      <span className="crm-pipe-cur-label">{PIPE_STAGES[cur]?.label}</span>
    </div>
  );
}

// ── Doc checklist compacto (para coluna aguardando_docs) ──────────────────────
const CARD_DOCS = [
  { id: 'rg',         label: 'RG' },
  { id: 'cnh',        label: 'CNH' },
  { id: 'holerite_1', label: 'Holerite 1' },
  { id: 'holerite_2', label: 'Holerite 2' },
  { id: 'holerite_3', label: 'Holerite 3' },
  { id: 'ir',         label: 'Imp. Renda' },
  { id: 'extrato',    label: 'Extrato' },
  { id: 'comp_res',   label: 'Residência' },
  { id: 'ctps',       label: 'CTPS' },
  { id: 'fgts',       label: 'FGTS' },
  { id: 'certidao',   label: 'Certidão' },
];
const CORE_DOCS = new Set(['rg','holerite_1','ir','extrato','comp_res']);

function DocStrip({ docs }) {
  const d = docs || {};
  const visible = CARD_DOCS.filter(doc => d[doc.id] || CORE_DOCS.has(doc.id));
  const approved = visible.filter(doc => d[doc.id]?.status === 'approved').length;
  const total = visible.length;
  const allDone = approved === total;

  return (
    <div className="crm-doc-strip">
      <div className="crm-doc-strip-head">
        <span className="crm-doc-strip-title">Documentação</span>
        <span className={`crm-doc-strip-count${allDone ? ' done' : ''}`}>
          {approved}/{total}
        </span>
      </div>
      <div className="crm-doc-strip-items">
        {visible.map(doc => {
          const status = d[doc.id]?.status;
          const ok = status === 'approved';
          return (
            <span key={doc.id}
                  className={`crm-doc-pill${ok ? ' ok' : ''}`}
                  title={`${doc.label}: ${ok ? 'Enviado' : status === 'pending' ? 'Pendente' : 'Não enviado'}`}>
              {ok
                ? <svg width="9" height="9" viewBox="0 0 10 10" fill="none"><path d="M1.5 5l2.5 2.5L8.5 2" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>
                : <span className="crm-doc-dot" />
              }
              {doc.label}
            </span>
          );
        })}
      </div>
    </div>
  );
}

// ── Etiquetas do WhatsApp no card do Kanban ────────────────────────────────
// Lê o catálogo (wa_labels) e as conversas do usuário logado, casa pelo telefone do lead e
// grava no WhatsApp pelo mesmo endpoint da tela de Mensagens (PUT /api/whatsapp-instance).
// Nomes com prefixo KBWA_ para não colidir com as constantes _WA_* de messages.jsx.
const KBWA_COLORS = [
  '#ff9485','#64c4ff','#ffd429','#dfaef0','#99b6c1','#55ccb3','#ff9dff','#d3a91d','#6d7cce','#d7e752',
  '#00d0e2','#ffc5c7','#93ceac','#f74848','#00a0f2','#83e422','#ffaf04','#b5ebff','#9ba6ff','#9368cf',
];
const kbWaColor = (idx) => KBWA_COLORS[((idx || 0) >>> 0) % KBWA_COLORS.length];
// Listas do sistema ("Não lidas", "Grupos", "Favoritos") não são etiquetas de contato
const kbWaIsSystem = (l) => /^(nao lidas|no lidas|unread|grupos|groups|favoritos|favoritas|favorites)$/
  .test(String(l?.name || '').normalize('NFD').replace(/[^a-zA-Z ]/g, '').trim().toLowerCase());
// Chave do telefone: só dígitos, sem o 55; também a variante com/sem o 9º dígito
function kbWaPhoneKeys(raw) {
  let d = String(raw || '').split('@')[0].replace(/\D/g, '');
  if (d.length > 13) return [];                       // LID, não é telefone
  if ((d.length === 12 || d.length === 13) && d.startsWith('55')) d = d.slice(2);
  if (d.length < 10) return [];
  const keys = [d];
  if (d.length === 11 && d[2] === '9') keys.push(d.slice(0, 2) + d.slice(3));
  if (d.length === 10) keys.push(d.slice(0, 2) + '9' + d.slice(2));
  return keys;
}

const kbWa = { catalog: null, byPhone: new Map(), loadedAt: 0, loading: null, listeners: new Set() };
const kbWaEmit = () => kbWa.listeners.forEach(f => f());

function kbWaIndex(conv) {
  const keys = kbWaPhoneKeys(conv.contact_phone).concat(kbWaPhoneKeys(conv.wa_jid));
  for (const k of keys) {
    const cur = kbWa.byPhone.get(k);
    // Prefere a conversa que já tem etiquetas; depois a do número (não @lid)
    const score = c => ((c.labels || []).length ? 2 : 0) + (String(c.wa_jid).endsWith('@s.whatsapp.net') ? 1 : 0);
    if (!cur || score(conv) > score(cur)) kbWa.byPhone.set(k, conv);
  }
}

// Conversas + etiquetas para os cards. O Realtime (kbWaSubscribe) mantém tudo em dia, então a
// recarga completa (até 20 páginas de 1000) só acontece a cada 10 min — e fica guardada na aba
// (sessionStorage) para trocar de tela sem baixar de novo. Antes: a cada 60 s e a cada foco (2026-10-06).
const KBWA_TTL = 10 * 60 * 1000;
function kbWaLoad(force) {
  if (kbWa.loading) return kbWa.loading;
  if (!force && kbWa.catalog && Date.now() - kbWa.loadedAt < KBWA_TTL) return Promise.resolve();
  kbWa.loading = (async () => {
    const sb = window.HOROS_SB;
    const uid = window.horosActUid?.() || window.HOROS_USER?.id;
    if (!sb || !uid) return;
    const ck = 'horos-kbwa:' + uid;
    let cached = null;
    if (!force) { try { cached = JSON.parse(sessionStorage.getItem(ck) || 'null'); } catch (_) {} }
    let cat, convs;
    if (cached && Date.now() - cached.at < KBWA_TTL) {
      ({ cat, convs } = cached);
      kbWa.loadedAt = cached.at;
      // ao conectar o Realtime, kbWaCatchUp traz o que mudou desde que a cópia foi guardada
      kbWa.checkedAt = new Date(cached.at - 5000).toISOString();
    } else {
      ({ data: cat } = await sb.from('wa_labels').select('label_id, name, color').eq('user_id', uid));
      convs = [];
      for (let page = 0; page < 20; page++) {               // PostgREST: máx. 1000 por consulta
        const { data, error } = await sb.from('conversations')
          .select('id, wa_jid, contact_phone, labels')
          .eq('user_id', uid).not('wa_jid', 'like', '%@g.us')
          .order('id').range(page * 1000, page * 1000 + 999);
        if (error || !data) break;
        convs.push(...data);
        if (data.length < 1000) break;
      }
      kbWa.loadedAt = Date.now();
      try { sessionStorage.setItem(ck, JSON.stringify({ at: kbWa.loadedAt, cat: cat || [], convs: convs.filter(c => (c.labels || []).length || c.contact_phone || c.wa_jid) })); } catch (_) {}
    }
    kbWa.catalog = (cat || []).filter(l => !kbWaIsSystem(l))
      .sort((a, b) => (parseInt(a.label_id, 10) || 0) - (parseInt(b.label_id, 10) || 0));
    kbWa.byPhone = new Map();
    convs.forEach(c => kbWaIndex({ ...c, labels: (Array.isArray(c.labels) ? c.labels : []).map(String) }));
    kbWaSubscribe(sb, uid);
  })().catch(() => {}).finally(() => { kbWa.loading = null; kbWaEmit(); });
  return kbWa.loading;
}

// Tempo real: etiqueta colocada/tirada no WhatsApp (ou conversa @lid ligada ao número) aparece
// no card sem recarregar. Catálogo novo (etiqueta criada/renomeada) → recarrega tudo.
// Canal caído (CHANNEL_ERROR/TIMED_OUT/CLOSED — aba dormindo, rede, token renovado) reinscreve
// sozinho; e uma conferência por `labels_updated_at` (5 s com o canal fora, 20 s com ele ativo, ao
// voltar para a aba e ao reconectar a rede) garante a etiqueta mesmo quando o Realtime perde o evento.
function kbWaApply(row) {
  if (!row || String(row.wa_jid || '').endsWith('@g.us')) return false;
  const conv = { id: row.id, wa_jid: row.wa_jid, contact_phone: row.contact_phone, labels: (Array.isArray(row.labels) ? row.labels : []).map(String) };
  // Atualiza a conversa já indexada (mesmo objeto em todas as chaves de telefone)
  let found = false;
  kbWa.byPhone.forEach(c => { if (c.id === conv.id) { c.labels = conv.labels; c.wa_jid = conv.wa_jid; c.contact_phone = conv.contact_phone; found = true; } });
  if (!found) kbWaIndex(conv);
  return true;
}

async function kbWaCatchUp() {
  const sb = window.HOROS_SB, uid = kbWa.uid;
  if (!sb || !uid || kbWa.catching) return;
  kbWa.catching = true;
  const since = kbWa.checkedAt || new Date(kbWa.loadedAt || Date.now()).toISOString();
  const startedAt = new Date(Date.now() - 5000).toISOString();   // folga para relógio/latência
  try {
    const { data, error } = await sb.from('conversations')
      .select('id, wa_jid, contact_phone, labels, labels_updated_at')
      .eq('user_id', uid).gt('labels_updated_at', since)
      .order('labels_updated_at', { ascending: false }).limit(500);
    if (!error) {
      kbWa.checkedAt = startedAt;
      if ((data || []).map(kbWaApply).some(Boolean)) kbWaEmit();
    }
  } catch (_) {} finally { kbWa.catching = false; }
}

// Etiqueta criada/renomeada no WhatsApp: recarrega só o catálogo (antes baixava todas as conversas de novo)
async function kbWaReloadCatalog() {
  const sb = window.HOROS_SB, uid = kbWa.uid;
  if (!sb || !uid) return;
  const { data: cat } = await sb.from('wa_labels').select('label_id, name, color').eq('user_id', uid);
  if (!cat) return;
  kbWa.catalog = cat.filter(l => !kbWaIsSystem(l))
    .sort((a, b) => (parseInt(a.label_id, 10) || 0) - (parseInt(b.label_id, 10) || 0));
  try { const ck = 'horos-kbwa:' + uid, c = JSON.parse(sessionStorage.getItem(ck) || 'null'); if (c) { c.cat = cat; sessionStorage.setItem(ck, JSON.stringify(c)); } } catch (_) {}
  kbWaEmit();
}

function kbWaSubscribe(sb, uid) {
  if (!sb?.channel) return;
  if (kbWa.uid && kbWa.uid !== uid && kbWa.channel) { try { sb.removeChannel(kbWa.channel); } catch (_) {} kbWa.channel = null; }
  kbWa.uid = uid;
  if (!kbWa.checkedAt) kbWa.checkedAt = new Date(Date.now() - 5000).toISOString();
  if (kbWa.channel) return;
  const ch = sb.channel(`kbwa-${uid}-${Date.now()}`)
    .on('postgres_changes', { event: 'UPDATE', schema: 'public', table: 'conversations', filter: `user_id=eq.${uid}` }, p => { if (kbWaApply(p.new)) kbWaEmit(); })
    .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'conversations', filter: `user_id=eq.${uid}` }, p => { if (kbWaApply(p.new)) kbWaEmit(); })
    .on('postgres_changes', { event: '*', schema: 'public', table: 'wa_labels', filter: `user_id=eq.${uid}` }, () => { setTimeout(kbWaReloadCatalog, 500); });
  kbWa.channel = ch;
  ch.subscribe(status => {
    if (kbWa.channel !== ch) return;
    if (status === 'SUBSCRIBED') {
      kbWa.rtOk = true; kbWa.retry = 0;
      kbWaCatchUp();                                      // o que mudou enquanto o canal estava fora
    } else if (status === 'CHANNEL_ERROR' || status === 'TIMED_OUT' || status === 'CLOSED') {
      kbWa.rtOk = false;
      kbWa.channel = null;                                // antes do remove: o "leave" chama este callback de novo (laço infinito no iPhone)
      try { sb.removeChannel(ch); } catch (_) {}
      const wait = Math.min(30000, 2000 * Math.pow(2, kbWa.retry || 0));
      kbWa.retry = (kbWa.retry || 0) + 1;
      clearTimeout(kbWa.retryTimer);
      kbWa.retryTimer = setTimeout(() => kbWaSubscribe(sb, uid), wait);
    }
  });
  if (!kbWa.timer) {
    let last = 0;
    kbWa.timer = setInterval(() => {
      if (document.hidden) return;
      const every = kbWa.rtOk ? 20000 : 5000;
      if (Date.now() - last < every) return;
      last = Date.now();
      kbWaCatchUp();
    }, 5000);
    const wake = () => { if (!document.hidden) { kbWaCatchUp(); if (!kbWa.channel && kbWa.uid) kbWaSubscribe(window.HOROS_SB, kbWa.uid); } };
    document.addEventListener('visibilitychange', wake);
    window.addEventListener('online', wake);
  }
}

// Adiciona/remove uma etiqueta no WhatsApp do contato do lead. Devolve '' ou a mensagem de erro.
async function kbWaToggle(lead, labelId, active) {
  const sb = window.HOROS_SB;
  const uid = window.horosActUid?.() || window.HOROS_USER?.id;
  const keys = kbWaPhoneKeys(lead.phone);
  if (!keys.length) return 'Lead sem telefone válido.';
  let conv = keys.map(k => kbWa.byPhone.get(k)).find(Boolean);
  if (!conv) {
    // Ainda não há conversa com esse número — cria para poder etiquetar
    const jid = '55' + keys[0] + '@s.whatsapp.net';
    const ins = await sb.from('conversations')
      .insert({ user_id: uid, wa_jid: jid, contact_phone: '55' + keys[0], contact_name: lead.name })
      .select('id, wa_jid, contact_phone, labels').single();
    let row = ins.data;
    if (!row) {
      const { data } = await sb.from('conversations').select('id, wa_jid, contact_phone, labels')
        .eq('user_id', uid).eq('wa_jid', jid).limit(1);
      row = data?.[0];
    }
    if (!row) return 'Não foi possível abrir a conversa deste contato.';
    conv = { ...row, labels: (row.labels || []).map(String) };
    kbWaIndex(conv);
  }
  const before = conv.labels;
  const id = String(labelId);
  conv.labels = active ? before.filter(l => l !== id) : [...new Set([...before, id])];
  kbWaEmit();
  try {
    const tok = window.horosToken ? await window.horosToken() : window.HOROS_SESSION?.access_token;
    const r = await fetch('/api/whatsapp-instance', {
      method: 'PUT',
      headers: { 'Authorization': `Bearer ${tok}`, 'Content-Type': 'application/json' },
      body: JSON.stringify({ conversationId: conv.id, labelId: id, labelAction: active ? 'remove' : 'add' }),
    });
    const d = await r.json().catch(() => ({}));
    if (!r.ok) throw new Error(d.error || 'O WhatsApp não aceitou a alteração.');
    if (Array.isArray(d.labels)) conv.labels = d.labels.map(String);
    kbWaEmit();
    return '';
  } catch (e) {
    conv.labels = before;                                  // o WhatsApp recusou: desfaz
    kbWaEmit();
    return e.message || 'O WhatsApp não aceitou a alteração.';
  }
}

// Coloca (add) ou tira uma etiqueta de vários leads de uma vez — 4 por vez para não sobrecarregar
// o WhatsApp. Só mexe em quem precisa (quem já tem não é tocado). Devolve { total, ok, fail, err }.
async function kbWaBulkLabel(leads, labelId, add, onProgress) {
  await kbWaLoad();
  const id = String(labelId);
  const seen = new Set();
  const todo = [];
  for (const l of leads) {
    const keys = kbWaPhoneKeys(l.phone);
    if (!keys.length || seen.has(keys[0])) continue;      // sem telefone ou mesmo contato repetido
    keys.forEach(k => seen.add(k));
    const conv = keys.map(k => kbWa.byPhone.get(k)).find(Boolean);
    const has = !!conv && (conv.labels || []).includes(id);
    if (add ? !has : has) todo.push(l);
  }
  let done = 0, ok = 0, fail = 0, err = '';
  onProgress && onProgress(0, todo.length);
  let i = 0;
  const worker = async () => {
    while (i < todo.length) {
      const lead = todo[i++];
      const e = await kbWaToggle(lead, id, !add);
      if (e) { fail++; err = err || e; } else ok++;
      done++;
      onProgress && onProgress(done, todo.length);
    }
  };
  await Promise.all(Array.from({ length: Math.min(4, todo.length) }, worker));
  return { total: todo.length, ok, fail, err };
}

function useKbWaLabels() {
  const [, force] = React.useState(0);
  React.useEffect(() => {
    const f = () => force(x => x + 1);
    kbWa.listeners.add(f);
    kbWaLoad();
    const onFocus = () => { kbWaLoad(); kbWaCatchUp(); };  // volta para a aba: confere o que mudou no WhatsApp
    window.addEventListener('focus', onFocus);
    return () => { kbWa.listeners.delete(f); window.removeEventListener('focus', onFocus); };
  }, []);
  return kbWa;
}

// Etiquetas do contato no card + botão para adicionar/remover (menu num portal: o card tem overflow hidden)
function KbWaLabels({ lead, disabled }) {
  const store = useKbWaLabels();
  const [open, setOpen] = React.useState(null);            // { top, left } do menu
  const [busy, setBusy] = React.useState(null);
  const [err, setErr]   = React.useState('');
  const btnRef = React.useRef(null);
  const popRef = React.useRef(null);

  React.useEffect(() => {
    if (!open) return;
    const close = (e) => {
      if (e.type === 'keydown' && e.key !== 'Escape') return;
      if (e.type === 'mousedown' && (popRef.current?.contains(e.target) || btnRef.current?.contains(e.target))) return;
      setOpen(null);
    };
    const onScroll = (e) => { if (!popRef.current?.contains(e.target)) setOpen(null); };
    document.addEventListener('mousedown', close);
    document.addEventListener('keydown', close);
    window.addEventListener('scroll', onScroll, true);
    return () => {
      document.removeEventListener('mousedown', close);
      document.removeEventListener('keydown', close);
      window.removeEventListener('scroll', onScroll, true);
    };
  }, [open]);

  const keys = kbWaPhoneKeys(lead.phone);
  const catalog = store.catalog || [];
  if (!keys.length || !catalog.length) return null;
  const conv = keys.map(k => store.byPhone.get(k)).find(Boolean);
  const active = new Set(conv?.labels || []);
  const current = catalog.filter(l => active.has(String(l.label_id)));

  const toggleOpen = (e) => {
    e.stopPropagation();
    if (open) { setOpen(null); return; }
    const r = btnRef.current.getBoundingClientRect();
    const h = Math.min(320, 44 + catalog.length * 32);
    const top = r.bottom + 6 + h > window.innerHeight ? Math.max(8, r.top - 6 - h) : r.bottom + 6;
    setErr('');
    setOpen({ top, left: Math.min(r.left, window.innerWidth - 248) });
  };

  const toggle = async (l) => {
    const id = String(l.label_id);
    setBusy(id); setErr('');
    const e = await kbWaToggle(lead, id, active.has(id));
    setBusy(null);
    if (e) setErr(e);
  };

  return (
    <div className="kbwa-row" onClick={e => e.stopPropagation()} onMouseDown={e => e.stopPropagation()}>
      {current.map(l => (
        <span key={l.label_id} className="kbwa-chip"
          style={{ '--lc': kbWaColor(l.color) }}>
          {l.name}
        </span>
      ))}
      <button ref={btnRef} className={`kbwa-add${open ? ' on' : ''}`} disabled={disabled}
        title="Etiquetas do WhatsApp" onClick={toggleOpen}>
        <Icon name="tag" size={12} stroke={2} />
        {current.length ? '' : 'Etiqueta'}
      </button>
      {open && ReactDOM.createPortal(
        <div ref={popRef} className="kbwa-pop" style={{ top: open.top, left: open.left }}
          onClick={e => e.stopPropagation()}>
          <div className="kbwa-pop-title">Etiquetas no WhatsApp</div>
          <div className="kbwa-pop-list">
            {catalog.map(l => {
              const on = active.has(String(l.label_id));
              return (
                <button key={l.label_id} className={`kbwa-pop-item${on ? ' on' : ''}`}
                  disabled={busy !== null} onClick={() => toggle(l)}>
                  <span className="kbwa-dot" style={{ background: kbWaColor(l.color) }} />
                  <span className="kbwa-pop-name">{l.name}</span>
                  {busy === String(l.label_id)
                    ? <span className="kbwa-spin" />
                    : on && <Icon name="check" size={13} stroke={2.4} />}
                </button>
              );
            })}
          </div>
          {err && <div className="kbwa-pop-err">{err}</div>}
        </div>,
        document.body
      )}
    </div>
  );
}

// ── Envia texto pelo WhatsApp do usuário logado para um telefone (acha ou cria a conversa) ──
// Corpo de envio de mídia para /api/whatsapp-send (2026-10-11). A Vercel recusa corpo acima de ~4,5 MB,
// então vídeo/áudio/foto grande nunca chegava (base64 de 13 MB = 17 MB). Agora:
//  · arquivo de resposta rápida (file.__qrRef) → manda só o caminho no bucket quick-replies;
//  · até 2,5 MB → base64 como antes;
//  · maior → sobe para wa-media/{uid}/out/… e manda o caminho (o servidor apaga depois de enviar).
const HOROS_WA_INLINE_MAX = 2.5 * 1024 * 1024;
async function horosWaMediaBody(file) {
  if (file && file.__qrRef) return { media: file.__qrRef };
  if (file.size <= HOROS_WA_INLINE_MAX) {
    const base64 = await new Promise((resolve, reject) => {
      const rd = new FileReader();
      rd.onload = (ev) => resolve(String(ev.target.result).split(',')[1]);
      rd.onerror = () => reject(new Error('Não foi possível ler o arquivo'));
      rd.readAsDataURL(file);
    });
    return { base64 };
  }
  const sb = window.HOROS_SB;
  const uid = (window.HOROS_SESSION && window.HOROS_SESSION.user && window.HOROS_SESSION.user.id) || (window.HOROS_USER && window.HOROS_USER.id);
  if (!sb || !uid) throw new Error('Sessão expirada');
  const safe = (file.name || 'arquivo').normalize('NFD').replace(/[^\w.\-]/g, '_').slice(-80) || 'arquivo';
  const path = `${uid}/out/${(crypto.randomUUID && crypto.randomUUID()) || Date.now().toString(36)}/${safe}`;
  const { error } = await sb.storage.from('wa-media').upload(path, file, {
    contentType: (file.type || 'application/octet-stream').split(';')[0], upsert: false,
  });
  if (error) throw new Error('Não foi possível enviar o arquivo (' + (error.message || 'armazenamento') + ')');
  return { media: { bucket: 'wa-media', path } };
}
window.horosWaMediaBody = horosWaMediaBody;

async function horosSendWaText(phone, name, text, files) {
  const sb = window.HOROS_SB;
  const uid = window.horosActUid?.() || window.HOROS_USER?.id;
  const keys = kbWaPhoneKeys(phone);
  if (!keys.length) return 'Lead sem telefone válido.';
  const vars = [...new Set(keys.flatMap(k => [k, '55' + k]))];
  const { data: found } = await sb.from('conversations').select('id, wa_jid')
    .eq('user_id', uid)
    .or(vars.map(v => `contact_phone.eq.${v},wa_jid.eq.${v}@s.whatsapp.net`).join(','))
    .limit(1);
  let convId = found?.[0]?.id;
  if (!convId) {
    const jid = '55' + keys[0] + '@s.whatsapp.net';
    const ins = await sb.from('conversations')
      .insert({ user_id: uid, wa_jid: jid, contact_phone: '55' + keys[0], contact_name: name || null })
      .select('id').single();
    convId = ins.data?.id;
    if (!convId) return 'Não foi possível abrir a conversa deste contato.';
  }
  const tok = window.horosToken ? await window.horosToken() : window.HOROS_SESSION?.access_token;
  const post = async (body) => {
    const r = await fetch('/api/whatsapp-send', {
      method: 'POST',
      headers: { 'Authorization': `Bearer ${tok}`, 'Content-Type': 'application/json' },
      body: JSON.stringify({ conversationId: convId, ...body }),
    });
    if (r.ok) return '';
    const d = await r.json().catch(() => ({}));
    return d.error || 'O WhatsApp não enviou a mensagem.';
  };
  if (!files || !files.length) return post({ text });
  // Anexos (respostas rápidas): o texto vai de legenda no 1º anexo que não é áudio; senão segue depois
  const mt = (m) => m.startsWith('image/') ? 'image' : m.startsWith('video/') ? 'video' : m.startsWith('audio/') ? 'audio' : 'document';
  const capIdx = files.findIndex(f => mt(f.type || '') !== 'audio');
  let textSent = !text;
  for (let i = 0; i < files.length; i++) {
    const f = files[i];
    const caption = i === capIdx && text ? text : '';
    if (caption) textSent = true;
    const e = await post({ mediaType: mt(f.type || ''), ...(await window.horosWaMediaBody(f)),
      mimetype: f.type || 'application/octet-stream', fileName: f.name, caption });
    if (e) return e;
  }
  return textSent ? '' : post({ text });
}

// ── Autorização de consulta de crédito (link para o cliente assinar) ──
// Tabela credit_auth_requests · página pública credit-authorization.html?t={token} · API /api/credit-auth
const CAP_STATUS = {
  pending:   { label: 'Link enviado — aguardando o cliente', cls: 'wait' },
  opened:    { label: 'O cliente abriu o link — ainda não assinou', cls: 'wait' },
  completed: { label: 'Autorização assinada', cls: 'ok' },
};
const capLink = (token) => `${location.origin}/credit-authorization.html?t=${token}`;
const capFmtDate = (ts) => ts ? new Date(ts).toLocaleString('pt-BR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }) : '';
const CAP_LABELS = {
  sexo: { feminino: 'Feminino', masculino: 'Masculino', nao_informado: 'Não informado' },
  estadoCivil: { solteiro: 'Solteiro(a)', casado: 'Casado(a)', uniao_estavel: 'União estável', divorciado: 'Divorciado(a)', viuvo: 'Viúvo(a)' },
};
const capAddr = (a) => !a ? '—' : a.nenhum ? 'Não possui' : a.mesmo ? 'Mesmo do residencial'
  : [a.endereco && `${a.endereco}, ${a.numero || 's/n'}`, a.complemento, a.bairro, a.cidade && `${a.cidade}/${a.estado}`,
     a.cep && `CEP ${String(a.cep).replace(/^(\d{5})(\d{3})$/, '$1-$2')}`].filter(Boolean).join(' · ') || '—';

function CreditAuthPanel({ lead, onSigned, compact }) {
  const [req, setReq]       = React.useState(undefined); // undefined = carregando, null = nenhum
  const [busy, setBusy]     = React.useState('');
  const [msg, setMsg]       = React.useState(null);      // { ok, text }
  const [details, setDetails] = React.useState(false);
  const signedRef = React.useRef(false);

  const load = React.useCallback(async () => {
    if (!lead?.id || !window.HOROS_SB) { setReq(null); return; }
    const { data } = await window.HOROS_SB.from('credit_auth_requests')
      .select('*').eq('lead_id', lead.id).neq('status', 'canceled')
      .order('created_at', { ascending: false }).limit(1);
    setReq(data?.[0] || null);
  }, [lead?.id]);
  React.useEffect(() => { load(); }, [load]);
  // Outro quadro da mesma tela (ficha: lateral + aba Crédito) gerou/cancelou um link
  React.useEffect(() => {
    const h = (e) => { if (e.detail === lead?.id) load(); };
    window.addEventListener('horos:credit-auth-changed', h);
    return () => window.removeEventListener('horos:credit-auth-changed', h);
  }, [lead?.id, load]);

  // Enquanto o cliente não assina, confere a cada 15s (e ao voltar para a aba)
  React.useEffect(() => {
    if (!req || req.status === 'completed') return;
    const t = setInterval(load, 15000);
    const onVis = () => { if (document.visibilityState === 'visible') load(); };
    document.addEventListener('visibilitychange', onVis);
    return () => { clearInterval(t); document.removeEventListener('visibilitychange', onVis); };
  }, [req?.id, req?.status, load]);

  // A selfie fica só no sistema (bucket privado, sem leitura pelo app) — o corretor não vê a foto
  React.useEffect(() => {
    if (req?.status !== 'completed') return;
    if (!signedRef.current) { signedRef.current = true; onSigned?.(req); }
  }, [req?.status]);

  const create = async () => {
    setBusy('create'); setMsg(null);
    if (req && req.status !== 'completed') {
      await window.HOROS_SB.from('credit_auth_requests').update({ status: 'canceled' }).eq('id', req.id);
    }
    const { data, error } = await window.HOROS_SB.from('credit_auth_requests')
      .insert({ lead_id: lead.id, workspace_id: window.HOROS_WORKSPACE?.workspace_id || null })
      .select('*').single();
    setBusy('');
    if (error) { setMsg({ ok: false, text: 'Não foi possível gerar o link: ' + error.message }); return; }
    signedRef.current = false;
    setReq(data);
    window.dispatchEvent(new CustomEvent('horos:credit-auth-changed', { detail: lead.id }));
    return data;
  };

  const copy = async (r = req) => {
    try { await navigator.clipboard.writeText(capLink(r.token)); setMsg({ ok: true, text: 'Link copiado.' }); }
    catch { setMsg({ ok: false, text: 'Não foi possível copiar — selecione o link e copie.' }); }
  };

  const sendWa = async () => {
    let r = req;
    if (!r || r.status === 'completed') r = await create();
    if (!r) return;
    setBusy('wa'); setMsg(null);
    const first = String(lead.name || '').trim().split(/\s+/)[0];
    const text = `Olá${first && !/^(lead|contato)$/i.test(first) ? ' ' + first : ''}! Para seguirmos com a sua análise de crédito, preciso da sua autorização para a consulta. `
      + `É rápido: preencha seus dados e tire uma selfie neste link:\n${capLink(r.token)}`;
    const err = await horosSendWaText(lead.phone, lead.name, text);
    setBusy('');
    setMsg(err ? { ok: false, text: err } : { ok: true, text: 'Link enviado pelo WhatsApp.' });
  };

  if (req === undefined) return <div className="cap-card" role="status" aria-label="Carregando autorização"><Skel h={13} w="55%" /><SkelText lines={2} last="70%" style={{ marginTop: 12 }} /><Skel h={34} r={8} style={{ marginTop: 14 }} /></div>;
  const st = req ? CAP_STATUS[req.status] : null;
  const a = req?.answers || {};
  const doc = a.documento || {};

  return (
    <div className={`cap-card${compact ? ' compact' : ''}`}>
      <div className="cap-head">
        <span className={`cap-ic ${st?.cls || ''}`}><Icon name={req?.status === 'completed' ? 'check' : 'shield'} size={15} stroke={2} /></span>
        <div style={{ minWidth: 0 }}>
          <div className="cap-title">Autorização de consulta de crédito</div>
          <div className="cap-sub">
            {!req ? 'Obrigatória antes da análise. O cliente preenche os dados, aceita o termo (SCR/Bacen e birôs) e tira uma selfie.'
              : req.status === 'completed' ? `Assinada em ${capFmtDate(req.completed_at)}${a.nome ? ` por ${a.nome} ${a.sobrenome || ''}`.trimEnd() : ''}`
              : `${st.label} · gerado em ${capFmtDate(req.created_at)}${req.opened_at ? ` · aberto em ${capFmtDate(req.opened_at)}` : ''}`}
          </div>
        </div>
      </div>

      {req && req.status !== 'completed' && (
        <div className="cap-link">
          <input readOnly value={capLink(req.token)} onFocus={e => e.target.select()} />
          <button type="button" className="cap-btn ghost" onClick={() => copy()}><Icon name="paperclip" size={13} /> Copiar</button>
        </div>
      )}

      {req?.status === 'completed' && (
        <div className="cap-signed">
          <div className="cap-selfie-lock" title="A selfie fica guardada no sistema e não é exibida">
            <Icon name="shield" size={20} stroke={1.8} />
            <span>{req.selfie_path ? 'Selfie registrada' : 'Sem selfie'}</span>
          </div>
          <div className="cap-signed-meta">
            <div><b>CPF</b> {a.cpf || '—'}</div>
            <div><b>Documento</b> {doc.numero ? `${doc.tipo || ''} ${doc.numero}`.trim() : '—'}</div>
            <div><b>Órgão emissor / UF</b> {[doc.orgao, doc.uf].filter(Boolean).join(' / ') || '—'}</div>
            <div><b>Data de emissão</b> {doc.dataEmissao ? doc.dataEmissao.split('-').reverse().join('/') : '—'}</div>
            <div><b>IP</b> {req.signer_ip || '—'}</div>
            <button type="button" className="cap-linkbtn" onClick={() => setDetails(v => !v)}>{details ? 'Ocultar detalhes' : 'Ver dados e termo aceito'}</button>
          </div>
        </div>
      )}
      {req?.status === 'completed' && details && (
        <div className="cap-details">
          <div><b>Nome</b> {`${a.nome || ''} ${a.sobrenome || ''}`.trim() || '—'}</div>
          <div><b>Celular</b> {window.fmtPhone ? window.fmtPhone(a.celular) : a.celular}</div>
          <div><b>E-mail</b> {a.email || '—'}</div>
          <div><b>Nascimento</b> {a.dataNascimento ? a.dataNascimento.split('-').reverse().join('/') : '—'}</div>
          <div><b>Sexo</b> {CAP_LABELS.sexo[a.sexo] || '—'}</div>
          <div><b>Estado civil</b> {CAP_LABELS.estadoCivil[a.estadoCivil] || '—'}</div>
          <div><b>Profissão</b> {a.profissao || '—'}</div>
          <div><b>Residencial</b> {capAddr(a.residencial)}</div>
          {a.comercial && <div><b>Comercial</b> {capAddr(a.comercial)}</div>}
          <div><b>Documento</b> {doc.numero ? `${doc.tipo || ''} ${doc.numero}`.trim() : '—'}</div>
          <div><b>Órgão emissor</b> {doc.orgao || '—'}</div>
          <div><b>UF do documento</b> {doc.uf || '—'}</div>
          <div><b>Data de emissão</b> {doc.dataEmissao ? doc.dataEmissao.split('-').reverse().join('/') : '—'}</div>
          <div><b>Nacionalidade</b> {doc.nacionalidade || '—'}</div>
          <div className="cap-consent"><b>Termo aceito ({req.consent_version})</b>{req.consent_text}</div>
          <div className="cap-sub">Navegador: {req.signer_ua || '—'}</div>
        </div>
      )}

      <div className="cap-actions">
        {(!req || req.status !== 'completed') && (
          <button type="button" className="cap-btn" onClick={sendWa} disabled={!!busy || !lead.phone}
            title={lead.phone ? '' : 'Lead sem telefone'}>
            <Icon name="message" size={13} /> {busy === 'wa' ? 'Enviando…' : req ? 'Reenviar pelo WhatsApp' : 'Enviar link pelo WhatsApp'}
          </button>
        )}
        {!req && (
          <button type="button" className="cap-btn ghost" disabled={!!busy}
            onClick={async () => { const r = await create(); if (r) copy(r); }}>
            <Icon name="paperclip" size={13} /> {busy === 'create' ? 'Gerando…' : 'Gerar e copiar link'}
          </button>
        )}
        {req && (
          <button type="button" className="cap-btn ghost" disabled={!!busy} onClick={create}
            title={req.status === 'completed' ? 'Pedir uma nova autorização (ex.: venceu ou mudou algum dado)' : 'Cancela o link atual e gera outro'}>
            <Icon name="refresh" size={13} /> {req.status === 'completed' ? 'Pedir nova autorização' : 'Gerar novo link'}
          </button>
        )}
      </div>
      {msg && <div className={`cap-msg ${msg.ok ? 'ok' : 'err'}`}>{msg.text}</div>}
    </div>
  );
}

// Conserta texto de planilha: UTF-8 lido como Latin-1 ("Ã§" → "ç", "Â " → espaço), espaços especiais e "=" solto do Excel
function horosFixText(v) {
  let s = String(v ?? '');
  if (/[ÃÂ][\u0080-¿]/.test(s) && !/[^\u0000-ÿ]/.test(s)) {
    try { s = decodeURIComponent(escape(s)); } catch {}
  }
  return s.replace(/[   ]/g, ' ').replace(/[​-‍﻿]/g, '')
    .replace(/^[\s=]+|[\s=]+$/g, '').replace(/\s+/g, ' ');
}
// Lê a 1ª aba de XLSX/XLS/CSV. CSV sem BOM o XLSX lê como Windows-1252 — decodifica como UTF-8 antes
function horosReadSheetRows(buf, fileName, opts) {
  let wb;
  if (/\.(csv|txt)$/i.test(fileName || '')) {
    let text;
    try { text = new TextDecoder('utf-8', { fatal: true }).decode(buf); }
    catch { text = new TextDecoder('windows-1252').decode(buf); }
    wb = XLSX.read(text.replace(/^﻿/, ''), { type: 'string' });
  } else {
    wb = XLSX.read(buf, { type: 'array' });
  }
  return XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]], { defval: '', header: 1, ...(opts || {}) })
    .map(r => r.map(c => typeof c === 'string' ? horosFixText(c) : c));
}
window.horosFixText = horosFixText;
window.horosReadSheetRows = horosReadSheetRows;

// Exporta leads para CSV (abre no Excel: separador ";" e BOM UTF-8 para os acentos)
function exportLeadsCSV(leads, fileName) {
  const cols = loadKanbanCols();
  const stage = id => (cols.find(c => c.id === id)?.title) || id || '';
  const temp = { hot: 'Quente', warm: 'Morno', cold: 'Frio' };
  const head = ['Nome', 'Telefone', 'E-mail', 'Etapa', 'Temperatura', 'Região', 'Valor', 'Score', segTerm('Agent', 'Corretor'), 'Criado em'];
  const cell = v => {
    const s = String(v ?? '');
    return /[";\n\r]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
  };
  const rows = (leads || []).map(l => [
    l.name, window.fmtPhone ? window.fmtPhone(l.phone) : l.phone, l.email || '', stage(l.col),
    temp[l.temp] || '', l.cat && l.cat !== '—' ? l.cat : '', l.value || '', l.score ?? '', l.broker || '',
    l.created_at ? new Date(l.created_at).toLocaleDateString('pt-BR') : '',
  ]);
  const csv = '﻿' + [head, ...rows].map(r => r.map(cell).join(';')).join('\r\n');
  const url = URL.createObjectURL(new Blob([csv], { type: 'text/csv;charset=utf-8' }));
  const a = document.createElement('a');
  a.href = url;
  a.download = fileName || `leads-${new Date().toISOString().slice(0, 10)}.csv`;
  document.body.appendChild(a); a.click(); a.remove();
  setTimeout(() => URL.revokeObjectURL(url), 1000);
}

Object.assign(window, {
  kbWaLoad, kbWaToggle, KbWaLabels, exportLeadsCSV, horosSendWaText, CreditAuthPanel,
  SEGMENTS, ARCHIVED_COLS, COL_TO_STATUS, COLUMNS,
  initials, fmtCurrency, AVATAR_PALETTES,
  SCORE_RULES, calcScore,
  LEAD_DOC_LABELS, buildCreditDocs,
  loadKanbanCols, saveKanbanCols, useKanbanCols, KANBAN_PALETTE, KanbanEditModal,
  loadPipelines, usePipelines, savePipelines, pipelineCols, leadPipeline, kanbanColTitle,
  horosMoveLeadsPipeline, newPipelineId, defaultPipelineCols,
  ARCHIVE_COLS, isArchivedCol, isArchivedLead, leadMoney, KanbanBulkBar, runBulkLeadRpc,
  ARCHIVE_REASONS, archiveReasonLabel, archiveReasonRecoverable, ArchiveReasonsManager, useArchiveReasons, horosLoadArchiveReasons, horosArchiveLeads, useKanbanSelection, KbColCheck,
  kbLeadMatches, KbColSearchBtn, KbColSearchInput, kbSince, kbDate, kbLeadPage, kbScrollToCard, horosGoToLead,
  MANAGER_ROLES, enrichLeadStages, LeadStages,
  PIPE_STAGES, STAGE_IDX, stageIdxOf, PipelineStrip,
  CARD_DOCS, DocStrip,
});

// ── Mapa de horários (Relatórios + Analytics) — 100% dados reais ─────────────
// Quando os clientes mandam mensagem no WhatsApp: RPC report_heatmap conta, por dia da semana × hora
// (horário de Brasília), as conversas individuais com mensagem recebida — sem grupos. Leads importados
// por planilha não entram (a data deles é a da importação, não a do cliente).
//
// Visual "skyline" (inspirado no Contribution Skyline): uma cena só em canvas — a grade 24h × 7 dias
// vista de cima é o mapa de calor (2D); a mesma grade vista do canto vira prédios (3D). Trocar de modo
// gira a câmera enquanto as colunas sobem em onda da 0h às 23h. Passe o mouse para ver o horário,
// setas do teclado navegam, passar o mouse na legenda isola um nível, em 3D arrastar gira (duplo clique volta).
const HM_DAYS = ['Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb', 'Dom'];
const HM_DAYS_LONG = ['Segunda', 'Terça', 'Quarta', 'Quinta', 'Sexta', 'Sábado', 'Domingo'];
const HM_PERIOD = { today: 0, '7d': 6, '30d': 29, '90d': 89 };
const HM_PERIOD_LBL = { today: 'hoje', '7d': 'últimos 7 dias', '30d': 'últimos 30 dias', '90d': 'últimos 90 dias' };
const HM_PARTS = [
  { name: 'Madrugada', from: 0, to: 6 }, { name: 'Manhã', from: 6, to: 12 },
  { name: 'Tarde', from: 12, to: 18 },   { name: 'Noite', from: 18, to: 24 },
];
const HM_LEVELS = ['Sem mensagens', 'Pouco', 'Moderado', 'Alto', 'Muito alto', 'Pico (100%)'];

const _hmClamp = v => (v > 0 ? (v < 1 ? v : 1) : 0);
const _hmLerp = (a, b, t) => a + (b - a) * t;
const _hmEaseInOut = x => { const t = _hmClamp(x); return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; };
const _hmEaseOut = x => 1 - Math.pow(1 - _hmClamp(x), 3);
const _hmSmooth = (a, b, x) => { const t = _hmClamp((x - a) / (b - a)); return t * t * (3 - 2 * t); };
const _hmMix = (a, b, t) => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];
const _hmLum = c => (0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2]) / 255;
const _hmRgb = (r, g, b) => 'rgb(' + Math.round(r) + ',' + Math.round(g) + ',' + Math.round(b) + ')';
const _hmHH = h => String(h).padStart(2, '0') + 'h';

const HM_W = 24, HM_D = 7;          // colunas = horas, linhas = dias
const HM_WAVE = 0.42;               // parte da animação que cada coluna passa esperando a onda
const HM_YAW = Math.PI / 4, HM_ELEV = (34 * Math.PI) / 180;
const HM_YAW_RANGE = [(8 * Math.PI) / 180, (82 * Math.PI) / 180];
const HM_ELEV_RANGE = [(18 * Math.PI) / 180, (62 * Math.PI) / 180];

// e=0 olha de cima (mapa de calor); e=1 é a vista isométrica do canto. Giro só vale na proporção de e.
const _hmCamera = (e, dYaw = 0, dElev = 0) => {
  const yaw = Math.min(HM_YAW_RANGE[1], Math.max(0, _hmLerp(0, HM_YAW + dYaw, e)));
  const elev = _hmLerp(Math.PI / 2, Math.min(HM_ELEV_RANGE[1], Math.max(HM_ELEV_RANGE[0], HM_ELEV + dElev)), e);
  return { cs: Math.cos(yaw), sn: Math.sin(yaw), se: Math.sin(elev), ce: Math.cos(elev) };
};
// No 2D as linhas (dias) ficam mais baixas que largas; no 3D a base é quadrada.
const _hmRowK = e => _hmLerp(0.62, 1, e);
const _hmRise = (t, x, y) => _hmEaseOut((t - ((x / (HM_W - 1)) * 0.36 + (y / (HM_D - 1)) * 0.06)) / (1 - HM_WAVE));
const _hmBarH = (v, max) => (v > 0 && max > 0 ? 0.35 + Math.pow(v / max, 0.85) * 6.4 : 0.15);

// Qualquer cor CSS → RGB, deixando o navegador pintar (aceita var resolvida, rgba, nomes…)
let _hmProbe = null;
const _hmToRGB = (color, fallback) => {
  if (!color) return fallback;
  if (!_hmProbe) { const c = document.createElement('canvas'); c.width = c.height = 1; _hmProbe = c.getContext('2d', { willReadFrequently: true }); }
  if (!_hmProbe) return fallback;
  _hmProbe.clearRect(0, 0, 1, 1);
  _hmProbe.fillStyle = 'rgba(0,0,0,0)';
  _hmProbe.fillStyle = color;
  _hmProbe.fillRect(0, 0, 1, 1);
  const d = _hmProbe.getImageData(0, 0, 1, 1).data;
  return d[3] < 8 ? fallback : [d[0], d[1], d[2]];
};

const _hmInQuad = (p, o, x, y) => {
  let sign = 0;
  for (let k = 0; k < 4; k++) {
    const ax = p[o + k * 2], ay = p[o + k * 2 + 1];
    const bx = p[o + ((k + 1) % 4) * 2], by = p[o + ((k + 1) % 4) * 2 + 1];
    const cross = (bx - ax) * (y - ay) - (by - ay) * (x - ax);
    if (Math.abs(cross) < 1e-9) continue;
    const s = cross > 0 ? 1 : -1;
    if (sign === 0) sign = s; else if (s !== sign) return false;
  }
  return sign !== 0;
};
const _hmQuadPath = (ctx, p, o, r) => {
  if (r < 0.3) {
    ctx.moveTo(p[o], p[o + 1]); ctx.lineTo(p[o + 2], p[o + 3]); ctx.lineTo(p[o + 4], p[o + 5]); ctx.lineTo(p[o + 6], p[o + 7]);
    ctx.closePath(); return;
  }
  ctx.moveTo((p[o + 6] + p[o]) / 2, (p[o + 7] + p[o + 1]) / 2);
  for (let k = 0; k < 4; k++) { const b = (k + 1) % 4; ctx.arcTo(p[o + k * 2], p[o + k * 2 + 1], p[o + b * 2], p[o + b * 2 + 1], r); }
  ctx.closePath();
};

// grid 7×24 → células (i = dia*24 + hora) com nível 0 vazio · 1–4 por quartos do pico · 5 = pico (laranja)
function _hmModel(grid) {
  const cells = [];
  let max = 0, total = 0;
  grid.forEach(row => row.forEach(v => { if (v > max) max = v; total += v; }));
  for (let d = 0; d < HM_D; d++) for (let h = 0; h < HM_W; h++) {
    const v = grid[d][h] || 0;
    const level = v <= 0 ? 0 : v === max ? 5 : 1 + Math.min(3, Math.floor((v / max) * 4));
    cells.push({ d, h, v, level });
  }
  const peaks = cells.filter(c => c.level === 5);
  const byDay = grid.map(r => r.reduce((s, v) => s + v, 0));
  const bestDay = byDay.indexOf(Math.max(...byDay));
  const parts = HM_PARTS.map(p => ({ ...p, n: grid.reduce((s, r) => s + r.slice(p.from, p.to).reduce((a, v) => a + v, 0), 0) }));
  const bestPart = parts.reduce((a, b) => (b.n > a.n ? b : a), parts[0]);
  return { cells, max, total, peaks, byDay, bestDay, bestPart };
}

function _HmStat({ label, value, unit, sub, size, align }) {
  return (
    <div className={'hsky-stat ' + align}>
      <div className="hsky-stat-lbl">{label}</div>
      <div className="hsky-stat-main">
        <span className="hsky-stat-val" style={{ fontSize: size }}>{value}</span>
        {unit && <span className="hsky-stat-unit">{unit}</span>}
      </div>
      <div className="hsky-stat-sub">{sub}</div>
    </div>
  );
}

function _HmSkyline({ model, view, legendLevel, perLabel }) {
  const [width, setWidth] = React.useState(0);
  const [active, setActive] = React.useState(-1);
  const [announce, setAnnounce] = React.useState('');
  const stageRef = React.useRef(null);
  const canvasRef = React.useRef(null);
  const tipRef = React.useRef(null);
  const engine = React.useRef(null);

  const describe = i => {
    const c = model.cells[i];
    if (!c) return '';
    return (c.v ? c.v + (c.v === 1 ? ' cliente' : ' clientes') : 'Nenhum cliente') + ' — ' + HM_DAYS_LONG[c.d] + ', ' + _hmHH(c.h) + ' às ' + _hmHH((c.h + 1) % 24);
  };
  const cfg = React.useRef(null);
  cfg.current = { model, legendLevel, target: view === '3d' ? 1 : 0, setActive, setWidth, setAnnounce, describe };

  React.useEffect(() => {
    const stage = stageRef.current, canvas = canvasRef.current, tip = tipRef.current;
    if (!stage || !canvas || !tip) return;
    const ctx = canvas.getContext('2d');
    if (!ctx) return;
    const panel = stage.closest('.panel') || stage;
    const reduceMq = window.matchMedia('(prefers-reduced-motion: reduce)');
    let reduced = reduceMq.matches;

    const n = HM_W * HM_D;
    let t = 0, target = 0, entered = false;
    let yaw = 0, elev = 0, yawGoal = 0, elevGoal = 0;
    let W = 0, H2 = 0, H3 = 0, Hmax = 0, lastH = -1, dpr = 1, gutter = 34, font = '10px sans-serif';
    const col = new Float32Array(18), colGoal = new Float32Array(18);
    let colReady = false, fg = [236, 237, 238], bg = [30, 32, 37];
    const xs = new Float32Array(n), ys = new Float32Array(n), lv = new Uint8Array(n), hgt = new Float32Array(n), zs = new Float32Array(n);
    const hover = new Float32Array(n), dim = new Float32Array(n), polys = new Float32Array(n * 24), faces = new Uint8Array(n);
    const order = Array.from({ length: n }, (_, i) => i);
    let hovered = -1, pinned = -1, activeIdx = -1, tipW = 0, raf = 0, last = 0;

    const load = () => {
      const m = cfg.current.model;
      for (let i = 0; i < n; i++) { const c = m.cells[i]; xs[i] = c.h; ys[i] = c.d; lv[i] = c.level; hgt[i] = _hmBarH(c.v, m.max); }
    };

    const retheme = () => {
      const rs = getComputedStyle(document.documentElement);
      const ps = getComputedStyle(panel);
      fg = _hmToRGB(ps.color, null) || _hmToRGB(rs.getPropertyValue('--fg').trim(), [236, 237, 238]);
      bg = _hmToRGB(ps.backgroundColor, null) || _hmToRGB(rs.getPropertyValue('--bg-elev').trim(), _hmLum(fg) > 0.5 ? [30, 32, 37] : [255, 255, 255]);
      const dark = _hmLum(bg) < 0.45;
      const accent = _hmToRGB(rs.getPropertyValue('--accent').trim(), [31, 147, 255]);
      const hot = _hmToRGB(rs.getPropertyValue('--hot').trim(), [255, 90, 78]);
      const warm = _hmToRGB(rs.getPropertyValue('--warm').trim(), [245, 181, 68]);
      font = '500 10.5px ' + (ps.fontFamily || 'sans-serif');
      const empty = _hmMix(bg, fg, dark ? 0.08 : 0.06);
      const ramp = dark ? [0.18, 0.36, 0.58, 0.82] : [0.14, 0.3, 0.52, 0.8];
      // pico = azul mais escuro (sem laranja — pedido do usuário: menos colorido)
      const all = [empty, ...ramp.map(k => _hmMix(dark ? empty : bg, accent, k)), _hmMix(accent, dark ? [255, 255, 255] : [0, 0, 0], dark ? 0.18 : 0.22)];
      for (let k = 0; k < 6; k++) for (let ch = 0; ch < 3; ch++) colGoal[k * 3 + ch] = all[k][ch];
      if (!colReady || reduced) { col.set(colGoal); colReady = true; }
      ctx.font = font;
      gutter = Math.ceil(Math.max(...HM_DAYS.map(d => ctx.measureText(d).width))) + 10;
      kick();
    };

    const extent = (cam, e, full) => {
      const w = _hmLerp(0.8, 0.86, e), off = (1 - w) / 2, rk = _hmRowK(e);
      let minx = Infinity, maxx = -Infinity, miny = Infinity, maxy = -Infinity;
      const add = (x, y, z) => {
        const px = x * cam.cs - y * rk * cam.sn, py = (x * cam.sn + y * rk * cam.cs) * cam.se - z * cam.ce;
        if (px < minx) minx = px; if (px > maxx) maxx = px; if (py < miny) miny = py; if (py > maxy) maxy = py;
      };
      for (let i = 0; i < n; i++) {
        const x0 = xs[i] + off, y0 = ys[i] + off, z = full ? hgt[i] * e : zs[i];
        add(x0, y0, z); add(x0 + w, y0, z); add(x0, y0 + w, z);
        add(x0 + w, y0 + w, 0); add(x0, y0 + w, 0); add(x0 + w, y0, 0);
      }
      // espaço para as horas na borda da frente e os dias na borda esquerda (3D)
      add(0, HM_D + 1.6 * e, 0); add(HM_W, HM_D + 1.6 * e, 0); add(-1.8 * e, HM_D, 0);
      return { minx, maxx, miny, maxy };
    };

    const relayout = () => {
      const w = Math.round(stage.clientWidth);
      if (!w) return;
      W = w;
      const g = W < 420 ? 0 : gutter;
      dpr = Math.min(2, window.devicePixelRatio || 1);
      const b2 = extent(_hmCamera(0), 0, true);
      H2 = Math.min(300, 22 + 4 + ((b2.maxy - b2.miny) / (b2.maxx - b2.minx)) * (W - g - 4));
      const b3 = extent(_hmCamera(1), 1, true);
      const natural = ((b3.maxy - b3.miny) / (b3.maxx - b3.minx)) * (W - 40) + 40;
      H3 = Math.max(Math.min(natural, W * 0.62, 460), Math.min(natural, 240));
      Hmax = Math.ceil(Math.max(H2, H3));
      canvas.width = Math.round(W * dpr); canvas.height = Math.round(Hmax * dpr);
      canvas.style.width = W + 'px'; canvas.style.height = Hmax + 'px';
      lastH = -1;
      cfg.current.setWidth(W);
      draw();
    };

    const draw = () => {
      if (!W) return;
      const e = _hmEaseInOut(t);
      const cam = _hmCamera(e, yaw, elev);
      const rk = _hmRowK(e);
      const Hc = _hmLerp(H2, H3, e);
      if (Math.abs(Hc - lastH) > 0.2) { stage.style.height = Hc.toFixed(1) + 'px'; lastH = Hc; }
      for (let i = 0; i < n; i++) zs[i] = _hmRise(t, xs[i], ys[i]) * hgt[i];
      const g = W < 420 ? 0 : gutter;
      const b = extent(cam, e, false);
      const pad = _hmLerp(2, 16, e);
      const left = pad + g * (1 - e), top = pad + 20 * (1 - e);
      const aw = W - left - pad, ah = Hc - top - pad;
      const bw = Math.max(1e-6, b.maxx - b.minx), bh = Math.max(1e-6, b.maxy - b.miny);
      const s = Math.min(aw / bw, ah / bh);
      const ox = left + (aw - bw * s) / 2 - b.minx * s, oy = top + (ah - bh * s) / 2 - b.miny * s;
      const { cs, sn, se, ce } = cam;
      const px = (x, y) => ox + (x * cs - y * rk * sn) * s;
      const py = (x, y, z) => oy + ((x * sn + y * rk * cs) * se - z * ce) * s;

      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      ctx.clearRect(0, 0, W, Hmax);
      order.sort((a, c) => ((xs[a] + 0.5) * sn + (ys[a] + 0.5) * cs) - ((xs[c] + 0.5) * sn + (ys[c] + 0.5) * cs));

      const w = _hmLerp(0.8, 0.86, e), off = (1 - w) / 2;
      const radius = _hmLerp(0.16, 0.04, e) * s;
      const lift = 0.5 * e;
      const ex = col[0], ey = col[1], ez = col[2];

      for (let k = 0; k < n; k++) {
        const i = order[k];
        const x0 = xs[i] + off, y0 = ys[i] + off, x1 = x0 + w, y1 = y0 + w;
        const z = zs[i] + hover[i] * lift;
        const o = i * 24;
        polys[o] = px(x0, y0);      polys[o + 1] = py(x0, y0, z);
        polys[o + 2] = px(x1, y0);  polys[o + 3] = py(x1, y0, z);
        polys[o + 4] = px(x1, y1);  polys[o + 5] = py(x1, y1, z);
        polys[o + 6] = px(x0, y1);  polys[o + 7] = py(x0, y1, z);
        polys[o + 8] = px(x0, y1);  polys[o + 9] = py(x0, y1, 0);
        polys[o + 10] = px(x1, y1); polys[o + 11] = py(x1, y1, 0);
        polys[o + 12] = polys[o + 4]; polys[o + 13] = polys[o + 5];
        polys[o + 14] = polys[o + 6]; polys[o + 15] = polys[o + 7];
        polys[o + 16] = px(x1, y0); polys[o + 17] = py(x1, y0, 0);
        polys[o + 18] = polys[o + 10]; polys[o + 19] = polys[o + 11];
        polys[o + 20] = polys[o + 4]; polys[o + 21] = polys[o + 5];
        polys[o + 22] = polys[o + 2]; polys[o + 23] = polys[o + 3];

        const tall = z * ce * s;
        let f = 0;
        if (tall > 0.35 && w * cs * s > 0.35) f |= 1;
        if (tall > 0.35 && w * sn * s > 0.35) f |= 2;
        faces[i] = f;

        const L = lv[i] * 3;
        let r = col[L], gg = col[L + 1], bl = col[L + 2];
        const d = dim[i];
        if (d > 0.002) { r += (ex - r) * 0.72 * d; gg += (ey - gg) * 0.72 * d; bl += (ez - bl) * 0.72 * d; }
        const hv = hover[i];
        if (hv > 0.002) { const m = 0.18 * hv; r += (fg[0] - r) * m; gg += (fg[1] - gg) * m; bl += (fg[2] - bl) * m; }
        if (f & 1) { ctx.beginPath(); _hmQuadPath(ctx, polys, o + 8, 0); ctx.fillStyle = _hmRgb(r * 0.84, gg * 0.84, bl * 0.84); ctx.fill(); }
        if (f & 2) { ctx.beginPath(); _hmQuadPath(ctx, polys, o + 16, 0); ctx.fillStyle = _hmRgb(r * 0.68, gg * 0.68, bl * 0.68); ctx.fill(); }
        ctx.beginPath();
        _hmQuadPath(ctx, polys, o, radius);
        ctx.fillStyle = _hmRgb(r, gg, bl);
        ctx.fill();
        if (hv > 0.02) {
          ctx.strokeStyle = 'rgba(' + fg[0] + ',' + fg[1] + ',' + fg[2] + ',' + (0.85 * hv).toFixed(3) + ')';
          ctx.lineWidth = 1.5;
          ctx.stroke();
        }
      }

      // Rótulos: em cima e à esquerda no 2D, nas bordas da frente no 3D. Somem/aparecem suavemente.
      const muted = _hmMix(bg, fg, 0.55);
      const rgba = a => 'rgba(' + Math.round(muted[0]) + ',' + Math.round(muted[1]) + ',' + Math.round(muted[2]) + ',' + a.toFixed(3) + ')';
      ctx.font = font;
      const a2 = 1 - _hmSmooth(0, 0.4, e), a3 = _hmSmooth(0.62, 1, e);
      const step = W < 560 ? 6 : 3;
      if (a2 > 0.004) {
        ctx.fillStyle = rgba(a2);
        ctx.textAlign = 'center'; ctx.textBaseline = 'bottom';
        for (let h = 0; h < HM_W; h += step) ctx.fillText(_hmHH(h), px(h + 0.5, -0.3), py(h + 0.5, -0.3, 0) - 2);
        if (g > 0) {
          ctx.textAlign = 'right'; ctx.textBaseline = 'middle';
          for (let d = 0; d < HM_D; d++) ctx.fillText(HM_DAYS[d], px(0, d + 0.5) - 8, py(0, d + 0.5, 0));
        }
      }
      if (a3 > 0.004) {
        ctx.fillStyle = rgba(a3);
        ctx.textAlign = 'left'; ctx.textBaseline = 'top';
        let edge = -Infinity;
        for (let h = 0; h < HM_W; h += step) {
          const x = px(h + 0.5, HM_D + 0.35), tw = ctx.measureText(_hmHH(h)).width;
          if (x < edge || x + tw > W) continue;
          ctx.fillText(_hmHH(h), x, py(h + 0.5, HM_D + 0.35, 0) + 2);
          edge = x + tw + 6;
        }
        ctx.textAlign = 'right'; ctx.textBaseline = 'middle';
        for (let d = 0; d < HM_D; d++) ctx.fillText(HM_DAYS[d], px(-0.35, d + 0.5) - 2, py(-0.35, d + 0.5, 0) + 4);
      }

      // Balão acompanha a célula ativa durante a animação e o giro.
      if (activeIdx >= 0 && activeIdx < n) {
        const i = activeIdx;
        const z = zs[i] + hover[i] * lift;
        const tx = px(xs[i] + 0.5, ys[i] + 0.5);
        const ty = Math.min(py(xs[i] + off, ys[i] + off, z), py(xs[i] + off + w, ys[i] + off, z), py(xs[i] + off, ys[i] + off + w, z));
        const half = tipW / 2;
        const cx = Math.min(W - half - 2, Math.max(half + 2, tx));
        tip.style.transform = 'translate(' + (cx - half).toFixed(1) + 'px,' + (ty - 8).toFixed(1) + 'px) translateY(-100%)';
        tip.style.setProperty('--arrow', (tx - cx + half).toFixed(1) + 'px');
      }
    };

    const tick = now => {
      raf = 0;
      const dt = Math.min(0.05, Math.max(0, (now - last) / 1000));
      last = now;
      let moving = false;
      if (t !== target) {
        const st = reduced ? 1 : (dt * 1000) / 1300;
        t = target > t ? Math.min(target, t + st) : Math.max(target, t - st);
        moving = true;
      }
      const ko = reduced ? 1 : 1 - Math.exp(-dt * 12);
      yaw += (yawGoal - yaw) * ko; elev += (elevGoal - elev) * ko;
      if (Math.abs(yawGoal - yaw) > 1e-4 || Math.abs(elevGoal - elev) > 1e-4) moving = true; else { yaw = yawGoal; elev = elevGoal; }
      const kc = reduced ? 1 : 1 - Math.exp(-dt * 7);
      for (let k = 0; k < 18; k++) {
        const d = colGoal[k] - col[k];
        if (Math.abs(d) > 0.4) { col[k] += d * kc; moving = true; } else col[k] = colGoal[k];
      }
      const kh = reduced ? 1 : 1 - Math.exp(-dt * 16), kd = reduced ? 1 : 1 - Math.exp(-dt * 10);
      const leg = cfg.current.legendLevel;
      for (let i = 0; i < n; i++) {
        const hg = i === activeIdx ? 1 : 0, dg = leg >= 0 && lv[i] !== leg ? 1 : 0;
        const h = hover[i], d = dim[i];
        if (h !== hg) { hover[i] = Math.abs(hg - h) < 0.003 ? hg : h + (hg - h) * kh; moving = true; }
        if (d !== dg) { dim[i] = Math.abs(dg - d) < 0.003 ? dg : d + (dg - d) * kd; moving = true; }
      }
      draw();
      if (moving) raf = requestAnimationFrame(tick);
    };
    const kick = () => { if (raf) return; last = performance.now(); raf = requestAnimationFrame(tick); };

    const refreshActive = () => {
      const next = hovered >= 0 ? hovered : pinned;
      if (next === activeIdx) return;
      activeIdx = next;
      cfg.current.setActive(next);
      kick();
    };
    const hit = (x, y) => {
      for (let k = n - 1; k >= 0; k--) {
        const i = order[k], o = i * 24;
        if (_hmInQuad(polys, o, x, y)) return i;
        if (faces[i] & 1 && _hmInQuad(polys, o + 8, x, y)) return i;
        if (faces[i] & 2 && _hmInQuad(polys, o + 16, x, y)) return i;
      }
      return -1;
    };
    const local = ev => { const r = canvas.getBoundingClientRect(); return [ev.clientX - r.left, ev.clientY - r.top]; };

    let drag = null;
    const onDown = ev => {
      if (ev.button !== 0) return;
      const can = target === 1;
      drag = { id: ev.pointerId, x: ev.clientX, y: ev.clientY, yaw: yawGoal, elev: elevGoal, moved: false, orbit: can, mouse: ev.pointerType === 'mouse' };
      if (can) { try { canvas.setPointerCapture(ev.pointerId); } catch (_) {} }
    };
    const onMove = ev => {
      if (drag && drag.orbit && ev.pointerId === drag.id) {
        const dx = ev.clientX - drag.x, dy = ev.clientY - drag.y;
        if (drag.moved || Math.hypot(dx, dy) > 4) {
          drag.moved = true;
          yawGoal = Math.min(HM_YAW_RANGE[1] - HM_YAW, Math.max(HM_YAW_RANGE[0] - HM_YAW, drag.yaw + dx * 0.006));
          if (drag.mouse) elevGoal = Math.min(HM_ELEV_RANGE[1] - HM_ELEV, Math.max(HM_ELEV_RANGE[0] - HM_ELEV, drag.elev + dy * 0.004));
          canvas.style.cursor = 'grabbing';
          hovered = -1; refreshActive(); kick();
          return;
        }
      }
      if (ev.pointerType !== 'mouse') return;
      const [x, y] = local(ev);
      const i = hit(x, y);
      if (i !== hovered) { hovered = i; refreshActive(); }
      canvas.style.cursor = target === 1 ? 'grab' : i >= 0 ? 'pointer' : 'default';
    };
    const onUp = ev => {
      if (!drag || ev.pointerId !== drag.id) return;
      const wasMoved = drag.moved;
      drag = null;
      if (canvas.hasPointerCapture(ev.pointerId)) canvas.releasePointerCapture(ev.pointerId);
      canvas.style.cursor = target === 1 ? 'grab' : 'default';
      if (wasMoved) return;
      const [x, y] = local(ev);
      const i = hit(x, y);
      pinned = i === pinned ? -1 : i;
      if (ev.pointerType !== 'mouse') hovered = -1;
      refreshActive();
    };
    const onCancel = () => { drag = null; };
    const onLeave = () => { if (drag) return; hovered = -1; refreshActive(); };
    const onDbl = () => { yawGoal = 0; elevGoal = 0; kick(); };
    const onKey = ev => {
      if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End', 'Escape'].includes(ev.key)) return;
      ev.preventDefault();
      if (ev.key === 'Escape') { pinned = -1; hovered = -1; refreshActive(); return; }
      let i = pinned >= 0 ? pinned : activeIdx >= 0 ? activeIdx : -1;
      if (i < 0) { const m = cfg.current.model; i = m.peaks.length ? m.peaks[0].d * HM_W + m.peaks[0].h : 0; }
      else {
        if (ev.key === 'ArrowLeft') i -= 1;
        if (ev.key === 'ArrowRight') i += 1;
        if (ev.key === 'ArrowUp') i -= HM_W;
        if (ev.key === 'ArrowDown') i += HM_W;
        if (ev.key === 'Home') i = 0;
        if (ev.key === 'End') i = n - 1;
      }
      i = Math.max(0, Math.min(n - 1, i));
      pinned = i; hovered = -1; refreshActive();
      cfg.current.setAnnounce(cfg.current.describe(i));
    };
    const onBlur = () => { pinned = -1; refreshActive(); };

    const setTarget = () => {
      const goal = cfg.current.target;
      if (!entered || goal === target) return;
      target = goal;
      if (goal === 0) { yawGoal = 0; elevGoal = 0; }
      canvas.style.cursor = target === 1 ? 'grab' : 'default';
      kick();
    };

    load(); retheme(); relayout();

    // O 3D sobe do mapa plano na primeira vez que aparece na tela.
    const enter = () => { if (entered) return; entered = true; if (reduced) t = cfg.current.target; setTarget(); };
    let io = null;
    if ('IntersectionObserver' in window) {
      io = new IntersectionObserver(en => { if (en.some(x => x.isIntersecting)) { enter(); io && io.disconnect(); } }, { threshold: 0.35 });
      io.observe(stage);
    } else enter();

    const ro = new ResizeObserver(() => { if (Math.round(stage.clientWidth) !== W) relayout(); });
    ro.observe(stage);
    const mo = new MutationObserver(retheme);
    mo.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'style', 'data-theme'] });
    const onReduce = () => { reduced = reduceMq.matches; kick(); };
    reduceMq.addEventListener('change', onReduce);

    canvas.addEventListener('pointerdown', onDown);
    canvas.addEventListener('pointermove', onMove);
    canvas.addEventListener('pointerup', onUp);
    canvas.addEventListener('pointercancel', onCancel);
    canvas.addEventListener('pointerleave', onLeave);
    canvas.addEventListener('dblclick', onDbl);
    canvas.addEventListener('keydown', onKey);
    canvas.addEventListener('blur', onBlur);

    engine.current = {
      kick: () => { setTarget(); kick(); },
      load: () => { load(); relayout(); kick(); },
      tipWidth: w => { tipW = w; draw(); },
    };
    return () => {
      if (raf) cancelAnimationFrame(raf);
      io && io.disconnect(); ro.disconnect(); mo.disconnect();
      reduceMq.removeEventListener('change', onReduce);
      canvas.removeEventListener('pointerdown', onDown);
      canvas.removeEventListener('pointermove', onMove);
      canvas.removeEventListener('pointerup', onUp);
      canvas.removeEventListener('pointercancel', onCancel);
      canvas.removeEventListener('pointerleave', onLeave);
      canvas.removeEventListener('dblclick', onDbl);
      canvas.removeEventListener('keydown', onKey);
      canvas.removeEventListener('blur', onBlur);
      engine.current = null;
    };
  }, []);

  React.useEffect(() => { engine.current && engine.current.kick(); }, [view, legendLevel]);
  React.useEffect(() => { engine.current && engine.current.load(); }, [model]);
  React.useLayoutEffect(() => {
    const tip = tipRef.current;
    if (tip && active >= 0 && engine.current) engine.current.tipWidth(tip.offsetWidth);
  }, [active, model]);

  const is3d = view === '3d';
  const corners = false;   // números sempre na linha de baixo (por cima do 3D ficava embolado)
  const big = Math.round(Math.max(26, Math.min(44, width * 0.042)));
  const m = model;
  const pk = m.peaks[0];
  const pct = n => (m.total ? Math.round((n / m.total) * 100) : 0) + '% do total';
  const stats = [
    { label: 'Total no período', value: m.total.toLocaleString('pt-BR'), unit: m.total === 1 ? 'conversa' : 'conversas', sub: perLabel },
    { label: 'Horário de pico', value: pk ? _hmHH(pk.h) : '—', unit: pk ? HM_DAYS[pk.d] : '',
      sub: m.peaks.length > 1 ? m.peaks.length + ' horários empatados · ' + m.max + ' clientes' : m.max + (m.max === 1 ? ' cliente' : ' clientes') },
    { label: 'Melhor dia', value: HM_DAYS[m.bestDay], unit: '', sub: m.byDay[m.bestDay].toLocaleString('pt-BR') + ' · ' + pct(m.byDay[m.bestDay]) },
    { label: 'Melhor período', value: m.bestPart.name, unit: '', sub: _hmHH(m.bestPart.from) + '–' + _hmHH(m.bestPart.to % 24) + ' · ' + pct(m.bestPart.n) },
  ];
  const showRow = !(is3d && corners);
  const c = active >= 0 ? m.cells[active] : null;

  return (
    <>
      <div className="hsky-stage-wrap">
        <div ref={stageRef} className="hsky-stage">
          <canvas ref={canvasRef} tabIndex={0} role="img" className="hsky-canvas"
            style={{ touchAction: is3d ? 'pan-y' : 'auto' }}
            aria-label={m.total + ' conversas com mensagem de cliente, ' + perLabel + ', em ' + (is3d ? 'prédios 3D' : 'mapa de calor') + '. Use as setas para ler cada horário.'} />
          {corners && (
            <>
              <div className={'hsky-corner tr' + (is3d ? ' on' : '')} aria-hidden={!is3d}>
                <_HmStat {...stats[0]} size={big} align="end" />
                <_HmStat {...stats[1]} size={big} align="end" />
              </div>
              <div className={'hsky-corner bl' + (is3d ? ' on' : '')} aria-hidden={!is3d}>
                <_HmStat {...stats[2]} size={big} align="start" />
                <_HmStat {...stats[3]} size={big} align="start" />
              </div>
            </>
          )}
        </div>
        <div ref={tipRef} role="tooltip" aria-hidden={!c} className={'hsky-tip' + (c ? ' on' : '')}>
          {c ? (
            <>
              <strong>{c.v ? c.v + (c.v === 1 ? ' cliente' : ' clientes') : 'Nenhum cliente'}</strong>
              <span> · {HM_DAYS[c.d]} {_hmHH(c.h)}–{_hmHH((c.h + 1) % 24)}{c.v ? ' · ' + Math.round((c.v / m.max) * 100) + '% do pico' : ''}</span>
            </>
          ) : ' '}
          <span className="hsky-tip-arrow" aria-hidden="true" />
        </div>
      </div>
      <div className={'hsky-stats-row' + (showRow ? ' on' : '')} aria-hidden={!showRow}>
        <div className="hsky-stats-inner">
          {stats.map(s => <_HmStat key={s.label} {...s} size={24} align="stack" />)}
        </div>
      </div>
      <p aria-live="polite" className="hsky-sr">{announce}</p>
    </>
  );
}

function HorosHeatmap({ filter = '30d', scope = 'me', range = null }) {   // range: { start, end, label } (ISO) — período personalizado
  const [grid, setGrid] = React.useState(null);   // null = carregando · 7×24 com contagens reais
  const [err, setErr]   = React.useState(false);
  const [view, setView] = React.useState(() => { try { return localStorage.getItem('horos-hm-view') === '2d' ? '2d' : '3d'; } catch (_) { return '3d'; } });
  const [legendLevel, setLegendLevel] = React.useState(-1);
  const [swatches, setSwatches] = React.useState(null);
  const legendRef = React.useRef(null);

  React.useEffect(() => {
    let alive = true;
    setGrid(null); setErr(false);
    let end = new Date(); end.setHours(23, 59, 59, 999);
    let start = new Date(+end - (HM_PERIOD[filter] ?? 29) * 86400000); start.setHours(0, 0, 0, 0);
    if (range && range.start && range.end) { start = new Date(range.start); end = new Date(range.end); }
    window.HOROS_SB.rpc('report_heatmap', { p_scope: scope, p_start: start.toISOString(), p_end: end.toISOString() })
      .then(({ data, error }) => {
        if (!alive) return;
        if (error) { setErr(true); setGrid([]); return; }
        const g = Array.from({ length: 7 }, () => Array(24).fill(0));
        (data || []).forEach(r => { if (g[r.dow] && r.hour >= 0 && r.hour < 24) g[r.dow][r.hour] = r.n; });
        setGrid(g);
      });
    return () => { alive = false; };
  }, [filter, scope, range && range.start, range && range.end]);

  const model = React.useMemo(() => (grid && grid.length ? _hmModel(grid) : null), [grid]);

  // Cores da legenda = as mesmas do canvas (lidas do tema; acompanham claro/escuro)
  React.useEffect(() => {
    const calc = () => {
      const rs = getComputedStyle(document.documentElement);
      const el = legendRef.current && legendRef.current.closest('.panel');
      const ps = el ? getComputedStyle(el) : rs;
      const fg = _hmToRGB(ps.color, null) || _hmToRGB(rs.getPropertyValue('--fg').trim(), [236, 237, 238]);
      const bg = _hmToRGB(ps.backgroundColor, null) || _hmToRGB(rs.getPropertyValue('--bg-elev').trim(), [30, 32, 37]);
      const dark = _hmLum(bg) < 0.45;
      const accent = _hmToRGB(rs.getPropertyValue('--accent').trim(), [31, 147, 255]);
      const empty = _hmMix(bg, fg, dark ? 0.08 : 0.06);
      const ramp = dark ? [0.18, 0.36, 0.58, 0.82] : [0.14, 0.3, 0.52, 0.8];
      const peak = _hmMix(accent, dark ? [255, 255, 255] : [0, 0, 0], dark ? 0.18 : 0.22);
      setSwatches([empty, ...ramp.map(k => _hmMix(dark ? empty : bg, accent, k)), peak].map(c => _hmRgb(c[0], c[1], c[2])));
    };
    calc();
    const mo = new MutationObserver(calc);
    mo.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'style', 'data-theme'] });
    return () => mo.disconnect();
  }, [!!model]);

  const pickView = v => { setView(v); try { localStorage.setItem('horos-hm-view', v); } catch (_) {} };
  const per = (range && range.label) || HM_PERIOD_LBL[filter] || 'últimos 30 dias';
  const is3d = view === '3d';

  const head = (
    <div className="panel-head">
      <h3 className="panel-title">
        Mapa de horários
        <span className="count">clientes que mandaram mensagem · {per}</span>
      </h3>
      {model && model.total > 0 && (
        <div className="panel-actions">
          {model.peaks.length > 0 && (
            <span className="an-peak-pill"><Icon name="clock" size={11} stroke={2} /> Pico {HM_DAYS[model.peaks[0].d]} {_hmHH(model.peaks[0].h)}</span>
          )}
          <div className="hsky-toggle" role="group" aria-label="Visualização">
            <span className={'hsky-toggle-thumb' + (is3d ? ' r' : '')} aria-hidden="true" />
            {['2d', '3d'].map(v => (
              <button key={v} type="button" aria-pressed={view === v} className={view === v ? 'on' : ''}
                title={v === '2d' ? 'Mapa de calor' : 'Prédios 3D'} aria-label={v === '2d' ? 'Mapa de calor' : 'Prédios 3D'}
                onClick={() => pickView(v)}>
                <Icon name={v === '2d' ? 'gridView' : 'cube'} size={14} stroke={1.8} />
              </button>
            ))}
          </div>
        </div>
      )}
    </div>
  );

  if (!grid) return <div className="panel">{head}<div style={{ padding: '8px 4px 4px' }}><Skel h={260} r={8} /></div></div>;
  if (!model || !model.total) return (
    <div className="panel">{head}
      <div style={{ padding: '36px 20px', textAlign: 'center', color: 'var(--fg-4)', fontSize: 13 }}>
        {err ? 'Não foi possível carregar o mapa.' : 'Nenhuma mensagem de cliente no período.'}
      </div>
    </div>
  );

  const hints = ['Passe o mouse num horário · setas do teclado navegam', 'Arraste para girar · clique duplo volta'];
  return (
    <div className="panel hsky">
      {head}
      <div className="hsky-body">
        <_HmSkyline model={model} view={view} legendLevel={legendLevel} perLabel={per} />
        <div className="hsky-foot">
          <span className="hsky-hints">
            {hints.map((h, i) => <span key={i} className={(is3d ? i === 1 : i === 0) ? 'on' : ''} aria-hidden={(is3d ? i !== 1 : i !== 0)}>{h}</span>)}
          </span>
          <div ref={legendRef} className="hsky-legend" onMouseLeave={() => setLegendLevel(-1)}>
            <span>Menos</span>
            {(swatches || Array(6).fill('var(--line)')).map((c, i) => (
              <button key={i} type="button" title={HM_LEVELS[i]} aria-label={'Destacar: ' + HM_LEVELS[i]} aria-pressed={legendLevel === i}
                className={'hsky-swatch' + (i === 5 ? ' peak' : '')} style={{ background: c }}
                onMouseEnter={() => setLegendLevel(i)} onFocus={() => setLegendLevel(i)} onBlur={() => setLegendLevel(-1)}
                onClick={() => setLegendLevel(l => (l === i ? -1 : i))} />
            ))}
            <span>Pico</span>
          </div>
        </div>
      </div>
    </div>
  );
}
window.HorosHeatmap = HorosHeatmap;

// ── Ranking: pódio + lista paginada (LeaderboardCard) ───────────────────────
// Adaptação do leaderboard-card (shadcn/Tailwind/TS) para o padrão do Horos: JSX global, CSS .lb-* em styles.css.
// Usado no Dashboard (Ranking do mês), Equipe (Ranking da equipe) e Relatórios (Ranking).
// Itens (já ordenados): { userId, userName, rank, value, byline?, avatarUrl?, color?, displayed? }
const LB_MONTHS_SHORT = ['jan','fev','mar','abr','mai','jun','jul','ago','set','out','nov','dez'];
function lbFormatDate(date) {
  if (!date) return '';
  const d = date instanceof Date ? date : new Date(String(date).length === 10 ? `${date}T12:00:00` : date);
  if (Number.isNaN(d.getTime())) return '';
  return `${d.getDate()} ${LB_MONTHS_SHORT[d.getMonth()]} ${d.getFullYear()}`;
}
const lbHasScore = (r) => (Number(r && r.value) || 0) > 0;
const lbFmtValue = (v) => (Number(v) || 0).toLocaleString('pt-BR');
// cor estável por nome quando o item não traz cor
const lbHue = (n) => { let h = 0; for (const c of String(n || '')) h = (h * 31 + c.charCodeAt(0)) % 360; return `hsl(${h} 52% 46%)`; };

function LbAvatar({ name, url, size = 36, color }) {
  const [broken, setBroken] = React.useState(false);
  const ini = (window.initials ? window.initials(name || '') : String(name || '?').slice(0, 2).toUpperCase()) || '?';
  if (url && !broken) {
    return <img className="lb-avatar" src={url} alt="" style={{ width: size, height: size }} onError={() => setBroken(true)} />;
  }
  return (
    <span className="lb-avatar lb-avatar-ini" aria-hidden="true"
      style={{ width: size, height: size, fontSize: Math.round(size * 0.36), '--lb-av': color || lbHue(name) }}>
      {ini}
    </span>
  );
}

// Pódio: 2º · 1º · 3º na tela; leitores de tela recebem 1-2-3
function LeaderboardPodium({ rankings, currentUserId, formatValue = lbFmtValue, unit, className = '' }) {
  const top = (rankings || []).slice(0, 3);
  if (top.length === 0) return null;
  const slots = [top[1], top[0], top[2]];
  return (
    <ol className={`lb-podium ${className}`} aria-label="Pódio">
      {top.map((r, idx) => {
        const place = Math.min(3, Math.max(1, r.rank || idx + 1)); // empate: mesma altura e cor
        const me = currentUserId && r.userId === currentUserId;
        return (
          <li key={r.userId} className={`lb-pod lb-pod-${place}${me ? ' me' : ''}`} style={{ order: slots.indexOf(r) }}>
            <div className="lb-pod-who">
              <span className="lb-pod-ring">
                {place === 1 && <span className="lb-pod-crown" aria-hidden="true"><Icon name="crown" size={16} /></span>}
                <LbAvatar name={r.userName} url={r.avatarUrl} color={r.color} size={place === 1 ? 56 : 44} />
              </span>
              <span className="lb-pod-name" title={r.userName}>{me ? 'Você' : r.userName}</span>
              <span className="lb-pod-val">{formatValue(r.value)}{unit && <small> {unit}</small>}</span>
            </div>
            <div className="lb-pod-step"><span>{r.rank}º</span></div>
          </li>
        );
      })}
      {top.length < 3 && slots.map((r, i) => !r && <li key={`e${i}`} className="lb-pod lb-pod-empty" style={{ order: i }} aria-hidden="true" />)}
    </ol>
  );
}

// Lista: posição · avatar · nome/linha de apoio · valor. Paginação; "sua posição" fixa no fim quando você está em outra página
function LeaderboardRankings({ rankings, currentUserId, showPagination = true, defaultPageSize = 10,
  formatValue = lbFmtValue, unit, skip = 0, className = '' }) {
  const rows = (rankings || []).filter(r => r.displayed !== false).slice(skip);
  const pageSize = showPagination ? defaultPageSize : Math.max(1, rows.length);
  const pages = Math.max(1, Math.ceil(rows.length / pageSize));
  const [page, setPage] = React.useState(0);
  React.useEffect(() => { if (page > pages - 1) setPage(pages - 1); }, [pages]);
  if (rows.length === 0) return null;
  const slice = rows.slice(page * pageSize, page * pageSize + pageSize);
  const meRow = currentUserId ? rows.find(r => r.userId === currentUserId) : null;
  const meOnPage = meRow && slice.includes(meRow);
  const row = (r, pinned) => {
    const me = currentUserId && r.userId === currentUserId;
    return (
      <li key={r.userId} className={`lb-row${me ? ' me' : ''}${pinned ? ' pinned' : ''}`}>
        <span className="lb-row-pos" title={lbHasScore(r) ? undefined : 'Ainda sem pontos'}>{lbHasScore(r) ? `${r.rank}º` : '—'}</span>
        <LbAvatar name={r.userName} url={r.avatarUrl} color={r.color} size={32} />
        <span className="lb-row-who">
          <span className="lb-row-name" title={r.userName}><span>{r.userName}</span>{me && <em className="lb-you">você</em>}</span>
          {r.byline && <span className="lb-row-by">{r.byline}</span>}
        </span>
        <span className="lb-row-val">{formatValue(r.value)}{unit && <small> {unit}</small>}</span>
      </li>
    );
  };
  return (
    <div className={`lb-rankings ${className}`}>
      <ol className="lb-list">{slice.map(r => row(r))}</ol>
      {meRow && !meOnPage && (
        <>
          <div className="lb-gap" aria-hidden="true">···</div>
          <ol className="lb-list">{row(meRow, true)}</ol>
        </>
      )}
      {showPagination && pages > 1 && (
        <div className="lb-pager">
          <span className="lb-pager-info">{page * pageSize + 1}–{Math.min(rows.length, (page + 1) * pageSize)} de {rows.length}</span>
          <div className="lb-pager-btns">
            <button type="button" className="lb-pager-btn" aria-label="Página anterior" disabled={page === 0} onClick={() => setPage(p => p - 1)}>
              <Icon name="arrLeft" size={14} />
            </button>
            <button type="button" className="lb-pager-btn" aria-label="Próxima página" disabled={page >= pages - 1} onClick={() => setPage(p => p + 1)}>
              <Icon name="arrRightS" size={14} />
            </button>
          </div>
        </div>
      )}
    </div>
  );
}

// Cartão completo: título + período + seletor (runOptions) + pódio + lista.
// `rankings` traz todos em ordem (inclusive o top 3); sem `podiumRankings`, o pódio são os 3 primeiros e a lista começa no 4º
function LeaderboardCard({ title = 'Ranking', fromDate, toDate, podiumRankings, rankings, currentUserId,
  runOptions, selectedRunId, onRunChange, formatValue, unit, badge, children, emptyText = 'Sem dados ainda',
  pageSize = 10, loading = false, zeroText = 'Ninguém pontuou ainda. O primeiro a pontuar assume o 1º lugar.', className = '', ...props }) {
  const resolvedRunId = selectedRunId ?? runOptions?.[0]?.id ?? '';
  const [localRunId, setLocalRunId] = React.useState(resolvedRunId);
  React.useEffect(() => { if (!onRunChange) setLocalRunId(resolvedRunId); }, [resolvedRunId, !!onRunChange]);
  const activeRunId = onRunChange ? resolvedRunId : localRunId;
  const fromLabel = lbFormatDate(fromDate), toLabel = lbFormatDate(toDate);
  const list = rankings || [];
  // Pódio só para quem pontuou: com todos em 0 ninguém aparece como 1º
  const podium = podiumRankings || list.slice(0, 3).filter(lbHasScore);
  const skip = podiumRankings ? 0 : podium.length;
  const nobody = list.length > 0 && !list.some(lbHasScore);
  return (
    <div className={`lb-card ${className}`} {...props}>
      <div className="lb-head">
        <div className="lb-head-txt">
          <h3 className="lb-title">{title}{badge != null && badge !== '' && <span className="lb-badge">{badge}</span>}</h3>
          {(fromLabel || toLabel) && <p className="lb-range">{fromLabel}{fromLabel && toLabel ? ' – ' : ''}{toLabel}</p>}
        </div>
        {runOptions && runOptions.length > 0 && (
          <select className="lb-select" aria-label="Escolher ranking" value={activeRunId}
            onChange={e => (onRunChange ? onRunChange(e.target.value) : setLocalRunId(e.target.value))}>
            {runOptions.map(o => <option key={o.id} value={o.id}>{o.label}</option>)}
          </select>
        )}
      </div>
      {children}
      {loading ? (
          <div role="status" aria-label="Carregando ranking">
            <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'flex-end', gap: 18, padding: '12px 0 18px' }}>
              {[64, 92, 48].map((h, i) => <div key={i} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8 }}><Skel circle h={i === 1 ? 46 : 38} /><Skel w={56} h={9} /><Skel w={72} h={h} r={8} /></div>)}
            </div>
            <SkelList rows={3} avatar={28} right lines={1} pad="9px 4px" />
          </div>
        )
        : list.length === 0 && podium.length === 0 ? <p className="lb-empty">{emptyText}</p>
        : (
          <>
            {nobody && zeroText && <p className="lb-zero">{zeroText}</p>}
            <LeaderboardPodium rankings={podium} currentUserId={currentUserId} formatValue={formatValue} unit={unit} />
            <LeaderboardRankings rankings={list} skip={skip} currentUserId={currentUserId} defaultPageSize={pageSize}
              formatValue={formatValue} unit={unit} />
          </>
        )}
    </div>
  );
}
Object.assign(window, { LeaderboardCard, LeaderboardPodium, LeaderboardRankings, LbAvatar });

// ── Ligações pela Api4Com (2026-10-14) ──────────────────────────────────────
// my_call_setup() diz se a imobiliária conectou a Api4Com e qual é o ramal da pessoa (cache na sessão).
let _callSetupP = null;
function horosCallSetup() {
  if (!_callSetupP) {
    _callSetupP = (async () => {
      try { const { data } = await window.HOROS_SB.rpc('my_call_setup'); return data || { enabled: false }; }
      catch { return { enabled: false }; }
    })();
  }
  return _callSetupP;
}
window.horosCallSetupReset = () => { _callSetupP = null; };

function LeadCallButton({ lead }) {
  const [setup, setSetup] = React.useState(null); // null = carregando
  const [busy, setBusy] = React.useState(false);
  const [msg, setMsg] = React.useState('');
  React.useEffect(() => { let on = true; horosCallSetup().then(s => on && setSetup(s)); return () => { on = false; }; }, []);
  if (!lead?.phone) return null;

  const rawDigits = lead.phone.replace(/\D/g, '');
  // Remove o prefixo 55 do Brasil se já estiver no número (ex: 5511959981473 → 11959981473)
  const digits = rawDigits.startsWith('55') && rawDigits.length > 11 ? rawDigits.slice(2) : rawDigits;
  const useApi4Com = setup?.enabled;

  const handleClick = async () => {
    if (useApi4Com) {
      // Api4Com: liga pelo ramal configurado
      if (!setup.extension) { location.href = 'integrations.html?app=api4com'; return; }
      setBusy(true); setMsg('');
      try {
        const tok = window.horosToken ? await window.horosToken() : window.HOROS_SESSION?.access_token;
        const r = await fetch('/api/lead-partners', {
          method: 'POST', cache: 'no-store',
          headers: { Authorization: `Bearer ${tok}`, 'Content-Type': 'application/json' },
          body: JSON.stringify({ action: 'call', lead_id: lead.id }),
        });
        const j = await r.json().catch(() => ({}));
        if (!r.ok) throw new Error(j.error === 'no_extension' ? 'Informe seu ramal em Integrações › Api4Com.' : (j.error || 'Não foi possível ligar.'));
        setMsg('Atenda o seu ramal: a ligação para o cliente começa em seguida.');
        window.dispatchEvent(new CustomEvent('horos:call-started', { detail: { leadId: lead.id } }));
      } catch (e) { setMsg(e.message); }
      setBusy(false);
      setTimeout(() => setMsg(''), 7000);
    } else {
      // Fallback: discador nativo do sistema (Phone Link no Windows, celular, FaceTime no Mac…)
      window.location.href = `tel:+55${digits}`;
    }
  };

  const title = useApi4Com
    ? (setup.extension ? `Ligar pelo ramal ${setup.extension} (Api4Com)` : 'Informe seu ramal da Api4Com para ligar')
    : 'Ligar pelo discador do sistema (Phone Link, celular…)';

  return (
    <span className="lead-call">
      <button type="button" className="lead-call-btn" onClick={handleClick} disabled={busy} title={title}>
        <Icon name="phone" size={12} /> {busy ? 'Chamando…' : 'Ligar'}
      </button>
      {msg && <span className="lead-call-msg">{msg}</span>}
    </span>
  );
}
window.LeadCallButton = LeadCallButton;

const _CALL_STATUS = { dialing: 'Chamando', answered: 'Em andamento', ended: 'Atendida', failed: 'Não atendida' };
function _fmtDur(s) { s = Number(s) || 0; const m = Math.floor(s / 60); return m ? `${m} min ${String(s % 60).padStart(2, '0')} s` : `${s} s`; }
function LeadCallsList({ leadId }) {
  const [rows, setRows] = React.useState(null);
  const load = React.useCallback(async () => {
    try {
      const { data } = await window.HOROS_SB.from('lead_calls')
        .select('id,status,direction,duration,record_url,hangup_cause,started_at,created_at')
        .eq('lead_id', leadId).order('created_at', { ascending: false }).limit(20);
      setRows(data || []);
    } catch { setRows([]); }
  }, [leadId]);
  React.useEffect(() => {
    if (!leadId || !window.HOROS_SB) return;
    load();
    const ch = window.HOROS_SB.channel('calls-' + leadId)
      .on('postgres_changes', { event: '*', schema: 'public', table: 'lead_calls', filter: `lead_id=eq.${leadId}` }, load)
      .subscribe();
    const onStart = (e) => { if (e.detail?.leadId === leadId) setTimeout(load, 800); };
    window.addEventListener('horos:call-started', onStart);
    return () => { try { window.HOROS_SB.removeChannel(ch); } catch {} window.removeEventListener('horos:call-started', onStart); };
  }, [leadId, load]);
  if (!rows || !rows.length) return null;
  return (
    <div className="lead-calls">
      <div className="lead-calls-title">Ligações</div>
      {rows.map(c => (
        <div key={c.id} className="lead-calls-row">
          <Icon name="phone" size={13} />
          <span className="lead-calls-main">
            <b>{_CALL_STATUS[c.status] || c.status}</b>
            <span>{new Date(c.started_at || c.created_at).toLocaleString('pt-BR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })}
              {c.status === 'ended' ? ` · ${_fmtDur(c.duration)}` : ''}{c.direction === 'inbound' ? ' · recebida' : ''}</span>
          </span>
          {c.record_url && <a href={c.record_url} target="_blank" rel="noopener noreferrer" className="lead-calls-rec">Ouvir gravação</a>}
        </div>
      ))}
    </div>
  );
}
window.LeadCallsList = LeadCallsList;

// ── Imóveis ligados ao lead (2026-10-14): tela Imóveis → "Enviar para um cliente" / "Ligar a um cliente"
// (property_leads). Aparece no Resumo do Horos 360; clique abre o imóvel em properties.html?id=
function LeadPropertiesList({ leadId }) {
  const [rows, setRows] = React.useState(null);
  React.useEffect(() => {
    if (!leadId || !window.HOROS_SB) return;
    let alive = true;
    (async () => {
      try {
        const sb = window.HOROS_SB;
        const { data: links } = await sb.from('property_leads').select('property_id, kind, sent_at, created_at')
          .eq('lead_id', leadId).order('created_at', { ascending: false }).limit(20);
        const ids = (links || []).map(l => l.property_id);
        if (!ids.length) { if (alive) setRows([]); return; }
        const { data: props } = await sb.from('properties').select('id, name, price, status, media_urls, details, address').in('id', ids);
        const byId = Object.fromEntries((props || []).map(p => [p.id, p]));
        if (alive) setRows((links || []).filter(l => byId[l.property_id]).map(l => ({ ...l, p: byId[l.property_id] })));
      } catch { if (alive) setRows([]); }
    })();
    return () => { alive = false; };
  }, [leadId]);
  if (!rows || !rows.length) return null;
  const brl = (v) => v ? Number(v).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL', maximumFractionDigits: 0 }) : '';
  return (
    <div className="lead-calls lead-props">
      <div className="lead-calls-title">Imóveis de interesse</div>
      {rows.map(r => {
        const d = r.p.details || {};
        const price = d.transacao === 'aluguel' ? (brl(d.preco_aluguel || r.p.price) && `${brl(d.preco_aluguel || r.p.price)}/mês`) : brl(d.preco_venda || r.p.price);
        const when = new Date(r.sent_at || r.created_at).toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit' });
        return (
          <a key={r.property_id} className="lead-calls-row lead-props-row" href={`properties.html?id=${r.p.id}`}>
            <span className="lead-props-img">{r.p.media_urls?.[0] ? <img src={r.p.media_urls[0]} alt="" loading="lazy" /> : <Icon name="building" size={14} />}</span>
            <span className="lead-calls-main">
              <b>{r.p.name}</b>
              <span>{[price, r.p.address?.bairro, r.kind === 'sent' ? `enviado em ${when}` : `ligado em ${when}`, r.p.status !== 'available' ? 'indisponível' : ''].filter(Boolean).join(' · ')}</span>
            </span>
          </a>
        );
      })}
    </div>
  );
}
window.LeadPropertiesList = LeadPropertiesList;

// ── Gmail (2026-10-14): enviar e-mail ao lead pela conta Gmail da própria pessoa ─────────────
async function horosGmailCall(body) {
  const tok = window.horosToken ? await window.horosToken() : window.HOROS_SESSION?.access_token;
  const r = await fetch('/api/gmail', {
    method: body ? 'POST' : 'GET', cache: 'no-store',
    headers: { Authorization: `Bearer ${tok}`, ...(body ? { 'Content-Type': 'application/json' } : {}) },
    body: body ? JSON.stringify(body) : undefined,
  });
  const j = await r.json().catch(() => ({}));
  if (!r.ok) { const e = new Error(j.error || 'Não foi possível concluir.'); e.data = j; throw e; }
  return j;
}
window.horosGmailCall = horosGmailCall;

// Abre a janela do Google pedindo só a permissão de enviar e-mails; devolve { connected, email }
async function horosGmailConnect() {
  const cfg = await horosGmailCall();
  const params = new URLSearchParams({
    client_id: cfg.client_id, redirect_uri: 'https://horos.sale/google-oauth-callback.html', response_type: 'code',
    scope: `${cfg.scope} email openid`, access_type: 'offline', prompt: 'consent',
  });
  const popup = window.open(`https://accounts.google.com/o/oauth2/v2/auth?${params}`, 'gmail-oauth', 'width=520,height=680,top=80,left=200');
  if (!popup) throw new Error('O navegador bloqueou a janela do Google. Permita pop-ups para horos.sale.');
  return new Promise((resolve, reject) => {
    const done = () => { window.removeEventListener('message', onMsg); clearInterval(timer); };
    const onMsg = async (ev) => {
      if (ev.origin !== location.origin || !ev.data) return;
      if (ev.data.type === 'google-oauth-error') { done(); reject(new Error('O Google não autorizou: ' + ev.data.error)); return; }
      if (!ev.data.code) return;
      done();
      try { resolve(await horosGmailCall({ action: 'connect', code: ev.data.code })); } catch (e) { reject(e); }
    };
    window.addEventListener('message', onMsg);
    const timer = setInterval(() => { if (popup.closed) setTimeout(() => { done(); reject(new Error('closed')); }, 900); }, 700);
  });
}
window.horosGmailConnect = horosGmailConnect;

function LeadEmailComposer({ lead, onClose }) {
  const [g, setG] = React.useState(null);
  const [to, setTo] = React.useState(lead.email || '');
  const [subject, setSubject] = React.useState('');
  const [body, setBody] = React.useState(() => `Olá, ${String(lead.name || '').split(' ')[0]}!\n\n`);
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState('');
  React.useEffect(() => { horosGmailCall().then(setG).catch(e => setG({ connected: false, error: e.message })); }, []);
  React.useEffect(() => {
    const onKey = (e) => { if (e.key === 'Escape') { e.stopPropagation(); onClose(); } };
    window.addEventListener('keydown', onKey, true);
    return () => window.removeEventListener('keydown', onKey, true);
  }, [onClose]);
  const connect = async () => {
    setErr(''); setBusy(true);
    try { const j = await horosGmailConnect(); setG(x => ({ ...x, connected: true, email: j.email })); }
    catch (e) { if (e.message !== 'closed') setErr(e.message); }
    setBusy(false);
  };
  const send = async () => {
    setErr(''); setBusy(true);
    try {
      await horosGmailCall({ action: 'send', lead_id: lead.id, to, subject, body });
      window.dispatchEvent(new CustomEvent('horos:toast', { detail: { message: 'E-mail enviado.' } }));
      window.dispatchEvent(new CustomEvent('horos:email-sent', { detail: { leadId: lead.id } }));
      onClose();
    } catch (e) {
      if (e.data?.error === 'not_connected' || e.data?.reconnect) setG(x => ({ ...x, connected: false }));
      setErr(e.data?.error === 'not_connected' ? 'Conecte o seu Gmail para enviar.' : e.message);
    }
    setBusy(false);
  };
  return ReactDOM.createPortal(
    <div className="lem-ov" onMouseDown={e => { if (e.target === e.currentTarget) onClose(); }}>
      <div className="lem-box" role="dialog" aria-label="Enviar e-mail">
        <div className="lem-head">
          <b>Enviar e-mail</b>
          <button className="lem-x" onClick={onClose} aria-label="Fechar">
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="m6 6 12 12M18 6 6 18" /></svg>
          </button>
        </div>
        {!g ? <div className="lem-body"><div className="lem-muted">Carregando…</div></div> : !g.connected ? (
          <div className="lem-body">
            <p className="lem-muted">Conecte o seu Gmail uma vez: o Horos só pede permissão para <b>enviar</b> e-mails — não lê a sua caixa de entrada. O e-mail sai da sua conta, como se você tivesse escrito no Gmail.</p>
            {err && <div className="lem-err">{err}</div>}
            <div className="lem-foot"><button className="lem-btn" onClick={onClose}>Cancelar</button><button className="lem-btn lem-btn--primary" onClick={connect} disabled={busy}>{busy ? 'Conectando…' : 'Conectar o Gmail'}</button></div>
          </div>
        ) : (
          <div className="lem-body">
            <div className="lem-muted">De: {g.email}</div>
            <label className="lem-field"><span>Para</span><input value={to} onChange={e => setTo(e.target.value)} /></label>
            <label className="lem-field"><span>Assunto</span><input value={subject} onChange={e => setSubject(e.target.value)} autoFocus /></label>
            <label className="lem-field"><span>Mensagem</span><textarea rows={9} value={body} onChange={e => setBody(e.target.value)} /></label>
            {err && <div className="lem-err">{err}</div>}
            <div className="lem-foot"><button className="lem-btn" onClick={onClose}>Cancelar</button><button className="lem-btn lem-btn--primary" onClick={send} disabled={busy || !to || !subject.trim() || !body.trim()}>{busy ? 'Enviando…' : 'Enviar'}</button></div>
          </div>
        )}
      </div>
    </div>,
    document.body
  );
}

function LeadEmailButton({ lead }) {
  const [open, setOpen] = React.useState(false);
  if (!lead?.email) return null;
  return (
    <>
      <button type="button" className="lead-call-btn" onClick={() => setOpen(true)} title="Enviar e-mail pelo seu Gmail"><Icon name="msgs" size={12} /> E-mail</button>
      {open && <LeadEmailComposer lead={lead} onClose={() => setOpen(false)} />}
    </>
  );
}
window.LeadEmailButton = LeadEmailButton;

function LeadEmailsList({ leadId }) {
  const [rows, setRows] = React.useState(null);
  const load = React.useCallback(async () => {
    try {
      const { data } = await window.HOROS_SB.from('lead_emails').select('id,subject,to_addr,status,error,created_at')
        .eq('lead_id', leadId).order('created_at', { ascending: false }).limit(20);
      setRows(data || []);
    } catch { setRows([]); }
  }, [leadId]);
  React.useEffect(() => {
    if (!leadId || !window.HOROS_SB) return;
    load();
    const on = (e) => { if (e.detail?.leadId === leadId) load(); };
    window.addEventListener('horos:email-sent', on);
    return () => window.removeEventListener('horos:email-sent', on);
  }, [leadId, load]);
  if (!rows || !rows.length) return null;
  return (
    <div className="lead-calls">
      <div className="lead-calls-title">E-mails enviados</div>
      {rows.map(m => (
        <div key={m.id} className="lead-calls-row">
          <Icon name="msgs" size={13} />
          <span className="lead-calls-main">
            <b>{m.subject || '(sem assunto)'}</b>
            <span>{new Date(m.created_at).toLocaleString('pt-BR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })} · para {m.to_addr}{m.status === 'failed' ? ' · não enviado' : ''}</span>
          </span>
        </div>
      ))}
    </div>
  );
}
window.LeadEmailsList = LeadEmailsList;
