957e912a7f
Sprints B (05-03) e C (05-04) acumulados: - NotificationDrawer/Item redesign (visual mais limpo, ações inline) - Dock pins compose (useMelissaDockPins) + cache store global (melissaCacheStore) - MelissaAgenda: timeline FullCalendar parity + cards resumo, histórico card com useMelissaAgendaHistorico, MelissaEventoPanel ajustado - useFeriados: cache opt-in pra evitar fetch redundante de feriados - PatientProntuario: aba Visão Geral nova; PatientConversationsTab polish - AgendaClinicMosaic / AgendaTerapeutaPage / useAgendaSettings: ajustes de paridade com Melissa - DocumentsListPage: pequenos ajustes - DB migration 20260504000001: fix do trigger pra status 'excluido' nas cancel_notifications Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
167 lines
6.5 KiB
JavaScript
167 lines
6.5 KiB
JavaScript
/*
|
||
|--------------------------------------------------------------------------
|
||
| Agência PSI
|
||
|--------------------------------------------------------------------------
|
||
| Criado e desenvolvido por Leonardo Nohama
|
||
|
|
||
| Tecnologia aplicada à escuta.
|
||
| Estrutura para o cuidado.
|
||
|
|
||
| Arquivo: src/composables/useFeriados.js
|
||
| Data: 2026
|
||
| Local: São Carlos/SP — Brasil
|
||
|--------------------------------------------------------------------------
|
||
| © 2026 — Todos os direitos reservados
|
||
|--------------------------------------------------------------------------
|
||
*/
|
||
|
||
import { ref, computed } from 'vue';
|
||
import { supabase } from '@/lib/supabase/client';
|
||
import { getFeriadosNacionais } from '@/utils/feriadosBR';
|
||
import { useMelissaCacheStore, MELISSA_CACHE_TTL } from '@/stores/melissaCacheStore';
|
||
|
||
// opts.cache (opt-in): habilita stale-while-revalidate via melissaCacheStore.
|
||
// Default false pra preservar comportamento em páginas SaaS/admin que
|
||
// editam feriados (esperam invalidação imediata após criar/remover).
|
||
export function useFeriados(opts = {}) {
|
||
const useCache = !!opts.cache;
|
||
const cache = useCache ? useMelissaCacheStore() : null;
|
||
|
||
const ano = ref(new Date().getFullYear());
|
||
const loading = ref(false);
|
||
const municipais = ref([]); // linhas da tabela `feriados`
|
||
|
||
// ── Nacionais (algoritmo, sem DB) ─────────────────────────
|
||
const nacionais = computed(() => getFeriadosNacionais(ano.value).map((f) => ({ ...f, tipo: 'nacional' })));
|
||
|
||
// ── Todos juntos, ordenados por data ─────────────────────
|
||
const todos = computed(() => [...nacionais.value, ...municipais.value.map((f) => ({ ...f, tipo: f.tipo || 'municipal' }))].sort((a, b) => a.data.localeCompare(b.data)));
|
||
|
||
// ── Feriados de um mês (1–12) ─────────────────────────────
|
||
function doMes(mes) {
|
||
const m = String(mes).padStart(2, '0');
|
||
const prefix = `${ano.value}-${m}`;
|
||
return todos.value.filter((f) => f.data.startsWith(prefix));
|
||
}
|
||
|
||
// ── Próximos N dias ───────────────────────────────────────
|
||
function proximos(dias = 30) {
|
||
const hoje = new Date();
|
||
const limite = new Date(hoje);
|
||
limite.setDate(limite.getDate() + dias);
|
||
const hojeISO = toISO(hoje);
|
||
const limiteISO = toISO(limite);
|
||
return todos.value.filter((f) => f.data >= hojeISO && f.data <= limiteISO);
|
||
}
|
||
|
||
function toISO(d) {
|
||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||
}
|
||
|
||
async function _doFetch(tenantId, cacheKey) {
|
||
const { data, error } = await supabase
|
||
.from('feriados')
|
||
.select('*')
|
||
.or(`tenant_id.eq.${tenantId},tenant_id.is.null`)
|
||
.gte('data', `${ano.value}-01-01`)
|
||
.lte('data', `${ano.value}-12-31`)
|
||
.order('data');
|
||
if (error) throw error;
|
||
const rows = data || [];
|
||
if (cache && cacheKey) cache.set('feriados', rows, cacheKey);
|
||
municipais.value = rows;
|
||
return rows;
|
||
}
|
||
|
||
// ── Load municipais do Supabase ───────────────────────────
|
||
async function load(tenantId, year) {
|
||
if (year) ano.value = year;
|
||
if (!tenantId) return;
|
||
|
||
if (cache) {
|
||
const cacheKey = `${tenantId}:${ano.value}`;
|
||
const cached = cache.get('feriados', cacheKey, MELISSA_CACHE_TTL.feriados);
|
||
if (cached) {
|
||
municipais.value = cached;
|
||
_doFetch(tenantId, cacheKey).catch((e) => {
|
||
// eslint-disable-next-line no-console
|
||
console.warn('[useFeriados] revalidate', e);
|
||
});
|
||
return;
|
||
}
|
||
loading.value = true;
|
||
try { await _doFetch(tenantId, cacheKey); }
|
||
finally { loading.value = false; }
|
||
return;
|
||
}
|
||
|
||
// Comportamento legado (sem cache) — páginas de admin que editam.
|
||
loading.value = true;
|
||
try {
|
||
const { data, error } = await supabase
|
||
.from('feriados')
|
||
.select('*')
|
||
.or(`tenant_id.eq.${tenantId},tenant_id.is.null`)
|
||
.gte('data', `${ano.value}-01-01`)
|
||
.lte('data', `${ano.value}-12-31`)
|
||
.order('data');
|
||
if (error) throw error;
|
||
municipais.value = data || [];
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
// ── Criar feriado municipal ───────────────────────────────
|
||
async function criar(payload) {
|
||
const { data, error } = await supabase.from('feriados').insert(payload).select().single();
|
||
if (error) throw error;
|
||
municipais.value = [...municipais.value, data].sort((a, b) => a.data.localeCompare(b.data));
|
||
if (cache) cache.invalidate('feriados');
|
||
return data;
|
||
}
|
||
|
||
// ── Remover feriado municipal ─────────────────────────────
|
||
async function remover(id) {
|
||
const { error } = await supabase.from('feriados').delete().eq('id', id);
|
||
if (error) throw error;
|
||
municipais.value = municipais.value.filter((f) => f.id !== id);
|
||
if (cache) cache.invalidate('feriados');
|
||
}
|
||
|
||
// ── Verificar duplicata ───────────────────────────────────
|
||
function isDuplicata(data, nome) {
|
||
return todos.value.some((f) => f.data === data && f.nome.trim().toLowerCase() === nome.trim().toLowerCase());
|
||
}
|
||
|
||
// ── Converter para eventos do FullCalendar (background) ──
|
||
function toFcEvents(lista) {
|
||
return lista.map((f) => ({
|
||
id: `feriado_${f.id || f.data}_${f.nome}`,
|
||
title: f.nome,
|
||
start: f.data,
|
||
allDay: true,
|
||
display: 'background',
|
||
color: 'rgba(251, 191, 36, 0.25)',
|
||
extendedProps: { _feriado: true, tipo: f.tipo }
|
||
}));
|
||
}
|
||
|
||
const fcEvents = computed(() => toFcEvents(todos.value));
|
||
|
||
return {
|
||
ano,
|
||
loading,
|
||
nacionais,
|
||
municipais,
|
||
todos,
|
||
fcEvents,
|
||
load,
|
||
criar,
|
||
remover,
|
||
doMes,
|
||
proximos,
|
||
isDuplicata
|
||
};
|
||
}
|