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
+164 -9
View File
@@ -11,7 +11,7 @@
* Lógica idêntica à RelatoriosPage (query agenda_eventos + grouping
* isoWeek/isoMonth + Chart.js).
*/
import { ref, computed, watch, onMounted } from 'vue';
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue';
import { supabase } from '@/lib/supabase/client';
import { useTenantStore } from '@/stores/tenantStore';
// Chart/DataTable/Column/Tag/Skeleton: auto via PrimeVueResolver
@@ -19,6 +19,17 @@ import { useTenantStore } from '@/stores/tenantStore';
const emit = defineEmits(['close']);
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; }
// ── Período ────────────────────────────────────────────
const PERIOD_OPTIONS = [
{ key: 'week', label: 'Esta semana', icon: 'pi pi-calendar' },
@@ -245,12 +256,52 @@ watch(selectedPeriod, () => {
loadSessions();
});
onMounted(loadSessions);
onMounted(async () => {
if (typeof window !== 'undefined' && window.matchMedia) {
_mqMobile = window.matchMedia('(max-width: 1023px)');
isMobile.value = _mqMobile.matches;
_mqMobile.addEventListener('change', _onMqMobileChange);
}
// Garante tenant carregado antes de carregar sessoes — sem isso, a
// primeira render via URL direta pode pegar tenantStore vazio.
if (typeof tenantStore.ensureLoaded === 'function') {
await tenantStore.ensureLoaded();
}
await loadSessions();
});
onBeforeUnmount(() => {
if (_mqMobile) _mqMobile.removeEventListener('change', _onMqMobileChange);
});
</script>
<template>
<!-- Drawer host (mobile) -->
<aside
class="mr-mobile-drawer"
:class="{ 'is-open': drawerOpen }"
v-show="isMobile"
aria-label="Estatísticas e filtros"
>
<div id="mr-mobile-drawer-target" class="mr-mobile-drawer__scroll" />
</aside>
<Transition name="mr-drawer-fade">
<div
v-if="isMobile && drawerOpen"
class="mr-mobile-drawer__backdrop"
@click="fecharDrawer"
/>
</Transition>
<section class="mr-page">
<header class="mr-page__head">
<button
class="mr-menu-btn mr-menu-btn--mobile-only"
v-tooltip.bottom="'Estatísticas & filtros'"
@click="toggleDrawer"
>
<i class="pi pi-bars" />
<span>Menu Relatórios</span>
</button>
<div class="mr-page__title">
<i class="pi pi-chart-bar mr-page__title-icon" />
<span>Relatórios</span>
@@ -283,6 +334,7 @@ onMounted(loadSessions);
<div class="mr-body">
<!-- COL 1: Stats + filtros -->
<Teleport to="#mr-mobile-drawer-target" :disabled="!isMobile">
<aside class="mr-side">
<div class="mr-side__scroll">
<!-- Stats -->
@@ -367,6 +419,7 @@ onMounted(loadSessions);
</div>
</Transition>
</aside>
</Teleport>
<!-- COL 2: Gráfico + Tabela -->
<div class="mr-main">
@@ -998,17 +1051,119 @@ onMounted(loadSessions);
gap: 6px;
}
/* ─── Botão Menu mobile ─── */
.mr-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;
}
.mr-menu-btn:hover {
background: color-mix(in srgb, var(--m-accent) 88%, white);
transform: translateY(-1px);
}
.mr-menu-btn > i { font-size: 0.85rem; }
/* ─── Drawer mobile ─── */
.mr-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;
}
.mr-mobile-drawer.is-open { transform: translateX(0); }
.mr-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;
}
.mr-mobile-drawer__scroll::-webkit-scrollbar { width: 5px; }
.mr-mobile-drawer__scroll::-webkit-scrollbar-thumb {
background: var(--m-border-strong);
border-radius: 3px;
}
.mr-mobile-drawer__scroll .mr-side {
width: 100%;
flex: 1;
min-height: 0;
overflow: visible;
padding: 0;
background: transparent;
border-right: none;
display: flex;
flex-direction: column;
}
.mr-mobile-drawer__scroll .mr-side__scroll {
flex: none;
min-height: 0;
overflow: visible;
display: flex;
flex-direction: column;
gap: 12px;
}
.mr-mobile-drawer__scroll .mr-w--side {
margin: 0;
}
.mr-mobile-drawer__scroll .mr-w--side:last-of-type { margin-bottom: 0; }
.mr-mobile-drawer__scroll .mr-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;
}
.mr-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;
}
.mr-drawer-fade-enter-active,
.mr-drawer-fade-leave-active { transition: opacity 200ms ease; }
.mr-drawer-fade-enter-from,
.mr-drawer-fade-leave-to { opacity: 0; }
/* Mobile */
@media (max-width: 1023px) {
.mr-body { flex-direction: column; padding: 0; }
.mr-side {
width: 100%;
max-height: 50vh;
border-right: none;
border-bottom: 1px solid var(--m-border);
}
.mr-main { padding: 8px; }
.mr-main { width: 100%; padding: 8px; }
.mr-page__title > span:first-of-type { display: none; }
.mr-menu-btn--mobile-only { display: inline-flex; }
.mr-stats { grid-template-columns: repeat(3, 1fr); }
}
</style>