feat(appshell): add user-info card to sidebar bottom; remove topbar avatar

Consolidates the user menu into a single sidebar-bottom location.
SidebarUserCard.vue shows avatar (initial), full name, role (Dutch
label mapped from org pivot role or 'Super Admin' fallback) and a
chevron-up that opens a PrimeVue Menu with "Mijn Profiel" and
"Uitloggen". The Menu uses popup mode; PrimeVue v4's absolutePosition
logic auto-flips above the trigger when the panel would overflow the
viewport bottom — verify in Phase C.

AppShell loses the topbar avatar Button + Menu and the associated
state (userMenuRef, userInitial, userMenuItems, toggleUserMenu) plus
its imports (Avatar, Menu, useAuthStore, computed). The component is
now a pure layout shell with no auth-store coupling. The topbar's
right side is intentionally empty in this commit; B4 fills it with
breadcrumb / notification bell / help icon.

Layout: nav uses min-h-0 flex-1 overflow-y-auto so it shrinks under
viewport pressure and lets the user card stay pinned at the bottom
of the sidebar. Mobile Drawer's content pt-override sets the same
flex-column behaviour so the user card sits flush at the bottom of
the drawer overlay.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-12 00:43:54 +02:00
parent 4089a14bb8
commit f8fddc0e14
2 changed files with 123 additions and 61 deletions

View File

