feat(protese): Fase 2 backend — Laboratório como workspace de 1ª classe
- Workspace do protético passa a ser tipo='laboratorio' (re-tag dos existentes + novos nascem assim no /api/register). Área de gestão própria, como a clínica. - Roteamento por laboratorio_id: OS nascem com o lab (POST /os e GTO→OS); a Bancada filtra por laboratorio_id com fallback por protetico_id (OS legadas). Helper labDoProtetico(). Backfill aditivo + índice. - Receita espelhada (2.1): a entrega lança RECEITA no workspace do lab (clinica_id=laboratorio_id, valor=os.custo), espelho da DESPESA da clínica — idempotente (financeiro_lab_id), isolado em try/catch. fix: corrige coluna `paciente_nome`→`pacientenome` no INSERT da DESPESA de prótese (bug pré-existente da Fase 1, em produção): a entrega de OS com custo>0 abortava com "column paciente_nome does not exist". A mesma correção vale para a RECEITA nova. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+59
-13
@@ -634,9 +634,11 @@ app.post('/api/register', async (req, res) => {
|
||||
: userRole === 'protetico' ? `LABORATÓRIO DE ${nome.trim()}`
|
||||
: nome.trim();
|
||||
const wsNome = (workspaceNome && workspaceNome.trim() ? workspaceNome.trim() : defaultNome).toUpperCase();
|
||||
// Fase 2: o workspace do protético é um LABORATÓRIO (área de gestão de 1ª classe).
|
||||
const wsTipo = userRole === 'protetico' ? 'laboratorio' : 'pessoal';
|
||||
await pool.query(
|
||||
`INSERT INTO clinicas (id, nome_fantasia, tipo, owner_id) VALUES ($1, $2, 'pessoal', $3)`,
|
||||
[wsId, wsNome, userId]
|
||||
`INSERT INTO clinicas (id, nome_fantasia, tipo, owner_id) VALUES ($1, $2, $3, $4)`,
|
||||
[wsId, wsNome, wsTipo, userId]
|
||||
);
|
||||
await pool.query(
|
||||
'INSERT INTO vinculos (id, usuario_id, clinica_id, role) VALUES ($1, $2, $3, $4)',
|
||||
@@ -3782,6 +3784,15 @@ app.delete('/api/procedimentos-realizados/:id', tenantGuard, async (req, res) =>
|
||||
const PROTESE_STATUS = ['solicitado', 'recebido', 'producao', 'prova', 'ajuste', 'finalizado', 'entregue', 'cancelado'];
|
||||
|
||||
// Verifica o acesso de um usuário a uma OS: laboratório (destinatário) ou clínica de origem (com vínculo).
|
||||
// O workspace 'laboratorio' do protético (1 por protético nesta fase). Usado para escopar a
|
||||
// bancada e as OS por laboratório (empresa), além do protetico_id (pessoa) que segue como fallback.
|
||||
async function labDoProtetico(proteticoId) {
|
||||
if (!proteticoId) return null;
|
||||
const { rows } = await pool.query(
|
||||
"SELECT id FROM clinicas WHERE owner_id=$1 AND tipo='laboratorio' LIMIT 1", [proteticoId]);
|
||||
return rows[0]?.id || null;
|
||||
}
|
||||
|
||||
async function proteseOsAccesso(os, uid) {
|
||||
if (!os) return { isLab: false, isClinic: false };
|
||||
const isLab = !!uid && os.protetico_id === uid;
|
||||
@@ -3904,10 +3915,11 @@ app.post('/api/protese/os', tenantGuard, async (req, res) => {
|
||||
if (ehGarantia) custo = 0;
|
||||
const id = `pos_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
|
||||
const uid = req.authUser?.userId;
|
||||
const labId = await labDoProtetico(b.protetico_id); // Fase 2: escopo por laboratório (empresa)
|
||||
await pool.query(
|
||||
`INSERT INTO protese_os (id, clinica_id, protetico_id, protetico_nome, paciente_id, paciente_nome, dentista_id, dentista_nome, procedimento_id, agendamento_id, tipo, produto_id, tipo_os, origem_os_id, garantia_meses, material, cor, dentes, observacoes, prazo_entrega, valor, custo, status, created_by)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,'solicitado',$23)`,
|
||||
[id, req.clinicaId, b.protetico_id, prot[0].nome || null, b.paciente_id || null, b.paciente_nome || null,
|
||||
`INSERT INTO protese_os (id, clinica_id, protetico_id, laboratorio_id, protetico_nome, paciente_id, paciente_nome, dentista_id, dentista_nome, procedimento_id, agendamento_id, tipo, produto_id, tipo_os, origem_os_id, garantia_meses, material, cor, dentes, observacoes, prazo_entrega, valor, custo, status, created_by)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,'solicitado',$24)`,
|
||||
[id, req.clinicaId, b.protetico_id, labId, prot[0].nome || null, b.paciente_id || null, b.paciente_nome || null,
|
||||
b.dentista_id || null, b.dentista_nome || null, b.procedimento_id || null, b.agendamento_id || null,
|
||||
tipo, produtoId, ehGarantia ? 'garantia' : 'nova', ehGarantia ? b.origem_os_id : null, garantiaMeses,
|
||||
b.material || null, b.cor || null, b.dentes || null, b.observacoes || null,
|
||||
@@ -3939,8 +3951,10 @@ app.get('/api/protese/os', tenantGuard, async (req, res) => {
|
||||
app.get('/api/protese/bancada', authGuard, async (req, res) => {
|
||||
try {
|
||||
const uid = req.authUser.userId;
|
||||
const vals = [uid];
|
||||
let where = 'protetico_id=$1 AND deleted_at IS NULL';
|
||||
const labId = await labDoProtetico(uid); // Fase 2: a bancada é do LABORATÓRIO (empresa)
|
||||
const vals = [uid, labId];
|
||||
// Roteia por laboratorio_id; fallback por pessoa cobre OS legadas sem laboratorio_id.
|
||||
let where = `( (laboratorio_id IS NOT NULL AND laboratorio_id=$2) OR (laboratorio_id IS NULL AND protetico_id=$1) ) AND deleted_at IS NULL`;
|
||||
if (req.query.status && PROTESE_STATUS.includes(req.query.status)) { vals.push(req.query.status); where += ` AND status=$${vals.length}`; }
|
||||
const { rows } = await pool.query(
|
||||
`SELECT *, (prazo_entrega IS NOT NULL AND prazo_entrega < CURRENT_DATE AND status NOT IN ('entregue','cancelado')) AS atrasada
|
||||
@@ -3985,7 +3999,7 @@ app.post('/api/protese/os/:id/status', authGuard, async (req, res) => {
|
||||
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.' });
|
||||
// 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;
|
||||
let garantiaAte = null, finId = os.financeiro_id || null, finLabId = os.financeiro_lab_id || null;
|
||||
if (novo === 'entregue') {
|
||||
const { rows: fc } = await pool.query('SELECT COUNT(*)::int AS n FROM protese_os_foto WHERE os_id=$1', [os.id]);
|
||||
if (!fc[0].n) return res.status(422).json({ error: 'Anexe ao menos 1 foto do trabalho antes de entregar (evidência da garantia).' });
|
||||
@@ -3996,13 +4010,27 @@ app.post('/api/protese/os/:id/status', authGuard, async (req, res) => {
|
||||
if (!finId && Number(os.custo) > 0 && os.tipo_os !== 'garantia') {
|
||||
finId = `fin_pos_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
||||
await pool.query(
|
||||
`INSERT INTO financeiro (id, clinica_id, descricao, valor, tipo, status, datavencimento, data_competencia, profissional_id, paciente_id, paciente_nome, origem, centro_custo, fornecedor, sem_comissao)
|
||||
`INSERT INTO financeiro (id, clinica_id, descricao, valor, tipo, status, datavencimento, data_competencia, profissional_id, paciente_id, pacientenome, origem, centro_custo, fornecedor, sem_comissao)
|
||||
VALUES ($1,$2,$3,$4,'DESPESA','Pendente',CURRENT_DATE,CURRENT_DATE,$5,$6,$7,'protese','LABORATÓRIO',$8,true)`,
|
||||
[finId, os.clinica_id, `PRÓTESE: ${os.tipo} (lab ${os.protetico_nome || ''})`.trim(), Number(os.custo),
|
||||
os.protetico_id, os.paciente_id || null, os.paciente_nome || null, os.protetico_nome || null]);
|
||||
}
|
||||
// Fase 2.1: espelho — RECEITA no workspace do laboratório (clinica_id = laboratorio_id).
|
||||
// Mesmo valor/condição da DESPESA; isolado para nunca derrubar a transição de status.
|
||||
try {
|
||||
const labId = os.laboratorio_id || await labDoProtetico(os.protetico_id);
|
||||
if (!finLabId && labId && Number(os.custo) > 0 && os.tipo_os !== 'garantia') {
|
||||
const { rows: cl } = await pool.query('SELECT nome_fantasia FROM clinicas WHERE id=$1', [os.clinica_id]);
|
||||
finLabId = `fin_lab_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
||||
await pool.query(
|
||||
`INSERT INTO financeiro (id, clinica_id, descricao, valor, tipo, status, datavencimento, data_competencia, paciente_id, pacientenome, origem, centro_custo, sem_comissao)
|
||||
VALUES ($1,$2,$3,$4,'RECEITA','Pendente',CURRENT_DATE,CURRENT_DATE,$5,$6,'protese','PRÓTESE',true)`,
|
||||
[finLabId, labId, `PRÓTESE: ${os.tipo} (cliente ${cl[0]?.nome_fantasia || ''})`.trim(), Number(os.custo),
|
||||
os.paciente_id || null, os.paciente_nome || null]);
|
||||
}
|
||||
} catch (e) { console.error('[protese entrega→receita lab]', e.message); }
|
||||
}
|
||||
await pool.query('UPDATE protese_os SET status=$1, updated_at=NOW(), garantia_ate=COALESCE($2,garantia_ate), financeiro_id=COALESCE($3,financeiro_id) WHERE id=$4', [novo, garantiaAte, finId, os.id]);
|
||||
await pool.query('UPDATE protese_os SET status=$1, updated_at=NOW(), garantia_ate=COALESCE($2,garantia_ate), financeiro_id=COALESCE($3,financeiro_id), financeiro_lab_id=COALESCE($4,financeiro_lab_id) WHERE id=$5', [novo, garantiaAte, finId, finLabId, os.id]);
|
||||
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, novo, req.body?.observacao || null, uid, await actorNome(uid)]);
|
||||
@@ -4476,10 +4504,11 @@ app.put('/api/gto-builder/:id/finalizar', authGuard, async (req, res) => {
|
||||
if (!pr.length) continue;
|
||||
const { rows: prot } = await pool.query("SELECT nome FROM usuarios WHERE id=$1 AND role='protetico'", [it.protetico_id]);
|
||||
const osId = `pos_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
|
||||
const labId = await labDoProtetico(it.protetico_id); // Fase 2: escopo por laboratório
|
||||
await pool.query(
|
||||
`INSERT INTO protese_os (id, clinica_id, protetico_id, protetico_nome, paciente_id, paciente_nome, dentista_id, procedimento_id, tipo, produto_id, tipo_os, garantia_meses, dentes, observacoes, valor, custo, status, created_by, gto_item_id)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,'nova',$11,$12,$13,$14,$15,'solicitado',$16,$17)`,
|
||||
[osId, b.clinica_id, it.protetico_id, prot[0]?.nome || null, b.pacienteid, b.paciente_nome,
|
||||
`INSERT INTO protese_os (id, clinica_id, protetico_id, laboratorio_id, protetico_nome, paciente_id, paciente_nome, dentista_id, procedimento_id, tipo, produto_id, tipo_os, garantia_meses, dentes, observacoes, valor, custo, status, created_by, gto_item_id)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,'nova',$12,$13,$14,$15,$16,'solicitado',$17,$18)`,
|
||||
[osId, b.clinica_id, it.protetico_id, labId, prot[0]?.nome || null, b.pacienteid, b.paciente_nome,
|
||||
b.dentistaid, it.procedimentoid || null, pr[0].nome, pr[0].id, Number(pr[0].garantia_meses) || 12,
|
||||
it.dente || null, it.observacao || null, Number(it.valortotal) || 0, parseFloat(pr[0].preco || 0), uid, it.id]);
|
||||
await pool.query(
|
||||
@@ -5686,6 +5715,23 @@ async function runMigrations() {
|
||||
// Especialidade de prótese vinha marcada como 'odontologia' (legado) → re-tag para 'protese'
|
||||
// permite o front detectar a especialidade e puxar o catálogo do protético. Idempotente.
|
||||
`UPDATE especialidades SET area='protese' WHERE area IS DISTINCT FROM 'protese' AND (nome ILIKE '%protese%' OR nome ILIKE '%prótese%')`,
|
||||
// ── FASE 2.0: Laboratório como workspace de 1ª classe ──────────────────────
|
||||
// RECEITA espelhada no lab (preenchida na entrega; idempotência) e catálogo por lab.
|
||||
`ALTER TABLE protese_os ADD COLUMN IF NOT EXISTS financeiro_lab_id TEXT`,
|
||||
`ALTER TABLE protese_produtos ADD COLUMN IF NOT EXISTS laboratorio_id TEXT`,
|
||||
// (protese_os.laboratorio_id já existe — coluna reservada na criação da tabela.)
|
||||
// Re-tag: o workspace pessoal do protético passa a ser 'laboratorio'.
|
||||
`UPDATE clinicas SET tipo='laboratorio'
|
||||
WHERE tipo='pessoal' AND owner_id IN (SELECT id FROM usuarios WHERE role='protetico')`,
|
||||
// Backfill: cada OS herda o laboratorio do workspace do seu protético.
|
||||
`UPDATE protese_os o SET laboratorio_id = c.id
|
||||
FROM clinicas c
|
||||
WHERE o.laboratorio_id IS NULL AND c.tipo='laboratorio' AND c.owner_id = o.protetico_id`,
|
||||
// Backfill: catálogo passa a ser do laboratório (mantém protetico_id p/ compat).
|
||||
`UPDATE protese_produtos p SET laboratorio_id = c.id
|
||||
FROM clinicas c
|
||||
WHERE p.laboratorio_id IS NULL AND c.tipo='laboratorio' AND c.owner_id = p.protetico_id`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_protese_os_labid ON protese_os (laboratorio_id, status)`,
|
||||
`CREATE TABLE IF NOT EXISTS protese_produtos (
|
||||
id TEXT PRIMARY KEY,
|
||||
protetico_id TEXT NOT NULL, -- dono (usuário protético)
|
||||
|
||||
Reference in New Issue
Block a user