carousel, agenda arquivados, agenda cor, agenda arquivados, grupos pacientes, pacientes arquivados - desativados, sessoes verificadas, ajuste notificações, Prontuario, Agenda Animation, Menu Profile, bagdes Profile, Offline

This commit is contained in:
Leonardo
2026-03-18 09:26:09 -03:00
parent 66f67cd40f
commit d6d2fe29d1
55 changed files with 3655 additions and 1512 deletions

View File

@@ -6,20 +6,17 @@ import Popover from 'primevue/popover'
import { sessionUser, sessionRole } from '@/app/session'
import { supabase } from '@/lib/supabase/client'
import { useRoleGuard } from '@/composables/useRoleGuard'
const router = useRouter()
const pop = ref(null)
const props = defineProps({
variant: { type: String, default: 'sidebar' }
})
const router = useRouter()
const pop = ref(null)
// ------------------------------------------------------
// RBAC (Tenant): fonte da verdade para permissões por papel
// ------------------------------------------------------
const { role, canSee } = useRoleGuard()
// ------------------------------------------------------
// UI labels (nome/iniciais)
// ------------------------------------------------------
const initials = computed(() => {
const name = sessionUser.value?.user_metadata?.full_name || sessionUser.value?.email || ''
const parts = String(name).trim().split(/\s+/).filter(Boolean)
@@ -33,167 +30,181 @@ const label = computed(() => {
return name || sessionUser.value?.email || 'Conta'
})
/**
* sublabel:
* Prefere exibir o papel do TENANT (role do useRoleGuard),
* porque governa a UI dentro da clínica.
*/
const sublabel = computed(() => {
const r = role.value || sessionRole.value
if (!r) return 'Sessão'
// tenant roles
if (r === 'clinic_admin' || r === 'tenant_admin' || r === 'admin') return 'Administrador'
if (r === 'therapist') return 'Terapeuta'
// portal/global roles
if (r === 'portal_user') return 'Portal'
if (r === 'patient') return 'Portal' // legado (caso ainda exista em algum lugar)
if (r === 'portal_user' || r === 'patient') return 'Portal'
return r
})
// ------------------------------------------------------
// Popover helpers
// ------------------------------------------------------
function toggle (e) {
pop.value?.toggle(e)
}
const avatarUrl = computed(() => sessionUser.value?.user_metadata?.avatar_url || null)
function close () {
try { pop.value?.hide() } catch {}
}
function toggle (e) { pop.value?.toggle(e) }
function close () { try { pop.value?.hide() } catch {} }
// ------------------------------------------------------
// Navegação segura (resolve antes; fallback se não existir)
// ------------------------------------------------------
async function safePush (target, fallback) {
try {
const r = router.resolve(target)
if (r?.matched?.length) return await router.push(target)
} catch {}
if (fallback) {
try { return await router.push(fallback) } catch {}
}
if (fallback) { try { return await router.push(fallback) } catch {} }
return router.push('/')
}
// ------------------------------------------------------
// Actions
// ------------------------------------------------------
function goMyProfile () {
function goMyProfile () { close(); safePush({ name: 'account-profile' }, '/account/profile') }
function goSecurity () { close(); safePush({ name: 'account-security' }, '/account/security') }
function goSettings () {
close()
safePush({ name: 'account-profile' }, '/account/profile')
}
function goSettings () {
close()
// ✅ Configurações é RBAC (quem pode ver, vê)
if (canSee('settings.view')) {
return safePush({ name: 'ConfiguracoesAgenda' }, '/admin/settings') // fallback genérico
}
// ✅ quem não pode (ex.: paciente), manda pro portal correto
if (canSee('settings.view')) return safePush({ name: 'ConfiguracoesAgenda' }, '/admin/settings')
return safePush({ name: 'portal-sessoes' }, '/portal')
}
function goSecurity () {
close()
// ✅ Segurança é "Account": todos podem acessar
return safePush(
{ name: 'account-security' },
'/account/security'
)
}
async function signOut () {
close()
try {
await supabase.auth.signOut()
} catch {
// se falhar, ainda assim manda pro login
} finally {
router.push('/auth/login')
}
try { await supabase.auth.signOut() } catch {}
finally { router.push('/auth/login') }
}
defineExpose({ toggle })
</script>
<template>
<div class="sticky bottom-0 z-20 border-t border-[var(--surface-border)] bg-[var(--surface-card)]">
<button
type="button"
class="w-full px-3 py-3 flex items-center gap-3 hover:bg-[var(--surface-ground)] transition"
@click="toggle"
>
<!-- avatar -->
<img
v-if="sessionUser?.user_metadata?.avatar_url"
:src="sessionUser.user_metadata.avatar_url"
class="h-9 w-9 rounded-xl object-cover border border-[var(--surface-border)]"
alt="avatar"
/>
<div
v-else
class="h-9 w-9 rounded-xl border border-[var(--surface-border)] bg-[var(--surface-ground)] grid place-items-center text-sm font-semibold"
<!-- SIDEBAR: trigger + popover -->
<template v-if="variant === 'sidebar'">
<div class="sticky bottom-0 z-20 border-t border-[var(--surface-border)] bg-[var(--surface-card)]">
<button
type="button"
class="w-full px-3 py-3 flex items-center gap-3 hover:bg-[var(--surface-ground)] transition-colors duration-150"
@click="toggle"
>
{{ initials }}
</div>
<!-- labels -->
<div class="min-w-0 flex-1 text-left">
<div class="truncate text-sm font-semibold text-[var(--text-color)]">
{{ label }}
<img v-if="avatarUrl" :src="avatarUrl" class="h-9 w-9 rounded-xl object-cover border border-[var(--surface-border)]" alt="avatar" />
<div v-else class="h-9 w-9 rounded-xl border border-[var(--surface-border)] bg-[var(--surface-ground)] grid place-items-center text-sm font-semibold">{{ initials }}</div>
<div class="min-w-0 flex-1 text-left">
<div class="truncate text-sm font-semibold text-[var(--text-color)]">{{ label }}</div>
<div class="truncate text-xs text-[var(--text-color-secondary)]">{{ sublabel }}</div>
</div>
<div class="truncate text-xs text-[var(--text-color-secondary)]">
{{ sublabel }}
</div>
</div>
<i class="pi pi-angle-up text-xs opacity-40" />
</button>
<i class="pi pi-angle-up text-xs opacity-70" />
</button>
<Popover ref="pop" appendTo="body">
<!-- conteúdo reutilizado via template inline -->
<template v-if="true">
<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-[1rem] 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-[1rem] font-bold text-[var(--text-color)] truncate tracking-tight">{{ label }}</span>
<span class="text-[0.85rem] 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-[1rem] 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-[1rem] 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-[1rem] 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>
<!-- Footer: 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-[1rem] 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>
</Popover>
</div>
</template>
<!-- RAIL: o popover, trigger externo -->
<template v-else>
<Popover ref="pop" appendTo="body">
<div class="min-w-[220px] p-1">
<Button
v-if="canSee('settings.view')"
label="Configurações"
icon="pi pi-cog"
text
class="w-full justify-start"
@click="goSettings"
/>
<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-[1rem] 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-[1rem] font-bold text-[var(--text-color)] truncate tracking-tight">{{ label }}</span>
<span class="text-[0.85rem] text-[var(--text-color-secondary)] truncate opacity-70">{{ sessionUser?.email }}</span>
</div>
</div>
<Button
label="Segurança"
icon="pi pi-shield"
text
class="w-full justify-start"
@click="goSecurity"
/>
<!-- Nav items -->
<div class="py-1.5">
<button
class="group flex items-center gap-2.5 w-full px-3.5 py-[7px] text-[1rem] 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-[1rem] 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-[1rem] 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>
<Button
label="Meu Perfil"
icon="pi pi-user"
text
class="w-full justify-start"
@click="goMyProfile"
/>
<div class="my-1 border-t border-[var(--surface-border)]" />
<Button
label="Sair"
icon="pi pi-sign-out"
severity="danger"
text
class="w-full justify-start"
@click="signOut"
/>
<!-- Footer: 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-[1rem] 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>
</Popover>
</div>
</template>
</template>
</template>
<style>
/* Zero padding nativo do PrimeVue — mínimo inevitável pois não há prop pra isso */
.p-popover-content { padding: 0 !important; }
</style>