// Tela: Painel — visão geral /* ── Modal: Nova Tarefa ──────────────────────────────────── */ function NovaTarefaModal({ onClose, onAdd }) { const [titulo, setTitulo] = React.useState(''); const [data, setData] = React.useState(''); const [hora, setHora] = React.useState('09:00'); const [prio, setPrio] = React.useState('Média'); const [conta, setConta] = React.useState(''); const toast = useToast(); const PRIOS = ['Baixa', 'Média', 'Alta', 'Urgente']; const PRIO_CLS = { Baixa: 'pill', Média: 'pill-blue', Alta: 'pill-yellow', Urgente: 'pill-pink' }; const PRIO_DOT = { Baixa: 'lav', Média: 'blue', Alta: 'yellow', Urgente: 'pink' }; const valid = titulo.trim().length > 0; const submit = () => { if (!valid) return; if (onAdd) onAdd({ titulo: titulo.trim(), data, hora, prio, conta }); toast(`Tarefa criada: "${titulo.trim()}" ✓`); onClose(); }; return (
e.stopPropagation()} style={{width: 500}}>
Nova Tarefa
Adicionar à lista de hoje
{ICN.x}
setTitulo(e.target.value)} autoFocus />
setData(e.target.value)} />
setHora(e.target.value)} />
{PRIOS.map(p => ( setPrio(p)} style={{cursor:'default', fontSize:11}}>{p} ))}
setConta(e.target.value)} />
); } function StatCard({ icon, label, value, sub, color, onClick }) { const palette = { blue: { bg: 'oklch(0.93 0.06 250)', ic: 'oklch(0.45 0.18 250)', border: 'oklch(0.82 0.1 250)' }, mint: { bg: 'oklch(0.93 0.05 165)', ic: 'oklch(0.45 0.14 165)', border: 'oklch(0.82 0.09 165)' }, yellow: { bg: 'oklch(0.95 0.07 85)', ic: 'oklch(0.52 0.18 85)', border: 'oklch(0.86 0.12 85)' }, pink: { bg: 'oklch(0.94 0.05 10)', ic: 'oklch(0.52 0.18 10)', border: 'oklch(0.84 0.1 10)' }, lav: { bg: 'oklch(0.93 0.06 290)', ic: 'oklch(0.48 0.16 290)', border: 'oklch(0.82 0.1 290)' }, }; const p = palette[color] || palette.blue; return (
{ if (onClick) { e.currentTarget.style.boxShadow = '0 6px 24px -6px rgba(80,60,180,.16)'; e.currentTarget.style.transform = 'translateY(-1px)'; }}} onMouseLeave={e => { e.currentTarget.style.boxShadow = ''; e.currentTarget.style.transform = ''; }} > {/* Topo: label + ícone */}
{label}
{icon}
{/* Valor principal */}
{value}
{/* Rodapé: sub + barra colorida */}
{sub}
); } const painelHeaders = () => { const t = localStorage.getItem('ezza_token'); return { 'Content-Type':'application/json', ...(t?{'Authorization':`Bearer ${t}`}:{}) }; }; function Painel({ goto, onOpenContact, user }) { const [weight, setWeight] = React.useState('ponderado'); const [doneTasks, setDoneTasks] = React.useState({}); const [showTarefa, setShowTarefa] = React.useState(false); const [extraTarefas, setExtraTarefas] = React.useState([]); const [kpis, setKpis] = React.useState(null); const [ops, setOps] = React.useState(null); // oportunidades reais const [contatosReais, setContatosReais] = React.useState(null); const [tarefas, setTarefas] = React.useState(null); // tarefas reais const carregarTarefas = () => { fetch('/api/tarefas', { headers: painelHeaders() }) .then(r => r.json()) .then(d => { if (d.ok) setTarefas(d.tarefas); }) .catch(() => {}); }; React.useEffect(() => { fetch('/api/painel/kpis', { headers: painelHeaders() }) .then(r => r.json()) .then(d => { if (d.ok) setKpis(d.kpis); }) .catch(() => {}); fetch('/api/oportunidades', { headers: painelHeaders() }) .then(r => r.json()) .then(d => { if (d.ok) setOps(d.oportunidades.map(o => ({ ...o, valor: parseFloat(o.valor) || 0 }))); }) .catch(() => {}); fetch('/api/contatos', { headers: painelHeaders() }) .then(r => r.json()) .then(d => { if (d.ok) setContatosReais(d.contatos); }) .catch(() => {}); carregarTarefas(); }, []); // Cria tarefa no banco const criarTarefaReal = async (form) => { try { const r = await fetch('/api/tarefas', { method: 'POST', headers: painelHeaders(), body: JSON.stringify({ titulo: form.titulo + (form.conta ? ` — ${form.conta}` : ''), quando: form.data ? `${form.data}${form.hora ? ' ' + form.hora : ''}` : (form.hora || null), cor: { Baixa:'lav', Média:'blue', Alta:'yellow', Urgente:'pink' }[form.prio] || 'blue', }), }); const d = await r.json(); if (d.ok) setTarefas(prev => [d.tarefa, ...(prev || [])]); } catch (e) {} }; // Marca/desmarca tarefa const toggleTarefa = async (t) => { setTarefas(prev => prev.map(x => x.id === t.id ? { ...x, concluida: x.concluida ? 0 : 1 } : x)); try { await fetch(`/api/tarefas/${t.id}`, { method: 'PUT', headers: painelHeaders(), body: JSON.stringify({ ...t, concluida: t.concluida ? 0 : 1 }), }); } catch (e) {} }; // Oportunidades em uso: reais se houver, senão demo const OPS = (ops && ops.length) ? ops : window.OPORTUNIDADES; const toast = useToast(); const handleAddTarefa = (t) => { criarTarefaReal(t); // salva no banco setExtraTarefas(prev => [...prev, t]); // fallback visual (demo / offline) }; // ── Saudação dinâmica ────────────────────────────────────── const now = new Date(); const hour = now.getHours(); const saudacao = hour < 12 ? 'Bom dia' : hour < 18 ? 'Boa tarde' : 'Boa noite'; const primeiroNome = React.useMemo(() => { const raw = (user && user.name) ? user.name : 'Usuário'; return raw.split(' ')[0]; }, [user]); const dataHoje = React.useMemo(() => { const dias = ['Domingo','Segunda','Terça','Quarta','Quinta','Sexta','Sábado']; const meses = ['janeiro','fevereiro','março','abril','maio','junho','julho','agosto','setembro','outubro','novembro','dezembro']; const d = new Date(); return `${dias[d.getDay()]}, ${d.getDate()} de ${meses[d.getMonth()]}`; }, []); const BASE_TAREFAS = [ { t: "Enviar proposta — Construtora Paineiras", w: "Hoje 14:00", c: "pink" }, { t: "Ligar para Ana Carvalho (Tecnoluz)", w: "Hoje 15:30", c: "blue" }, { t: "Revisar SLA com Brasil Logística", w: "Amanhã 09:00", c: "yellow" }, { t: "Aprovar materiais — Editora Pampa", w: "Sexta 11:00", c: "mint" }, { t: "Demo para Café do Vale", w: "Próx. segunda", c: "lav" }, ]; const totalTarefas = BASE_TAREFAS.length + extraTarefas.length; const tarefasHoje = BASE_TAREFAS.filter(t => t.w.startsWith('Hoje')).length + extraTarefas.filter(e => e.data === new Date().toISOString().slice(0,10)).length; return ( <> {showTarefa && setShowTarefa(false)} onAdd={handleAddTarefa} />} {saudacao}, {primeiroNome}} subtitle={`${dataHoje} · ${tarefasHoje} ${tarefasHoje === 1 ? 'tarefa' : 'tarefas'} para hoje`} right={
{(() => { const avg = Math.round(OPS.reduce((s,n) => s + (n.prob||0), 0) / Math.max(1, OPS.length)); return `Pipeline · ${avg}% prob. média`; })()}
setShowTarefa(true)} />
} /> {(() => { // KPIs: usa dados reais do banco se disponível, senão usa dados demo const k = kpis; const negFechados = k ? k.opsFechadas : NEGOCIOS.filter(n => n.etapa === 'Fechado').length; const receitaV = k ? k.receita : NEGOCIOS.filter(n => n.etapa === 'Fechado').reduce((s,n) => s+n.valor, 0); const receitaFmt = receitaV >= 1e6 ? `R$ ${(receitaV/1e6).toFixed(2).replace('.',',')}M` : receitaV > 0 ? `R$ ${receitaV.toLocaleString('pt-BR')}` : 'R$ 0'; const totalNegocios = k ? k.opsTotal : NEGOCIOS.length; const totalContatos = k ? k.totalContatos : CONTATOS.length; const casosAbertos = k ? k.casosAbertos : CASOS.filter(c => c.status !== 'Resolvido').length; const casosCriticos = k ? k.casosCriticos : CASOS.filter(c => c.prioridade === 'Crítica').length; const leadsAtivos = k ? k.leadsAtivos : LEADS.filter(l => l.etapa !== 'fechado' && l.etapa !== 'perdido').length; const leadsTotal = k ? k.leadsTotal : LEADS.length; return (
goto('relatorios')} /> goto('contatos')} /> goto('oportunidades')} /> goto('casos')} />
); })()}
Funil de Pipeline
R$ {(OPS.filter(n => n.etapa !== 'Fechado').reduce((s,n) => s + n.valor, 0) / 1000).toFixed(0)}k em aberto
setWeight('ponderado')} style={{cursor:'default'}}>Ponderado setWeight('total')} style={{cursor:'default'}}>Total
Próximas tarefas
setShowTarefa(true)} onPeriod={() => goto('calendario')} />
    {/* Tarefas reais do banco (se autenticado) */} {tarefas !== null && tarefas.map((t) => (
  • {t.titulo} {t.quando || 'Sem data'}
  • ))} {/* Empty state real */} {tarefas !== null && tarefas.length === 0 && (
  • Nenhuma tarefa. Crie a primeira no botão acima.
  • )} {/* Fallback demo (não autenticado) */} {tarefas === null && [ ...BASE_TAREFAS, ...extraTarefas.map(e => ({ t: e.titulo + (e.conta ? ` — ${e.conta}` : ''), w: e.data ? `${e.data}${e.hora ? ' ' + e.hora : ''}` : e.hora || 'Sem data', c: { Baixa:'lav', Média:'blue', Alta:'yellow', Urgente:'pink' }[e.prio] || 'blue' })), ].map((it, i) => (
  • {it.t} {it.w}
  • ))}
