Drawer mobile + footer colado + Menu nomeado + tenant ensureLoaded

Tres ajustes globais nas Melissa Pages com sidebar:

1) FOOTER "Limpar filtros" colado no bottom do drawer mobile

   Problema: o sticky bottom precisa que algum container parent
   tenha altura definida e overflow. No drawer, o `.xx-side` tinha
   `height: auto` — entao o footer ficava no fluxo natural (logo
   apos os cards) mesmo com pouco conteudo, em vez de empurrado pro
   bottom do drawer.

   Fix: `.xx-mobile-drawer__scroll .xx-side` ganha
   `flex: 1; min-height: 0; display: flex; flex-direction: column`
   pra ocupar altura disponivel; o `.xx-side__footer` ganha
   `margin: auto -12px -24px` (margin-top: auto empurra pro fim).
   Sticky bottom continua pro caso de scroll com muito conteudo.

   Aplicado em: Compromissos, Grupos, Tags, Medicos, Conversas,
   Recorrencias, Pacientes (caso especial — separa .mp-side de
   .mp-quick), Cadastros Recebidos, FinanceiroLancamentos.

2) DRAWER MOBILE adicionado em Notificacoes, Documentos e
   Relatorios (estavam com sidebar virando topo via max-height
   50vh — faltava o pattern oficial das demais Melissa Pages).

   Pattern aplicado:
   - Aside host com id="<prefix>-mobile-drawer-target" + Transition
     backdrop com fade
   - Botao "Menu <Secao>" no header (esquerda do titulo)
   - <Teleport :disabled="!isMobile"> envolvendo a sidebar
   - Script: drawerOpen + isMobile + matchMedia listener registrado
     no onMounted, removido no onBeforeUnmount
   - CSS completo: .xx-mobile-drawer (fixed, transform translateX),
     __scroll (overflow + padding), __backdrop (rgba 0.45 + blur),
     overrides quando teleportada (sidebar perde bg/border-right,
     footer vira sticky bottom com margin-top auto)

3) Botao "Menu" passa a ter sufixo da pagina:
   - "Menu Lancamentos" (FinanceiroLancamentos)
   - "Menu Notificacoes" (Notificacoes)
   - "Menu Documentos" (Documentos)
   - "Menu Relatorios" (Relatorios)
   - "Menu Agendamentos" (AgendamentosRecebidos — corrigido tambem)

4) Bug de "lista vazia ao carregar via URL direto":

   FinanceiroLancamentos e Relatorios usam composables que dependem
   de tenantStore.activeTenantId. Quando aberta direto via URL
   (sem navegar pelo menu), o tenantStore pode nao estar inicializado
   ainda — entao fetchRecords() / loadSessions() retornam vazio.

   Fix: adicionar `await tenantStore.ensureLoaded()` no onMounted
   antes do fetch. Ja era pattern usado em outras Melissa Pages
   (Compromissos, etc).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Leonardo
