feat(agenda-bridge): enforcement de janela ao contatar dentista
/dentista-contato passa a ler pessoa_comunicacao (via dentistas.usuario_id):
- Com perfil: só devolve número se a hora atual cair numa janela permitida
(contato_agora); senão bloqueado_por_janela=true + proxima_janela ("sábado às 08:00").
- Sem perfil: back-compat — usa telefone legado e não bloqueia.
Helpers contatoPermitidoAgora/proximaJanela (TZ do container = America/Sao_Paulo).
Verificado E2E: fora da janela -> bloqueado; dentro -> libera nº; sem perfil -> ok.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -123,6 +123,44 @@ async function findConflict(db, dentistaId, start, end, excludeId = null) {
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
// ── Comunicação interna: janelas de horário permitido p/ contatar um número ──────
|
||||
const toMin = (t) => { const [h, m] = String(t ?? '').split(':').map(Number); return (h || 0) * 60 + (m || 0); };
|
||||
// Retorna o 1º número contactável AGORA (dia/horário dentro de alguma janela) ou null.
|
||||
function contatoPermitidoAgora(numeros, now = new Date()) {
|
||||
const dow = now.getDay();
|
||||
const hm = now.getHours() * 60 + now.getMinutes();
|
||||
for (const n of (numeros || [])) {
|
||||
const tel = String(n?.numero || '').replace(/\D/g, '');
|
||||
if (tel.length < 10) continue;
|
||||
for (const j of (n.janelas || [])) {
|
||||
if (!Array.isArray(j.dias) || !j.dias.includes(dow)) continue;
|
||||
if (hm >= toMin(j.inicio) && hm <= toMin(j.fim)) return { numero: tel, descricao: n.descricao || null, situacoes: n.situacoes || null };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// Próxima janela permitida (varre os próximos 7 dias) → "segunda-feira às 08:00" | null.
|
||||
function proximaJanela(numeros, now = new Date()) {
|
||||
const nowDow = now.getDay();
|
||||
const nowHm = now.getHours() * 60 + now.getMinutes();
|
||||
let best = null; // { offset, ini }
|
||||
for (const n of (numeros || [])) {
|
||||
if (String(n?.numero || '').replace(/\D/g, '').length < 10) continue;
|
||||
for (const j of (n.janelas || [])) {
|
||||
if (!Array.isArray(j.dias)) continue;
|
||||
for (const d of j.dias) {
|
||||
let offset = (d - nowDow + 7) % 7;
|
||||
const ini = toMin(j.inicio);
|
||||
if (offset === 0 && ini <= nowHm) offset = 7; // hoje já passou → próxima semana
|
||||
if (!best || offset < best.offset || (offset === best.offset && ini < best.ini)) best = { offset, ini, dow: d, inicio: j.inicio };
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!best) return null;
|
||||
const quando = best.offset === 0 ? 'hoje' : best.offset === 1 ? 'amanhã' : DIAS_NOME[best.dow];
|
||||
return `${quando} às ${best.inicio}`;
|
||||
}
|
||||
|
||||
export function createAgendaBridge(pool) {
|
||||
const router = express.Router();
|
||||
|
||||
@@ -408,11 +446,26 @@ export function createAgendaBridge(pool) {
|
||||
if (req.query.dentista_id) { params.push(String(req.query.dentista_id)); where += ` AND id = $${params.length}`; }
|
||||
else if (req.query.nome) { params.push(String(req.query.nome)); where += ` AND lower(nome) LIKE '%'||lower($${params.length})||'%'`; }
|
||||
const { rows } = await pool.query(
|
||||
`SELECT id, nome, telefone FROM dentistas WHERE ${where} AND ativo IS DISTINCT FROM 0 ORDER BY nome LIMIT 1`, params);
|
||||
`SELECT id, nome, telefone, usuario_id FROM dentistas WHERE ${where} AND ativo IS DISTINCT FROM 0 ORDER BY nome LIMIT 1`, params);
|
||||
if (!rows.length) return res.json({ encontrado: false });
|
||||
const d = rows[0];
|
||||
const tel = String(d.telefone || '').replace(/\D/g, '');
|
||||
res.json({ encontrado: true, dentista_id: d.id, nome: d.nome, telefone: tel || null });
|
||||
const legacyTel = String(d.telefone || '').replace(/\D/g, '') || null;
|
||||
// Perfil de comunicação interna (múltiplos números + janelas de horário permitido).
|
||||
let numeros = [], temPerfil = false;
|
||||
if (d.usuario_id) {
|
||||
const { rows: pc } = await pool.query('SELECT numeros FROM pessoa_comunicacao WHERE usuario_id=$1 AND clinica_id=$2', [d.usuario_id, clinicaId]);
|
||||
if (pc[0]) { temPerfil = true; numeros = Array.isArray(pc[0].numeros) ? pc[0].numeros : []; }
|
||||
}
|
||||
// Sem perfil → back-compat: usa o telefone legado e NÃO bloqueia (não quebra dentistas antigos).
|
||||
if (!temPerfil) {
|
||||
return res.json({ encontrado: true, dentista_id: d.id, nome: d.nome, telefone: legacyTel, tem_perfil: false,
|
||||
contato_agora: legacyTel ? { numero: legacyTel, descricao: null } : null, bloqueado_por_janela: false, proxima_janela: null });
|
||||
}
|
||||
// Com perfil → só permite contato dentro de uma janela; senão devolve a próxima.
|
||||
const agora = contatoPermitidoAgora(numeros);
|
||||
res.json({ encontrado: true, dentista_id: d.id, nome: d.nome, tem_perfil: true,
|
||||
telefone: agora ? agora.numero : null, contato_agora: agora,
|
||||
bloqueado_por_janela: !agora, proxima_janela: agora ? null : proximaJanela(numeros) });
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user