193 lines
9.1 KiB
Vue
193 lines
9.1 KiB
Vue
<!--
|
|
|--------------------------------------------------------------------------
|
|
| Agência PSI
|
|
|--------------------------------------------------------------------------
|
|
| Criado e desenvolvido por Leonardo Nohama
|
|
|
|
|
| Arquivo: src/layout/AppMenuPopoverContent.vue
|
|
| Data: 2026
|
|
| Local: São Carlos/SP — Brasil
|
|
|--------------------------------------------------------------------------
|
|
| © 2026 — Todos os direitos reservados
|
|
|--------------------------------------------------------------------------
|
|
-->
|
|
<script setup>
|
|
import { computed, inject, nextTick } from 'vue';
|
|
import { useRouter } from 'vue-router';
|
|
|
|
import { sessionUser } from '@/app/session';
|
|
import { useRoleGuard } from '@/composables/useRoleGuard';
|
|
import { useLayout } from '@/layout/composables/layout';
|
|
import { supabase } from '@/lib/supabase/client';
|
|
import { useEntitlementsStore } from '@/stores/entitlementsStore';
|
|
import { useTenantFeaturesStore } from '@/stores/tenantFeaturesStore';
|
|
import { useTenantStore } from '@/stores/tenantStore';
|
|
|
|
const emit = defineEmits(['close']);
|
|
|
|
const router = useRouter();
|
|
const { role, canSee } = useRoleGuard();
|
|
const { isDarkTheme, toggleDarkMode } = useLayout();
|
|
|
|
const queuePatch = inject('queueUserSettingsPatch', null);
|
|
const openConfigurator = inject('openConfigurator', null);
|
|
|
|
// ── Identidade ───────────────────────────────────────────────────────────
|
|
const initials = computed(() => {
|
|
const name = sessionUser.value?.user_metadata?.full_name || sessionUser.value?.email || '';
|
|
const parts = String(name).trim().split(/\s+/).filter(Boolean);
|
|
const a = parts[0]?.[0] || 'U';
|
|
const b = parts.length > 1 ? parts[parts.length - 1][0] : '';
|
|
return (a + b).toUpperCase();
|
|
});
|
|
|
|
const label = computed(() => {
|
|
const name = sessionUser.value?.user_metadata?.full_name;
|
|
return name || sessionUser.value?.email || 'Conta';
|
|
});
|
|
|
|
const avatarUrl = computed(() => sessionUser.value?.user_metadata?.avatar_url || null);
|
|
|
|
// ── Navegação ────────────────────────────────────────────────────────────
|
|
async function safePush(target, fallback) {
|
|
try {
|
|
const r = router.resolve(target);
|
|
if (r?.matched?.length) return await router.push(target);
|
|
} catch (e) {
|
|
console.warn('Erro ao resolver rota:', e);
|
|
}
|
|
if (fallback) {
|
|
try {
|
|
return await router.push(fallback);
|
|
} catch (e) {
|
|
console.warn('Erro ao navegar para fallback:', e);
|
|
}
|
|
}
|
|
return router.push('/');
|
|
}
|
|
|
|
function goMyProfile() {
|
|
emit('close');
|
|
safePush({ name: 'account-profile' }, '/account/profile');
|
|
}
|
|
function goSecurity() {
|
|
emit('close');
|
|
safePush({ name: 'account-security' }, '/account/security');
|
|
}
|
|
function goSettings() {
|
|
emit('close');
|
|
if (canSee('settings.view')) return safePush({ name: 'ConfiguracoesAgenda' }, '/admin/settings');
|
|
return safePush({ name: 'portal-sessoes' }, '/portal');
|
|
}
|
|
|
|
// ── Dark mode + persistência ─────────────────────────────────────────────
|
|
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('[AppMenuPopoverContent][theme] falhou:', e?.message || e);
|
|
}
|
|
}
|
|
|
|
// ── Configurador ─────────────────────────────────────────────────────────
|
|
function handleOpenConfigurator() {
|
|
emit('close'); // fecha o popover primeiro
|
|
openConfigurator?.(); // abre o footer bar via AppLayout
|
|
}
|
|
|
|
// ── Sign out ─────────────────────────────────────────────────────────────
|
|
async function signOut() {
|
|
emit('close');
|
|
const tenant = useTenantStore();
|
|
const ent = useEntitlementsStore();
|
|
const tf = useTenantFeaturesStore();
|
|
try {
|
|
await supabase.auth.signOut();
|
|
} finally {
|
|
tenant.reset();
|
|
ent.invalidate();
|
|
tf.invalidate();
|
|
sessionStorage.clear();
|
|
localStorage.clear();
|
|
router.replace('/auth/login');
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="w-[224px] overflow-hidden rounded-[inherit]">
|
|
<!-- ── Header ── -->
|
|
<div class="flex items-center gap-2.5 px-3.5 pt-3.5 pb-3 bg-[var(--primary-color)]/[0.06] border-b border-[var(--surface-border)]">
|
|
<div class="relative shrink-0">
|
|
<img v-if="avatarUrl" :src="avatarUrl" class="w-9 h-9 rounded-[10px] object-cover ring-[1.5px] ring-[var(--primary-color)]/30" alt="avatar" />
|
|
<div v-else class="w-9 h-9 rounded-[10px] grid place-items-center text-base font-bold text-[var(--primary-color)] bg-[var(--primary-color)]/10 ring-[1.5px] ring-[var(--primary-color)]/20">{{ initials }}</div>
|
|
<span class="absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full bg-green-500 border-2 border-[var(--surface-card)]" />
|
|
</div>
|
|
<div class="min-w-0 flex flex-col gap-px">
|
|
<span class="text-base font-bold text-[var(--text-color)] truncate tracking-tight">{{ label }}</span>
|
|
<span class="text-[0.8rem] text-[var(--text-color-secondary)] truncate opacity-70">{{ sessionUser?.email }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Nav items ── -->
|
|
<div class="py-1.5">
|
|
<button class="group flex items-center gap-2.5 w-full px-3.5 py-[7px] text-sm font-medium text-[var(--text-color)] hover:bg-[var(--surface-ground)] hover:pl-4 transition-all duration-150" @click="goMyProfile">
|
|
<i class="pi pi-user text-[0.72rem] opacity-40 group-hover:opacity-100 group-hover:text-[var(--primary-color)] transition-all duration-150" />
|
|
Meu perfil
|
|
</button>
|
|
|
|
<button class="group flex items-center gap-2.5 w-full px-3.5 py-[7px] text-sm font-medium text-[var(--text-color)] hover:bg-[var(--surface-ground)] hover:pl-4 transition-all duration-150" @click="goSecurity">
|
|
<i class="pi pi-shield text-[0.72rem] opacity-40 group-hover:opacity-100 group-hover:text-[var(--primary-color)] transition-all duration-150" />
|
|
Segurança
|
|
</button>
|
|
|
|
<button
|
|
v-if="canSee('settings.view')"
|
|
class="group flex items-center gap-2.5 w-full px-3.5 py-[7px] text-sm font-medium text-[var(--text-color)] hover:bg-[var(--surface-ground)] hover:pl-4 transition-all duration-150"
|
|
@click="goSettings"
|
|
>
|
|
<i class="pi pi-cog text-[0.72rem] opacity-40 group-hover:opacity-100 group-hover:text-[var(--primary-color)] transition-all duration-150" />
|
|
Configurações
|
|
</button>
|
|
</div>
|
|
|
|
<!-- ── Preferências ── -->
|
|
<div class="border-t border-[var(--surface-border)] py-1.5">
|
|
<button class="group flex items-center gap-2.5 w-full px-3.5 py-[7px] text-sm font-medium text-[var(--text-color)] hover:bg-[var(--surface-ground)] hover:pl-4 transition-all duration-150" @click="toggleDarkAndPersist">
|
|
<i :class="['pi text-[0.72rem] opacity-40 group-hover:opacity-100 group-hover:text-[var(--primary-color)] transition-all duration-150', isDarkTheme ? 'pi-sun' : 'pi-moon']" />
|
|
{{ isDarkTheme ? 'Modo claro' : 'Modo escuro' }}
|
|
</button>
|
|
|
|
<button class="group flex items-center gap-2.5 w-full px-3.5 py-[7px] text-sm font-medium text-[var(--text-color)] hover:bg-[var(--surface-ground)] hover:pl-4 transition-all duration-150" @click="handleOpenConfigurator">
|
|
<i class="pi pi-palette text-[0.72rem] opacity-40 group-hover:opacity-100 group-hover:text-[var(--primary-color)] transition-all duration-150" />
|
|
Personalizar tema
|
|
</button>
|
|
</div>
|
|
|
|
<!-- ── Sair ── -->
|
|
<div class="border-t border-[var(--surface-border)] py-1.5">
|
|
<button class="group flex items-center gap-2.5 w-full px-3.5 py-[7px] text-sm font-medium text-red-500 hover:bg-red-500/[0.06] hover:pl-4 transition-all duration-150" @click="signOut">
|
|
<i class="pi pi-sign-out text-[0.72rem] opacity-60 group-hover:opacity-100 transition-opacity duration-150" />
|
|
Sair
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</template>
|