Enhance Role-Based Visibility and Navigation Logic
- Introduced role visibility checks in AreaTabs.vue to filter displayed areas based on the user's active role, improving user experience and accessibility. - Updated RoleSwitcher.vue to enhance accessibility with a high-contrast checkmark for better visibility. - Modified useActiveRole.ts to streamline role initialization and refresh logic for role-based product visibility. - Added explicit keys for role-based data fetching in product-related pages to ensure accurate data refresh. - Enhanced API endpoint for product retrieval to return 404 if a product is not accessible based on the user's role, ensuring security and clarity.
This commit is contained in:
@@ -3,6 +3,8 @@ import { Wrench, FlaskConical, Ticket, Sparkles, GraduationCap, Home } from 'luc
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
|
||||
type RoleCode = 'private' | 'educator' | 'company'
|
||||
|
||||
interface ProductArea {
|
||||
id: string
|
||||
label: string
|
||||
@@ -11,6 +13,7 @@ interface ProductArea {
|
||||
visible: boolean
|
||||
badge?: string
|
||||
route: string
|
||||
roleVisibility?: 'all' | RoleCode[]
|
||||
}
|
||||
|
||||
const areas: ProductArea[] = [
|
||||
@@ -21,6 +24,7 @@ const areas: ProductArea[] = [
|
||||
enabled: true,
|
||||
visible: true,
|
||||
route: '/',
|
||||
roleVisibility: 'all',
|
||||
},
|
||||
{
|
||||
id: 'makerspace',
|
||||
@@ -29,6 +33,7 @@ const areas: ProductArea[] = [
|
||||
enabled: true,
|
||||
visible: true,
|
||||
route: '/products',
|
||||
roleVisibility: 'all',
|
||||
},
|
||||
{
|
||||
id: 'educator',
|
||||
@@ -38,6 +43,7 @@ const areas: ProductArea[] = [
|
||||
visible: true,
|
||||
badge: 'Demnächst',
|
||||
route: '/educator',
|
||||
roleVisibility: ['educator'],
|
||||
},
|
||||
{
|
||||
id: 'experimenta',
|
||||
@@ -47,6 +53,7 @@ const areas: ProductArea[] = [
|
||||
visible: true,
|
||||
badge: 'Demnächst',
|
||||
route: '/experimenta',
|
||||
roleVisibility: 'all',
|
||||
},
|
||||
{
|
||||
id: 'labs',
|
||||
@@ -56,22 +63,41 @@ const areas: ProductArea[] = [
|
||||
visible: false,
|
||||
badge: 'Demnächst',
|
||||
route: '/labs',
|
||||
roleVisibility: ['educator', 'company'],
|
||||
},
|
||||
]
|
||||
|
||||
const route = useRoute()
|
||||
const { activeRole } = useActiveRole()
|
||||
|
||||
// Filter areas by role visibility
|
||||
const visibleAreas = computed(() => {
|
||||
return areas.filter(area => {
|
||||
// Legacy visible flag check
|
||||
if (!area.visible) return false
|
||||
|
||||
// No role requirement = visible to all (backward compatible)
|
||||
if (!area.roleVisibility) return true
|
||||
|
||||
// Explicitly set to 'all'
|
||||
if (area.roleVisibility === 'all') return true
|
||||
|
||||
// Check if user's active role matches
|
||||
return area.roleVisibility.includes(activeRole.value as RoleCode)
|
||||
})
|
||||
})
|
||||
|
||||
const currentArea = computed(() => {
|
||||
// Determine current area based on route - check areas array dynamically
|
||||
// Determine current area based on route - check visibleAreas array dynamically
|
||||
const currentPath = route.path
|
||||
|
||||
// Exact match for root path
|
||||
if (currentPath === '/') {
|
||||
return areas.find(area => area.route === '/')?.id || ''
|
||||
return visibleAreas.value.find(area => area.route === '/')?.id || ''
|
||||
}
|
||||
|
||||
// Find area where route path starts with area.route
|
||||
const matchedArea = areas.find(area =>
|
||||
const matchedArea = visibleAreas.value.find(area =>
|
||||
area.route !== '/' && currentPath.startsWith(area.route)
|
||||
)
|
||||
|
||||
@@ -90,7 +116,7 @@ function navigateToArea(area: ProductArea) {
|
||||
<!-- Desktop: Tabs -->
|
||||
<Tabs :model-value="currentArea" class="hidden md:block">
|
||||
<TabsList class="h-auto p-2 bg-white/5">
|
||||
<TabsTrigger v-for="area in areas.filter(area => area.visible)" :key="area.id" :value="area.id"
|
||||
<TabsTrigger v-for="area in visibleAreas" :key="area.id" :value="area.id"
|
||||
:disabled="!area.enabled" :class="[
|
||||
'gap-2 py-3 md:py-4 data-[state=active]:bg-accent data-[state=active]:text-white data-[state=active]:shadow-md',
|
||||
!area.enabled && 'opacity-50 cursor-not-allowed',
|
||||
@@ -112,7 +138,7 @@ function navigateToArea(area: ProductArea) {
|
||||
<!-- Mobile: Horizontal scroll with cards (matching desktop styling) -->
|
||||
<div class="md:hidden overflow-x-auto scrollbar-hide">
|
||||
<div class="inline-flex h-auto items-center justify-center rounded-[35px] bg-white/5 p-2 min-w-max">
|
||||
<button v-for="area in areas.filter(area => area.visible)" :key="area.id" :disabled="!area.enabled" :class="[
|
||||
<button v-for="area in visibleAreas" :key="area.id" :disabled="!area.enabled" :class="[
|
||||
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-[25px] px-4 py-3 text-lg font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-0',
|
||||
currentArea === area.id
|
||||
? 'bg-accent text-white shadow-md'
|
||||
|
||||
@@ -83,7 +83,7 @@ async function handleRoleSwitch(roleCode: string, hasRole: boolean) {
|
||||
</Alert>
|
||||
|
||||
<DropdownMenuLabel class="text-sm font-normal text-muted-foreground py-3">
|
||||
Ich kaufe als...
|
||||
Du kaufst als...
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
@@ -103,8 +103,13 @@ async function handleRoleSwitch(roleCode: string, hasRole: boolean) {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Checkmark if active -->
|
||||
<Check v-if="activeRole === role.code && role.hasRole" class="h-5 w-5 text-success flex-shrink-0" />
|
||||
<!-- High-contrast checkmark for colorblind accessibility -->
|
||||
<div v-if="activeRole === role.code && role.hasRole" 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>
|
||||
|
||||
<!-- Badge if not approved -->
|
||||
<Badge v-if="!role.hasRole" variant="secondary" class="text-xs px-2 py-0.5 flex-shrink-0">
|
||||
|
||||
@@ -36,7 +36,6 @@ export function useActiveRole() {
|
||||
const roleChangedByAdmin = useState<boolean>('roleChangedByAdmin', () => false)
|
||||
const loading = useState<boolean>('roleLoading', () => false)
|
||||
const error = useState<string | null>('roleError', () => null)
|
||||
const initialized = useState<boolean>('roleInitialized', () => false)
|
||||
|
||||
/**
|
||||
* Fetch current role status from server
|
||||
@@ -88,8 +87,13 @@ export function useActiveRole() {
|
||||
activeRole.value = roleCode
|
||||
roleChangedByAdmin.value = false
|
||||
|
||||
// Refresh products if on products page
|
||||
await refreshNuxtData('products')
|
||||
// Refresh all product list pages (role-based visibility)
|
||||
// Note: Product detail pages will handle visibility via API 404 check
|
||||
await Promise.all([
|
||||
refreshNuxtData('products-list'), // Main products page
|
||||
refreshNuxtData('educator-products'), // Educator products page
|
||||
refreshNuxtData('experimenta-products'), // Experimenta products page
|
||||
])
|
||||
|
||||
return true
|
||||
} catch (err: any) {
|
||||
@@ -112,17 +116,13 @@ export function useActiveRole() {
|
||||
const hasMultipleRoles = computed(() => approvedRoles.value.length > 1)
|
||||
|
||||
/**
|
||||
* Auto-initialize on first use (fetch role status from server)
|
||||
* This ensures the role is correct immediately after login
|
||||
* Auto-fetch roles when user logs in
|
||||
* This ensures the role button shows the correct role immediately after login
|
||||
* Uses callOnce to prevent redundant API calls
|
||||
*/
|
||||
const { loggedIn } = useUserSession()
|
||||
if (!initialized.value && loggedIn.value) {
|
||||
initialized.value = true
|
||||
// Fetch initial role status (don't await to avoid blocking)
|
||||
fetchRoleStatus().catch((err) => {
|
||||
// Silent fail - user can still use the app with default role
|
||||
console.warn('Failed to initialize role status:', err)
|
||||
})
|
||||
if (loggedIn.value) {
|
||||
callOnce('init-roles', () => fetchRoleStatus())
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -27,6 +27,7 @@ interface Product {
|
||||
|
||||
// Fetch products from API - only educator passes
|
||||
const { data: products, error, pending } = await useFetch<Product[]>('/api/products', {
|
||||
key: 'educator-products', // Explicit key for role-based refresh
|
||||
query: {
|
||||
category: 'educator-annual-pass',
|
||||
},
|
||||
|
||||
@@ -27,6 +27,7 @@ interface Product {
|
||||
|
||||
// Fetch products from API - only experimenta passes
|
||||
const { data: products, error, pending } = await useFetch<Product[]>('/api/products', {
|
||||
key: 'experimenta-products', // Explicit key for role-based refresh
|
||||
query: {
|
||||
category: 'annual-pass',
|
||||
},
|
||||
|
||||
@@ -33,7 +33,24 @@ interface Product {
|
||||
}
|
||||
|
||||
// Fetch product from API
|
||||
const { data: product, error, pending } = await useFetch<Product>(`/api/products/${productId}`)
|
||||
const { data: product, error, pending } = await useFetch<Product>(`/api/products/${productId}`, {
|
||||
key: `product-${productId}`, // Explicit key for role-based refresh
|
||||
})
|
||||
|
||||
// Auto-redirect to products list if 404 (e.g., after role switch)
|
||||
watch(error, (newError) => {
|
||||
if (newError?.statusCode === 404) {
|
||||
// Show notification
|
||||
toast.error('Produkt nicht verfügbar', {
|
||||
description: 'Dieses Produkt ist für deine aktuelle Rolle nicht sichtbar.',
|
||||
})
|
||||
|
||||
// Redirect after short delay
|
||||
setTimeout(() => {
|
||||
navigateTo('/products')
|
||||
}, 1500)
|
||||
}
|
||||
})
|
||||
|
||||
// Format price in EUR
|
||||
const formattedPrice = computed(() => {
|
||||
|
||||
@@ -27,6 +27,7 @@ interface Product {
|
||||
|
||||
// Fetch products from API - only Makerspace and Educator passes
|
||||
const { data: products, error, pending } = await useFetch<Product[]>('/api/products', {
|
||||
key: 'products-list', // Explicit key for role-based refresh
|
||||
query: {
|
||||
category: 'makerspace-annual-pass,educator-annual-pass',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user