Implement Role Management Features and UI Enhancements
- Introduced a new composable `useActiveRole` for managing user roles, including fetching role status and switching roles with server validation. - Updated `RoleSwitcher.vue` to utilize the new composable, enhancing role selection with improved error handling and UI feedback. - Added new API endpoints for role management, including fetching user role status and switching active roles. - Enhanced product visibility logic to filter based on the user's active role, ensuring a tailored experience. - Updated database schema to support last active role tracking for users, improving session management. - Refined UI components across the application to reflect role-based changes and improve user experience.
This commit is contained in:
@@ -74,7 +74,6 @@ async function handleLogout() {
|
||||
<!-- Avatar with status ring (logged in indicator) -->
|
||||
<Avatar
|
||||
class="h-10 w-10 md:h-12 md:w-12 border-3 border-experimenta-purple shadow-lg bg-experimenta-accent ring-2 ring-experimenta-accent ring-offset-2 ring-offset-white dark:ring-offset-zinc-950">
|
||||
<AvatarImage :src="undefined" :alt="extendedUser?.firstName || ''" />
|
||||
<AvatarFallback
|
||||
class="bg-experimenta-accent text-white font-bold text-sm md:text-base flex items-center justify-center w-full h-full">
|
||||
{{ userInitials }}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { User, GraduationCap, Building2, Check } from 'lucide-vue-next'
|
||||
import { User, GraduationCap, Building2, Check, AlertCircle, ChevronDown } from 'lucide-vue-next'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -11,110 +12,103 @@ import {
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
// User role types matching our database schema
|
||||
type UserRole = 'private' | 'educator' | 'company'
|
||||
// Role management composable
|
||||
const { activeRole, roles, roleChangedByAdmin, switchRole, fetchRoleStatus, loading } =
|
||||
useActiveRole()
|
||||
|
||||
interface RoleOption {
|
||||
value: UserRole
|
||||
label: string
|
||||
description: string
|
||||
icon: any
|
||||
color: string
|
||||
enabled: boolean
|
||||
badge?: string
|
||||
// Icon mapping for each role
|
||||
const roleIcons: Record<string, any> = {
|
||||
private: User,
|
||||
educator: GraduationCap,
|
||||
company: Building2,
|
||||
}
|
||||
|
||||
const roles: RoleOption[] = [
|
||||
{
|
||||
value: 'private',
|
||||
label: 'Privatperson',
|
||||
description: 'Private Nutzung',
|
||||
icon: User,
|
||||
color: 'text-purple-600',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
value: 'educator',
|
||||
label: 'Pädagoge',
|
||||
description: 'Lehrkräfte und Schulen',
|
||||
icon: GraduationCap,
|
||||
color: 'text-orange-500',
|
||||
enabled: true,
|
||||
// badge: 'Demnächst',
|
||||
},
|
||||
{
|
||||
value: 'company',
|
||||
label: 'Firma',
|
||||
description: 'Geschäftskunden',
|
||||
icon: Building2,
|
||||
color: 'text-blue-600',
|
||||
enabled: true,
|
||||
// badge: 'Demnächst',
|
||||
},
|
||||
]
|
||||
// Color mapping for each role
|
||||
const roleColors: Record<string, string> = {
|
||||
private: 'text-purple-600',
|
||||
educator: 'text-orange-500',
|
||||
company: 'text-blue-600',
|
||||
}
|
||||
|
||||
// Current role - will come from user session later
|
||||
const currentRole = ref<UserRole>('private')
|
||||
// Dropdown open/close state
|
||||
const dropdownOpen = ref(false)
|
||||
|
||||
const currentRoleData = computed(() => {
|
||||
return roles.find((r) => r.value === currentRole.value) || roles[0]
|
||||
// Fetch role status when dropdown opens
|
||||
watch(dropdownOpen, async (isOpen) => {
|
||||
if (isOpen && !loading.value) {
|
||||
await fetchRoleStatus()
|
||||
}
|
||||
})
|
||||
|
||||
function switchRole(role: UserRole) {
|
||||
if (roles.find((r) => r.value === role)?.enabled) {
|
||||
currentRole.value = role
|
||||
// TODO: Update user session/store with new role
|
||||
// Current role data (for button display)
|
||||
const currentRoleData = computed(() => roles.value.find((r) => r.code === activeRole.value))
|
||||
|
||||
// Handle role switch with error handling
|
||||
async function handleRoleSwitch(roleCode: string, hasRole: boolean) {
|
||||
if (!hasRole || loading.value) return
|
||||
|
||||
try {
|
||||
await switchRole(roleCode)
|
||||
dropdownOpen.value = false // Close dropdown after successful switch
|
||||
} catch (error) {
|
||||
console.error('Role switch failed:', error)
|
||||
// Error is already set in composable, will be displayed if needed
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenu>
|
||||
<dropdownMenuTrigger as-child>
|
||||
<DropdownMenu v-model:open="dropdownOpen">
|
||||
<DropdownMenuTrigger as-child>
|
||||
<Button variant="outline"
|
||||
class="gap-2 border-2 border-white/30 bg-white/5 text-white hover:bg-white/10 hover:border-white/50 transition-all duration-200 group"
|
||||
title="Rolle wechseln">
|
||||
<!-- Context label + role -->
|
||||
<span class="text-xs text-white/70 font-normal hidden md:inline">Du kaufst als:</span>
|
||||
<component :is="currentRoleData.icon" class="h-4 w-4 text-white" />
|
||||
<span class="font-medium text-white">{{ currentRoleData.label }}</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
class="ml-1 opacity-70 text-white group-hover:opacity-100 transition-opacity">
|
||||
<polyline points="6 9 12 15 18 9" />
|
||||
</svg>
|
||||
<component :is="roleIcons[activeRole] || User" class="h-4 w-4 text-white" />
|
||||
<span class="font-medium text-white">{{
|
||||
currentRoleData?.displayName || 'Privatperson'
|
||||
}}</span>
|
||||
<ChevronDown class="h-4 w-4 text-white/70 group-hover:text-white transition-colors" />
|
||||
</Button>
|
||||
</dropdownMenuTrigger>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent align="start" class="w-[312px]">
|
||||
<!-- Admin Notice (if role was changed by admin) -->
|
||||
<Alert v-if="roleChangedByAdmin" variant="warning" class="m-2 mb-3">
|
||||
<AlertCircle class="h-4 w-4" />
|
||||
<AlertDescription class="text-sm">
|
||||
Deine Rolle wurde von einem Administrator geändert.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<DropdownMenuLabel class="text-sm font-normal text-muted-foreground py-3">
|
||||
Ich kaufe als...
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem v-for="role in roles" :key="role.value" :disabled="!role.enabled" :class="[
|
||||
<!-- All roles (approved + not-approved) -->
|
||||
<DropdownMenuItem v-for="role in roles" :key="role.code" :disabled="!role.hasRole || loading" :class="[
|
||||
'gap-3 py-3 cursor-pointer group',
|
||||
!role.enabled && 'opacity-60 cursor-not-allowed',
|
||||
currentRole === role.value && 'bg-purple-50 dark:bg-purple-950',
|
||||
]" @click="switchRole(role.value)">
|
||||
<component :is="role.icon" :class="['h-5 w-5', role.color]" />
|
||||
!role.hasRole && 'opacity-60 cursor-not-allowed',
|
||||
activeRole === role.code && 'bg-purple-50 dark:bg-purple-950',
|
||||
]" @click="handleRoleSwitch(role.code, role.hasRole)">
|
||||
<component :is="roleIcons[role.code]" :class="['h-5 w-5', roleColors[role.code]]" />
|
||||
|
||||
<div class="flex-1 flex flex-col gap-1">
|
||||
<span class="text-sm font-semibold">{{ role.label }}</span>
|
||||
<span class="text-sm font-semibold">{{ role.displayName }}</span>
|
||||
<span
|
||||
class="text-sm text-muted-foreground group-hover:text-foreground group-data-[highlighted]:text-accent-foreground transition-colors">{{
|
||||
role.description }}</span>
|
||||
class="text-sm text-muted-foreground group-hover:text-foreground group-data-[highlighted]:text-accent-foreground transition-colors">
|
||||
{{ role.description }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- High-contrast checkmark for colorblind accessibility -->
|
||||
<div v-if="currentRole === role.value" class="relative flex-shrink-0">
|
||||
<!-- Background circle for contrast -->
|
||||
<div class="absolute inset-0 bg-white dark:bg-gray-900 rounded-full opacity-90"></div>
|
||||
<!-- Checkmark icon with high contrast -->
|
||||
<Check class="h-5 w-5 text-gray-900 dark:text-white relative z-10" />
|
||||
</div>
|
||||
<!-- Checkmark if active -->
|
||||
<Check v-if="activeRole === role.code && role.hasRole" class="h-5 w-5 text-success flex-shrink-0" />
|
||||
|
||||
<Badge v-if="role.badge" variant="secondary" class="text-xs px-2 py-0.5 flex-shrink-0">
|
||||
{{ role.badge }}
|
||||
<!-- Badge if not approved -->
|
||||
<Badge v-if="!role.hasRole" variant="secondary" class="text-xs px-2 py-0.5 flex-shrink-0">
|
||||
{{ role.requiresApproval ? 'Demnächst' : 'Gesperrt' }}
|
||||
</Badge>
|
||||
</DropdownMenuItem>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user