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:
@@ -0,0 +1,192 @@
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| 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>
|
||||
Reference in New Issue
Block a user