add user privileges & add translations

This commit is contained in:
2026-04-20 19:12:27 +03:00
parent f16a830eb2
commit fc96a95335
17 changed files with 1073 additions and 426 deletions

View File

@@ -1,11 +1,14 @@
<template>
<div class="min-h-screen bg-gray-50">
<!-- Sidebar -->
<aside class="fixed inset-y-0 left-0 w-64 bg-white border-r border-gray-200">
<aside
class="fixed inset-y-0 left-0 bg-white border-r border-gray-200 transition-all duration-300 z-20"
:class="sidebarCollapsed ? 'w-16' : 'w-64'"
>
<div class="flex flex-col h-full">
<!-- Logo -->
<div class="flex items-center h-16 px-6 border-b border-gray-200">
<div class="flex items-center space-x-2">
<!-- Logo / Toggle Button -->
<div class="flex items-center h-16 px-4 border-b border-gray-200" :class="sidebarCollapsed ? 'justify-center' : 'justify-between'">
<div v-if="!sidebarCollapsed" class="flex items-center space-x-2">
<div class="w-8 h-8 bg-gradient-to-br from-primary-500 to-primary-700 rounded-lg flex items-center justify-center">
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
@@ -13,82 +16,109 @@
</div>
<span class="text-xl font-bold text-gray-900">{{ settings.siteName }}</span>
</div>
<button
@click="toggleSidebar"
class="p-2 rounded-lg text-gray-500 hover:bg-gray-100 transition-colors"
:class="sidebarCollapsed ? 'mx-auto' : ''"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path v-if="!sidebarCollapsed" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 19l-7-7 7-7m8 14l-7-7 7-7" />
<path v-else stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 5l7 7-7 7M5 5l7 7-7 7" />
</svg>
</button>
</div>
<!-- Navigation -->
<nav class="flex-1 px-4 py-6 space-y-1 overflow-y-auto">
<nav class="flex-1 px-2 py-6 space-y-1 overflow-y-auto">
<router-link
to="/dashboard"
class="flex items-center px-4 py-3 text-gray-700 rounded-lg hover:bg-gray-100 transition-colors group"
:class="{ 'bg-primary-50 text-primary-700': $route.path === '/dashboard' }"
class="flex items-center rounded-lg hover:bg-gray-100 transition-colors group"
:class="[
$route.path === '/dashboard' ? 'bg-primary-50 text-primary-700' : 'text-gray-700',
sidebarCollapsed ? 'justify-center p-2' : 'px-4 py-3 space-x-3'
]"
:title="sidebarCollapsed ? t('app.dashboard') : ''"
>
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-5 h-5 shrink-0" 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>
Dashboard
<span v-if="!sidebarCollapsed" class="truncate">{{ t('app.dashboard') }}</span>
</router-link>
<router-link
v-if="userStore.role === 'admin'"
to="/users"
class="flex items-center px-4 py-3 text-gray-700 rounded-lg hover:bg-gray-100 transition-colors"
:class="{ 'bg-primary-50 text-primary-700': $route.path === '/users' }"
class="flex items-center rounded-lg hover:bg-gray-100 transition-colors group"
:class="[
$route.path === '/users' ? 'bg-primary-50 text-primary-700' : 'text-gray-700',
sidebarCollapsed ? 'justify-center p-2' : 'px-4 py-3 space-x-3'
]"
:title="sidebarCollapsed ? t('app.users') : ''"
>
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-5 h-5 shrink-0" 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 7a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
Users
<span v-if="!sidebarCollapsed" class="truncate">{{ t('app.users') }}</span>
</router-link>
<router-link
to="/restaurants"
class="flex items-center px-4 py-3 text-gray-700 rounded-lg hover:bg-gray-100 transition-colors"
:class="{ 'bg-primary-50 text-primary-700': $route.path === '/restaurants' }"
class="flex items-center rounded-lg hover:bg-gray-100 transition-colors group"
:class="[
$route.path === '/restaurants' ? 'bg-primary-50 text-primary-700' : 'text-gray-700',
sidebarCollapsed ? 'justify-center p-2' : 'px-4 py-3 space-x-3'
]"
:title="sidebarCollapsed ? t('app.restaurants') : ''"
>
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-5 h-5 shrink-0" 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>
Restaurants
<span v-if="!sidebarCollapsed" class="truncate">{{ t('app.restaurants') }}</span>
</router-link>
<router-link
v-if="userStore.role === 'admin'"
to="/settings"
class="flex items-center px-4 py-3 text-gray-700 rounded-lg hover:bg-gray-100 transition-colors"
:class="{ 'bg-primary-50 text-primary-700': $route.path === '/settings' }"
class="flex items-center rounded-lg hover:bg-gray-100 transition-colors group"
:class="[
$route.path === '/settings' ? 'bg-primary-50 text-primary-700' : 'text-gray-700',
sidebarCollapsed ? 'justify-center p-2' : 'px-4 py-3 space-x-3'
]"
:title="sidebarCollapsed ? t('app.settings') : ''"
>
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-5 h-5 shrink-0" 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.065 2.572c1.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.572 1.065c-.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.065-2.572c-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>
Settings
<span v-if="!sidebarCollapsed" class="truncate">{{ t('app.settings') }}</span>
</router-link>
</nav>
<!-- User Profile -->
<div class="p-4 border-t border-gray-200">
<!-- User Info (collapsed aware) -->
<div v-if="!sidebarCollapsed" class="p-4 border-t border-gray-200">
<div class="flex items-center space-x-3">
<div class="w-10 h-10 bg-gradient-to-br from-primary-500 to-primary-700 rounded-full flex items-center justify-center text-white font-semibold">
{{ userInitials }}
</div>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-gray-900 truncate">{{ userName }}</p>
<p class="text-xs text-gray-500 truncate">Administrator</p>
<p class="text-xs text-gray-500 truncate">{{ userStore.role === 'admin' ? 'Administrator' : 'User' }}</p>
</div>
<button @click="logout" class="p-2 text-gray-400 hover:text-gray-600 rounded-lg hover:bg-gray-100 transition-colors">
<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 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>
</button>
</div>
</div>
<div v-else class="p-2 border-t border-gray-200 flex justify-center">
<div class="w-8 h-8 bg-gradient-to-br from-primary-500 to-primary-700 rounded-full flex items-center justify-center text-white font-semibold text-sm">
{{ userInitials }}
</div>
</div>
</div>
</aside>
<!-- Main Content -->
<main class="ml-64">
<!-- Header -->
<header class="bg-white border-b border-gray-200">
<div class="flex items-center justify-between h-16 px-8">
<h1 class="text-2xl font-semibold text-gray-900">{{ pageTitle }}</h1>
<main class="transition-all duration-300" :class="sidebarCollapsed ? 'ml-16' : 'ml-64'">
<!-- Header (без заголовка) -->
<header class="bg-white border-b border-gray-200 sticky top-0 z-10">
<div class="flex items-center justify-end h-16 px-8">
<div class="flex items-center space-x-4">
<!-- Search -->
<div class="relative">
@@ -109,6 +139,25 @@
</svg>
<span class="absolute top-1 right-1 w-2 h-2 bg-red-500 rounded-full"></span>
</button>
<!-- User actions -->
<div class="flex items-center space-x-1 border-l pl-4 ml-2">
<button @click="toggleLanguage" class="p-2 text-gray-400 hover:text-gray-600 rounded-lg hover:bg-gray-100 transition-colors" :title="t('app.language')">
<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 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.129" />
</svg>
</button>
<router-link to="/profile" class="p-2 text-gray-400 hover:text-gray-600 rounded-lg hover:bg-gray-100 transition-colors" :title="t('app.profile')">
<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="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</router-link>
<button @click="logout" class="p-2 text-gray-400 hover:text-gray-600 rounded-lg hover:bg-gray-100 transition-colors" :title="t('app.logout')">
<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 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>
</button>
</div>
</div>
</div>
</header>
@@ -125,33 +174,47 @@
import { computed, ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useSettingsStore } from '../../stores/settings'
import { useUserStore } from '../../stores/user'
import { useI18n } from 'vue-i18n'
const settings = useSettingsStore()
const userStore = useUserStore()
const route = useRoute()
const router = useRouter()
const userName = ref('Loading...')
const userLogin = ref('')
const { t, locale } = useI18n()
onMounted(async () => {
try {
const res = await fetch('/api/admin/me')
if (res.ok) {
const data = await res.json()
userLogin.value = data.login
userName.value = data.login // или можно сделать красивое отображение
}
} catch (e) {
userName.value = 'User'
const userName = computed(() => userStore.login || 'User')
const userInitials = computed(() => (userName.value[0] || 'U').toUpperCase())
// Sidebar collapsed state with localStorage
const SIDEBAR_STORAGE_KEY = 'admin_sidebar_collapsed'
const sidebarCollapsed = ref(false)
onMounted(() => {
const saved = localStorage.getItem(SIDEBAR_STORAGE_KEY)
if (saved !== null) {
sidebarCollapsed.value = saved === 'true'
}
})
const userInitials = computed(() => {
return (userName.value[0] || 'U').toUpperCase()
})
function toggleSidebar() {
sidebarCollapsed.value = !sidebarCollapsed.value
localStorage.setItem(SIDEBAR_STORAGE_KEY, String(sidebarCollapsed.value))
}
async function logout() {
await fetch('/api/logout', { method: 'POST' })
userStore.clear()
router.push('/login')
}
async function toggleLanguage() {
const newLang = locale.value === 'en' ? 'ru' : 'en'
const ok = await userStore.updateProfile({ language: newLang })
if (ok) {
locale.value = newLang
} else {
locale.value = newLang
}
}
</script>