feat(protese): modal da OS em abas + cupom de etapa + monitoramento de custódia

Reorganiza o gerenciamento da OS em ABAS (sem o scrollão único):
- Etapas: dados + stepper; substitui entregue/cancelar por "imprimir cupom" de cada
  etapa (comprovante imprimível que circula com o trabalho — janela de impressão).
- Linha do tempo: histórico com data/hora/autor; AQUI ficam marcar entregue / cancelar
  (e o fluxo do lab / acionar garantia).
- Itens & Valores: componentes, fotos e pagamentos.
- Monitoramento: custódia do trabalho (clínica ↔ laboratório) — "protético retirou" /
  "devolveu à clínica", com local atual em destaque e histórico de movimentações.

Backend: protese_os.local_atual + tabela protese_os_custodia + POST .../custodia
(valida transição, registra movimento e evento). GET detalhe expõe local_atual + custodia.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
VPS 4 Builder
2026-06-24 01:32:31 +02:00
parent 623c9990d6
commit 8cf4a83161
3 changed files with 216 additions and 77 deletions
+41
View File
@@ -3976,12 +3976,15 @@ app.get('/api/protese/os/:id', authGuard, async (req, res) => {
const { rows: fotos } = await pool.query('SELECT id, original_name, uploaded_nome, legenda, created_at FROM protese_os_foto WHERE os_id=$1 ORDER BY created_at', [req.params.id]);
const { rows: componentes } = await pool.query('SELECT id, nome, quantidade, observacao, enviado_nome, created_at FROM protese_os_componente WHERE os_id=$1 ORDER BY created_at', [req.params.id]);
const { rows: pagamentos } = await pool.query('SELECT id, lado, valor, forma, observacao, data, recebido_nome, created_at FROM protese_os_pagamento WHERE os_id=$1 ORDER BY data, created_at', [req.params.id]);
const { rows: custodia } = await pool.query('SELECT id, de_local, para_local, etapa, observacao, actor_nome, created_at FROM protese_os_custodia WHERE os_id=$1 ORDER BY created_at', [req.params.id]);
res.json({
...os, valor: parseFloat(os.valor || 0), custo: parseFloat(os.custo || 0),
local_atual: os.local_atual || 'clinica',
eventos,
fotos: fotos.map(f => ({ ...f, url: `/api/protese/os/fotos/${f.id}/raw?t=${signMedia(f.id)}` })),
componentes,
pagamentos: pagamentos.map(p => ({ ...p, valor: parseFloat(p.valor || 0) })),
custodia,
});
} catch (err) { res.status(500).json({ error: err.message }); }
});
@@ -4082,6 +4085,28 @@ app.delete('/api/protese/os/pagamentos/:id', authGuard, async (req, res) => {
} catch (err) { res.status(500).json({ error: err.message }); }
});
// Custódia: registrar que o trabalho mudou de mãos (clínica ↔ laboratório).
app.post('/api/protese/os/:id/custodia', authGuard, async (req, res) => {
try {
const uid = req.authUser.userId;
const acc = await carregaOSComAcesso(req.params.id, uid);
if (acc.error) return res.status(acc.status).json({ error: acc.error });
const para = ['clinica', 'laboratorio'].includes(req.body?.para_local) ? req.body.para_local : null;
if (!para) return res.status(400).json({ error: "para_local deve ser 'clinica' ou 'laboratorio'." });
const de = acc.os.local_atual || 'clinica';
if (de === para) return res.status(409).json({ error: `O trabalho já está ${para === 'laboratorio' ? 'com o laboratório' : 'na clínica'}.` });
const id = `pcus_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
const nome = await actorNome(uid);
await pool.query(
`INSERT INTO protese_os_custodia (id, os_id, clinica_id, de_local, para_local, etapa, observacao, actor_id, actor_nome) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)`,
[id, acc.os.id, acc.os.clinica_id, de, para, acc.os.status, req.body?.observacao || null, uid, nome]);
await pool.query('UPDATE protese_os SET local_atual=$1 WHERE id=$2', [para, acc.os.id]);
const rotulo = para === 'laboratorio' ? 'Protético retirou o trabalho (foi p/ o laboratório)' : 'Trabalho devolvido à clínica';
await logEventoOS(acc.os, uid, rotulo, 'custodia');
res.json({ success: true, id, local_atual: para });
} catch (err) { res.status(500).json({ error: err.message }); }
});
// Mudar status (laboratório avança a produção; clínica entrega/cancela).
app.post('/api/protese/os/:id/status', authGuard, async (req, res) => {
const novo = req.body?.status;
@@ -5874,6 +5899,22 @@ async function runMigrations() {
created_at TIMESTAMPTZ DEFAULT NOW()
)`,
`CREATE INDEX IF NOT EXISTS idx_protese_os_pag ON protese_os_pagamento (os_id, data)`,
// Custódia: onde o trabalho físico está agora (clinica|laboratorio) + log de movimentações
// (saber se o protético "pegou" o trabalho ou se está na clínica).
`ALTER TABLE protese_os ADD COLUMN IF NOT EXISTS local_atual TEXT DEFAULT 'clinica'`,
`CREATE TABLE IF NOT EXISTS protese_os_custodia (
id TEXT PRIMARY KEY,
os_id TEXT NOT NULL,
clinica_id TEXT,
de_local TEXT,
para_local TEXT NOT NULL,
etapa TEXT,
observacao TEXT,
actor_id TEXT,
actor_nome TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
)`,
`CREATE INDEX IF NOT EXISTS idx_protese_os_cust ON protese_os_custodia (os_id, created_at)`,
`CREATE TABLE IF NOT EXISTS protese_produtos (
id TEXT PRIMARY KEY,
protetico_id TEXT NOT NULL, -- dono (usuário protético)
+6
View File
@@ -813,6 +813,12 @@ export const HybridBackend = {
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao remover pagamento');
return await res.json();
},
// Custódia: registra que o trabalho mudou de mãos (para_local: 'clinica' | 'laboratorio')
moverProteseCustodia: async (osId: string, para_local: 'clinica' | 'laboratorio', observacao?: string) => {
const res = await apiFetch(`${API_URL}/protese/os/${osId}/custodia`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ para_local, observacao }) });
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao registrar movimentação');
return await res.json();
},
deleteProteseOS: async (id: string) => {
const clinicaId = HybridBackend.getActiveWorkspace()?.id || '';
await apiFetch(`${API_URL}/protese/os/${id}?clinicaId=${encodeURIComponent(clinicaId)}`, { method: 'DELETE' });
+130 -38
View File
@@ -1,5 +1,5 @@
import React, { useState, useEffect, useCallback } from 'react';
import { FlaskConical, Plus, X, Search, Clock, AlertTriangle, ImagePlus, ChevronRight, Trash2, RefreshCw, ShieldCheck, Tags, Pencil, Check, Package, Wallet, Camera, CheckCircle2, Circle, History } from 'lucide-react';
import { FlaskConical, Plus, X, Search, Clock, AlertTriangle, ImagePlus, ChevronRight, Trash2, RefreshCw, ShieldCheck, Tags, Pencil, Check, Package, Wallet, Camera, CheckCircle2, Circle, History, Printer, MapPin, Building2 } from 'lucide-react';
import { HybridBackend } from '../services/backend.ts';
import { useToast } from '../contexts/ToastContext.tsx';
import { useConfirm } from '../contexts/ConfirmContext.tsx';
@@ -181,6 +181,7 @@ const OSDetailModal: React.FC<{ id: string; modo: 'bancada' | 'clinica'; onClose
const confirm = useConfirm();
const [os, setOs] = useState<any>(null);
const [busy, setBusy] = useState(false);
const [tab, setTab] = useState<'etapas' | 'timeline' | 'itens' | 'monitor'>('etapas');
const carregar = useCallback(() => {
HybridBackend.getProteseOSDetalhe(id).then(setOs).catch(() => toast.error('ERRO AO CARREGAR.'));
@@ -195,14 +196,6 @@ const OSDetailModal: React.FC<{ id: string; modo: 'bancada' | 'clinica'; onClose
finally { setBusy(false); }
};
const anexar = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]; if (!file) return;
setBusy(true);
try { await HybridBackend.uploadProteseFoto(id, file); toast.success('FOTO ANEXADA.'); carregar(); }
catch (err: any) { toast.error((err.message || 'ERRO').toUpperCase()); }
finally { setBusy(false); }
};
const acionarGarantia = async () => {
if (!(await confirm('ABRIR UMA OS DE GARANTIA (SEM CUSTO) PARA REFAZER ESTE TRABALHO?'))) return;
setBusy(true);
@@ -221,36 +214,89 @@ const OSDetailModal: React.FC<{ id: string; modo: 'bancada' | 'clinica'; onClose
finally { setBusy(false); }
};
const moverCustodia = async (para: 'clinica' | 'laboratorio') => {
setBusy(true);
try { await HybridBackend.moverProteseCustodia(id, para); toast.success(para === 'laboratorio' ? 'TRABALHO COM O LABORATÓRIO.' : 'TRABALHO NA CLÍNICA.'); carregar(); onChanged(); }
catch (e: any) { toast.error((e.message || 'ERRO').toUpperCase()); }
finally { setBusy(false); }
};
if (!os) return null;
const finalizado = os.status === 'entregue' || os.status === 'cancelado';
const naGarantia = os.status === 'entregue' && os.garantia_ate && os.garantia_ate >= new Date().toISOString().slice(0, 10);
const noLab = (os.local_atual || 'clinica') === 'laboratorio';
// Cupom imprimível de uma etapa (comprovante que circula com o trabalho).
const imprimirCupom = (etapa: string) => {
const comps = (os.componentes || []).map((c: any) => `${c.quantidade}× ${c.nome}`).join(', ') || '—';
const w = window.open('', '_blank', 'width=420,height=640'); if (!w) return;
w.document.write(`<html><head><title>Cupom ${STATUS_LABEL[etapa] || etapa}</title><style>
*{font-family:Arial,Helvetica,sans-serif;color:#111}body{margin:0;padding:16px;font-size:12px}
h1{font-size:14px;margin:0 0 2px}.muted{color:#666;font-size:10px}
.et{background:#111;color:#fff;padding:6px 10px;border-radius:6px;font-weight:bold;text-align:center;text-transform:uppercase;margin:10px 0}
table{width:100%;border-collapse:collapse;margin-top:6px}td{padding:3px 0;vertical-align:top}
.lbl{color:#666;text-transform:uppercase;font-size:9px;width:38%}
.sign{margin-top:30px;display:flex;justify-content:space-between;gap:14px}
.sign div{flex:1;border-top:1px solid #111;padding-top:4px;text-align:center;font-size:9px;text-transform:uppercase;color:#666}
.hr{border-top:1px dashed #999;margin:10px 0}</style></head><body>
<h1>ORDEM DE PRÓTESE</h1><div class="muted">OS ${os.id} · ${new Date().toLocaleString('pt-BR')}</div>
<div class="et">Etapa: ${STATUS_LABEL[etapa] || etapa}</div>
<table>
<tr><td class="lbl">Paciente</td><td><b>${os.paciente_nome || '—'}</b></td></tr>
<tr><td class="lbl">Trabalho</td><td>${os.tipo || '—'}</td></tr>
<tr><td class="lbl">Dentista</td><td>${os.dentista_nome || '—'}</td></tr>
<tr><td class="lbl">Dentes</td><td>${os.dentes || '—'}</td></tr>
<tr><td class="lbl">Material / Cor</td><td>${os.material || '—'} / ${os.cor || '—'}</td></tr>
<tr><td class="lbl">Componentes</td><td>${comps}</td></tr>
<tr><td class="lbl">Prazo</td><td>${os.prazo_entrega ? dataBR(os.prazo_entrega) : '—'}</td></tr>
<tr><td class="lbl">Local atual</td><td>${noLab ? 'Laboratório' : 'Clínica'}</td></tr>
</table><div class="hr"></div>
${os.observacoes ? `<div class="muted">Obs.: ${os.observacoes}</div>` : ''}
<div class="sign"><div>Entregue por</div><div>Recebido por</div></div>
</body></html>`);
w.document.close(); w.focus(); setTimeout(() => w.print(), 250);
};
const TABS = [
{ id: 'etapas', label: 'Etapas' },
{ id: 'timeline', label: 'Linha do tempo' },
{ id: 'itens', label: 'Itens & Valores' },
{ id: 'monitor', label: 'Monitoramento' },
] as const;
return (
<div className="fixed inset-0 bg-black/40 z-50 flex items-center justify-center p-4" onClick={onClose}>
<div className="bg-white rounded-2xl w-full max-w-2xl max-h-[92vh] overflow-y-auto" onClick={e => e.stopPropagation()}>
<div className="flex items-center justify-between p-5 border-b border-gray-100 sticky top-0 bg-white z-10">
<div>
<div className="bg-white rounded-2xl w-full max-w-2xl h-[88vh] flex flex-col" onClick={e => e.stopPropagation()}>
{/* Header */}
<div className="flex items-center justify-between px-5 py-4 border-b border-gray-100">
<div className="min-w-0">
<h3 className="font-black text-gray-800 uppercase text-sm flex items-center gap-2"><FlaskConical size={16} className="text-violet-600" /> {os.tipo}</h3>
<p className="text-xs text-gray-400 mt-0.5">{os.paciente_nome || 'Sem paciente'} {os.dentes ? `· dentes ${os.dentes}` : ''}</p>
<p className="text-xs text-gray-400 mt-0.5 truncate">{os.paciente_nome || 'Sem paciente'} {os.dentes ? `· dentes ${os.dentes}` : ''}</p>
</div>
<div className="flex items-center gap-2 shrink-0">
<Badge s={os.status} />
<button onClick={onClose}><X size={20} className="text-gray-400" /></button>
</div>
<div className="p-5 space-y-5">
</div>
{/* Abas */}
<div className="flex border-b border-gray-100 px-2 overflow-x-auto">
{TABS.map(t => (
<button key={t.id} onClick={() => setTab(t.id)}
className={`px-3 py-2.5 text-xs font-black uppercase border-b-2 -mb-px whitespace-nowrap transition-colors ${tab === t.id ? 'border-violet-600 text-violet-700' : 'border-transparent text-gray-400 hover:text-gray-600'}`}>{t.label}</button>
))}
</div>
<div className="flex-1 overflow-y-auto p-5">
{tab === 'etapas' && (
<div className="space-y-5">
<div className="flex items-center gap-2 flex-wrap">
<Badge s={os.status} />
{os.tipo_os === 'garantia' && <span className="text-[9px] font-black px-2 py-0.5 rounded-full bg-emerald-100 text-emerald-700 uppercase flex items-center gap-1"><ShieldCheck size={10} /> Garantia</span>}
{os.atrasada && <span className="text-[9px] font-black px-2 py-0.5 rounded-full bg-red-100 text-red-600 uppercase flex items-center gap-1"><AlertTriangle size={10} /> Atrasada</span>}
{os.prazo_entrega && <span className="text-[10px] text-gray-400 flex items-center gap-1"><Clock size={11} /> Prazo {dataBR(os.prazo_entrega)}</span>}
<span className={`text-[9px] font-black px-2 py-0.5 rounded-full uppercase ${noLab ? 'bg-violet-100 text-violet-700' : 'bg-blue-100 text-blue-700'}`}>{noLab ? 'Com o laboratório' : 'Na clínica'}</span>
</div>
{/* Stepper de etapas */}
<EtapasStepper os={os} />
{os.garantia_ate && (
<div className="bg-emerald-50 rounded-xl px-3 py-2 flex items-center gap-2 text-xs text-emerald-700 font-bold">
<ShieldCheck size={14} /> Garantia até {dataBR(os.garantia_ate)} ({os.garantia_meses || 12} meses)
</div>
)}
{os.garantia_ate && (<div className="bg-emerald-50 rounded-xl px-3 py-2 flex items-center gap-2 text-xs text-emerald-700 font-bold"><ShieldCheck size={14} /> Garantia até {dataBR(os.garantia_ate)} ({os.garantia_meses || 12} meses)</div>)}
<div className="grid grid-cols-2 sm:grid-cols-3 gap-x-4 gap-y-1.5 text-xs">
{os.material && <div><span className="text-gray-400 font-bold uppercase text-[9px] block">Material</span>{os.material}</div>}
{os.cor && <div><span className="text-gray-400 font-bold uppercase text-[9px] block">Cor</span>{os.cor}</div>}
@@ -259,18 +305,24 @@ const OSDetailModal: React.FC<{ id: string; modo: 'bancada' | 'clinica'; onClose
{modo === 'clinica' && Number(os.valor) > 0 && <div><span className="text-gray-400 font-bold uppercase text-[9px] block">Valor cobrado</span>{fmtBRL(os.valor)}</div>}
</div>
{os.observacoes && <p className="text-xs text-gray-600 bg-gray-50 rounded-xl p-3">{os.observacoes}</p>}
<ComponentesSecao os={os} podeEditar={!finalizado} onChanged={carregar} />
<FotosSecao os={os} podeEditar={!finalizado} onChanged={carregar} />
<PagamentosSecao os={os} modo={modo} podeEditar={!finalizado} onChanged={carregar} />
{/* Histórico (auditoria) */}
<div>
<p className="text-[10px] font-black text-gray-400 uppercase mb-2 flex items-center gap-1.5"><History size={12} /> Histórico</p>
<p className="text-[10px] font-black text-gray-400 uppercase mb-2 flex items-center gap-1.5"><Printer size={12} /> Imprimir cupom de etapa</p>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
{FLUXO_OS.map(s => (
<button key={s} onClick={() => imprimirCupom(s)} className="flex items-center justify-center gap-1.5 px-2 py-2 rounded-xl border border-gray-200 text-gray-600 text-[11px] font-black uppercase hover:bg-gray-50"><Printer size={12} /> {STATUS_LABEL[s]}</button>
))}
</div>
</div>
</div>
)}
{tab === 'timeline' && (
<div className="space-y-4">
<p className="text-[10px] font-black text-gray-400 uppercase flex items-center gap-1.5"><History size={12} /> Linha do tempo</p>
<div className="space-y-2.5">
{os.eventos?.map((ev: any, i: number) => (
<div key={i} className="flex items-start gap-2.5 text-xs">
<span className={`w-2 h-2 rounded-full mt-1 shrink-0 ${ev.status === 'cancelado' ? 'bg-red-400' : ev.status === 'entregue' ? 'bg-green-500' : 'bg-violet-400'}`} />
<span className={`w-2 h-2 rounded-full mt-1 shrink-0 ${ev.status === 'cancelado' ? 'bg-red-400' : ev.status === 'entregue' ? 'bg-green-500' : ev.status === 'custodia' ? 'bg-blue-400' : 'bg-violet-400'}`} />
<div className="min-w-0">
<span className="font-bold text-gray-700">{ev.observacao || STATUS_LABEL[ev.status] || ev.status}</span>
<span className="text-gray-400 block text-[11px]">{dataHoraBR(ev.created_at)}{ev.actor_nome ? ` · ${ev.actor_nome}` : ''}</span>
@@ -278,12 +330,8 @@ const OSDetailModal: React.FC<{ id: string; modo: 'bancada' | 'clinica'; onClose
</div>
))}
</div>
</div>
</div>
{/* Ações por papel */}
{!finalizado && (
<div className="p-5 border-t border-gray-100 sticky bottom-0 bg-white space-y-2">
<div className="pt-3 border-t border-gray-100 space-y-2">
{modo === 'bancada' ? (
<div className="flex gap-2 flex-wrap">
{LAB_FLUXO.map(s => (
@@ -293,7 +341,7 @@ const OSDetailModal: React.FC<{ id: string; modo: 'bancada' | 'clinica'; onClose
</div>
) : (
<>
{!os.fotos?.length && <p className="text-[10px] text-amber-600 font-bold uppercase flex items-center gap-1"><AlertTriangle size={11} /> Anexe ao menos 1 foto antes de entregar (garantia)</p>}
{!os.fotos?.length && <p className="text-[10px] text-amber-600 font-bold uppercase flex items-center gap-1"><AlertTriangle size={11} /> Anexe ao menos 1 foto (aba Itens) antes de entregar</p>}
<div className="flex gap-2">
<button disabled={busy} onClick={() => mudar('entregue')} className="flex-1 py-2.5 rounded-xl bg-green-600 text-white font-black text-sm uppercase disabled:opacity-50">Marcar entregue</button>
<button disabled={busy} onClick={() => mudar('cancelado')} className="px-4 py-2.5 rounded-xl border border-red-200 text-red-600 font-black text-sm uppercase disabled:opacity-50">Cancelar</button>
@@ -303,10 +351,54 @@ const OSDetailModal: React.FC<{ id: string; modo: 'bancada' | 'clinica'; onClose
</div>
)}
{naGarantia && modo === 'clinica' && (
<div className="p-5 border-t border-gray-100 sticky bottom-0 bg-white">
<button disabled={busy} onClick={acionarGarantia} className="w-full py-2.5 rounded-xl bg-emerald-600 text-white font-black text-sm uppercase disabled:opacity-50 flex items-center justify-center gap-2"><ShieldCheck size={16} /> Acionar garantia (refazer sem custo)</button>
)}
</div>
)}
{tab === 'itens' && (
<div className="space-y-5">
<ComponentesSecao os={os} podeEditar={!finalizado} onChanged={carregar} />
<FotosSecao os={os} podeEditar={!finalizado} onChanged={carregar} />
<PagamentosSecao os={os} modo={modo} podeEditar={!finalizado} onChanged={carregar} />
</div>
)}
{tab === 'monitor' && <MonitoramentoSecao os={os} podeEditar={!finalizado} busy={busy} onMover={moverCustodia} />}
</div>
</div>
</div>
);
};
// ── Seção: Monitoramento de custódia (onde o trabalho está) ─────────────────
const MonitoramentoSecao: React.FC<{ os: any; podeEditar: boolean; busy: boolean; onMover: (p: 'clinica' | 'laboratorio') => void }> = ({ os, podeEditar, busy, onMover }) => {
const noLab = (os.local_atual || 'clinica') === 'laboratorio';
return (
<div className="space-y-5">
<p className="text-[10px] font-black text-gray-400 uppercase flex items-center gap-1.5"><MapPin size={12} /> Onde está o trabalho</p>
<div className={`rounded-2xl p-5 text-center ${noLab ? 'bg-violet-50' : 'bg-blue-50'}`}>
<div className={`w-14 h-14 rounded-2xl mx-auto flex items-center justify-center text-white ${noLab ? 'bg-violet-600' : 'bg-blue-600'}`}>{noLab ? <FlaskConical size={26} /> : <Building2 size={26} />}</div>
<p className={`mt-3 font-black uppercase text-sm ${noLab ? 'text-violet-700' : 'text-blue-700'}`}>{noLab ? 'Com o laboratório' : 'Na clínica'}</p>
<p className="text-[11px] text-gray-400 font-bold uppercase mt-0.5">{noLab ? 'O protético está com o trabalho' : 'O trabalho está na clínica'}</p>
</div>
{podeEditar && (
<div className="flex gap-2">
<button disabled={busy || noLab} onClick={() => onMover('laboratorio')} className="flex-1 py-2.5 rounded-xl bg-violet-600 text-white font-black text-xs uppercase disabled:opacity-40 flex items-center justify-center gap-2"><FlaskConical size={14} /> Protético retirou</button>
<button disabled={busy || !noLab} onClick={() => onMover('clinica')} className="flex-1 py-2.5 rounded-xl bg-blue-600 text-white font-black text-xs uppercase disabled:opacity-40 flex items-center justify-center gap-2"><Building2 size={14} /> Devolveu à clínica</button>
</div>
)}
<div>
<p className="text-[10px] font-black text-gray-400 uppercase mb-2 flex items-center gap-1.5"><History size={12} /> Movimentações</p>
<div className="space-y-2">
{(os.custodia || []).map((c: any) => (
<div key={c.id} className="flex items-center gap-2 text-xs bg-gray-50 rounded-lg px-3 py-2">
<span className="text-[9px] font-black px-1.5 py-0.5 rounded uppercase bg-gray-200 text-gray-600">{c.de_local === 'clinica' ? 'Clínica' : 'Lab'} {c.para_local === 'clinica' ? 'Clínica' : 'Lab'}</span>
<span className="text-[10px] text-gray-400 flex-1 truncate">{c.actor_nome} · {dataHoraBR(c.created_at)}</span>
</div>
))}
{(os.custodia || []).length === 0 && <p className="text-[11px] text-gray-300 font-bold uppercase">Sem movimentações registradas.</p>}
</div>
</div>
</div>
);