Ajuste Layout, Dashboard Terapeuta, Timeline, Suporte técnico, Documentação e FAQ
This commit is contained in:
@@ -16,7 +16,8 @@
|
|||||||
"Bash(cd \"/d/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai\" && C:/Users/lmnohama/AppData/Local/Programs/Python/Python310/python.exe -c \"\nimport zipfile\nimport xml.etree.ElementTree as ET\n\nfor fname in ['spec-wizard.docx', 'spec-v2.docx']:\n print\\('=== ' + fname + ' ==='\\)\n try:\n with zipfile.ZipFile\\(fname, 'r'\\) as z:\n with z.open\\('word/document.xml'\\) as f:\n tree = ET.parse\\(f\\)\n root = tree.getroot\\(\\)\n texts = []\n for para in root.iter\\('{http://schemas.openxmlformats.org/wordprocessingml/2006/main}p'\\):\n parts = []\n for t in para.iter\\('{http://schemas.openxmlformats.org/wordprocessingml/2006/main}t'\\):\n if t.text:\n parts.append\\(t.text\\)\n line = ''.join\\(parts\\)\n texts.append\\(line\\)\n print\\('\\\\n'.join\\(texts\\)\\)\n except Exception as e:\n print\\('Error: ' + str\\(e\\)\\)\n print\\(\\)\n\")",
|
"Bash(cd \"/d/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai\" && C:/Users/lmnohama/AppData/Local/Programs/Python/Python310/python.exe -c \"\nimport zipfile\nimport xml.etree.ElementTree as ET\n\nfor fname in ['spec-wizard.docx', 'spec-v2.docx']:\n print\\('=== ' + fname + ' ==='\\)\n try:\n with zipfile.ZipFile\\(fname, 'r'\\) as z:\n with z.open\\('word/document.xml'\\) as f:\n tree = ET.parse\\(f\\)\n root = tree.getroot\\(\\)\n texts = []\n for para in root.iter\\('{http://schemas.openxmlformats.org/wordprocessingml/2006/main}p'\\):\n parts = []\n for t in para.iter\\('{http://schemas.openxmlformats.org/wordprocessingml/2006/main}t'\\):\n if t.text:\n parts.append\\(t.text\\)\n line = ''.join\\(parts\\)\n texts.append\\(line\\)\n print\\('\\\\n'.join\\(texts\\)\\)\n except Exception as e:\n print\\('Error: ' + str\\(e\\)\\)\n print\\(\\)\n\")",
|
||||||
"Bash(C:/Users/lmnohama/AppData/Local/Programs/Python/Python310/python.exe -c \"\nimport sys\nwith open\\('/d/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai/DBS/2026-03-12/schema.sql', 'r', encoding='utf-8'\\) as f:\n lines = f.readlines\\(\\)\nprint\\(f'Total lines: {len\\(lines\\)}'\\)\n\" 2>&1)",
|
"Bash(C:/Users/lmnohama/AppData/Local/Programs/Python/Python310/python.exe -c \"\nimport sys\nwith open\\('/d/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai/DBS/2026-03-12/schema.sql', 'r', encoding='utf-8'\\) as f:\n lines = f.readlines\\(\\)\nprint\\(f'Total lines: {len\\(lines\\)}'\\)\n\" 2>&1)",
|
||||||
"Bash(C:/Users/lmnohama/AppData/Local/Programs/Python/Python310/python.exe -c \"\nimport sys\nfpath = 'D:/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai/DBS/2026-03-12/schema.sql'\nwith open\\(fpath, 'r', encoding='utf-8'\\) as f:\n lines = f.readlines\\(\\)\nsys.stdout.buffer.write\\(\\('Total lines: ' + str\\(len\\(lines\\)\\) + '\\\\n'\\).encode\\('utf-8'\\)\\)\n\" 2>&1)",
|
"Bash(C:/Users/lmnohama/AppData/Local/Programs/Python/Python310/python.exe -c \"\nimport sys\nfpath = 'D:/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai/DBS/2026-03-12/schema.sql'\nwith open\\(fpath, 'r', encoding='utf-8'\\) as f:\n lines = f.readlines\\(\\)\nsys.stdout.buffer.write\\(\\('Total lines: ' + str\\(len\\(lines\\)\\) + '\\\\n'\\).encode\\('utf-8'\\)\\)\n\" 2>&1)",
|
||||||
"Bash(find /d/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai -type f \\\\\\( -name \"*convenio*\" -o -name \"*Convenio*\" \\\\\\) 2>/dev/null | head -20)"
|
"Bash(find /d/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai -type f \\\\\\( -name \"*convenio*\" -o -name \"*Convenio*\" \\\\\\) 2>/dev/null | head -20)",
|
||||||
|
"Bash(find:*)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
21933
DBS/2026-03-15/schema.sql
Normal file
21933
DBS/2026-03-15/schema.sql
Normal file
File diff suppressed because it is too large
Load Diff
21
src/app/bootstrapUserSettings.js
vendored
21
src/app/bootstrapUserSettings.js
vendored
@@ -78,10 +78,25 @@ export async function bootstrapUserSettings({
|
|||||||
|
|
||||||
if (error || !settings) return
|
if (error || !settings) return
|
||||||
|
|
||||||
// layout variant (rail / classic)
|
// layout variant: respeita a preferência já gravada no localStorage.
|
||||||
if (settings.layout_variant === 'rail' || settings.layout_variant === 'classic') {
|
// Se localStorage está vazio, só aplica 'rail' do banco (confirma o padrão).
|
||||||
setVariant(settings.layout_variant)
|
// Nunca aplica 'classic' automaticamente quando não há preferência local —
|
||||||
|
// dado antigo no banco não deve sobrescrever o padrão do app.
|
||||||
|
const _lsV = (() => {
|
||||||
|
try {
|
||||||
|
const v = localStorage.getItem('layout_variant')
|
||||||
|
return (v === 'rail' || v === 'classic') ? v : null
|
||||||
|
} catch { return null }
|
||||||
|
})()
|
||||||
|
|
||||||
|
if (_lsV !== null) {
|
||||||
|
// localStorage já tem valor → aplica ele (garante coerência com layoutConfig)
|
||||||
|
if (_lsV !== (settings.layout_variant ?? _lsV)) setVariant(_lsV)
|
||||||
|
} else if (settings.layout_variant === 'rail') {
|
||||||
|
// localStorage vazio + banco tem 'rail' → aplica e grava no localStorage
|
||||||
|
setVariant('rail')
|
||||||
}
|
}
|
||||||
|
// localStorage vazio + banco tem 'classic' → mantém padrão 'rail' (não aplica)
|
||||||
|
|
||||||
// menu mode
|
// menu mode
|
||||||
if (settings.menu_mode && settings.menu_mode !== layoutConfig.menuMode) {
|
if (settings.menu_mode && settings.menu_mode !== layoutConfig.menuMode) {
|
||||||
|
|||||||
@@ -1,62 +1,62 @@
|
|||||||
@use 'mixins' as *;
|
@use 'mixins' as *;
|
||||||
|
|
||||||
|
/* ── Sidebar container ─────────────────────────────────────── */
|
||||||
.layout-sidebar {
|
.layout-sidebar {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
width: 20rem;
|
width: 20rem;
|
||||||
height: calc(100vh - 8rem);
|
height: calc(100vh - 56px);
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
top: 6rem;
|
top: 56px;
|
||||||
left: 2rem;
|
left: 0;
|
||||||
transition:
|
transition:
|
||||||
transform var(--layout-section-transition-duration),
|
transform var(--layout-section-transition-duration),
|
||||||
left var(--layout-section-transition-duration);
|
left var(--layout-section-transition-duration);
|
||||||
background-color: var(--surface-overlay);
|
background-color: var(--surface-card);
|
||||||
border-radius: var(--content-border-radius);
|
border-radius: 0;
|
||||||
padding: 0.5rem 1.5rem;
|
padding: 0;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: var(--surface-border) transparent;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar { width: 4px; }
|
||||||
|
&::-webkit-scrollbar-track { background: transparent; }
|
||||||
|
&::-webkit-scrollbar-thumb { background: var(--surface-border); border-radius: 4px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Menu list ─────────────────────────────────────────────── */
|
||||||
.layout-menu {
|
.layout-menu {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0.5rem 0 2rem;
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
|
|
||||||
|
/* ── Section header ─────────────────────────────────────── */
|
||||||
.layout-root-menuitem {
|
.layout-root-menuitem {
|
||||||
> .layout-menuitem-root-text {
|
> .layout-menuitem-root-text {
|
||||||
font-size: 0.857rem;
|
font-size: 0.6rem;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-weight: 700;
|
font-weight: 800;
|
||||||
color: var(--text-color);
|
letter-spacing: 0.11em;
|
||||||
margin: 0.75rem 0;
|
color: var(--text-color-secondary);
|
||||||
|
opacity: 0.45;
|
||||||
|
padding: 1.1rem 1.25rem 0.3rem;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
> a {
|
> a { display: none; }
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
/* ── Active toggler ─────────────────────────────────────── */
|
||||||
user-select: none;
|
a.active-menuitem > .layout-submenu-toggler,
|
||||||
|
li.active-menuitem > a .layout-submenu-toggler {
|
||||||
&.active-menuitem {
|
|
||||||
> .layout-submenu-toggler {
|
|
||||||
transform: rotate(-180deg);
|
transform: rotate(-180deg);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
li.active-menuitem {
|
|
||||||
> a {
|
|
||||||
.layout-submenu-toggler {
|
|
||||||
transform: rotate(-180deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/* ── Links ──────────────────────────────────────────────── */
|
||||||
ul {
|
ul {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0 0.625rem;
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
@@ -64,30 +64,57 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
outline: 0 none;
|
outline: 0 none;
|
||||||
color: var(--text-color);
|
color: var(--text-color-secondary);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.48rem 0.75rem;
|
||||||
border-radius: var(--content-border-radius);
|
border-radius: 8px;
|
||||||
transition:
|
font-size: 0.825rem;
|
||||||
box-shadow var(--element-transition-duration);
|
font-weight: 500;
|
||||||
|
line-height: 1.4;
|
||||||
|
transition: background 0.12s, color 0.12s;
|
||||||
|
gap: 0;
|
||||||
|
|
||||||
.layout-menuitem-icon {
|
.layout-menuitem-icon {
|
||||||
margin-right: 0.5rem;
|
font-size: 0.85rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
opacity: 0.6;
|
||||||
|
margin-right: 0.6rem;
|
||||||
|
transition: opacity 0.12s;
|
||||||
|
width: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-menuitem-text {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.layout-submenu-toggler {
|
.layout-submenu-toggler {
|
||||||
font-size: 75%;
|
font-size: 68%;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
|
padding-left: 0.35rem;
|
||||||
transition: transform var(--element-transition-duration);
|
transition: transform var(--element-transition-duration);
|
||||||
|
opacity: 0.4;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active-route {
|
&.active-route {
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
|
background: color-mix(in srgb, var(--primary-color) 9%, transparent);
|
||||||
|
|
||||||
|
.layout-menuitem-icon { opacity: 1; }
|
||||||
|
.layout-submenu-toggler { opacity: 0.6; }
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--surface-hover);
|
background: var(--surface-ground);
|
||||||
|
color: var(--text-color);
|
||||||
|
|
||||||
|
.layout-menuitem-icon { opacity: 0.85; }
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
@@ -95,49 +122,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Sub-items ────────────────────────────────────────── */
|
||||||
ul {
|
ul {
|
||||||
|
padding: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: var(--content-border-radius);
|
border-radius: 0;
|
||||||
|
|
||||||
li {
|
li a {
|
||||||
a {
|
font-size: 0.8rem;
|
||||||
margin-left: 1rem;
|
padding: 0.4rem 0.75rem 0.4rem 2.1rem;
|
||||||
|
font-weight: 400;
|
||||||
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
li {
|
li li a { padding-left: 2.9rem; }
|
||||||
a {
|
li li li a { padding-left: 3.5rem; }
|
||||||
margin-left: 2rem;
|
li li li li a { padding-left: 4rem; }
|
||||||
}
|
li li li li li a { padding-left: 4.5rem; }
|
||||||
|
li li li li li li a { padding-left: 5rem; }
|
||||||
li {
|
|
||||||
a {
|
|
||||||
margin-left: 2.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
a {
|
|
||||||
margin-left: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
a {
|
|
||||||
margin-left: 3.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
a {
|
|
||||||
margin-left: 4rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Submenu transition ────────────────────────────────────── */
|
||||||
.layout-submenu-enter-from,
|
.layout-submenu-enter-from,
|
||||||
.layout-submenu-leave-to {
|
.layout-submenu-leave-to {
|
||||||
max-height: 0;
|
max-height: 0;
|
||||||
@@ -150,10 +157,10 @@
|
|||||||
|
|
||||||
.layout-submenu-leave-active {
|
.layout-submenu-leave-active {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: max-height 0.45s cubic-bezier(0, 1, 0, 1);
|
transition: max-height 0.35s cubic-bezier(0, 1, 0, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.layout-submenu-enter-active {
|
.layout-submenu-enter-active {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: max-height 1s ease-in-out;
|
transition: max-height 0.45s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { computed, inject } from 'vue'
|
|||||||
import { useLayout } from '@/layout/composables/layout'
|
import { useLayout } from '@/layout/composables/layout'
|
||||||
import { primaryColors, surfaces, presetOptions, applyThemeEngine } from '@/theme/theme.options'
|
import { primaryColors, surfaces, presetOptions, applyThemeEngine } from '@/theme/theme.options'
|
||||||
|
|
||||||
const { layoutConfig, isDarkTheme, changeMenuMode } = useLayout()
|
const { layoutConfig, isDarkTheme, changeMenuMode, setVariant } = useLayout()
|
||||||
|
|
||||||
// ✅ vem do AppTopbar (mesma instância)
|
// ✅ vem do AppTopbar (mesma instância)
|
||||||
const queuePatch = inject('queueUserSettingsPatch', null)
|
const queuePatch = inject('queueUserSettingsPatch', null)
|
||||||
@@ -101,7 +101,8 @@ function updateColors (type, item) {
|
|||||||
<SelectButton v-model="presetModel" :options="presetOptions" :allowEmpty="false" />
|
<SelectButton v-model="presetModel" :options="presetOptions" :allowEmpty="false" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<!-- Menu Mode: visível apenas no Layout Clássico -->
|
||||||
|
<div v-show="layoutConfig.variant === 'classic'" class="flex flex-col gap-2">
|
||||||
<span class="text-sm text-muted-color font-semibold">Menu Mode</span>
|
<span class="text-sm text-muted-color font-semibold">Menu Mode</span>
|
||||||
<SelectButton
|
<SelectButton
|
||||||
v-model="menuModeModel"
|
v-model="menuModeModel"
|
||||||
@@ -111,6 +112,98 @@ function updateColors (type, item) {
|
|||||||
optionValue="value"
|
optionValue="value"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<span class="text-sm text-muted-color font-semibold">Layout</span>
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<!-- Layout Rail -->
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="layout-option"
|
||||||
|
:class="{ 'layout-option--active': layoutConfig.variant === 'rail' }"
|
||||||
|
@click="setVariant('rail')"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
:class="layoutConfig.variant === 'rail' ? 'pi pi-check-circle' : 'pi pi-circle'"
|
||||||
|
class="layout-option__icon"
|
||||||
|
/>
|
||||||
|
<span class="layout-option__label">Layout Rail</span>
|
||||||
|
<span v-if="layoutConfig.variant === 'rail'" class="layout-option__badge layout-option__badge--default">Padrão</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Layout Clássico -->
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="layout-option"
|
||||||
|
:class="{ 'layout-option--active': layoutConfig.variant === 'classic' }"
|
||||||
|
@click="setVariant('classic')"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
:class="layoutConfig.variant === 'classic' ? 'pi pi-check-circle' : 'pi pi-circle'"
|
||||||
|
class="layout-option__icon"
|
||||||
|
/>
|
||||||
|
<span class="layout-option__label">Layout Clássico</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.layout-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.45rem 0.6rem;
|
||||||
|
border-radius: var(--border-radius, 6px);
|
||||||
|
border: 1px solid var(--surface-border);
|
||||||
|
background: var(--surface-ground);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
transition: border-color 0.15s, background 0.15s;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-option:hover {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-option--active {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
background: color-mix(in srgb, var(--primary-color) 8%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-option__icon {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-option--active .layout-option__icon {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-option__label {
|
||||||
|
flex: 1;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-option__badge {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
padding: 0.1rem 0.45rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-option__badge--default {
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: var(--primary-color-text, #fff);
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -8,7 +8,7 @@ import AppSidebar from './AppSidebar.vue'
|
|||||||
import AppTopbar from './AppTopbar.vue'
|
import AppTopbar from './AppTopbar.vue'
|
||||||
import AppRail from './AppRail.vue'
|
import AppRail from './AppRail.vue'
|
||||||
import AppRailPanel from './AppRailPanel.vue'
|
import AppRailPanel from './AppRailPanel.vue'
|
||||||
import AppRailTopbar from './AppRailTopbar.vue'
|
import AppRailSidebar from './AppRailSidebar.vue'
|
||||||
import AjudaDrawer from '@/components/AjudaDrawer.vue'
|
import AjudaDrawer from '@/components/AjudaDrawer.vue'
|
||||||
|
|
||||||
import { fetchDocsForPath, useAjuda } from '@/composables/useAjuda'
|
import { fetchDocsForPath, useAjuda } from '@/composables/useAjuda'
|
||||||
@@ -27,7 +27,6 @@ import { useTenantFeaturesStore } from '@/stores/tenantFeaturesStore'
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const { layoutConfig, layoutState, hideMobileMenu, isDesktop } = useLayout()
|
const { layoutConfig, layoutState, hideMobileMenu, isDesktop } = useLayout()
|
||||||
|
|
||||||
// ✅ área do layout definida por rota (shell único)
|
|
||||||
const layoutArea = computed(() => route.meta?.area || null)
|
const layoutArea = computed(() => route.meta?.area || null)
|
||||||
provide('layoutArea', layoutArea)
|
provide('layoutArea', layoutArea)
|
||||||
|
|
||||||
@@ -60,10 +59,8 @@ async function revalidateAfterSessionRefresh () {
|
|||||||
if (!tenantStore.loaded && !tenantStore.loading) {
|
if (!tenantStore.loaded && !tenantStore.loading) {
|
||||||
await tenantStore.loadSessionAndTenant()
|
await tenantStore.loadSessionAndTenant()
|
||||||
}
|
}
|
||||||
|
|
||||||
const tid = getTenantId()
|
const tid = getTenantId()
|
||||||
if (!tid) return
|
if (!tid) return
|
||||||
|
|
||||||
await Promise.allSettled([
|
await Promise.allSettled([
|
||||||
entitlementsStore.loadForTenant?.(tid, { force: true }),
|
entitlementsStore.loadForTenant?.(tid, { force: true }),
|
||||||
tf.fetchForTenant?.(tid, { force: true })
|
tf.fetchForTenant?.(tid, { force: true })
|
||||||
@@ -74,20 +71,16 @@ async function revalidateAfterSessionRefresh () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onSessionRefreshed () {
|
function onSessionRefreshed () {
|
||||||
// ✅ Só revalidar tenantStore/entitlements em áreas TENANT.
|
|
||||||
// Em /portal e /account isso causa vazamento de contexto e troca de menu.
|
|
||||||
const p = String(route.path || '')
|
const p = String(route.path || '')
|
||||||
const isTenantArea =
|
const isTenantArea =
|
||||||
p.startsWith('/admin') ||
|
p.startsWith('/admin') ||
|
||||||
p.startsWith('/therapist') ||
|
p.startsWith('/therapist') ||
|
||||||
p.startsWith('/supervisor') ||
|
p.startsWith('/supervisor') ||
|
||||||
p.startsWith('/saas')
|
p.startsWith('/saas')
|
||||||
|
|
||||||
if (!isTenantArea) return
|
if (!isTenantArea) return
|
||||||
revalidateAfterSessionRefresh()
|
revalidateAfterSessionRefresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dispara busca de docs de ajuda sempre que a rota muda
|
|
||||||
watch(() => route.path, (path) => fetchDocsForPath(path), { immediate: true })
|
watch(() => route.path, (path) => fetchDocsForPath(path), { immediate: true })
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -100,18 +93,19 @@ onBeforeUnmount(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- ══ Fullscreen: setup wizard (sem sidebar/topbar/footer) ══ -->
|
<!-- ══ Fullscreen ══ -->
|
||||||
<template v-if="route.meta?.fullscreen">
|
<template v-if="route.meta?.fullscreen">
|
||||||
<router-view />
|
<router-view />
|
||||||
<Toast />
|
<Toast />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- ══ Layout 2: Rail + Painel + Main (full-width) ══════════ -->
|
<!-- ══ Layout Rail ══ -->
|
||||||
<template v-else-if="layoutConfig.variant === 'rail' && isDesktop()">
|
<template v-else-if="layoutConfig.variant === 'rail'">
|
||||||
<div class="l2-root">
|
<div class="l2-root">
|
||||||
|
<!-- Rail de ícones: oculto em mobile (≤ 1200px) via CSS -->
|
||||||
<AppRail />
|
<AppRail />
|
||||||
<div class="l2-body">
|
<div class="l2-body">
|
||||||
<AppRailTopbar />
|
<AppTopbar />
|
||||||
<div class="l2-content">
|
<div class="l2-content">
|
||||||
<AppRailPanel />
|
<AppRailPanel />
|
||||||
<div class="l2-main" :style="ajudaPushStyle">
|
<div class="l2-main" :style="ajudaPushStyle">
|
||||||
@@ -120,11 +114,19 @@ onBeforeUnmount(() => {
|
|||||||
</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 />
|
<AjudaDrawer />
|
||||||
<Toast />
|
<Toast />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- ══ Layout 1: Clássico ═══════════════════════════════════ -->
|
<!-- ══ Layout Clássico melhorado ══ -->
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="layout-wrapper" :class="containerClass">
|
<div class="layout-wrapper" :class="containerClass">
|
||||||
<AppTopbar />
|
<AppTopbar />
|
||||||
@@ -142,8 +144,62 @@ onBeforeUnmount(() => {
|
|||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* ──────────────────────────────────────────────
|
||||||
|
LAYOUT CLÁSSICO — ajustes globais (não scoped)
|
||||||
|
para sobrescrever o tema PrimeVue/Sakai
|
||||||
|
────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
/* ── Sidebar — sempre abaixo da topbar fixed (56px) ────────
|
||||||
|
z-index: 999 para flutuar sobre o conteúdo em overlay.
|
||||||
|
Topbar (z-index 1000) fica sempre acessível acima da sidebar. */
|
||||||
|
.layout-sidebar {
|
||||||
|
position: fixed !important;
|
||||||
|
top: 56px !important;
|
||||||
|
left: 0 !important;
|
||||||
|
height: calc(100vh - 56px) !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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Topbar no layout Clássico — sempre tela toda, acima da sidebar */
|
||||||
|
.layout-wrapper .rail-topbar {
|
||||||
|
z-index: 1000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Conteúdo — margem esquerda por modo ───────────────────
|
||||||
|
Static ativo : afasta da sidebar
|
||||||
|
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: 56px !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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* ─── Layout 2 ───────────────────────────────────────────── */
|
/* ─── Layout Rail (inalterado) ────────────────── */
|
||||||
.l2-root {
|
.l2-root {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
@@ -152,7 +208,6 @@ onBeforeUnmount(() => {
|
|||||||
background: var(--surface-ground);
|
background: var(--surface-ground);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Coluna direita do rail: topbar + conteúdo */
|
|
||||||
.l2-body {
|
.l2-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -160,9 +215,9 @@ onBeforeUnmount(() => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
padding-top: 56px; /* compensa a topbar fixed */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Linha: painel lateral + main */
|
|
||||||
.l2-content {
|
.l2-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
@@ -170,13 +225,35 @@ onBeforeUnmount(() => {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Área de conteúdo principal */
|
|
||||||
.l2-main {
|
.l2-main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
/* Headers sticky no Rail colam no topo do scroll container (já abaixo da topbar) */
|
|
||||||
--layout-sticky-top: 0px;
|
--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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -334,7 +334,7 @@ function onSearchFocus () {
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col h-full">
|
<div class="flex flex-col h-full">
|
||||||
<!-- 🔎 TOPO FIXO -->
|
<!-- 🔎 TOPO FIXO -->
|
||||||
<div ref="searchWrapEl" class="pb-2 border-b border-[var(--surface-border)] bg-[var(--surface-card)]">
|
<div ref="searchWrapEl" class="px-3 pt-3 pb-2 border-b border-[var(--surface-border)] bg-[var(--surface-card)]">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div
|
<div
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
|
|||||||
@@ -56,7 +56,12 @@ function isSameRoute (current, target) {
|
|||||||
const cur = typeof current === 'string' ? current : toPath(current)
|
const cur = typeof current === 'string' ? current : toPath(current)
|
||||||
const tar = typeof target === 'string' ? target : toPath(target)
|
const tar = typeof target === 'string' ? target : toPath(target)
|
||||||
if (!cur || !tar) return false
|
if (!cur || !tar) return false
|
||||||
return cur === tar || cur.startsWith(tar + '/')
|
if (cur === tar) return true
|
||||||
|
// Prefix match apenas para paths com 2+ segmentos (ex: /therapist/patients).
|
||||||
|
// Paths de 1 segmento (ex: /therapist) só ativam em match exato,
|
||||||
|
// evitando que o Dashboard fique ativo em todas as sub-rotas.
|
||||||
|
const segments = tar.split('/').filter(Boolean)
|
||||||
|
return segments.length >= 2 && cur.startsWith(tar + '/')
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasActiveDescendant (node, currentPath) {
|
function hasActiveDescendant (node, currentPath) {
|
||||||
@@ -195,7 +200,7 @@ async function irCadastroCompleto () {
|
|||||||
:is="item.to && !item.items ? 'router-link' : 'a'"
|
:is="item.to && !item.items ? 'router-link' : 'a'"
|
||||||
v-bind="item.to && !item.items ? { to: item.to } : { href: item.url }"
|
v-bind="item.to && !item.items ? { to: item.to } : { href: item.url }"
|
||||||
@click="itemClick($event, item)"
|
@click="itemClick($event, item)"
|
||||||
:class="[item.class, isBlocked ? 'opacity-60 cursor-pointer' : '']"
|
:class="[item.class, isBlocked ? 'opacity-60 cursor-pointer' : '', { 'active-route': isActive && !item.items }]"
|
||||||
:target="item.target"
|
:target="item.target"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@mouseenter="onMouseEnter"
|
@mouseenter="onMouseEnter"
|
||||||
|
|||||||
@@ -27,11 +27,20 @@ function isLocked (item) {
|
|||||||
try { return !entitlements.has(item.feature) } catch { 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) {
|
function isActive (item) {
|
||||||
const active = String(layoutState.activePath || route.path || '')
|
const active = String(layoutState.activePath || route.path || '')
|
||||||
if (!item.to) return false
|
if (!item.to) return false
|
||||||
const p = typeof item.to === 'string' ? item.to : ''
|
const p = toPath(item.to)
|
||||||
return active === p || active.startsWith(p + '/')
|
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) {
|
function navigate (item) {
|
||||||
|
|||||||
312
src/layout/AppRailSidebar.vue
Normal file
312
src/layout/AppRailSidebar.vue
Normal file
@@ -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>
|
||||||
@@ -1,171 +0,0 @@
|
|||||||
<!-- src/layout/AppRailTopbar.vue — Topbar leve para Layout 2 (Rail) -->
|
|
||||||
<script setup>
|
|
||||||
import { computed, ref, nextTick } from 'vue'
|
|
||||||
|
|
||||||
import AppConfigurator from './AppConfigurator.vue'
|
|
||||||
import { useLayout } from '@/layout/composables/layout'
|
|
||||||
import { useUserSettingsPersistence } from '@/composables/useUserSettingsPersistence'
|
|
||||||
import { useTenantStore } from '@/stores/tenantStore'
|
|
||||||
import { useAjuda } from '@/composables/useAjuda'
|
|
||||||
|
|
||||||
const { toggleDarkMode, isDarkTheme } = useLayout()
|
|
||||||
const { queuePatch } = useUserSettingsPersistence()
|
|
||||||
const tenantStore = useTenantStore()
|
|
||||||
const { openDrawer: openAjudaDrawer } = useAjuda()
|
|
||||||
|
|
||||||
const tenantName = computed(() => {
|
|
||||||
const t =
|
|
||||||
tenantStore.activeTenant ||
|
|
||||||
tenantStore.tenant ||
|
|
||||||
tenantStore.currentTenant ||
|
|
||||||
null
|
|
||||||
return (
|
|
||||||
t?.name ||
|
|
||||||
t?.nome ||
|
|
||||||
t?.display_name ||
|
|
||||||
t?.fantasy_name ||
|
|
||||||
t?.razao_social ||
|
|
||||||
null
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
function isDarkNow () {
|
|
||||||
return document.documentElement.classList.contains('app-dark')
|
|
||||||
}
|
|
||||||
|
|
||||||
async function waitForDarkFlip (before, timeoutMs = 900) {
|
|
||||||
const start = performance.now()
|
|
||||||
while (performance.now() - start < timeoutMs) {
|
|
||||||
await nextTick()
|
|
||||||
await new Promise((r) => requestAnimationFrame(r))
|
|
||||||
if (isDarkNow() !== before) return isDarkNow()
|
|
||||||
}
|
|
||||||
return isDarkNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function toggleDarkAndPersist () {
|
|
||||||
try {
|
|
||||||
const before = isDarkNow()
|
|
||||||
toggleDarkMode()
|
|
||||||
const after = await waitForDarkFlip(before)
|
|
||||||
await queuePatch({ theme_mode: after ? 'dark' : 'light' }, { flushNow: true })
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[RailTopbar][theme] falhou:', e?.message || e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<header class="rail-topbar">
|
|
||||||
<!-- Tenant pill -->
|
|
||||||
<div class="rail-topbar__left">
|
|
||||||
<span v-if="tenantName" class="rail-topbar__tenant" :title="tenantName">
|
|
||||||
{{ tenantName }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Ações -->
|
|
||||||
<div class="rail-topbar__actions">
|
|
||||||
<!-- Ajuda -->
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="rail-topbar__btn"
|
|
||||||
title="Ajuda"
|
|
||||||
@click="openAjudaDrawer"
|
|
||||||
>
|
|
||||||
<i class="pi pi-question-circle" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Dark mode -->
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="rail-topbar__btn"
|
|
||||||
:title="isDarkTheme ? 'Modo claro' : 'Modo escuro'"
|
|
||||||
@click="toggleDarkAndPersist"
|
|
||||||
>
|
|
||||||
<i :class="['pi', isDarkTheme ? 'pi-sun' : 'pi-moon']" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Tema / paleta -->
|
|
||||||
<div class="relative">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="rail-topbar__btn rail-topbar__btn--highlight"
|
|
||||||
title="Configurar tema"
|
|
||||||
v-styleclass="{
|
|
||||||
selector: '@next',
|
|
||||||
enterFromClass: 'hidden',
|
|
||||||
enterActiveClass: 'p-anchored-overlay-enter-active',
|
|
||||||
leaveToClass: 'hidden',
|
|
||||||
leaveActiveClass: 'p-anchored-overlay-leave-active',
|
|
||||||
hideOnOutsideClick: true
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<i class="pi pi-palette" />
|
|
||||||
</button>
|
|
||||||
<AppConfigurator />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.rail-topbar {
|
|
||||||
height: 56px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0 1.25rem;
|
|
||||||
border-bottom: 1px solid var(--surface-border);
|
|
||||||
background: var(--surface-card);
|
|
||||||
z-index: 20;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rail-topbar__left {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rail-topbar__tenant {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-color-secondary);
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
max-width: 320px;
|
|
||||||
padding: 0.2rem 0.6rem;
|
|
||||||
border-radius: 999px;
|
|
||||||
border: 1px solid var(--surface-border);
|
|
||||||
background: var(--surface-ground);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rail-topbar__actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rail-topbar__btn {
|
|
||||||
width: 2.25rem;
|
|
||||||
height: 2.25rem;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--text-color-secondary);
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1rem;
|
|
||||||
transition: background 0.15s, color 0.15s;
|
|
||||||
}
|
|
||||||
.rail-topbar__btn:hover {
|
|
||||||
background: var(--surface-ground);
|
|
||||||
color: var(--text-color);
|
|
||||||
}
|
|
||||||
.rail-topbar__btn--highlight {
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -16,7 +16,10 @@ import { useRoleGuard } from '@/composables/useRoleGuard'
|
|||||||
const { canSee } = useRoleGuard()
|
const { canSee } = useRoleGuard()
|
||||||
|
|
||||||
import { useAjuda } from '@/composables/useAjuda'
|
import { useAjuda } from '@/composables/useAjuda'
|
||||||
const { openDrawer: openAjudaDrawer } = useAjuda()
|
const { openDrawer: openAjudaDrawer, closeDrawer: closeAjudaDrawer, drawerOpen: ajudaDrawerOpen } = useAjuda()
|
||||||
|
function toggleAjuda () {
|
||||||
|
ajudaDrawerOpen.value ? closeAjudaDrawer() : openAjudaDrawer()
|
||||||
|
}
|
||||||
|
|
||||||
import { useUserSettingsPersistence } from '@/composables/useUserSettingsPersistence'
|
import { useUserSettingsPersistence } from '@/composables/useUserSettingsPersistence'
|
||||||
import { applyThemeEngine } from '@/theme/theme.options'
|
import { applyThemeEngine } from '@/theme/theme.options'
|
||||||
@@ -27,7 +30,7 @@ const toast = useToast()
|
|||||||
const entitlementsStore = useEntitlementsStore()
|
const entitlementsStore = useEntitlementsStore()
|
||||||
const tenantStore = useTenantStore()
|
const tenantStore = useTenantStore()
|
||||||
|
|
||||||
const { toggleMenu, toggleDarkMode, isDarkTheme, layoutConfig, changeMenuMode } = useLayout()
|
const { toggleMenu, toggleDarkMode, isDarkTheme, layoutConfig, changeMenuMode, isRailMobile } = useLayout()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
@@ -60,45 +63,9 @@ async function loadSessionIdentity () {
|
|||||||
|
|
||||||
const tenantId = computed(() => tenantStore.activeTenantId || null)
|
const tenantId = computed(() => tenantStore.activeTenantId || null)
|
||||||
|
|
||||||
// ✅ tenta achar “nome/email” da clínica do jeito mais tolerante possível
|
// ✅ Admin Dev
|
||||||
const tenantName = computed(() => {
|
|
||||||
const t =
|
|
||||||
tenantStore.activeTenant ||
|
|
||||||
tenantStore.tenant ||
|
|
||||||
tenantStore.currentTenant ||
|
|
||||||
null
|
|
||||||
|
|
||||||
return (
|
|
||||||
t?.name ||
|
|
||||||
t?.nome ||
|
|
||||||
t?.display_name ||
|
|
||||||
t?.fantasy_name ||
|
|
||||||
t?.razao_social ||
|
|
||||||
null
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const tenantEmail = computed(() => {
|
|
||||||
const t =
|
|
||||||
tenantStore.activeTenant ||
|
|
||||||
tenantStore.tenant ||
|
|
||||||
tenantStore.currentTenant ||
|
|
||||||
null
|
|
||||||
|
|
||||||
return (
|
|
||||||
t?.email ||
|
|
||||||
t?.clinic_email ||
|
|
||||||
t?.contact_email ||
|
|
||||||
null
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const ctxItems = computed(() => {
|
const ctxItems = computed(() => {
|
||||||
const items = []
|
const items = []
|
||||||
|
|
||||||
if (tenantName.value) items.push({ k: 'Clínica', v: tenantName.value })
|
|
||||||
if (tenantEmail.value) items.push({ k: 'Email', v: tenantEmail.value })
|
|
||||||
|
|
||||||
// ids (sempre úteis pra debug)
|
// ids (sempre úteis pra debug)
|
||||||
if (tenantId.value) items.push({ k: 'Tenant', v: tenantId.value })
|
if (tenantId.value) items.push({ k: 'Tenant', v: tenantId.value })
|
||||||
if (sessionUid.value) items.push({ k: 'UID', v: sessionUid.value })
|
if (sessionUid.value) items.push({ k: 'UID', v: sessionUid.value })
|
||||||
@@ -160,12 +127,15 @@ async function loadAndApplyUserSettings () {
|
|||||||
// ✅ IMPORTANTE:
|
// ✅ IMPORTANTE:
|
||||||
// changeMenuMode NÃO é só "setar menuMode".
|
// changeMenuMode NÃO é só "setar menuMode".
|
||||||
// Ele reseta estados do sidebar/overlay/mobile e previne drift.
|
// Ele reseta estados do sidebar/overlay/mobile e previne drift.
|
||||||
|
// No layout Rail, não deve ser chamado — ele não usa menuMode.
|
||||||
|
if (layoutConfig.variant !== 'rail') {
|
||||||
try {
|
try {
|
||||||
changeMenuMode(layoutConfig.menuMode)
|
changeMenuMode(layoutConfig.menuMode)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
try { changeMenuMode({ value: layoutConfig.menuMode }) } catch {}
|
try { changeMenuMode({ value: layoutConfig.menuMode }) } catch {}
|
||||||
console.warn('[Topbar][bootstrap] changeMenuMode falhou:', e?.message || e)
|
console.warn('[Topbar][bootstrap] changeMenuMode falhou:', e?.message || e)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[Topbar][bootstrap] erro:', e?.message || e)
|
console.error('[Topbar][bootstrap] erro:', e?.message || e)
|
||||||
}
|
}
|
||||||
@@ -197,6 +167,19 @@ const showPlanDevMenu = computed(() => {
|
|||||||
return canSee('settings.view') && enablePlanToggle.value
|
return canSee('settings.view') && enablePlanToggle.value
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const ctxMenu = ref()
|
||||||
|
const ctxMenuModel = computed(() =>
|
||||||
|
ctxItems.value.length
|
||||||
|
? ctxItems.value.map(it => ({
|
||||||
|
label: `${it.k}: ${it.v}`,
|
||||||
|
icon: it.k === 'Tenant' ? 'pi pi-building' : 'pi pi-user'
|
||||||
|
}))
|
||||||
|
: [{ label: 'Sem contexto', icon: 'pi pi-info-circle', disabled: true }]
|
||||||
|
)
|
||||||
|
function openCtxMenu (event) {
|
||||||
|
ctxMenu.value?.toggle?.(event)
|
||||||
|
}
|
||||||
|
|
||||||
const planMenu = ref()
|
const planMenu = ref()
|
||||||
const planMenuLoading = ref(false)
|
const planMenuLoading = ref(false)
|
||||||
const planMenuTarget = ref(null) // 'therapist' | 'clinic' | null
|
const planMenuTarget = ref(null) // 'therapist' | 'clinic' | null
|
||||||
@@ -535,22 +518,20 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Toast />
|
<Toast />
|
||||||
|
<header class="rail-topbar">
|
||||||
<div class="layout-topbar">
|
<!-- Esquerda -->
|
||||||
<div class="layout-topbar-logo-container">
|
<div class="rail-topbar__left">
|
||||||
<button class="layout-menu-button layout-topbar-action" @click="toggleMenu">
|
<!-- Hamburguer: só aparece em ≤ 1200px no Rail -->
|
||||||
|
<button class="layout-menu-button rail-topbar__btn rail-topbar__hamburger" @click="toggleMenu">
|
||||||
<i class="pi pi-bars"></i>
|
<i class="pi pi-bars"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<router-link to="/" class="layout-topbar-logo">
|
<router-link to="/" class="layout-topbar-logo ml-3">
|
||||||
<svg viewBox="0 0 54 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<span>Agência PSI</span>
|
||||||
<!-- ... SVG ... -->
|
|
||||||
</svg>
|
|
||||||
<span>SAKAI</span>
|
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<div v-if="ctxItems.length" class="topbar-ctx">
|
<!-- Pills: visíveis apenas em > 1200px -->
|
||||||
<div class="topbar-ctx-row">
|
<div class="topbar-ctx-row ml-2">
|
||||||
<span
|
<span
|
||||||
v-for="(it, idx) in ctxItems"
|
v-for="(it, idx) in ctxItems"
|
||||||
:key="`${it.k}-${idx}`"
|
:key="`${it.k}-${idx}`"
|
||||||
@@ -561,62 +542,40 @@ onMounted(async () => {
|
|||||||
<span class="topbar-ctx-v">{{ it.v }}</span>
|
<span class="topbar-ctx-v">{{ it.v }}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="layout-topbar-actions">
|
<!-- Botão Tenant/UID: visível apenas em ≤ 1200px -->
|
||||||
<div class="layout-config-menu">
|
|
||||||
<button type="button" class="layout-topbar-action" @click="toggleDarkAndPersistSilently">
|
|
||||||
<i :class="['pi', { 'pi-moon': isDarkTheme, 'pi-sun': !isDarkTheme }]"></i>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="relative">
|
|
||||||
<button
|
<button
|
||||||
v-styleclass="{
|
|
||||||
selector: '@next',
|
|
||||||
enterFromClass: 'hidden',
|
|
||||||
enterActiveClass: 'p-anchored-overlay-enter-active',
|
|
||||||
leaveToClass: 'hidden',
|
|
||||||
leaveActiveClass: 'p-anchored-overlay-leave-active',
|
|
||||||
hideOnOutsideClick: true
|
|
||||||
}"
|
|
||||||
type="button"
|
type="button"
|
||||||
class="layout-topbar-action layout-topbar-action-highlight"
|
class="rail-topbar__btn topbar-ctx-btn ml-2"
|
||||||
|
title="Tenant / UID"
|
||||||
|
@click="openCtxMenu"
|
||||||
>
|
>
|
||||||
<i class="pi pi-palette"></i>
|
<i class="pi pi-id-card" />
|
||||||
|
<span class="topbar-ctx-btn__label">Tenant / UID</span>
|
||||||
</button>
|
</button>
|
||||||
|
<Menu
|
||||||
<AppConfigurator />
|
ref="ctxMenu"
|
||||||
</div>
|
:model="ctxMenuModel"
|
||||||
|
popup
|
||||||
|
appendTo="body"
|
||||||
|
:baseZIndex="3000"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<!-- Ações -->
|
||||||
class="layout-topbar-menu-button layout-topbar-action"
|
<div class="rail-topbar__actions">
|
||||||
v-styleclass="{
|
<!-- Plan Dev Button -->
|
||||||
selector: '@next',
|
|
||||||
enterFromClass: 'hidden',
|
|
||||||
enterActiveClass: 'p-anchored-overlay-enter-active',
|
|
||||||
leaveToClass: 'hidden',
|
|
||||||
leaveActiveClass: 'p-anchored-overlay-leave-active',
|
|
||||||
hideOnOutsideClick: true
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<i class="pi pi-ellipsis-v"></i>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="layout-topbar-menu hidden lg:block">
|
|
||||||
<div class="layout-topbar-menu-content">
|
|
||||||
<Button
|
<Button
|
||||||
v-if="showPlanDevMenu"
|
v-if="showPlanDevMenu"
|
||||||
ref="planBtn"
|
ref="planBtn"
|
||||||
label="Plano (DEV)"
|
|
||||||
icon="pi pi-sliders-h"
|
|
||||||
severity="contrast"
|
|
||||||
outlined
|
outlined
|
||||||
:loading="planMenuLoading || trocandoPlano"
|
:loading="planMenuLoading || trocandoPlano"
|
||||||
:disabled="planMenuLoading || trocandoPlano"
|
:disabled="planMenuLoading || trocandoPlano"
|
||||||
@click="openPlanMenu"
|
@click="openPlanMenu"
|
||||||
/>
|
class="rail-topbar__btn"
|
||||||
|
>
|
||||||
|
<i class="pi pi-sliders-h" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
<Menu
|
<Menu
|
||||||
ref="planMenu"
|
ref="planMenu"
|
||||||
@@ -626,54 +585,127 @@ onMounted(async () => {
|
|||||||
:baseZIndex="3000"
|
:baseZIndex="3000"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<!-- Ajuda -->
|
||||||
icon="pi pi-question-circle"
|
<button
|
||||||
label="Ajuda"
|
type="button"
|
||||||
severity="secondary"
|
class="rail-topbar__btn"
|
||||||
outlined
|
:class="{ 'rail-topbar__btn--active': ajudaDrawerOpen }"
|
||||||
class="ajuda-btn"
|
:title="ajudaDrawerOpen ? 'Fechar ajuda' : 'Ajuda'"
|
||||||
@click="openAjudaDrawer"
|
@click="toggleAjuda"
|
||||||
/>
|
>
|
||||||
|
<i class="pi pi-question-circle" />
|
||||||
<button type="button" class="layout-topbar-action">
|
|
||||||
<i class="pi pi-calendar"></i>
|
|
||||||
<span>Calendar</span>
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button type="button" class="layout-topbar-action">
|
<!-- Dark mode -->
|
||||||
<i class="pi pi-inbox"></i>
|
<button
|
||||||
<span>Messages</span>
|
type="button"
|
||||||
|
class="rail-topbar__btn"
|
||||||
|
:title="isDarkTheme ? 'Modo claro' : 'Modo escuro'"
|
||||||
|
@click="toggleDarkAndPersistSilently"
|
||||||
|
>
|
||||||
|
<i :class="['pi', { 'pi-moon': isDarkTheme, 'pi-sun': !isDarkTheme }]"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button type="button" class="layout-topbar-action">
|
<!-- Tema / paleta -->
|
||||||
<i class="pi pi-user"></i>
|
<div class="relative">
|
||||||
<span>Profile</span>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rail-topbar__btn rail-topbar__btn--highlight"
|
||||||
|
title="Configurar tema"
|
||||||
|
v-styleclass="{
|
||||||
|
selector: '@next',
|
||||||
|
enterFromClass: 'hidden',
|
||||||
|
enterActiveClass: 'p-anchored-overlay-enter-active',
|
||||||
|
leaveToClass: 'hidden',
|
||||||
|
leaveActiveClass: 'p-anchored-overlay-leave-active',
|
||||||
|
hideOnOutsideClick: true
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<i class="pi pi-palette" />
|
||||||
</button>
|
</button>
|
||||||
|
<AppConfigurator />
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="button" class="layout-topbar-action" @click="logout">
|
<button type="button" class="rail-topbar__btn" @click="logout">
|
||||||
<i class="pi pi-sign-out"></i>
|
<i class="pi pi-sign-out"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</header>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.topbar-ctx {
|
.rail-topbar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 56px;
|
||||||
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-left: 0.75rem;
|
justify-content: space-between;
|
||||||
max-width: min(62vw, 980px);
|
padding: 0 1.25rem;
|
||||||
|
border-bottom: 1px solid var(--surface-border);
|
||||||
|
background: var(--surface-card);
|
||||||
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rail-topbar__left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hamburguer: visível apenas em ≤ 1200px
|
||||||
|
!important necessário para sobrescrever CSS do tema Sakai (.layout-menu-button) */
|
||||||
|
.rail-topbar__hamburger {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.rail-topbar__hamburger {
|
||||||
|
display: grid !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rail-topbar__actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rail-topbar__btn {
|
||||||
|
width: 2.25rem;
|
||||||
|
height: 2.25rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: background 0.15s, color 0.15s;
|
||||||
|
}
|
||||||
|
.rail-topbar__btn:hover {
|
||||||
|
background: var(--surface-ground);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
.rail-topbar__btn--highlight {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
.rail-topbar__btn--active {
|
||||||
|
background: var(--surface-ground);
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
.config-panel {
|
||||||
|
z-index: 200;
|
||||||
|
}
|
||||||
.topbar-ctx-row {
|
.topbar-ctx-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.45rem;
|
gap: 0.45rem;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topbar-ctx-pill {
|
.topbar-ctx-pill {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -683,28 +715,28 @@ onMounted(async () => {
|
|||||||
border: 1px solid var(--surface-border);
|
border: 1px solid var(--surface-border);
|
||||||
background: var(--surface-card);
|
background: var(--surface-card);
|
||||||
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.02);
|
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.02);
|
||||||
max-width: 320px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.topbar-ctx-k {
|
/* Botão Tenant/UID: só em ≤ 1200px */
|
||||||
font-size: 0.75rem;
|
.topbar-ctx-btn {
|
||||||
opacity: 0.7;
|
display: none !important;
|
||||||
white-space: nowrap;
|
width: auto !important;
|
||||||
}
|
border-radius: 999px !important;
|
||||||
|
padding: 0 0.65rem !important;
|
||||||
.ajuda-btn {
|
gap: 0.35rem;
|
||||||
border-radius: 999px;
|
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
padding: 0.3rem 0.8rem;
|
border: 1px solid var(--surface-border) !important;
|
||||||
height: 2rem;
|
|
||||||
}
|
}
|
||||||
|
.topbar-ctx-btn__label {
|
||||||
.topbar-ctx-v {
|
font-size: 0.8rem;
|
||||||
font-size: 0.75rem;
|
color: var(--text-color-secondary);
|
||||||
opacity: 0.95;
|
}
|
||||||
overflow: hidden;
|
@media (max-width: 1200px) {
|
||||||
text-overflow: ellipsis;
|
.topbar-ctx-row {
|
||||||
white-space: nowrap;
|
display: none !important;
|
||||||
max-width: 240px;
|
}
|
||||||
|
.topbar-ctx-btn {
|
||||||
|
display: inline-flex !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -6,7 +6,7 @@ function _loadVariant () {
|
|||||||
const v = localStorage.getItem('layout_variant')
|
const v = localStorage.getItem('layout_variant')
|
||||||
if (v === 'rail' || v === 'classic') return v
|
if (v === 'rail' || v === 'classic') return v
|
||||||
} catch {}
|
} catch {}
|
||||||
return 'classic'
|
return 'rail'
|
||||||
}
|
}
|
||||||
|
|
||||||
const layoutConfig = reactive({
|
const layoutConfig = reactive({
|
||||||
@@ -31,7 +31,10 @@ const layoutState = reactive({
|
|||||||
activePath: null,
|
activePath: null,
|
||||||
// ── Layout 2 (rail) ─────────────────────────────────────
|
// ── Layout 2 (rail) ─────────────────────────────────────
|
||||||
railSectionKey: null, // qual seção está ativa no rail
|
railSectionKey: null, // qual seção está ativa no rail
|
||||||
railPanelOpen: false // painel lateral expandido
|
railPanelOpen: false, // painel lateral expandido
|
||||||
|
// ── variant dirty: true quando o usuário mudou o variant
|
||||||
|
// mas ainda não salvou — impede que loadUserSettings reverta
|
||||||
|
_variantDirty: false
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -75,12 +78,21 @@ export function useLayout () {
|
|||||||
|
|
||||||
const isDesktop = () => window.innerWidth > 991
|
const isDesktop = () => window.innerWidth > 991
|
||||||
|
|
||||||
|
// breakpoint do botão hamburguer no Rail (≤ 1200px)
|
||||||
|
const isRailMobile = () => window.innerWidth <= 1200
|
||||||
|
|
||||||
const toggleMenu = () => {
|
const toggleMenu = () => {
|
||||||
|
// No Rail, o botão hamburguer (≤1200px) controla a sidebar mobile
|
||||||
|
if (layoutConfig.variant === 'rail') {
|
||||||
|
layoutState.mobileMenuActive = !layoutState.mobileMenuActive
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Layout clássico — comportamento original
|
||||||
if (isDesktop()) {
|
if (isDesktop()) {
|
||||||
if (layoutConfig.menuMode === 'static') {
|
if (layoutConfig.menuMode === 'static') {
|
||||||
layoutState.staticMenuInactive = !layoutState.staticMenuInactive
|
layoutState.staticMenuInactive = !layoutState.staticMenuInactive
|
||||||
}
|
}
|
||||||
|
|
||||||
if (layoutConfig.menuMode === 'overlay') {
|
if (layoutConfig.menuMode === 'overlay') {
|
||||||
layoutState.overlayMenuActive = !layoutState.overlayMenuActive
|
layoutState.overlayMenuActive = !layoutState.overlayMenuActive
|
||||||
}
|
}
|
||||||
@@ -134,13 +146,15 @@ export function useLayout () {
|
|||||||
layoutState.anchored = false
|
layoutState.anchored = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const setVariant = (v) => {
|
const setVariant = (v, { fromUser = true } = {}) => {
|
||||||
if (v !== 'classic' && v !== 'rail') return
|
if (v !== 'classic' && v !== 'rail') return
|
||||||
layoutConfig.variant = v
|
layoutConfig.variant = v
|
||||||
try { localStorage.setItem('layout_variant', v) } catch {}
|
try { localStorage.setItem('layout_variant', v) } catch {}
|
||||||
// reset rail state ao trocar
|
// reset rail state ao trocar
|
||||||
layoutState.railSectionKey = null
|
layoutState.railSectionKey = null
|
||||||
layoutState.railPanelOpen = false
|
layoutState.railPanelOpen = false
|
||||||
|
// marca que o usuário fez uma escolha explícita (não restauração do DB)
|
||||||
|
if (fromUser) layoutState._variantDirty = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDarkTheme = computed(() => layoutConfig.darkTheme)
|
const isDarkTheme = computed(() => layoutConfig.darkTheme)
|
||||||
@@ -158,6 +172,7 @@ export function useLayout () {
|
|||||||
changeMenuMode,
|
changeMenuMode,
|
||||||
setVariant,
|
setVariant,
|
||||||
isDesktop,
|
isDesktop,
|
||||||
|
isRailMobile,
|
||||||
hasOpenOverlay
|
hasOpenOverlay
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,23 +7,24 @@
|
|||||||
|
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
label: 'Editor',
|
label: 'Início',
|
||||||
items: [
|
items: [
|
||||||
// ======================================================
|
{ label: 'Dashboard', icon: 'pi pi-fw pi-home', to: '/editor' }
|
||||||
// 📊 DASHBOARD
|
]
|
||||||
// ======================================================
|
},
|
||||||
{ label: 'Dashboard', icon: 'pi pi-fw pi-home', to: '/editor' },
|
|
||||||
|
|
||||||
// ======================================================
|
{
|
||||||
// 📚 CONTEÚDO
|
label: 'Conteúdo',
|
||||||
// ======================================================
|
items: [
|
||||||
{ label: 'Cursos', icon: 'pi pi-fw pi-book', to: '/editor/cursos' },
|
{ label: 'Cursos', icon: 'pi pi-fw pi-book', to: '/editor/cursos' },
|
||||||
{ label: 'Módulos', icon: 'pi pi-fw pi-th-large', to: '/editor/modulos' },
|
{ label: 'Módulos', icon: 'pi pi-fw pi-th-large', to: '/editor/modulos' },
|
||||||
{ label: 'Publicados', icon: 'pi pi-fw pi-check-circle', to: '/editor/publicados' },
|
{ label: 'Publicados', icon: 'pi pi-fw pi-check-circle', to: '/editor/publicados' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
// ======================================================
|
{
|
||||||
// 👤 CONTA
|
label: 'Conta',
|
||||||
// ======================================================
|
items: [
|
||||||
{ label: 'Meu plano', icon: 'pi pi-fw pi-credit-card', to: '/editor/meu-plano' },
|
{ label: 'Meu plano', icon: 'pi pi-fw pi-credit-card', to: '/editor/meu-plano' },
|
||||||
{ label: 'Meu Perfil', icon: 'pi pi-fw pi-user', to: '/account/profile' },
|
{ label: 'Meu Perfil', icon: 'pi pi-fw pi-user', to: '/account/profile' },
|
||||||
{ label: 'Segurança', icon: 'pi pi-fw pi-shield', to: '/account/security' }
|
{ label: 'Segurança', icon: 'pi pi-fw pi-shield', to: '/account/security' }
|
||||||
|
|||||||
@@ -1,52 +1,26 @@
|
|||||||
|
// src/navigation/menus/portal.menu.js
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
label: 'Paciente',
|
label: 'Início',
|
||||||
|
items: [
|
||||||
|
{ label: 'Dashboard', icon: 'pi pi-fw pi-home', to: '/portal' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
label: 'Minhas sessões',
|
||||||
|
items: [
|
||||||
|
{ label: 'Sessões', icon: 'pi pi-fw pi-calendar', to: '/portal/sessoes' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
label: 'Conta',
|
||||||
items: [
|
items: [
|
||||||
// ======================
|
|
||||||
// ✅ Básico (sempre)
|
|
||||||
// ======================
|
|
||||||
{ label: 'Dashboard', icon: 'pi pi-fw pi-home', to: '/portal' },
|
|
||||||
{ label: 'Minhas sessões', icon: 'pi pi-fw pi-user', to: '/portal/sessoes' },
|
|
||||||
// ✅ Conta é global, não do portal
|
|
||||||
{ label: 'Meu plano', icon: 'pi pi-fw pi-credit-card', to: '/portal/meu-plano' },
|
{ label: 'Meu plano', icon: 'pi pi-fw pi-credit-card', to: '/portal/meu-plano' },
|
||||||
{ label: 'Minha Conta', icon: 'pi pi-fw pi-user', to: '/account/profile' },
|
{ label: 'Minha Conta', icon: 'pi pi-fw pi-user', to: '/account/profile' },
|
||||||
{ label: 'Segurança', icon: 'pi pi-fw pi-shield', to: '/account/security' }
|
{ label: 'Segurança', icon: 'pi pi-fw pi-shield', to: '/account/security' }
|
||||||
|
|
||||||
// =====================================================
|
|
||||||
// 🔒 PRO (exemplos futuros no portal do paciente)
|
|
||||||
// =====================================================
|
|
||||||
// A lógica do AppMenuItem que ajustamos suporta:
|
|
||||||
// - feature: 'chave_da_feature'
|
|
||||||
// - proBadge: true -> aparece "PRO" quando bloqueado
|
|
||||||
//
|
|
||||||
// ⚠️ Só descomente quando a rota existir.
|
|
||||||
//
|
|
||||||
// 1) Página pública de agendamento (se você criar um “link do paciente”)
|
|
||||||
// {
|
|
||||||
// label: 'Agendar online',
|
|
||||||
// icon: 'pi pi-fw pi-globe',
|
|
||||||
// to: '/portal/online-scheduling',
|
|
||||||
// feature: 'online_scheduling.public',
|
|
||||||
// proBadge: true
|
|
||||||
// },
|
|
||||||
//
|
|
||||||
// 2) Documentos/Arquivos (muito comum em SaaS clínico)
|
|
||||||
// {
|
|
||||||
// label: 'Documentos',
|
|
||||||
// icon: 'pi pi-fw pi-file',
|
|
||||||
// to: '/portal/documents',
|
|
||||||
// feature: 'patient_documents',
|
|
||||||
// proBadge: true
|
|
||||||
// },
|
|
||||||
//
|
|
||||||
// 3) Teleatendimento / Sala (se for ter)
|
|
||||||
// {
|
|
||||||
// label: 'Sala de atendimento',
|
|
||||||
// icon: 'pi pi-fw pi-video',
|
|
||||||
// to: '/portal/telehealth',
|
|
||||||
// feature: 'telehealth',
|
|
||||||
// proBadge: true
|
|
||||||
// }
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -16,61 +16,57 @@ export default function saasMenu (sessionCtx, opts = {}) {
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: 'SaaS',
|
label: 'Início',
|
||||||
icon: 'pi pi-building',
|
|
||||||
path: '/saas',
|
|
||||||
items: [
|
items: [
|
||||||
{ label: 'Dashboard', icon: 'pi pi-chart-bar', to: '/saas' },
|
{ label: 'Dashboard', icon: 'pi pi-fw pi-chart-bar', to: '/saas' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
label: 'Planos',
|
label: 'Planos',
|
||||||
icon: 'pi pi-star',
|
|
||||||
path: '/saas/plans',
|
|
||||||
items: [
|
items: [
|
||||||
{ label: 'Planos e Preços', icon: 'pi pi-list', to: '/saas/plans' },
|
{ label: 'Planos e Preços', icon: 'pi pi-fw pi-list', to: '/saas/plans' },
|
||||||
{ label: 'Vitrine Pública', icon: 'pi pi-megaphone', to: '/saas/plans-public' },
|
{ label: 'Vitrine Pública', icon: 'pi pi-fw pi-megaphone', to: '/saas/plans-public' },
|
||||||
{ label: 'Recursos', icon: 'pi pi-bolt', to: '/saas/features' },
|
{ label: 'Recursos', icon: 'pi pi-fw pi-bolt', to: '/saas/features' },
|
||||||
{ label: 'Controle de Recursos', icon: 'pi pi-th-large', to: '/saas/plan-features' },
|
{ label: 'Controle de Recursos',icon: 'pi pi-fw pi-th-large', to: '/saas/plan-features' },
|
||||||
{ label: 'Limites por Plano', icon: 'pi pi-sliders-h', to: '/saas/plan-limits' }
|
{ label: 'Limites por Plano', icon: 'pi pi-fw pi-sliders-h', to: '/saas/plan-limits' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
label: 'Assinaturas',
|
label: 'Assinaturas',
|
||||||
icon: 'pi pi-credit-card',
|
|
||||||
path: '/saas/subscriptions',
|
|
||||||
items: [
|
items: [
|
||||||
{ label: 'Listagem de Assinaturas', icon: 'pi pi-list', to: '/saas/subscriptions' },
|
{ label: 'Listagem', icon: 'pi pi-fw pi-list', to: '/saas/subscriptions' },
|
||||||
{ label: 'Intenções', icon: 'pi pi-inbox', to: '/saas/subscription-intents' },
|
{ label: 'Intenções', icon: 'pi pi-fw pi-inbox', to: '/saas/subscription-intents' },
|
||||||
{ label: 'Histórico', icon: 'pi pi-history', to: '/saas/subscription-events' },
|
{ label: 'Histórico', icon: 'pi pi-fw pi-history', to: '/saas/subscription-events' },
|
||||||
{
|
{
|
||||||
label: 'Saúde das Assinaturas',
|
label: 'Saúde das Assinaturas',
|
||||||
icon: 'pi pi-shield',
|
icon: 'pi pi-fw pi-shield',
|
||||||
to: '/saas/subscription-health',
|
to: '/saas/subscription-health',
|
||||||
...mismatchBadge
|
...mismatchBadge
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
{ label: 'Clínicas (Tenants)', icon: 'pi pi-users', to: '/saas/tenants' },
|
{
|
||||||
{ label: 'Feriados', icon: 'pi pi-star', to: '/saas/feriados' },
|
label: 'Operações',
|
||||||
{ label: 'Suporte Técnico', icon: 'pi pi-headphones', to: '/saas/support' },
|
items: [
|
||||||
|
{ label: 'Clínicas (Tenants)', icon: 'pi pi-fw pi-users', to: '/saas/tenants' },
|
||||||
|
{ label: 'Feriados', icon: 'pi pi-fw pi-star', to: '/saas/feriados' },
|
||||||
|
{ label: 'Suporte Técnico', icon: 'pi pi-fw pi-headphones', to: '/saas/support' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
label: 'Conteúdo',
|
label: 'Conteúdo',
|
||||||
icon: 'pi pi-book',
|
|
||||||
path: '/saas/content',
|
|
||||||
...(docsAtencaoCount > 0 ? { badge: String(docsAtencaoCount), badgeClass: 'p-badge p-badge-danger' } : {}),
|
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
label: 'Documentação',
|
label: 'Documentação',
|
||||||
icon: 'pi pi-question-circle',
|
icon: 'pi pi-fw pi-question-circle',
|
||||||
to: '/saas/docs',
|
to: '/saas/docs',
|
||||||
...docsBadge
|
...docsBadge
|
||||||
},
|
},
|
||||||
{ label: 'FAQ', icon: 'pi pi-comments', to: '/saas/faq' }
|
{ label: 'FAQ', icon: 'pi pi-fw pi-comments', to: '/saas/faq' }
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,27 +1,28 @@
|
|||||||
// src/navigation/menus/supervisor.menu.js
|
// src/navigation/menus/supervisor.menu.js
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
|
{
|
||||||
|
label: 'Início',
|
||||||
|
items: [
|
||||||
|
{ label: 'Dashboard', icon: 'pi pi-fw pi-home', to: '/supervisor' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
label: 'Supervisão',
|
label: 'Supervisão',
|
||||||
items: [
|
items: [
|
||||||
// ======================================================
|
|
||||||
// 📊 DASHBOARD
|
|
||||||
// ======================================================
|
|
||||||
{ label: 'Dashboard', icon: 'pi pi-fw pi-home', to: '/supervisor' },
|
|
||||||
|
|
||||||
// ======================================================
|
|
||||||
// 🎓 SALA DE SUPERVISÃO
|
|
||||||
// ======================================================
|
|
||||||
{
|
{
|
||||||
label: 'Sala de Supervisão',
|
label: 'Sala de Supervisão',
|
||||||
icon: 'pi pi-fw pi-users',
|
icon: 'pi pi-fw pi-users',
|
||||||
to: '/supervisor/sala',
|
to: '/supervisor/sala',
|
||||||
feature: 'supervisor.access'
|
feature: 'supervisor.access'
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
// ======================================================
|
{
|
||||||
// 💳 PLANO / CONTA
|
label: 'Conta',
|
||||||
// ======================================================
|
items: [
|
||||||
{ label: 'Meu plano', icon: 'pi pi-fw pi-credit-card', to: '/supervisor/meu-plano' },
|
{ label: 'Meu plano', icon: 'pi pi-fw pi-credit-card', to: '/supervisor/meu-plano' },
|
||||||
{ label: 'Meu Perfil', icon: 'pi pi-fw pi-user', to: '/account/profile' },
|
{ label: 'Meu Perfil', icon: 'pi pi-fw pi-user', to: '/account/profile' },
|
||||||
{ label: 'Segurança', icon: 'pi pi-fw pi-shield', to: '/account/security' }
|
{ label: 'Segurança', icon: 'pi pi-fw pi-shield', to: '/account/security' }
|
||||||
|
|||||||
@@ -2,39 +2,36 @@
|
|||||||
|
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
label: 'Terapeuta',
|
label: 'Início',
|
||||||
items: [
|
items: [
|
||||||
// ======================================================
|
{ label: 'Dashboard', icon: 'pi pi-fw pi-home', to: '/therapist' }
|
||||||
// 📊 DASHBOARD
|
]
|
||||||
// ======================================================
|
},
|
||||||
{ label: 'Dashboard', icon: 'pi pi-fw pi-home', to: '/therapist' },
|
|
||||||
|
|
||||||
// ======================================================
|
|
||||||
// 📅 AGENDA
|
|
||||||
// ======================================================
|
|
||||||
{ label: 'Agenda', icon: 'pi pi-fw pi-calendar', to: '/therapist/agenda', feature: 'agenda.view', proBadge: true },
|
|
||||||
|
|
||||||
// ✅ NOVO: Compromissos determinísticos (tipos)
|
|
||||||
{ label: 'Compromissos', icon: 'pi pi-fw pi-clock', to: '/therapist/agenda/compromissos', feature: 'agenda.view', proBadge: true },
|
|
||||||
|
|
||||||
// ======================================================
|
|
||||||
// 👥 PATIENTS
|
|
||||||
// ======================================================
|
|
||||||
{ label: 'Meus pacientes', icon: 'pi pi-list', to: '/therapist/patients' },
|
|
||||||
|
|
||||||
{ label: 'Grupo de pacientes', icon: 'pi pi-fw pi-users', to: '/therapist/patients/grupos' },
|
|
||||||
|
|
||||||
{ label: 'Tags', icon: 'pi pi-tags', to: '/therapist/patients/tags' },
|
|
||||||
|
|
||||||
{ label: 'Meu link de cadastro', icon: 'pi pi-link', to: '/therapist/patients/link-externo' },
|
|
||||||
|
|
||||||
{ label: 'Cadastros recebidos', icon: 'pi pi-inbox', to: '/therapist/patients/cadastro/recebidos' },
|
|
||||||
|
|
||||||
// ======================================================
|
|
||||||
// 🔒 PRO — Online Scheduling
|
|
||||||
// ======================================================
|
|
||||||
{
|
{
|
||||||
label: 'Online Scheduling',
|
label: 'Agenda',
|
||||||
|
items: [
|
||||||
|
{ label: 'Agenda', icon: 'pi pi-fw pi-calendar', to: '/therapist/agenda', feature: 'agenda.view', proBadge: true },
|
||||||
|
{ label: 'Compromissos', icon: 'pi pi-fw pi-clock', to: '/therapist/agenda/compromissos', feature: 'agenda.view', proBadge: true }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
label: 'Pacientes',
|
||||||
|
items: [
|
||||||
|
{ label: 'Meus pacientes', icon: 'pi pi-list', to: '/therapist/patients' },
|
||||||
|
{ label: 'Grupo de pacientes', icon: 'pi pi-fw pi-users', to: '/therapist/patients/grupos' },
|
||||||
|
{ label: 'Tags', icon: 'pi pi-tags', to: '/therapist/patients/tags' },
|
||||||
|
{ label: 'Meu link de cadastro', icon: 'pi pi-link', to: '/therapist/patients/link-externo' },
|
||||||
|
{ label: 'Cadastros recebidos', icon: 'pi pi-inbox', to: '/therapist/patients/cadastro/recebidos' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
label: 'Agendamento Online',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: 'Configurar página',
|
||||||
icon: 'pi pi-fw pi-globe',
|
icon: 'pi pi-fw pi-globe',
|
||||||
to: '/therapist/online-scheduling',
|
to: '/therapist/online-scheduling',
|
||||||
feature: 'online_scheduling.manage',
|
feature: 'online_scheduling.manage',
|
||||||
@@ -46,16 +43,20 @@ export default [
|
|||||||
to: '/therapist/agendamentos-recebidos',
|
to: '/therapist/agendamentos-recebidos',
|
||||||
feature: 'online_scheduling.manage',
|
feature: 'online_scheduling.manage',
|
||||||
proBadge: true
|
proBadge: true
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
// ======================================================
|
{
|
||||||
// 📈 RELATÓRIOS
|
label: 'Relatórios',
|
||||||
// ======================================================
|
items: [
|
||||||
{ label: 'Relatórios', icon: 'pi pi-fw pi-chart-bar', to: '/therapist/relatorios', feature: 'agenda.view' },
|
{ label: 'Relatórios', icon: 'pi pi-fw pi-chart-bar', to: '/therapist/relatorios', feature: 'agenda.view' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
// ======================================================
|
{
|
||||||
// 👤 ACCOUNT
|
label: 'Conta',
|
||||||
// ======================================================
|
items: [
|
||||||
{ label: 'Meu plano', icon: 'pi pi-fw pi-credit-card', to: '/therapist/meu-plano' },
|
{ label: 'Meu plano', icon: 'pi pi-fw pi-credit-card', to: '/therapist/meu-plano' },
|
||||||
{ label: 'Meu Perfil', icon: 'pi pi-fw pi-user', to: '/account/profile' },
|
{ label: 'Meu Perfil', icon: 'pi pi-fw pi-user', to: '/account/profile' },
|
||||||
{ label: 'Segurança', icon: 'pi pi-fw pi-shield', to: '/account/security' }
|
{ label: 'Segurança', icon: 'pi pi-fw pi-shield', to: '/account/security' }
|
||||||
|
|||||||
@@ -396,6 +396,32 @@ export function applyGuards (router) {
|
|||||||
// ======================================
|
// ======================================
|
||||||
const isAccountArea = (to.path === '/account' || to.path.startsWith('/account/'))
|
const isAccountArea = (to.path === '/account' || to.path.startsWith('/account/'))
|
||||||
if (isAccountArea) {
|
if (isAccountArea) {
|
||||||
|
// Garante menu + entitlements ao recarregar diretamente em /account/* (ex.: F5).
|
||||||
|
// globalRole (profiles.role) não mapeia para menus reais → precisamos da tenant role.
|
||||||
|
const _menuStore = useMenuStore()
|
||||||
|
if (!_menuStore.ready) {
|
||||||
|
try {
|
||||||
|
const _tStore = useTenantStore()
|
||||||
|
if (!_tStore.activeRole) {
|
||||||
|
await _tStore.loadSessionAndTenant()
|
||||||
|
}
|
||||||
|
const _role = _tStore.activeRole
|
||||||
|
const _tid = _tStore.activeTenantId || null
|
||||||
|
if (_role && _tid) {
|
||||||
|
// Carrega entitlements do tenant (mesma lógica do guard principal)
|
||||||
|
const _ent = useEntitlementsStore()
|
||||||
|
if (shouldLoadEntitlements(_ent, _tid)) {
|
||||||
|
await loadEntitlementsSafe(_ent, _tid, true)
|
||||||
|
}
|
||||||
|
// Entitlements pessoais (therapist/supervisor têm assinatura própria)
|
||||||
|
const _roleNorm = normalizeRole(_role)
|
||||||
|
if (['therapist', 'supervisor'].includes(_roleNorm) && _ent.loadedForUser !== uid) {
|
||||||
|
try { await _ent.loadForUser(uid) } catch {}
|
||||||
|
}
|
||||||
|
await ensureMenuBuilt({ uid, tenantId: _tid, tenantRole: _role, globalRole })
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
_perfEnd()
|
_perfEnd()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -574,8 +574,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Menu Mode -->
|
<!-- Menu Mode: só relevante no Layout Clássico -->
|
||||||
<div class="col-span-12 md:col-span-4">
|
<div v-if="layoutConfig.variant === 'classic'" class="col-span-12 md:col-span-4">
|
||||||
<div class="prof-ctrl-box">
|
<div class="prof-ctrl-box">
|
||||||
<div class="prof-ctrl-box__head">
|
<div class="prof-ctrl-box__head">
|
||||||
<div>
|
<div>
|
||||||
@@ -1125,7 +1125,7 @@ async function uploadAvatarIfNeeded () {
|
|||||||
/* ----------------------------
|
/* ----------------------------
|
||||||
Aparência (SEM duplicar engine)
|
Aparência (SEM duplicar engine)
|
||||||
----------------------------- */
|
----------------------------- */
|
||||||
const { layoutConfig, toggleDarkMode, changeMenuMode } = useLayout()
|
const { layoutConfig, layoutState, toggleDarkMode, changeMenuMode } = useLayout()
|
||||||
|
|
||||||
function isDarkNow () {
|
function isDarkNow () {
|
||||||
return document.documentElement.classList.contains('app-dark')
|
return document.documentElement.classList.contains('app-dark')
|
||||||
@@ -1155,9 +1155,12 @@ const menuModeModel = computed({
|
|||||||
set: (val) => {
|
set: (val) => {
|
||||||
if (!val || val === layoutConfig.menuMode) return
|
if (!val || val === layoutConfig.menuMode) return
|
||||||
layoutConfig.menuMode = val
|
layoutConfig.menuMode = val
|
||||||
|
// Não chama changeMenuMode() no Rail — ela reseta estados do sidebar
|
||||||
|
if (layoutConfig.variant !== 'rail') {
|
||||||
try { changeMenuMode?.(val) } catch {
|
try { changeMenuMode?.(val) } catch {
|
||||||
try { changeMenuMode?.({ value: val }) } catch {}
|
try { changeMenuMode?.({ value: val }) } catch {}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (!silentApplying.value) markDirty()
|
if (!silentApplying.value) markDirty()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -1229,13 +1232,9 @@ async function loadUserSettings (uid) {
|
|||||||
// fazendo a sidebar desaparecer ao entrar na página.
|
// fazendo a sidebar desaparecer ao entrar na página.
|
||||||
}
|
}
|
||||||
|
|
||||||
// layout variant — só aplica se mudou, para não resetar o estado do layout
|
// Variant NÃO é re-aplicada aqui: bootstrapUserSettings cuida disso no arranque.
|
||||||
if (
|
// Re-aplicar no loadUserSettings causava regressão (dado stale do banco sobrescrevia
|
||||||
(settings.layout_variant === 'rail' || settings.layout_variant === 'classic') &&
|
// o variant ativo). A UI lê layoutConfig.variant diretamente para exibir a seleção.
|
||||||
settings.layout_variant !== layoutConfig.variant
|
|
||||||
) {
|
|
||||||
setVariant(settings.layout_variant)
|
|
||||||
}
|
|
||||||
|
|
||||||
applyThemeEngine(layoutConfig)
|
applyThemeEngine(layoutConfig)
|
||||||
|
|
||||||
@@ -1405,7 +1404,7 @@ async function saveAll () {
|
|||||||
primary_color: layoutConfig.primary || 'noir',
|
primary_color: layoutConfig.primary || 'noir',
|
||||||
surface_color: layoutConfig.surface || 'slate',
|
surface_color: layoutConfig.surface || 'slate',
|
||||||
menu_mode: layoutConfig.menuMode || 'static',
|
menu_mode: layoutConfig.menuMode || 'static',
|
||||||
layout_variant: layoutConfig.variant || 'classic',
|
layout_variant: layoutConfig.variant || 'rail',
|
||||||
updated_at: new Date().toISOString()
|
updated_at: new Date().toISOString()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1425,6 +1424,7 @@ async function saveAll () {
|
|||||||
|
|
||||||
clearAvatarFile()
|
clearAvatarFile()
|
||||||
dirty.value = false
|
dirty.value = false
|
||||||
|
layoutState._variantDirty = false
|
||||||
toast.add({ severity: 'success', summary: 'Salvo', detail: 'Seu perfil foi atualizado.', life: 2500 })
|
toast.add({ severity: 'success', summary: 'Salvo', detail: 'Seu perfil foi atualizado.', life: 2500 })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.add({ severity: 'error', summary: 'Erro ao salvar', detail: e?.message || 'Não consegui salvar.', life: 6000 })
|
toast.add({ severity: 'error', summary: 'Erro ao salvar', detail: e?.message || 'Não consegui salvar.', life: 6000 })
|
||||||
@@ -1510,9 +1510,6 @@ onBeforeUnmount(() => {
|
|||||||
.prof-root {
|
.prof-root {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
@media (min-width: 768px) {
|
|
||||||
.prof-root { padding: 1.5rem; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ─── Hero ──────────────────────────────────────────────── */
|
/* ─── Hero ──────────────────────────────────────────────── */
|
||||||
.prof-hero-sentinel { height: 1px; }
|
.prof-hero-sentinel { height: 1px; }
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user