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

This commit is contained in:
Leonardo
2026-03-24 21:26:58 -03:00
parent a89d1f5560
commit 53a4980396
453 changed files with 121427 additions and 174407 deletions
+403 -383
View File
@@ -16,463 +16,483 @@
-->
<!-- Painel expansível do Layout 2 -->
<script setup>
import { computed, ref, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { computed, ref, watch, nextTick, onMounted, onBeforeUnmount } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { useMenuStore } from '@/stores/menuStore'
import { useLayout } from './composables/layout'
import { useEntitlementsStore } from '@/stores/entitlementsStore'
import { useMenuBadges } from '@/composables/useMenuBadges'
import { useMenuStore } from '@/stores/menuStore';
import { useLayout } from './composables/layout';
import { useEntitlementsStore } from '@/stores/entitlementsStore';
import { useMenuBadges } from '@/composables/useMenuBadges';
import PatientCreatePopover from '@/components/ui/PatientCreatePopover.vue'
import ComponentCadastroRapido from '@/components/ComponentCadastroRapido.vue'
import PatientCreatePopover from '@/components/ui/PatientCreatePopover.vue';
import ComponentCadastroRapido from '@/components/ComponentCadastroRapido.vue';
const menuStore = useMenuStore()
const { layoutState } = useLayout()
const entitlements = useEntitlementsStore()
const menuBadges = useMenuBadges()
const router = useRouter()
const route = useRoute()
const menuStore = useMenuStore();
const { layoutState } = useLayout();
const entitlements = useEntitlementsStore();
const menuBadges = useMenuBadges();
const router = useRouter();
const route = useRoute();
function menuBadgeLabel (item) {
const key = item?.badgeKey
if (!key) return null
const val = menuBadges[key]?.value || 0
if (!val) return null
return key === 'agendaHoje' ? `${val} hoje` : String(val)
function menuBadgeLabel(item) {
const key = item?.badgeKey;
if (!key) return null;
const val = menuBadges[key]?.value || 0;
if (!val) return null;
return key === 'agendaHoje' ? `${val} hoje` : String(val);
}
// ── Seção ativa ──────────────────────────────────────────────
const currentSection = computed(() => {
const model = menuStore.model || []
return model.find(s => s.label === layoutState.railSectionKey) || null
})
const model = menuStore.model || [];
return model.find((s) => s.label === layoutState.railSectionKey) || null;
});
// Todos os grupos do menu
const allSections = computed(() => {
const model = menuStore.model || []
return model.filter(s => s.label && Array.isArray(s.items) && s.items.length)
})
const model = menuStore.model || [];
return model.filter((s) => s.label && Array.isArray(s.items) && s.items.length);
});
// "Início" = chave especial __home__
const isHome = computed(() => layoutState.railSectionKey === '__home__')
const isHome = computed(() => layoutState.railSectionKey === '__home__');
// Seções visíveis: tudo em Início, só a selecionada nos demais
const visibleSections = computed(() =>
isHome.value ? allSections.value : (currentSection.value ? [currentSection.value] : [])
)
const visibleSections = computed(() => (isHome.value ? allSections.value : currentSection.value ? [currentSection.value] : []));
const panelTitle = computed(() =>
isHome.value ? 'Início' : currentSection.value?.label || 'Menu'
)
const panelTitle = computed(() => (isHome.value ? 'Início' : currentSection.value?.label || 'Menu'));
// ── Helpers ──────────────────────────────────────────────────
function isLocked (item) {
if (!item.proBadge || !item.feature) return false
try { return !entitlements.has(item.feature) } catch { return false }
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 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 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 || '' } })
return
}
if (item.to) {
layoutState.activePath = typeof item.to === 'string' ? item.to : router.resolve(item.to).path
router.push(item.to)
}
function navigate(item) {
if (isLocked(item)) {
router.push({ name: 'upgrade', query: { feature: item.feature || '' } });
return;
}
if (item.to) {
layoutState.activePath = typeof item.to === 'string' ? item.to : router.resolve(item.to).path;
router.push(item.to);
}
}
function closePanel () {
layoutState.railPanelOpen = false
function closePanel() {
layoutState.railPanelOpen = false;
}
// ── QuickCreate (Pacientes) ───────────────────────────────
const createPopover = ref(null)
const quickDialog = ref(false)
const createPopover = ref(null);
const quickDialog = ref(false);
function openQuickCreate (event, item) {
createPopover.value?.toggle(event)
function openQuickCreate(event, item) {
createPopover.value?.toggle(event);
}
function onQuickCreate() {
quickDialog.value = true;
}
function onQuickCreate () { quickDialog.value = true }
// ── Busca (todo o menu) ──────────────────────────────────────
const query = ref('')
const showResults = ref(false)
const activeIndex = ref(-1)
const forcedOpen = ref(false)
const searchEl = ref(null)
const searchWrapEl = ref(null)
const query = ref('');
const showResults = ref(false);
const activeIndex = ref(-1);
const forcedOpen = ref(false);
const searchEl = ref(null);
const searchWrapEl = ref(null);
const RECENT_KEY = 'menu_search_recent'
const recent = ref([])
function loadRecent () {
try { recent.value = JSON.parse(localStorage.getItem(RECENT_KEY) || '[]') } catch { recent.value = [] }
const RECENT_KEY = 'menu_search_recent';
const recent = ref([]);
function loadRecent() {
try {
recent.value = JSON.parse(localStorage.getItem(RECENT_KEY) || '[]');
} catch {
recent.value = [];
}
}
function saveRecent (q) {
const v = String(q || '').trim()
if (!v) return
const list = [v, ...recent.value.filter(x => x !== v)].slice(0, 8)
recent.value = list
localStorage.setItem(RECENT_KEY, JSON.stringify(list))
function saveRecent(q) {
const v = String(q || '').trim();
if (!v) return;
const list = [v, ...recent.value.filter((x) => x !== v)].slice(0, 8);
recent.value = list;
localStorage.setItem(RECENT_KEY, JSON.stringify(list));
}
function clearRecent () {
recent.value = []
try { localStorage.removeItem(RECENT_KEY) } catch {}
function clearRecent() {
recent.value = [];
try {
localStorage.removeItem(RECENT_KEY);
} catch {}
}
loadRecent()
loadRecent();
watch(query, (v) => {
const hasText = !!v?.trim()
if (hasText) { forcedOpen.value = false; showResults.value = true; return }
showResults.value = forcedOpen.value
})
function clearSearch () {
query.value = ''
activeIndex.value = -1
showResults.value = false
forcedOpen.value = false
}
function norm (s) {
return String(s || '').toLowerCase().normalize('NFD').replace(/\p{Diacritic}/gu, '').trim()
}
function isVisibleItem (it) {
const v = it?.visible
if (typeof v === 'function') return !!v()
if (v === undefined || v === null) return true
return v !== false
}
function flattenMenu (items, trail = []) {
const out = []
for (const it of (items || [])) {
if (!isVisibleItem(it)) continue
const nextTrail = [...trail, it?.label].filter(Boolean)
if (it?.to && !it?.items?.length) {
out.push({ label: it.label || it.to, to: it.to, icon: it.icon, trail: nextTrail, proBadge: !!(it.__showProBadge ?? it.proBadge), feature: it.feature || null })
const hasText = !!v?.trim();
if (hasText) {
forcedOpen.value = false;
showResults.value = true;
return;
}
if (it?.items?.length) out.push(...flattenMenu(it.items, nextTrail))
}
return out
showResults.value = forcedOpen.value;
});
function clearSearch() {
query.value = '';
activeIndex.value = -1;
showResults.value = false;
forcedOpen.value = false;
}
const allLinks = computed(() => flattenMenu(menuStore.model || []))
function norm(s) {
return String(s || '')
.toLowerCase()
.normalize('NFD')
.replace(/\p{Diacritic}/gu, '')
.trim();
}
function isVisibleItem(it) {
const v = it?.visible;
if (typeof v === 'function') return !!v();
if (v === undefined || v === null) return true;
return v !== false;
}
function flattenMenu(items, trail = []) {
const out = [];
for (const it of items || []) {
if (!isVisibleItem(it)) continue;
const nextTrail = [...trail, it?.label].filter(Boolean);
if (it?.to && !it?.items?.length) {
out.push({ label: it.label || it.to, to: it.to, icon: it.icon, trail: nextTrail, proBadge: !!(it.__showProBadge ?? it.proBadge), feature: it.feature || null });
}
if (it?.items?.length) out.push(...flattenMenu(it.items, nextTrail));
}
return out;
}
const allLinks = computed(() => flattenMenu(menuStore.model || []));
const results = computed(() => {
const q = norm(query.value)
if (!q) return []
const wantPro = q === 'pro' || q.startsWith('pro ') || q.includes(' pro')
return allLinks.value
.filter(r => {
const hay = `${norm(r.label)} ${norm(r.trail.join(' > '))} ${norm(r.to)}`
if (hay.includes(q)) return true
if (wantPro && (r.proBadge || r.feature)) return true
return false
})
.slice(0, 12)
})
const q = norm(query.value);
if (!q) return [];
const wantPro = q === 'pro' || q.startsWith('pro ') || q.includes(' pro');
return allLinks.value
.filter((r) => {
const hay = `${norm(r.label)} ${norm(r.trail.join(' > '))} ${norm(r.to)}`;
if (hay.includes(q)) return true;
if (wantPro && (r.proBadge || r.feature)) return true;
return false;
})
.slice(0, 12);
});
watch(results, (list) => { activeIndex.value = list.length ? 0 : -1 })
watch(results, (list) => {
activeIndex.value = list.length ? 0 : -1;
});
function escapeHtml (s) {
return String(s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;')
function escapeHtml(s) {
return String(s || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
function highlight (text, q) {
const queryNorm = norm(q)
const raw = String(text || '')
if (!queryNorm) return escapeHtml(raw)
const rawNorm = norm(raw)
const idx = rawNorm.indexOf(queryNorm)
if (idx < 0) return escapeHtml(raw)
const before = escapeHtml(raw.slice(0, idx))
const mid = escapeHtml(raw.slice(idx, idx + queryNorm.length))
const after = escapeHtml(raw.slice(idx + queryNorm.length))
return `${before}<mark class="px-1 rounded bg-yellow-200/40">${mid}</mark>${after}`
function highlight(text, q) {
const queryNorm = norm(q);
const raw = String(text || '');
if (!queryNorm) return escapeHtml(raw);
const rawNorm = norm(raw);
const idx = rawNorm.indexOf(queryNorm);
if (idx < 0) return escapeHtml(raw);
const before = escapeHtml(raw.slice(0, idx));
const mid = escapeHtml(raw.slice(idx, idx + queryNorm.length));
const after = escapeHtml(raw.slice(idx + queryNorm.length));
return `${before}<mark class="px-1 rounded bg-yellow-200/40">${mid}</mark>${after}`;
}
function onSearchKeydown (e) {
if (e.key === 'Escape') { showResults.value = false; forcedOpen.value = false; return }
if (e.key === 'ArrowDown') {
e.preventDefault()
if (!results.value.length) return
showResults.value = true
activeIndex.value = (activeIndex.value + 1) % results.value.length
return
}
if (e.key === 'ArrowUp') {
e.preventDefault()
if (!results.value.length) return
showResults.value = true
activeIndex.value = (activeIndex.value - 1 + results.value.length) % results.value.length
return
}
if (e.key === 'Enter' && showResults.value && results.value.length && activeIndex.value >= 0) {
e.preventDefault()
goToResult(results.value[activeIndex.value])
}
function onSearchKeydown(e) {
if (e.key === 'Escape') {
showResults.value = false;
forcedOpen.value = false;
return;
}
if (e.key === 'ArrowDown') {
e.preventDefault();
if (!results.value.length) return;
showResults.value = true;
activeIndex.value = (activeIndex.value + 1) % results.value.length;
return;
}
if (e.key === 'ArrowUp') {
e.preventDefault();
if (!results.value.length) return;
showResults.value = true;
activeIndex.value = (activeIndex.value - 1 + results.value.length) % results.value.length;
return;
}
if (e.key === 'Enter' && showResults.value && results.value.length && activeIndex.value >= 0) {
e.preventDefault();
goToResult(results.value[activeIndex.value]);
}
}
function onSearchFocus () {
if (!query.value?.trim()) { forcedOpen.value = true; showResults.value = true }
function onSearchFocus() {
if (!query.value?.trim()) {
forcedOpen.value = true;
showResults.value = true;
}
}
function applyRecent (q) {
query.value = q
forcedOpen.value = true
showResults.value = true
activeIndex.value = 0
nextTick(() => searchEl.value?.$el?.querySelector?.('input')?.focus?.())
function applyRecent(q) {
query.value = q;
forcedOpen.value = true;
showResults.value = true;
activeIndex.value = 0;
nextTick(() => searchEl.value?.$el?.querySelector?.('input')?.focus?.());
}
function onDocMouseDown (e) {
if (!showResults.value) return
if (!searchWrapEl.value?.contains(e.target)) { showResults.value = false; forcedOpen.value = false }
function onDocMouseDown(e) {
if (!showResults.value) return;
if (!searchWrapEl.value?.contains(e.target)) {
showResults.value = false;
forcedOpen.value = false;
}
}
onMounted(() => document.addEventListener('mousedown', onDocMouseDown))
onBeforeUnmount(() => document.removeEventListener('mousedown', onDocMouseDown))
onMounted(() => document.addEventListener('mousedown', onDocMouseDown));
onBeforeUnmount(() => document.removeEventListener('mousedown', onDocMouseDown));
async function goToResult (r) {
saveRecent(query.value)
query.value = ''
showResults.value = false
activeIndex.value = -1
forcedOpen.value = false
layoutState.activePath = typeof r.to === 'string' ? r.to : router.resolve(r.to).path
await router.push(r.to)
async function goToResult(r) {
saveRecent(query.value);
query.value = '';
showResults.value = false;
activeIndex.value = -1;
forcedOpen.value = false;
layoutState.activePath = typeof r.to === 'string' ? r.to : router.resolve(r.to).path;
await router.push(r.to);
}
</script>
<template>
<Transition name="panel-slide">
<aside
v-if="layoutState.railPanelOpen"
class="w-[260px] shrink-0 h-screen flex flex-col border-r border-[var(--surface-border)] bg-[var(--surface-card)] overflow-hidden"
aria-label="Menu lateral"
>
<!-- Header -->
<div class="h-14 shrink-0 flex items-center justify-between px-4 border-b border-[var(--surface-border)]">
<span class="text-[0.9rem] font-bold tracking-tight text-[var(--text-color)]">
{{ panelTitle }}
</span>
<button
class="w-7 h-7 rounded-lg border-none bg-transparent text-[var(--text-color-secondary)] cursor-pointer grid place-items-center text-xs transition-colors hover:bg-[var(--surface-ground)] hover:text-[var(--text-color)]"
aria-label="Fechar painel"
@click="closePanel"
>
<i class="pi pi-times" />
</button>
</div>
<!-- Busca no Início -->
<div v-if="isHome" ref="searchWrapEl" class="shrink-0 px-2.5 pt-2.5 border-b border-[var(--surface-border)]">
<!-- Campo -->
<div class="relative">
<div aria-hidden="true" style="position:absolute;left:-9999px;top:-9999px;width:1px;height:1px;overflow:hidden;">
<input type="email" name="email" autocomplete="username" tabindex="-1" disabled />
<input type="password" name="password" autocomplete="current-password" tabindex="-1" disabled />
</div>
<FloatLabel variant="on" class="w-full">
<IconField class="w-full">
<InputIcon class="pi pi-search" />
<InputText
ref="searchEl"
id="rp_menu_search"
name="rp_menu_search"
type="text"
inputmode="search"
autocomplete="off"
autocapitalize="off"
autocorrect="off"
spellcheck="false"
data-lpignore="true"
data-1p-ignore="true"
v-model="query"
class="w-full pr-8"
variant="filled"
@focus="onSearchFocus"
@keydown="onSearchKeydown"
/>
</IconField>
<label for="rp_menu_search">Encontrar menu...</label>
</FloatLabel>
<button
v-if="query.trim()"
type="button"
class="absolute right-2 top-1/2 -translate-y-1/2 bg-transparent border-none cursor-pointer opacity-60 hover:opacity-100 text-[var(--text-color-secondary)] text-xs grid place-items-center p-0.5"
@mousedown.prevent="clearSearch"
aria-label="Limpar busca"
>
<i class="pi pi-times" />
</button>
</div>
<!-- Recentes -->
<div
v-if="showResults && !query.trim() && recent.length"
class="mt-1.5 mb-2.5 border border-[var(--surface-border)] rounded-xl bg-[var(--surface-card)] shadow-sm overflow-hidden"
>
<div class="flex items-center justify-between px-2.5 py-1.5 text-[1rem] opacity-65 text-[var(--text-color-secondary)]">
<span>Recentes</span>
<button type="button" class="bg-transparent border-none cursor-pointer opacity-70 hover:opacity-100 text-inherit text-[1rem]" @mousedown.prevent="clearRecent" aria-label="Limpar recentes">
<i class="pi pi-trash" />
</button>
</div>
<button
v-for="q in recent" :key="q"
class="w-full flex items-center gap-2 px-2.5 py-1.5 bg-transparent border-none cursor-pointer text-[var(--text-color)] text-[1rem] text-left transition-colors hover:bg-[var(--surface-hover)]"
type="button"
@click.stop.prevent="applyRecent(q)"
>
<i class="pi pi-history text-[0.8rem] opacity-70 shrink-0" />
<span class="flex-1">{{ q }}</span>
</button>
</div>
<!-- Resultados de busca -->
<div
v-else-if="showResults && results.length"
class="mt-1.5 mb-2.5 border border-[var(--surface-border)] rounded-xl bg-[var(--surface-card)] shadow-sm overflow-hidden"
>
<button
v-for="(r, i) in results" :key="String(r.to)"
type="button"
@mousedown.prevent="goToResult(r)"
class="w-full flex items-center gap-2 px-2.5 py-1.5 bg-transparent border-none cursor-pointer text-[var(--text-color)] text-[1rem] text-left transition-colors"
:class="i === activeIndex ? 'bg-[var(--surface-hover)]' : 'hover:bg-[var(--surface-hover)]'"
>
<i v-if="r.icon" :class="r.icon" class="text-[0.8rem] opacity-70 shrink-0" />
<div class="flex flex-col flex-1">
<span class="font-medium leading-tight" v-html="highlight(r.label, query)" />
<small class="text-[0.68rem] opacity-60">{{ r.trail.join(' > ') }}</small>
</div>
<span v-if="r.proBadge" class="text-[0.58rem] font-extrabold uppercase tracking-widest px-1.5 py-px rounded border border-[var(--surface-border)] text-[var(--text-color-secondary)] opacity-70">PRO</span>
</button>
</div>
<div v-else-if="showResults && query && !results.length" class="py-2 pb-2.5 text-[1rem] opacity-60 text-[var(--text-color-secondary)]">
Nenhum item encontrado.
</div>
<div v-else class="pb-2.5" />
</div>
<!-- Nav: todo o menu -->
<nav class="flex-1 overflow-y-auto px-2 py-2.5 flex flex-col gap-0.5 [scrollbar-width:thin] [scrollbar-color:var(--surface-border)_transparent]">
<template v-for="section in visibleSections" :key="section.label">
<!-- Label da seção exibe quando mostrando múltiplas seções -->
<div
v-if="visibleSections.length > 1"
class="text-[0.62rem] font-extrabold uppercase tracking-widest text-[var(--text-color-secondary)] opacity-55 px-2.5 pb-1.5 pt-3 first:pt-1"
>
{{ section.label }}
</div>
<template v-for="item in section.items" :key="item.to || item.label">
<!-- Sub-grupo -->
<div v-if="item.items?.length" class="flex flex-col gap-px">
<div class="text-[0.6rem] font-bold uppercase tracking-widest text-[var(--text-color-secondary)] opacity-40 px-2.5 pb-1 pt-2">
{{ item.label }}
</div>
<button
v-for="child in item.items"
:key="child.to || child.label"
class="w-full flex items-center gap-2.5 px-2.5 py-2 rounded-[9px] border-none bg-transparent text-[var(--text-color-secondary)] cursor-pointer text-left text-[0.83rem] font-medium transition-colors hover:bg-[var(--surface-ground)] hover:text-[var(--text-color)]"
:class="{
'!bg-[color-mix(in_srgb,var(--primary-color)_10%,transparent)] !text-[var(--primary-color)] !font-semibold': isActive(child),
'opacity-55': isLocked(child)
}"
@click="navigate(child)"
>
<i v-if="child.icon" :class="child.icon" class="text-[1rem] shrink-0 opacity-75" />
<span class="flex-1">{{ child.label }}</span>
<span v-if="isLocked(child)" class="text-[0.58rem] font-extrabold uppercase tracking-widest px-1.5 py-px rounded border border-[var(--surface-border)] text-[var(--text-color-secondary)] opacity-70">PRO</span>
<span v-if="menuBadgeLabel(child)" class="text-[0.62rem] font-bold px-1.5 py-px rounded-full bg-[var(--primary-color)] text-white leading-none">{{ menuBadgeLabel(child) }}</span>
</button>
<Transition name="panel-slide">
<aside v-if="layoutState.railPanelOpen" class="w-[260px] shrink-0 h-screen flex flex-col border-r border-[var(--surface-border)] bg-[var(--surface-card)] overflow-hidden" aria-label="Menu lateral">
<!-- Header -->
<div class="h-14 shrink-0 flex items-center justify-between px-4 border-b border-[var(--surface-border)]">
<span class="text-[0.9rem] font-bold tracking-tight text-[var(--text-color)]">
{{ panelTitle }}
</span>
<button
class="w-7 h-7 rounded-lg border-none bg-transparent text-[var(--text-color-secondary)] cursor-pointer grid place-items-center text-xs transition-colors hover:bg-[var(--surface-ground)] hover:text-[var(--text-color)]"
aria-label="Fechar painel"
@click="closePanel"
>
<i class="pi pi-times" />
</button>
</div>
<!-- Item folha -->
<div v-else class="flex items-center gap-1">
<button
class="flex-1 flex items-center gap-2.5 px-2.5 py-2 rounded-[9px] border-none bg-transparent text-[var(--text-color-secondary)] cursor-pointer text-left text-[0.83rem] font-medium transition-colors hover:bg-[var(--surface-ground)] hover:text-[var(--text-color)]"
:class="{
'!bg-[color-mix(in_srgb,var(--primary-color)_10%,transparent)] !text-[var(--primary-color)] !font-semibold': isActive(item),
'opacity-55': isLocked(item)
}"
@click="navigate(item)"
>
<i v-if="item.icon" :class="item.icon" class="text-[1rem] shrink-0 opacity-75" />
<span class="flex-1">{{ item.label }}</span>
<span v-if="isLocked(item)" class="text-[0.58rem] font-extrabold uppercase tracking-widest px-1.5 py-px rounded border border-[var(--surface-border)] text-[var(--text-color-secondary)] opacity-70">PRO</span>
<span v-if="menuBadgeLabel(item)" class="text-[0.62rem] font-bold px-1.5 py-px rounded-full bg-[var(--primary-color)] text-white leading-none">{{ menuBadgeLabel(item) }}</span>
</button>
<button
v-if="item.quickCreate"
class="w-6 h-6 shrink-0 rounded-md border-none bg-transparent text-[var(--text-color-secondary)] cursor-pointer grid place-items-center text-xs transition-colors hover:bg-[var(--surface-ground)] hover:text-[var(--text-color)]"
@click.stop="openQuickCreate($event, item)"
title="Novo paciente"
>
<i class="pi pi-plus" />
</button>
<!-- Busca no Início -->
<div v-if="isHome" ref="searchWrapEl" class="shrink-0 px-2.5 pt-2.5 border-b border-[var(--surface-border)]">
<!-- Campo -->
<div class="relative">
<div aria-hidden="true" style="position: absolute; left: -9999px; top: -9999px; width: 1px; height: 1px; overflow: hidden">
<input type="email" name="email" autocomplete="username" tabindex="-1" disabled />
<input type="password" name="password" autocomplete="current-password" tabindex="-1" disabled />
</div>
<FloatLabel variant="on" class="w-full">
<IconField class="w-full">
<InputIcon class="pi pi-search" />
<InputText
ref="searchEl"
id="rp_menu_search"
name="rp_menu_search"
type="text"
inputmode="search"
autocomplete="off"
autocapitalize="off"
autocorrect="off"
spellcheck="false"
data-lpignore="true"
data-1p-ignore="true"
v-model="query"
class="w-full pr-8"
variant="filled"
@focus="onSearchFocus"
@keydown="onSearchKeydown"
/>
</IconField>
<label for="rp_menu_search">Encontrar menu...</label>
</FloatLabel>
<button
v-if="query.trim()"
type="button"
class="absolute right-2 top-1/2 -translate-y-1/2 bg-transparent border-none cursor-pointer opacity-60 hover:opacity-100 text-[var(--text-color-secondary)] text-xs grid place-items-center p-0.5"
@mousedown.prevent="clearSearch"
aria-label="Limpar busca"
>
<i class="pi pi-times" />
</button>
</div>
<!-- Recentes -->
<div v-if="showResults && !query.trim() && recent.length" class="mt-1.5 mb-2.5 border border-[var(--surface-border)] rounded-xl bg-[var(--surface-card)] shadow-sm overflow-hidden">
<div class="flex items-center justify-between px-2.5 py-1.5 text-[1rem] opacity-65 text-[var(--text-color-secondary)]">
<span>Recentes</span>
<button type="button" class="bg-transparent border-none cursor-pointer opacity-70 hover:opacity-100 text-inherit text-[1rem]" @mousedown.prevent="clearRecent" aria-label="Limpar recentes">
<i class="pi pi-trash" />
</button>
</div>
<button
v-for="q in recent"
:key="q"
class="w-full flex items-center gap-2 px-2.5 py-1.5 bg-transparent border-none cursor-pointer text-[var(--text-color)] text-[1rem] text-left transition-colors hover:bg-[var(--surface-hover)]"
type="button"
@click.stop.prevent="applyRecent(q)"
>
<i class="pi pi-history text-[0.8rem] opacity-70 shrink-0" />
<span class="flex-1">{{ q }}</span>
</button>
</div>
<!-- Resultados de busca -->
<div v-else-if="showResults && results.length" class="mt-1.5 mb-2.5 border border-[var(--surface-border)] rounded-xl bg-[var(--surface-card)] shadow-sm overflow-hidden">
<button
v-for="(r, i) in results"
:key="String(r.to)"
type="button"
@mousedown.prevent="goToResult(r)"
class="w-full flex items-center gap-2 px-2.5 py-1.5 bg-transparent border-none cursor-pointer text-[var(--text-color)] text-[1rem] text-left transition-colors"
:class="i === activeIndex ? 'bg-[var(--surface-hover)]' : 'hover:bg-[var(--surface-hover)]'"
>
<i v-if="r.icon" :class="r.icon" class="text-[0.8rem] opacity-70 shrink-0" />
<div class="flex flex-col flex-1">
<span class="font-medium leading-tight" v-html="highlight(r.label, query)" />
<small class="text-[0.68rem] opacity-60">{{ r.trail.join(' > ') }}</small>
</div>
<span v-if="r.proBadge" class="text-[0.58rem] font-extrabold uppercase tracking-widest px-1.5 py-px rounded border border-[var(--surface-border)] text-[var(--text-color-secondary)] opacity-70">PRO</span>
</button>
</div>
<div v-else-if="showResults && query && !results.length" class="py-2 pb-2.5 text-[1rem] opacity-60 text-[var(--text-color-secondary)]">Nenhum item encontrado.</div>
<div v-else class="pb-2.5" />
</div>
</template>
</template>
</nav>
<!-- Nav: todo o menu -->
<nav class="flex-1 overflow-y-auto px-2 py-2.5 flex flex-col gap-0.5 [scrollbar-width:thin] [scrollbar-color:var(--surface-border)_transparent]">
<template v-for="section in visibleSections" :key="section.label">
<!-- Label da seção exibe quando mostrando múltiplas seções -->
<div v-if="visibleSections.length > 1" class="text-[0.62rem] font-extrabold uppercase tracking-widest text-[var(--text-color-secondary)] opacity-55 px-2.5 pb-1.5 pt-3 first:pt-1">
{{ section.label }}
</div>
<!-- PatientCreatePopover (shared) -->
<PatientCreatePopover
ref="createPopover"
@quick-create="onQuickCreate"
/>
<template v-for="item in section.items" :key="item.to || item.label">
<!-- Sub-grupo -->
<div v-if="item.items?.length" class="flex flex-col gap-px">
<div class="text-[0.6rem] font-bold uppercase tracking-widest text-[var(--text-color-secondary)] opacity-40 px-2.5 pb-1 pt-2">
{{ item.label }}
</div>
<button
v-for="child in item.items"
:key="child.to || child.label"
class="w-full flex items-center gap-2.5 px-2.5 py-2 rounded-[9px] border-none bg-transparent text-[var(--text-color-secondary)] cursor-pointer text-left text-[0.83rem] font-medium transition-colors hover:bg-[var(--surface-ground)] hover:text-[var(--text-color)]"
:class="{
'!bg-[color-mix(in_srgb,var(--primary-color)_10%,transparent)] !text-[var(--primary-color)] !font-semibold': isActive(child),
'opacity-55': isLocked(child)
}"
@click="navigate(child)"
>
<i v-if="child.icon" :class="child.icon" class="text-[1rem] shrink-0 opacity-75" />
<span class="flex-1">{{ child.label }}</span>
<span v-if="isLocked(child)" class="text-[0.58rem] font-extrabold uppercase tracking-widest px-1.5 py-px rounded border border-[var(--surface-border)] text-[var(--text-color-secondary)] opacity-70">PRO</span>
<span v-if="menuBadgeLabel(child)" class="text-[0.62rem] font-bold px-1.5 py-px rounded-full bg-[var(--primary-color)] text-white leading-none">{{ menuBadgeLabel(child) }}</span>
</button>
</div>
<!-- Cadastro Rápido Dialog -->
<ComponentCadastroRapido
v-model="quickDialog"
title="Cadastro Rápido"
table-name="patients"
name-field="nome_completo"
email-field="email_principal"
phone-field="telefone"
:extra-payload="{ status: 'Ativo' }"
@created="quickDialog = false"
/>
</aside>
</Transition>
<!-- Item folha -->
<div v-else class="flex items-center gap-1">
<button
class="flex-1 flex items-center gap-2.5 px-2.5 py-2 rounded-[9px] border-none bg-transparent text-[var(--text-color-secondary)] cursor-pointer text-left text-[0.83rem] font-medium transition-colors hover:bg-[var(--surface-ground)] hover:text-[var(--text-color)]"
:class="{
'!bg-[color-mix(in_srgb,var(--primary-color)_10%,transparent)] !text-[var(--primary-color)] !font-semibold': isActive(item),
'opacity-55': isLocked(item)
}"
@click="navigate(item)"
>
<i v-if="item.icon" :class="item.icon" class="text-[1rem] shrink-0 opacity-75" />
<span class="flex-1">{{ item.label }}</span>
<span v-if="isLocked(item)" class="text-[0.58rem] font-extrabold uppercase tracking-widest px-1.5 py-px rounded border border-[var(--surface-border)] text-[var(--text-color-secondary)] opacity-70">PRO</span>
<span v-if="menuBadgeLabel(item)" class="text-[0.62rem] font-bold px-1.5 py-px rounded-full bg-[var(--primary-color)] text-white leading-none">{{ menuBadgeLabel(item) }}</span>
</button>
<button
v-if="item.quickCreate"
class="w-6 h-6 shrink-0 rounded-md border-none bg-transparent text-[var(--text-color-secondary)] cursor-pointer grid place-items-center text-xs transition-colors hover:bg-[var(--surface-ground)] hover:text-[var(--text-color)]"
@click.stop="openQuickCreate($event, item)"
title="Novo paciente"
>
<i class="pi pi-plus" />
</button>
</div>
</template>
</template>
</nav>
<!-- PatientCreatePopover (shared) -->
<PatientCreatePopover ref="createPopover" @quick-create="onQuickCreate" />
<!-- Cadastro Rápido Dialog -->
<ComponentCadastroRapido
v-model="quickDialog"
title="Cadastro Rápido"
table-name="patients"
name-field="nome_completo"
email-field="email_principal"
phone-field="telefone"
:extra-payload="{ status: 'Ativo' }"
@created="quickDialog = false"
/>
</aside>
</Transition>
</template>
<style scoped>
.panel-slide-enter-active,
.panel-slide-leave-active {
transition: width 0.22s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.18s ease;
overflow: hidden;
transition:
width 0.22s cubic-bezier(0.22, 1, 0.36, 1),
opacity 0.18s ease;
overflow: hidden;
}
.panel-slide-enter-from,
.panel-slide-leave-to {
width: 0 !important;
opacity: 0;
width: 0 !important;
opacity: 0;
}
</style>
</style>