feat(procedimentos): atende/não-atende + motivo + como falar (backend)

- Tabela agenda_procedimentos (clinica_id, nome, atende, motivo, como_falar).
- agenda-config GET/PUT /clinica/:id/procedimentos-atende (GET semeia do catálogo
  procedimentos — incl. globais clinica_id NULL; PUT só dono).
- /contexto (bridge) devolve procedimentos_nao_atende (nome+motivo+como_falar).

Verificado E2E: seed 4; PUT marca não-atende; /contexto entrega o script.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
VPS 4 Builder
2026-07-08 08:34:05 +02:00
parent 1d0de944e1
commit b42257f584
3 changed files with 66 additions and 0 deletions
+7
View File
@@ -451,6 +451,12 @@ export function createAgendaBridge(pool) {
const { rows } = await pool.query(`SELECT texto FROM agenda_situacoes WHERE clinica_id = $1 AND ativo = 1 ORDER BY ordem, created_at`, [clinicaId]);
situacoes = rows.map((r) => r.texto);
} catch { /* tabela pode não existir ainda */ }
// Procedimentos que a clínica NÃO atende → a Secretária responde com o script.
let procedimentos_nao_atende = [];
try {
const { rows } = await pool.query(`SELECT nome, motivo, como_falar FROM agenda_procedimentos WHERE clinica_id = $1 AND atende = 0 ORDER BY ordem, nome`, [clinicaId]);
procedimentos_nao_atende = rows.map((r) => ({ nome: r.nome, motivo: r.motivo || null, como_falar: r.como_falar || null }));
} catch { /* tabela pode não existir ainda */ }
// Cautelas do checklist do dono (declaração × dados reais) → a IA não chuta.
let cautelas = [];
try {
@@ -466,6 +472,7 @@ export function createAgendaBridge(pool) {
especialidades: Array.isArray(d.especialidades) ? d.especialidades : (d.especialidade ? [d.especialidade] : []),
})),
situacoes,
procedimentos_nao_atende,
cautelas,
});
} catch (e) { res.status(500).json({ error: e.message }); }
+44
View File
@@ -243,6 +243,50 @@ export function createAgendaConfig(pool) {
finally { client.release(); }
});
// ── Procedimentos que a clínica atende / não atende (dono) ────────────────────
router.get('/clinica/:clinicaId/procedimentos-atende', async (req, res) => {
const clinicaId = req.params.clinicaId;
try {
let { rows } = await pool.query(
`SELECT id, nome, atende, motivo, como_falar, ordem FROM agenda_procedimentos WHERE clinica_id = $1 ORDER BY ordem, nome`, [clinicaId]);
// Primeira vez: semeia a lista a partir do catálogo de procedimentos da clínica (atende=1).
if (!rows.length) {
const { rows: cat } = await pool.query(
`SELECT DISTINCT nome FROM procedimentos WHERE (clinica_id = $1 OR clinica_id IS NULL) AND ativo IS DISTINCT FROM false AND nome IS NOT NULL AND nome <> '' ORDER BY nome`, [clinicaId]);
const client = await pool.connect();
try {
await client.query('BEGIN');
let i = 0;
for (const c of cat) await client.query(`INSERT INTO agenda_procedimentos (id, clinica_id, nome, atende, ordem) VALUES ($1,$2,$3,1,$4)`, [uid('prc'), clinicaId, c.nome, i++]);
await client.query('COMMIT');
} catch { try { await client.query('ROLLBACK'); } catch {} } finally { client.release(); }
({ rows } = await pool.query(`SELECT id, nome, atende, motivo, como_falar, ordem FROM agenda_procedimentos WHERE clinica_id = $1 ORDER BY ordem, nome`, [clinicaId]));
}
res.json({ procedimentos: rows });
} catch (e) { res.status(500).json({ error: e.message }); }
});
// Substitui toda a lista. Body: { procedimentos: [{nome, atende, motivo, como_falar}] }
router.put('/clinica/:clinicaId/procedimentos-atende', async (req, res) => {
const clinicaId = req.params.clinicaId;
if (!(await isDono(pool, clinicaId, req.nwUser.userId))) return res.status(403).json({ error: 'Apenas o dono pode configurar os procedimentos.' });
const linhas = Array.isArray(req.body?.procedimentos) ? req.body.procedimentos : [];
const client = await pool.connect();
try {
await client.query('BEGIN');
await client.query('DELETE FROM agenda_procedimentos WHERE clinica_id = $1', [clinicaId]);
let i = 0;
for (const p of linhas) {
const nome = String(p?.nome || '').trim(); if (!nome) continue;
const atende = (p?.atende === false || p?.atende === 0) ? 0 : 1;
await client.query(
`INSERT INTO agenda_procedimentos (id, clinica_id, nome, atende, motivo, como_falar, ordem) VALUES ($1,$2,$3,$4,$5,$6,$7)`,
[uid('prc'), clinicaId, nome, atende, atende ? null : (String(p?.motivo || '').trim() || null), atende ? null : (String(p?.como_falar || '').trim() || null), i++]);
}
await client.query('COMMIT'); res.json({ ok: true });
} catch (e) { try { await client.query('ROLLBACK'); } catch {} res.status(500).json({ error: e.message }); }
finally { client.release(); }
});
// ── Checklist do DONO + pendências (só o dono do workspace) ───────────────────
// Conta dentistas/pacientes reais da clínica p/ conferir contra as respostas.
async function contagens(clinicaId) {
+15
View File
@@ -98,6 +98,21 @@ export async function ensureAgendaSchema(pool) {
)`);
await pool.query(`CREATE INDEX IF NOT EXISTS idx_agenda_situacoes_clinica ON agenda_situacoes (clinica_id)`);
// ── agenda_procedimentos — o que a clínica ATENDE ou não. Desmarcado (atende=0)
// = não atende: guarda o motivo e o "como_falar" (script p/ a Secretária responder).
await pool.query(`
CREATE TABLE IF NOT EXISTS agenda_procedimentos (
id varchar PRIMARY KEY,
clinica_id varchar NOT NULL,
nome text NOT NULL,
atende smallint DEFAULT 1,
motivo text,
como_falar text,
ordem integer DEFAULT 0,
created_at timestamptz DEFAULT now()
)`);
await pool.query(`CREATE INDEX IF NOT EXISTS idx_agenda_procedimentos_clinica ON agenda_procedimentos (clinica_id)`);
// ── agenda_checklist — respostas Sim/Não do DONO sobre a clínica (ver
// checklist-defs.js). Conferidas contra o banco → geram pendências de config
// e deixam a Secretária cautelosa. resposta: 1=sim, 0=não, NULL=não respondido.