feat(protese): etapas livres, ajustes do trabalho e privacidade de valores do paciente
- Etapas repetíveis/puláveis: cada etapa tem ação Ir/Voltar/Repetir (não só "avançar para a próxima"). 'entregue' segue na Linha do tempo. - Ajustes do trabalho (aba Etapas): editar cor/material (PATCH .../os/:id); trocar o trabalho por outro + MOTIVO obrigatório (POST .../trocar); acréscimo/redução do valor cobrado do paciente com observação (POST .../ajuste-valor) — tudo auditado no histórico. - Privacidade: o protético (lab que não é da clínica) NÃO vê o que o paciente paga/deve — GET detalhe omite pagamentos do lado clínica e zera o valor cobrado; a bancada também zera o valor. O ajuste de valor do paciente é exclusivo da clínica (403 para o lab). No front, o modo bancada esconde "Recebido do paciente". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -318,24 +318,26 @@ const OSDetailModal: React.FC<{ id: string; modo: 'bancada' | 'clinica'; onClose
|
||||
<div className="space-y-1.5">
|
||||
{FLUXO_OS.map((s, i) => {
|
||||
const done = i < idxAtual, atual = i === idxAtual;
|
||||
const podeAvancar = !finalizado && i === idxAtual + 1 && s !== 'entregue';
|
||||
const ev = (os.eventos || []).find((e: any) => e.status === s);
|
||||
// Etapas livres: pode ir para qualquer uma (pular/repetir/voltar), exceto 'entregue' (timeline).
|
||||
const acaoLabel = atual ? 'Repetir' : done ? 'Voltar' : 'Ir';
|
||||
return (
|
||||
<div key={s} className="flex items-center gap-2 bg-gray-50 rounded-xl px-3 py-2">
|
||||
{done ? <CheckCircle2 size={16} className="text-teal-600 shrink-0" /> : atual ? <div className="w-4 h-4 rounded-full bg-violet-600 shrink-0" /> : <Circle size={16} className="text-gray-300 shrink-0" />}
|
||||
<span className={`text-xs font-black uppercase ${atual ? 'text-violet-700' : done ? 'text-gray-700' : 'text-gray-400'}`}>{STATUS_LABEL[s]}</span>
|
||||
<span className="text-[10px] text-gray-400">{ev ? dataBR(ev.created_at) : ''}</span>
|
||||
<div className="flex-1" />
|
||||
{podeAvancar && <button disabled={busy} onClick={() => mudar(s)} className="px-2.5 py-1 rounded-lg bg-violet-600 text-white text-[10px] font-black uppercase disabled:opacity-50 flex items-center gap-1"><ChevronRight size={11} /> Avançar</button>}
|
||||
{!finalizado && i === idxAtual + 1 && s === 'entregue' && <span className="text-[9px] text-amber-500 font-black uppercase">aba Linha do tempo</span>}
|
||||
{!finalizado && s !== 'entregue' && <button disabled={busy} onClick={() => mudar(s)} className={`px-2.5 py-1 rounded-lg text-[10px] font-black uppercase disabled:opacity-50 ${atual ? 'border border-violet-200 text-violet-700 hover:bg-violet-50' : 'bg-violet-600 text-white'}`}>{acaoLabel}</button>}
|
||||
{!finalizado && s === 'entregue' && <span className="text-[9px] text-amber-500 font-black uppercase">aba Linha do tempo</span>}
|
||||
<button onClick={() => imprimirCupom(s, 'termica')} title="Imprimir em impressora térmica (bobina 80mm)" className="p-1.5 rounded-lg border border-gray-200 text-gray-500 hover:bg-white"><Printer size={13} /></button>
|
||||
<button onClick={() => imprimirCupom(s, 'laser')} title="Imprimir em impressora laser (folha A4)" className="px-2 py-1.5 rounded-lg border border-gray-200 text-gray-500 hover:bg-white text-[9px] font-black uppercase">A4</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p className="text-[9px] text-gray-300 mt-1.5 uppercase font-bold flex items-center gap-1"><Printer size={10} /> = térmica 80mm · A4 = laser</p>
|
||||
<p className="text-[9px] text-gray-300 mt-1.5 uppercase font-bold flex items-center gap-1"><Printer size={10} /> = térmica 80mm · A4 = laser · etapas podem ser repetidas/puladas</p>
|
||||
</div>
|
||||
{!finalizado && <AjustesSecao os={os} modo={modo} onChanged={carregar} />}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -383,6 +385,75 @@ const OSDetailModal: React.FC<{ id: string; modo: 'bancada' | 'clinica'; onClose
|
||||
);
|
||||
};
|
||||
|
||||
// ── Seção: Ajustes do trabalho (cor/material, valor ao paciente, troca) ─────
|
||||
const AjustesSecao: React.FC<{ os: any; modo: 'bancada' | 'clinica'; onChanged: () => void }> = ({ os, modo, onChanged }) => {
|
||||
const toast = useToast();
|
||||
const [cor, setCor] = useState(os.cor || '');
|
||||
const [material, setMaterial] = useState(os.material || '');
|
||||
const [delta, setDelta] = useState('');
|
||||
const [obsVal, setObsVal] = useState('');
|
||||
const [trocaOpen, setTrocaOpen] = useState(false);
|
||||
const [novoTrab, setNovoTrab] = useState('');
|
||||
const [motivo, setMotivo] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const salvarCM = async () => {
|
||||
setBusy(true);
|
||||
try { await HybridBackend.editarProteseOS(os.id, { cor: cor.trim(), material: material.trim() }); toast.success('TRABALHO ATUALIZADO.'); onChanged(); }
|
||||
catch (e: any) { toast.error((e.message || 'ERRO').toUpperCase()); } finally { setBusy(false); }
|
||||
};
|
||||
const ajustar = async (sinal: number) => {
|
||||
const v = parseFloat(delta);
|
||||
if (!(v > 0)) { toast.error('INFORME O VALOR DO AJUSTE.'); return; }
|
||||
setBusy(true);
|
||||
try { await HybridBackend.ajustarValorProtese(os.id, sinal * v, obsVal.trim() || undefined); toast.success('VALOR DO PACIENTE AJUSTADO.'); setDelta(''); setObsVal(''); onChanged(); }
|
||||
catch (e: any) { toast.error((e.message || 'ERRO').toUpperCase()); } finally { setBusy(false); }
|
||||
};
|
||||
const trocar = async () => {
|
||||
if (!novoTrab.trim() || !motivo.trim()) { toast.error('INFORME O NOVO TRABALHO E O MOTIVO.'); return; }
|
||||
setBusy(true);
|
||||
try { await HybridBackend.trocarTrabalhoProtese(os.id, { tipo: novoTrab.trim(), motivo: motivo.trim() }); toast.success('TRABALHO TROCADO.'); setTrocaOpen(false); setNovoTrab(''); setMotivo(''); onChanged(); }
|
||||
catch (e: any) { toast.error((e.message || 'ERRO').toUpperCase()); } finally { setBusy(false); }
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="border-t border-gray-100 pt-4 space-y-3">
|
||||
<p className="text-[10px] font-black text-gray-400 uppercase flex items-center gap-1.5"><Pencil size={12} /> Ajustes do trabalho</p>
|
||||
{/* Cor / material */}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<input value={cor} onChange={e => setCor(e.target.value)} placeholder="Cor (ex.: A2)" className="p-2 bg-gray-50 border border-gray-100 rounded-lg text-xs outline-none focus:border-teal-400" />
|
||||
<input value={material} onChange={e => setMaterial(e.target.value)} placeholder="Material" className="p-2 bg-gray-50 border border-gray-100 rounded-lg text-xs outline-none focus:border-teal-400" />
|
||||
</div>
|
||||
<button disabled={busy} onClick={salvarCM} className="px-3 py-1.5 rounded-lg bg-gray-700 text-white text-[11px] font-black uppercase disabled:opacity-50">Salvar cor / material</button>
|
||||
|
||||
{/* Ajuste de valor ao paciente — só clínica (protético não vê) */}
|
||||
{modo === 'clinica' && (
|
||||
<div className="bg-gray-50 rounded-xl p-3 space-y-2">
|
||||
<p className="text-[10px] font-black text-gray-500 uppercase">Ajustar valor do paciente {Number(os.valor) > 0 ? `· atual ${fmtBRL(os.valor)}` : ''}</p>
|
||||
<div className="flex gap-2">
|
||||
<input value={delta} onChange={e => setDelta(e.target.value)} type="number" step="0.01" placeholder="0,00" className="w-24 p-2 bg-white border border-gray-100 rounded-lg text-xs font-bold outline-none focus:border-teal-400" />
|
||||
<input value={obsVal} onChange={e => setObsVal(e.target.value)} placeholder="Motivo (ex.: pino extra)" className="flex-1 p-2 bg-white border border-gray-100 rounded-lg text-xs outline-none focus:border-teal-400" />
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button disabled={busy} onClick={() => ajustar(1)} className="flex-1 py-1.5 rounded-lg bg-green-600 text-white text-[11px] font-black uppercase disabled:opacity-50">+ Acrescentar</button>
|
||||
<button disabled={busy} onClick={() => ajustar(-1)} className="flex-1 py-1.5 rounded-lg bg-amber-500 text-white text-[11px] font-black uppercase disabled:opacity-50">− Reduzir</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Trocar trabalho */}
|
||||
<button onClick={() => setTrocaOpen(o => !o)} className="px-3 py-1.5 rounded-lg border border-violet-200 text-violet-700 text-[11px] font-black uppercase hover:bg-violet-50 flex items-center gap-1.5"><RefreshCw size={12} /> Solicitar troca de trabalho</button>
|
||||
{trocaOpen && (
|
||||
<div className="bg-violet-50 rounded-xl p-3 space-y-2">
|
||||
<input value={novoTrab} onChange={e => setNovoTrab(e.target.value)} placeholder="Novo trabalho (ex.: PPR metálica)" className="w-full p-2 bg-white border border-violet-100 rounded-lg text-xs outline-none focus:border-violet-400" />
|
||||
<input value={motivo} onChange={e => setMotivo(e.target.value)} placeholder="Motivo da troca (obrigatório)" className="w-full p-2 bg-white border border-violet-100 rounded-lg text-xs outline-none focus:border-violet-400" />
|
||||
<button disabled={busy} onClick={trocar} className="w-full py-2 rounded-lg bg-violet-600 text-white text-[11px] font-black uppercase disabled:opacity-50">Confirmar troca</button>
|
||||
</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';
|
||||
@@ -545,8 +616,9 @@ const PagamentosSecao: React.FC<{ os: any; modo: 'bancada' | 'clinica'; podeEdit
|
||||
return (
|
||||
<div>
|
||||
<p className="text-[10px] font-black text-gray-400 uppercase mb-2 flex items-center gap-1.5"><Wallet size={12} /> Valores recebidos</p>
|
||||
<div className="grid grid-cols-2 gap-2 mb-2">
|
||||
<div className="bg-green-50 rounded-xl px-3 py-2"><p className="text-[9px] font-black text-green-600 uppercase">Recebido do paciente</p><p className="text-sm font-black text-green-700">{fmtBRL(totalClinica)}</p></div>
|
||||
{/* O protético (modo bancada) NÃO vê o que o paciente paga — só o que recebe do lab. */}
|
||||
<div className={`grid ${modo === 'bancada' ? 'grid-cols-1' : 'grid-cols-2'} gap-2 mb-2`}>
|
||||
{modo === 'clinica' && <div className="bg-green-50 rounded-xl px-3 py-2"><p className="text-[9px] font-black text-green-600 uppercase">Recebido do paciente</p><p className="text-sm font-black text-green-700">{fmtBRL(totalClinica)}</p></div>}
|
||||
<div className="bg-violet-50 rounded-xl px-3 py-2"><p className="text-[9px] font-black text-violet-600 uppercase">Pago ao laboratório</p><p className="text-sm font-black text-violet-700">{fmtBRL(totalLab)}</p></div>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
@@ -562,10 +634,14 @@ const PagamentosSecao: React.FC<{ os: any; modo: 'bancada' | 'clinica'; podeEdit
|
||||
</div>
|
||||
{podeEditar && (
|
||||
<div className="flex gap-2 mt-2">
|
||||
<select value={lado} onChange={e => setLado(e.target.value as any)} className="p-2 bg-gray-50 border border-gray-100 rounded-lg text-xs font-bold outline-none focus:border-teal-400">
|
||||
<option value="clinica">Paciente</option>
|
||||
<option value="lab">Lab</option>
|
||||
</select>
|
||||
{modo === 'clinica' ? (
|
||||
<select value={lado} onChange={e => setLado(e.target.value as any)} className="p-2 bg-gray-50 border border-gray-100 rounded-lg text-xs font-bold outline-none focus:border-teal-400">
|
||||
<option value="clinica">Paciente</option>
|
||||
<option value="lab">Lab</option>
|
||||
</select>
|
||||
) : (
|
||||
<span className="p-2 bg-violet-50 rounded-lg text-xs font-black text-violet-700 uppercase flex items-center">Lab</span>
|
||||
)}
|
||||
<input value={valor} onChange={e => setValor(e.target.value)} type="number" step="0.01" placeholder="0,00" className="w-20 p-2 bg-gray-50 border border-gray-100 rounded-lg text-xs font-bold outline-none focus:border-teal-400" />
|
||||
<input value={forma} onChange={e => setForma(e.target.value)} placeholder="Forma (pix...)" className="flex-1 p-2 bg-gray-50 border border-gray-100 rounded-lg text-xs outline-none focus:border-teal-400" />
|
||||
<button disabled={busy} onClick={add} className="px-3 rounded-lg bg-teal-600 text-white text-xs font-black uppercase disabled:opacity-50 flex items-center gap-1"><Plus size={13} /></button>
|
||||
|
||||
Reference in New Issue
Block a user