// Caririaçu Guincho — Assistências Screen const { useState: useStateA, useEffect: useEffectA } = React; const SERVICOS = ['Guincho Leve','Guincho Pesado','Pane Seca','Bateria','Chaveiro','Outros']; const STATUSES = ['aberta','atendimento','agendada','finalizada','cancelada']; // ── Modal Nova Assistência ─────────────────────────────────────────────── const NewAssistModal = ({ open, onClose, onSave }) => { const [step, setStep] = useStateA(1); const [motoristas, setMotoristas] = useStateA([]); const [frota, setFrota] = useStateA([]); const [seguradoras, setSeguradoras] = useStateA([]); const [form, setForm] = useStateA({ cliente: 'Particular', servico: 'Guincho Leve', data: new Date().toISOString().slice(0,16), placa: '', veiculo: '', origem: '', destino: '', motorista_id: '', valor: '', obs: '' }); useEffectA(() => { if (!open) return; db.list('motoristas', { eq: { status: 'disponivel' }, order: 'nome', asc: true }).then(setMotoristas); db.list('frota', { eq: { status: 'ativo' }, order: 'code', asc: true }).then(setFrota); db.list('seguradoras', { eq: { status: 'ativo' }, order: 'nome', asc: true }).then(setSeguradoras); setStep(1); setForm({ cliente: 'Particular', servico: 'Guincho Leve', data: new Date().toISOString().slice(0,16), placa: '', veiculo: '', origem: '', destino: '', motorista_id: '', valor: '', obs: '' }); }, [open]); const set = (k, v) => setForm(f => ({ ...f, [k]: v })); const totalSteps = 4; const stepLabels = ['Chamado','Veículo','Operação','Confirmação']; const handleSave = async () => { const motoristaSel = motoristas.find(m => m.id === parseInt(form.motorista_id)); const seguradoraSel = seguradoras.find(s => s.nome === form.cliente); const payload = { data: form.data, cliente: form.cliente, seguradora_id: seguradoraSel?.id || null, servico: form.servico, placa: form.placa, veiculo: form.veiculo, origem: form.origem, destino: form.destino, motorista_id: motoristaSel?.id || null, motorista_nome: motoristaSel?.nome || null, motorista_avatar: motoristaSel?.avatar || null, valor: parseFloat(form.valor) || 0, obs: form.obs, status: 'aberta', }; const created = await db.insert('assistencias', payload); if (created) { showToast(`Assistência ${created.numero || '#' + created.id} criada com sucesso!`, 'success'); onSave?.(created); onClose(); } }; return (
{stepLabels.map((l, i) => { const n = i + 1; const done = n < step, active = n === step; return (
{done ? '✓' : n}
{l}
{i < totalSteps - 1 && (
)} ); })}
{step === 1 && (
set('data', e.target.value)} />
)} {step === 2 && (
set('placa', e.target.value)} /> set('veiculo', e.target.value)} />
)} {step === 3 && (
set('origem', e.target.value)} /> set('destino', e.target.value)} /> set('valor', e.target.value)} /> set('obs', e.target.value)} />
)} {step === 4 && (
{[ ['Cliente', form.cliente], ['Categoria', form.servico], ['Data/Hora', form.data?.replace('T',' ')], ['Veículo', `${form.veiculo} ${form.placa}`], ['Origem', form.origem], ['Destino', form.destino], ['Motorista', motoristas.find(m => m.id === parseInt(form.motorista_id))?.nome || '—'], ['Valor', form.valor ? `R$ ${form.valor}` : '—'], ].map(([k,v]) => (
{k}
{v || '—'}
))}
Confirme os dados antes de salvar. O número da assistência será gerado automaticamente.
)}
step > 1 ? setStep(s => s-1) : onClose()}> {step > 1 ? '← Anterior' : 'Cancelar'} step < totalSteps ? setStep(s => s+1) : handleSave()}> {step === totalSteps ? '✓ Salvar Assistência' : 'Próximo →'}
); }; // ── Modal Finalizar com Assinatura ────────────────────────────────────── const FinalizeAssistModal = ({ assist, onClose, onFinalized }) => { const [signGuin, setSignGuin] = useStateA(null); const [signCli, setSignCli] = useStateA(null); const [nomeGuin, setNomeGuin] = useStateA(''); const [nomeCli, setNomeCli] = useStateA(''); const [saving, setSaving] = useStateA(false); useEffectA(() => { if (assist) { setSignGuin(null); setSignCli(null); setNomeGuin(assist.motorista_nome || ''); setNomeCli(''); } }, [assist?.id]); if (!assist) return null; const handleFinalizar = async () => { if (!signGuin || !signCli) { showToast('Coletar as 2 assinaturas antes de finalizar', 'warning'); return; } if (!nomeCli.trim()) { showToast('Informe o nome do cliente', 'warning'); return; } setSaving(true); const updated = await db.update('assistencias', assist.id, { status: 'finalizada', assinatura_guincheiro: signGuin, assinatura_cliente: signCli, nome_guincheiro: nomeGuin || assist.motorista_nome, nome_cliente: nomeCli, data_finalizacao: new Date().toISOString(), }); setSaving(false); if (updated) { showToast(`${assist.numero || '#' + assist.id} finalizada com assinaturas`, 'success'); onFinalized?.(); onClose(); } }; return (
Veículo: {assist.veiculo} · {assist.placa}
Cliente: {assist.cliente}
Valor: R$ {Number(assist.valor).toFixed(2)}
Coletar as duas assinaturas no celular do guincheiro antes de finalizar.
setNomeGuin(e.target.value)} />
setNomeCli(e.target.value)} />
📅 Data de finalização será registrada em {new Date().toLocaleString('pt-BR')}
Cancelar {saving ? 'Salvando...' : 'Finalizar com Assinaturas'}
); }; // ── Modal Editar Assistência ───────────────────────────────────────────── const EditAssistModal = ({ assist, onClose, onSave }) => { const [form, setForm] = useStateA({}); const [motoristas, setMotoristas] = useStateA([]); const [seguradoras, setSeguradoras] = useStateA([]); useEffectA(() => { if (!assist) return; setForm({ servico: assist.servico, cliente: assist.cliente, status: assist.status, motorista_id: assist.motorista_id || '', valor: assist.valor, origem: assist.origem || '', destino: assist.destino || '', placa: assist.placa, veiculo: assist.veiculo, obs: assist.obs || '' }); db.list('motoristas', { order: 'nome', asc: true }).then(setMotoristas); db.list('seguradoras', { eq: { status: 'ativo' } }).then(setSeguradoras); }, [assist?.id]); if (!assist) return null; const set = (k, v) => setForm(f => ({ ...f, [k]: v })); const handleSave = async () => { const motoristaSel = motoristas.find(m => m.id === parseInt(form.motorista_id)); const seguradoraSel = seguradoras.find(s => s.nome === form.cliente); const payload = { servico: form.servico, cliente: form.cliente, status: form.status, seguradora_id: seguradoraSel?.id || null, motorista_id: motoristaSel?.id || null, motorista_nome: motoristaSel?.nome || null, motorista_avatar: motoristaSel?.avatar || null, valor: parseFloat(form.valor) || 0, origem: form.origem, destino: form.destino, placa: form.placa, veiculo: form.veiculo, obs: form.obs, }; const updated = await db.update('assistencias', assist.id, payload); if (updated) { onSave?.(updated); onClose(); } }; return (
set('status', e.target.value)} options={STATUSES.map(v => ({ value: v, label: v.charAt(0).toUpperCase() + v.slice(1) }))} /> set('placa', e.target.value)} /> set('veiculo', e.target.value)} /> set('motorista_id', e.target.value)} options={[{value:'',label:'Sem motorista'}, ...motoristas.map(m => ({ value: m.id, label: `${m.nome} — ${m.guincho_code || ''}` }))]} />
set('origem', e.target.value)} /> set('destino', e.target.value)} />
set('valor', e.target.value)} /> set('obs', e.target.value)} />
Cancelar Salvar Alterações
); }; // ── Geradores de PDF ───────────────────────────────────────────────────── const gerarAssistenciaPDF = async (assist) => { const motoristaInfo = assist.motorista_id ? await db.one('motoristas', assist.motorista_id) : null; const seguradoraInfo = assist.seguradora_id ? await db.one('seguradoras', assist.seguradora_id) : null; const dataEmissao = new Date().toLocaleDateString('pt-BR'); const numDoc = assist.numero || `ASS-${assist.id}`; const STATUS_BG = { aberta: 'linear-gradient(135deg,#3B82F6,#2563EB)', atendimento: 'linear-gradient(135deg,#F59E0B,#D97706)', agendada: 'linear-gradient(135deg,#8B5CF6,#7C3AED)', finalizada: 'linear-gradient(135deg,#10B981,#059669)', cancelada: 'linear-gradient(135deg,#EF4444,#DC2626)', }; const STATUS_LABEL = { aberta: 'Aberta', atendimento: 'Em Atendimento', agendada: 'Agendada', finalizada: 'Finalizada', cancelada: 'Cancelada' }; const html = ` Ordem de Serviço ${numDoc}
${numDoc}
${dataEmissao}
${numDoc}
${assist.servico} · ${new Date(assist.data).toLocaleString('pt-BR')}
Status
${STATUS_LABEL[assist.status] || assist.status}

