admin felület fejlesztése garázs, előfizeztési csomagok

This commit is contained in:
Roo
2026-06-25 01:58:04 +00:00
parent 80a5d67f79
commit 52011606ff
334 changed files with 46636 additions and 1606 deletions

View File

@@ -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(() => {