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:
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user