// 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 (
{i < totalSteps - 1 && (
)}
);
})}
{step === 1 && (
)}
{step === 2 && (
set('placa', e.target.value)} />
set('veiculo', e.target.value)} />
)}
{step === 3 && (
)}
{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]) => (
))}
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.
📅 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('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}
${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 ? `
` : ''}
${(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
Total de Registros
${rows.length}
Finalizadas
${finalizadas}
Faturamento Total
R$ ${total.toFixed(2)}
Ticket Médio
R$ ${(total / (rows.length || 1)).toFixed(2)}
| Nº | Data | Placa | Veículo | Serviço | Cliente | Motorista | Valor | Status |
${linhas || `| 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 }) => (
);
const Field = ({ label, value, mono }) => (
);
return (
Identificador
{assist.numero || `#${assist.id}`}
{new Date(assist.data).toLocaleString('pt-BR')}
{assist.servico}
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) => (
{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' }} />
{ setFiltroStatus(e.target.value); setPage(1); }} style={{ padding: '9px 12px', borderRadius: 8, border: '1px solid #E2E8F0', fontSize: 13, color: '#374151', outline: 'none', background: '#fff', cursor: 'pointer' }}>
{STATUSES.map(s => )}
{ setFiltroServ(e.target.value); setPage(1); }} style={{ padding: '9px 12px', borderRadius: 8, border: '1px solid #E2E8F0', fontSize: 13, color: '#374151', outline: 'none', background: '#fff', cursor: 'pointer' }}>
{SERVICOS.map(s => )}
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 => (
| {h} |
))}
{paged.map((a, i) => (
setViewing(a)}
onMouseEnter={e => e.currentTarget.style.background = '#EFF6FF'}
onMouseLeave={e => e.currentTarget.style.background = i % 2 ? '#F8FAFC' : '#fff'}>
| {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 });