Histórico de Interações
goto('oportunidades')} onPeriod={() => goto('relatorios')} />
{(() => { const CORES = ['azul','teal','preto','amarelo','claro']; const fmtData = (o) => { const raw = o.criado_em || o.fecha; if (!raw) return '—'; const d = new Date(raw); if (!isNaN(d)) return `${String(d.getDate()).padStart(2,'0')}/${String(d.getMonth()+1).padStart(2,'0')}`; return String(raw).slice(0, 5); // já formatado "DD/MM" }; const interacoes = OPS.slice(0, 6).map((o, i) => ({ d: fmtData(o), t: o.conta, empresa: o.conta, tipo: o.etapa === 'Fechado' ? 'fechamento' : o.etapa === 'Proposta' ? 'proposta' : o.etapa === 'Negociação' ? 'reuniao' : 'ligacao', status: o.etapa === 'Fechado' ? 'ganho' : 'andamento', v: `R$ ${(o.valor || 0).toLocaleString('pt-BR')}`, c: CORES[i % CORES.length], ids: [i % 12, (i + 4) % 12], })); if (interacoes.length === 0) { return
Nenhuma oportunidade ainda. Crie a primeira na aba Oportunidades.
; } return interacoes.map((it,i) => ); })()}
{(() => { // Contato em destaque: real se houver, demo se não autenticado, empty state se cliente sem contatos const destaque = contatosReais === null ? window.CONTATOS[4] : (contatosReais[0] || null); if (!destaque) { return (
Cliente em destaque
{ICN.users}
Nenhum contato cadastrado ainda.
); } const avatarSrc = destaque.avatar || AVATAR_URLS[(destaque.id || 0) % 12]; return (
onOpenContact(destaque.id)}>
Cliente em destaque
{ navigator.clipboard?.writeText(`${window.location.origin}/contatos/${destaque.id}`).catch(()=>{}); toast('Link copiado ✓'); }} /> onOpenContact(destaque.id)} />
{destaque.nome.split(' ').slice(0,2).join(' ')}
{[destaque.cargo, destaque.empresa].filter(Boolean).join(' · ') || '—'}
e.stopPropagation()}> onOpenContact(destaque.id)} /> {destaque.email && window.open(`mailto:${destaque.email}?subject=Olá, ${destaque.nome.split(' ')[0]}!`)} />} {destaque.fone && window.open(`tel:${destaque.fone.replace(/\D/g,'')}`)} />} goto && goto('oportunidades')} /> goto && goto('calendario')} />
{destaque.tag &&
{destaque.tag}
}
); })()}
); } function InteractionCard({ it, goto }) { const bg = { azul: 'oklch(0.55 0.16 250)', teal: 'oklch(0.50 0.10 200)', preto: 'oklch(0.18 0.01 250)', amarelo: 'oklch(0.82 0.16 85)', claro: 'oklch(0.96 0.01 250)', }[it.c]; const isLight = it.c === 'amarelo' || it.c === 'claro'; const ink = isLight ? 'oklch(0.18 0.02 250)' : 'white'; const inkMid = isLight ? 'rgba(0,0,0,.5)' : 'rgba(255,255,255,.65)'; const pillBg = isLight ? 'rgba(0,0,0,.09)' : 'rgba(255,255,255,.18)'; const TIPO_ICN = { proposta: '📄', reuniao: '🗓️', fechamento: '🏆', email: '✉️', ligacao: '📞', }; const STATUS = { ganho: { label: 'Ganho', bg: 'rgba(34,197,94,.22)', color: isLight ? 'oklch(0.35 0.14 145)' : 'oklch(0.88 0.14 145)' }, perdido: { label: 'Perdido', bg: 'rgba(239,68,68,.22)', color: isLight ? 'oklch(0.40 0.18 25)' : 'oklch(0.88 0.14 25)' }, andamento: { label: 'Em andamento',bg: 'rgba(255,255,255,.14)',color: inkMid }, }; const st = STATUS[it.status] || STATUS.andamento; return (
goto && goto('pipeline')} style={{ background: bg, color: ink, borderRadius: 18, padding: '14px 16px', minHeight: 130, display: 'flex', flexDirection: 'column', justifyContent: 'space-between', cursor: 'pointer', boxShadow: '0 8px 24px -12px rgba(20,30,60,.3)', transition: 'transform .15s, box-shadow .15s', }} onMouseEnter={e => { e.currentTarget.style.transform='translateY(-2px)'; e.currentTarget.style.boxShadow='0 14px 32px -12px rgba(20,30,60,.4)'; }} onMouseLeave={e => { e.currentTarget.style.transform=''; e.currentTarget.style.boxShadow='0 8px 24px -12px rgba(20,30,60,.3)'; }} > {/* Topo: data + tipo */}
{it.d} {TIPO_ICN[it.tipo] || '💬'}
{/* Título + empresa */}
{it.t}
{it.empresa}
{/* Rodapé: valor + status + avatares */}
{it.v}
{st.label}
{it.ids.slice(0,3).map(id => )}
); } function FunnelBars({ weight = 'ponderado', goto, ops }) { // Agrupa oportunidades por etapa, soma valor bruto e valor ponderado por prob individual const STAGE_META = { 'Qualificado': { label: 'Qualificação', color: 'blue' }, 'Proposta': { label: 'Proposta', color: 'pink' }, 'Negociação': { label: 'Negociação', color: 'mint' }, 'Fechado': { label: 'Fechado', color: 'lav' }, }; const ORDER = ['Qualificado', 'Proposta', 'Negociação', 'Fechado']; const grouped = {}; (ops || window.OPORTUNIDADES || []).forEach(n => { if (!grouped[n.etapa]) grouped[n.etapa] = { v: 0, w: 0 }; grouped[n.etapa].v += n.valor; grouped[n.etapa].w += n.valor * ((n.prob || 0) / 100); }); const stages = ORDER.filter(k => grouped[k]).map(k => ({ label: STAGE_META[k].label, color: STAGE_META[k].color, v: grouped[k].v, w: grouped[k].w, })); if (stages.length === 0) { return
Nenhuma oportunidade no funil ainda.
; } const maxV = Math.max(...stages.map(s => s.v), 1); const maxW = Math.max(...stages.map(s => s.w), 1); return (
{stages.map((s, i) => { const pct = weight === 'ponderado' ? (s.w / maxW) * 100 : (s.v / maxV) * 100; const displayV = weight === 'ponderado' ? Math.round(s.w) : s.v; return (
{s.label} R$ {displayV.toLocaleString('pt-BR')}
goto ? goto('pipeline') : null} />
); })}
); } window.Painel = Painel; window.InteractionCard = InteractionCard; window.FunnelBars = FunnelBars; window.StatCard = StatCard;