40bed96515
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>
405 lines
23 KiB
JavaScript
405 lines
23 KiB
JavaScript
// 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;
|
||
}
|