admin felület fejlesztése garázs, előfizeztési csomagok
This commit is contained in:
@@ -10,24 +10,27 @@
|
||||
<!-- 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',
|
||||
sidebarOpen ? 'w-64 translate-x-0' : '-translate-x-full lg:translate-x-0 lg:w-20',
|
||||
'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 -->
|
||||
<div class="flex items-center h-16 px-4 border-b border-slate-700">
|
||||
<div v-if="sidebarOpen || !sidebarCollapsed" class="flex items-center gap-3">
|
||||
<img src="/sf_logo.png" class="h-8 w-auto" alt="SF Logo" />
|
||||
<span class="text-lg font-bold text-white whitespace-nowrap">Admin Panel</span>
|
||||
<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">Service Finder</div>
|
||||
<div class="text-[10px] font-semibold uppercase tracking-wider text-indigo-400 leading-tight">Admin</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="mx-auto">
|
||||
<img src="/sf_logo.png" class="h-8 w-auto" alt="SF Logo" />
|
||||
</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"
|
||||
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
|
||||
@@ -42,12 +45,12 @@
|
||||
</button>
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="flex-1 px-2 py-4 space-y-2 overflow-y-auto">
|
||||
<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-if="!sidebarCollapsed || sidebarOpen"
|
||||
class="px-3 py-1 text-xs font-semibold uppercase tracking-wider text-slate-500"
|
||||
v-show="sidebarOpen || !sidebarCollapsed"
|
||||
class="px-3 py-1 text-xs font-semibold uppercase tracking-wider text-slate-500 whitespace-nowrap overflow-hidden"
|
||||
>
|
||||
{{ group.title }}
|
||||
</div>
|
||||
@@ -62,12 +65,18 @@
|
||||
@click="toggleGroup(item)"
|
||||
>
|
||||
<span class="flex-shrink-0 w-5 h-5" v-html="item.icon" />
|
||||
<span v-show="!sidebarCollapsed || sidebarOpen" class="flex-1 text-left whitespace-nowrap">{{ item.label }}</span>
|
||||
<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'"
|
||||
>{{ item.label }}</span>
|
||||
<!-- Chevron -->
|
||||
<svg
|
||||
v-show="!sidebarCollapsed || sidebarOpen"
|
||||
class="w-4 h-4 transition-transform duration-200"
|
||||
:class="{ 'rotate-90': expandedGroups.has(item.label) }"
|
||||
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"
|
||||
@@ -77,7 +86,7 @@
|
||||
</button>
|
||||
<!-- Submenu -->
|
||||
<div
|
||||
v-show="expandedGroups.has(item.label) && (!sidebarCollapsed || sidebarOpen)"
|
||||
v-show="expandedGroups.has(item.label) && (sidebarOpen || !sidebarCollapsed)"
|
||||
class="ml-2 mt-1 space-y-1 overflow-hidden transition-all duration-200"
|
||||
>
|
||||
<NuxtLink
|
||||
@@ -89,7 +98,7 @@
|
||||
@click="sidebarOpen = false"
|
||||
>
|
||||
<span class="flex-shrink-0 w-4 h-4" v-html="child.icon" />
|
||||
<span class="whitespace-nowrap">{{ child.label }}</span>
|
||||
<span class="whitespace-nowrap overflow-hidden">{{ child.label }}</span>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
@@ -103,22 +112,30 @@
|
||||
@click="sidebarOpen = false"
|
||||
>
|
||||
<span class="flex-shrink-0 w-5 h-5" v-html="item.icon" />
|
||||
<span v-show="!sidebarCollapsed || sidebarOpen" class="whitespace-nowrap">{{ item.label }}</span>
|
||||
<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'"
|
||||
>{{ item.label }}</span>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
</template>
|
||||
</nav>
|
||||
|
||||
<!-- Sidebar Footer -->
|
||||
<div class="p-4 border-t border-slate-700">
|
||||
<div v-if="!sidebarCollapsed || sidebarOpen" class="text-xs text-slate-500">
|
||||
<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'"
|
||||
>
|
||||
ServiceFinder v2.0
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<div class="flex-1 flex flex-col min-w-0">
|
||||
<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">
|
||||
@@ -135,8 +152,25 @@
|
||||
<h1 class="text-lg font-semibold text-white truncate">{{ pageTitle }}</h1>
|
||||
</div>
|
||||
|
||||
<!-- Right: Avatar / Dropdown -->
|
||||
<div class="relative" ref="dropdownRef">
|
||||
<!-- Right: Language Switcher + Avatar / Dropdown -->
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- Language Switcher (persisted to backend) -->
|
||||
<div class="flex items-center bg-slate-700/50 rounded-lg p-0.5">
|
||||
<button
|
||||
v-for="locale in availableLocales"
|
||||
:key="locale.code"
|
||||
@click="switchLocale(locale.code)"
|
||||
class="px-2.5 py-1 text-xs font-medium rounded-md transition"
|
||||
:class="currentLocale === locale.code
|
||||
? 'bg-indigo-600 text-white shadow-sm'
|
||||
: 'text-slate-400 hover:text-white'"
|
||||
>
|
||||
{{ locale.code === 'hu' ? 'HU' : 'EN' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 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"
|
||||
@@ -157,7 +191,7 @@
|
||||
>
|
||||
<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">Administrator</p>
|
||||
<p class="text-xs text-slate-400 mt-0.5">{{ authStore.userRole === 'SUPERADMIN' ? 'Super Administrator' : authStore.userRole || 'Administrator' }}</p>
|
||||
</div>
|
||||
<NuxtLink
|
||||
to="/profile"
|
||||
@@ -179,6 +213,7 @@
|
||||
Sign Out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -194,11 +229,42 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRegionStore } from '~/stores/region'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const regionStore = useRegionStore()
|
||||
|
||||
// ── i18n / Language Switcher (persisted to backend) ──────────────
|
||||
const { locale, locales } = useI18n()
|
||||
const availableLocales = computed(() => locales.value as { code: string; name: string }[])
|
||||
const currentLocale = computed(() => locale.value)
|
||||
|
||||
function getAuthHeaders() {
|
||||
const tokenCookie = useCookie('access_token')
|
||||
const token = tokenCookie.value
|
||||
return token ? { Authorization: `Bearer ${token}` } : {}
|
||||
}
|
||||
|
||||
async function switchLocale(code: string) {
|
||||
locale.value = code
|
||||
// Persist language preference to backend
|
||||
try {
|
||||
await $fetch('/api/v1/auth/me/language', {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
...getAuthHeaders(),
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: { preferred_language: code },
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('[Layout] Failed to persist language preference:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// Sidebar state
|
||||
const sidebarOpen = ref(false)
|
||||
@@ -212,10 +278,10 @@ const dropdownRef = ref<HTMLElement | null>(null)
|
||||
const expandedGroups = ref<Set<string>>(new Set(['Központ']))
|
||||
|
||||
// User info from auth store
|
||||
const userEmail = computed(() => authStore.userEmail || 'admin@servicefinder.hu')
|
||||
const userEmail = computed(() => authStore.userEmail || 'Ismeretlen Felhasználó')
|
||||
const userInitials = computed(() => {
|
||||
const email = authStore.userEmail
|
||||
if (!email) return 'AD'
|
||||
if (!email) return '??'
|
||||
return email
|
||||
.split('@')[0]
|
||||
.split('.')
|
||||
@@ -282,6 +348,16 @@ const menuGroups: MenuGroup[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Pénzügyek',
|
||||
items: [
|
||||
{
|
||||
path: '/packages',
|
||||
label: 'Csomagok',
|
||||
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>',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Rendszer',
|
||||
items: [
|
||||
@@ -312,6 +388,7 @@ const pageTitle = computed(() => {
|
||||
if (route.path.startsWith('/garages')) return 'Garázsok'
|
||||
if (route.path.startsWith('/users')) return 'Felhasználók'
|
||||
if (route.path.startsWith('/logs')) return 'Rendszernaplók'
|
||||
if (route.path.startsWith('/packages')) return 'Csomagok'
|
||||
return 'Admin Panel'
|
||||
})
|
||||
|
||||
@@ -348,6 +425,13 @@ function handleClickOutside(e: MouseEvent) {
|
||||
|
||||
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(() => {
|
||||
|
||||
Reference in New Issue
Block a user