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:
@@ -509,7 +509,7 @@ onBeforeUnmount(() => {
|
|||||||
@click="toggleDrawer"
|
@click="toggleDrawer"
|
||||||
>
|
>
|
||||||
<i class="pi pi-bars" />
|
<i class="pi pi-bars" />
|
||||||
<span>Menu</span>
|
<span>Menu Agendamentos</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="mar-page__title">
|
<div class="mar-page__title">
|
||||||
<i class="pi pi-inbox mar-page__title-icon" />
|
<i class="pi pi-inbox mar-page__title-icon" />
|
||||||
|
|||||||
@@ -1846,11 +1846,14 @@ onBeforeUnmount(() => {
|
|||||||
precisa do bg/border-right que tem em desktop. */
|
precisa do bg/border-right que tem em desktop. */
|
||||||
.mcr-mobile-drawer__scroll .mcr-side {
|
.mcr-mobile-drawer__scroll .mcr-side {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border-right: none;
|
border-right: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
.mcr-mobile-drawer__scroll .mcr-w--side {
|
.mcr-mobile-drawer__scroll .mcr-w--side {
|
||||||
margin: 0 0 12px;
|
margin: 0 0 12px;
|
||||||
|
|||||||
@@ -1862,11 +1862,14 @@ async function onDelete(c) {
|
|||||||
do drawer via position: sticky. */
|
do drawer via position: sticky. */
|
||||||
.mc-mobile-drawer__scroll .mc-side {
|
.mc-mobile-drawer__scroll .mc-side {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border-right: none;
|
border-right: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
.mc-mobile-drawer__scroll .mc-side__scroll {
|
.mc-mobile-drawer__scroll .mc-side__scroll {
|
||||||
flex: none;
|
flex: none;
|
||||||
@@ -1878,7 +1881,7 @@ async function onDelete(c) {
|
|||||||
.mc-mobile-drawer__scroll .mc-side__footer {
|
.mc-mobile-drawer__scroll .mc-side__footer {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
margin: 8px -12px -24px; /* compensa o padding do drawer pra ficar de borda a borda */
|
margin: auto -12px -24px; /* compensa o padding do drawer pra ficar de borda a borda */
|
||||||
background: var(--m-bg-medium);
|
background: var(--m-bg-medium);
|
||||||
border-top: 1px solid var(--m-border);
|
border-top: 1px solid var(--m-border);
|
||||||
backdrop-filter: blur(24px) saturate(160%);
|
backdrop-filter: blur(24px) saturate(160%);
|
||||||
|
|||||||
@@ -1210,11 +1210,14 @@ watch(() => tenantStore.activeTenantId, async () => {
|
|||||||
tem padding). Footer vira sticky no bottom do drawer. */
|
tem padding). Footer vira sticky no bottom do drawer. */
|
||||||
.mw-mobile-drawer__scroll .mw-side {
|
.mw-mobile-drawer__scroll .mw-side {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border-right: none;
|
border-right: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
.mw-mobile-drawer__scroll .mw-side__scroll {
|
.mw-mobile-drawer__scroll .mw-side__scroll {
|
||||||
flex: none;
|
flex: none;
|
||||||
@@ -1232,7 +1235,7 @@ watch(() => tenantStore.activeTenantId, async () => {
|
|||||||
.mw-mobile-drawer__scroll .mw-side__footer {
|
.mw-mobile-drawer__scroll .mw-side__footer {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
margin: 8px -12px -24px;
|
margin: auto -12px -24px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
background: var(--m-bg-medium);
|
background: var(--m-bg-medium);
|
||||||
border-top: 1px solid var(--m-border);
|
border-top: 1px solid var(--m-border);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
* abrir um paciente específico via /melissa/pacientes (atualmente
|
* abrir um paciente específico via /melissa/pacientes (atualmente
|
||||||
* desabilitados aqui). Lista é read-only nesse contexto.
|
* desabilitados aqui). Lista é read-only nesse contexto.
|
||||||
*/
|
*/
|
||||||
import { ref, computed, onMounted, watch } from 'vue';
|
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue';
|
||||||
import { useToast } from 'primevue/usetoast';
|
import { useToast } from 'primevue/usetoast';
|
||||||
import { useConfirm } from 'primevue/useconfirm';
|
import { useConfirm } from 'primevue/useconfirm';
|
||||||
|
|
||||||
@@ -28,6 +28,17 @@ const emit = defineEmits(['close']);
|
|||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
|
|
||||||
|
// ── 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; }
|
||||||
|
|
||||||
// patientId = null → modo "todos os pacientes" (read-only).
|
// patientId = null → modo "todos os pacientes" (read-only).
|
||||||
const patientId = computed(() => null);
|
const patientId = computed(() => null);
|
||||||
|
|
||||||
@@ -110,15 +121,48 @@ function onSign(doc) {
|
|||||||
watch(filters, () => fetchDocuments(), { deep: true });
|
watch(filters, () => fetchDocuments(), { deep: true });
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
if (typeof window !== 'undefined' && window.matchMedia) {
|
||||||
|
_mqMobile = window.matchMedia('(max-width: 1023px)');
|
||||||
|
isMobile.value = _mqMobile.matches;
|
||||||
|
_mqMobile.addEventListener('change', _onMqMobileChange);
|
||||||
|
}
|
||||||
await Promise.all([fetchDocuments(), fetchUsedTags()]);
|
await Promise.all([fetchDocuments(), fetchUsedTags()]);
|
||||||
});
|
});
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (_mqMobile) _mqMobile.removeEventListener('change', _onMqMobileChange);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ConfirmDialog />
|
<ConfirmDialog />
|
||||||
|
|
||||||
|
<!-- Drawer host (mobile) -->
|
||||||
|
<aside
|
||||||
|
class="md-mobile-drawer"
|
||||||
|
:class="{ 'is-open': drawerOpen }"
|
||||||
|
v-show="isMobile"
|
||||||
|
aria-label="Estatísticas e filtros"
|
||||||
|
>
|
||||||
|
<div id="md-mobile-drawer-target" class="md-mobile-drawer__scroll" />
|
||||||
|
</aside>
|
||||||
|
<Transition name="md-drawer-fade">
|
||||||
|
<div
|
||||||
|
v-if="isMobile && drawerOpen"
|
||||||
|
class="md-mobile-drawer__backdrop"
|
||||||
|
@click="fecharDrawer"
|
||||||
|
/>
|
||||||
|
</Transition>
|
||||||
|
|
||||||
<section class="md-page">
|
<section class="md-page">
|
||||||
<header class="md-page__head">
|
<header class="md-page__head">
|
||||||
|
<button
|
||||||
|
class="md-menu-btn md-menu-btn--mobile-only"
|
||||||
|
v-tooltip.bottom="'Estatísticas & filtros'"
|
||||||
|
@click="toggleDrawer"
|
||||||
|
>
|
||||||
|
<i class="pi pi-bars" />
|
||||||
|
<span>Menu Documentos</span>
|
||||||
|
</button>
|
||||||
<div class="md-page__title">
|
<div class="md-page__title">
|
||||||
<i class="pi pi-file md-page__title-icon" />
|
<i class="pi pi-file md-page__title-icon" />
|
||||||
<span>Documentos</span>
|
<span>Documentos</span>
|
||||||
@@ -151,6 +195,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<div class="md-body">
|
<div class="md-body">
|
||||||
<!-- ═══ COL 1: Stats + filtros ═══ -->
|
<!-- ═══ COL 1: Stats + filtros ═══ -->
|
||||||
|
<Teleport to="#md-mobile-drawer-target" :disabled="!isMobile">
|
||||||
<aside class="md-side">
|
<aside class="md-side">
|
||||||
<div class="md-side__scroll">
|
<div class="md-side__scroll">
|
||||||
<!-- Stats -->
|
<!-- Stats -->
|
||||||
@@ -236,6 +281,7 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
</aside>
|
</aside>
|
||||||
|
</Teleport>
|
||||||
|
|
||||||
<!-- ═══ COL 2: Lista ═══ -->
|
<!-- ═══ COL 2: Lista ═══ -->
|
||||||
<div class="md-main">
|
<div class="md-main">
|
||||||
@@ -688,16 +734,118 @@ onMounted(async () => {
|
|||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── Botão Menu mobile (abre drawer com sidebar) ─── */
|
||||||
|
.md-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;
|
||||||
|
}
|
||||||
|
.md-menu-btn:hover {
|
||||||
|
background: color-mix(in srgb, var(--m-accent) 88%, white);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
.md-menu-btn > i { font-size: 0.85rem; }
|
||||||
|
|
||||||
|
/* ─── Drawer mobile ─── */
|
||||||
|
.md-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;
|
||||||
|
}
|
||||||
|
.md-mobile-drawer.is-open { transform: translateX(0); }
|
||||||
|
.md-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;
|
||||||
|
}
|
||||||
|
.md-mobile-drawer__scroll::-webkit-scrollbar { width: 5px; }
|
||||||
|
.md-mobile-drawer__scroll::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--m-border-strong);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-mobile-drawer__scroll .md-side {
|
||||||
|
width: 100%;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: visible;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
border-right: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.md-mobile-drawer__scroll .md-side__scroll {
|
||||||
|
flex: none;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: visible;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.md-mobile-drawer__scroll .md-w--side {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.md-mobile-drawer__scroll .md-w--side:last-of-type { margin-bottom: 0; }
|
||||||
|
.md-mobile-drawer__scroll .md-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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-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;
|
||||||
|
}
|
||||||
|
.md-drawer-fade-enter-active,
|
||||||
|
.md-drawer-fade-leave-active { transition: opacity 200ms ease; }
|
||||||
|
.md-drawer-fade-enter-from,
|
||||||
|
.md-drawer-fade-leave-to { opacity: 0; }
|
||||||
|
|
||||||
/* ─── Mobile (<1024px) ─── */
|
/* ─── Mobile (<1024px) ─── */
|
||||||
@media (max-width: 1023px) {
|
@media (max-width: 1023px) {
|
||||||
.md-body { flex-direction: column; padding: 0; }
|
.md-body { flex-direction: column; padding: 0; }
|
||||||
.md-side {
|
.md-main { width: 100%; padding: 8px; }
|
||||||
width: 100%;
|
|
||||||
max-height: 50vh;
|
|
||||||
border-right: none;
|
|
||||||
border-bottom: 1px solid var(--m-border);
|
|
||||||
}
|
|
||||||
.md-main { padding: 8px; }
|
|
||||||
.md-page__title > span:first-of-type { display: none; }
|
.md-page__title > span:first-of-type { display: none; }
|
||||||
|
.md-menu-btn--mobile-only { display: inline-flex; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
* RPCs + dialogs registrar pagamento e lançamento manual). Só o chrome
|
* RPCs + dialogs registrar pagamento e lançamento manual). Só o chrome
|
||||||
* muda pra eliminar o triplo-header.
|
* muda pra eliminar o triplo-header.
|
||||||
*/
|
*/
|
||||||
import { ref, computed, watch, onMounted } from 'vue';
|
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue';
|
||||||
import { useToast } from 'primevue/usetoast';
|
import { useToast } from 'primevue/usetoast';
|
||||||
import { useConfirm } from 'primevue/useconfirm';
|
import { useConfirm } from 'primevue/useconfirm';
|
||||||
import { supabase } from '@/lib/supabase/client';
|
import { supabase } from '@/lib/supabase/client';
|
||||||
@@ -27,6 +27,17 @@ const toast = useToast();
|
|||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
const tenantStore = useTenantStore();
|
const tenantStore = useTenantStore();
|
||||||
|
|
||||||
|
// ── Breakpoints + drawer mobile (mesmo pattern das demais Melissa Pages) ──
|
||||||
|
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; }
|
||||||
|
|
||||||
// ── Helpers de formatação ─────────────────────────────
|
// ── Helpers de formatação ─────────────────────────────
|
||||||
const _brl = new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' });
|
const _brl = new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' });
|
||||||
function fmtBRL(v) { return _brl.format(v ?? 0); }
|
function fmtBRL(v) { return _brl.format(v ?? 0); }
|
||||||
@@ -258,15 +269,54 @@ async function saveManualRecord() {
|
|||||||
|
|
||||||
// ── Lifecycle ─────────────────────────────────────────
|
// ── Lifecycle ─────────────────────────────────────────
|
||||||
onMounted(async () => {
|
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 fetch — sem isso, o useFinancialRecords
|
||||||
|
// retorna vazio na primeira render direta via URL (bug: lista vazia até
|
||||||
|
// navegar pelo menu e remontar o componente).
|
||||||
|
if (typeof tenantStore.ensureLoaded === 'function') {
|
||||||
|
await tenantStore.ensureLoaded();
|
||||||
|
}
|
||||||
await Promise.all([loadPatients(), applyFilters()]);
|
await Promise.all([loadPatients(), applyFilters()]);
|
||||||
});
|
});
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (_mqMobile) _mqMobile.removeEventListener('change', _onMqMobileChange);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ConfirmDialog />
|
<ConfirmDialog />
|
||||||
|
|
||||||
|
<!-- Drawer host (mobile) — sidebar é teleportada pra cá quando isMobile -->
|
||||||
|
<aside
|
||||||
|
class="mfl-mobile-drawer"
|
||||||
|
:class="{ 'is-open': drawerOpen }"
|
||||||
|
v-show="isMobile"
|
||||||
|
aria-label="Estatísticas e filtros"
|
||||||
|
>
|
||||||
|
<div id="mfl-mobile-drawer-target" class="mfl-mobile-drawer__scroll" />
|
||||||
|
</aside>
|
||||||
|
<Transition name="mfl-drawer-fade">
|
||||||
|
<div
|
||||||
|
v-if="isMobile && drawerOpen"
|
||||||
|
class="mfl-mobile-drawer__backdrop"
|
||||||
|
@click="fecharDrawer"
|
||||||
|
/>
|
||||||
|
</Transition>
|
||||||
|
|
||||||
<section class="mfl-page">
|
<section class="mfl-page">
|
||||||
<header class="mfl-page__head">
|
<header class="mfl-page__head">
|
||||||
|
<button
|
||||||
|
class="mfl-menu-btn mfl-menu-btn--mobile-only"
|
||||||
|
v-tooltip.bottom="'Estatísticas & filtros'"
|
||||||
|
@click="toggleDrawer"
|
||||||
|
>
|
||||||
|
<i class="pi pi-bars" />
|
||||||
|
<span>Menu Lançamentos</span>
|
||||||
|
</button>
|
||||||
<div class="mfl-page__title">
|
<div class="mfl-page__title">
|
||||||
<i class="pi pi-list mfl-page__title-icon" />
|
<i class="pi pi-list mfl-page__title-icon" />
|
||||||
<span>Lançamentos financeiros</span>
|
<span>Lançamentos financeiros</span>
|
||||||
@@ -308,6 +358,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<div class="mfl-body">
|
<div class="mfl-body">
|
||||||
<!-- ═══ COL 1: Stats + filtros ═══ -->
|
<!-- ═══ COL 1: Stats + filtros ═══ -->
|
||||||
|
<Teleport to="#mfl-mobile-drawer-target" :disabled="!isMobile">
|
||||||
<aside class="mfl-side">
|
<aside class="mfl-side">
|
||||||
<div class="mfl-side__scroll">
|
<div class="mfl-side__scroll">
|
||||||
<!-- Stats -->
|
<!-- Stats -->
|
||||||
@@ -461,6 +512,7 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
</aside>
|
</aside>
|
||||||
|
</Teleport>
|
||||||
|
|
||||||
<!-- ═══ COL 2: DataTable ═══ -->
|
<!-- ═══ COL 2: DataTable ═══ -->
|
||||||
<div class="mfl-main">
|
<div class="mfl-main">
|
||||||
@@ -1507,17 +1559,124 @@ onMounted(async () => {
|
|||||||
color: white !important;
|
color: white !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── Botão "Menu" mobile (abre drawer com sidebar) ─── */
|
||||||
|
.mfl-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;
|
||||||
|
}
|
||||||
|
.mfl-menu-btn:hover {
|
||||||
|
background: color-mix(in srgb, var(--m-accent) 88%, white);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
.mfl-menu-btn > i { font-size: 0.85rem; }
|
||||||
|
|
||||||
|
/* ─── Drawer mobile (Teleport target) ─── */
|
||||||
|
.mfl-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;
|
||||||
|
}
|
||||||
|
.mfl-mobile-drawer.is-open { transform: translateX(0); }
|
||||||
|
.mfl-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;
|
||||||
|
}
|
||||||
|
.mfl-mobile-drawer__scroll::-webkit-scrollbar { width: 5px; }
|
||||||
|
.mfl-mobile-drawer__scroll::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--m-border-strong);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar teleportada perde bg/border-right (drawer já tem chrome próprio).
|
||||||
|
Cards perdem margem lateral (drawer já tem padding). Footer vira sticky
|
||||||
|
no bottom do drawer pra "Limpar filtros" sempre visível. */
|
||||||
|
.mfl-mobile-drawer__scroll .mfl-side {
|
||||||
|
width: 100%;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: visible;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
border-right: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.mfl-mobile-drawer__scroll .mfl-side__scroll {
|
||||||
|
flex: none;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: visible;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.mfl-mobile-drawer__scroll .mfl-w--side {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.mfl-mobile-drawer__scroll .mfl-w--side:last-of-type { margin-bottom: 0; }
|
||||||
|
.mfl-mobile-drawer__scroll .mfl-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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mfl-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;
|
||||||
|
}
|
||||||
|
.mfl-drawer-fade-enter-active,
|
||||||
|
.mfl-drawer-fade-leave-active { transition: opacity 200ms ease; }
|
||||||
|
.mfl-drawer-fade-enter-from,
|
||||||
|
.mfl-drawer-fade-leave-to { opacity: 0; }
|
||||||
|
|
||||||
/* ─── Mobile (<1024px) ─── */
|
/* ─── Mobile (<1024px) ─── */
|
||||||
@media (max-width: 1023px) {
|
@media (max-width: 1023px) {
|
||||||
|
/* Sidebar saiu pro drawer via Teleport — body fica só com .mfl-main. */
|
||||||
.mfl-body { flex-direction: column; padding: 0; }
|
.mfl-body { flex-direction: column; padding: 0; }
|
||||||
.mfl-side {
|
.mfl-main { width: 100%; padding: 8px; }
|
||||||
width: 100%;
|
|
||||||
max-height: 50vh;
|
|
||||||
border-right: none;
|
|
||||||
border-bottom: 1px solid var(--m-border);
|
|
||||||
}
|
|
||||||
.mfl-main { padding: 8px; }
|
|
||||||
.mfl-page__title > span:first-of-type { display: none; }
|
.mfl-page__title > span:first-of-type { display: none; }
|
||||||
|
.mfl-menu-btn--mobile-only { display: inline-flex; }
|
||||||
.mfl-act-btn--primary span { display: none; }
|
.mfl-act-btn--primary span { display: none; }
|
||||||
.mfl-act-btn--primary { width: 32px; padding: 0; justify-content: center; }
|
.mfl-act-btn--primary { width: 32px; padding: 0; justify-content: center; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2311,11 +2311,14 @@ watch(editPatientDialog, (isOpen) => {
|
|||||||
}
|
}
|
||||||
.mg-mobile-drawer__scroll .mg-side {
|
.mg-mobile-drawer__scroll .mg-side {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border-right: none;
|
border-right: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
.mg-mobile-drawer__scroll .mg-side__scroll {
|
.mg-mobile-drawer__scroll .mg-side__scroll {
|
||||||
flex: none;
|
flex: none;
|
||||||
@@ -2327,7 +2330,7 @@ watch(editPatientDialog, (isOpen) => {
|
|||||||
.mg-mobile-drawer__scroll .mg-side__footer {
|
.mg-mobile-drawer__scroll .mg-side__footer {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
margin: 8px -12px -24px;
|
margin: auto -12px -24px;
|
||||||
background: var(--m-bg-medium);
|
background: var(--m-bg-medium);
|
||||||
border-top: 1px solid var(--m-border);
|
border-top: 1px solid var(--m-border);
|
||||||
backdrop-filter: blur(24px) saturate(160%);
|
backdrop-filter: blur(24px) saturate(160%);
|
||||||
|
|||||||
@@ -2488,11 +2488,14 @@ watch(editPatientDialog, (isOpen) => {
|
|||||||
}
|
}
|
||||||
.mm-mobile-drawer__scroll .mm-side {
|
.mm-mobile-drawer__scroll .mm-side {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border-right: none;
|
border-right: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
.mm-mobile-drawer__scroll .mm-side__scroll {
|
.mm-mobile-drawer__scroll .mm-side__scroll {
|
||||||
flex: none;
|
flex: none;
|
||||||
@@ -2504,7 +2507,7 @@ watch(editPatientDialog, (isOpen) => {
|
|||||||
.mm-mobile-drawer__scroll .mm-side__footer {
|
.mm-mobile-drawer__scroll .mm-side__footer {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
margin: 8px -12px -24px;
|
margin: auto -12px -24px;
|
||||||
background: var(--m-bg-medium);
|
background: var(--m-bg-medium);
|
||||||
border-top: 1px solid var(--m-border);
|
border-top: 1px solid var(--m-border);
|
||||||
backdrop-filter: blur(24px) saturate(160%);
|
backdrop-filter: blur(24px) saturate(160%);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
* - outras com deeplink → router.push(deeplink)
|
* - outras com deeplink → router.push(deeplink)
|
||||||
* - todas marcam como lida automaticamente se ainda não estiverem.
|
* - 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 { useRouter } from 'vue-router';
|
||||||
import { useToast } from 'primevue/usetoast';
|
import { useToast } from 'primevue/usetoast';
|
||||||
import { useConfirm } from 'primevue/useconfirm';
|
import { useConfirm } from 'primevue/useconfirm';
|
||||||
@@ -35,6 +35,17 @@ const notifStore = useNotificationStore();
|
|||||||
const conversationDrawer = useConversationDrawerStore();
|
const conversationDrawer = useConversationDrawerStore();
|
||||||
const tenantStore = useTenantStore();
|
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 ─────────────────────────────────────────────
|
// ── Estado ─────────────────────────────────────────────
|
||||||
const ownerId = ref(null);
|
const ownerId = ref(null);
|
||||||
const items = ref([]);
|
const items = ref([]);
|
||||||
@@ -306,15 +317,48 @@ function initials(item) {
|
|||||||
watch(() => statusFilter.value, () => { /* noop — filter applies via computed */ });
|
watch(() => statusFilter.value, () => { /* noop — filter applies via computed */ });
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
if (typeof window !== 'undefined' && window.matchMedia) {
|
||||||
|
_mqMobile = window.matchMedia('(max-width: 1023px)');
|
||||||
|
isMobile.value = _mqMobile.matches;
|
||||||
|
_mqMobile.addEventListener('change', _onMqMobileChange);
|
||||||
|
}
|
||||||
load();
|
load();
|
||||||
});
|
});
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (_mqMobile) _mqMobile.removeEventListener('change', _onMqMobileChange);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ConfirmDialog />
|
<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">
|
<section class="mn-page">
|
||||||
<header class="mn-page__head">
|
<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">
|
<div class="mn-page__title">
|
||||||
<i class="pi pi-bell mn-page__title-icon" />
|
<i class="pi pi-bell mn-page__title-icon" />
|
||||||
<span>Notificações</span>
|
<span>Notificações</span>
|
||||||
@@ -361,6 +405,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
<div class="mn-body">
|
<div class="mn-body">
|
||||||
<!-- ═══ COL 1: Stats + filtros ═══ -->
|
<!-- ═══ COL 1: Stats + filtros ═══ -->
|
||||||
|
<Teleport to="#mn-mobile-drawer-target" :disabled="!isMobile">
|
||||||
<aside class="mn-side">
|
<aside class="mn-side">
|
||||||
<div class="mn-side__scroll">
|
<div class="mn-side__scroll">
|
||||||
<!-- Stats -->
|
<!-- Stats -->
|
||||||
@@ -448,6 +493,7 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
</aside>
|
</aside>
|
||||||
|
</Teleport>
|
||||||
|
|
||||||
<!-- ═══ COL 2: Toolbar + Lista ═══ -->
|
<!-- ═══ COL 2: Toolbar + Lista ═══ -->
|
||||||
<div class="mn-main">
|
<div class="mn-main">
|
||||||
@@ -1294,17 +1340,123 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
.mn-row__btn > i { font-size: 0.78rem; }
|
.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) ─── */
|
/* ─── Mobile (<1024px) ─── */
|
||||||
@media (max-width: 1023px) {
|
@media (max-width: 1023px) {
|
||||||
|
/* Sidebar saiu pro drawer via Teleport — body fica só com .mn-main. */
|
||||||
.mn-body { flex-direction: column; padding: 0; }
|
.mn-body { flex-direction: column; padding: 0; }
|
||||||
.mn-side {
|
.mn-main { width: 100%; padding: 8px; }
|
||||||
width: 100%;
|
|
||||||
max-height: 50vh;
|
|
||||||
border-right: none;
|
|
||||||
border-bottom: 1px solid var(--m-border);
|
|
||||||
}
|
|
||||||
.mn-main { padding: 8px; }
|
|
||||||
.mn-page__title > span:first-of-type { display: none; }
|
.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 span { display: none; }
|
||||||
.mn-act-btn { width: 32px; padding: 0; justify-content: center; }
|
.mn-act-btn { width: 32px; padding: 0; justify-content: center; }
|
||||||
.mn-row__actions { opacity: 1; }
|
.mn-row__actions { opacity: 1; }
|
||||||
|
|||||||
@@ -2935,7 +2935,6 @@ function sessaoStatusColor(s) {
|
|||||||
.mp-side__scroll também perde scroll/padding (drawer já tem) e o
|
.mp-side__scroll também perde scroll/padding (drawer já tem) e o
|
||||||
.mp-side__footer vira sticky no bottom do drawer pra manter o
|
.mp-side__footer vira sticky no bottom do drawer pra manter o
|
||||||
"Limpar filtros" sempre acessível. */
|
"Limpar filtros" sempre acessível. */
|
||||||
.mp-mobile-drawer__scroll .mp-side,
|
|
||||||
.mp-mobile-drawer__scroll .mp-quick {
|
.mp-mobile-drawer__scroll .mp-quick {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
@@ -2946,6 +2945,21 @@ function sessaoStatusColor(s) {
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
/* .mp-side ganha flex: 1 pra ocupar altura disponível, com flex column
|
||||||
|
interno — assim o footer sticky é empurrado pro fim do drawer pelo
|
||||||
|
margin: auto, mesmo quando há pouco conteúdo. */
|
||||||
|
.mp-mobile-drawer__scroll .mp-side {
|
||||||
|
width: 100%;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: visible;
|
||||||
|
border-right: none;
|
||||||
|
border-left: none;
|
||||||
|
background: transparent;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
.mp-mobile-drawer__scroll .mp-side__scroll {
|
.mp-mobile-drawer__scroll .mp-side__scroll {
|
||||||
flex: none;
|
flex: none;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
@@ -2955,7 +2969,7 @@ function sessaoStatusColor(s) {
|
|||||||
.mp-mobile-drawer__scroll .mp-side__footer {
|
.mp-mobile-drawer__scroll .mp-side__footer {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
margin: 8px -12px -24px; /* compensa o padding do drawer pra ficar de borda a borda */
|
margin: auto -12px -24px; /* margin-top: auto empurra o footer pro bottom + margin lateral compensa o padding do drawer */
|
||||||
background: var(--m-bg-medium);
|
background: var(--m-bg-medium);
|
||||||
border-top: 1px solid var(--m-border);
|
border-top: 1px solid var(--m-border);
|
||||||
backdrop-filter: blur(24px) saturate(160%);
|
backdrop-filter: blur(24px) saturate(160%);
|
||||||
|
|||||||
@@ -1389,11 +1389,14 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
.mr-mobile-drawer__scroll .mr-side {
|
.mr-mobile-drawer__scroll .mr-side {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border-right: none;
|
border-right: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
.mr-mobile-drawer__scroll .mr-side__scroll {
|
.mr-mobile-drawer__scroll .mr-side__scroll {
|
||||||
flex: none;
|
flex: none;
|
||||||
@@ -1410,7 +1413,7 @@ onBeforeUnmount(() => {
|
|||||||
.mr-mobile-drawer__scroll .mr-side__footer {
|
.mr-mobile-drawer__scroll .mr-side__footer {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
margin: 8px -12px -24px;
|
margin: auto -12px -24px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
background: var(--m-bg-medium);
|
background: var(--m-bg-medium);
|
||||||
border-top: 1px solid var(--m-border);
|
border-top: 1px solid var(--m-border);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
* Lógica idêntica à RelatoriosPage (query agenda_eventos + grouping
|
* Lógica idêntica à RelatoriosPage (query agenda_eventos + grouping
|
||||||
* isoWeek/isoMonth + Chart.js).
|
* 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 { supabase } from '@/lib/supabase/client';
|
||||||
import { useTenantStore } from '@/stores/tenantStore';
|
import { useTenantStore } from '@/stores/tenantStore';
|
||||||
// Chart/DataTable/Column/Tag/Skeleton: auto via PrimeVueResolver
|
// Chart/DataTable/Column/Tag/Skeleton: auto via PrimeVueResolver
|
||||||
@@ -19,6 +19,17 @@ import { useTenantStore } from '@/stores/tenantStore';
|
|||||||
const emit = defineEmits(['close']);
|
const emit = defineEmits(['close']);
|
||||||
const tenantStore = useTenantStore();
|
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 ────────────────────────────────────────────
|
// ── Período ────────────────────────────────────────────
|
||||||
const PERIOD_OPTIONS = [
|
const PERIOD_OPTIONS = [
|
||||||
{ key: 'week', label: 'Esta semana', icon: 'pi pi-calendar' },
|
{ key: 'week', label: 'Esta semana', icon: 'pi pi-calendar' },
|
||||||
@@ -245,12 +256,52 @@ watch(selectedPeriod, () => {
|
|||||||
loadSessions();
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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">
|
<section class="mr-page">
|
||||||
<header class="mr-page__head">
|
<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">
|
<div class="mr-page__title">
|
||||||
<i class="pi pi-chart-bar mr-page__title-icon" />
|
<i class="pi pi-chart-bar mr-page__title-icon" />
|
||||||
<span>Relatórios</span>
|
<span>Relatórios</span>
|
||||||
@@ -283,6 +334,7 @@ onMounted(loadSessions);
|
|||||||
|
|
||||||
<div class="mr-body">
|
<div class="mr-body">
|
||||||
<!-- ═══ COL 1: Stats + filtros ═══ -->
|
<!-- ═══ COL 1: Stats + filtros ═══ -->
|
||||||
|
<Teleport to="#mr-mobile-drawer-target" :disabled="!isMobile">
|
||||||
<aside class="mr-side">
|
<aside class="mr-side">
|
||||||
<div class="mr-side__scroll">
|
<div class="mr-side__scroll">
|
||||||
<!-- Stats -->
|
<!-- Stats -->
|
||||||
@@ -367,6 +419,7 @@ onMounted(loadSessions);
|
|||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
</aside>
|
</aside>
|
||||||
|
</Teleport>
|
||||||
|
|
||||||
<!-- ═══ COL 2: Gráfico + Tabela ═══ -->
|
<!-- ═══ COL 2: Gráfico + Tabela ═══ -->
|
||||||
<div class="mr-main">
|
<div class="mr-main">
|
||||||
@@ -998,17 +1051,119 @@ onMounted(loadSessions);
|
|||||||
gap: 6px;
|
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 */
|
/* Mobile */
|
||||||
@media (max-width: 1023px) {
|
@media (max-width: 1023px) {
|
||||||
.mr-body { flex-direction: column; padding: 0; }
|
.mr-body { flex-direction: column; padding: 0; }
|
||||||
.mr-side {
|
.mr-main { width: 100%; padding: 8px; }
|
||||||
width: 100%;
|
|
||||||
max-height: 50vh;
|
|
||||||
border-right: none;
|
|
||||||
border-bottom: 1px solid var(--m-border);
|
|
||||||
}
|
|
||||||
.mr-main { padding: 8px; }
|
|
||||||
.mr-page__title > span:first-of-type { display: none; }
|
.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); }
|
.mr-stats { grid-template-columns: repeat(3, 1fr); }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2273,11 +2273,14 @@ watch(editPatientDialog, (isOpen) => {
|
|||||||
}
|
}
|
||||||
.mt-mobile-drawer__scroll .mt-side {
|
.mt-mobile-drawer__scroll .mt-side {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border-right: none;
|
border-right: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
.mt-mobile-drawer__scroll .mt-side__scroll {
|
.mt-mobile-drawer__scroll .mt-side__scroll {
|
||||||
flex: none;
|
flex: none;
|
||||||
@@ -2289,7 +2292,7 @@ watch(editPatientDialog, (isOpen) => {
|
|||||||
.mt-mobile-drawer__scroll .mt-side__footer {
|
.mt-mobile-drawer__scroll .mt-side__footer {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
margin: 8px -12px -24px;
|
margin: auto -12px -24px;
|
||||||
background: var(--m-bg-medium);
|
background: var(--m-bg-medium);
|
||||||
border-top: 1px solid var(--m-border);
|
border-top: 1px solid var(--m-border);
|
||||||
backdrop-filter: blur(24px) saturate(160%);
|
backdrop-filter: blur(24px) saturate(160%);
|
||||||
|
|||||||
Reference in New Issue
Block a user