Correcao Sidebar Classico e Rail, Correcao Layout, Ajuste de Breakpoint para Tailwind, Ajuste AppTopbar, Ajuste Menu PopOver, Recriado Paleta de Cores, Inserido algumas animações leves, Reajuste Cor items NOVOS da tabela, Drawer Ajuda Corrigido no Logout, Whatsapp, sms, email, recursos extras

This commit is contained in:
Leonardo
2026-03-24 21:26:58 -03:00
parent a89d1f5560
commit 53a4980396
453 changed files with 121427 additions and 174407 deletions

View File

@@ -15,165 +15,161 @@
|--------------------------------------------------------------------------
-->
<script setup>
import { useLayout } from '@/layout/composables/layout'
import { computed, onMounted, onBeforeUnmount, provide, watch } from 'vue'
import { useRoute } from 'vue-router'
import { useLayout } from '@/layout/composables/layout';
import { computed, onMounted, onBeforeUnmount, provide, watch } from 'vue';
import { useRoute } from 'vue-router';
import AppFooter from './AppFooter.vue'
import AppSidebar from './AppSidebar.vue'
import AppTopbar from './AppTopbar.vue'
import AppRail from './AppRail.vue'
import AppRailPanel from './AppRailPanel.vue'
import AppRailSidebar from './AppRailSidebar.vue'
import AjudaDrawer from '@/components/AjudaDrawer.vue'
import SupportDebugBanner from '@/support/components/SupportDebugBanner.vue'
import GlobalNoticeBanner from '@/features/notices/GlobalNoticeBanner.vue'
import AppFooter from './AppFooter.vue';
import AppSidebar from './AppSidebar.vue';
import AppTopbar from './AppTopbar.vue';
import AppRail from './AppRail.vue';
import AppRailPanel from './AppRailPanel.vue';
import AppRailSidebar from './AppRailSidebar.vue';
import AjudaDrawer from '@/components/AjudaDrawer.vue';
import SupportDebugBanner from '@/support/components/SupportDebugBanner.vue';
import GlobalNoticeBanner from '@/features/notices/GlobalNoticeBanner.vue';
import AppThemeBar from './AppThemeBar.vue';
import { useConfiguratorBar } from './composables/useConfiguratorBar';
import { useNoticeStore } from '@/stores/noticeStore'
import { useTenantStore } from '@/stores/tenantStore'
import { useEntitlementsStore } from '@/stores/entitlementsStore'
import { useTenantFeaturesStore } from '@/stores/tenantFeaturesStore'
const { open: themeBarOpen } = useConfiguratorBar();
import { fetchDocsForPath, useAjuda } from '@/composables/useAjuda'
import { useNoticeStore } from '@/stores/noticeStore';
import { useTenantStore } from '@/stores/tenantStore';
import { useEntitlementsStore } from '@/stores/entitlementsStore';
import { useTenantFeaturesStore } from '@/stores/tenantFeaturesStore';
const { drawerOpen } = useAjuda()
import { fetchDocsForPath, useAjuda } from '@/composables/useAjuda';
const { drawerOpen } = useAjuda();
const ajudaPushStyle = computed(() => ({
transition: 'padding-right 0.3s ease',
paddingRight: drawerOpen.value ? '420px' : '0'
}))
transition: 'padding-right 0.3s ease',
paddingRight: drawerOpen.value ? '420px' : '0'
}));
const route = useRoute()
const noticeStore = useNoticeStore()
const { layoutConfig, layoutState, hideMobileMenu, isDesktop, effectiveVariant } = useLayout()
const route = useRoute();
const noticeStore = useNoticeStore();
const { layoutConfig, layoutState, hideMobileMenu, isDesktop, effectiveVariant, effectiveMenuMode } = useLayout();
const layoutArea = computed(() => route.meta?.area || null)
provide('layoutArea', layoutArea)
const layoutArea = computed(() => route.meta?.area || null);
provide('layoutArea', layoutArea);
const tenantStore = useTenantStore()
const entitlementsStore = useEntitlementsStore()
const tf = useTenantFeaturesStore()
const tenantStore = useTenantStore();
const entitlementsStore = useEntitlementsStore();
const tf = useTenantFeaturesStore();
// ── Atualiza contexto dos notices ao mudar de rota ────────────
watch(
() => route.path,
(path) => noticeStore.updateContext(path, tenantStore.role),
{ immediate: false }
)
() => route.path,
(path) => noticeStore.updateContext(path, tenantStore.role),
{ immediate: false }
);
const containerClass = computed(() => {
return {
'layout-overlay': layoutConfig.menuMode === 'overlay',
'layout-static': layoutConfig.menuMode === 'static',
'layout-overlay-active': layoutState.overlayMenuActive,
'layout-mobile-active': layoutState.mobileMenuActive,
'layout-static-inactive': layoutState.staticMenuInactive
}
})
return {
'layout-overlay': effectiveMenuMode.value === 'overlay',
'layout-static': effectiveMenuMode.value === 'static',
'layout-overlay-active': layoutState.overlayMenuActive,
'layout-mobile-active': layoutState.mobileMenuActive,
'layout-static-inactive': layoutState.staticMenuInactive
};
});
function getTenantId () {
return (
tenantStore.activeTenantId ||
tenantStore.tenantId ||
tenantStore.currentTenantId ||
tenantStore.tenant?.id ||
null
)
function getTenantId() {
return tenantStore.activeTenantId || tenantStore.tenantId || tenantStore.currentTenantId || tenantStore.tenant?.id || null;
}
async function revalidateAfterSessionRefresh () {
try {
if (!tenantStore.loaded && !tenantStore.loading) {
await tenantStore.loadSessionAndTenant()
async function revalidateAfterSessionRefresh() {
try {
if (!tenantStore.loaded && !tenantStore.loading) {
await tenantStore.loadSessionAndTenant();
}
const tid = getTenantId();
if (!tid) return;
await Promise.allSettled([entitlementsStore.loadForTenant?.(tid, { force: true }), tf.fetchForTenant?.(tid, { force: true })]);
} catch (e) {
console.warn('[AppLayout] revalidateAfterSessionRefresh failed:', e?.message || e);
}
const tid = getTenantId()
if (!tid) return
await Promise.allSettled([
entitlementsStore.loadForTenant?.(tid, { force: true }),
tf.fetchForTenant?.(tid, { force: true })
])
} catch (e) {
console.warn('[AppLayout] revalidateAfterSessionRefresh failed:', e?.message || e)
}
}
function onSessionRefreshed () {
const p = String(route.path || '')
const isTenantArea =
p.startsWith('/admin') ||
p.startsWith('/therapist') ||
p.startsWith('/supervisor') ||
p.startsWith('/saas')
if (!isTenantArea) return
revalidateAfterSessionRefresh()
function onSessionRefreshed() {
const p = String(route.path || '');
const isTenantArea = p.startsWith('/admin') || p.startsWith('/therapist') || p.startsWith('/supervisor') || p.startsWith('/saas');
if (!isTenantArea) return;
revalidateAfterSessionRefresh();
}
watch(() => route.path, (path) => fetchDocsForPath(path), { immediate: true })
watch(
() => route.path,
(path) => fetchDocsForPath(path),
{ immediate: true }
);
onMounted(() => {
window.addEventListener('app:session-refreshed', onSessionRefreshed)
noticeStore.init(tenantStore.role, route.path)
})
window.addEventListener('app:session-refreshed', onSessionRefreshed);
noticeStore.init(tenantStore.role, route.path);
});
onBeforeUnmount(() => {
window.removeEventListener('app:session-refreshed', onSessionRefreshed)
})
window.removeEventListener('app:session-refreshed', onSessionRefreshed);
});
</script>
<template>
<!-- Fullscreen -->
<template v-if="route.meta?.fullscreen">
<router-view />
<Toast />
</template>
<!-- Fullscreen -->
<template v-if="route.meta?.fullscreen">
<router-view />
<Toast />
</template>
<!-- Layout Rail -->
<template v-else-if="effectiveVariant === 'rail'">
<div class="l2-root">
<!-- Rail de ícones: oculto em mobile ( 1200px) via CSS -->
<AppRail />
<div class="l2-body">
<AppTopbar />
<div class="l2-content">
<AppRailPanel />
<div class="l2-main" :style="ajudaPushStyle">
<router-view />
</div>
<!-- Layout Rail -->
<template v-else-if="effectiveVariant === 'rail'">
<div class="l2-root">
<!-- Rail de ícones: oculto em mobile ( xl (1280px)) via CSS -->
<AppRail />
<div class="l2-body">
<AppTopbar />
<div class="l2-content">
<AppRailPanel />
<div class="l2-main" :style="ajudaPushStyle">
<router-view />
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Sidebar mobile exclusiva do Rail -->
<AppRailSidebar />
<!-- Overlay escuro ao abrir sidebar mobile -->
<div
v-if="layoutState.mobileMenuActive"
class="l2-mobile-overlay"
@click="hideMobileMenu"
/>
<AjudaDrawer />
<Toast />
</template>
<!-- Sidebar mobile exclusiva do Rail -->
<AppRailSidebar />
<!-- Overlay escuro ao abrir sidebar mobile -->
<div v-if="layoutState.mobileMenuActive" class="l2-mobile-overlay" @click="hideMobileMenu" />
<AjudaDrawer />
<Toast />
</template>
<!-- Layout Clássico melhorado -->
<template v-else>
<div class="layout-wrapper" :class="containerClass">
<AppTopbar />
<AppSidebar />
<div class="layout-main-container" :style="ajudaPushStyle">
<div class="layout-main">
<router-view />
<!-- Layout Clássico melhorado -->
<template v-else>
<div class="layout-wrapper" :class="containerClass">
<AppTopbar />
<AppSidebar />
<div class="layout-main-container" :style="ajudaPushStyle">
<div class="layout-main">
<router-view />
</div>
<AppFooter />
</div>
<div class="layout-mask animate-fadein" @click="hideMobileMenu" />
</div>
<AppFooter />
</div>
<div class="layout-mask animate-fadein" @click="hideMobileMenu" />
</div>
<AjudaDrawer />
<Toast />
</template>
<AjudaDrawer />
<Toast />
</template>
<!-- Global fora de todos os branches, persiste em qualquer layout/rota -->
<SupportDebugBanner />
<GlobalNoticeBanner />
<!-- Barra de tema persiste em qualquer layout/rota -->
<Transition name="theme-bar">
<AppThemeBar v-if="themeBarOpen" />
</Transition>
<!-- Global fora de todos os branches, persiste em qualquer layout/rota -->
<SupportDebugBanner />
<GlobalNoticeBanner />
</template>
<style>
@@ -185,31 +181,31 @@ onBeforeUnmount(() => {
/* ── Global Notice Banner: variável de altura ─────────────
Definida aqui como fallback; o componente altera via JS */
:root {
--notice-banner-height: 0px;
--notice-banner-height: 0px;
}
/* ── Topbar: desce pelo banner ─────────────────────────── */
.rail-topbar {
top: var(--notice-banner-height) !important;
top: var(--notice-banner-height) !important;
}
/* ── Sidebar — sempre abaixo da topbar fixed (56px) ────────
Desce pelo banner também. */
.layout-sidebar {
position: fixed !important;
top: calc(56px + var(--notice-banner-height)) !important;
left: 0 !important;
height: calc(100vh - 56px - var(--notice-banner-height)) !important;
border-radius: 0 !important;
padding: 0 !important;
box-shadow: 2px 0 6px rgba(0,0,0,.06) !important;
border-right: 1px solid var(--surface-border) !important;
z-index: 999;
position: fixed !important;
top: calc(56px + var(--notice-banner-height)) !important;
left: 0 !important;
height: calc(100vh - 56px - var(--notice-banner-height)) !important;
border-radius: 0 !important;
padding: 0 !important;
box-shadow: 2px 0 6px rgba(0, 0, 0, 0.06) !important;
border-right: 1px solid var(--surface-border) !important;
z-index: 999;
}
/* ── Topbar no layout Clássico — sempre tela toda, acima da sidebar */
.layout-wrapper .rail-topbar {
z-index: 1000 !important;
z-index: 1000 !important;
}
/* ── Conteúdo — margem esquerda por modo ───────────────────
@@ -217,88 +213,99 @@ onBeforeUnmount(() => {
Static inativo: sem margem
Overlay : sem margem (sidebar flutua sobre o conteúdo) */
.layout-main-container {
margin-left: 20rem !important;
padding-left: 0 !important;
padding-top: calc(56px + var(--notice-banner-height)) !important;
margin-left: 20rem !important;
padding-left: 0 !important;
padding-top: calc(56px + var(--notice-banner-height)) !important;
}
.layout-overlay .layout-main-container,
.layout-static-inactive .layout-main-container {
margin-left: 0 !important;
}
@media (max-width: 1200px) {
.layout-main-container {
margin-left: 0 !important;
}
}
@media (width <= theme(--breakpoint-xl, 1280px)) {
.layout-main-container {
margin-left: 0 !important;
}
}
/* ── Overlay: hambúrguer sempre visível ─────────────────────
Em overlay a sidebar não ocupa espaço fixo — o botão precisa
estar disponível em qualquer largura de tela. */
.layout-overlay .rail-topbar__hamburger {
display: grid !important;
display: grid !important;
}
/* ── AppThemeBar — slide-up ao abrir, slide-down ao fechar ───
A transição está definida no próprio componente (.theme-bar).
Aqui apenas declaramos o estado inicial (enter) / final (leave). */
.theme-bar-enter-from,
.theme-bar-leave-to {
transform: translateY(100%);
}
</style>
<style scoped>
/* ─── Layout Rail ─────────────────────────────── */
.l2-root {
position: fixed;
top: var(--notice-banner-height, 0px); /* desce pelo banner */
left: 0;
right: 0;
bottom: 0;
display: flex;
overflow: hidden;
background: var(--surface-ground);
position: fixed;
top: var(--notice-banner-height, 0px); /* desce pelo banner */
left: 0;
right: 0;
bottom: 0;
display: flex;
overflow: hidden;
background: var(--surface-ground);
}
.l2-body {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
padding-top: 56px; /* compensa topbar — banner já absorvido pelo l2-root */
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
padding-top: 56px; /* compensa topbar — banner já absorvido pelo l2-root */
}
.l2-content {
flex: 1;
min-height: 0;
display: flex;
overflow: hidden;
flex: 1;
min-height: 0;
display: flex;
overflow: hidden;
}
.l2-main {
flex: 1;
min-width: 0;
overflow-y: auto;
overflow-x: hidden;
--layout-sticky-top: 0px;
flex: 1;
min-width: 0;
overflow-y: auto;
overflow-x: hidden;
--layout-sticky-top: 0px;
}
/* Rail de ícones: oculto em mobile */
@media (max-width: 1200px) {
.l2-root :deep(.rail) {
display: none;
}
/* Painel lateral: também oculto em mobile (substituído pelo AppRailSidebar) */
.l2-content :deep(.rp) {
display: none;
}
@media (width <= theme(--breakpoint-xl, 1280px)) {
.l2-root :deep(.rail) {
display: none;
}
/* Painel lateral: também oculto em mobile (substituído pelo AppRailSidebar) */
.l2-content :deep(.rp) {
display: none;
}
}
/* Overlay escuro ao abrir sidebar mobile no Rail */
.l2-mobile-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.4);
z-index: 98;
animation: fadeIn 0.2s ease;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.4);
z-index: 98;
animation: fadeIn 0.2s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
from {
opacity: 0;
}
to {
opacity: 1;
}
}
</style>
</style>