@@ -16,14 +16,11 @@
// PrimeVue Drawer overlay. Content area renders the default slot
// (a RouterView from the wrapping layout file).
import { computed, ref } from 'vue'
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import Drawer from 'primevue/drawer'
import Button from 'primevue/button'
import Avatar from 'primevue/avatar'
import Menu from 'primevue/menu'
import Icon from '@/components/Icon.vue'
import { useAuthStore } from '@/stores/useAuthStore'
interface NavHeading {
heading: string
@@ -45,37 +42,8 @@ withDefaults(defineProps<Props>(), {
})
const router = useRouter()
const authStore = useAuthStore()
const mobileNavOpen = ref(false)
const userMenuRef = ref<InstanceType<typeof Menu> | null>(null)
const userInitial = computed(() => {
const name = authStore.user?.full_name ?? ''
return name.charAt(0).toUpperCase() || '?'
})
const userMenuItems = computed(() => [
{
label: authStore.user?.full_name ?? 'Gebruiker',
items: [
{
label: 'Mijn Profiel',
icon: 'tabler-user',
command: () => router.push({ name: 'account-settings' }),
},
{
label: 'Uitloggen',
icon: 'tabler-logout',
command: async () => {
await authStore.logout()
await router.push('/login')
},
},
],
},
])
function isHeading(item: NavItem): item is NavHeading {
return 'heading' in item
@@ -85,10 +53,6 @@ function navigate(item: NavLink) {
mobileNavOpen.value = false
router.push(item.to)
}
function toggleUserMenu(event: Event) {
userMenuRef.value?.toggle(event)
}
</script>
<template>
@@ -96,7 +60,7 @@ function toggleUserMenu(event: Event) {
<!-- Desktop sidebar (lg+) -->
<aside class="hidden lg:flex w-72 flex-col border-r border-surface-200 bg-surface-0">
<SidebarHeader :title="title" />
<nav class="flex-1 overflow-y-auto p-3">
<nav class="min-h-0 flex-1 overflow-y-auto p-3">
<template
v-for="(item, idx) in navItems"
:key="idx"
@@ -121,6 +85,7 @@ function toggleUserMenu(event: Event) {
</button>
</template>
</nav>
<SidebarUserCard />
</aside>
<!-- Mobile drawer (overlay, <lg) -->
@@ -128,12 +93,16 @@ function toggleUserMenu(event: Event) {
v-model:visible="mobileNavOpen"
position="left"
class="lg:hidden"
:pt="{ root: { style: { width: '18rem' } }, header: { class: 'p-0' } }"
:pt="{
root: { style: { width: '18rem' } },
header: { class: 'p-0' },
content: { class: 'flex flex-col flex-1 min-h-0 p-0' },
}"
>
<template #header>
<SidebarHeader :title="title" />
</template>
<nav class="flex flex-col p-3">
<nav class="min-h-0 flex-1 overflow-y-auto p-3">
<template
v-for="(item, idx) in navItems"
:key="idx"
@@ -158,6 +127,7 @@ function toggleUserMenu(event: Event) {
</button>
</template>
</nav>
<SidebarUserCard />
</Drawer>
<!-- Main column -->
@@ -180,27 +150,6 @@ function toggleUserMenu(event: Event) {
</Button>
<span class="text-base font-medium text-surface-700 lg:hidden">{{ title }}</span>
</div>
<div class="flex items-center gap-2">
<Button
severity="secondary"
text
rounded
aria-label="Gebruikersmenu openen"
@click="toggleUserMenu"
>
<Avatar
:label="userInitial"
shape="circle"
class="bg-primary-500 text-white"
/>
</Button>
<Menu
ref="userMenuRef"
:model="userMenuItems"
:popup="true"
/>
</div>
</header>
<!-- Page content -->

View File

@@ -0,0 +1,113 @@
<script setup lang="ts">
// SidebarUserCard — bottom-of-sidebar user identity + menu, introduced
// in F3.5 per Bert's A6.5 decision. Consolidates the user menu in a
// single location (sidebar bottom) and removes the topbar avatar
// dropdown, eliminating two paths to the same Logout / Profile actions.
//
// The PrimeVue Menu in popup mode auto-flips above the trigger when
// the panel would overflow the viewport bottom (per PrimeVue v4's
// DomHandler.absolutePosition logic). The card sits flush against the
// bottom of the sidebar, so the dropdown reliably opens upward.
import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
import Menu from 'primevue/menu'
import Avatar from 'primevue/avatar'
import Icon from '@/components/Icon.vue'
import { useAuthStore } from '@/stores/useAuthStore'
const router = useRouter()
const authStore = useAuthStore()
const menuRef = ref<InstanceType<typeof Menu> | null>(null)
const userInitial = computed(() => {
const name = authStore.user?.full_name ?? ''
return name.charAt(0).toUpperCase() || '?'
})
const userName = computed(() => authStore.user?.full_name ?? 'Gebruiker')
const roleLabels: Record<string, string> = {
super_admin: 'Super Admin',
org_admin: 'Beheerder',
org_member: 'Lid',
event_manager: 'Eventmanager',
staff_coordinator: 'Staf coördinator',
volunteer_coordinator: 'Vrijwilligers coördinator',
}
const roleLabel = computed(() => {
const orgRole = authStore.currentOrganisation?.role
if (orgRole)
return roleLabels[orgRole] ?? orgRole
if (authStore.isSuperAdmin)
return 'Super Admin'
return ''
})
const menuItems = computed(() => [
{
label: userName.value,
items: [
{
label: 'Mijn Profiel',
icon: 'tabler-user',
command: () => router.push({ name: 'account-settings' }),
},
{
label: 'Uitloggen',
icon: 'tabler-logout',
command: async () => {
await authStore.logout()
await router.push('/login')
},
},
],
},
])
function toggleMenu(event: Event) {
menuRef.value?.toggle(event)
}
</script>
<template>
<div class="border-t border-surface-200">
<button
type="button"
class="flex w-full items-center gap-3 px-3 py-3 text-left transition hover:bg-surface-100"
aria-label="Gebruikersmenu openen"
aria-haspopup="menu"
@click="toggleMenu"
>
<Avatar
:label="userInitial"
shape="circle"
class="shrink-0 bg-primary-500 text-white"
/>
<div class="flex min-w-0 flex-1 flex-col">
<span class="truncate text-sm font-semibold text-surface-900">
{{ userName }}
</span>
<span
v-if="roleLabel"
class="truncate text-xs text-surface-500"
>{{ roleLabel }}</span>
</div>
<Icon
name="tabler-chevron-up"
size="18"
class="shrink-0 text-surface-500"
/>
</button>
<Menu
ref="menuRef"
:model="menuItems"
:popup="true"
/>
</div>
</template>