Melissa polish + Prontuario Visao Geral + agenda historico
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>
This commit is contained in:
@@ -181,8 +181,15 @@ export function useMelissaAgenda() {
|
||||
);
|
||||
|
||||
// ── Settings + workRules ────────────────────────────────────
|
||||
const { settings, workRules, load: loadSettings } = useAgendaSettings();
|
||||
const ownerId = computed(() => settings.value?.owner_id || '');
|
||||
// cache: stale-while-revalidate via melissaCacheStore — abertura
|
||||
// subsequente da Agenda na mesma sessão usa cache instantâneo.
|
||||
const { settings, workRules, load: loadSettings } = useAgendaSettings({ cache: true });
|
||||
// _bootUid: pegado em paralelo no mount via supabase.auth.getUser().
|
||||
// Sem isso, ownerId ficava null até loadSettings completar (~300ms),
|
||||
// bloqueando o primeiro fetch dos eventos. Como owner_id da agenda
|
||||
// é literalmente o uid do user logado, podemos resolver imediato.
|
||||
const _bootUid = ref('');
|
||||
const ownerId = computed(() => settings.value?.owner_id || _bootUid.value || '');
|
||||
|
||||
// ── Eventos reais (CRUD) ────────────────────────────────────
|
||||
const {
|
||||
@@ -245,7 +252,16 @@ export function useMelissaAgenda() {
|
||||
});
|
||||
|
||||
// ── Feriados + commitment services ──────────────────────────
|
||||
const { todos: feriados, fcEvents: feriadoFcEvents, load: loadFeriadosBase } = useFeriados();
|
||||
// Instância única de useFeriados — antes MelissaAgenda.vue criava
|
||||
// sua própria também, fazendo dupla requisição de feriados municipais
|
||||
// toda vez que a agenda abria. Agora MelissaAgenda lê esses refs do
|
||||
// composable injetado (M.feriadosAno, M.loadFeriadosBase, etc).
|
||||
const {
|
||||
todos: feriados,
|
||||
fcEvents: feriadoFcEvents,
|
||||
load: loadFeriadosBase,
|
||||
ano: feriadosAno
|
||||
} = useFeriados({ cache: true });
|
||||
const { saveRuleItems, propagateToSerie } = useCommitmentServices();
|
||||
|
||||
// ── Linhas combinadas (real + virtual) ──────────────────────
|
||||
@@ -294,13 +310,18 @@ export function useMelissaAgenda() {
|
||||
const e = viewEnd.value;
|
||||
if (!s || !e) return;
|
||||
|
||||
// Aguarda ownerId — settings é async
|
||||
if (!ownerId.value) {
|
||||
const unwatch = watch(ownerId, async (v) => {
|
||||
if (!v) return;
|
||||
unwatch();
|
||||
await _reloadRange();
|
||||
});
|
||||
// Espera ownerId E tenant — qualquer um faltando significa boot
|
||||
// ainda em curso (auth/tenantStore/settings async). Watcher one-shot
|
||||
// re-dispara assim que o último ficar disponível, sem polling.
|
||||
if (!ownerId.value || !clinicTenantId.value) {
|
||||
const unwatch = watch(
|
||||
() => [ownerId.value, clinicTenantId.value],
|
||||
([uid, tid]) => {
|
||||
if (!uid || !tid) return;
|
||||
unwatch();
|
||||
_reloadRange();
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -308,9 +329,14 @@ export function useMelissaAgenda() {
|
||||
const end = new Date(e);
|
||||
const tid = clinicTenantId.value;
|
||||
|
||||
// Etapa 1: eventos reais — `rows` é reativo, FullCalendar re-renderiza
|
||||
// assim que esse await resolve (o user já vê as sessões agendadas).
|
||||
await loadMyRange(start.toISOString(), end.toISOString(), ownerId.value);
|
||||
|
||||
// Expande regras + merge com sessões reais
|
||||
// Etapa 2: ocorrências virtuais (regras de recorrência expandidas).
|
||||
// Continuamos awaitando porque saveRule/cancel dependem do estado
|
||||
// final estar pronto pra UI consistente, mas a janela visual onde
|
||||
// o usuário vê só eventos reais é a metade do tempo de antes.
|
||||
const merged = await loadAndExpand(ownerId.value, start, end, rows.value, tid);
|
||||
_occurrenceRows.value = merged.filter((r) => r.is_occurrence);
|
||||
}
|
||||
@@ -320,8 +346,37 @@ export function useMelissaAgenda() {
|
||||
}
|
||||
|
||||
// ── Inicialização ───────────────────────────────────────────
|
||||
onMounted(async () => {
|
||||
await loadSettings();
|
||||
// Boot paralelo: auth uid + tenant + settings todos disparam ao mesmo
|
||||
// tempo. Antes era serial (loadSettings precisava terminar pra ownerId
|
||||
// ficar disponível e o watch disparar _reloadRange) — adicionava ~300ms
|
||||
// de waterfall antes da primeira query de eventos sair.
|
||||
onMounted(() => {
|
||||
// 1) Resolve o uid o quanto antes — destrava _reloadRange.
|
||||
// getSession() lê do storage local (fast path, <10ms);
|
||||
// getUser() faria round-trip pro auth server. Fallback pro
|
||||
// getUser só se a sessão ainda não estiver no storage.
|
||||
supabase.auth.getSession()
|
||||
.then(({ data }) => {
|
||||
const uid = data?.session?.user?.id;
|
||||
if (uid) {
|
||||
_bootUid.value = uid;
|
||||
} else {
|
||||
// Cold start sem sessão hidratada — fallback pro round-trip.
|
||||
return supabase.auth.getUser().then(({ data: u }) => {
|
||||
if (u?.user?.id) _bootUid.value = u.user.id;
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => { /* noop — settings ainda pode resolver */ });
|
||||
|
||||
// 2) Garante que o tenant está hidratado (idempotente — se já
|
||||
// estiver carregado, retorna imediato).
|
||||
if (typeof tenantStore.ensureLoaded === 'function') {
|
||||
tenantStore.ensureLoaded().catch(() => {});
|
||||
}
|
||||
|
||||
// 3) Settings em paralelo (não bloqueia mais nada)
|
||||
loadSettings();
|
||||
});
|
||||
|
||||
// Refetch settings + workRules quando o user salva jornada/ritmo/online
|
||||
@@ -354,11 +409,10 @@ export function useMelissaAgenda() {
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// Reload quando view muda OU quando settings/ownerId aparece
|
||||
// Reload quando o range visível muda. _reloadRange já tem guard
|
||||
// interno pra esperar uid+tenant (one-shot watcher) — sem necessidade
|
||||
// de outro watch global em ownerId, que disparava _reloadRange duplicado.
|
||||
watch([viewStart, viewEnd], _reloadRange);
|
||||
watch(ownerId, (v) => {
|
||||
if (v) _reloadRange();
|
||||
});
|
||||
|
||||
// ──────────────────────────────────────────────────────────
|
||||
// Handlers — populados na Stage 2
|
||||
@@ -405,6 +459,8 @@ export function useMelissaAgenda() {
|
||||
commitmentOptions,
|
||||
feriados,
|
||||
feriadoFcEvents,
|
||||
feriadosAno,
|
||||
loadFeriadosBase,
|
||||
allEventsForDialog,
|
||||
|
||||
// Handlers
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Arquivo: src/layout/melissa/composables/useMelissaAgendaHistorico.js
|
||||
| Data: 2026-05-04
|
||||
|
|
||||
| Histórico recente de ações na agenda do terapeuta logado.
|
||||
|
|
||||
| Lê de `audit_logs` (populado automaticamente pela trigger
|
||||
| `trg_audit_agenda_eventos`). Não precisa criar nada — todas as ações
|
||||
| INSERT/UPDATE/DELETE em agenda_eventos já viram linhas auditadas.
|
||||
|
|
||||
| Filtros aplicados:
|
||||
| - entity_type = 'agenda_eventos'
|
||||
| - user_id = uid do user logado (mostra só ações dele)
|
||||
| - created_at >= 7 dias atrás
|
||||
| - tenant_id = tenant ativo
|
||||
| - LIMIT 20 (mais recentes primeiro)
|
||||
|
|
||||
| Pra exibir nome do paciente, fazemos um lookup separado em `patients`
|
||||
| usando os IDs extraídos de new_values/old_values (não dá pra fazer JOIN
|
||||
| na audit_logs porque entity_id é dinâmico).
|
||||
|
|
||||
| Returns:
|
||||
| - entries: ref de objetos normalizados:
|
||||
| { id, kind, label, when, paciente, evento_id, raw }
|
||||
| onde kind ∈ { 'create' | 'move' | 'status' | 'edit' | 'delete' }
|
||||
| - loading: ref<boolean>
|
||||
| - refetch: function()
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
import { ref } from 'vue';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
|
||||
const STATUS_LABEL = {
|
||||
agendado: 'Agendado',
|
||||
realizado: 'Realizada',
|
||||
realizada: 'Realizada',
|
||||
faltou: 'Falta',
|
||||
cancelado: 'Cancelada',
|
||||
cancelada: 'Cancelada',
|
||||
remarcar: 'Remarcar',
|
||||
remarcado: 'Remarcado',
|
||||
confirmado: 'Confirmada'
|
||||
};
|
||||
|
||||
function fmtTime(iso) {
|
||||
if (!iso) return '';
|
||||
const d = new Date(iso);
|
||||
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
|
||||
}
|
||||
function fmtDateBR(iso) {
|
||||
if (!iso) return '';
|
||||
const d = new Date(iso);
|
||||
return `${String(d.getDate()).padStart(2, '0')}/${String(d.getMonth() + 1).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
// Classifica a entrada pra um dos 5 "kinds" visuais. Decisão por
|
||||
// changed_fields quando action=update — ordem importa: hora primeiro
|
||||
// (mais frequente em movimentação), depois status, depois "edit" genérico.
|
||||
function classify(row) {
|
||||
const action = String(row.action || '').toLowerCase();
|
||||
if (action === 'insert') return 'create';
|
||||
if (action === 'delete') return 'delete';
|
||||
if (action === 'update') {
|
||||
const fields = new Set(row.changed_fields || []);
|
||||
if (fields.has('inicio_em') || fields.has('fim_em')) return 'move';
|
||||
if (fields.has('status')) return 'status';
|
||||
return 'edit';
|
||||
}
|
||||
return 'edit';
|
||||
}
|
||||
|
||||
function buildLabel(kind, row) {
|
||||
const oldV = row.old_values || {};
|
||||
const newV = row.new_values || {};
|
||||
switch (kind) {
|
||||
case 'create': {
|
||||
const ini = newV.inicio_em ? `${fmtDateBR(newV.inicio_em)} ${fmtTime(newV.inicio_em)}` : '—';
|
||||
return `Criou sessão em ${ini}`;
|
||||
}
|
||||
case 'delete': {
|
||||
const ini = oldV.inicio_em ? `${fmtDateBR(oldV.inicio_em)} ${fmtTime(oldV.inicio_em)}` : '—';
|
||||
return `Removeu sessão de ${ini}`;
|
||||
}
|
||||
case 'move': {
|
||||
const from = oldV.inicio_em ? `${fmtDateBR(oldV.inicio_em)} ${fmtTime(oldV.inicio_em)}` : '—';
|
||||
const to = newV.inicio_em ? `${fmtDateBR(newV.inicio_em)} ${fmtTime(newV.inicio_em)}` : '—';
|
||||
return `Moveu ${from} → ${to}`;
|
||||
}
|
||||
case 'status': {
|
||||
const lbl = STATUS_LABEL[String(newV.status || '').toLowerCase()] || newV.status || '—';
|
||||
return `Status: ${lbl}`;
|
||||
}
|
||||
case 'edit':
|
||||
default: {
|
||||
const fields = (row.changed_fields || []).filter((f) => f !== 'updated_at');
|
||||
if (!fields.length) return 'Editou';
|
||||
return `Editou ${fields.join(', ')}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve o ID do paciente a partir de new_values/old_values (delete usa OLD).
|
||||
function extractPatientId(row) {
|
||||
return row.new_values?.patient_id || row.old_values?.patient_id || null;
|
||||
}
|
||||
|
||||
export function useMelissaAgendaHistorico(opts = {}) {
|
||||
const limit = opts.limit ?? 20;
|
||||
const days = opts.days ?? 7;
|
||||
|
||||
const tenantStore = useTenantStore();
|
||||
const entries = ref([]);
|
||||
const loading = ref(false);
|
||||
const error = ref('');
|
||||
|
||||
async function _ensureUid() {
|
||||
const { data: ses } = await supabase.auth.getSession();
|
||||
if (ses?.session?.user?.id) return ses.session.user.id;
|
||||
const { data, error: err } = await supabase.auth.getUser();
|
||||
if (err) return null;
|
||||
return data?.user?.id || null;
|
||||
}
|
||||
|
||||
async function refetch() {
|
||||
loading.value = true;
|
||||
error.value = '';
|
||||
try {
|
||||
const userId = await _ensureUid();
|
||||
if (typeof tenantStore.ensureLoaded === 'function') {
|
||||
await tenantStore.ensureLoaded();
|
||||
}
|
||||
const tid = tenantStore.activeTenantId || tenantStore.tenantId || null;
|
||||
if (!userId || !tid) {
|
||||
entries.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
|
||||
|
||||
const { data: rows, error: err } = await supabase
|
||||
.from('audit_logs')
|
||||
.select('id, action, entity_id, changed_fields, old_values, new_values, created_at, user_id, tenant_id')
|
||||
.eq('entity_type', 'agenda_eventos')
|
||||
.eq('user_id', userId)
|
||||
.eq('tenant_id', tid)
|
||||
.gte('created_at', since)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(limit);
|
||||
|
||||
if (err) throw err;
|
||||
|
||||
const list = rows || [];
|
||||
|
||||
// Resolve nomes dos pacientes em uma única query.
|
||||
const patientIds = [...new Set(list.map(extractPatientId).filter(Boolean))];
|
||||
const patientMap = new Map();
|
||||
if (patientIds.length) {
|
||||
const { data: pats } = await supabase
|
||||
.from('patients')
|
||||
.select('id, nome_completo')
|
||||
.in('id', patientIds);
|
||||
for (const p of pats || []) patientMap.set(p.id, p.nome_completo);
|
||||
}
|
||||
|
||||
entries.value = list.map((r) => {
|
||||
const kind = classify(r);
|
||||
const pid = extractPatientId(r);
|
||||
return {
|
||||
id: r.id,
|
||||
kind,
|
||||
label: buildLabel(kind, r),
|
||||
when: r.created_at,
|
||||
paciente: pid ? (patientMap.get(pid) || '') : '',
|
||||
evento_id: r.entity_id,
|
||||
raw: r
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
error.value = e?.message || 'Falha ao carregar histórico';
|
||||
entries.value = [];
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('[useMelissaAgendaHistorico]', e);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return { entries, loading, error, refetch };
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Arquivo: src/layout/melissa/composables/useMelissaDockPins.js
|
||||
| Data: 2026-05-04
|
||||
|
|
||||
| Pins dinâmicos do dock Melissa — modelo híbrido:
|
||||
|
|
||||
| - PINNED (manual, max 4): user fixa via menu de contexto, persiste
|
||||
| entre sessões em localStorage. Sempre visíveis, ordenados por ordem
|
||||
| de fixação.
|
||||
| - RECENT (MRU automático, max 3): toda vez que o user abre uma seção
|
||||
| que NÃO é built-in (agenda/conversas) e NÃO tá pinned, vira o pin
|
||||
| temporário mais recente, empurrando os mais antigos pra fora.
|
||||
|
|
||||
| Persistência: localStorage com chave `melissa.dock.pins.v1`. Salva só
|
||||
| slugs de seção (string), nada de dado clínico — LGPD-safe. Singleton via
|
||||
| módulo (estado fora da função) pra todas as instâncias compartilharem.
|
||||
|
|
||||
| Builtin (não-pinnável, não-recente): agenda, conversas — esses já têm
|
||||
| pin permanente próprio no template (.dock-pin com hardcode).
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
const STORAGE_KEY = 'melissa.dock.pins.v1';
|
||||
const MAX_PINNED = 4;
|
||||
const MAX_RECENT = 3;
|
||||
const BUILTIN_SLUGS = new Set(['agenda', 'conversas']);
|
||||
|
||||
// Estado singleton compartilhado entre todas as instâncias.
|
||||
const pinned = ref([]);
|
||||
const recent = ref([]);
|
||||
let _hydrated = false;
|
||||
|
||||
function _hydrate() {
|
||||
if (_hydrated) return;
|
||||
_hydrated = true;
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return;
|
||||
const parsed = JSON.parse(raw);
|
||||
if (Array.isArray(parsed?.pinned)) {
|
||||
pinned.value = parsed.pinned.filter((s) => typeof s === 'string').slice(0, MAX_PINNED);
|
||||
}
|
||||
if (Array.isArray(parsed?.recent)) {
|
||||
recent.value = parsed.recent.filter((s) => typeof s === 'string').slice(0, MAX_RECENT);
|
||||
}
|
||||
} catch { /* localStorage corrompido — ignora silenciosamente */ }
|
||||
}
|
||||
|
||||
function _persist() {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify({
|
||||
pinned: pinned.value,
|
||||
recent: recent.value
|
||||
}));
|
||||
} catch { /* quota excedida ou storage desabilitado — ok, em memória */ }
|
||||
}
|
||||
|
||||
let _persistWatcherActive = false;
|
||||
function _ensurePersistWatcher() {
|
||||
if (_persistWatcherActive) return;
|
||||
_persistWatcherActive = true;
|
||||
watch([pinned, recent], _persist, { deep: true });
|
||||
}
|
||||
|
||||
export function useMelissaDockPins() {
|
||||
_hydrate();
|
||||
_ensurePersistWatcher();
|
||||
|
||||
function isBuiltin(slug) {
|
||||
return BUILTIN_SLUGS.has(slug);
|
||||
}
|
||||
function isPinned(slug) {
|
||||
return pinned.value.includes(slug);
|
||||
}
|
||||
function isRecent(slug) {
|
||||
return recent.value.includes(slug);
|
||||
}
|
||||
|
||||
// Chamado quando o user abre uma seção. Builtins e já-pinned não viram
|
||||
// recent (não duplica). Mais recente entra no topo, expulsa o mais
|
||||
// antigo se passar do limite.
|
||||
function pushRecent(slug) {
|
||||
if (!slug || isBuiltin(slug) || isPinned(slug)) return;
|
||||
recent.value = [slug, ...recent.value.filter((s) => s !== slug)].slice(0, MAX_RECENT);
|
||||
}
|
||||
|
||||
// Move um slug de "recent" pra "pinned" (ou cria pinned direto).
|
||||
// Retorna { ok, reason } — reason='full' quando já tem 4 pinned.
|
||||
function pin(slug) {
|
||||
if (!slug || isBuiltin(slug)) return { ok: false, reason: 'builtin' };
|
||||
if (isPinned(slug)) return { ok: true, reason: 'already' };
|
||||
if (pinned.value.length >= MAX_PINNED) return { ok: false, reason: 'full' };
|
||||
recent.value = recent.value.filter((s) => s !== slug);
|
||||
pinned.value = [...pinned.value, slug];
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
// Tira de "pinned" — não volta automaticamente pra recent (o user
|
||||
// explicitamente desafixou). Próxima abertura da seção vai pra recent
|
||||
// pelo fluxo normal de pushRecent.
|
||||
function unpin(slug) {
|
||||
pinned.value = pinned.value.filter((s) => s !== slug);
|
||||
}
|
||||
|
||||
// Remove completamente (de ambas as listas). Usado pelo "Remover" do menu.
|
||||
function remove(slug) {
|
||||
pinned.value = pinned.value.filter((s) => s !== slug);
|
||||
recent.value = recent.value.filter((s) => s !== slug);
|
||||
}
|
||||
|
||||
function clearAll() {
|
||||
pinned.value = [];
|
||||
recent.value = [];
|
||||
}
|
||||
|
||||
return {
|
||||
pinned,
|
||||
recent,
|
||||
isBuiltin,
|
||||
isPinned,
|
||||
isRecent,
|
||||
pushRecent,
|
||||
pin,
|
||||
unpin,
|
||||
remove,
|
||||
clearAll,
|
||||
MAX_PINNED,
|
||||
MAX_RECENT
|
||||
};
|
||||
}
|
||||
@@ -26,6 +26,7 @@
|
||||
import { ref, watch, onMounted, computed } from 'vue';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
import { useMelissaCacheStore, MELISSA_CACHE_TTL } from '@/stores/melissaCacheStore';
|
||||
|
||||
// ── Cores por tipo/status (consistente com o resto do Melissa) ──
|
||||
function pickColor(tipo, status) {
|
||||
@@ -319,17 +320,49 @@ export function useMelissaTodasSessoesPaciente() {
|
||||
}
|
||||
|
||||
// ── COMPOSABLE 2: apenas hoje (MelissaLayout) ──────────────────
|
||||
export function useMelissaEventosHoje() {
|
||||
// opts: { autoFetch=true } — passar false pra adiar o fetch inicial
|
||||
// (MelissaLayout faz isso quando a URL inicial já tem uma seção, pra
|
||||
// não competir com o fetch da seção que vai cobrir o resumo).
|
||||
export function useMelissaEventosHoje(opts = {}) {
|
||||
const autoFetch = opts.autoFetch !== false;
|
||||
const cache = useMelissaCacheStore();
|
||||
const eventos = ref([]);
|
||||
const loading = ref(false);
|
||||
const error = ref(null);
|
||||
|
||||
async function fetch() {
|
||||
async function _doFetch(cacheKey) {
|
||||
const { start, end } = rangeHoje();
|
||||
const data = await _fetchRange(start, end);
|
||||
cache.set('eventosHoje', data, cacheKey);
|
||||
eventos.value = data;
|
||||
return data;
|
||||
}
|
||||
|
||||
// useCache=true (boot/auto): stale-while-revalidate.
|
||||
// useCache=false (refetch pós-mutation: status sessão, etc): força.
|
||||
async function _fetch({ useCache = true } = {}) {
|
||||
const today = new Date();
|
||||
// Cache key amarra ao dia — depois de 00:00 vira automaticamente outro slot.
|
||||
const cacheKey = `${today.getFullYear()}-${today.getMonth()}-${today.getDate()}`;
|
||||
|
||||
if (useCache) {
|
||||
const cached = cache.get('eventosHoje', cacheKey, MELISSA_CACHE_TTL.eventosHoje);
|
||||
if (cached) {
|
||||
eventos.value = cached;
|
||||
_doFetch(cacheKey).catch((e) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('[useMelissaEventosHoje] revalidate', e);
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
cache.invalidate('eventosHoje');
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const { start, end } = rangeHoje();
|
||||
eventos.value = await _fetchRange(start, end);
|
||||
await _doFetch(cacheKey);
|
||||
} catch (e) {
|
||||
error.value = e?.message || 'Erro ao carregar agenda';
|
||||
eventos.value = [];
|
||||
@@ -340,7 +373,15 @@ export function useMelissaEventosHoje() {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(fetch);
|
||||
if (autoFetch) onMounted(() => _fetch({ useCache: true }));
|
||||
|
||||
return { eventos, loading, error, refetch: fetch };
|
||||
return {
|
||||
eventos,
|
||||
loading,
|
||||
error,
|
||||
// refetch força query nova (após status update etc).
|
||||
refetch: () => _fetch({ useCache: false }),
|
||||
// fetchCached é stale-while-revalidate (idle/defer).
|
||||
fetchCached: () => _fetch({ useCache: true })
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user