// 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'; 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, 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; await client.query( `INSERT INTO dentistas_horarios (id, dentista_id, clinica_id, dia_semana, hora_inicio, hora_fim, ativo) VALUES ($1,$2,$3,$4,$5,$6,1)`, [uid('dh'), dentistaId, dent.clinica_id, 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(); } }); // ── 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 }); } }); // 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; }