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:
+69
-3
@@ -3960,7 +3960,8 @@ app.get('/api/protese/bancada', authGuard, async (req, res) => {
|
||||
`SELECT *, (prazo_entrega IS NOT NULL AND prazo_entrega < CURRENT_DATE AND status NOT IN ('entregue','cancelado')) AS atrasada
|
||||
FROM protese_os WHERE ${where}
|
||||
ORDER BY status='entregue', status='cancelado', prazo_entrega ASC NULLS LAST, created_at DESC LIMIT 300`, vals);
|
||||
res.json(rows.map(r => ({ ...r, valor: parseFloat(r.valor || 0), custo: parseFloat(r.custo || 0), atrasada: !!r.atrasada })));
|
||||
// Bancada é a fila do laboratório: ele vê o próprio custo, não o valor cobrado ao paciente.
|
||||
res.json(rows.map(r => ({ ...r, valor: 0, custo: parseFloat(r.custo || 0), atrasada: !!r.atrasada })));
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
@@ -3977,13 +3978,16 @@ app.get('/api/protese/os/:id', authGuard, async (req, res) => {
|
||||
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]);
|
||||
// Privacidade: o protético (lab que NÃO é da clínica) não vê o que o paciente paga/deve.
|
||||
const soLab = isLab && !isClinic;
|
||||
const pagsVisiveis = soLab ? pagamentos.filter(p => p.lado !== 'clinica') : pagamentos;
|
||||
res.json({
|
||||
...os, valor: parseFloat(os.valor || 0), custo: parseFloat(os.custo || 0),
|
||||
...os, valor: soLab ? 0 : 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) })),
|
||||
pagamentos: pagsVisiveis.map(p => ({ ...p, valor: parseFloat(p.valor || 0) })),
|
||||
custodia,
|
||||
});
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
@@ -4107,6 +4111,68 @@ app.post('/api/protese/os/:id/custodia', authGuard, async (req, res) => {
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
// Editar campos do trabalho (cor, material, dentes, observações) — lab ou clínica. Auditado.
|
||||
app.patch('/api/protese/os/:id', 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 });
|
||||
if (acc.os.status === 'entregue' || acc.os.status === 'cancelado') return res.status(409).json({ error: 'OS finalizada não pode ser editada.' });
|
||||
const b = req.body || {};
|
||||
const muda = [];
|
||||
for (const campo of ['cor', 'material', 'dentes', 'observacoes']) {
|
||||
if (b[campo] !== undefined) {
|
||||
const novo = b[campo] === null || b[campo] === '' ? null : String(b[campo]);
|
||||
if ((acc.os[campo] || null) !== novo) { await pool.query(`UPDATE protese_os SET ${campo}=$1 WHERE id=$2`, [novo, acc.os.id]); muda.push(`${campo}: ${acc.os[campo] || '—'} → ${novo || '—'}`); }
|
||||
}
|
||||
}
|
||||
if (muda.length) await logEventoOS(acc.os, uid, `Trabalho editado (${muda.join('; ')})`, 'edicao');
|
||||
res.json({ success: true });
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
// Ajuste do valor cobrado do paciente (acréscimo/redução) — SOMENTE clínica. Auditado.
|
||||
app.post('/api/protese/os/:id/ajuste-valor', 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 });
|
||||
if (!acc.isClinic) return res.status(403).json({ error: 'Apenas a clínica ajusta o valor do paciente.' });
|
||||
const delta = Number(req.body?.delta);
|
||||
if (!delta || isNaN(delta)) return res.status(400).json({ error: 'Informe um valor de ajuste (+ ou −).' });
|
||||
const atual = parseFloat(acc.os.valor || 0);
|
||||
const novo = Math.max(0, atual + delta);
|
||||
await pool.query('UPDATE protese_os SET valor=$1 WHERE id=$2', [novo, acc.os.id]);
|
||||
const sinal = delta >= 0 ? '+' : '−';
|
||||
const obs = req.body?.observacao ? ` · ${String(req.body.observacao).slice(0, 200)}` : '';
|
||||
await logEventoOS(acc.os, uid, `Valor ao paciente ${sinal}${Math.abs(delta).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })}${obs} (novo total ${novo.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })})`, 'valor');
|
||||
res.json({ success: true, valor: novo });
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
// Trocar o trabalho por outro (produto do catálogo do lab OU tipo livre) + motivo. Auditado.
|
||||
app.post('/api/protese/os/:id/trocar', 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 });
|
||||
if (acc.os.status === 'entregue' || acc.os.status === 'cancelado') return res.status(409).json({ error: 'OS finalizada não pode ser trocada.' });
|
||||
const b = req.body || {};
|
||||
if (!b.motivo || !String(b.motivo).trim()) return res.status(400).json({ error: 'Informe o motivo da troca.' });
|
||||
const de = acc.os.tipo;
|
||||
let novoTipo = b.tipo ? String(b.tipo).trim() : null, custo = parseFloat(acc.os.custo || 0), garantia = acc.os.garantia_meses, produtoId = acc.os.produto_id;
|
||||
if (b.produto_id) {
|
||||
const { rows: pr } = await pool.query('SELECT id, nome, preco, garantia_meses FROM protese_produtos WHERE id=$1 AND protetico_id=$2 AND deleted_at IS NULL', [b.produto_id, acc.os.protetico_id]);
|
||||
if (!pr.length) return res.status(404).json({ error: 'Produto não pertence a este laboratório.' });
|
||||
produtoId = pr[0].id; novoTipo = pr[0].nome; custo = parseFloat(pr[0].preco || 0); garantia = Number(pr[0].garantia_meses) || 12;
|
||||
}
|
||||
if (!novoTipo) return res.status(400).json({ error: 'Selecione o novo trabalho.' });
|
||||
await pool.query('UPDATE protese_os SET tipo=$1, produto_id=$2, custo=$3, garantia_meses=$4 WHERE id=$5', [novoTipo, produtoId, custo, garantia, acc.os.id]);
|
||||
await logEventoOS(acc.os, uid, `Trabalho trocado: ${de} → ${novoTipo} · Motivo: ${String(b.motivo).trim()}`, 'troca');
|
||||
res.json({ success: true, tipo: novoTipo });
|
||||
} 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;
|
||||
|
||||
Reference in New Issue
Block a user