Files
scoreodonto.com/backend/newwhats/agenda-config.js
T
VPS 4 Builder 40bed96515 feat(newwhats): agenda inteligente — especialidades, situações, dúvidas, checklist do dono e pendências
Satélite da Secretária IA:
- /contexto (dentistas+especialidades+situações+cautelas) e /duvida (encaminhar
  ao humano sem agendar); agenda_pedidos ganha tipo=duvida + resumo.
- Situações da clínica: tabela agenda_situacoes + config (dono) + aba na UI.
- Checklist do dono (checklist-defs.js + agenda_checklist): perguntas Sim/Não
  conferidas contra o banco (dentistas/pacientes) → pendências de config +
  cautelas p/ a IA não chutar.
- Área de PENDÊNCIAS DO DONO (PendenciasDono.tsx): engloba config + fila
  operacional (horários/dúvidas); botão+badge no /wa-secretaria (só o dono).
- Fila humana (PedidosPendentes): cards de dúvida + resolver; /resolver.
- Botão DESLIGAR global da Secretária no ThinNav (secPowerApi, visível a todos).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 15:40:51 +02:00

405 lines
23 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Config de horários/feriados — API para o FRONTEND do scoreodonto (dono/dentista).
// Auth: JWT do scoreodonto (Authorization: Bearer). Ownership:
// - Horário da CLÍNICA + feriados → só o DONO (clinicas.owner_id / vínculo dono).
// - Horário do DENTISTA → o próprio dentista (dentistas.usuario_id) OU o dono.
//
// Montado em /api/nw/agenda-config pelo registerNewwhats.
import express from 'express';
import jwt from 'jsonwebtoken';
import { feriadosNacionais } from './feriados.js';
import { CHECKLIST_QUESTOES, pendenciasDeConfig } from './checklist-defs.js';
function authFromReq(req) {
try {
const tk = (req.headers['authorization'] || '').replace(/^Bearer\s+/i, '').trim();
if (!tk) return null;
const p = jwt.verify(tk, process.env.JWT_SECRET);
return { userId: p.userId || null, email: (p.email || '').toLowerCase() || null };
} catch { return null; }
}
// É dono do workspace (clínica)? owner_id direto OU vínculo dono/admin.
async function isDono(pool, clinicaId, userId) {
if (!clinicaId || !userId) return false;
try {
const { rows: cl } = await pool.query('SELECT owner_id FROM clinicas WHERE id = $1', [clinicaId]);
if (cl[0]?.owner_id && cl[0].owner_id === userId) return true;
const { rows: v } = await pool.query(
`SELECT 1 FROM vinculos WHERE clinica_id = $1 AND usuario_id = $2
AND role IN ('donoclinica','donoconsultorio','admin') LIMIT 1`, [clinicaId, userId]);
return v.length > 0;
} catch { return false; }
}
const uid = (p) => `${p}_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
const hhmm = (v) => { const m = String(v ?? '').match(/^(\d{1,2}):(\d{2})/); return m ? `${m[1].padStart(2, '0')}:${m[2]}` : null; };
export function createAgendaConfig(pool) {
const router = express.Router();
router.use((req, res, next) => {
const auth = authFromReq(req);
if (!auth) return res.status(401).json({ error: 'Sessão inválida.' });
req.nwUser = auth;
next();
});
// ── Horário de FUNCIONAMENTO da clínica (dono) ────────────────────────────────
router.get('/clinica/:clinicaId/horarios', async (req, res) => {
try {
const { rows } = await pool.query(
`SELECT id, dia_semana, to_char(hora_inicio,'HH24:MI') AS hora_inicio,
to_char(hora_fim,'HH24:MI') AS hora_fim, ativo
FROM clinicas_horarios WHERE clinica_id = $1 ORDER BY dia_semana, hora_inicio`,
[req.params.clinicaId]);
res.json({ horarios: rows });
} catch (e) { res.status(500).json({ error: e.message }); }
});
// Substitui TODA a grade da clínica (mais simples e previsível que patch por dia).
// Body: { horarios: [{ dia_semana, hora_inicio, hora_fim }] }
router.put('/clinica/:clinicaId/horarios', 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 o horário da clínica.' });
const linhas = Array.isArray(req.body?.horarios) ? req.body.horarios : [];
const client = await pool.connect();
try {
await client.query('BEGIN');
await client.query('DELETE FROM clinicas_horarios WHERE clinica_id = $1', [clinicaId]);
for (const h of linhas) {
const dow = Number(h.dia_semana); const ini = hhmm(h.hora_inicio); const fim = hhmm(h.hora_fim);
if (!(dow >= 0 && dow <= 6) || !ini || !fim || ini >= fim) continue;
await client.query(
`INSERT INTO clinicas_horarios (id, clinica_id, dia_semana, hora_inicio, hora_fim, ativo)
VALUES ($1,$2,$3,$4,$5,1)`, [uid('ch'), clinicaId, dow, ini, fim]);
}
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(); }
});
// ── Horário do DENTISTA (o próprio dentista ou o dono) ────────────────────────
router.get('/dentista/:dentistaId/horarios', async (req, res) => {
try {
const { rows } = await pool.query(
`SELECT id, dia_semana, to_char(hora_inicio,'HH24:MI') AS hora_inicio,
to_char(hora_fim,'HH24:MI') AS hora_fim,
to_char(hora_inicio_flex,'HH24:MI') AS hora_inicio_flex,
to_char(hora_fim_flex,'HH24:MI') AS hora_fim_flex, ativo
FROM dentistas_horarios WHERE dentista_id = $1 ORDER BY dia_semana, hora_inicio`,
[req.params.dentistaId]);
res.json({ horarios: rows });
} catch (e) { res.status(500).json({ error: e.message }); }
});
router.put('/dentista/:dentistaId/horarios', async (req, res) => {
const dentistaId = req.params.dentistaId;
let dent;
try { const { rows } = await pool.query('SELECT clinica_id, usuario_id FROM dentistas WHERE id = $1', [dentistaId]); dent = rows[0]; }
catch (e) { return res.status(500).json({ error: e.message }); }
if (!dent) return res.status(404).json({ error: 'Dentista não encontrado.' });
const ehProprio = dent.usuario_id && dent.usuario_id === req.nwUser.userId;
const ehDono = await isDono(pool, dent.clinica_id, req.nwUser.userId);
if (!ehProprio && !ehDono)
return res.status(403).json({ error: 'Só o próprio dentista (ou o dono) pode configurar estes horários.' });
const linhas = Array.isArray(req.body?.horarios) ? req.body.horarios : [];
const client = await pool.connect();
try {
await client.query('BEGIN');
await client.query('DELETE FROM dentistas_horarios WHERE dentista_id = $1', [dentistaId]);
for (const h of linhas) {
const dow = Number(h.dia_semana); const ini = hhmm(h.hora_inicio); const fim = hhmm(h.hora_fim);
if (!(dow >= 0 && dow <= 6) || !ini || !fim || ini >= fim) continue;
// Janela estendida (zona cinza): flex_inicio ≤ inicio e flex_fim ≥ fim.
let fIni = hhmm(h.hora_inicio_flex), fFim = hhmm(h.hora_fim_flex);
if (fIni && fIni >= ini) fIni = null; // só vale se for MAIS cedo
if (fFim && fFim <= fim) fFim = null; // só vale se for MAIS tarde
await client.query(
`INSERT INTO dentistas_horarios (id, dentista_id, clinica_id, dia_semana, hora_inicio, hora_fim, hora_inicio_flex, hora_fim_flex, ativo)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,1)`, [uid('dh'), dentistaId, dent.clinica_id, dow, ini, fim, fIni, fFim]);
}
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(); }
});
// ── Folgas/férias do DENTISTA por data (o próprio dentista ou o dono) ─────────
async function guardDentista(pool, dentistaId, user) {
const { rows } = await pool.query('SELECT clinica_id, usuario_id FROM dentistas WHERE id = $1', [dentistaId]);
const dent = rows[0];
if (!dent) return { ok: false, code: 404, msg: 'Dentista não encontrado.' };
const ehProprio = dent.usuario_id && dent.usuario_id === user.userId;
const ehDono = await isDono(pool, dent.clinica_id, user.userId);
if (!ehProprio && !ehDono) return { ok: false, code: 403, msg: 'Só o próprio dentista (ou o dono) pode alterar.' };
return { ok: true, dent };
}
router.get('/dentista/:dentistaId/folgas', async (req, res) => {
try {
const { rows } = await pool.query(
`SELECT id, to_char(data_inicio,'YYYY-MM-DD') AS data_inicio, to_char(data_fim,'YYYY-MM-DD') AS data_fim, motivo
FROM dentistas_folgas WHERE dentista_id = $1 AND data_fim >= CURRENT_DATE ORDER BY data_inicio`,
[req.params.dentistaId]);
res.json({ folgas: rows });
} catch (e) { res.status(500).json({ error: e.message }); }
});
// Body: { data_inicio (YYYY-MM-DD), data_fim? (def = inicio), motivo? }
router.post('/dentista/:dentistaId/folgas', async (req, res) => {
const g = await guardDentista(pool, req.params.dentistaId, req.nwUser).catch((e) => ({ ok: false, code: 500, msg: e.message }));
if (!g.ok) return res.status(g.code).json({ error: g.msg });
const b = req.body || {};
if (!b.data_inicio) return res.status(400).json({ error: 'data_inicio obrigatória' });
const fim = b.data_fim && b.data_fim >= b.data_inicio ? b.data_fim : b.data_inicio;
try {
const { rows } = await pool.query(
`INSERT INTO dentistas_folgas (id, dentista_id, clinica_id, data_inicio, data_fim, motivo)
VALUES ($1,$2,$3,$4,$5,$6)
RETURNING id, to_char(data_inicio,'YYYY-MM-DD') AS data_inicio, to_char(data_fim,'YYYY-MM-DD') AS data_fim, motivo`,
[uid('fol'), req.params.dentistaId, g.dent.clinica_id, b.data_inicio, fim, b.motivo || null]);
res.status(201).json(rows[0]);
} catch (e) { res.status(500).json({ error: e.message }); }
});
router.delete('/dentista/:dentistaId/folgas/:id', async (req, res) => {
const g = await guardDentista(pool, req.params.dentistaId, req.nwUser).catch((e) => ({ ok: false, code: 500, msg: e.message }));
if (!g.ok) return res.status(g.code).json({ error: g.msg });
try { await pool.query('DELETE FROM dentistas_folgas WHERE id = $1 AND dentista_id = $2', [req.params.id, req.params.dentistaId]); res.json({ ok: true }); }
catch (e) { res.status(500).json({ error: e.message }); }
});
// ── Feriados/fechamentos (dono) ───────────────────────────────────────────────
// Lista os NACIONAIS (computados) + as exceções da clínica de um ano.
router.get('/clinica/:clinicaId/feriados', async (req, res) => {
const clinicaId = req.params.clinicaId;
const ano = Number(req.query.ano) || new Date().getFullYear();
try {
const nac = Object.entries(feriadosNacionais(ano)).map(([data, nome]) => ({ data, nome, origem: 'nacional' }));
const { rows: exc } = await pool.query(
`SELECT id, to_char(data,'YYYY-MM-DD') AS data, nome, fecha,
to_char(hora_inicio,'HH24:MI') AS hora_inicio, to_char(hora_fim,'HH24:MI') AS hora_fim, recorrente
FROM feriados WHERE clinica_id = $1 AND (extract(year from data) = $2 OR recorrente = 1) ORDER BY data`,
[clinicaId, ano]);
res.json({ nacionais: nac, excecoes: exc.map((e) => ({ ...e, origem: 'clinica' })) });
} catch (e) { res.status(500).json({ error: e.message }); }
});
// Cria uma exceção da clínica (fechamento/ponte/horário especial). Dono.
// Body: { data (YYYY-MM-DD), nome, fecha?(bool), hora_inicio?, hora_fim?, recorrente?(bool) }
router.post('/clinica/:clinicaId/feriados', 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 feriados/fechamentos.' });
const b = req.body || {};
if (!b.data || !b.nome) return res.status(400).json({ error: 'data e nome obrigatórios' });
const fecha = b.fecha === false ? 0 : 1;
const ini = fecha ? null : hhmm(b.hora_inicio);
const fim = fecha ? null : hhmm(b.hora_fim);
try {
const { rows } = await pool.query(
`INSERT INTO feriados (id, clinica_id, data, nome, fecha, hora_inicio, hora_fim, recorrente)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8)
RETURNING id, to_char(data,'YYYY-MM-DD') AS data, nome, fecha, recorrente`,
[uid('fer'), clinicaId, b.data, b.nome, fecha, ini, fim, b.recorrente ? 1 : 0]);
res.status(201).json(rows[0]);
} catch (e) { res.status(500).json({ error: e.message }); }
});
router.delete('/clinica/:clinicaId/feriados/:id', async (req, res) => {
if (!(await isDono(pool, req.params.clinicaId, req.nwUser.userId)))
return res.status(403).json({ error: 'Apenas o dono pode remover feriados/fechamentos.' });
try {
await pool.query('DELETE FROM feriados WHERE id = $1 AND clinica_id = $2', [req.params.id, req.params.clinicaId]);
res.json({ ok: true });
} catch (e) { res.status(500).json({ error: e.message }); }
});
// ── Situações / regras da clínica (dono) — a "área de situações" da Secretária ─
router.get('/clinica/:clinicaId/situacoes', async (req, res) => {
try {
const { rows } = await pool.query(`SELECT id, texto, ativo, ordem FROM agenda_situacoes WHERE clinica_id = $1 ORDER BY ordem, created_at`, [req.params.clinicaId]);
res.json({ situacoes: rows });
} catch (e) { res.status(500).json({ error: e.message }); }
});
// Substitui toda a lista. Body: { situacoes: [texto] }
router.put('/clinica/:clinicaId/situacoes', 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 as situações.' });
const linhas = Array.isArray(req.body?.situacoes) ? req.body.situacoes : [];
const client = await pool.connect();
try {
await client.query('BEGIN');
await client.query('DELETE FROM agenda_situacoes WHERE clinica_id = $1', [clinicaId]);
let i = 0;
for (const s of linhas) {
const t = String(s || '').trim(); if (!t) continue;
await client.query(`INSERT INTO agenda_situacoes (id, clinica_id, texto, ativo, ordem) VALUES ($1,$2,$3,1,$4)`, [uid('sit'), clinicaId, t, 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) {
const q = async (sql) => { try { const { rows } = await pool.query(sql, [clinicaId]); return Number(rows[0]?.n || 0); } catch { return 0; } };
const dentistas = await q(`SELECT COUNT(*) n FROM dentistas WHERE clinica_id = $1 AND ativo IS DISTINCT FROM 0`);
const pacientes = await q(`SELECT COUNT(*) n FROM pacientes WHERE clinica_id = $1`);
return { dentistas, pacientes };
}
async function respostasChecklist(clinicaId) {
const map = {};
try { const { rows } = await pool.query('SELECT chave, resposta FROM agenda_checklist WHERE clinica_id = $1', [clinicaId]); for (const r of rows) map[r.chave] = r.resposta == null ? null : Number(r.resposta); } catch { /* tabela nova */ }
return map;
}
router.get('/clinica/:clinicaId/checklist', 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 acessa o checklist.' });
try {
const map = await respostasChecklist(clinicaId);
res.json({ questoes: CHECKLIST_QUESTOES.map((q) => ({ ...q, resposta: map[q.chave] ?? null })) });
} catch (e) { res.status(500).json({ error: e.message }); }
});
// Salva respostas. Body: { respostas: { chave: 1|0|null } }
router.put('/clinica/:clinicaId/checklist', 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 o checklist.' });
const respostas = req.body?.respostas || {};
const client = await pool.connect();
try {
await client.query('BEGIN');
for (const q of CHECKLIST_QUESTOES) {
if (!(q.chave in respostas)) continue;
const v = respostas[q.chave];
const r = v === 1 || v === true ? 1 : v === 0 || v === false ? 0 : null;
await client.query(
`INSERT INTO agenda_checklist (clinica_id, chave, resposta, updated_at) VALUES ($1,$2,$3,now())
ON CONFLICT (clinica_id, chave) DO UPDATE SET resposta = EXCLUDED.resposta, updated_at = now()`,
[clinicaId, q.chave, r]);
}
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(); }
});
// Área de pendências do DONO — engloba TUDO: pendências de config (checklist ×
// banco) + a fila operacional (pedidos de horário + dúvidas aguardando humano).
router.get('/clinica/:clinicaId/pendencias', 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 acessa as pendências.' });
try {
const counts = await contagens(clinicaId);
const respostas = await respostasChecklist(clinicaId);
const config = pendenciasDeConfig(respostas, counts);
let horarios = 0, duvidas = 0;
try {
const { rows } = await pool.query(
`SELECT COALESCE(tipo,'horario') tipo, COUNT(*) n FROM agenda_pedidos WHERE clinica_id = $1 AND status = 'pendente' GROUP BY 1`, [clinicaId]);
for (const r of rows) { if (r.tipo === 'duvida') duvidas = Number(r.n); else horarios = Number(r.n); }
} catch { /* tabela nova */ }
res.json({
contagens: counts,
config,
operacionais: { horarios, duvidas, total: horarios + duvidas },
total: config.length + horarios + duvidas,
});
} catch (e) { res.status(500).json({ error: e.message }); }
});
// ── Pedidos pendentes (zona cinza) — a secretária humana confirma/recusa ──────
async function ehMembro(clinicaId, userId) {
if (await isDono(pool, clinicaId, userId)) return true;
try { const { rows } = await pool.query('SELECT 1 FROM vinculos WHERE clinica_id = $1 AND usuario_id = $2 LIMIT 1', [clinicaId, userId]); return rows.length > 0; }
catch { return false; }
}
router.get('/clinica/:clinicaId/pedidos', async (req, res) => {
if (!(await ehMembro(req.params.clinicaId, req.nwUser.userId))) return res.status(403).json({ error: 'Sem acesso.' });
const status = req.query.status ? String(req.query.status) : 'pendente';
try {
const { rows } = await pool.query(
`SELECT p.id, COALESCE(p.tipo,'horario') AS tipo, p.resumo,
to_char(p.data,'YYYY-MM-DD') AS data, to_char(p.hora_inicio,'HH24:MI') AS hora_inicio,
to_char(p.hora_fim,'HH24:MI') AS hora_fim, p.paciente_nome, p.paciente_telefone, p.procedimento,
p.dentista_id, d.nome AS dentista_nome, p.status, p.created_at
FROM agenda_pedidos p LEFT JOIN dentistas d ON d.id = p.dentista_id
WHERE p.clinica_id = $1 AND p.status = $2 ORDER BY p.created_at DESC LIMIT 100`,
[req.params.clinicaId, status]);
res.json({ pedidos: rows, total: rows.length });
} catch (e) { res.status(500).json({ error: e.message }); }
});
// Confirma → cria o agendamento real e marca o pedido como confirmado.
// Body: { dentista_id? (se o pedido não tinha), duracao_min? }
router.post('/clinica/:clinicaId/pedidos/:id/confirmar', async (req, res) => {
const clinicaId = req.params.clinicaId;
if (!(await ehMembro(clinicaId, req.nwUser.userId))) return res.status(403).json({ error: 'Sem acesso.' });
const client = await pool.connect();
try {
await client.query('BEGIN');
const { rows } = await client.query('SELECT * FROM agenda_pedidos WHERE id = $1 AND clinica_id = $2 FOR UPDATE', [req.params.id, clinicaId]);
const ped = rows[0];
if (!ped) { await client.query('ROLLBACK'); return res.status(404).json({ error: 'Pedido não encontrado.' }); }
if (ped.status !== 'pendente') { await client.query('ROLLBACK'); return res.status(409).json({ error: 'Pedido já resolvido.' }); }
const dentistaId = req.body?.dentista_id || ped.dentista_id;
if (!dentistaId) { await client.query('ROLLBACK'); return res.status(400).json({ error: 'Escolha o dentista para confirmar.' }); }
const dur = Math.max(10, Number(req.body?.duracao_min) || 30);
const hi = String(ped.hora_inicio).slice(0, 5);
const start = `${(ped.data instanceof Date ? ped.data.toISOString().slice(0, 10) : String(ped.data).slice(0, 10))} ${hi}:00`;
const endD = new Date(`${start.replace(' ', 'T')}`); endD.setMinutes(endD.getMinutes() + dur);
const end = `${start.slice(0, 10)} ${String(endD.getHours()).padStart(2, '0')}:${String(endD.getMinutes()).padStart(2, '0')}:00`;
const { rows: dd } = await client.query('SELECT setor_id FROM dentistas WHERE id = $1', [dentistaId]);
const agId = `ag_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
await client.query(
`INSERT INTO agendamentos (id, clinica_id, pacientenome, pacientecelular, dentistaid, procedimento,
start_time, end_time, status, setor_id, sala_id, created_by_nome, created_at, updated_at)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,'agendado',$9,$10,'Secretária (confirmado)',NOW(),NOW())`,
[agId, clinicaId, ped.paciente_nome || 'Cliente WhatsApp', ped.paciente_telefone || null, dentistaId,
ped.procedimento || 'Consulta', start, end, dd[0]?.setor_id || null, ped.sala_id || null]);
await client.query(`UPDATE agenda_pedidos SET status='confirmado', agendamento_id=$1, resolved_at=NOW(), resolved_by=$2 WHERE id=$3`,
[agId, req.nwUser.userId, req.params.id]);
await client.query('COMMIT');
res.json({ ok: true, agendamento_id: agId });
} catch (e) { try { await client.query('ROLLBACK'); } catch {} res.status(500).json({ error: e.message }); }
finally { client.release(); }
});
// Resolve uma DÚVIDA (tipo='duvida') sem criar agendamento — a humana já
// decidiu/atendeu (ou vai agendar manualmente uma avaliação).
router.post('/clinica/:clinicaId/pedidos/:id/resolver', async (req, res) => {
if (!(await ehMembro(req.params.clinicaId, req.nwUser.userId))) return res.status(403).json({ error: 'Sem acesso.' });
try {
await pool.query(`UPDATE agenda_pedidos SET status='resolvido', resolved_at=NOW(), resolved_by=$1 WHERE id=$2 AND clinica_id=$3 AND status='pendente'`,
[req.nwUser.userId, req.params.id, req.params.clinicaId]);
res.json({ ok: true });
} catch (e) { res.status(500).json({ error: e.message }); }
});
router.post('/clinica/:clinicaId/pedidos/:id/recusar', async (req, res) => {
if (!(await ehMembro(req.params.clinicaId, req.nwUser.userId))) return res.status(403).json({ error: 'Sem acesso.' });
try {
await pool.query(`UPDATE agenda_pedidos SET status='recusado', resolved_at=NOW(), resolved_by=$1 WHERE id=$2 AND clinica_id=$3 AND status='pendente'`,
[req.nwUser.userId, req.params.id, req.params.clinicaId]);
res.json({ ok: true });
} catch (e) { res.status(500).json({ error: e.message }); }
});
// Lista dentistas da clínica (p/ a UI do dono escolher quem configurar / e o
// dentista achar seu registro). Retorna também se o usuário logado é o dentista.
router.get('/clinica/:clinicaId/dentistas', async (req, res) => {
try {
const { rows } = await pool.query(
`SELECT id, nome, usuario_id, ativo FROM dentistas WHERE clinica_id = $1 ORDER BY ordem NULLS LAST, nome`,
[req.params.clinicaId]);
res.json({ dentistas: rows.map((d) => ({ ...d, eh_voce: d.usuario_id === req.nwUser.userId })) });
} catch (e) { res.status(500).json({ error: e.message }); }
});
return router;
}