/* ============================================================ KeS Metas — CRM (Funil de Vendas / Kanban) ============================================================ */ const { useState, useRef } = React; const ORIGENS = ["Instagram", "LinkedIn", "WhatsApp", "Indicação", "Site", "Outbound", "Evento", "Outro"]; // Ordem das colunas no CSV de import/export (modelo da planilha). const CSV_COLS = ["nome", "empresa", "socios", "porte", "telefone", "telefone2", "telefone3", "email", "endereco", "instagram", "linkedin", "google", "origem", "valor", "responsavel", "tags", "observacoes", "etapa"]; function csvExampleRows(viewer, stages) { return [ { nome: "Maria Souza", empresa: "Loja Exemplo", socios: "Carlos Andrade; Marina Costa", porte: "Pequena", telefone: "(54) 9 9999-0000", telefone2: "(54) 3221-0000", telefone3: "", email: "maria@exemplo.com.br", endereco: "Av. Brasil, 1200 — Caxias do Sul/RS", instagram: "@lojaexemplo", linkedin: "linkedin.com/company/lojaexemplo", google: "g.page/lojaexemplo", origem: "Instagram", valor: "8500", responsavel: viewer.email, tags: "Quente;Inbound", observacoes: "Pediu proposta", etapa: stages[0].nome }, { nome: "João Lima", empresa: "Empresa XYZ", socios: "Roberto Lima", porte: "Média", telefone: "(54) 9 8888-1111", telefone2: "", telefone3: "", email: "joao@xyz.com.br", endereco: "Rua Os Dezoito do Forte, 800 — Bento Gonçalves/RS", instagram: "@empresaxyz", linkedin: "linkedin.com/company/xyz", google: "xyz.com.br", origem: "Indicação", valor: "15000", responsavel: viewer.email, tags: "Morno", observacoes: "", etapa: stages[1] ? stages[1].nome : stages[0].nome }, ]; } function tagClass(t) { const k = t.toLowerCase(); if (k === "quente") return "tag t-quente"; if (k === "morno") return "tag t-morno"; if (k === "frio") return "tag t-frio"; return "tag"; } function stageColor(tipo) { return tipo === "won" ? "var(--ok)" : tipo === "lost" ? "var(--kes-red)" : "var(--info)"; } function parseMoney(s) { if (typeof s === "number") return s; const n = String(s).replace(/[^\d,.-]/g, "").replace(/\.(?=\d{3}(\D|$))/g, "").replace(",", "."); return Math.max(0, Math.round(parseFloat(n) || 0)); } /* ---------------- Card de lead ---------------- */ function LeadCard({ lead, user, onOpen, onAgendar, onDragStart, onDragEnd, dragging }) { const K = window.KES; return (
onDragStart(e, lead)} onDragEnd={onDragEnd} onClick={onOpen}>
{lead.nome}
{lead.empresa}
{lead.valor ? K.fmtMoeda(lead.valor) : "—"}
{lead.tags && lead.tags.length > 0 && (
{lead.tags.map((t) => {t})}
)}
{lead.origem}{lead.porte ? ` · ${lead.porte}` : ""}
e.stopPropagation()}> {user && }
); } /* ---------------- Coluna do funil ---------------- */ function KanbanColumn({ stage, leads, usersById, onDropLead, onOpen, onAgendar, drag }) { const K = window.KES; const [over, setOver] = useState(false); const total = leads.reduce((t, l) => t + (l.valor || 0), 0); return (
{ e.preventDefault(); setOver(true); }} onDragLeave={() => setOver(false)} onDrop={(e) => { e.preventDefault(); setOver(false); onDropLead(stage.id); }}>
{stage.nome} {leads.length}
{K.fmtMoeda(total)}
{leads.length === 0 ? (
Arraste leads para cá
) : leads.map((l) => ( onOpen(l)} onAgendar={() => onAgendar(l)} onDragStart={drag.start} onDragEnd={drag.end} /> ))}
); } /* ---------------- Modal de lead (cadastro completo) ---------------- */ function LeadModal({ initial, stages, users, viewer, onClose, onSave, onDelete, onAgendar }) { const editing = !!(initial && initial.id); const isAdmin = viewer.papel === "admin"; const [f, setF] = useState(initial || { nome: "", empresa: "", telefone: "", telefone2: "", telefone3: "", email: "", socios: "", instagram: "", google: "", linkedin: "", porte: "", endereco: "", origem: "Instagram", valor: "", responsavelId: viewer.id, tags: [], observacoes: "", stageId: stages[0].id, }); const [tagInput, setTagInput] = useState(""); const [err, setErr] = useState(""); const set = (k, v) => setF((s) => ({ ...s, [k]: v })); function addTag(v) { const t = v.trim(); if (t && !f.tags.includes(t)) set("tags", [...f.tags, t]); setTagInput(""); } function save() { if (!f.nome.trim()) return setErr("Informe o nome do contato."); onSave({ ...f, valor: parseMoney(f.valor) }); } return ( {editing && onDelete && Excluir} Cancelar {editing ? "Salvar" : "Cadastrar"} }>
set("nome", e.target.value)} placeholder="Nome e sobrenome" /> set("empresa", e.target.value)} placeholder="Empresa" />
set("socios", e.target.value)} placeholder="Nomes dos sócios (separe por vírgula)" />
Contato
set("telefone", e.target.value)} placeholder="(54) 9 9999-9999" /> set("telefone2", e.target.value)} placeholder="(54) 9 9999-9999" />
set("telefone3", e.target.value)} placeholder="(54) 9 9999-9999" /> set("email", e.target.value)} placeholder="contato@empresa.com.br" />
set("endereco", e.target.value)} placeholder="Rua, número — Cidade/UF" />
Presença digital
set("instagram", e.target.value)} placeholder="@perfil" /> set("linkedin", e.target.value)} placeholder="linkedin.com/company/…" />
set("google", e.target.value)} placeholder="g.page/… ou site da empresa" />
Negócio
set("valor", e.target.value)} placeholder="0" />
{f.tags.map((t) => ( {t} ))} setTagInput(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === ",") { e.preventDefault(); addTag(tagInput); } }} />