Veículo Atendido

Placa
${assist.placa || '—'}
Modelo / Cor
${assist.veiculo || '—'}

Cliente / Seguradora

Nome
${assist.cliente || '—'}
Contato
${seguradoraInfo?.contato || '—'}
Telefone
${seguradoraInfo?.telefone || '—'}
E-mail
${seguradoraInfo?.email || '—'}

Rota do Serviço

Origem
${assist.origem || '—'}
Destino
${assist.destino || '—'}

Motorista Responsável

Nome
${assist.motorista_nome || '—'}
Telefone
${motoristaInfo?.telefone || '—'}
CNH
Cat. ${motoristaInfo?.categoria || '—'}
Guincho
${motoristaInfo?.guincho_code || '—'}
Categoria${assist.servico}
Data de Abertura${new Date(assist.data).toLocaleString('pt-BR')}
VALOR DO SERVIÇOR$ ${Number(assist.valor).toFixed(2).replace('.', ',')}
${assist.obs ? `

Observações

${assist.obs}
` : ''} ${(assist.assinatura_guincheiro || assist.assinatura_cliente) ? `

Assinaturas Coletadas

${assist.assinatura_guincheiro ? `` : '
'}
${assist.nome_guincheiro || assist.motorista_nome || 'Guincheiro'}
Assinatura do Guincheiro
${assist.assinatura_cliente ? `` : '
'}
${assist.nome_cliente || 'Cliente'}
Assinatura do Cliente
${assist.data_finalizacao ? `
📅 Finalizado em ${new Date(assist.data_finalizacao).toLocaleString('pt-BR')}
` : ''}
` : `
Assinatura do Cliente
Assinatura do Motorista
`}
`; const win = window.open('', '_blank', 'width=860,height=900'); if (!win) { showToast('Habilite popups para gerar o PDF', 'warning'); return; } win.document.write(html); win.document.close(); showToast(`OS de ${numDoc} pronta`, 'success'); }; const gerarListaAssistenciasPDF = (rows) => { const dataEmissao = new Date().toLocaleDateString('pt-BR'); const numDoc = `LIST-${Date.now().toString().slice(-6)}`; const total = rows.reduce((s, a) => s + Number(a.valor || 0), 0); const finalizadas = rows.filter(r => r.status === 'finalizada').length; const STATUS_LABEL = { aberta: 'Aberta', atendimento: 'Em Atend.', agendada: 'Agendada', finalizada: 'Finalizada', cancelada: 'Cancelada' }; const 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)} ${STATUS_LABEL[a.status] || a.status} `).join(''); const html = `Lista de Assistências
Nº ${numDoc}
${dataEmissao}
Total de Registros
${rows.length}
Finalizadas
${finalizadas}
Faturamento Total
R$ ${total.toFixed(2)}
Ticket Médio
R$ ${(total / (rows.length || 1)).toFixed(2)}
${linhas || ``}
DataPlacaVeículoServiçoClienteMotoristaValorStatus
Nenhuma assistência registrada
`; 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('Lista exportada — selecione "Salvar como PDF"', 'success'); }; // ── Modal de Detalhes da Assistência ───────────────────────────────────── const ViewAssistModal = ({ assist, onClose, onEdit, onCancel, onComplete }) => { const [motoristaInfo, setMotoristaInfo] = useStateA({}); const [seguradoraInfo, setSeguradoraInfo] = useStateA({}); const [frotaInfo, setFrotaInfo] = useStateA({}); useEffectA(() => { if (!assist) return; (async () => { const m = assist.motorista_id ? await db.one('motoristas', assist.motorista_id) : null; const s = assist.seguradora_id ? await db.one('seguradoras', assist.seguradora_id) : null; setMotoristaInfo(m || {}); setSeguradoraInfo(s || {}); if (m?.guincho_code) { const fr = await db.list('frota', { eq: { code: m.guincho_code } }); setFrotaInfo(fr[0] || {}); } else setFrotaInfo({}); })(); }, [assist?.id]); if (!assist) return null; const STATUS_COLORS = { aberta: { bg: 'linear-gradient(135deg,#3B82F6 0%,#2563EB 100%)', label: 'Aberta' }, atendimento: { bg: 'linear-gradient(135deg,#F59E0B 0%,#D97706 100%)', label: 'Em Atendimento' }, agendada: { bg: 'linear-gradient(135deg,#8B5CF6 0%,#7C3AED 100%)', label: 'Agendada' }, finalizada: { bg: 'linear-gradient(135deg,#10B981 0%,#059669 100%)', label: 'Finalizada' }, cancelada: { bg: 'linear-gradient(135deg,#EF4444 0%,#DC2626 100%)', label: 'Cancelada' }, }; const sc = STATUS_COLORS[assist.status] || STATUS_COLORS.aberta; const buildTimeline = () => { const t = new Date(assist.data).toLocaleString('pt-BR'); const base = [{ icon: 'plus', label: 'Chamado registrado', desc: `Origem: ${assist.cliente}`, time: t, color: '#3B82F6' }]; if (['atendimento','finalizada','cancelada'].includes(assist.status)) { base.push({ icon: 'user', label: 'Motorista designado', desc: `${assist.motorista_nome || '—'}`, time: t, color: '#8B5CF6' }); } if (['atendimento','finalizada'].includes(assist.status)) { base.push({ icon: 'truck', label: 'Em atendimento', desc: `Saída para ${assist.origem}`, time: t, color: '#F59E0B' }); } if (assist.status === 'finalizada') { base.push({ icon: 'check', label: 'Serviço finalizado', desc: `Entregue em ${assist.destino}`, time: t, color: '#10B981' }); } if (assist.status === 'cancelada') { base.push({ icon: 'x', label: 'Assistência cancelada', desc: 'Cancelamento registrado', time: t, color: '#EF4444' }); } return base; }; const InfoBlock = ({ icon, title, color, children, full }) => (
{title}
{children}
); const Field = ({ label, value, mono }) => (
{label}
{value || '—'}
); return (
Identificador
{assist.numero || `#${assist.id}`}
{new Date(assist.data).toLocaleString('pt-BR')} {assist.servico}
Status
{sc.label}
R$ {Number(assist.valor).toFixed(2)}
Valor cobrado pelo serviço
Origem
{assist.origem || '—'}
Destino
{assist.destino || '—'}
{assist.motorista_id ? (
w[0]).join('').slice(0,2).toUpperCase()} size={52} />
) : (
Sem motorista designado
)}
{buildTimeline().map((ev, i, arr) => (
{i < arr.length - 1 &&
}
{ev.label}
{ev.desc}
{ev.time}
))}
{assist.obs || 'Nenhuma observação registrada.'}
gerarAssistenciaPDF(assist)}>Imprimir
{assist.status !== 'cancelada' && assist.status !== 'finalizada' && ( Cancelar )} {assist.status !== 'finalizada' && assist.status !== 'cancelada' && ( Finalizar )} Editar
); }; const AssistenciasScreen = () => { const [rows, setRows] = useStateA([]); const [loading, setLoading] = useStateA(true); const [busca, setBusca] = useStateA(''); const [filtroStatus, setFiltroStatus] = useStateA(''); const [filtroServ, setFiltroServ] = useStateA(''); const [page, setPage] = useStateA(1); const [modalNew, setModalNew] = useStateA(false); const [viewing, setViewing] = useStateA(null); const [editing, setEditing] = useStateA(null); const [finalizing, setFinalizing] = useStateA(null); const PER = 6; const refresh = async () => { setLoading(true); setRows(await db.list('assistencias', { order: 'data' })); setLoading(false); }; useEffectA(() => { refresh(); }, []); const filtered = rows.filter(r => { const q = busca.toLowerCase(); const matchQ = !q || (r.numero || '').toLowerCase().includes(q) || (r.placa || '').toLowerCase().includes(q) || (r.cliente || '').toLowerCase().includes(q) || (r.motorista_nome || '').toLowerCase().includes(q); const matchS = !filtroStatus || r.status === filtroStatus; const matchSv = !filtroServ || r.servico === filtroServ; return matchQ && matchS && matchSv; }); const paged = filtered.slice((page-1)*PER, page*PER); const handleCancel = async (id) => { const updated = await db.update('assistencias', id, { status: 'cancelada' }); if (updated) { showToast('Assistência cancelada', 'warning'); refresh(); } }; const openFinalize = (a) => setFinalizing(a); return (

Assistências

{rows.length} assistências cadastradas
setModalNew(true)}>Nova Assistência
{ setBusca(e.target.value); setPage(1); }} placeholder="Buscar por número, placa, cliente..." style={{ width: '100%', paddingLeft: 34, paddingRight: 12, paddingTop: 9, paddingBottom: 9, borderRadius: 8, border: '1px solid #E2E8F0', fontSize: 13, color: '#0F172A', fontFamily: 'Inter, sans-serif', outline: 'none', boxSizing: 'border-box' }} />
gerarListaAssistenciasPDF(filtered)}>Exportar PDF
{loading ? (
Carregando assistências...
) : paged.length === 0 ? (
{rows.length === 0 ? 'Nenhuma assistência cadastrada' : 'Nenhuma assistência encontrada'}
{rows.length === 0 ? 'Clique em "Nova Assistência" para começar' : 'Tente ajustar os filtros'}
{rows.length === 0 && setModalNew(true)} style={{ marginTop: 16 }}>Nova Assistência}
) : (
{['Nº','Data/Hora','Placa','Veículo','Serviço','Cliente','Motorista','Valor','Status','Ações'].map(h => ( ))} {paged.map((a, i) => ( setViewing(a)} onMouseEnter={e => e.currentTarget.style.background = '#EFF6FF'} onMouseLeave={e => e.currentTarget.style.background = i % 2 ? '#F8FAFC' : '#fff'}> ))}
{h}
{a.numero || `#${a.id}`} {new Date(a.data).toLocaleString('pt-BR')} {a.placa || '—'} {a.veiculo || '—'} {a.servico} {a.cliente} {a.motorista_nome ? (
w[0]).join('').slice(0,2).toUpperCase()} size={24} /> {a.motorista_nome.split(' ')[0]}
) : }
R$ {Number(a.valor).toFixed(2)} e.stopPropagation()}>
)}
Mostrando {Math.min(filtered.length, PER)} de {filtered.length} registros
setModalNew(false)} onSave={() => refresh()} /> setEditing(null)} onSave={() => { showToast('Assistência atualizada!', 'success'); refresh(); }} /> setViewing(null)} onEdit={() => { const a = viewing; setViewing(null); setEditing(a); }} onCancel={() => { handleCancel(viewing.id); setViewing(null); }} onComplete={() => { const a = viewing; setViewing(null); openFinalize(a); }} /> setFinalizing(null)} onFinalized={refresh} />
); }; Object.assign(window, { AssistenciasScreen });