From f4f433b14c6dc0e62da5b41b0c209af09c4c7e1e Mon Sep 17 00:00:00 2001 From: VPS 4 Builder Date: Wed, 24 Jun 2026 02:06:29 +0200 Subject: [PATCH] =?UTF-8?q?feat(protese):=20avan=C3=A7ar=20etapas=20pela?= =?UTF-8?q?=20cl=C3=ADnica=20+=20recibo=20t=C3=A9rmico/laser=20+=20abas=20?= =?UTF-8?q?reordenadas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Aba "Linha do tempo" movida para o FINAL (entregue/cancelar continuam nela). - Etapas agora avançáveis também pela clínica (não só pelo lab): nova lista "Etapas do trabalho" na 1ª aba, com botão "Avançar" por etapa. Backend: avanço da produção passa a aceitar lab OU clínica de origem (auditado por actor). 'entregue' segue na timeline (exige foto). - Impressão por etapa em DOIS formatos: térmica (bobina 80mm) e laser (A4), com os dados da OS preenchidos no recibo (paciente, trabalho, dentista, lab, dentes, material/cor, componentes, prazo, local atual + assinaturas). Co-Authored-By: Claude Opus 4.8 --- backend/server.js | 4 +- frontend/views/ProteseView.tsx | 84 +++++++++++++++++++--------------- 2 files changed, 51 insertions(+), 37 deletions(-) diff --git a/backend/server.js b/backend/server.js index f5afe84..22edc1a 100644 --- a/backend/server.js +++ b/backend/server.js @@ -4122,7 +4122,9 @@ app.post('/api/protese/os/:id/status', authGuard, async (req, res) => { // Entregue/cancelado são da clínica; estados de produção são do laboratório. const estadosClinica = ['entregue', 'cancelado']; if (estadosClinica.includes(novo) && !isClinic) return res.status(403).json({ error: 'Só a clínica de origem marca entregue/cancelado.' }); - if (!estadosClinica.includes(novo) && !isLab) return res.status(403).json({ error: 'Só o laboratório avança a produção.' }); + // Etapas de produção (recebido→…→finalizado): lab OU clínica de origem podem avançar + // (o coordenador da clínica acompanha o trabalho). Auditado por actor no histórico. + if (!estadosClinica.includes(novo) && !isLab && !isClinic) return res.status(403).json({ error: 'Sem acesso para avançar a produção.' }); // Garantia com fotos: para ENTREGAR é obrigatório ter ao menos 1 foto do trabalho (evidência da garantia). let garantiaAte = null, finId = os.financeiro_id || null, finLabId = os.financeiro_lab_id || null; if (novo === 'entregue') { diff --git a/frontend/views/ProteseView.tsx b/frontend/views/ProteseView.tsx index b07ffcc..6fd87ec 100644 --- a/frontend/views/ProteseView.tsx +++ b/frontend/views/ProteseView.tsx @@ -22,7 +22,6 @@ const STATUS_COR: Record = { prova: 'bg-indigo-100 text-indigo-700', ajuste: 'bg-orange-100 text-orange-700', finalizado: 'bg-teal-100 text-teal-700', entregue: 'bg-green-100 text-green-700', cancelado: 'bg-red-100 text-red-600', }; -const LAB_FLUXO = ['recebido', 'producao', 'prova', 'ajuste', 'finalizado']; const Badge: React.FC<{ s: string }> = ({ s }) => ( {STATUS_LABEL[s] || s} @@ -226,25 +225,31 @@ const OSDetailModal: React.FC<{ id: string; modo: 'bancada' | 'clinica'; onClose 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) => { + // Recibo imprimível de uma etapa (comprovante que circula com o trabalho). + // formato 'termica' (bobina 80mm) | 'laser' (folha A4). Dados preenchidos da OS. + const imprimirCupom = (etapa: string, formato: 'termica' | 'laser') => { 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(`Cupom ${STATUS_LABEL[etapa] || etapa} -

ORDEM DE PRÓTESE

OS ${os.id} · ${new Date().toLocaleString('pt-BR')}
+ .lbl{color:#555;text-transform:uppercase;font-size:9px;width:40%} + .hr{border-top:1px dashed #999;margin:9px 0}${sign}${page} +

ORDEM DE PRÓTESE

OS ${os.id}
${new Date().toLocaleString('pt-BR')}
Etapa: ${STATUS_LABEL[etapa] || etapa}
+ @@ -257,11 +262,13 @@ const OSDetailModal: React.FC<{ id: string; modo: 'bancada' | 'clinica'; onClose w.document.close(); w.focus(); setTimeout(() => w.print(), 250); }; + const idxAtual = FLUXO_OS.indexOf(os.status); + const TABS = [ { id: 'etapas', label: 'Etapas' }, - { id: 'timeline', label: 'Linha do tempo' }, { id: 'itens', label: 'Itens & Valores' }, { id: 'monitor', label: 'Monitoramento' }, + { id: 'timeline', label: 'Linha do tempo' }, ] as const; return ( @@ -305,13 +312,29 @@ const OSDetailModal: React.FC<{ id: string; modo: 'bancada' | 'clinica'; onClose {modo === 'clinica' && Number(os.valor) > 0 &&
Valor cobrado{fmtBRL(os.valor)}
} {os.observacoes &&

{os.observacoes}

} + {/* Etapas do trabalho: avançar + imprimir recibo (térmica/laser) por etapa */}
-

Imprimir cupom de etapa

-
- {FLUXO_OS.map(s => ( - - ))} +

Etapas do trabalho

+
+ {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); + return ( +
+ {done ? : atual ?
: } + {STATUS_LABEL[s]} + {ev ? dataBR(ev.created_at) : ''} +
+ {podeAvancar && } + {!finalizado && i === idxAtual + 1 && s === 'entregue' && aba Linha do tempo} + + +
+ ); + })}
+

= térmica 80mm · A4 = laser

)} @@ -330,24 +353,13 @@ const OSDetailModal: React.FC<{ id: string; modo: 'bancada' | 'clinica'; onClose
))}
- {!finalizado && ( + {!finalizado && modo === 'clinica' && (
- {modo === 'bancada' ? ( -
- {LAB_FLUXO.map(s => ( - - ))} -
- ) : ( - <> - {!os.fotos?.length &&

Anexe ao menos 1 foto (aba Itens) antes de entregar

} -
- - -
- - )} + {!os.fotos?.length &&

Anexe ao menos 1 foto (aba Itens) antes de entregar

} +
+ + +
)} {naGarantia && modo === 'clinica' && (
Paciente${os.paciente_nome || '—'}
Trabalho${os.tipo || '—'}
Dentista${os.dentista_nome || '—'}
Laboratório${os.protetico_nome || '—'}
Dentes${os.dentes || '—'}
Material / Cor${os.material || '—'} / ${os.cor || '—'}
Componentes${comps}