Added the support for generating docs from template doc
This commit is contained in:
@@ -0,0 +1,271 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from "vue";
|
||||
import { Head, Link, router, usePage } from "@inertiajs/vue3";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import {
|
||||
faUserGroup,
|
||||
faShieldHalved,
|
||||
faArrowLeft,
|
||||
faFileWord,
|
||||
faBars,
|
||||
faGears,
|
||||
faKey,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
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";
|
||||
|
||||
const props = defineProps({ title: { type: String, default: "Administrator" } });
|
||||
|
||||
// Basic state reused (simplified vs AppLayout)
|
||||
const sidebarCollapsed = ref(false);
|
||||
const isMobile = ref(false);
|
||||
const mobileSidebarOpen = ref(false);
|
||||
function handleResize() {
|
||||
if (typeof window === "undefined") return;
|
||||
isMobile.value = window.innerWidth < 1024;
|
||||
if (!isMobile.value) mobileSidebarOpen.value = false;
|
||||
sidebarCollapsed.value = isMobile.value; // auto collapse on small
|
||||
}
|
||||
onMounted(() => {
|
||||
handleResize();
|
||||
window.addEventListener("resize", handleResize);
|
||||
});
|
||||
onUnmounted(() => window.removeEventListener("resize", handleResize));
|
||||
|
||||
function toggleSidebar() {
|
||||
if (isMobile.value) mobileSidebarOpen.value = !mobileSidebarOpen.value;
|
||||
else sidebarCollapsed.value = !sidebarCollapsed.value;
|
||||
}
|
||||
|
||||
const logout = () => router.post(route("logout"));
|
||||
const page = usePage();
|
||||
|
||||
// Categorized admin navigation groups (removed global 'Nastavitve')
|
||||
const navGroups = computed(() => [
|
||||
{
|
||||
key: "core",
|
||||
label: "Jedro",
|
||||
items: [
|
||||
{
|
||||
key: "admin.dashboard",
|
||||
label: "Pregled",
|
||||
route: "admin.index",
|
||||
icon: faShieldHalved,
|
||||
active: ["admin.index"],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "users",
|
||||
label: "Uporabniki & Dovoljenja",
|
||||
items: [
|
||||
{
|
||||
key: "admin.users",
|
||||
label: "Uporabniki",
|
||||
route: "admin.users.index",
|
||||
icon: faUserGroup,
|
||||
active: ["admin.users.index"],
|
||||
},
|
||||
{
|
||||
key: "admin.permissions.index",
|
||||
label: "Dovoljenja",
|
||||
route: "admin.permissions.index",
|
||||
icon: faKey,
|
||||
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: faGears,
|
||||
active: ["admin.document-settings.index"],
|
||||
},
|
||||
{
|
||||
key: "admin.document-templates.index",
|
||||
label: "Predloge dokumentov",
|
||||
route: "admin.document-templates.index",
|
||||
icon: faFileWord,
|
||||
active: ["admin.document-templates.index"],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
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-200 z-50',
|
||||
isMobile
|
||||
? 'fixed inset-y-0 left-0 transform ' +
|
||||
(mobileSidebarOpen ? 'translate-x-0' : '-translate-x-full')
|
||||
: 'sticky top-0 h-screen',
|
||||
]"
|
||||
>
|
||||
<div class="h-16 px-4 flex items-center justify-between border-b">
|
||||
<Link :href="route('dashboard')" class="flex items-center gap-2">
|
||||
<ApplicationMark class="h-8 w-auto" />
|
||||
<span v-if="!sidebarCollapsed" class="text-sm font-semibold">Admin</span>
|
||||
</Link>
|
||||
</div>
|
||||
<nav class="py-4 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-200">
|
||||
<div v-for="group in navGroups" :key="group.key" class="mt-2 first:mt-0">
|
||||
<p
|
||||
v-if="!sidebarCollapsed"
|
||||
class="px-4 mb-1 mt-4 first:mt-0 text-[10px] font-semibold uppercase tracking-wider text-gray-400"
|
||||
>
|
||||
{{ group.label }}
|
||||
</p>
|
||||
<ul class="space-y-1">
|
||||
<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-4 py-2 text-sm hover:bg-gray-100',
|
||||
isActive(item.active) ? 'bg-gray-100 text-gray-900' : 'text-gray-600',
|
||||
]"
|
||||
>
|
||||
<FontAwesomeIcon :icon="item.icon" class="w-5 h-5 text-gray-600" />
|
||||
<span v-if="!sidebarCollapsed">{{ item.label }}</span>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="mt-6 border-t pt-4 space-y-2 px-4">
|
||||
<Link
|
||||
:href="route('dashboard')"
|
||||
class="text-xs text-gray-500 hover:underline flex items-center gap-1"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faArrowLeft" class="w-3.5 h-3.5" />
|
||||
<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-100 px-4 flex items-center justify-between sticky top-0 z-30"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
@click="toggleSidebar"
|
||||
class="inline-flex items-center justify-center w-9 h-9 rounded-md text-gray-500 hover:text-gray-700 hover:bg-gray-100"
|
||||
aria-label="Toggle sidebar"
|
||||
>
|
||||
<!-- Replaced raw SVG with FontAwesome icon -->
|
||||
<FontAwesomeIcon :icon="faBars" class="w-5 h-5" />
|
||||
</button>
|
||||
<h1 class="text-base font-semibold text-gray-800 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-gray-300 transition"
|
||||
>
|
||||
<img
|
||||
class="h-8 w-8 rounded-full object-cover"
|
||||
:src="$page.props.auth.user.profile_photo_url"
|
||||
:alt="$page.props.auth.user.name"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<span v-else class="inline-flex rounded-md">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none focus:bg-gray-50 active:bg-gray-50 transition ease-in-out duration-150"
|
||||
>
|
||||
{{ $page.props.auth.user.name }}
|
||||
<svg
|
||||
class="ms-2 -me-0.5 h-4 w-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M19.5 8.25l-7.5 7.5-7.5-7.5"
|
||||
/>
|
||||
</svg>
|
||||
</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>
|
||||
|
||||
<main class="p-4">
|
||||
<div
|
||||
v-if="$page.props.flash?.success"
|
||||
class="mb-4 rounded bg-emerald-50 border border-emerald-200 text-emerald-700 px-4 py-2 text-sm"
|
||||
>
|
||||
{{ $page.props.flash.success }}
|
||||
</div>
|
||||
<div
|
||||
v-if="$page.props.errors && Object.keys($page.props.errors).length"
|
||||
class="mb-4 rounded bg-rose-50 border border-rose-200 text-rose-700 px-4 py-2 text-sm"
|
||||
>
|
||||
<ul class="list-disc ml-5 space-y-1">
|
||||
<li v-for="(err, key) in $page.props.errors" :key="key">{{ err }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<GlobalSearch :open="false" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -203,17 +203,42 @@ const rawMenuGroups = [
|
||||
routeName: "settings",
|
||||
active: ["settings", "settings.*"],
|
||||
},
|
||||
// Admin panel (roles & permissions management)
|
||||
// Only shown if current user has admin role or manage-settings permission.
|
||||
// We'll filter it out below if not authorized.
|
||||
{
|
||||
key: "admin-panel",
|
||||
title: "Administrator",
|
||||
routeName: "admin.index",
|
||||
active: ["admin.index", "admin.users.index", "admin.permissions.create"],
|
||||
requires: { role: "admin", permission: "manage-settings" },
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const menuGroups = computed(() => {
|
||||
return rawMenuGroups.map((g) => ({
|
||||
label: g.label,
|
||||
items: [...g.items].sort((a, b) =>
|
||||
a.title.localeCompare(b.title, "sl", { sensitivity: "base" })
|
||||
),
|
||||
}));
|
||||
const user = page.props.auth?.user || {};
|
||||
const roles = (user.roles || []).map((r) => r.slug);
|
||||
const permissions = user.permissions || [];
|
||||
|
||||
// Helper to determine inclusion based on optional requires meta
|
||||
function allowed(item) {
|
||||
if (!item.requires) return true;
|
||||
const needRole = item.requires.role;
|
||||
const needPerm = item.requires.permission;
|
||||
return (
|
||||
(needRole && roles.includes(needRole)) ||
|
||||
(needPerm && permissions.includes(needPerm))
|
||||
);
|
||||
}
|
||||
|
||||
return rawMenuGroups.map((g) => {
|
||||
const items = g.items
|
||||
.filter(allowed)
|
||||
.sort((a, b) => a.title.localeCompare(b.title, "sl", { sensitivity: "base" }));
|
||||
return { label: g.label, items };
|
||||
});
|
||||
});
|
||||
|
||||
// Icon map for menu keys -> FontAwesome icon definitions
|
||||
@@ -227,6 +252,7 @@ const menuIconMap = {
|
||||
"import-templates-new": faFileCirclePlus,
|
||||
fieldjobs: faMap,
|
||||
settings: faGear,
|
||||
"admin-panel": faUserGroup,
|
||||
};
|
||||
|
||||
function isActive(patterns) {
|
||||
|
||||
Reference in New Issue
Block a user