feat(protese): Fase 3 — Clientes e Financeiro do laboratório (operação multi-clínica)
Provedor de Prótese, Fase 3 (doc/PROVEDOR-PROTESE-DECISAO.md): - Clientes (lab-clientes): clínicas que enviam OS, com OS totais/ativas/atrasadas/entregues, faturado e a-receber por cliente. GET /protese/lab/clientes (agrega por clinica_id no escopo do laboratório). - Financeiro do lab (lab-financeiro): KPIs faturado/recebido/a-receber + extrato de recebimentos (pagamentos lado lab) com clínica/paciente/forma/data. GET /protese/lab/pagamentos. Regra: faturado = custo das OS entregues (≠ garantia); a-receber = faturado − recebido. - Menu do laboratório ganha CLIENTES e FINANCEIRO (Sidebar + App rotas/contexto + nova LabClientesView com abas). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -3967,6 +3967,49 @@ app.get('/api/protese/bancada', authGuard, async (req, res) => {
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
// FASE 3 — Clientes do laboratório: clínicas que enviam OS, com métricas operacionais e financeiras.
|
||||
app.get('/api/protese/lab/clientes', authGuard, async (req, res) => {
|
||||
try {
|
||||
const uid = req.authUser.userId;
|
||||
const labId = await labDoProtetico(uid);
|
||||
const escopo = `(laboratorio_id = $2 OR (laboratorio_id IS NULL AND protetico_id = $1)) AND deleted_at IS NULL`;
|
||||
const { rows } = await pool.query(
|
||||
`SELECT clinica_id, (SELECT nome_fantasia FROM clinicas WHERE id = o.clinica_id) AS clinica_nome,
|
||||
COUNT(*)::int AS total,
|
||||
COUNT(*) FILTER (WHERE status NOT IN ('entregue','cancelado'))::int AS ativas,
|
||||
COUNT(*) FILTER (WHERE prazo_entrega IS NOT NULL AND prazo_entrega < CURRENT_DATE AND status NOT IN ('entregue','cancelado'))::int AS atrasadas,
|
||||
COUNT(*) FILTER (WHERE status = 'entregue')::int AS entregues,
|
||||
COALESCE(SUM(custo) FILTER (WHERE status = 'entregue' AND tipo_os <> 'garantia'), 0) AS faturado
|
||||
FROM protese_os o WHERE ${escopo}
|
||||
GROUP BY clinica_id ORDER BY total DESC`, [uid, labId]);
|
||||
const { rows: pagos } = await pool.query(
|
||||
`SELECT o.clinica_id, COALESCE(SUM(p.valor), 0) AS recebido
|
||||
FROM protese_os_pagamento p JOIN protese_os o ON o.id = p.os_id
|
||||
WHERE p.lado = 'lab' AND (o.laboratorio_id = $2 OR (o.laboratorio_id IS NULL AND o.protetico_id = $1))
|
||||
GROUP BY o.clinica_id`, [uid, labId]);
|
||||
const recPorClinica = Object.fromEntries(pagos.map(p => [p.clinica_id, parseFloat(p.recebido || 0)]));
|
||||
res.json(rows.map(r => {
|
||||
const faturado = parseFloat(r.faturado || 0), recebido = recPorClinica[r.clinica_id] || 0;
|
||||
return { ...r, faturado, recebido, a_receber: Math.max(0, faturado - recebido) };
|
||||
}));
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
// FASE 3 — Extrato de recebimentos do laboratório (pagamentos lado 'lab').
|
||||
app.get('/api/protese/lab/pagamentos', authGuard, async (req, res) => {
|
||||
try {
|
||||
const uid = req.authUser.userId;
|
||||
const labId = await labDoProtetico(uid);
|
||||
const { rows } = await pool.query(
|
||||
`SELECT p.id, p.valor, p.forma, p.data, p.recebido_nome, o.tipo, o.paciente_nome,
|
||||
(SELECT nome_fantasia FROM clinicas WHERE id = o.clinica_id) AS clinica_nome
|
||||
FROM protese_os_pagamento p JOIN protese_os o ON o.id = p.os_id
|
||||
WHERE p.lado = 'lab' AND (o.laboratorio_id = $2 OR (o.laboratorio_id IS NULL AND o.protetico_id = $1))
|
||||
ORDER BY p.data DESC, p.created_at DESC LIMIT 200`, [uid, labId]);
|
||||
res.json(rows.map(r => ({ ...r, valor: parseFloat(r.valor || 0) })));
|
||||
} 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