2026-05-06 10:40:07 -03:00
parent 532204708e
commit 48bf2726a5
13 changed files with 698 additions and 49 deletions
+160 -8
View File
@@ -14,7 +14,7 @@
* - outras com deeplink → router.push(deeplink)
* - todas marcam como lida automaticamente se ainda não estiverem.
*/
import { ref, computed, onMounted, watch } from 'vue';
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue';
import { useRouter } from 'vue-router';
import { useToast } from 'primevue/usetoast';
import { useConfirm } from 'primevue/useconfirm';
@@ -35,6 +35,17 @@ const notifStore = useNotificationStore();
const conversationDrawer = useConversationDrawerStore();
const tenantStore = useTenantStore();
// ── Breakpoints + drawer mobile ────────────────────────
const drawerOpen = ref(false);
const isMobile = ref(false);
let _mqMobile = null;
function _onMqMobileChange(e) {
isMobile.value = e.matches;
if (!e.matches) drawerOpen.value = false;
}
function toggleDrawer() { drawerOpen.value = !drawerOpen.value; }
function fecharDrawer() { drawerOpen.value = false; }
// ── Estado ─────────────────────────────────────────────
const ownerId = ref(null);
const items = ref([]);
@@ -306,15 +317,48 @@ function initials(item) {
watch(() => statusFilter.value, () => { /* noop — filter applies via computed */ });
onMounted(() => {
if (typeof window !== 'undefined' && window.matchMedia) {
_mqMobile = window.matchMedia('(max-width: 1023px)');
isMobile.value = _mqMobile.matches;
_mqMobile.addEventListener('change', _onMqMobileChange);
}
load();
});
onBeforeUnmount(() => {
if (_mqMobile) _mqMobile.removeEventListener('change', _onMqMobileChange);
});
</script>
<template>
<ConfirmDialog />
<!-- Drawer host (mobile) -->
<aside
class="mn-mobile-drawer"
:class="{ 'is-open': drawerOpen }"
v-show="isMobile"
aria-label="Estatísticas e filtros"
>
<div id="mn-mobile-drawer-target" class="mn-mobile-drawer__scroll" />
</aside>
<Transition name="mn-drawer-fade">
<div
v-if="isMobile && drawerOpen"
class="mn-mobile-drawer__backdrop"
@click="fecharDrawer"
/>
</Transition>
<section class="mn-page">
<header class="mn-page__head">
<button
class="mn-menu-btn mn-menu-btn--mobile-only"
v-tooltip.bottom="'Estatísticas & filtros'"
@click="toggleDrawer"
>
<i class="pi pi-bars" />
<span>Menu Notificações</span>
</button>
<div class="mn-page__title">
<i class="pi pi-bell mn-page__title-icon" />
<span>Notificações</span>
@@ -361,6 +405,7 @@ onMounted(() => {
<div class="mn-body">
<!-- COL 1: Stats + filtros -->
<Teleport to="#mn-mobile-drawer-target" :disabled="!isMobile">
<aside class="mn-side">
<div class="mn-side__scroll">
<!-- Stats -->
@@ -448,6 +493,7 @@ onMounted(() => {
</div>
</Transition>
</aside>
</Teleport>
<!-- COL 2: Toolbar + Lista -->
<div class="mn-main">
@@ -1294,17 +1340,123 @@ onMounted(() => {
}
.mn-row__btn > i { font-size: 0.78rem; }
/* ─── Botão Menu mobile (abre drawer com sidebar) ─── */
.mn-menu-btn {
display: none;
height: 32px;
align-items: center;
gap: 6px;
flex-shrink: 0;
background: var(--m-accent);
border: 1px solid var(--m-accent);
color: white;
padding: 0 11px;
border-radius: 9px;
cursor: pointer;
font-family: inherit;
font-size: 0.78rem;
font-weight: 600;
transition: background-color 140ms ease, transform 140ms ease;
}
.mn-menu-btn:hover {
background: color-mix(in srgb, var(--m-accent) 88%, white);
transform: translateY(-1px);
}
.mn-menu-btn > i { font-size: 0.85rem; }
/* ─── Drawer mobile (Teleport target) ─── */
.mn-mobile-drawer {
position: fixed;
top: 0;
left: 0;
height: 100vh;
height: 100dvh;
width: min(360px, 88vw);
z-index: 80;
background: var(--m-bg-medium);
backdrop-filter: blur(28px) saturate(160%);
-webkit-backdrop-filter: blur(28px) saturate(160%);
border-right: 1px solid var(--m-border);
transform: translateX(-100%);
transition: transform 250ms cubic-bezier(0.4, 0, 0.2, 1);
color: var(--m-text);
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
}
.mn-mobile-drawer.is-open { transform: translateX(0); }
.mn-mobile-drawer__scroll {
height: 100%;
overflow-y: auto;
overflow-x: hidden;
padding: 12px 12px 24px;
display: flex;
flex-direction: column;
gap: 12px;
scrollbar-width: thin;
scrollbar-color: var(--m-border-strong) transparent;
}
.mn-mobile-drawer__scroll::-webkit-scrollbar { width: 5px; }
.mn-mobile-drawer__scroll::-webkit-scrollbar-thumb {
background: var(--m-border-strong);
border-radius: 3px;
}
/* Sidebar teleportada — ocupa altura disponivel pra empurrar o footer
pro bottom (margin: auto faz o trabalho). */
.mn-mobile-drawer__scroll .mn-side {
width: 100%;
flex: 1;
min-height: 0;
overflow: visible;
padding: 0;
background: transparent;
border-right: none;
display: flex;
flex-direction: column;
}
.mn-mobile-drawer__scroll .mn-side__scroll {
flex: none;
min-height: 0;
overflow: visible;
display: flex;
flex-direction: column;
gap: 12px;
}
.mn-mobile-drawer__scroll .mn-w--side {
margin: 0;
}
.mn-mobile-drawer__scroll .mn-w--side:last-of-type { margin-bottom: 0; }
.mn-mobile-drawer__scroll .mn-side__footer {
position: sticky;
bottom: 0;
margin: auto -12px -24px;
padding: 12px;
background: var(--m-bg-medium);
border-top: 1px solid var(--m-border);
backdrop-filter: blur(24px) saturate(160%);
-webkit-backdrop-filter: blur(24px) saturate(160%);
z-index: 5;
}
.mn-mobile-drawer__backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.45);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
z-index: 79;
}
.mn-drawer-fade-enter-active,
.mn-drawer-fade-leave-active { transition: opacity 200ms ease; }
.mn-drawer-fade-enter-from,
.mn-drawer-fade-leave-to { opacity: 0; }
/* ─── Mobile (<1024px) ─── */
@media (max-width: 1023px) {
/* Sidebar saiu pro drawer via Teleport — body fica só com .mn-main. */
.mn-body { flex-direction: column; padding: 0; }
.mn-side {
width: 100%;
max-height: 50vh;
border-right: none;
border-bottom: 1px solid var(--m-border);
}
.mn-main { padding: 8px; }
.mn-main { width: 100%; padding: 8px; }
.mn-page__title > span:first-of-type { display: none; }
.mn-menu-btn--mobile-only { display: inline-flex; }
.mn-act-btn span { display: none; }
.mn-act-btn { width: 32px; padding: 0; justify-content: center; }
.mn-row__actions { opacity: 1; }