// Caririaçu Guincho — Motoristas + Seguradoras Screens
const { useState: useStateM, useEffect: useEffectMC } = React;
// ── Modal de Detalhes do Motorista ───────────────────────────────────────
const MotoristaDetailModal = ({ motorista, onClose, onEdit, onToggleStatus }) => {
const [assistencias, setAssist] = useStateM([]);
const [frotaInfo, setFrotaInfo] = useStateM({});
useEffectMC(() => {
if (!motorista) return;
db.list('assistencias', { eq: { motorista_id: motorista.id }, order: 'data' }).then(setAssist);
if (motorista.guincho_code) {
db.list('frota', { eq: { code: motorista.guincho_code } }).then(rows => setFrotaInfo(rows[0] || {}));
} else setFrotaInfo({});
}, [motorista?.id]);
if (!motorista) return null;
const sc = {
disponivel: { bg: 'linear-gradient(135deg,#10B981 0%,#059669 100%)', label: 'Disponível' },
atendimento: { bg: 'linear-gradient(135deg,#F59E0B 0%,#D97706 100%)', label: 'Em Atendimento' },
inativo: { bg: 'linear-gradient(135deg,#64748B 0%,#475569 100%)', label: 'Inativo' },
}[motorista.status] || { bg: 'linear-gradient(135deg,#3B82F6 0%,#2563EB 100%)', label: motorista.status };
const faturamento = assistencias.filter(a => a.status === 'finalizada').reduce((s,a) => s + Number(a.valor || 0), 0);
const finalizadas = assistencias.filter(a => a.status === 'finalizada').length;
const taxa = assistencias.length > 0 ? Math.round(finalizadas / assistencias.length * 100) : 0;
const venc = motorista.venc_cnh ? new Date(motorista.venc_cnh) : null;
const hoje = new Date();
const diasParaVenc = venc ? Math.ceil((venc - hoje) / (1000 * 60 * 60 * 24)) : null;
const cnhAlerta = diasParaVenc !== null && diasParaVenc < 90;
const KpiCard = ({ icon, label, value, color, sub }) => (
);
const InfoBlock = ({ icon, title, color, children, full }) => (
);
const Field = ({ label, value, mono, color }) => (
);
return (
{motorista.avatar || motorista.nome.split(' ').map(w => w[0]).join('').slice(0,2).toUpperCase()}
{motorista.nome}
{motorista.telefone || '—'}
{motorista.guincho_code || '—'}
0 ? `${diasParaVenc} dias` : `vencida há ${-diasParaVenc} dias`) : '—'} />
0 ? 'Regular' : 'Vencida') : '—'} color={diasParaVenc !== null && diasParaVenc > 0 ? '#10B981' : '#EF4444'} />
{cnhAlerta && diasParaVenc > 0 && (
CNH vence em {diasParaVenc} dias — providenciar renovação
)}
{frotaInfo.placa ? (
) : (
Sem guincho vinculado
)}
Taxa de Finalização
{taxa}%
Volume de Atendimentos
{assistencias.length}
Assistências recentes
{assistencias.length} registros
{assistencias.length === 0 ? (
Nenhuma assistência vinculada a este motorista.
) : (
{['Nº','Data','Cliente','Serviço','Valor','Status'].map(h => (
| {h} |
))}
{assistencias.map((a,i) => (
| {a.numero || `#${a.id}`} |
{new Date(a.data).toLocaleDateString('pt-BR')} |
{a.cliente} |
{a.servico} |
R$ {Number(a.valor).toFixed(2)} |
|
))}
)}
motorista.telefone ? window.location.href = `tel:${motorista.telefone}` : showToast('Sem telefone cadastrado','warning')}>Ligar
{motorista.status === 'inativo' ? 'Ativar' : 'Inativar'}
Editar
);
};
const MotoristasScreen = () => {
const [motoristas, setMotoristas] = useStateM([]);
const [loading, setLoading] = useStateM(true);
const [modal, setModal] = useStateM(false);
const [editItem, setEditItem] = useStateM(null);
const [detailItem, setDetailItem] = useStateM(null);
const [busca, setBusca] = useStateM('');
const [form, setForm] = useStateM({ nome: '', telefone: '', cpf: '', cnh: '', categoria: 'E', venc_cnh: '', status: 'disponivel' });
const set = (k, v) => setForm(f => ({ ...f, [k]: v }));
const refresh = async () => {
setLoading(true);
setMotoristas(await db.list('motoristas', { order: 'nome', asc: true }));
setLoading(false);
};
useEffectMC(() => { refresh(); }, []);
const filtered = motoristas.filter(m => !busca || m.nome.toLowerCase().includes(busca.toLowerCase()));
const openNew = () => { setEditItem(null); setForm({ nome: '', telefone: '', cpf: '', cnh: '', categoria: 'E', venc_cnh: '', status: 'disponivel' }); setModal(true); };
const openEdit = (item) => { setEditItem(item); setForm({ nome: item.nome, telefone: item.telefone || '', cpf: item.cpf || '', cnh: item.cnh || '', categoria: item.categoria || 'E', venc_cnh: item.venc_cnh || '', status: item.status }); setModal(true); };
const handleSave = async () => {
if (!form.nome) { showToast('Informe o nome', 'warning'); return; }
const av = form.nome.split(' ').map(w => w[0]).join('').slice(0,2).toUpperCase();
const payload = { ...form, avatar: av, venc_cnh: form.venc_cnh || null };
if (editItem) {
const updated = await db.update('motoristas', editItem.id, payload);
if (updated) { showToast('Motorista atualizado!', 'success'); refresh(); }
} else {
const created = await db.insert('motoristas', payload);
if (created) { showToast('Motorista cadastrado!', 'success'); refresh(); }
}
setModal(false);
};
const toggleStatus = async (m) => {
const novoStatus = m.status === 'inativo' ? 'disponivel' : 'inativo';
const updated = await db.update('motoristas', m.id, { status: novoStatus });
if (updated) { showToast('Status atualizado!', 'success'); refresh(); }
};
return (
Motoristas
{motoristas.length} motoristas · {motoristas.filter(m=>m.status==='disponivel').length} disponíveis
Novo Motorista
{[
{ label: 'Disponíveis', val: motoristas.filter(m=>m.status==='disponivel').length, color: '#10B981' },
{ label: 'Em Atendimento', val: motoristas.filter(m=>m.status==='atendimento').length, color: '#F59E0B' },
{ label: 'Inativos', val: motoristas.filter(m=>m.status==='inativo').length, color: '#94A3B8' },
{ label: 'Total', val: motoristas.length, color: '#3B82F6' },
].map(s => (
{s.val}
{s.label}
))}
setBusca(e.target.value)} placeholder="Buscar por nome..."
style={{ width: '100%', paddingLeft: 34, paddingRight: 12, paddingTop: 9, paddingBottom: 9, borderRadius: 8, border: '1px solid #E2E8F0', fontSize: 13, fontFamily: 'Inter, sans-serif', outline: 'none', boxSizing: 'border-box' }} />
{loading ? (
Carregando...
) : filtered.length === 0 ? (
{motoristas.length === 0 ? 'Nenhum motorista cadastrado' : 'Nenhum resultado'}
{motoristas.length === 0 ? 'Cadastre o primeiro motorista clicando em "Novo Motorista"' : 'Tente outra busca'}
) : (
{['Motorista','Telefone','CNH','Categoria','Venc. CNH','Guincho','Assistências','Status','Ações'].map(h => (
| {h} |
))}
{filtered.map((m, i) => (
setDetailItem(m)}
onMouseEnter={e=>e.currentTarget.style.background='#EFF6FF'}
onMouseLeave={e=>e.currentTarget.style.background=i%2?'#F8FAFC':'#fff'}>
w[0]).join('').slice(0,2).toUpperCase()} size={32} />
{m.nome}
|
{m.telefone || '—'} |
{m.cnh || '—'} |
Cat. {m.categoria || '—'}
|
{m.venc_cnh ? new Date(m.venc_cnh).toLocaleDateString('pt-BR') : '—'} |
{m.guincho_code || '—'} |
{m.assistencias || 0} |
|
e.stopPropagation()}>
|
))}
)}
setDetailItem(null)}
onEdit={() => { const m = detailItem; setDetailItem(null); openEdit(m); }}
onToggleStatus={() => { const m = detailItem; setDetailItem(null); toggleStatus(m); }}
/>
setModal(false)} title={editItem ? 'Editar Motorista' : 'Novo Motorista'} width={520}>
set('nome',e.target.value)} style={{gridColumn:'1/-1'}}/>
set('telefone',e.target.value)}/>
set('cpf',e.target.value)}/>
set('cnh',e.target.value)}/>
setModal(false)}>Cancelar
{editItem?'Salvar':'Cadastrar'}
);
};
// ── Modal de Detalhes da Seguradora ──────────────────────────────────────
const SeguradoraDetailModal = ({ seg, onClose, onEdit }) => {
const [assistencias, setAssist] = useStateM([]);
useEffectMC(() => {
if (!seg) return;
db.list('assistencias', { eq: { seguradora_id: seg.id }, order: 'data' }).then(setAssist);
}, [seg?.id]);
if (!seg) return null;
const fmtBRL = v => Number(v || 0).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' });
const sc = seg.status === 'ativo'
? { bg: 'linear-gradient(135deg,#3B82F6 0%,#2563EB 100%)', label: 'Contrato Ativo' }
: { bg: 'linear-gradient(135deg,#64748B 0%,#475569 100%)', label: 'Inativo' };
const finalizadas = assistencias.filter(a => a.status === 'finalizada');
const fatTotal = finalizadas.reduce((s,a) => s + Number(a.valor || 0), 0);
const ticketMedio = finalizadas.length > 0 ? fatTotal / finalizadas.length : 0;
const abertas = assistencias.filter(a => a.status === 'aberta' || a.status === 'atendimento').length;
const porServico = {};
assistencias.forEach(a => { porServico[a.servico] = (porServico[a.servico] || 0) + 1; });
const servicoTop = Object.entries(porServico).sort((a,b) => b[1] - a[1])[0];
const KpiCard = ({ icon, label, value, color, sub }) => (
);
const InfoBlock = ({ icon, title, color, children, full }) => (
);
const Field = ({ label, value, mono }) => (
);
return (
{seg.nome}
{seg.contato || '—'}
{seg.telefone || '—'}
0 ? fmtBRL(ticketMedio) : '—'} color="#F59E0B" sub="por atendimento" />
0 ? '#EF4444' : '#10B981'} sub={abertas > 0 ? 'aguardando' : 'tudo em dia'} />
{Object.keys(porServico).length > 0 && (
{Object.entries(porServico).sort((a,b) => b[1] - a[1]).map(([servico, qtd]) => {
const pct = Math.round((qtd / assistencias.length) * 100);
return (
);
})}
)}
Assistências realizadas
{assistencias.length} registros
{assistencias.length === 0 ? (
Nenhuma assistência vinculada a esta seguradora.
) : (
{['Nº','Data','Veículo','Serviço','Motorista','Valor','Status'].map(h => (
| {h} |
))}
{assistencias.map((a,i) => (
| {a.numero || `#${a.id}`} |
{new Date(a.data).toLocaleDateString('pt-BR')} |
{a.veiculo} |
{a.servico} |
{(a.motorista_nome || '').split(' ')[0]} |
R$ {Number(a.valor).toFixed(2)} |
|
))}
)}
seg.telefone ? window.location.href = `tel:${seg.telefone}` : showToast('Sem telefone','warning')}>Ligar
seg.email ? window.location.href = `mailto:${seg.email}` : showToast('Sem e-mail','warning')}>E-mail
Editar
);
};
const SeguradorasScreen = () => {
const [seguradoras, setSeguradoras] = useStateM([]);
const [loading, setLoading] = useStateM(true);
const [modal, setModal] = useStateM(false);
const [editItem, setEditItem] = useStateM(null);
const [detailItem, setDetailItem] = useStateM(null);
const [form, setForm] = useStateM({ nome:'', contato:'', telefone:'', email:'', status:'ativo' });
const set = (k,v) => setForm(f=>({...f,[k]:v}));
const fmtBRL = v => Number(v || 0).toLocaleString('pt-BR',{style:'currency',currency:'BRL'});
const refresh = async () => {
setLoading(true);
setSeguradoras(await db.list('seguradoras', { order: 'nome', asc: true }));
setLoading(false);
};
useEffectMC(() => { refresh(); }, []);
const openNew = () => { setEditItem(null); setForm({nome:'',contato:'',telefone:'',email:'',status:'ativo'}); setModal(true); };
const openEdit = (s) => { setEditItem(s); setForm({nome:s.nome,contato:s.contato||'',telefone:s.telefone||'',email:s.email||'',status:s.status}); setModal(true); };
const handleSave = async () => {
if (!form.nome) { showToast('Informe o nome', 'warning'); return; }
if (editItem) {
const updated = await db.update('seguradoras', editItem.id, form);
if (updated) { showToast('Seguradora atualizada!','success'); refresh(); }
} else {
const created = await db.insert('seguradoras', form);
if (created) { showToast('Seguradora cadastrada!','success'); refresh(); }
}
setModal(false);
};
return (
Seguradoras
{seguradoras.length} seguradoras cadastradas
Nova Seguradora
{loading ? (
Carregando...
) : seguradoras.length === 0 ? (
Nenhuma seguradora cadastrada
Cadastre a primeira clicando em "Nova Seguradora"
) : (
{['Seguradora','Contato','Telefone','Email','Assist./mês','Faturamento','Status','Ações'].map(h=>(
| {h} |
))}
{seguradoras.map((s,i)=>(
setDetailItem(s)}
onMouseEnter={e=>e.currentTarget.style.background='#EFF6FF'}
onMouseLeave={e=>e.currentTarget.style.background=i%2?'#F8FAFC':'#fff'}>
|
|
{s.contato || '—'} |
{s.telefone || '—'} |
{s.email || '—'} |
{s.assistencias_mes || 0} |
{fmtBRL(s.faturamento)} |
|
e.stopPropagation()}>
|
))}
)}
setDetailItem(null)}
onEdit={() => { const s = detailItem; setDetailItem(null); openEdit(s); }}
/>
setModal(false)} title={editItem?'Editar Seguradora':'Nova Seguradora'} width={480}>
);
};
Object.assign(window, { MotoristasScreen, SeguradorasScreen });