Ajuste Layout, Dashboard Terapeuta, Timeline, Suporte técnico, Documentação e FAQ
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
<!-- src/layout/AppRailSidebar.vue — Drawer mobile para Layout Rail -->
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
|
||||
import { useMenuStore } from '@/stores/menuStore'
|
||||
import { useLayout } from './composables/layout'
|
||||
import { useEntitlementsStore } from '@/stores/entitlementsStore'
|
||||
|
||||
const menuStore = useMenuStore()
|
||||
const { layoutState, hideMobileMenu } = useLayout()
|
||||
const entitlements = useEntitlementsStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const sections = computed(() => {
|
||||
const model = menuStore.model || []
|
||||
return model
|
||||
.filter(s => s.label && Array.isArray(s.items) && s.items.length)
|
||||
.map(s => ({
|
||||
key: s.label,
|
||||
label: s.label,
|
||||
icon: s.icon || s.items.find(i => i.icon)?.icon || 'pi pi-circle',
|
||||
items: s.items
|
||||
}))
|
||||
})
|
||||
|
||||
// Seções expandidas no accordion
|
||||
const openSections = ref([])
|
||||
|
||||
// Ao abrir o drawer, expande a seção ativa (ou a primeira)
|
||||
watch(() => layoutState.mobileMenuActive, (open) => {
|
||||
if (!open) return
|
||||
if (openSections.value.length === 0 && sections.value.length > 0) {
|
||||
const activeKey = layoutState.railSectionKey
|
||||
openSections.value = [activeKey || sections.value[0].key]
|
||||
}
|
||||
})
|
||||
|
||||
function isSectionOpen (key) {
|
||||
return openSections.value.includes(key)
|
||||
}
|
||||
|
||||
function toggleSection (key) {
|
||||
const idx = openSections.value.indexOf(key)
|
||||
if (idx >= 0) {
|
||||
openSections.value.splice(idx, 1)
|
||||
} else {
|
||||
openSections.value.push(key)
|
||||
}
|
||||
}
|
||||
|
||||
function isLocked (item) {
|
||||
if (!item.proBadge || !item.feature) return false
|
||||
try { return !entitlements.has(item.feature) } catch { return false }
|
||||
}
|
||||
|
||||
function toPath (to) {
|
||||
if (!to) return ''
|
||||
if (typeof to === 'string') return to
|
||||
try { return router.resolve(to).path || '' } catch { return '' }
|
||||
}
|
||||
|
||||
function isActive (item) {
|
||||
const active = String(layoutState.activePath || route.path || '')
|
||||
if (!item.to) return false
|
||||
const p = toPath(item.to)
|
||||
if (!p) return false
|
||||
if (active === p) return true
|
||||
const segments = p.split('/').filter(Boolean)
|
||||
return segments.length >= 2 && active.startsWith(p + '/')
|
||||
}
|
||||
|
||||
function navigate (item) {
|
||||
if (isLocked(item)) {
|
||||
router.push({ name: 'upgrade', query: { feature: item.feature || '' } })
|
||||
hideMobileMenu()
|
||||
return
|
||||
}
|
||||
if (item.to) {
|
||||
layoutState.activePath = toPath(item.to)
|
||||
router.push(item.to)
|
||||
hideMobileMenu()
|
||||
}
|
||||
}
|
||||
|
||||
// Fecha ao navegar
|
||||
watch(() => route.path, () => hideMobileMenu())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Transition name="rs-slide">
|
||||
<aside v-if="layoutState.mobileMenuActive" class="rs">
|
||||
<!-- Header -->
|
||||
<div class="rs__head">
|
||||
<span class="rs__brand">Agência PSI</span>
|
||||
<button class="rs__close" aria-label="Fechar menu" @click="hideMobileMenu">
|
||||
<i class="pi pi-times" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Nav -->
|
||||
<nav class="rs__nav">
|
||||
<template v-for="section in sections" :key="section.key">
|
||||
<!-- Cabeçalho da seção -->
|
||||
<button
|
||||
class="rs__sec"
|
||||
:class="{ 'rs__sec--open': isSectionOpen(section.key) }"
|
||||
@click="toggleSection(section.key)"
|
||||
>
|
||||
<i :class="section.icon" class="rs__sec-icon" />
|
||||
<span class="rs__sec-label">{{ section.label }}</span>
|
||||
<i class="pi pi-chevron-down rs__sec-arrow" />
|
||||
</button>
|
||||
|
||||
<!-- Items da seção -->
|
||||
<div v-show="isSectionOpen(section.key)" class="rs__items">
|
||||
<template v-for="item in section.items" :key="item.to || item.label">
|
||||
<!-- Sub-grupo -->
|
||||
<template v-if="item.items?.length">
|
||||
<div class="rs__group-label">{{ item.label }}</div>
|
||||
<button
|
||||
v-for="child in item.items"
|
||||
:key="child.to || child.label"
|
||||
class="rs__item"
|
||||
:class="{
|
||||
'rs__item--active': isActive(child),
|
||||
'rs__item--locked': isLocked(child)
|
||||
}"
|
||||
@click="navigate(child)"
|
||||
>
|
||||
<i v-if="child.icon" :class="child.icon" class="rs__item-icon" />
|
||||
<span>{{ child.label }}</span>
|
||||
<span v-if="isLocked(child)" class="rs__pro">PRO</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<!-- Item folha -->
|
||||
<button
|
||||
v-else
|
||||
class="rs__item"
|
||||
:class="{
|
||||
'rs__item--active': isActive(item),
|
||||
'rs__item--locked': isLocked(item)
|
||||
}"
|
||||
@click="navigate(item)"
|
||||
>
|
||||
<i v-if="item.icon" :class="item.icon" class="rs__item-icon" />
|
||||
<span>{{ item.label }}</span>
|
||||
<span v-if="isLocked(item)" class="rs__pro">PRO</span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</nav>
|
||||
</aside>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.rs {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 280px;
|
||||
height: 100vh;
|
||||
z-index: 99; /* acima do overlay (98), abaixo da topbar (100) quando necessário */
|
||||
background: var(--surface-card);
|
||||
border-right: 1px solid var(--surface-border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ── Header ──────────────────────────────────────────────── */
|
||||
.rs__head {
|
||||
height: 56px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
border-bottom: 1px solid var(--surface-border);
|
||||
}
|
||||
.rs__brand {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-color);
|
||||
}
|
||||
.rs__close {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 7px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-color-secondary);
|
||||
cursor: pointer;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-size: 0.75rem;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
.rs__close:hover {
|
||||
background: var(--surface-ground);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
/* ── Nav list ─────────────────────────────────────────────── */
|
||||
.rs__nav {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--surface-border) transparent;
|
||||
}
|
||||
|
||||
/* ── Section accordion ───────────────────────────────────── */
|
||||
.rs__sec {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 9px 10px;
|
||||
border-radius: 9px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
font-size: 0.83rem;
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
transition: background 0.13s;
|
||||
}
|
||||
.rs__sec:hover { background: var(--surface-ground); }
|
||||
.rs__sec-icon {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-color-secondary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.rs__sec-label { flex: 1; }
|
||||
.rs__sec-arrow {
|
||||
font-size: 0.65rem;
|
||||
color: var(--text-color-secondary);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.rs__sec--open .rs__sec-arrow { transform: rotate(180deg); }
|
||||
|
||||
/* ── Items container ─────────────────────────────────────── */
|
||||
.rs__items {
|
||||
padding-left: 10px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
.rs__group-label {
|
||||
font-size: 0.62rem;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--text-color-secondary);
|
||||
opacity: 0.55;
|
||||
padding: 8px 10px 4px;
|
||||
}
|
||||
.rs__item {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
padding: 7px 10px;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-color-secondary);
|
||||
cursor: pointer;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 500;
|
||||
text-align: left;
|
||||
transition: background 0.13s, color 0.13s;
|
||||
}
|
||||
.rs__item:hover {
|
||||
background: var(--surface-ground);
|
||||
color: var(--text-color);
|
||||
}
|
||||
.rs__item--active {
|
||||
background: color-mix(in srgb, var(--primary-color) 10%, transparent);
|
||||
color: var(--primary-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
.rs__item--locked { opacity: 0.55; }
|
||||
.rs__item-icon {
|
||||
font-size: 0.82rem;
|
||||
flex-shrink: 0;
|
||||
opacity: 0.75;
|
||||
}
|
||||
.rs__pro {
|
||||
font-size: 0.58rem;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
padding: 1px 5px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--surface-border);
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
|
||||
/* ── Slide-in da esquerda ────────────────────────────────── */
|
||||
.rs-slide-enter-active,
|
||||
.rs-slide-leave-active {
|
||||
transition: transform 0.25s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
.rs-slide-enter-from,
|
||||
.rs-slide-leave-to {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user