// Caririaçu Guincho — Relatórios + Configurações Screens const { useState: useStateR, useEffect: useEffectR } = React; // ── RELATÓRIOS ───────────────────────────────────────────────────────────── const RELATORIOS = [ { id: 'assistencias', icon: 'clipboard', color: '#3B82F6', title: 'Relatório de Assistências', desc: 'Histórico completo de atendimentos, por período, motorista ou seguradora.' }, { id: 'financeiro', icon: 'dollar', color: '#10B981', title: 'Relatório Financeiro', desc: 'Entradas, saídas e saldo por período. Análise de rentabilidade.' }, { id: 'motorista', icon: 'users', color: '#8B5CF6', title: 'Relatório por Motorista', desc: 'Performance individual: assistências, valores, tempo médio.' }, { id: 'seguradora', icon: 'shield', color: '#F59E0B', title: 'Relatório por Seguradora', desc: 'Volume e faturamento por parceira. Comparativo mensal.' }, { id: 'manutencoes', icon: 'wrench', color: '#EF4444', title: 'Relatório de Manutenções', desc: 'Manutenções da frota com custos e tipos por veículo.' }, ]; const RelatoriosScreen = () => { const [genModal, setGenModal] = useStateR(false); const [selRel, setSelRel] = useStateR(null); const [motoristas, setMotoristas] = useStateR([]); const [seguradoras, setSeguradoras] = useStateR([]); const [stats, setStats] = useStateR({ totalAss: 0, finalizadas: 0, faturamento: 0, ticket: 0, motoristasAtivos: 0, manutencoes: 0 }); const [form, setForm] = useStateR({ dataIni: new Date(Date.now() - 30 * 86400000).toISOString().slice(0,10), dataFim: new Date().toISOString().slice(0,10), motorista: '', seguradora: '', status: '' }); const set = (k,v) => setForm(f=>({...f,[k]:v})); useEffectR(() => { (async () => { const [m, s, ass, manut] = await Promise.all([ db.list('motoristas', { order: 'nome', asc: true }), db.list('seguradoras', { order: 'nome', asc: true }), db.list('assistencias'), db.list('manutencoes'), ]); setMotoristas(m); setSeguradoras(s); const finalizadas = ass.filter(a => a.status === 'finalizada'); const faturamento = finalizadas.reduce((sum, a) => sum + Number(a.valor || 0), 0); setStats({ totalAss: ass.length, finalizadas: finalizadas.length, faturamento, ticket: finalizadas.length > 0 ? faturamento / finalizadas.length : 0, motoristasAtivos: m.filter(x => x.status !== 'inativo').length, manutencoes: manut.length, }); })(); }, []); const openGen = (rel) => { setSelRel(rel); setGenModal(true); }; const handleGen = async () => { const dataEmissao = new Date().toLocaleDateString('pt-BR'); const numDoc = `REL-${selRel.id.toUpperCase()}-${Date.now().toString().slice(-6)}`; const periodoLabel = `${new Date(form.dataIni).toLocaleDateString('pt-BR')} a ${new Date(form.dataFim).toLocaleDateString('pt-BR')}`; let titulo = selRel.title; let colunas = []; let linhas = []; let totaisHTML = ''; if (selRel.id === 'assistencias') { let rows = await db.list('assistencias', { order: 'data' }); rows = rows.filter(r => { const d = new Date(r.data).toISOString().slice(0,10); return d >= form.dataIni && d <= form.dataFim; }); if (form.motorista) rows = rows.filter(r => r.motorista_id === parseInt(form.motorista)); if (form.seguradora) rows = rows.filter(r => r.seguradora_id === parseInt(form.seguradora)); if (form.status) rows = rows.filter(r => r.status === form.status); const total = rows.reduce((s, a) => s + Number(a.valor || 0), 0); colunas = ['Nº','Data','Placa','Veículo','Serviço','Cliente','Motorista','Valor','Status']; linhas = rows.map(a => [ `${a.numero || '#' + a.id}`, new Date(a.data).toLocaleDateString('pt-BR'), a.placa || '', a.veiculo || '', a.servico, a.cliente, (a.motorista_nome || '').split(' ')[0], `R$ ${Number(a.valor).toFixed(2)}`, `${a.status}` ]); totaisHTML = `
Total de registros${rows.length}
FATURAMENTO TOTALR$ ${total.toFixed(2)}
`; } else if (selRel.id === 'financeiro') { let rows = await db.list('transacoes', { order: 'data' }); rows = rows.filter(r => r.data >= form.dataIni && r.data <= form.dataFim); const ent = rows.filter(t => t.tipo === 'entrada').reduce((s,t) => s + Number(t.valor || 0), 0); const sai = rows.filter(t => t.tipo === 'saida').reduce((s,t) => s + Number(t.valor || 0), 0); colunas = ['Data','Descrição','Categoria','Tipo','Valor']; linhas = rows.map(t => [ new Date(t.data).toLocaleDateString('pt-BR'), t.descricao, t.categoria || '—', `${t.tipo}`, `${t.tipo === 'entrada' ? '+' : '−'} R$ ${Number(t.valor).toFixed(2)}` ]); totaisHTML = `
Total EntradasR$ ${ent.toFixed(2)}
Total SaídasR$ ${sai.toFixed(2)}
SALDOR$ ${(ent-sai).toFixed(2)}
`; } else if (selRel.id === 'motorista') { const rows = await db.list('motoristas', { order: 'nome', asc: true }); colunas = ['Motorista','Telefone','CNH','Categoria','Guincho','Assistências','Status']; linhas = rows.map(m => [ m.nome, m.telefone || '—', m.cnh || '—', m.categoria ? `Cat. ${m.categoria}` : '—', m.guincho_code || '—', `${m.assistencias || 0}`, `${m.status}` ]); } else if (selRel.id === 'seguradora') { const rows = await db.list('seguradoras', { order: 'nome', asc: true }); const totalFat = rows.reduce((s, r) => s + Number(r.faturamento || 0), 0); colunas = ['Seguradora','Contato','Telefone','Email','Assist./mês','Faturamento','Status']; linhas = rows.map(s => [ s.nome, s.contato || '—', s.telefone || '—', s.email || '—', `${s.assistencias_mes || 0}`, `R$ ${Number(s.faturamento).toFixed(2)}`, `${s.status}` ]); totaisHTML = `
FATURAMENTO TOTALR$ ${totalFat.toFixed(2)}
`; } else if (selRel.id === 'manutencoes') { let rows = await db.list('manutencoes', { order: 'data' }); rows = rows.filter(r => r.data >= form.dataIni && r.data <= form.dataFim); const total = rows.reduce((s, m) => s + Number(m.valor || 0), 0); colunas = ['Data','Guincho','Tipo','Descrição','Oficina','Valor']; linhas = rows.map(m => [ new Date(m.data).toLocaleDateString('pt-BR'), m.guincho_code || '—', `${m.tipo}`, m.descricao, m.oficina || '—', `R$ ${Number(m.valor).toFixed(2)}` ]); totaisHTML = `
TOTAL EM MANUTENÇÕESR$ ${total.toFixed(2)}
`; } const linhasHTML = linhas.length === 0 ? `Nenhum registro encontrado para os filtros selecionados` : linhas.map(l => `${l.map(c => `${c}`).join('')}`).join(''); const html = `${titulo}
Nº ${numDoc}
${dataEmissao}
Período
${periodoLabel}
Registros
${linhas.length}
Emissão
${dataEmissao}
${colunas.map(c => ``).join('')}${linhasHTML}
${c}
${totaisHTML ? `
${totaisHTML}
` : ''}
`; const win = window.open('', '_blank', 'width=1100,height=900'); if (!win) { showToast('Habilite popups para gerar o PDF', 'warning'); return; } win.document.write(html); win.document.close(); showToast(`${titulo} gerado`, 'success'); setGenModal(false); }; const fmtBRL = v => Number(v || 0).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL', maximumFractionDigits: 0 }); return (

Relatórios

Exporte dados em PDF com layout profissional
{RELATORIOS.map(rel => ( openGen(rel)} onMouseEnter={e=>{ e.currentTarget.style.boxShadow=`0 8px 24px ${rel.color}25`; e.currentTarget.style.transform='translateY(-2px)'; e.currentTarget.style.borderColor=rel.color+'50'; }} onMouseLeave={e=>{ e.currentTarget.style.boxShadow='0 1px 3px rgba(0,0,0,0.07)'; e.currentTarget.style.transform=''; e.currentTarget.style.borderColor='#E2E8F0'; }}>
{rel.title}
{rel.desc}
{ e.stopPropagation(); openGen(rel); }} style={{ alignSelf:'flex-start' }}>Gerar Relatório
))}
Resumo Geral
{[ { label:'Total Assistências', val: stats.totalAss, color:'#3B82F6' }, { label:'Finalizadas', val: stats.finalizadas, color:'#10B981' }, { label:'Faturamento', val: fmtBRL(stats.faturamento), color:'#F59E0B' }, { label:'Ticket Médio', val: fmtBRL(stats.ticket), color:'#8B5CF6' }, { label:'Motoristas Ativos', val: stats.motoristasAtivos, color:'#64748B' }, { label:'Manutenções', val: stats.manutencoes, color:'#EF4444' }, ].map(s=>(
{s.val}
{s.label}
))}
setGenModal(false)} title={selRel ? `Gerar: ${selRel.title}` : ''} width={500}>
set('dataIni',e.target.value)}/> set('dataFim',e.target.value)}/>
{(selRel?.id === 'motorista' || selRel?.id === 'assistencias') && ( set('seguradora',e.target.value)} options={[{value:'',label:'Todas as seguradoras'},...seguradoras.map(s=>({value:s.id,label:s.nome}))]}/> )} {selRel?.id === 'assistencias' && ( setEmp('nome', e.target.value)}/> setEmp('cnpj', e.target.value)}/> setEmp('telefone', e.target.value)}/> setEmp('email', e.target.value)}/>
setEmp('endereco', e.target.value)}/>
Logo
Logo da empresa
Para alterar, substitua o arquivo logo.png na pasta do projeto
Salvar Alterações
)} {activeTab === 'usuarios' && (

Usuários e Permissões

Convidar
{users.length === 0 ? (
Nenhum usuário cadastrado ainda.
) : ( {['Usuário','Email','Cargo','Status','Ações'].map(h=>)} {users.map((u,i)=>( ))}
{h}
w[0]).join('').slice(0,2).toUpperCase()} size={30}/>{u.nome}
{u.email} {u.cargo}
)}
)} {activeTab === 'alertas' && (

Configuração de Alertas

Alerta de Assistência Aberta
Notificar quando uma assistência ficar aberta sem atualização por:
setAlertConfig(c => ({ ...c, horas_aberta: +e.target.value }))} style={{ flex:1, accentColor:'#F59E0B' }}/> {alertConfig.horas_aberta}h
1 hora12 horas
{[ { key: 'email_enabled', label:'Notificação por email', desc:'Enviar email para administradores quando alerta disparar' }, { key: 'vistoria_pendente', label:'Alerta de vistoria pendente', desc:'Avisar quando assistência não tem vistoria após 30 min' }, { key: 'manutencao_alert', label:'Alerta de manutenção', desc:'Notificar quando guincho está em manutenção há mais de 48h' }, ].map(a=>(
{a.label}
{a.desc}
setAlertConfig(c => ({ ...c, [a.key]: !c[a.key] }))} style={{ width:44, height:24, borderRadius:12, background: alertConfig[a.key] ? '#F59E0B' : '#E2E8F0', cursor:'pointer', position:'relative', transition:'background 0.2s', flexShrink:0 }}>
))} Salvar Configurações
)} {activeTab === 'backup' && (

Backup dos Dados

Faça download de todos os dados do sistema em um único arquivo JSON.

{/* Status do último backup */}
7 ? '#FEF2F2' : diasDesdeUltimoBackup > 1 ? '#FFFBEB' : '#ECFDF5') : '#FEF2F2', border: lastBackup ? `1px solid ${diasDesdeUltimoBackup > 7 ? '#FECACA' : diasDesdeUltimoBackup > 1 ? '#FED7AA' : '#A7F3D0'}` : '1px solid #FECACA', marginBottom:20, display:'flex', alignItems:'center', gap:14 }}>
7 ? '#FEE2E2' : diasDesdeUltimoBackup > 1 ? '#FFF3C4' : '#D1FAE5') : '#FEE2E2', display:'flex', alignItems:'center', justifyContent:'center' }}> 7 ? 'alert' : 'check') : 'alert'} size={22} color={lastBackup ? (diasDesdeUltimoBackup > 7 ? '#DC2626' : diasDesdeUltimoBackup > 1 ? '#D97706' : '#059669') : '#DC2626'} />
{lastBackup ? ( <>
{diasDesdeUltimoBackup === 0 ? 'Backup feito hoje ✓' : diasDesdeUltimoBackup === 1 ? 'Backup feito ontem' : `Último backup há ${diasDesdeUltimoBackup} dias`}
{new Date(lastBackup).toLocaleString('pt-BR')}
) : ( <>
Nenhum backup feito ainda
Recomendado fazer backup pelo menos uma vez por semana
)}
{backupRunning ? 'Gerando...' : 'Fazer Backup Agora'}
{/* Frequência recomendada */}
Backup Diário

Recomendado para empresas com muito movimento. Faça antes do final do expediente todo dia.

Backup Semanal

Mínimo recomendado. Faça aos domingos ou segundas para garantir todos os dados da semana.

{/* O que está incluído */}
📦 O backup inclui:
{[ 'Todas as Assistências e OS', 'Frota completa de guinchos', 'Cadastro de motoristas', 'Cadastro de seguradoras', 'Histórico de manutenções', 'Transações financeiras', 'Vistorias com checklist', 'Configurações da empresa', 'Categorias financeiras', 'Usuários do sistema', ].map(item => (
{item}
))}
Backup automático na nuvem: além desse backup manual, o Supabase já faz backup automático diário do banco de dados. Esse backup em JSON é uma cópia adicional pra você guardar localmente.
)} {activeTab === 'categorias' && (

Categorias Financeiras

{categorias.length === 0 && !showNovaCat && (
Nenhuma categoria cadastrada.
)} {categorias.map((c,i)=>(
{editingCat === i ? ( <> setEditingCatVal(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') { renameCategoria(c, editingCatVal); setEditingCat(null); } if (e.key === 'Escape') setEditingCat(null); }} style={{ flex:1, padding:'4px 8px', borderRadius:6, border:'1px solid #3B82F6', fontSize:14, outline:'none', marginRight:8 }} /> ) : ( <> {c}
)}
))} {showNovaCat ? (
setNovaCat(e.target.value)} placeholder="Nome da categoria..." onKeyDown={e => { if (e.key === 'Enter' && novaCat.trim()) { addCategoria(novaCat); setNovaCat(''); setShowNovaCat(false); } if (e.key === 'Escape') { setShowNovaCat(false); setNovaCat(''); } }} style={{ flex:1, padding:'4px 8px', borderRadius:6, border:'1px solid #F59E0B', fontSize:14, outline:'none' }} />
) : ( setShowNovaCat(true)} style={{ marginTop:12 }}>Nova Categoria )}
)} {activeTab === 'combustiveis' && } {activeTab === 'tipos-despesa' && } {activeTab === 'postos' && } {activeTab === 'checklists' && }
setUserModal(false)} title={editingUser ? `Editar ${editingUser.nome}` : 'Convidar Usuário'} width={480}>
setUF('nome', e.target.value)}/> setUF('email', e.target.value)} disabled={!!editingUser}/>
setUF('status', e.target.value)} options={[{value:'ativo',label:'Ativo'},{value:'inativo',label:'Inativo'}]}/>
{!editingUser && (
Por enquanto, novos usuários precisam se cadastrar diretamente no Supabase Authentication. Aqui você pode editar permissões dos já cadastrados.
)}
setUserModal(false)}>Cancelar {editingUser ? 'Salvar' : 'OK'}
); }; Object.assign(window, { RelatoriosScreen, ConfigScreen });