feat(protese): Modo 1 fatia 1b — ações do protético pelo link (sem conta)
Pelo link /p/TOKEN o protético (sem conta) passa a ATUAR na OS, tudo auditado como "<nome> (link)": - POST /api/p/:token/custodia — confirmar recebimento / devolução (clínica↔lab) - POST /api/p/:token/status — avançar a produção (recebido…finalizado); entregue/cancelado seguem exclusivos da clínica (400 pelo link) - POST /api/p/:token/foto — anexar foto do trabalho Página pública ganha a seção "Atualizar status" (custódia/etapa/foto) enquanto a OS não está finalizada; header alterna Acompanhamento/Somente leitura. Validado em DEV: recebimento, avanço de etapa, foto; entregue pelo link → 400. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+65
-1
@@ -4202,11 +4202,75 @@ app.get('/api/p/:token', async (req, res) => {
|
||||
eventos: ev.rows,
|
||||
fotos: ft.rows.map(f => ({ ...f, url: `/api/protese/os/fotos/${f.id}/raw?t=${signMedia(f.id)}` })),
|
||||
componentes: cm.rows, ocorrencias: oc.rows, custodia: cu.rows,
|
||||
somente_leitura: true,
|
||||
finalizada: os.status === 'entregue' || os.status === 'cancelado',
|
||||
});
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
// Modo 1 (1b) — ações do protético PELO LINK (sem conta). Auditadas como "<nome> (link)".
|
||||
async function resolveLinkToken(token) {
|
||||
const { rows } = await pool.query("SELECT * FROM protese_os_link WHERE token=$1 AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at > NOW())", [token]);
|
||||
if (!rows.length) return null;
|
||||
const { rows: os } = await pool.query("SELECT * FROM protese_os WHERE id=$1 AND deleted_at IS NULL", [rows[0].os_id]);
|
||||
if (!os.length) return null;
|
||||
return { link: rows[0], os: os[0] };
|
||||
}
|
||||
async function logEventoLink(os, link, status, observacao) {
|
||||
const nome = `${await actorNome(link.protetico_id)} (link)`;
|
||||
await pool.query("INSERT INTO protese_os_evento (id, os_id, clinica_id, status, observacao, actor_id, actor_nome) VALUES ($1,$2,$3,$4,$5,$6,$7)",
|
||||
[`pev_${Date.now()}_${Math.random().toString(36).slice(2, 5)}`, os.id, os.clinica_id, status, observacao, link.protetico_id, nome]);
|
||||
return nome;
|
||||
}
|
||||
|
||||
app.post('/api/p/:token/custodia', async (req, res) => {
|
||||
try {
|
||||
const ctx = await resolveLinkToken(req.params.token);
|
||||
if (!ctx) return res.status(404).json({ error: 'Link inválido ou expirado.' });
|
||||
const { os, link } = ctx;
|
||||
const para = ['clinica', 'laboratorio'].includes(req.body?.para_local) ? req.body.para_local : null;
|
||||
if (!para) return res.status(400).json({ error: 'para_local inválido.' });
|
||||
const de = os.local_atual || 'clinica';
|
||||
if (de === para) return res.status(409).json({ error: `Já está ${para === 'laboratorio' ? 'com o laboratório' : 'na clínica'}.` });
|
||||
const nome = `${await actorNome(link.protetico_id)} (link)`;
|
||||
await pool.query("INSERT INTO protese_os_custodia (id, os_id, clinica_id, de_local, para_local, etapa, actor_id, actor_nome) VALUES ($1,$2,$3,$4,$5,$6,$7,$8)",
|
||||
[`pcus_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`, os.id, os.clinica_id, de, para, os.status, link.protetico_id, nome]);
|
||||
await pool.query("UPDATE protese_os SET local_atual=$1 WHERE id=$2", [para, os.id]);
|
||||
await logEventoLink(os, link, 'custodia', para === 'laboratorio' ? 'Protético recebeu o trabalho (via link)' : 'Trabalho devolvido à clínica (via link)');
|
||||
res.json({ success: true, local_atual: para });
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
app.post('/api/p/:token/status', async (req, res) => {
|
||||
try {
|
||||
const ctx = await resolveLinkToken(req.params.token);
|
||||
if (!ctx) return res.status(404).json({ error: 'Link inválido ou expirado.' });
|
||||
const { os, link } = ctx;
|
||||
const PROD = ['recebido', 'producao', 'prova', 'ajuste', 'finalizado'];
|
||||
if (!PROD.includes(req.body?.status)) return res.status(400).json({ error: 'Etapa inválida (somente produção pelo link).' });
|
||||
if (os.status === 'entregue' || os.status === 'cancelado') return res.status(409).json({ error: `OS já está "${os.status}".` });
|
||||
await pool.query('UPDATE protese_os SET status=$1, updated_at=NOW() WHERE id=$2', [req.body.status, os.id]);
|
||||
await logEventoLink(os, link, req.body.status, null);
|
||||
res.json({ success: true, status: req.body.status });
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
app.post('/api/p/:token/foto', ortoUpload.single('foto'), async (req, res) => {
|
||||
try {
|
||||
if (!req.file) return res.status(400).json({ error: 'foto obrigatória.' });
|
||||
const ctx = await resolveLinkToken(req.params.token);
|
||||
if (!ctx) return res.status(404).json({ error: 'Link inválido ou expirado.' });
|
||||
const { os, link } = ctx;
|
||||
const ext = ((req.file.originalname.split('.').pop() || 'jpg').toLowerCase().replace(/[^a-z0-9]/g, '')) || 'jpg';
|
||||
const rel = path.join('proteses', os.id, `${Date.now()}.${ext}`);
|
||||
await fs.promises.mkdir(path.join(MEDIA_ROOT, 'proteses', os.id), { recursive: true });
|
||||
await fs.promises.writeFile(path.join(MEDIA_ROOT, rel), req.file.buffer);
|
||||
const fid = `pof_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
||||
const nome = `${await actorNome(link.protetico_id)} (link)`;
|
||||
await pool.query("INSERT INTO protese_os_foto (id, os_id, clinica_id, filename, mimetype, original_name, uploaded_by, uploaded_nome) VALUES ($1,$2,$3,$4,$5,$6,$7,$8)",
|
||||
[fid, os.id, os.clinica_id, rel, req.file.mimetype, req.file.originalname, link.protetico_id, nome]);
|
||||
await logEventoLink(os, link, 'foto', 'Foto anexada (via link)');
|
||||
res.json({ success: true, id: fid });
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
// Detalhe + histórico de eventos + fotos.
|
||||
app.get('/api/protese/os/:id', authGuard, async (req, res) => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user