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>
135 lines
4.5 KiB
JavaScript
135 lines
4.5 KiB
JavaScript
/*
|
|
|--------------------------------------------------------------------------
|
|
| 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
|
|
};
|
|
}
|