77dec56dd0
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>
93 lines
3.7 KiB
JavaScript
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;
|
|
}
|