Files
service-finder/frontend_admin/layouts/default.vue
2026-07-24 09:56:21 +00:00

545 lines
28 KiB
Vue

<template>
<div class="min-h-screen bg-slate-900 text-white flex">
<!-- Sidebar Overlay (mobile) -->
<div
v-if="sidebarOpen"
class="fixed inset-0 bg-black/50 z-20 lg:hidden"
@click="sidebarOpen = false"
/>
<!-- Sidebar -->
<aside
:class="[
'fixed lg:static inset-y-0 left-0 z-30 flex flex-col bg-slate-800 border-r border-slate-700 transition-all duration-300 ease-in-out overflow-hidden',
sidebarOpen || !sidebarCollapsed ? 'w-52' : 'w-20',
]"
>
<!-- Logo Area -->
<NuxtLink to="/" class="flex items-center h-16 px-4 border-b border-slate-700 overflow-hidden hover:bg-slate-700/50 transition">
<div class="flex items-center gap-3 min-w-0">
<img src="/logo.svg" class="h-8 w-auto flex-shrink-0" alt="SF Logo" />
<div
class="transition-all duration-300 ease-in-out overflow-hidden whitespace-nowrap"
:class="sidebarOpen || !sidebarCollapsed ? 'opacity-100 max-w-[200px]' : 'opacity-0 max-w-0'"
>
<div class="text-sm font-bold text-white leading-tight">{{ $t('common.service_finder') }}</div>
<div class="text-[10px] font-semibold uppercase tracking-wider text-indigo-400 leading-tight">{{ $t('common.admin') }}</div>
</div>
</div>
</NuxtLink>
<!-- Collapse Toggle (desktop) -->
<button
class="hidden lg:flex items-center justify-center h-10 mx-2 mt-2 rounded-lg text-slate-400 hover:text-white hover:bg-slate-700 transition flex-shrink-0"
@click="sidebarCollapsed = !sidebarCollapsed"
>
<svg
class="w-5 h-5 transition-transform duration-300"
:class="{ 'rotate-180': sidebarCollapsed }"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 19l-7-7 7-7m8 14l-7-7 7-7" />
</svg>
</button>
<!-- Navigation -->
<nav class="flex-1 px-2 py-4 space-y-2 overflow-y-auto overflow-x-hidden">
<template v-for="group in menuGroups" :key="group.title">
<!-- Group Header (visible when expanded) -->
<div
v-show="sidebarOpen || !sidebarCollapsed"
class="px-3 py-1 text-xs font-semibold uppercase tracking-wider text-slate-500 whitespace-nowrap overflow-hidden"
>
{{ $t(group.title) }}
</div>
<!-- Menu Items -->
<template v-for="item in group.items" :key="item.label">
<!-- Item with submenu -->
<div v-if="item.children">
<button
class="flex items-center w-full gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition"
:class="isGroupActive(item) ? 'bg-indigo-600/20 text-indigo-300' : 'text-slate-300 hover:bg-slate-700 hover:text-white'"
@click="toggleGroup(item)"
>
<span class="flex-shrink-0 w-5 h-5" v-html="item.icon" />
<span
class="flex-1 text-left whitespace-nowrap overflow-hidden transition-all duration-300 ease-in-out"
:class="sidebarOpen || !sidebarCollapsed ? 'opacity-100 max-w-[200px]' : 'opacity-0 max-w-0'"
>{{ $t(item.label) }}</span>
<!-- Chevron -->
<svg
class="w-4 h-4 transition-all duration-300 ease-in-out flex-shrink-0"
:class="{
'rotate-90': expandedGroups.has(item.label),
'opacity-100': sidebarOpen || !sidebarCollapsed,
'opacity-0': sidebarCollapsed && !sidebarOpen
}"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</button>
<!-- Submenu -->
<div
v-show="expandedGroups.has(item.label) && (sidebarOpen || !sidebarCollapsed)"
class="ml-2 mt-1 space-y-1 overflow-hidden transition-all duration-200"
>
<NuxtLink
v-for="child in item.children"
:key="child.path"
:to="child.path"
class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition"
:class="isActive(child.path) ? 'bg-indigo-600 text-white' : 'text-slate-400 hover:bg-slate-700 hover:text-white'"
@click="sidebarOpen = false"
>
<span class="flex-shrink-0 w-4 h-4" v-html="child.icon" />
<span class="whitespace-nowrap overflow-hidden">{{ $t(child.label) }}</span>
</NuxtLink>
</div>
</div>
<!-- Plain link item -->
<NuxtLink
v-else
:to="item.path"
class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition"
:class="isActive(item.path) ? 'bg-indigo-600 text-white' : 'text-slate-300 hover:bg-slate-700 hover:text-white'"
@click="sidebarOpen = false"
>
<span class="flex-shrink-0 w-5 h-5" v-html="item.icon" />
<span
class="whitespace-nowrap overflow-hidden transition-all duration-300 ease-in-out"
:class="sidebarOpen || !sidebarCollapsed ? 'opacity-100 max-w-[200px]' : 'opacity-0 max-w-0'"
>{{ $t(item.label) }}</span>
</NuxtLink>
</template>
</template>
</nav>
<!-- Sidebar Footer -->
<div class="p-4 border-t border-slate-700 overflow-hidden">
<div
class="text-xs text-slate-500 whitespace-nowrap transition-all duration-300 ease-in-out"
:class="sidebarOpen || !sidebarCollapsed ? 'opacity-100 max-w-[200px]' : 'opacity-0 max-w-0'"
>
{{ $t('common.service_finder') }} {{ $t('common.version') }}
</div>
</div>
</aside>
<!-- Main Content Area -->
<div
class="flex-1 flex flex-col min-w-0 transition-all duration-300 ease-in-out"
>
<!-- Topbar -->
<header class="sticky top-0 z-10 bg-slate-800/95 backdrop-blur-sm border-b border-slate-700">
<div class="flex items-center justify-between h-16 px-4 lg:px-6">
<!-- Left: Hamburger + Page Title -->
<div class="flex items-center gap-3">
<button
class="lg:hidden p-2 rounded-lg text-slate-400 hover:text-white hover:bg-slate-700 transition"
@click="sidebarOpen = true"
>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
<h1 class="text-lg font-semibold text-white truncate">{{ $t(pageTitle) }}</h1>
</div>
<!-- Right: Language Switcher + Avatar / Dropdown -->
<div class="flex items-center gap-3">
<!-- Language Switcher Dropdown (scalable for 3+ languages) -->
<LanguageSwitcher />
<!-- Avatar / Dropdown -->
<div class="relative" ref="dropdownRef">
<button
class="flex items-center gap-2 p-1.5 rounded-lg hover:bg-slate-700 transition"
@click="dropdownOpen = !dropdownOpen"
>
<div class="w-8 h-8 rounded-full bg-indigo-600 flex items-center justify-center text-sm font-bold text-white">
{{ userInitials }}
</div>
<span class="hidden sm:block text-sm text-slate-300">{{ userEmail }}</span>
<svg class="w-4 h-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
<!-- Dropdown Menu -->
<div
v-if="dropdownOpen"
class="absolute right-0 mt-2 w-56 bg-slate-800 border border-slate-700 rounded-xl shadow-xl py-1 z-50"
>
<div class="px-4 py-3 border-b border-slate-700">
<p class="text-sm font-medium text-white">{{ userEmail }}</p>
<p class="text-xs text-slate-400 mt-0.5">{{ authStore.userRole === 'SUPERADMIN' ? $t('common.super_administrator') : $t('common.administrator') }}</p>
</div>
<NuxtLink
to="/profile"
class="flex items-center gap-3 px-4 py-2.5 text-sm text-slate-300 hover:bg-slate-700 hover:text-white transition"
@click="dropdownOpen = false"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
{{ $t('common.profile_settings') }}
</NuxtLink>
<button
class="flex items-center gap-3 w-full px-4 py-2.5 text-sm text-red-400 hover:bg-slate-700 hover:text-red-300 transition"
@click="handleLogout"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg>
{{ $t('common.sign_out') }}
</button>
</div>
</div>
</div>
</div>
</header>
<!-- Page Content -->
<main class="flex-1 p-4 lg:p-6 overflow-auto">
<slot />
</main>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useRoute } from 'vue-router'
import { useRegionStore } from '~/stores/region'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const route = useRoute()
const router = useRouter()
const authStore = useAuthStore()
const regionStore = useRegionStore()
// Sidebar state
const sidebarOpen = ref(false)
const sidebarCollapsed = ref(false)
// Dropdown state
const dropdownOpen = ref(false)
const dropdownRef = ref<HTMLElement | null>(null)
// Expanded submenu groups
const expandedGroups = ref<Set<string>>(new Set(['menu.center']))
// User info from auth store
const userEmail = computed(() => authStore.userEmail || t('common.unknown_user'))
const userInitials = computed(() => {
const email = authStore.userEmail
if (!email) return '??'
return email
.split('@')[0]
.split('.')
.map(s => s[0])
.join('')
.toUpperCase()
.slice(0, 2)
})
// --- Menu Group Definitions ---
interface MenuChild {
path: string
label: string
icon: string
}
interface MenuItem {
label: string
path?: string
icon: string
children?: MenuChild[]
}
interface MenuGroup {
title: string
items: MenuItem[]
}
const menuGroups: MenuGroup[] = [
{
title: 'menu.center',
items: [
{
path: '/',
label: 'menu.dashboard',
icon: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" /></svg>',
},
],
},
{
title: 'menu.providers_title',
items: [
{
label: 'menu.providers',
icon: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" /></svg>',
children: [
{
path: '/providers',
label: 'menu.all_providers',
icon: '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" /></svg>',
},
{
path: '/providers/pending',
label: 'menu.pending_providers',
icon: '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>',
},
],
},
],
},
{
title: 'menu.users_and_partners',
items: [
{
label: 'menu.garages',
icon: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" /></svg>',
children: [
{
path: '/garages',
label: 'menu.garage_list',
icon: '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" /></svg>',
},
],
},
{
label: 'menu.users',
icon: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" /></svg>',
children: [
{
path: '/users',
label: 'menu.user_list',
icon: '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" /></svg>',
},
{
path: '/persons',
label: 'menu.persons',
icon: '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" /></svg>',
},
],
},
],
},
{
title: 'menu.finance',
items: [
{
label: 'menu.finance_management',
icon: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.25 7.5l-.625 10.632a2.25 2.25 0 01-2.247 2.118H6.622a2.25 2.25 0 01-2.247-2.118L3.75 7.5m8.25 3v6.75m0 0l-3-3m3 3l3-3M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z" /></svg>',
children: [
{
path: '/packages',
label: 'menu.packages',
icon: '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" /></svg>',
},
{
path: '/finance/commission-rules',
label: 'menu.commission_rules',
icon: '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>',
},
],
},
],
},
{
title: 'menu.gamification',
items: [
{
label: 'menu.game_settings',
icon: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>',
children: [
{
path: '/gamification/point-rules',
label: 'menu.point_rules',
icon: '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>',
},
{
path: '/gamification/levels',
label: 'menu.levels',
icon: '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" /></svg>',
},
{
path: '/gamification/badges',
label: 'menu.badges',
icon: '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z" /></svg>',
},
{
path: '/gamification/validation-rules',
label: 'menu.validation_rules',
icon: '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" /></svg>',
},
],
},
{
label: 'menu.events_and_seasons',
icon: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>',
children: [
{
path: '/gamification/seasons',
label: 'menu.seasons',
icon: '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>',
},
{
path: '/gamification/competitions',
label: 'menu.competitions',
icon: '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 100 4 2 2 0 000-4z" /></svg>',
},
],
},
{
label: 'menu.user_data',
icon: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z" /></svg>',
children: [
{
path: '/gamification/users',
label: 'menu.statistics',
icon: '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" /></svg>',
},
{
path: '/gamification/leaderboard',
label: 'menu.leaderboard',
icon: '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" /></svg>',
},
{
path: '/gamification/ledger',
label: 'menu.ledger',
icon: '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" /></svg>',
},
],
},
{
path: '/gamification',
label: 'menu.gamification_hq',
icon: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" /></svg>',
},
{
path: '/gamification/config',
label: 'menu.system_config',
icon: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>',
},
{
path: '/gamification/parameters',
label: 'menu.system_parameters',
icon: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" /></svg>',
},
],
},
{
title: 'menu.system',
items: [
{
path: '/permissions',
label: 'menu.permissions',
icon: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /></svg>',
},
{
label: 'menu.system_logs',
icon: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>',
children: [
{
path: '/', /* P0 HOTFIX: placeholder until /logs page is built */
label: 'menu.system_logs',
icon: '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>',
},
],
},
],
},
]
// Page title based on route
const pageTitle = computed(() => {
if (route.path === '/') return 'menu.dashboard'
if (route.path.startsWith('/permissions')) return 'permissions.title'
if (route.path.startsWith('/garages')) return 'menu.garages'
if (route.path.startsWith('/persons')) return 'menu.persons'
if (route.path.startsWith('/users')) return 'menu.users'
if (route.path.startsWith('/logs')) return 'menu.system_logs'
if (route.path.startsWith('/packages')) return 'menu.packages'
if (route.path.startsWith('/finance/commission-rules')) return 'commission_rules.title'
if (route.path.startsWith('/providers')) {
if (route.path === '/providers/pending') return 'providers.pending_title'
if (route.path.startsWith('/providers/')) return 'providers.provider_detail'
return 'providers.title'
}
if (route.path.startsWith('/gamification')) {
if (route.path === '/gamification') return 'menu.gamification_hq'
if (route.path === '/gamification/point-rules') return 'menu.point_rules'
if (route.path === '/gamification/levels') return 'menu.levels'
if (route.path === '/gamification/badges') return 'menu.badges'
if (route.path === '/gamification/seasons') return 'menu.seasons'
if (route.path === '/gamification/competitions') return 'menu.competitions'
if (route.path === '/gamification/users') return 'gamification.users.title'
if (route.path.startsWith('/gamification/users/')) return 'gamification.users.user_detail_title'
if (route.path === '/gamification/leaderboard') return 'menu.leaderboard'
if (route.path === '/gamification/config') return 'menu.system_config'
if (route.path === '/gamification/parameters') return 'menu.system_parameters'
if (route.path === '/gamification/ledger') return 'menu.points_ledger'
if (route.path === '/gamification/validation-rules') return 'menu.validation_rules'
return 'menu.gamification'
}
return 'common.admin_panel'
})
function isActive(path: string) {
if (path === '/') return route.path === '/'
return route.path.startsWith(path)
}
function isGroupActive(item: MenuItem): boolean {
if (!item.children) return false
return item.children.some(child => isActive(child.path))
}
function toggleGroup(item: MenuItem) {
if (expandedGroups.value.has(item.label)) {
expandedGroups.value.delete(item.label)
} else {
expandedGroups.value.add(item.label)
}
}
function handleLogout() {
authStore.logout()
dropdownOpen.value = false
router.push('/login')
}
// Close dropdown on outside click
function handleClickOutside(e: MouseEvent) {
if (dropdownRef.value && !dropdownRef.value.contains(e.target as Node)) {
dropdownOpen.value = false
}
}
onMounted(() => {
document.addEventListener('click', handleClickOutside)
// Initialize region store and set active region from user profile
regionStore.fetchRegions().then(() => {
if (authStore.user?.region_code) {
regionStore.setActiveRegion(authStore.user.region_code)
}
})
})
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside)
})
</script>