Files
scoreodonto.com/backend/newwhats/feriados.js
T
VPS 4 Builder 77dec56dd0 feat(secretaria): horários de funcionamento + feriados na agenda/Secretária
A Secretária passa a saber quando a clínica está aberta e a NÃO oferecer
horário/agendar em dia fechado (domingo, fora do horário, feriado).

- feriados.js: feriados nacionais BR (fixos + móveis via Páscoa) + resolver
  com exceções da clínica.
- agenda-schema.js: tabelas clinicas_horarios (horário do dono) e feriados
  (exceções/fechamentos), idempotentes.
- agenda-config.js: CRUD com ownership (horário/feriado da clínica = dono;
  horário do dentista = ele mesmo ou o dono), em /api/nw/agenda-config.
- agenda-bridge.js: GET /status (aberto hoje?, motivo, próximo aberto, grade,
  feriados) + /slots agora = clínica ∩ dentista − feriado.
- HorariosConfig.tsx + botão no AgendaSettingsModal: telas de configuração
  (abas Clínica e Dentistas).

Testado em dev: a Secretária ofereceu "amanhã" ao pedirem "hoje" num domingo,
com os horários configurados da clínica.

Obs.: index.js também traz trabalho acumulado do satélite (proxy de mídia via
Wasabi/storage) que estava no working tree.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 01:23:27 +02:00

93 lines
3.7 KiB
JavaScript

// Feriados nacionais brasileiros (fixos + móveis) + resolução com exceções da
// clínica (tabela `feriados`). Usado pela agenda-bridge para a Secretária saber
// quando é feriado e o /slots pular esses dias.
//
// Fixos: datas nacionais. Móveis: derivados da Páscoa (Computus de Meeus).
// Consciência Negra (20/11) é nacional desde 2024 (Lei 14.759/2023).
const pad = (n) => String(n).padStart(2, '0');
const ymd = (y, m, d) => `${y}-${pad(m)}-${pad(d)}`;
// Domingo de Páscoa (algoritmo de Meeus/Jones/Butcher) — retorna {mes, dia}.
function pascoa(ano) {
const a = ano % 19;
const b = Math.floor(ano / 100);
const c = ano % 100;
const d = Math.floor(b / 4);
const e = b % 4;
const f = Math.floor((b + 8) / 25);
const g = Math.floor((b - f + 1) / 3);
const h = (19 * a + b - d - g + 15) % 30;
const i = Math.floor(c / 4);
const k = c % 4;
const l = (32 + 2 * e + 2 * i - h - k) % 7;
const m = Math.floor((a + 11 * h + 22 * l) / 451);
const mes = Math.floor((h + l - 7 * m + 114) / 31);
const dia = ((h + l - 7 * m + 114) % 31) + 1;
return { mes, dia };
}
// Soma dias a uma data (UTC, evita fuso) → 'YYYY-MM-DD'.
function addDias(y, m, d, delta) {
const base = new Date(Date.UTC(y, m - 1, d));
base.setUTCDate(base.getUTCDate() + delta);
return ymd(base.getUTCFullYear(), base.getUTCMonth() + 1, base.getUTCDate());
}
// Mapa { 'YYYY-MM-DD': 'Nome do feriado' } dos feriados NACIONAIS do ano.
export function feriadosNacionais(ano) {
const p = pascoa(ano);
const py = ano, pm = p.mes, pd = p.dia;
const map = {
[ymd(ano, 1, 1)]: 'Confraternização Universal',
[ymd(ano, 4, 21)]: 'Tiradentes',
[ymd(ano, 5, 1)]: 'Dia do Trabalho',
[ymd(ano, 9, 7)]: 'Independência do Brasil',
[ymd(ano, 10, 12)]: 'Nossa Senhora Aparecida',
[ymd(ano, 11, 2)]: 'Finados',
[ymd(ano, 11, 15)]: 'Proclamação da República',
[ymd(ano, 11, 20)]: 'Consciência Negra',
[ymd(ano, 12, 25)]: 'Natal',
// Móveis (relativos ao Domingo de Páscoa)
[addDias(py, pm, pd, -48)]: 'Carnaval (segunda-feira)',
[addDias(py, pm, pd, -47)]: 'Carnaval (terça-feira)',
[addDias(py, pm, pd, -2)]: 'Sexta-feira Santa',
[addDias(py, pm, pd, 60)]: 'Corpus Christi',
};
return map;
}
// Nome do feriado nacional numa data 'YYYY-MM-DD', ou null.
export function feriadoNacionalNome(dateStr) {
const ano = Number(String(dateStr).slice(0, 4));
if (!ano) return null;
return feriadosNacionais(ano)[dateStr] ?? null;
}
// Resolve o feriado de uma data para uma clínica: nacional OU exceção da clínica
// (tabela `feriados`, que também pode ANULAR um dia — ver `fecha`). Retorna
// { fecha: bool, nome, hora_inicio?, hora_fim? } ou null (dia normal).
//
// Regras:
// - Exceção da clínica tem prioridade (permite abrir num feriado nacional, ou
// fechar num dia comum, ou definir horário especial).
// - Sem exceção: cai no feriado nacional (fecha o dia inteiro).
export async function resolverFeriado(pool, clinicaId, dateStr) {
try {
const mmdd = String(dateStr).slice(5); // 'MM-DD'
const { rows } = await pool.query(
`SELECT nome, fecha, hora_inicio, hora_fim FROM feriados
WHERE clinica_id = $1
AND (data = $2 OR (recorrente = 1 AND to_char(data,'MM-DD') = $3))
ORDER BY (data = $2) DESC LIMIT 1`,
[clinicaId, dateStr, mmdd]);
if (rows.length) {
const r = rows[0];
return { fecha: r.fecha !== false, nome: r.nome || 'Fechamento', hora_inicio: r.hora_inicio || null, hora_fim: r.hora_fim || null, origem: 'clinica' };
}
} catch { /* tabela pode não existir ainda */ }
const nac = feriadoNacionalNome(dateStr);
if (nac) return { fecha: true, nome: nac, hora_inicio: null, hora_fim: null, origem: 'nacional' };
return null;
}