401 lines
12 KiB
Vue
401 lines
12 KiB
Vue
<script setup>
|
|
import { ref, computed, onMounted, onUnmounted, watch } from "vue";
|
|
import { Head, Link, router, usePage } from "@inertiajs/vue3";
|
|
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
|
import { faArrowLeft } from "@fortawesome/free-solid-svg-icons";
|
|
import {
|
|
MenuIcon,
|
|
ChevronDownIcon,
|
|
ShieldCheckIcon,
|
|
UsersIcon,
|
|
KeyRoundIcon,
|
|
Settings2Icon,
|
|
FileTextIcon,
|
|
MailOpenIcon,
|
|
InboxIcon,
|
|
AtSignIcon,
|
|
BookUserIcon,
|
|
MessageSquareIcon,
|
|
ArrowLeftIcon,
|
|
} from "lucide-vue-next";
|
|
import Dropdown from "@/Components/Dropdown.vue";
|
|
import DropdownLink from "@/Components/DropdownLink.vue";
|
|
import GlobalSearch from "@/Layouts/Partials/GlobalSearch.vue";
|
|
import NotificationsBell from "@/Layouts/Partials/NotificationsBell.vue";
|
|
import ApplicationMark from "@/Components/ApplicationMark.vue";
|
|
import Breadcrumbs from "@/Components/Breadcrumbs.vue";
|
|
import ToastContainer from "@/Components/Toast/ToastContainer.vue";
|
|
import { Button } from "@/Components/ui/button";
|
|
|
|
const props = defineProps({ title: { type: String, default: "Administrator" } });
|
|
|
|
// Collapsible sidebar state (persisted when user explicitly toggles)
|
|
const sidebarCollapsed = ref(false);
|
|
const hasSavedSidebarPref = ref(false);
|
|
// Mobile off-canvas state
|
|
const isMobile = ref(false);
|
|
const mobileSidebarOpen = ref(false);
|
|
|
|
function applyAutoCollapse() {
|
|
if (typeof window === "undefined") return;
|
|
isMobile.value = window.innerWidth < 1024; // Tailwind lg breakpoint
|
|
sidebarCollapsed.value = isMobile.value;
|
|
}
|
|
|
|
function handleResize() {
|
|
if (typeof window !== "undefined") {
|
|
isMobile.value = window.innerWidth < 1024;
|
|
if (!isMobile.value) mobileSidebarOpen.value = false; // close drawer when switching to desktop
|
|
}
|
|
if (!hasSavedSidebarPref.value) applyAutoCollapse();
|
|
}
|
|
|
|
onMounted(() => {
|
|
try {
|
|
const saved = localStorage.getItem("sidebarCollapsed");
|
|
if (saved !== null) {
|
|
hasSavedSidebarPref.value = true;
|
|
sidebarCollapsed.value = saved === "1";
|
|
} else {
|
|
applyAutoCollapse();
|
|
}
|
|
} catch {}
|
|
window.addEventListener("resize", handleResize);
|
|
});
|
|
|
|
onUnmounted(() => window.removeEventListener("resize", handleResize));
|
|
|
|
watch(sidebarCollapsed, (v) => {
|
|
if (!hasSavedSidebarPref.value) return; // don't persist auto behavior
|
|
try {
|
|
localStorage.setItem("sidebarCollapsed", v ? "1" : "0");
|
|
} catch {}
|
|
});
|
|
|
|
function toggleSidebar() {
|
|
hasSavedSidebarPref.value = true; // user explicitly chose
|
|
sidebarCollapsed.value = !sidebarCollapsed.value;
|
|
}
|
|
|
|
function toggleMobileSidebar() {
|
|
mobileSidebarOpen.value = !mobileSidebarOpen.value;
|
|
}
|
|
|
|
function handleSidebarToggleClick() {
|
|
if (isMobile.value) toggleMobileSidebar();
|
|
else toggleSidebar();
|
|
}
|
|
|
|
const logout = () => router.post(route("logout"));
|
|
const page = usePage();
|
|
|
|
// Categorized admin navigation groups with distinct icons
|
|
const navGroups = computed(() => [
|
|
{
|
|
key: "core",
|
|
label: "Jedro",
|
|
items: [
|
|
{
|
|
key: "admin.dashboard",
|
|
label: "Pregled",
|
|
route: "admin.index",
|
|
icon: ShieldCheckIcon,
|
|
active: ["admin.index"],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
key: "users",
|
|
label: "Uporabniki & Dovoljenja",
|
|
items: [
|
|
{
|
|
key: "admin.users",
|
|
label: "Uporabniki",
|
|
route: "admin.users.index",
|
|
icon: UsersIcon,
|
|
active: ["admin.users.index"],
|
|
},
|
|
{
|
|
key: "admin.permissions.index",
|
|
label: "Dovoljenja",
|
|
route: "admin.permissions.index",
|
|
icon: KeyRoundIcon,
|
|
active: ["admin.permissions.index", "admin.permissions.create"],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
key: "documents",
|
|
label: "Dokumenti",
|
|
items: [
|
|
{
|
|
key: "admin.document-settings.index",
|
|
label: "Nastavitve dokumentov",
|
|
route: "admin.document-settings.index",
|
|
icon: Settings2Icon,
|
|
active: ["admin.document-settings.index"],
|
|
},
|
|
{
|
|
key: "admin.document-templates.index",
|
|
label: "Predloge dokumentov",
|
|
route: "admin.document-templates.index",
|
|
icon: FileTextIcon,
|
|
active: ["admin.document-templates.index"],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
key: "email",
|
|
label: "Email",
|
|
items: [
|
|
{
|
|
key: "admin.email-templates.index",
|
|
label: "Email predloge",
|
|
route: "admin.email-templates.index",
|
|
icon: MailOpenIcon,
|
|
active: [
|
|
"admin.email-templates.index",
|
|
"admin.email-templates.create",
|
|
"admin.email-templates.edit",
|
|
],
|
|
},
|
|
{
|
|
key: "admin.email-logs.index",
|
|
label: "Email dnevniki",
|
|
route: "admin.email-logs.index",
|
|
icon: InboxIcon,
|
|
active: ["admin.email-logs.index", "admin.email-logs.show"],
|
|
},
|
|
{
|
|
key: "admin.mail-profiles.index",
|
|
label: "Mail profili",
|
|
route: "admin.mail-profiles.index",
|
|
icon: AtSignIcon,
|
|
active: ["admin.mail-profiles.index"],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
key: "sms",
|
|
label: "SMS",
|
|
items: [
|
|
{
|
|
key: "admin.sms-templates.index",
|
|
label: "SMS predloge",
|
|
route: "admin.sms-templates.index",
|
|
icon: FileTextIcon,
|
|
active: [
|
|
"admin.sms-templates.index",
|
|
"admin.sms-templates.create",
|
|
"admin.sms-templates.edit",
|
|
],
|
|
},
|
|
{
|
|
key: "admin.sms-logs.index",
|
|
label: "SMS dnevniki",
|
|
route: "admin.sms-logs.index",
|
|
icon: InboxIcon,
|
|
active: ["admin.sms-logs.index", "admin.sms-logs.show"],
|
|
},
|
|
{
|
|
key: "admin.sms-senders.index",
|
|
label: "SMS pošiljatelji",
|
|
route: "admin.sms-senders.index",
|
|
icon: BookUserIcon,
|
|
active: ["admin.sms-senders.index"],
|
|
},
|
|
{
|
|
key: "admin.sms-profiles.index",
|
|
label: "SMS profili",
|
|
route: "admin.sms-profiles.index",
|
|
icon: Settings2Icon,
|
|
active: ["admin.sms-profiles.index"],
|
|
},
|
|
{
|
|
key: "admin.packages.index",
|
|
label: "SMS paketi",
|
|
route: "admin.packages.index",
|
|
icon: MessageSquareIcon,
|
|
active: ["admin.packages.index", "admin.packages.show"],
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
|
|
function isActive(patterns) {
|
|
try {
|
|
return patterns.some((p) => route().current(p));
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="min-h-screen flex bg-gray-100">
|
|
<Head :title="title" />
|
|
<!-- Backdrop for mobile sidebar -->
|
|
<div
|
|
v-if="isMobile && mobileSidebarOpen"
|
|
class="fixed inset-0 z-40 bg-black/30"
|
|
@click="mobileSidebarOpen = false"
|
|
/>
|
|
|
|
<aside
|
|
:class="[
|
|
sidebarCollapsed ? 'w-16' : 'w-60',
|
|
'bg-white border-r border-gray-200 transition-all duration-300 ease-in-out z-50',
|
|
isMobile
|
|
? 'fixed inset-y-0 left-0 transform shadow-strong ' +
|
|
(mobileSidebarOpen ? 'translate-x-0' : '-translate-x-full')
|
|
: 'sticky top-0 h-screen overflow-y-auto',
|
|
]"
|
|
>
|
|
<div
|
|
class="h-16 px-4 flex items-center justify-between border-b border-gray-200 bg-white"
|
|
>
|
|
<Link
|
|
:href="route('dashboard')"
|
|
class="flex items-center gap-2 hover:opacity-80 transition-opacity"
|
|
>
|
|
<ApplicationMark />
|
|
<span
|
|
v-if="!sidebarCollapsed"
|
|
class="text-sm font-semibold text-gray-900 transition-opacity"
|
|
>
|
|
Admin
|
|
</span>
|
|
</Link>
|
|
</div>
|
|
<nav class="py-4 overflow-y-auto">
|
|
<ul class="space-y-4 px-2">
|
|
<li v-for="group in navGroups" :key="group.label">
|
|
<div
|
|
v-if="!sidebarCollapsed"
|
|
class="px-4 py-1.5 text-[11px] font-semibold uppercase tracking-wider text-sidebar-foreground/60"
|
|
>
|
|
{{ group.label }}
|
|
</div>
|
|
<ul class="space-y-0.5">
|
|
<li v-for="item in group.items" :key="item.key">
|
|
<Link
|
|
:href="route(item.route)"
|
|
:title="item.label"
|
|
:class="[
|
|
'flex items-center gap-3 px-3 py-2.5 text-sm rounded-lg transition-all duration-150',
|
|
isActive(item.active)
|
|
? 'bg-sidebar-primary/15 text-sidebar-primary font-medium shadow-sm'
|
|
: 'text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
|
|
]"
|
|
>
|
|
<component
|
|
v-if="item.icon"
|
|
:is="item.icon"
|
|
class="w-5 h-5 shrink-0 transition-colors"
|
|
/>
|
|
<span
|
|
v-if="!sidebarCollapsed"
|
|
class="truncate transition-opacity"
|
|
:class="{ 'font-medium': isActive(item.active) }"
|
|
>
|
|
{{ item.label }}
|
|
</span>
|
|
</Link>
|
|
</li>
|
|
</ul>
|
|
</li>
|
|
</ul>
|
|
<div class="mt-6 border-t border-gray-200 pt-4 space-y-2 px-4">
|
|
<Link
|
|
:href="route('dashboard')"
|
|
class="text-xs hover:underline flex items-center gap-2 px-3 py-2 rounded-lg text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground transition-all duration-150"
|
|
>
|
|
<ArrowLeftIcon size="18" />
|
|
<span v-if="!sidebarCollapsed">Nazaj na aplikacijo</span>
|
|
</Link>
|
|
</div>
|
|
</nav>
|
|
</aside>
|
|
|
|
<div class="flex-1 flex flex-col min-w-0">
|
|
<div
|
|
class="h-16 bg-white border-b border-gray-200 px-4 flex items-center justify-between sticky top-0 z-30 backdrop-blur-sm bg-white/95 shadow-sm"
|
|
>
|
|
<div class="flex items-center gap-3">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
@click="handleSidebarToggleClick"
|
|
:title="sidebarCollapsed ? 'Razširi meni' : 'Skrči meni'"
|
|
aria-label="Toggle sidebar"
|
|
>
|
|
<MenuIcon />
|
|
</Button>
|
|
<h1 class="text-base font-semibold text-gray-900 hidden sm:block">
|
|
{{ title }}
|
|
</h1>
|
|
</div>
|
|
<div class="flex items-center">
|
|
<NotificationsBell class="mr-2" />
|
|
<!-- User dropdown replicated from AppLayout style -->
|
|
<div class="ms-3 relative">
|
|
<Dropdown align="right" width="48">
|
|
<template #trigger>
|
|
<button
|
|
v-if="$page.props.jetstream?.managesProfilePhotos"
|
|
class="flex text-sm border-2 border-transparent rounded-full focus:outline-none focus:border-primary-500 focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 transition-all hover:ring-2 hover:ring-gray-200"
|
|
>
|
|
<img
|
|
class="h-8 w-8 rounded-full object-cover ring-2 ring-gray-100"
|
|
:src="$page.props.auth.user.profile_photo_url"
|
|
:alt="$page.props.auth.user.name"
|
|
/>
|
|
</button>
|
|
|
|
<span v-else class="inline-flex">
|
|
<Button variant="outline" size="default" type="button" class="gap-2">
|
|
{{ $page.props.auth.user.name }}
|
|
<ChevronDownIcon />
|
|
</Button>
|
|
</span>
|
|
</template>
|
|
|
|
<template #content>
|
|
<div class="block px-4 py-2 text-xs text-gray-400">Nastavitve računa</div>
|
|
<DropdownLink :href="route('profile.show')">Profil</DropdownLink>
|
|
<DropdownLink
|
|
v-if="$page.props.jetstream?.hasApiFeatures"
|
|
:href="route('api-tokens.index')"
|
|
>API Tokens</DropdownLink
|
|
>
|
|
<div class="border-t border-gray-200" />
|
|
<form @submit.prevent="logout">
|
|
<DropdownLink as="button">Izpis</DropdownLink>
|
|
</form>
|
|
</template>
|
|
</Dropdown>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Page Heading -->
|
|
<header v-if="$slots.header" class="bg-white border-b border-gray-200 shadow-sm">
|
|
<div class="max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8 space-y-2">
|
|
<Breadcrumbs
|
|
v-if="$page.props.breadcrumbs && $page.props.breadcrumbs.length"
|
|
:breadcrumbs="$page.props.breadcrumbs"
|
|
/>
|
|
<slot name="header" />
|
|
</div>
|
|
</header>
|
|
|
|
<main class="flex-1 p-4 sm:p-6">
|
|
<slot />
|
|
</main>
|
|
</div>
|
|
|
|
<!-- Toast Notification Container -->
|
|
<ToastContainer />
|
|
</div>
|
|
</template>
|