Added admin layout

This commit is contained in:
2026-06-21 16:42:37 +02:00
parent a8205a4f96
commit a012c16425
8 changed files with 469 additions and 8 deletions
+8
View File
@@ -20,6 +20,13 @@ class GlobalDataProvider {
public function __invoke() {
$this->user = auth()->user();
$canAccessAdmin = false;
if (null !== $this->user) {
$authCheck = new AuthCheckProvider();
$effectiveRole = $authCheck->getUserRole();
$canAccessAdmin = in_array($effectiveRole, [UserRole::USER_ROLE_ADMIN, UserRole::USER_ROLE_GROUP_LEADER], true);
}
return response()->json([
'user' => null !== $this->user ? new UserResource($this->user)->toArray(request()) : null,
'navbar' => $this->generateNavbar(),
@@ -27,6 +34,7 @@ class GlobalDataProvider {
'activeUsers' => $this->getActiveUsers(),
'version' => config('app.version'),
'currentEvent' => $this->getCurrentEventData(),
'canAccessAdmin' => $canAccessAdmin,
]);
}
+4 -4
View File
@@ -57,8 +57,8 @@ h1, h2, h3, h4, h5, h6 {
.header .user-info {
position: relative;
right: calc(-100% + 190px);
width: 195px;
right: calc(-100% + 250px);
width: 240px;
overflow: hidden;
border-radius: 50px 0 0 50px;
text-align: right;
@@ -108,11 +108,11 @@ h1, h2, h3, h4, h5, h6 {
display: flex;
align-items: center;
justify-content: center;
height: 180px;
}
.logo img {
width: 135px !important;
height: 70px !important;
width: 100% !important;
}
.footer {
Binary file not shown.

After

Width:  |  Height:  |  Size: 321 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 KiB

+444
View File
@@ -0,0 +1,444 @@
<script setup>
import {reactive, onMounted, ref, computed} from 'vue';
import Icon from "../../../app/Views/Components/Icon.vue";
import {toast} from "vue3-toastify";
import {useAjax} from "../components/ajaxHandler.js";
const { request } = useAjax()
const globalProps = reactive({
tenant: '',
user: null,
currentPath: '/',
errors: {},
availableLocalGroups: [],
message: '',
currentEvent: null,
version: '',
});
const sidebarOpen = ref(false);
function toggleSidebar() {
sidebarOpen.value = !sidebarOpen.value;
}
function closeSidebar() {
sidebarOpen.value = false;
}
onMounted(async () => {
const response = await fetch('/api/v1/core/retrieve-global-data');
const data = await response.json();
Object.assign(globalProps, data);
const messageResponse = await request('/api/v1/core/retrieve-messages', {
method: 'GET',
})
if (messageResponse.message !== '') {
if (messageResponse.messageType === 'success') {
toast.success(messageResponse.message)
} else {
toast.error(messageResponse.message)
}
}
});
const currentPath = window.location.pathname;
const props = defineProps({
title: { type: String, default: 'Administration' },
flash: { type: Object, default: () => ({}) }
});
</script>
<template>
<div class="app-layout">
<!-- Mobile Overlay -->
<div class="sidebar-overlay" :class="{ active: sidebarOpen }" @click="closeSidebar"></div>
<div class="main">
<!-- Header -->
<div class="header">
<button class="hamburger-btn" @click="toggleSidebar" aria-label="Menü öffnen">
<span></span>
<span></span>
<span></span>
</button>
<div class="left-side">
<h1>{{ props.title }}</h1>
<label id="show_username" v-if="globalProps.user !== null">Willkommen, {{ globalProps.user.nicename }}</label>
</div>
<div class="header-actions" v-if="globalProps.user !== null">
<div class="user-info">
<a href="/" class="header-link-anonymous" title="Zurück zur Anwendung">
<Icon name="desktop" />
</a>
<a href="/messages" class="header-link-anonymous" title="Meine Nachrichten">
<Icon name="envelope" />
</a>
<a href="/profile" class="header-link-anonymous" title="Mein Profil">
<Icon name="user" />
</a>
<a href="/logout" class="header-link-anonymous-logout" title="Abmelden">
<Icon name="lock" />
</a>
</div>
</div>
</div>
<!-- Flexbox: Sidebar + Content -->
<div class="flexbox">
<div class="sidebar" :class="{ 'sidebar-open': sidebarOpen }">
<div class="logo">
<img src="../../../public/images/mareike.png" alt="Logo" />
</div>
<nav class="nav">
</nav>
</div>
<div class="content-area">
<div class="content">
<slot />
</div>
</div>
</div>
<!-- Footer -->
<footer class="footer">
<div class="footer-inner">
<span>Version {{ globalProps.version }}</span>
<span class="footer-hide-mobile">mareike Modernes Anmeldesystem und richtig einfache Kostenerfassung</span>
<a href="/impress" class="footer-link">Impressum</a>
<a href="/gdpr" class="footer-link">Datenschutzerklärung</a>
<span>&copy; 2022 2026</span>
</div>
</footer>
</div>
<transition name="fade">
<div v-if="flash.message" class="toaster">
{{ flash.message }}
</div>
</transition>
</div>
</template>
<style scoped>
/* ─── Header ─── */
.header {
display: flex;
align-items: center;
height: 80px;
background: #ffffff;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
padding: 0;
position: relative;
z-index: 50;
flex-shrink: 0;
}
.left-side {
flex: 1;
padding: 0 20px;
overflow: hidden;
}
.left-side h1 {
margin: 0;
font-size: 1.4rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#show_username {
display: block;
font-weight: bold;
font-size: 0.85rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.header-actions {
display: flex;
align-items: center;
flex-shrink: 0;
width: 260px;
}
.header-link-anonymous,
.header-link-anonymous-logout {
color: #000000;
font-weight: bold;
text-decoration: none;
background-color: #ffffff;
padding: 10px 20px;
display: inline-block;
}
.header-link-anonymous:hover {
background-color: #1d4899;
color: #ffffff;
}
.header-link-anonymous-logout:hover {
background-color: #ff0000;
color: #ffffff;
}
/* ─── Hamburger ─── */
.hamburger-btn {
display: none;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 5px;
width: 50px;
height: 80px;
border: none;
background: transparent;
cursor: pointer;
flex-shrink: 0;
padding: 0 12px;
}
.hamburger-btn span {
display: block;
width: 24px;
height: 3px;
background-color: #333;
border-radius: 2px;
transition: all 0.2s;
}
/* ─── Layout ─── */
.app-layout {
display: flex;
height: 100vh;
background: #f0f2f5;
font-family: sans-serif;
overflow: hidden;
}
.main {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
margin: 20px;
box-shadow: 20px 20px 15px rgba(0, 0, 0, 0.1);
border-radius: 0 10px 0 0;
}
.flexbox {
display: flex;
flex: 1;
background-color: #FAFAFB;
overflow: hidden;
gap: 1px;
}
/* ─── Sidebar ─── */
.sidebar {
flex-basis: 275px;
flex-shrink: 0;
box-shadow: 2px 0 5px rgba(0,0,0,0.1);
display: flex;
flex-direction: column;
background-color: #ffffff;
overflow-y: auto;
transition: transform 0.3s ease;
}
.sidebar-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.4);
z-index: 99;
}
.logo {
display: flex;
align-items: center;
justify-content: center;
padding: 10px 0;
height: 180px;
width: 100%;
margin-bottom: 30px;
}
.logo img {
object-fit: contain;
}
/* ─── Nav ─── */
.nav {
flex: 1;
}
.nav ul {
list-style: none;
padding: 0;
margin: 0;
border-bottom: 1px solid #ddd;
}
.nav-links li a {
color: #b6b6b6;
background-color: #fff;
padding: 16px 25px;
display: block;
text-decoration: none;
font-weight: bold;
}
.nav a:hover {
background-color: #1d4899;
color: #ffffff;
}
/* ─── Content ─── */
.content-area {
flex: 1;
display: flex;
flex-direction: column;
overflow-y: auto;
overflow-x: hidden;
}
.content {
padding: 30px 20px;
flex: 1;
}
/* ─── Footer ─── */
.footer {
background: #666666;
border-top: 1px solid #ddd;
color: #ffffff;
padding: 10px 15px;
font-size: 11pt;
font-weight: bold;
flex-shrink: 0;
}
.footer-inner {
display: flex;
flex-wrap: wrap;
gap: 10px 20px;
justify-content: space-between;
align-items: center;
}
.footer-link {
color: #ffffff;
text-decoration: none;
}
.footer-link:hover {
text-decoration: underline;
}
/* ═══════════════════════════════════════════
TABLET (640px 1023px)
═══════════════════════════════════════════ */
@media (max-width: 1023px) {
.app-layout {
margin: 0;
height: 100vh;
}
.main {
margin: 0;
border-radius: 0;
box-shadow: none;
}
.hamburger-btn {
display: flex;
}
.sidebar {
position: fixed;
left: 0;
top: 0;
bottom: 0;
z-index: 100;
transform: translateX(-100%);
width: 260px;
flex-basis: 260px;
}
.sidebar.sidebar-open {
transform: translateX(0);
}
.sidebar-overlay.active {
display: block;
}
.header-link-anonymous,
.header-link-anonymous-logout {
padding: 10px 12px;
font-size: 0.9rem;
}
.left-side h1 {
font-size: 1.1rem;
}
}
/* ═══════════════════════════════════════════
SMARTPHONE (< 640px)
═══════════════════════════════════════════ */
@media (max-width: 639px) {
.header {
height: 60px;
}
.hamburger-btn {
height: 60px;
}
.left-side h1 {
font-size: 1rem;
}
#show_username {
display: none;
}
.header-link-anonymous,
.header-link-anonymous-logout {
padding: 6px 8px;
font-size: 0.75rem;
display: inline;
}
.footer-hide-mobile {
display: none;
}
.footer-inner {
justify-content: center;
font-size: 9pt;
gap: 6px 12px;
}
.content {
padding: 15px 10px;
}
.sidebar {
width: 240px;
flex-basis: 240px;
}
}
</style>
+11 -4
View File
@@ -61,6 +61,8 @@ const showCurrentEventLink = computed(() => {
return currentPath !== '/event/details/' + globalProps.currentEvent.identifier;
});
const canAccessAdmin = computed(() => globalProps.canAccessAdmin ?? false);
const props = defineProps({
title: { type: String, default: 'App' },
flash: { type: Object, default: () => ({}) }
@@ -99,6 +101,9 @@ const props = defineProps({
<div class="header-actions" v-if="globalProps.user !== null">
<div class="user-info">
<a v-if="canAccessAdmin" href="/admin" class="header-link-anonymous" title="Administration">
<Icon name="gear" />
</a>
<a href="/messages" class="header-link-anonymous" title="Meine Nachrichten">
<Icon name="envelope" />
</a>
@@ -123,7 +128,7 @@ const props = defineProps({
<div class="flexbox">
<div class="sidebar" :class="{ 'sidebar-open': sidebarOpen }">
<div class="logo">
<img src="../../../public/images/logo.png" alt="Logo" />
<img src="../../../public/images/mareike.png" alt="Logo" />
</div>
<nav class="nav">
<ul class="nav-links" v-if="globalProps.navbar.personal.length > 0">
@@ -235,7 +240,7 @@ const props = defineProps({
display: flex;
align-items: center;
flex-shrink: 0;
width: 200px;
width: 260px;
}
.header-link-anonymous,
@@ -335,11 +340,13 @@ const props = defineProps({
align-items: center;
justify-content: center;
padding: 10px 0;
height: 180px;
width: 100%;
margin-bottom: 30px;
}
.logo img {
width: 135px;
height: 70px;
width: 100%;
object-fit: contain;
}
+1
View File
@@ -7,6 +7,7 @@
<link rel="stylesheet" href="/css/modalBox.css" />
<link rel="stylesheet" href="/css/costunits.css" />
<link rel="stylesheet" href="/css/invoices.css" />
<link rel="icon" type="image/png" href="/favicon.png" />
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="csrf-token" content="{{ csrf_token() }}">
+1
View File
@@ -24,6 +24,7 @@ require_once __DIR__ . '/../app/Domains/Event/Routes/api.php';
require_once __DIR__ . '/../app/Domains/Budget/Routes/web.php';
require_once __DIR__ . '/../app/Domains/Budget/Routes/api.php';
require_once __DIR__ . '/../app/Domains/Legal/Routes/web.php';
require_once __DIR__ . '/../app/Domains/Admin/Routes/web.php';
Route::get('/LKvDUqWl', function () {