// Tela: Jornadas do Cliente — kanban de fluxo
const { useState: useJornState, useRef: useJornRef, useEffect: useJornEffect } = React;
const CHAT_MSGS = [
{ id: 1, from: 'them', text: 'Olá! Queria saber mais sobre os serviços de vocês.', time: '09:14' },
{ id: 2, from: 'me', ai: true, text: 'Oi! Que ótimo falar com você 😊 Me conta — qual é o principal desafio que você quer resolver agora?', time: '09:15' },
{ id: 3, from: 'them', text: 'Preciso melhorar meu processo comercial, tá bem desorganizado.', time: '09:17' },
{ id: 4, from: 'me', ai: true, text: 'Entendo! Você já usa alguma ferramenta hoje, ou está tudo em planilha?', time: '09:18' },
{ id: 5, from: 'them', text: 'Só planilha mesmo, já não aguento mais 😅', time: '09:19' },
{ id: 6, from: 'me', ai: true, text: 'Haha, a planilha sempre chega no limite! 📊 Tenho uma solução que encaixa certinho. Posso agendar uma demo rápida com nosso especialista esta semana?', time: '09:21' },
{ id: 7, from: 'them', text: 'Pode ser! Prefiro quinta ou sexta pela manhã.', time: '09:23' },
];
const AI_REPLIES = [
'Perfeito! Vou confirmar a disponibilidade do nosso especialista e já te retorno com o link de agendamento. 📅',
'Ótimo! Enquanto isso, posso te enviar um material rápido sobre como empresas do seu segmento usam nossa solução?',
'Entendido! Para personalizar a demo, qual é o tamanho da sua equipe comercial hoje?',
'Que legal! Com esse perfil você vai ver resultado muito rápido. Animada para a demo? 🚀',
];
// Atendentes do fluxo com contagem de casos ativos
const ATENDENTES_FLUXO = [
{ nome: 'Ana Beatriz', casos: 4, avatarIdx: 0 },
{ nome: 'Bruno Souza', casos: 2, avatarIdx: 1 },
{ nome: 'Camila Lima', casos: 3, avatarIdx: 2 },
{ nome: 'Diego Rocha', casos: 1, avatarIdx: 3 },
{ nome: 'Eva Pinto', casos: 4, avatarIdx: 4 },
{ nome: 'Felipe Couto', casos: 2, avatarIdx: 5 },
{ nome: 'Gabriela Sá', casos: 3, avatarIdx: 6 },
];
function ConversaDrawer({ contato, onClose }) {
const [msg, setMsg] = useJornState('');
const [msgs, setMsgs] = useJornState(CHAT_MSGS);
const [isAi, setIsAi] = useJornState(true);
const [aiTyping, setAiTyping] = useJornState(false);
const [sending, setSending] = useJornState(false);
const [analise, setAnalise] = useJornState(null);
const [analisando, setAnalisando] = useJornState(false);
const [showAnalise, setShowAnalise] = useJornState(false);
const endRef = useJornRef(null);
const fileRef = useJornRef(null);
const aiReplyIdx = useJornRef(0);
const toast = useToast();
const analisarConversa = async () => {
setAnalisando(true);
setShowAnalise(true);
try {
const historico = msgs.filter(m => m.text).map(m => ({ from: m.from, text: m.text }));
const res = await fetch('/api/ai/analise-conversa', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ contato, historico }),
});
const data = await res.json();
if (res.ok && data.ok) setAnalise(data);
else toast('Erro na análise: ' + (data.error || 'tente novamente'));
} catch {
toast('⚠️ Proxy offline — análise indisponível');
} finally {
setAnalisando(false);
}
};
const handleFile = (e) => {
const file = e.target.files?.[0];
if (!file) return;
const kb = (file.size / 1024).toFixed(0);
const mb = file.size > 1024 * 1024 ? (file.size / (1024 * 1024)).toFixed(1) + ' MB' : kb + ' KB';
const isImg = file.type.startsWith('image/');
if (isImg) {
const url = URL.createObjectURL(file);
setMsgs(prev => [...prev, { id: Date.now(), from: 'me', ai: false, type: 'img', url, name: file.name, size: mb, time: now() }]);
} else {
setMsgs(prev => [...prev, { id: Date.now(), from: 'me', ai: false, type: 'file', name: file.name, size: mb, time: now() }]);
}
toast('📎 Arquivo anexado — envio de mídia via API em breve');
e.target.value = '';
};
const now = () => new Date().toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' });
// Converte "(21) 98821-4490" ou "+55 21 9 8821-4490" → "5521988214490"
const toE164 = (raw = '') => {
const digits = (raw || '').replace(/\D/g, '');
return digits.startsWith('55') ? digits : '55' + digits;
};
const triggerAiReply = async (currentMsgs) => {
setAiTyping(true);
try {
const historico = (currentMsgs || msgs).filter(m => m.text).map(m => ({ from: m.from, text: m.text }));
const res = await fetch('/api/ai/sdr-reply', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ contato, historico }),
});
const data = await res.json();
const text = (res.ok && data.reply) ? data.reply : AI_REPLIES[aiReplyIdx.current % AI_REPLIES.length];
aiReplyIdx.current += 1;
setAiTyping(false);
setMsgs(prev => [...prev, { id: Date.now(), from: 'me', ai: true, text, time: now() }]);
} catch {
// fallback para resposta estática se proxy offline
const text = AI_REPLIES[aiReplyIdx.current % AI_REPLIES.length];
aiReplyIdx.current += 1;
setAiTyping(false);
setMsgs(prev => [...prev, { id: Date.now(), from: 'me', ai: true, text, time: now() }]);
}
};
const send = async () => {
if (!msg.trim() || sending) return;
const text = msg.trim();
// Adiciona na UI imediatamente (optimistic)
setMsgs(prev => [...prev, { id: Date.now(), from: 'me', ai: false, text, time: now() }]);
setMsg('');
// Tenta enviar via WhatsApp real
const phone = toE164(contato?.whatsapp || contato?.fone || '');
if (phone.length >= 12) {
setSending(true);
try {
const res = await fetch('/api/whatsapp/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ to: phone, message: text }),
});
const data = await res.json();
if (res.ok && data.ok) {
toast('✅ Mensagem enviada via WhatsApp');
} else {
toast('⚠️ Mensagem salva localmente — ' + (data.error || 'erro no envio'));
}
} catch {
toast('⚠️ Proxy offline — mensagem salva localmente');
} finally {
setSending(false);
}
}
};
const simulateLead = () => {
const leadMsgs = ['Tudo bem! Quando seria a demo?', 'Quantos usuários posso ter no plano inicial?', 'Tem integração com WhatsApp?', 'Qual é o valor mensal?'];
const text = leadMsgs[Math.floor(Math.random() * leadMsgs.length)];
const newMsg = { id: Date.now(), from: 'them', text, time: now() };
setMsgs(prev => {
const updated = [...prev, newMsg];
if (isAi) triggerAiReply(updated);
return updated;
});
};
const onKey = (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); } };
useJornEffect(() => { endRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [msgs, aiTyping]);
const WaIcon = () => (
);
const BoltIcon = () => (
);
return (
<>
{/* ── cabeçalho ── */}
{contato.nome}
online agora
WhatsApp
{/* ── painel análise Gemini ── */}
{showAnalise && (
✦ Análise Gemini
{analisando ? (
Analisando conversa…
) : analise ? (
{[
{ label:'Sentimento', value: analise.sentimento, color: analise.sentimento==='positivo'?'oklch(0.45 0.18 155)':analise.sentimento==='negativo'?'oklch(0.50 0.20 25)':'oklch(0.45 0.12 255)' },
{ label:'Urgência', value: analise.urgencia, color: analise.urgencia==='alta'?'oklch(0.50 0.20 25)':analise.urgencia==='media'?'oklch(0.55 0.18 75)':'oklch(0.45 0.12 255)' },
{ label:'Interesse', value: analise.interesse+'%', color: analise.interesse>=70?'oklch(0.45 0.18 155)':analise.interesse>=40?'oklch(0.55 0.18 75)':'oklch(0.50 0.20 25)' },
].map(({label,value,color}) => (
))}
Situação: {analise.resumo_curto}
💡 Ação: {analise.sugestao}
) : null}
)}
{/* ── banner IA ── */}
{isAi ? 'IA SDR respondendo automaticamente' : 'Modo manual — você está no controle'}
{isAi
?
:
}
{/* ── mensagens ── */}
Hoje
{msgs.map(m => (
{m.from === 'me' && m.ai && (
IA SDR
)}
{m.type === 'img' ? (
{m.name} · {m.size}
) : m.type === 'file' ? (
) : m.text}
{m.time}{m.from === 'me' && ' ✓✓'}
))}
{aiTyping && (
)}
{/* ── botão demo ── */}
{/* ── input ── */}
>
);
}
// ── JCardDrawer — detalhe + edição do card ─────────────────────────────────
function JCardDrawer({ card, colId, allCols, onClose, onSave, onDelete, onChat, onMove }) {
const [editing, setEditing] = useJornState(false);
const [titulo, setTitulo] = useJornState(card.titulo);
const [due, setDue] = useJornState(card.due || '');
const [status, setStatus] = useJornState(card.status || 'pending');
const toast = useToast();
const colAtual = allCols.find(c => c.id === colId);
const colMap = { blue: 'var(--acc-blue)', yellow: 'var(--acc-yellow)', pink: 'var(--acc-pink)', lav: 'var(--acc-lav)', mint: 'var(--acc-mint)' };
const save = () => {
onSave({ ...card, titulo, due, status });
setEditing(false);
toast('Card atualizado ✓');
};
const statusOpts = [
{ id: 'ok', label: '✓ Concluído', bg: 'oklch(0.93 0.07 155)', fg: 'oklch(0.30 0.12 155)' },
{ id: 'warn', label: '⚠ Atenção', bg: 'oklch(0.94 0.08 75)', fg: 'oklch(0.38 0.14 75)' },
{ id: 'pending', label: '○ Pendente', bg: 'oklch(0.95 0.01 250)', fg: 'var(--ink-muted)' },
];
return (
<>
{/* ── cabeçalho ── */}
{/* ── corpo ── */}
{/* título */}
{editing
?
setTitulo(e.target.value)} autoFocus onKeyDown={e => e.key === 'Enter' && save()} />
:
setEditing(true)} title="Clique para editar">{titulo}
}
{/* status */}
Status
{statusOpts.map(s => (
))}
{/* prazo */}
Prazo
{editing
?
setDue(e.target.value)} placeholder="Ex: Hoje, Quinta, 30/05" />
:
setEditing(true)}>{due || Sem prazo — clique para adicionar}
}
{/* contato */}
{card.contato && (
Contato
{card.contato.nome}
{card.contato.empresa &&
{card.contato.empresa}
}
)}
{/* mover para outra coluna */}
Mover para
{allCols.filter(c => c.id !== colId).map(c => (
))}
{/* ── ações ── */}
{card.contato && (
)}
{editing
?
:
}
>
);
}
function ColorDot({ c }) {
const map = { blue: 'var(--acc-blue)', yellow: 'var(--acc-yellow)', pink: 'var(--acc-pink)', mint: 'var(--acc-mint)', lav: 'var(--acc-lav)' };
return ;
}
function JTaskRow({ card, onChat, onOpen, isDragging, onDragStart, onDragEnd }) {
const statusIcon = card.status === 'ok'
? {ICN.doubleCheck}
: card.status === 'warn'
? {ICN.dot}
: null;
const WaBadgeIcon = () => (
);
return (
!isDragging && onOpen && onOpen(card)}
>
{card.contato && (
{ e.stopPropagation(); onChat && onChat(card.contato); }} title={`WhatsApp · ${card.contato.nome.split(' ')[0]}`}>
)}
{card.titulo}
{card.due &&
{card.due}
}
{statusIcon &&
{statusIcon}
}
{card.featured &&
{ICN.more}
}
);
}
function JColumn({ col, idx, onChat, onOpen, isOver, dragCardId, onDragStart, onDragEnd, onDragOver, onDragLeave, onDrop }) {
return (
{col.nome}
{col.descritor}
{col.cards.length}
{col.cards.map(card => {
if (col.id === 'novos') {
return (
onDragStart(e, card.id)}
onDragEnd={onDragEnd}
onClick={() => onOpen && onOpen(card)}
>
{card.pinned && {ICN.pin}}
{card.titulo}
);
}
return (
onDragStart(e, card.id)}
onDragEnd={onDragEnd}
/>
);
})}
);
}
// ── Modal: Novo Fluxo — usa 100% o design system existente ────────────────
function NovoFluxoModal({ onClose, onSave }) {
const [nome, setNome] = useJornState('');
const [cor, setCor] = useJornState('blue');
const [desc, setDesc] = useJornState('');
const COR_OPTS = [
{ id: 'blue', label: 'Azul', bg: 'var(--acc-blue)' },
{ id: 'yellow', label: 'Amarelo', bg: 'var(--acc-yellow)' },
{ id: 'pink', label: 'Rosa', bg: 'var(--acc-pink)' },
{ id: 'lav', label: 'Lavanda', bg: 'var(--acc-lav)' },
{ id: 'mint', label: 'Verde', bg: 'var(--acc-mint)' },
];
const submit = () => {
if (!nome.trim()) return;
onSave(nome.trim(), desc.trim(), cor);
};
return (
e.stopPropagation()}>
Novo Fluxo de Jornada
Configure o novo fluxo do kanban
{ICN.x}
);
}
/* ── Modal: Novo item de Conhecimento ────────────────────── */
function NovoConhecimentoModal({ onClose, onAdd }) {
const [assunto, setAssunto] = useJornState('');
const [resp, setResp] = useJornState('');
const [status, setStatus] = useJornState('Agendado');
const [inicio, setInicio] = useJornState('');
const [fim, setFim] = useJornState('');
const toast = useToast();
const STATUSES = [
{ label: 'Agendado', pillCls: 'pill-yellow' },
{ label: 'Em curso', pillCls: 'pill-blue' },
{ label: 'Executado', pillCls: 'pill-mint' },
{ label: 'Atrasado', pillCls: 'pill-pink' },
];
const valid = assunto.trim().length > 0;
const submit = () => {
if (!valid) return;
const st = STATUSES.find(s => s.label === status);
onAdd({ assunto: assunto.trim(), status, pillCls: st.pillCls, inicio: inicio || '—', fim: fim || '—', resp: resp.trim() || '—' });
toast(`"${assunto.trim()}" adicionado ✓`);
onClose();
};
return (
e.stopPropagation()} style={{width: 500}}>
Novo Conhecimento
Adicionar à base de conhecimento sugerido
{ICN.x}
);
}
/* ── Modal: Novo Ticket ──────────────────────────────────── */
function NovoTicketModal({ onClose, onCreate }) {
const [titulo, setTitulo] = useJornState('');
const [tipo, setTipo] = useJornState('Ativo');
const [resp, setResp] = useJornState('');
const toast = useToast();
const TIPOS = [
{ label: 'Executado', pillCls: 'pill-mint' },
{ label: 'Ativo', pillCls: 'pill-pink' },
{ label: 'Em revisão',pillCls: 'pill-yellow' },
];
const valid = titulo.trim().length > 0;
const submit = () => {
if (!valid) return;
if (onCreate) onCreate(titulo.trim(), resp.trim());
toast(`Ticket "${titulo.trim()}" criado ✓`);
onClose();
};
return (
e.stopPropagation()} style={{width: 460}}>
Novo Ticket
Adicionar à Jornada de Tickets
{ICN.x}
);
}
const jornHeaders = () => { const t = localStorage.getItem('ezza_token'); return { 'Content-Type':'application/json', ...(t?{'Authorization':`Bearer ${t}`}:{}) }; };
function Jornadas({ ticketStyle = 'rows', goto }) {
const [activeChat, setActiveChat] = useJornState(null);
const [activeCard, setActiveCard] = useJornState(null); // { card, colId }
const [cols, setCols] = useJornState([]); // carregado do banco
const [apiMode, setApiMode] = useJornState(false); // true quando dados vêm do banco
// Carrega colunas + cards reais do banco
useJornEffect(() => {
fetch('/api/jornadas', { headers: jornHeaders() })
.then(r => r.json())
.then(d => {
if (d.ok) {
const cards = d.cards || [];
setCols(d.colunas.map(col => ({
id: col.id, nome: col.titulo, color: col.cor || 'blue', descritor: '',
cards: cards.filter(c => c.coluna_id === col.id).map(c => ({
id: c.id, titulo: c.titulo, descricao: c.descricao,
contato: { nome: c.contato || '', avatar: AVATAR_URLS[(c.id || 0) % 12] },
status: 'ok', due: '', valor: parseFloat(c.valor) || 0,
})),
})));
setApiMode(true);
} else {
setCols(JORNADAS.map(c => ({ ...c, cards: [...c.cards] })));
}
})
.catch(() => setCols(JORNADAS.map(c => ({ ...c, cards: [...c.cards] }))));
}, []);
const [dragInfo, setDragInfo] = useJornState(null);
const [dragOverCol, setDragOverCol] = useJornState(null);
const [showNovoFluxo, setShowNovoFluxo] = useJornState(false);
const [filterAt, setFilterAt] = useJornState(null); // índice em ATENDENTES_FLUXO
const [filterStatus, setFilterStatus] = useJornState(null); // 'ok' | 'warn' | 'pending'
const [showFilterMenu, setShowFilterMenu] = useJornState(false);
const [showNovoConhecimento, setShowNovoConhecimento] = useJornState(false);
const [localConhecimento, setLocalConhecimento] = useJornState([
{ assunto: 'Sprint de design', status: 'Executado', inicio: '30/09 01:12', fim: '01/10 01:11', resp: 'Sam Frank', pillCls: 'pill-mint' },
{ assunto: 'Encerramento de lead', status: 'Agendado', inicio: '02/10 17:41', fim: '02/10 18:41', resp: 'Nikki Olin', pillCls: 'pill-yellow' },
{ assunto: 'Revisão de SLA Q2', status: 'Em curso', inicio: '03/10 09:00', fim: '03/10 11:00', resp: 'Ana Carvalho', pillCls: 'pill-blue' },
{ assunto: 'Onboarding Café do Vale',status: 'Atrasado', inicio: '28/09 14:00', fim: '29/09 18:00', resp: 'Camila Lima', pillCls: 'pill-pink' },
]);
const [showNovoTicket, setShowNovoTicket] = useJornState(false);
const toast = useToast();
const filterMenuRef = useJornRef(null);
useJornEffect(() => {
if (!showFilterMenu) return;
const handler = (e) => {
if (filterMenuRef.current && !filterMenuRef.current.contains(e.target)) {
setShowFilterMenu(false);
}
};
document.addEventListener('mousedown', handler);
return () => document.removeEventListener('mousedown', handler);
}, [showFilterMenu]);
// handler: criar novo fluxo (coluna)
const handleNovoFluxo = async (nome, desc, cor) => {
setShowNovoFluxo(false);
if (apiMode) {
try {
const r = await fetch('/api/jornadas/colunas', { method:'POST', headers: jornHeaders(), body: JSON.stringify({ titulo: nome, cor }) });
const d = await r.json();
if (d.ok) {
setCols(prev => [...prev, { id: d.coluna.id, nome: d.coluna.titulo, color: d.coluna.cor, descritor: desc||'', cards: [] }]);
toast(`Fluxo "${nome}" criado ✓`);
return;
}
} catch (e) {}
}
setCols(prev => [...prev, { id: 'fluxo-'+Date.now(), nome, color: cor, descritor: desc||'Novo fluxo', cards: [] }]);
toast(`Fluxo "${nome}" criado ✓`);
};
// handlers do drawer de card
const handleOpenCard = (card, colId) => setActiveCard({ card, colId });
const handleSaveCard = (updated) => {
setCols(prev => prev.map(col => ({
...col,
cards: col.cards.map(c => c.id === updated.id ? { ...c, ...updated } : c),
})));
setActiveCard(prev => prev ? { ...prev, card: { ...prev.card, ...updated } } : null);
};
const handleDeleteCard = (cardId, colId) => {
setCols(prev => prev.map(col =>
col.id === colId ? { ...col, cards: col.cards.filter(c => c.id !== cardId) } : col
));
if (apiMode) { try { fetch(`/api/jornadas/cards/${cardId}`, { method:'DELETE', headers: jornHeaders() }); } catch (e) {} }
};
const handleDragStart = (e, cardId, colId) => {
setDragInfo({ cardId, fromColId: colId });
e.dataTransfer.effectAllowed = 'move';
};
const handleDragEnd = () => { setDragInfo(null); setDragOverCol(null); };
const handleDragOver = (e, colId) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
if (dragOverCol !== colId) setDragOverCol(colId);
};
const handleDragLeave = (e) => {
const rel = e.relatedTarget;
if (!rel || !e.currentTarget.contains(rel)) setDragOverCol(null);
};
const handleDrop = (e, toColId, overrideCardId, overrideFromColId) => {
e.preventDefault();
const cardId = overrideCardId ?? dragInfo?.cardId;
const fromColId = overrideFromColId ?? dragInfo?.fromColId;
if (!cardId || fromColId === toColId) { setDragInfo(null); setDragOverCol(null); return; }
setCols(prev => {
const card = prev.find(c => c.id === fromColId)?.cards.find(c => c.id === cardId);
if (!card) return prev;
return prev.map(col => {
if (col.id === fromColId) return { ...col, cards: col.cards.filter(c => c.id !== cardId) };
if (col.id === toColId) return { ...col, cards: [...col.cards, card] };
return col;
});
});
if (apiMode) { try { fetch(`/api/jornadas/cards/${cardId}`, { method:'PATCH', headers: jornHeaders(), body: JSON.stringify({ colunaId: toColId }) }); } catch (e) {} }
setDragInfo(null);
setDragOverCol(null);
};
// cols visíveis (com filtro de status se ativo)
const colsVisiveis = cols.map(col => ({
...col,
cards: filterStatus
? col.cards.filter(c => (c.status || 'pending') === filterStatus)
: col.cards,
}));
const totalAtivos = cols.reduce((s, c) => s + c.cards.length, 0);
return (
<>
{showNovoFluxo && setShowNovoFluxo(false)} onSave={handleNovoFluxo} />}
{activeChat && setActiveChat(null)} />}
{!activeChat && activeCard && (
setActiveCard(null)}
onSave={handleSaveCard}
onDelete={handleDeleteCard}
onChat={(contato) => { setActiveCard(null); setActiveChat(contato); }}
onMove={(cardId, fromColId, toColId) => handleDrop({ preventDefault: () => {} }, toColId, cardId, fromColId)}
/>
)}
{showNovoConhecimento && (
setShowNovoConhecimento(false)}
onAdd={(item) => setLocalConhecimento(prev => [...prev, item])}
/>
)}
{showNovoTicket && (
setShowNovoTicket(false)} onCreate={async (titulo, resp) => {
const col0 = cols[0];
if (!col0) return;
if (apiMode) {
try {
const r = await fetch('/api/jornadas/cards', { method:'POST', headers: jornHeaders(), body: JSON.stringify({ colunaId: col0.id, titulo, contato: resp }) });
const d = await r.json();
if (d.ok) {
setCols(prev => prev.map(c => c.id === col0.id ? { ...c, cards: [...c.cards, { id: d.card.id, titulo, contato: { nome: resp, avatar: AVATAR_URLS[(d.card.id||0)%12] }, status:'ok', due:'', valor:0 }] } : c));
return;
}
} catch (e) {}
}
setCols(prev => prev.map(c => c.id === col0.id ? { ...c, cards: [...c.cards, { id:'card-'+Date.now(), titulo, contato:{nome:resp,avatar:AVATAR_URLS[0]}, status:'ok', due:'' }] } : c));
}} />
)}
Jornadas do Cliente>}
subtitle="Acompanhe o fluxo de cada caso da triagem ao encerramento"
right={
{cols.length} fluxos ativos
setShowFilterMenu(m => !m)}
/>
{showFilterMenu && (
Filtrar cards por status
{[
{ id: null, label: 'Todos os cards', dot: 'var(--ink-faint)' },
{ id: 'ok', label: 'Concluídos', dot: 'var(--ok)' },
{ id: 'warn', label: 'Em atenção', dot: 'var(--warn)' },
{ id: 'pending', label: 'Pendentes', dot: 'var(--ink-muted)' },
].map(opt => (
))}
)}
setShowNovoFluxo(true)}
/>
}
/>
Gestão de Novos Casos
Atualizado há 4 minutos — {totalAtivos} cards em {cols.length} fluxos
{filterStatus && filtro ativo: {filterStatus === 'ok' ? 'concluídos' : filterStatus === 'warn' ? 'atenção' : 'pendentes'} }
{ATENDENTES_FLUXO.map((at, i) => (
setFilterAt(filterAt === i ? null : i)}
title={`${at.nome} — ${at.casos} casos ativos`}
>
{at.casos}
{at.nome}
{at.casos} casos ativos
))}
+4
{filterAt !== null && (
Filtrando por {ATENDENTES_FLUXO[filterAt].nome}
)}
setShowNovoFluxo(true)} onPeriod={() => goto && goto('calendario')} />
{colsVisiveis.map((col, i) => (
handleOpenCard(card, col.id)}
isOver={dragOverCol === col.id}
dragCardId={dragInfo?.cardId}
onDragStart={(e, cardId) => handleDragStart(e, cardId, col.id)}
onDragEnd={handleDragEnd}
onDragOver={(e) => handleDragOver(e, col.id)}
onDragLeave={handleDragLeave}
onDrop={(e) => handleDrop(e, col.id)}
/>
))}
Conhecimento Sugerido
setShowNovoConhecimento(true)} onPeriod={() => goto && goto('calendario')} />
| Assunto | Status | Início | Fim | Responsável |
{localConhecimento.map((r, i) => (
| {ICN.star} |
{r.assunto} |
{r.status} |
{r.inicio} |
{r.fim} |
{r.resp} |
))}
setShowNovoTicket(true)} goto={goto} />
>
);
}
// ──────────────────────────────────────────────────────────────────────────
// Jornada de Tickets — 3 variantes selecionáveis via Tweaks
// ──────────────────────────────────────────────────────────────────────────
const TICKET_STAGES = [
{ id: 'exec', label: 'Executado', n: 5, color: 'blue', trend: [2,3,2,3,4,4,5], delta: '+2' },
{ id: 'ativo', label: 'Ativo', n: 7, color: 'pink', trend: [4,5,6,5,7,6,7], delta: '+1' },
{ id: 'rev', label: 'Em revisão', n: 3, color: 'yellow', trend: [2,2,3,3,2,3,3], delta: '0' },
];
const STAGE_COLOR = {
blue: 'oklch(0.78 0.10 250)',
pink: 'oklch(0.82 0.09 18)',
yellow: 'oklch(0.88 0.13 95)',
};
function TicketJourneyCard({ variant, onCreate, goto }) {
const total = TICKET_STAGES.reduce((s, x) => s + x.n, 0);
return (
Jornada de Tickets
Últimos 7 dias · {total} tickets
goto && goto('relatorios')} />
{variant === 'bars' &&
}
{variant === 'rows' &&
}
{variant === 'donut' &&
}
);
}
/* ─── Variant A: stacked bar + legend rows ─────────────────────────────── */
function TJBars({ total }) {
return (
{TICKET_STAGES.map((s, i) => (
{s.n}
))}
{TICKET_STAGES.map(s => {
const pct = Math.round((s.n / total) * 100);
return (
{s.label}
{pct}%
{s.n}
);
})}
);
}
/* ─── Variant B: dense rows with sparklines ────────────────────────────── */
function TJRows({ total }) {
return (
{TICKET_STAGES.map(s => (
{s.label}
{s.delta}
{s.n}
))}
);
}
function Sparkline({ points, color }) {
const w = 100, h = 28, pad = 2;
const min = Math.min(...points), max = Math.max(...points);
const range = max - min || 1;
const stepX = (w - pad * 2) / (points.length - 1);
const coords = points.map((p, i) => [pad + i * stepX, h - pad - ((p - min) / range) * (h - pad * 2)]);
const path = coords.map((c, i) => `${i === 0 ? 'M' : 'L'} ${c[0].toFixed(1)} ${c[1].toFixed(1)}`).join(' ');
const area = path + ` L ${coords[coords.length-1][0].toFixed(1)} ${h} L ${coords[0][0].toFixed(1)} ${h} Z`;
const last = coords[coords.length - 1];
return (
);
}
/* ─── Variant C: donut with center total + side legend ─────────────────── */
function TJDonut({ total }) {
const size = 132, stroke = 18, r = (size - stroke) / 2;
const C = 2 * Math.PI * r;
let offset = 0;
const segs = TICKET_STAGES.map(s => {
const frac = s.n / total;
const seg = { ...s, dash: frac * C, gap: C - frac * C, rot: (offset / C) * 360 };
offset += frac * C;
return seg;
});
return (
{TICKET_STAGES.map(s => (
{s.label}
{s.n}
))}
);
}
window.Jornadas = Jornadas;