1761 lines
72 KiB
HTML
Executable File
1761 lines
72 KiB
HTML
Executable File
<!--
|
||
Copyright 2025-2026 AIIrondev
|
||
|
||
Licensed under the Inventarsystem EULA (Endbenutzer-Lizenzvertrag).
|
||
See Legal/LICENSE for the full license text.
|
||
Unauthorized commercial use, SaaS hosting, or removal of branding is prohibited.
|
||
For commercial licensing inquiries: https://github.com/AIIrondev
|
||
-->
|
||
<!DOCTYPE html>
|
||
<html lang="de" data-module="{{ CURRENT_MODULE }}">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes">
|
||
<meta name="mobile-web-app-capable" content="yes">
|
||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||
<meta name="csrf-token" content="{{ csrf_token }}">
|
||
<title>{% block title %}Inventarsystem{% endblock %}</title>
|
||
{% block head %}
|
||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css', v=ASSET_VERSION) }}">
|
||
<link rel="stylesheet" href="{{ url_for('static', filename='css/planned_appointments.css', v=ASSET_VERSION) }}">
|
||
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||
<link rel="manifest" href="{{ url_for('static', filename='manifest.json') }}">
|
||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
|
||
<script>
|
||
(function () {
|
||
const csrfMeta = document.querySelector('meta[name="csrf-token"]');
|
||
const csrfToken = csrfMeta ? csrfMeta.content : '';
|
||
if (!csrfToken) {
|
||
return;
|
||
}
|
||
|
||
const safeMethods = new Set(['GET', 'HEAD', 'OPTIONS', 'TRACE']);
|
||
|
||
function sameOrigin(url) {
|
||
try {
|
||
return new URL(url, window.location.href).origin === window.location.origin;
|
||
} catch (error) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function ensureFormToken(form) {
|
||
const method = (form.getAttribute('method') || 'GET').toUpperCase();
|
||
if (safeMethods.has(method)) {
|
||
return;
|
||
}
|
||
|
||
const action = form.getAttribute('action') || window.location.href;
|
||
if (!sameOrigin(action)) {
|
||
return;
|
||
}
|
||
|
||
let tokenInput = form.querySelector('input[name="csrf_token"]');
|
||
if (!tokenInput) {
|
||
tokenInput = document.createElement('input');
|
||
tokenInput.type = 'hidden';
|
||
tokenInput.name = 'csrf_token';
|
||
form.appendChild(tokenInput);
|
||
}
|
||
tokenInput.value = csrfToken;
|
||
}
|
||
|
||
document.addEventListener('submit', function (event) {
|
||
const form = event.target;
|
||
if (form && form.tagName === 'FORM') {
|
||
ensureFormToken(form);
|
||
}
|
||
}, true);
|
||
|
||
const originalFetch = window.fetch.bind(window);
|
||
window.fetch = function (resource, init) {
|
||
const options = init ? { ...init } : {};
|
||
const method = (options.method || 'GET').toUpperCase();
|
||
const targetUrl = resource instanceof Request ? resource.url : String(resource);
|
||
if (!safeMethods.has(method) && sameOrigin(targetUrl)) {
|
||
const headers = new Headers(resource instanceof Request ? resource.headers : undefined);
|
||
if (options.headers) {
|
||
new Headers(options.headers).forEach((value, key) => headers.set(key, value));
|
||
}
|
||
headers.set('X-CSRFToken', csrfToken);
|
||
headers.set('X-Requested-With', 'fetch');
|
||
options.headers = headers;
|
||
}
|
||
return originalFetch(resource, options);
|
||
};
|
||
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
document.querySelectorAll('form[method="post"], form[method="POST"]').forEach(ensureFormToken);
|
||
});
|
||
})();
|
||
</script>
|
||
<style>
|
||
/* ===== MODULE DETECTION & SETUP ===== */
|
||
:root {
|
||
--module-primary-color: #2c3e50;
|
||
--module-primary-light: #34495e;
|
||
--module-accent-color: #3498db;
|
||
--module-accent-light: rgba(52, 152, 219, 0.1);
|
||
--module-brand-icon: "📦";
|
||
}
|
||
|
||
html[data-module="library"] {
|
||
--module-primary-color: #6c4e71;
|
||
--module-primary-light: #7d5a8a;
|
||
--module-accent-color: #9b59b6;
|
||
--module-accent-light: rgba(155, 89, 182, 0.1);
|
||
--module-brand-icon: "📚";
|
||
}
|
||
|
||
/* Global styles */
|
||
body {
|
||
font-family: "Manrope", "Segoe UI", "Noto Sans", sans-serif;
|
||
background:
|
||
radial-gradient(1200px 500px at 10% -10%, rgba(86, 145, 200, 0.08), transparent 55%),
|
||
radial-gradient(900px 420px at 95% 0%, rgba(52, 98, 150, 0.09), transparent 52%),
|
||
linear-gradient(180deg, #f2f5f8, #e7edf4);
|
||
margin: 0;
|
||
padding: 0;
|
||
padding-bottom: 30px;
|
||
color: #1f2d3d;
|
||
}
|
||
|
||
.container {
|
||
padding-top: 20px;
|
||
}
|
||
|
||
/* Navigation styles */
|
||
.navbar {
|
||
box-shadow: 0 4px 14px rgba(2, 6, 23, 0.24);
|
||
background-color: var(--module-primary-color) !important;
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 1900;
|
||
}
|
||
|
||
.navbar .container-fluid {
|
||
gap: 10px;
|
||
}
|
||
|
||
.navbar-brand {
|
||
font-weight: bold;
|
||
font-size: 1.4rem;
|
||
}
|
||
|
||
.navbar-brand::before {
|
||
content: attr(data-icon);
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.navbar-nav .nav-link {
|
||
font-size: 1rem;
|
||
font-weight: 600;
|
||
padding: 0.6rem 0.85rem;
|
||
border-radius: 8px;
|
||
transition: padding .18s ease, font-size .18s ease;
|
||
}
|
||
|
||
.navbar.nav-compact .navbar-brand {
|
||
font-size: 1.18rem;
|
||
}
|
||
|
||
.navbar.nav-compact .navbar-nav .nav-link {
|
||
font-size: 0.93rem;
|
||
padding: 0.48rem 0.62rem;
|
||
}
|
||
|
||
.navbar.nav-compact .function-search-wrap {
|
||
width: min(320px, 34vw);
|
||
}
|
||
|
||
.navbar.nav-compact .function-search-input,
|
||
.navbar.nav-compact .function-search-btn {
|
||
min-height: 34px;
|
||
padding-top: 6px;
|
||
padding-bottom: 6px;
|
||
}
|
||
|
||
.navbar.nav-ultra-compact .navbar-brand {
|
||
font-size: 1.04rem;
|
||
}
|
||
|
||
.navbar.nav-ultra-compact .navbar-nav .nav-link {
|
||
font-size: 0.86rem;
|
||
padding: 0.4rem 0.52rem;
|
||
}
|
||
|
||
.navbar.nav-ultra-compact .function-search-wrap {
|
||
width: min(270px, 30vw);
|
||
}
|
||
|
||
.navbar.nav-ultra-compact .function-search-btn {
|
||
padding-left: 9px;
|
||
padding-right: 9px;
|
||
font-size: 0.82rem;
|
||
}
|
||
|
||
.navbar.nav-ultra-compact .navbar-text {
|
||
margin-right: 0.45rem !important;
|
||
font-size: 0.86rem;
|
||
}
|
||
|
||
.navbar-nav .nav-link.nav-active {
|
||
background-color: rgba(255, 255, 255, 0.2);
|
||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.35);
|
||
}
|
||
|
||
.function-search-wrap {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-right: 10px;
|
||
width: min(420px, 42vw);
|
||
}
|
||
|
||
.function-search-form {
|
||
width: 100%;
|
||
display: flex;
|
||
gap: 6px;
|
||
align-items: center;
|
||
}
|
||
|
||
.function-search-input {
|
||
width: 100%;
|
||
min-height: 38px;
|
||
border-radius: 10px;
|
||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||
background: rgba(255, 255, 255, 0.12);
|
||
color: #ffffff;
|
||
padding: 8px 12px;
|
||
outline: none;
|
||
}
|
||
|
||
.function-search-input::placeholder {
|
||
color: rgba(255, 255, 255, 0.78);
|
||
}
|
||
|
||
.function-search-input:focus {
|
||
border-color: #93c5fd;
|
||
box-shadow: 0 0 0 2px rgba(147, 197, 253, 0.35);
|
||
background: rgba(255, 255, 255, 0.18);
|
||
}
|
||
|
||
.function-search-btn {
|
||
min-height: 38px;
|
||
border-radius: 10px;
|
||
border: 1px solid rgba(255, 255, 255, 0.45);
|
||
background: rgba(255, 255, 255, 0.15);
|
||
color: #ffffff;
|
||
padding: 7px 12px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.function-search-btn:hover {
|
||
background: rgba(255, 255, 255, 0.26);
|
||
}
|
||
|
||
.quick-link-pill {
|
||
border: 1px solid rgba(255, 255, 255, 0.35);
|
||
background: rgba(255, 255, 255, 0.08);
|
||
}
|
||
|
||
.navbar-nav .nav-link:hover,
|
||
.navbar-nav .nav-link:focus {
|
||
background-color: rgba(255, 255, 255, 0.12);
|
||
}
|
||
|
||
.nav-priority-link {
|
||
background: var(--module-accent-light);
|
||
border: 1px solid var(--module-accent-color);
|
||
}
|
||
|
||
.admin-nav-section {
|
||
margin-left: auto;
|
||
display: flex;
|
||
gap: 15px;
|
||
}
|
||
|
||
.admin-nav-section .nav-item {
|
||
background-color: var(--module-accent-color);
|
||
color: white;
|
||
padding: 5px 10px;
|
||
border-radius: 4px;
|
||
text-decoration: none;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.admin-nav-section .nav-item:hover {
|
||
opacity: 0.8;
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
/* Admin dropdown styling */
|
||
.nav-item.dropdown .nav-link.dropdown-toggle {
|
||
color: #ffffff;
|
||
font-weight: 500;
|
||
position: relative;
|
||
}
|
||
|
||
.nav-item.dropdown .nav-link.dropdown-toggle:hover {
|
||
color: #ffffff;
|
||
}
|
||
|
||
/* Consistent spacing for admin nav items */
|
||
.navbar-nav .nav-item {
|
||
margin-right: 5px;
|
||
}
|
||
|
||
.navbar-nav {
|
||
align-items: center;
|
||
}
|
||
|
||
.navbar-nav .nav-item:last-child {
|
||
margin-right: 0;
|
||
}
|
||
|
||
/* Flash messages */
|
||
.flashes {
|
||
margin-top: 20px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.flash {
|
||
padding: 12px 20px;
|
||
margin-bottom: 15px;
|
||
border-radius: 5px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.flash.success {
|
||
background-color: #d4edda;
|
||
color: #155724;
|
||
border-left: 5px solid #28a745;
|
||
}
|
||
|
||
.flash.error {
|
||
background-color: #f8d7da;
|
||
color: #721c24;
|
||
border-left: 5px solid #dc3545;
|
||
}
|
||
|
||
.flash.info {
|
||
background-color: #e2e3e5;
|
||
color: #383d41;
|
||
border-left: 5px solid #6c757d;
|
||
}
|
||
|
||
/* Dropdown menus - consistent with system design */
|
||
.dropdown-menu {
|
||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||
border: 1px solid #dee2e6;
|
||
border-radius: 8px;
|
||
padding: 8px 0;
|
||
min-width: 200px;
|
||
margin-top: 5px;
|
||
}
|
||
|
||
.dropdown-item {
|
||
padding: 10px 16px;
|
||
font-size: 0.9rem;
|
||
color: #495057;
|
||
transition: all 0.2s ease;
|
||
border: none;
|
||
background: none;
|
||
}
|
||
|
||
.dropdown-item:hover,
|
||
.dropdown-item:focus {
|
||
background-color: #f8f9fa;
|
||
color: #007bff;
|
||
transform: translateX(2px);
|
||
}
|
||
|
||
/* Navigation dropdown specific styles */
|
||
.nav-item.dropdown .dropdown-toggle {
|
||
position: relative;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.nav-item.dropdown .dropdown-toggle:hover {
|
||
background-color: rgba(255,255,255,0.1);
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.nav-item.dropdown .dropdown-toggle::after {
|
||
transition: transform 0.2s ease;
|
||
}
|
||
|
||
.nav-item.dropdown.show .dropdown-toggle::after {
|
||
transform: rotate(180deg);
|
||
}
|
||
|
||
/* User account dropdown specific styles */
|
||
.dropdown-menu-end {
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||
}
|
||
|
||
.dropdown-divider {
|
||
margin: 6px 0;
|
||
border-color: #dee2e6;
|
||
}
|
||
|
||
/* Version display */
|
||
.text-info {
|
||
opacity: 0.8;
|
||
}
|
||
|
||
/* Mobile responsive navigation styles */
|
||
@media screen and (max-width: 768px) {
|
||
.navbar-nav .dropdown-menu {
|
||
border: none;
|
||
box-shadow: none;
|
||
background-color: rgba(52, 58, 64, 0.95);
|
||
margin-left: 15px;
|
||
margin-top: 5px;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.navbar-nav .dropdown-item {
|
||
color: #ffffff;
|
||
padding: 12px 20px;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.navbar-nav .dropdown-item:hover,
|
||
.navbar-nav .dropdown-item:focus {
|
||
background-color: rgba(255,255,255,0.1);
|
||
color: #ffffff;
|
||
transform: none;
|
||
}
|
||
|
||
.nav-item.dropdown .dropdown-toggle {
|
||
padding: 8px 12px;
|
||
}
|
||
|
||
.navbar-toggler {
|
||
padding: 0.5rem 0.65rem;
|
||
border-width: 2px;
|
||
}
|
||
|
||
.function-search-wrap {
|
||
width: 100%;
|
||
margin: 8px 0 10px;
|
||
}
|
||
|
||
.navbar-nav .nav-item {
|
||
margin-right: 0;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.navbar-nav .nav-link {
|
||
min-height: 44px;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
/* Better touch targets for mobile */
|
||
.dropdown-toggle::after {
|
||
margin-left: 8px;
|
||
}
|
||
|
||
/* Account dropdown stays light theme on mobile */
|
||
.dropdown-menu-end {
|
||
background-color: #ffffff;
|
||
border: 1px solid #dee2e6;
|
||
}
|
||
|
||
.dropdown-menu-end .dropdown-item {
|
||
color: #495057;
|
||
}
|
||
|
||
.dropdown-menu-end .dropdown-item:hover {
|
||
background-color: #f8f9fa;
|
||
color: #007bff;
|
||
}
|
||
}
|
||
|
||
/* Module Selector Bar Styles */
|
||
.module-selector-bar {
|
||
background: linear-gradient(90deg, var(--module-primary-color) 0%, var(--module-primary-light) 100%);
|
||
border-bottom: 3px solid var(--module-accent-color);
|
||
padding: 10px 16px;
|
||
display: flex;
|
||
justify-content: flex-start;
|
||
gap: 12px;
|
||
align-items: center;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||
}
|
||
|
||
.module-selector-bar .module-label {
|
||
font-size: 0.7rem;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.12em;
|
||
color: rgba(255, 255, 255, 0.7);
|
||
font-weight: 800;
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.module-selector-bar .module-tabs {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
flex: 1;
|
||
}
|
||
|
||
.module-selector-bar .module-tab {
|
||
padding: 7px 16px;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border: 1.5px solid rgba(255, 255, 255, 0.2);
|
||
color: rgba(255, 255, 255, 0.8);
|
||
text-decoration: none;
|
||
border-radius: 6px;
|
||
font-size: 0.95rem;
|
||
font-weight: 700;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
display: inline-block;
|
||
}
|
||
|
||
.module-selector-bar .module-tab:hover {
|
||
background: rgba(255, 255, 255, 0.2);
|
||
border-color: rgba(255, 255, 255, 0.4);
|
||
color: #fff;
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.module-selector-bar .module-tab.active {
|
||
background: var(--module-accent-color);
|
||
border-color: rgba(255, 255, 255, 0.8);
|
||
color: #fff;
|
||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.25);
|
||
}
|
||
|
||
.module-separator {
|
||
width: 1px;
|
||
height: 24px;
|
||
background: rgba(255, 255, 255, 0.25);
|
||
margin: 0 4px;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.navbar {
|
||
border-bottom-left-radius: 14px;
|
||
border-bottom-right-radius: 14px;
|
||
}
|
||
|
||
.navbar .container-fluid {
|
||
align-items: flex-start;
|
||
flex-wrap: wrap;
|
||
padding-top: 0.2rem;
|
||
padding-bottom: 0.35rem;
|
||
}
|
||
|
||
.navbar-brand {
|
||
order: 1;
|
||
flex: 1 1 auto;
|
||
min-width: 0;
|
||
font-size: 1.08rem;
|
||
line-height: 1.1;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding-top: 0.2rem;
|
||
padding-bottom: 0.2rem;
|
||
}
|
||
|
||
.navbar-toggler {
|
||
order: 2;
|
||
margin-left: auto;
|
||
flex: 0 0 auto;
|
||
align-self: flex-start;
|
||
}
|
||
|
||
.navbar-collapse {
|
||
order: 4;
|
||
width: 100%;
|
||
margin-top: 8px;
|
||
padding: 10px 10px 6px;
|
||
border-radius: 14px;
|
||
background: rgba(15, 23, 42, 0.18);
|
||
backdrop-filter: blur(10px);
|
||
-webkit-backdrop-filter: blur(10px);
|
||
}
|
||
|
||
.navbar-collapse.show,
|
||
.navbar-collapse.collapsing {
|
||
display: block;
|
||
}
|
||
|
||
.navbar-nav {
|
||
width: 100%;
|
||
align-items: stretch;
|
||
gap: 8px;
|
||
padding-top: 2px;
|
||
}
|
||
|
||
.navbar-nav .nav-item {
|
||
width: 100%;
|
||
}
|
||
|
||
.navbar-nav .nav-link {
|
||
width: 100%;
|
||
justify-content: flex-start;
|
||
min-height: 46px;
|
||
padding: 0.8rem 0.95rem;
|
||
border-radius: 12px;
|
||
background: rgba(255, 255, 255, 0.08);
|
||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
|
||
}
|
||
|
||
.navbar-nav .nav-link.nav-active,
|
||
.navbar-nav .nav-link.quick-link-pill,
|
||
.navbar-nav .nav-link.nav-priority-link {
|
||
background: rgba(255, 255, 255, 0.16);
|
||
}
|
||
|
||
.navbar-nav .nav-item.dropdown {
|
||
width: 100%;
|
||
}
|
||
|
||
.navbar-nav .nav-item.dropdown .nav-link.dropdown-toggle {
|
||
width: 100%;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.navbar-nav .dropdown-menu {
|
||
width: 100%;
|
||
margin-left: 0;
|
||
margin-top: 6px;
|
||
border-radius: 12px;
|
||
}
|
||
|
||
.module-selector-bar {
|
||
padding: 8px 10px;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.module-selector-bar .module-label {
|
||
display: none;
|
||
}
|
||
|
||
.module-selector-bar .module-tabs {
|
||
width: 100%;
|
||
gap: 8px;
|
||
}
|
||
|
||
.module-selector-bar .module-tab {
|
||
width: 100%;
|
||
text-align: center;
|
||
padding: 8px 12px;
|
||
font-size: 0.92rem;
|
||
}
|
||
|
||
.module-separator {
|
||
display: none;
|
||
}
|
||
|
||
.function-search-wrap {
|
||
order: 3;
|
||
width: 100%;
|
||
margin: 0;
|
||
}
|
||
|
||
.function-search-form {
|
||
width: 100%;
|
||
flex-wrap: nowrap;
|
||
gap: 8px;
|
||
}
|
||
|
||
.function-search-input {
|
||
flex: 1 1 auto;
|
||
min-width: 0;
|
||
min-height: 44px;
|
||
}
|
||
|
||
.function-search-btn {
|
||
flex: 0 0 auto;
|
||
min-height: 44px;
|
||
min-width: 72px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.navbar-text {
|
||
order: 5;
|
||
width: 100%;
|
||
margin: 0 !important;
|
||
padding: 8px 10px;
|
||
border-radius: 12px;
|
||
background: rgba(255, 255, 255, 0.08);
|
||
text-align: center;
|
||
}
|
||
|
||
.user-menu-wrap {
|
||
order: 6;
|
||
width: 100%;
|
||
margin-right: 0 !important;
|
||
}
|
||
|
||
.user-menu-btn {
|
||
width: 100%;
|
||
min-height: 46px;
|
||
border-radius: 12px;
|
||
justify-content: center;
|
||
}
|
||
|
||
.dropdown-menu-end {
|
||
width: 100%;
|
||
}
|
||
|
||
@media (max-width: 520px) {
|
||
.function-search-form {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.function-search-input,
|
||
.function-search-btn,
|
||
.user-menu-btn,
|
||
.module-selector-bar .module-tab {
|
||
width: 100%;
|
||
}
|
||
|
||
.module-selector-bar .module-tabs {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.navbar-text {
|
||
font-size: 0.88rem;
|
||
}
|
||
}
|
||
}
|
||
</style>
|
||
{% endblock %}
|
||
</head>
|
||
<body>
|
||
{% set current_path = request.path %}
|
||
<!-- Module Selector Bar -->
|
||
{% if 'username' in session %}
|
||
<div class="module-selector-bar" id="moduleBar">
|
||
<span class="module-label">Module:</span>
|
||
<div class="module-tabs">
|
||
<a href="{{ url_for('home') }}"
|
||
class="module-tab {% if CURRENT_MODULE == 'inventory' %}active{% endif %}"
|
||
id="inventoryModule"
|
||
data-module="inventory">
|
||
📦 Inventarsystem
|
||
</a>
|
||
{% if library_module_enabled %}
|
||
<div class="module-separator"></div>
|
||
<a href="{{ url_for('library_view') }}"
|
||
class="module-tab {% if CURRENT_MODULE == 'library' %}active{% endif %}"
|
||
id="libraryModule"
|
||
data-module="library">
|
||
📚 Bibliothek
|
||
</a>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
{% if CURRENT_MODULE != 'library' %}
|
||
<nav class="navbar navbar-expand-lg navbar-dark" id="inventoryNavbar">
|
||
<div class="container-fluid">
|
||
<a class="navbar-brand" href="{{ url_for('home') }}" data-icon="📦">Inventarsystem</a>
|
||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#inventoryNavContent">
|
||
<span class="navbar-toggler-icon"></span>
|
||
</button>
|
||
<div class="collapse navbar-collapse" id="inventoryNavContent">
|
||
<ul class="navbar-nav me-auto mb-2 mb-lg-0" id="inventoryNavList">
|
||
{% if current_permissions.pages.get('home', True) %}
|
||
<li class="nav-item" data-nav-fixed="true">
|
||
<a class="nav-link {% if current_path == url_for('home') %}nav-active{% endif %}" href="{{ url_for('home') }}">Artikel</a>
|
||
</li>
|
||
{% endif %}
|
||
{% if 'username' in session %}
|
||
{% if current_permissions.pages.get('my_borrowed_items', True) %}
|
||
<li class="nav-item" data-nav-fixed="true">
|
||
<a class="nav-link quick-link-pill {% if current_path == url_for('my_borrowed_items') %}nav-active{% endif %}" href="{{ url_for('my_borrowed_items') }}">Meine Ausleihen</a>
|
||
</li>
|
||
{% endif %}
|
||
{% if current_permissions.pages.get('tutorial_page', True) %}
|
||
<li class="nav-item" data-nav-fixed="true">
|
||
<a class="nav-link quick-link-pill {% if current_path == url_for('tutorial_page') %}nav-active{% endif %}" href="{{ url_for('tutorial_page') }}">Tutorial</a>
|
||
</li>
|
||
{% endif %}
|
||
{% endif %}
|
||
{% if 'username' in session and current_permissions.pages.get('upload_admin', True) and current_permissions.actions.get('can_insert', True) %}
|
||
<li class="nav-item">
|
||
<a class="nav-link nav-priority-link {% if current_path == url_for('upload_admin') %}nav-active{% endif %}" href="{{ url_for('upload_admin') }}">➕ Hochladen</a>
|
||
</li>
|
||
{% endif %}
|
||
<li class="nav-item dropdown ms-lg-auto">
|
||
<a class="nav-link dropdown-toggle" href="#" id="invMoreDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false" title="Weitere Optionen">
|
||
Mehr Optionen
|
||
</a>
|
||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="invMoreDropdown">
|
||
{% if 'username' in session %}
|
||
{% if current_permissions.pages.get('my_borrowed_items', True) %}
|
||
<li><a class="dropdown-item" href="{{ url_for('my_borrowed_items') }}">Meine Ausleihen</a></li>
|
||
{% endif %}
|
||
{% if current_permissions.pages.get('tutorial_page', True) %}
|
||
<li><a class="dropdown-item" href="{{ url_for('tutorial_page') }}">Tutorial</a></li>
|
||
{% endif %}
|
||
<li><hr class="dropdown-divider"></li>
|
||
{% endif %}
|
||
{% if 'username' in session and (session.get('admin', False) or is_admin) and current_permissions.actions.get('can_manage_settings', True) %}
|
||
<li><h6 class="dropdown-header">Verwaltung</h6></li>
|
||
{% if current_permissions.pages.get('manage_filters', True) %}
|
||
<li><a class="dropdown-item" href="{{ url_for('manage_filters') }}">Filter verwalten</a></li>
|
||
{% endif %}
|
||
{% if current_permissions.pages.get('manage_locations', True) %}
|
||
<li><a class="dropdown-item" href="{{ url_for('manage_locations') }}">Orte verwalten</a></li>
|
||
{% endif %}
|
||
{% if current_permissions.pages.get('admin_borrowings', True) %}
|
||
<li><a class="dropdown-item" href="{{ url_for('admin_borrowings') }}">Ausleihen</a></li>
|
||
{% endif %}
|
||
{% if current_permissions.pages.get('admin_damaged_items', True) %}
|
||
<li><a class="dropdown-item" href="{{ url_for('admin_damaged_items') }}">Defekte Items</a></li>
|
||
{% endif %}
|
||
{% if current_permissions.actions.get('can_view_logs', True) and current_permissions.pages.get('admin_audit_dashboard', True) %}
|
||
<li><a class="dropdown-item" href="{{ url_for('admin_audit_dashboard') }}">Audit Dashboard</a></li>
|
||
{% endif %}
|
||
{% if current_permissions.actions.get('can_view_logs', True) and current_permissions.pages.get('logs', True) %}
|
||
<li><a class="dropdown-item" href="{{ url_for('logs') }}">Logs</a></li>
|
||
{% endif %}
|
||
<li><hr class="dropdown-divider"></li>
|
||
{% if current_permissions.actions.get('can_manage_users', True) %}
|
||
<li><h6 class="dropdown-header">System</h6></li>
|
||
{% if current_permissions.pages.get('user_del', True) %}
|
||
<li><a class="dropdown-item" href="{{ url_for('user_del') }}">Benutzer verwalten</a></li>
|
||
{% endif %}
|
||
{% if current_permissions.pages.get('register', True) %}
|
||
<li><a class="dropdown-item" href="{{ url_for('register') }}">Neuer Benutzer</a></li>
|
||
{% endif %}
|
||
<li><hr class="dropdown-divider"></li>
|
||
{% endif %}
|
||
{% endif %}
|
||
<li><a class="dropdown-item" href="{{ url_for('impressum') }}">Impressum</a></li>
|
||
<li><a class="dropdown-item" href="{{ url_for('license') }}">Lizenz</a></li>
|
||
</ul>
|
||
</li>
|
||
<li class="nav-item d-none" id="inv-nav-overflow-anchor" aria-hidden="true"></li>
|
||
</ul>
|
||
<div class="d-flex">
|
||
{% if 'username' in session %}
|
||
<div class="function-search-wrap">
|
||
<form class="function-search-form" data-function-search="true">
|
||
<input
|
||
class="function-search-input"
|
||
type="search"
|
||
name="function_search"
|
||
placeholder="Funktion suchen..."
|
||
list="function-search-options"
|
||
autocomplete="off"
|
||
>
|
||
<button class="function-search-btn" type="submit">Los</button>
|
||
</form>
|
||
</div>
|
||
<span class="navbar-text text-light me-3">{{ session['username'] }}</span>
|
||
<div class="dropdown me-2 user-menu-wrap">
|
||
<button class="btn btn-secondary dropdown-toggle user-menu-btn" type="button" id="invUserMenuDropdown" data-bs-toggle="dropdown" aria-expanded="false" data-notification-button="true">
|
||
👤
|
||
<span class="user-notification-dot {% if unread_notification_count and unread_notification_count > 0 %}visible{% endif %}" aria-hidden="true"></span>
|
||
</button>
|
||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="invUserMenuDropdown">
|
||
{% if current_permissions.pages.get('notifications_view', True) %}
|
||
<li><a class="dropdown-item" href="{{ url_for('notifications_view') }}">Benachrichtigungen</a></li>
|
||
{% endif %}
|
||
<li><hr class="dropdown-divider"></li>
|
||
<li><a class="dropdown-item" href="{{ url_for('change_password') }}">Passwort ändern</a></li>
|
||
<li><hr class="dropdown-divider"></li>
|
||
<li><a class="dropdown-item" href="{{ url_for('logout') }}">Logout</a></li>
|
||
</ul>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
{% endif %}
|
||
|
||
{% if library_module_enabled and CURRENT_MODULE == 'library' %}
|
||
<nav class="navbar navbar-expand-lg navbar-dark" id="libraryNavbar">
|
||
<div class="container-fluid">
|
||
<a class="navbar-brand" href="{{ url_for('library_view') }}" data-icon="📚">Bibliothek</a>
|
||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#libraryNavContent">
|
||
<span class="navbar-toggler-icon"></span>
|
||
</button>
|
||
<div class="collapse navbar-collapse" id="libraryNavContent">
|
||
<ul class="navbar-nav me-auto mb-2 mb-lg-0" id="libraryNavList">
|
||
{% if current_permissions.pages.get('library_view', True) %}
|
||
<li class="nav-item" data-nav-fixed="true">
|
||
<a class="nav-link {% if current_path == url_for('library_view') %}nav-active{% endif %}" href="{{ url_for('library_view') }}">Medien</a>
|
||
</li>
|
||
{% endif %}
|
||
{% if 'username' in session %}
|
||
{% if current_permissions.pages.get('my_borrowed_items', True) %}
|
||
<li class="nav-item" data-nav-fixed="true">
|
||
<a class="nav-link quick-link-pill {% if current_path == url_for('my_borrowed_items') %}nav-active{% endif %}" href="{{ url_for('my_borrowed_items') }}">Meine Medien</a>
|
||
</li>
|
||
{% endif %}
|
||
{% if current_permissions.pages.get('tutorial_page', True) %}
|
||
<li class="nav-item" data-nav-fixed="true">
|
||
<a class="nav-link quick-link-pill {% if current_path == url_for('tutorial_page') %}nav-active{% endif %}" href="{{ url_for('tutorial_page') }}">Tutorial</a>
|
||
</li>
|
||
{% endif %}
|
||
{% endif %}
|
||
{% if 'username' in session and current_permissions.actions.get('can_insert', True) and current_permissions.pages.get('library_admin', True) %}
|
||
<li class="nav-item">
|
||
<a class="nav-link nav-priority-link {% if current_path == url_for('library_admin') %}nav-active{% endif %}" href="{{ url_for('library_admin') }}">📖 Hochladen</a>
|
||
</li>
|
||
{% endif %}
|
||
<li class="nav-item dropdown ms-lg-auto">
|
||
<a class="nav-link dropdown-toggle" href="#" id="libMoreDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false" title="Weitere Optionen">
|
||
Mehr Optionen
|
||
</a>
|
||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="libMoreDropdown">
|
||
{% if 'username' in session %}
|
||
{% if current_permissions.pages.get('my_borrowed_items', True) %}
|
||
<li><a class="dropdown-item" href="{{ url_for('my_borrowed_items') }}">Meine Medien</a></li>
|
||
{% endif %}
|
||
{% if current_permissions.pages.get('tutorial_page', True) %}
|
||
<li><a class="dropdown-item" href="{{ url_for('tutorial_page') }}">Tutorial</a></li>
|
||
{% endif %}
|
||
<li><hr class="dropdown-divider"></li>
|
||
{% endif %}
|
||
{% if 'username' in session and (session.get('admin', False) or is_admin) and current_permissions.actions.get('can_manage_settings', True) %}
|
||
<li><h6 class="dropdown-header">Bibliotheks-Verwaltung</h6></li>
|
||
{% if current_permissions.pages.get('library_loans_admin', True) %}
|
||
<li><a class="dropdown-item" href="{{ url_for('library_loans_admin') }}">Ausleihen</a></li>
|
||
{% endif %}
|
||
{% if current_permissions.pages.get('admin_damaged_items', True) %}
|
||
<li><a class="dropdown-item" href="{{ url_for('admin_damaged_items') }}">Defekte Items</a></li>
|
||
{% endif %}
|
||
{% if student_cards_module_enabled %}
|
||
<li><a class="dropdown-item" href="{{ url_for('student_cards_admin') }}">Bibliotheksausweis</a></li>
|
||
{% endif %}
|
||
<li><hr class="dropdown-divider"></li>
|
||
{% if current_permissions.actions.get('can_manage_users', True) %}
|
||
<li><h6 class="dropdown-header">System</h6></li>
|
||
{% if current_permissions.pages.get('user_del', True) %}
|
||
<li><a class="dropdown-item" href="{{ url_for('user_del') }}">Benutzer verwalten</a></li>
|
||
{% endif %}
|
||
{% if current_permissions.pages.get('register', True) %}
|
||
<li><a class="dropdown-item" href="{{ url_for('register') }}">Neuer Benutzer</a></li>
|
||
{% endif %}
|
||
<li><hr class="dropdown-divider"></li>
|
||
{% endif %}
|
||
{% endif %}
|
||
<li><a class="dropdown-item" href="{{ url_for('impressum') }}">Impressum</a></li>
|
||
<li><a class="dropdown-item" href="{{ url_for('license') }}">Lizenz</a></li>
|
||
</ul>
|
||
</li>
|
||
<li class="nav-item d-none" id="lib-nav-overflow-anchor" aria-hidden="true"></li>
|
||
</ul>
|
||
<div class="d-flex">
|
||
{% if 'username' in session %}
|
||
<div class="function-search-wrap">
|
||
<form class="function-search-form" data-function-search="true">
|
||
<input
|
||
class="function-search-input"
|
||
type="search"
|
||
name="function_search"
|
||
placeholder="Funktion suchen..."
|
||
list="function-search-options"
|
||
autocomplete="off"
|
||
>
|
||
<button class="function-search-btn" type="submit">Los</button>
|
||
</form>
|
||
</div>
|
||
<span class="navbar-text text-light me-3">{{ session['username'] }}</span>
|
||
<div class="dropdown me-2 user-menu-wrap">
|
||
<button class="btn btn-secondary dropdown-toggle user-menu-btn" type="button" id="libUserMenuDropdown" data-bs-toggle="dropdown" aria-expanded="false" data-notification-button="true">
|
||
👤
|
||
<span class="user-notification-dot {% if unread_notification_count and unread_notification_count > 0 %}visible{% endif %}" aria-hidden="true"></span>
|
||
</button>
|
||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="libUserMenuDropdown">
|
||
{% if current_permissions.pages.get('notifications_view', True) %}
|
||
<li><a class="dropdown-item" href="{{ url_for('notifications_view') }}">Benachrichtigungen</a></li>
|
||
{% endif %}
|
||
<li><hr class="dropdown-divider"></li>
|
||
<li><a class="dropdown-item" href="{{ url_for('change_password') }}">Passwort ändern</a></li>
|
||
<li><hr class="dropdown-divider"></li>
|
||
<li><a class="dropdown-item" href="{{ url_for('logout') }}">Logout</a></li>
|
||
</ul>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
{% endif %}
|
||
<div class="container">
|
||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||
{% if messages %}
|
||
<div class="flashes">
|
||
{% for category, message in messages %}
|
||
<div class="flash {{ category }}">{{ message }}</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
{% endwith %}
|
||
{% block content %}{% endblock %}
|
||
</div>
|
||
<!-- Cookie consent banner -->
|
||
<style>
|
||
#cookie-banner { position: fixed; bottom: 0; left: 0; right: 0; background: rgba(33,37,41,.98); color: #fff; padding: 14px 16px; display: none; z-index: 2000; box-shadow: 0 -2px 8px rgba(0,0,0,.25); }
|
||
#cookie-banner .cb-inner { max-width: 1100px; margin: 0 auto; display:flex; gap:12px; align-items:center; justify-content:space-between; }
|
||
#cookie-banner .cb-text { font-size: .95rem; line-height:1.35; margin-right: 12px; }
|
||
#cookie-banner .cb-actions { display:flex; gap:8px; flex-shrink:0; }
|
||
#cookie-banner .btn-accept { background:#28a745; border:2px solid #28a745; color:#fff; padding:6px 12px; border-radius:4px; cursor:pointer; }
|
||
#cookie-banner .btn-decline { background:transparent; border:2px solid #ffc107; color:#ffc107; padding:6px 12px; border-radius:4px; cursor:pointer; }
|
||
#cookie-banner .btn-accept:hover { background:#218838; border-color:#218838; }
|
||
#cookie-banner .btn-decline:hover { background:#ffc107; color:#212529; }
|
||
|
||
#onboarding-overlay {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(15, 23, 42, 0.6);
|
||
display: none;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 2100;
|
||
padding: 16px;
|
||
}
|
||
|
||
#onboarding-modal {
|
||
width: min(680px, 100%);
|
||
background: #ffffff;
|
||
border-radius: 14px;
|
||
border: 1px solid #dbe3ee;
|
||
box-shadow: 0 18px 35px rgba(15, 23, 42, 0.25);
|
||
padding: 20px;
|
||
}
|
||
|
||
#onboarding-modal h3 {
|
||
margin: 0 0 8px;
|
||
color: #0f172a;
|
||
font-size: 1.2rem;
|
||
font-weight: 800;
|
||
}
|
||
|
||
#onboarding-modal p {
|
||
margin: 0;
|
||
color: #334155;
|
||
line-height: 1.45;
|
||
}
|
||
|
||
.onboarding-actions {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
margin-top: 14px;
|
||
}
|
||
|
||
.onboarding-actions .btn {
|
||
border-radius: 10px;
|
||
}
|
||
|
||
.dynamic-overflow-control .dropdown-menu {
|
||
min-width: 240px;
|
||
max-width: min(420px, 85vw);
|
||
max-height: 70vh;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.dynamic-overflow-control .dropdown-header {
|
||
font-weight: 800;
|
||
color: var(--module-accent-color);
|
||
font-size: 0.78rem;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.04em;
|
||
}
|
||
|
||
.notif-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-width: 20px;
|
||
height: 20px;
|
||
border-radius: 999px;
|
||
background: #dc2626;
|
||
color: #fff;
|
||
font-size: 0.72rem;
|
||
font-weight: 800;
|
||
padding: 0 6px;
|
||
margin-left: 8px;
|
||
line-height: 1;
|
||
}
|
||
|
||
.user-menu-wrap {
|
||
position: relative;
|
||
}
|
||
|
||
.user-menu-btn {
|
||
position: relative;
|
||
}
|
||
|
||
.user-notification-dot {
|
||
position: absolute;
|
||
top: 6px;
|
||
right: 10px;
|
||
width: 10px;
|
||
height: 10px;
|
||
border-radius: 50%;
|
||
background: #dc2626;
|
||
border: 2px solid #ffffff;
|
||
box-shadow: 0 0 0 1px rgba(220, 38, 38, 0.35);
|
||
display: none;
|
||
}
|
||
|
||
.user-notification-dot.visible {
|
||
display: block;
|
||
}
|
||
|
||
.notification-toast {
|
||
position: fixed;
|
||
right: 16px;
|
||
bottom: 18px;
|
||
z-index: 2300;
|
||
max-width: 360px;
|
||
background: #0f172a;
|
||
color: #fff;
|
||
border-radius: 10px;
|
||
border: 1px solid rgba(148, 163, 184, 0.35);
|
||
box-shadow: 0 18px 35px rgba(2, 6, 23, 0.4);
|
||
padding: 12px 14px;
|
||
font-size: 0.9rem;
|
||
line-height: 1.35;
|
||
display: none;
|
||
}
|
||
|
||
.notification-toast strong {
|
||
display: block;
|
||
margin-bottom: 2px;
|
||
}
|
||
|
||
.notification-toast.show {
|
||
display: block;
|
||
}
|
||
</style>
|
||
<div id="cookie-banner" role="dialog" aria-live="polite" aria-label="Cookie-Hinweis">
|
||
<div class="cb-inner">
|
||
<div class="cb-text">
|
||
Wir verwenden technisch notwendige Cookies, um Ihre Sitzung zu verwalten und die Anwendung bereitzustellen. Bitte akzeptieren Sie Cookies, um fortzufahren.
|
||
</div>
|
||
<div class="cb-actions">
|
||
<button class="btn-decline" id="cookie-decline">Ablehnen</button>
|
||
<button class="btn-accept" id="cookie-accept">Akzeptieren</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="onboarding-overlay" role="dialog" aria-modal="true" aria-label="Tutorial Vorschlag">
|
||
<div id="onboarding-modal">
|
||
<h3>Wollen Sie eine Vorstellung des Produkts?</h3>
|
||
<p>
|
||
Wir empfehlen ein kurzes Tutorial (2-5 Minuten), damit Sie die Kernfunktionen anhand einer vorgefertigten Seite kennenlernen und gut auf den Schulalltagsbetrieb vorbereitet sind.
|
||
</p>
|
||
<div class="onboarding-actions">
|
||
<button type="button" class="btn btn-primary btn-sm" id="onboarding-start">Ja, Tutorial starten</button>
|
||
<button type="button" class="btn btn-outline-secondary btn-sm" id="onboarding-later">Später erinnern</button>
|
||
<button type="button" class="btn btn-outline-dark btn-sm" id="onboarding-dismiss">Nicht mehr anzeigen</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="notification-toast" class="notification-toast" role="status" aria-live="polite">
|
||
<strong id="notification-toast-title">Neue Benachrichtigung</strong>
|
||
<span id="notification-toast-message"></span>
|
||
</div>
|
||
|
||
<datalist id="function-search-options">
|
||
<option value="Artikel"></option>
|
||
<option value="Meine Ausleihen"></option>
|
||
<option value="Benachrichtigungen"></option>
|
||
<option value="Tutorial"></option>
|
||
<option value="Impressum"></option>
|
||
<option value="Lizenz"></option>
|
||
{% if library_module_enabled %}
|
||
<option value="Bibliothek"></option>
|
||
<option value="Meine Medien"></option>
|
||
{% endif %}
|
||
{% if 'username' in session and (session.get('admin', False) or is_admin) %}
|
||
<option value="Hochladen"></option>
|
||
<option value="Ausleihen Verwaltung"></option>
|
||
<option value="Defekte Items"></option>
|
||
<option value="Filter verwalten"></option>
|
||
<option value="Orte verwalten"></option>
|
||
<option value="Audit Dashboard"></option>
|
||
<option value="Logs"></option>
|
||
<option value="Benutzer verwalten"></option>
|
||
<option value="Neuer Benutzer"></option>
|
||
{% if student_cards_module_enabled %}
|
||
<option value="Bibliotheksausweis"></option>
|
||
{% endif %}
|
||
{% endif %}
|
||
</datalist>
|
||
|
||
<script>
|
||
(function(){
|
||
function getCookie(name){
|
||
const v = document.cookie.split(';').map(s=>s.trim());
|
||
for(const c of v){ if(c.startsWith(name+'=')) return decodeURIComponent(c.split('=')[1]); }
|
||
return null;
|
||
}
|
||
function setCookie(name, value, days){
|
||
const d = new Date(); d.setTime(d.getTime() + (days*24*60*60*1000));
|
||
document.cookie = name + '=' + encodeURIComponent(value) + ';expires=' + d.toUTCString() + ';path=/;SameSite=Lax';
|
||
}
|
||
function showBanner(){ var el = document.getElementById('cookie-banner'); if(el) el.style.display = 'block'; }
|
||
function hideBanner(){ var el = document.getElementById('cookie-banner'); if(el) el.style.display = 'none'; }
|
||
|
||
// If not decided yet, show banner and block app until decision
|
||
const consent = getCookie('cookie_consent');
|
||
if(!consent){
|
||
showBanner();
|
||
// Optionally blur content until consent
|
||
document.body.style.filter = 'none';
|
||
}
|
||
document.getElementById('cookie-accept')?.addEventListener('click', function(){
|
||
setCookie('cookie_consent','accepted',365);
|
||
hideBanner();
|
||
});
|
||
document.getElementById('cookie-decline')?.addEventListener('click', function(){
|
||
setCookie('cookie_consent','declined',365);
|
||
window.location.href = 'https://www.ecosia.org/';
|
||
});
|
||
|
||
const username = {{ (session['username'] if 'username' in session else '')|tojson }};
|
||
const isTutorialPage = window.location.pathname === {{ url_for('tutorial_page')|tojson }};
|
||
const isLoginPage = window.location.pathname === {{ url_for('login')|tojson }};
|
||
const notificationsPagePath = {{ url_for('notifications_view')|tojson }};
|
||
const onboardingKey = username ? ('inventarsystem_tutorial_prompt_v1_' + username) : null;
|
||
const onboardingOverlay = document.getElementById('onboarding-overlay');
|
||
const notificationButtons = Array.from(document.querySelectorAll('[data-notification-button="true"]'));
|
||
const notificationToast = document.getElementById('notification-toast');
|
||
const notificationToastTitle = document.getElementById('notification-toast-title');
|
||
const notificationToastMessage = document.getElementById('notification-toast-message');
|
||
let lastUnreadCount = Number({{ unread_notification_count|default(0)|int }});
|
||
const loginHintKey = username ? ('inventarsystem_notification_login_hint_v1_' + username) : null;
|
||
|
||
const functionSearchEntries = [
|
||
{ label: 'Artikel', keywords: ['artikel', 'inventar', 'home'], url: {{ url_for('home')|tojson }} },
|
||
{ label: 'Meine Ausleihen', keywords: ['meine ausleihen', 'ausleihen', 'borrowed'], url: {{ url_for('my_borrowed_items')|tojson }} },
|
||
{ label: 'Benachrichtigungen', keywords: ['benachrichtigungen', 'nachrichten', 'notifications'], url: {{ url_for('notifications_view')|tojson }} },
|
||
{ label: 'Tutorial', keywords: ['tutorial', 'hilfe', 'anleitung'], url: {{ url_for('tutorial_page')|tojson }} },
|
||
{ label: 'Impressum', keywords: ['impressum'], url: {{ url_for('impressum')|tojson }} },
|
||
{ label: 'Lizenz', keywords: ['lizenz', 'license'], url: {{ url_for('license')|tojson }} },
|
||
{% if library_module_enabled %}
|
||
{ label: 'Bibliothek', keywords: ['bibliothek', 'medien'], url: {{ url_for('library_view')|tojson }} },
|
||
{% endif %}
|
||
{% if 'username' in session and (session.get('admin', False) or is_admin) %}
|
||
{ label: 'Hochladen', keywords: ['hochladen', 'upload'], url: {{ url_for('upload_admin')|tojson }} },
|
||
{ label: 'Ausleihen Verwaltung', keywords: ['ausleihen verwaltung', 'admin borrowings'], url: {{ url_for('admin_borrowings')|tojson }} },
|
||
{ label: 'Defekte Items', keywords: ['defekte items', 'defekt', 'schaden'], url: {{ url_for('admin_damaged_items')|tojson }} },
|
||
{ label: 'Filter verwalten', keywords: ['filter verwalten', 'filter'], url: {{ url_for('manage_filters')|tojson }} },
|
||
{ label: 'Orte verwalten', keywords: ['orte verwalten', 'orte', 'location'], url: {{ url_for('manage_locations')|tojson }} },
|
||
{ label: 'Audit Dashboard', keywords: ['audit', 'audit dashboard'], url: {{ url_for('admin_audit_dashboard')|tojson }} },
|
||
{ label: 'Logs', keywords: ['logs', 'protokoll'], url: {{ url_for('logs')|tojson }} },
|
||
{ label: 'Benutzer verwalten', keywords: ['benutzer verwalten', 'user'], url: {{ url_for('user_del')|tojson }} },
|
||
{ label: 'Neuer Benutzer', keywords: ['neuer benutzer', 'register'], url: {{ url_for('register')|tojson }} },
|
||
{% if library_module_enabled %}
|
||
{ label: 'Bibliotheks-Ausleihen', keywords: ['bibliotheks ausleihen', 'library loans'], url: {{ url_for('library_loans_admin')|tojson }} },
|
||
{% endif %}
|
||
{% if student_cards_module_enabled %}
|
||
{ label: 'Bibliotheksausweis', keywords: ['bibliotheksausweis', 'student card'], url: {{ url_for('student_cards_admin')|tojson }} },
|
||
{% endif %}
|
||
{% endif %}
|
||
];
|
||
|
||
function normalizeSearchText(value) {
|
||
return String(value || '')
|
||
.toLowerCase()
|
||
.replace(/[ä]/g, 'ae')
|
||
.replace(/[ö]/g, 'oe')
|
||
.replace(/[ü]/g, 'ue')
|
||
.replace(/[ß]/g, 'ss')
|
||
.trim();
|
||
}
|
||
|
||
function findFunctionRoute(rawValue) {
|
||
const input = normalizeSearchText(rawValue);
|
||
if (!input) {
|
||
return null;
|
||
}
|
||
|
||
let exact = null;
|
||
for (const entry of functionSearchEntries) {
|
||
const candidates = [entry.label].concat(entry.keywords || []);
|
||
for (const candidate of candidates) {
|
||
if (normalizeSearchText(candidate) === input) {
|
||
exact = entry;
|
||
break;
|
||
}
|
||
}
|
||
if (exact) {
|
||
break;
|
||
}
|
||
}
|
||
if (exact) {
|
||
return exact.url;
|
||
}
|
||
|
||
for (const entry of functionSearchEntries) {
|
||
const candidates = [entry.label].concat(entry.keywords || []);
|
||
for (const candidate of candidates) {
|
||
if (normalizeSearchText(candidate).includes(input) || input.includes(normalizeSearchText(candidate))) {
|
||
return entry.url;
|
||
}
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function bindFunctionSearchForms() {
|
||
const forms = document.querySelectorAll('form[data-function-search="true"]');
|
||
forms.forEach(function(form) {
|
||
form.addEventListener('submit', function(event) {
|
||
event.preventDefault();
|
||
const input = form.querySelector('input[name="function_search"]');
|
||
const targetUrl = findFunctionRoute(input ? input.value : '');
|
||
if (targetUrl) {
|
||
window.location.href = targetUrl;
|
||
return;
|
||
}
|
||
showInAppNotification('Keine Funktion gefunden', 'Bitte Suche verfeinern, z.B. "Defekte Items" oder "Benachrichtigungen".');
|
||
});
|
||
});
|
||
}
|
||
|
||
bindFunctionSearchForms();
|
||
|
||
function maybeShowLoginNotificationHint() {
|
||
if (!username || !loginHintKey) {
|
||
return;
|
||
}
|
||
if (window.location.pathname === notificationsPagePath) {
|
||
return;
|
||
}
|
||
|
||
const alreadyShown = sessionStorage.getItem(loginHintKey);
|
||
if (alreadyShown) {
|
||
return;
|
||
}
|
||
|
||
if (lastUnreadCount > 0) {
|
||
const hintTitle = 'Neue Benachrichtigungen';
|
||
const hintMessage =
|
||
lastUnreadCount === 1
|
||
? 'Sie haben 1 neue Benachrichtigung. Im Nutzer-Menue finden Sie den Eintrag Benachrichtigungen.'
|
||
: 'Sie haben ' + lastUnreadCount + ' neue Benachrichtigungen. Im Nutzer-Menue finden Sie den Eintrag Benachrichtigungen.';
|
||
showInAppNotification(hintTitle, hintMessage);
|
||
}
|
||
|
||
sessionStorage.setItem(loginHintKey, 'shown');
|
||
}
|
||
|
||
maybeShowLoginNotificationHint();
|
||
|
||
function updateNotificationDots(unreadCount) {
|
||
const hasUnread = Number(unreadCount) > 0;
|
||
notificationButtons.forEach(function(btn) {
|
||
const dot = btn.querySelector('.user-notification-dot');
|
||
if (!dot) {
|
||
return;
|
||
}
|
||
if (hasUnread) {
|
||
dot.classList.add('visible');
|
||
} else {
|
||
dot.classList.remove('visible');
|
||
}
|
||
});
|
||
}
|
||
|
||
function showInAppNotification(title, message) {
|
||
if (!notificationToast || !notificationToastTitle || !notificationToastMessage) {
|
||
return;
|
||
}
|
||
notificationToastTitle.textContent = title || 'Neue Benachrichtigung';
|
||
notificationToastMessage.textContent = message || '';
|
||
notificationToast.classList.add('show');
|
||
window.setTimeout(function() {
|
||
notificationToast.classList.remove('show');
|
||
}, 5000);
|
||
}
|
||
|
||
function showBrowserNotification(title, message) {
|
||
if (!('Notification' in window)) {
|
||
showInAppNotification(title, message);
|
||
return;
|
||
}
|
||
if (Notification.permission === 'granted') {
|
||
new Notification(title || 'Neue Benachrichtigung', {
|
||
body: message || '',
|
||
});
|
||
return;
|
||
}
|
||
if (Notification.permission === 'default') {
|
||
Notification.requestPermission().then(function(permission) {
|
||
if (permission === 'granted') {
|
||
new Notification(title || 'Neue Benachrichtigung', {
|
||
body: message || '',
|
||
});
|
||
} else {
|
||
showInAppNotification(title, message);
|
||
}
|
||
}).catch(function() {
|
||
showInAppNotification(title, message);
|
||
});
|
||
return;
|
||
}
|
||
showInAppNotification(title, message);
|
||
}
|
||
|
||
function pollNotificationStatus() {
|
||
if (!username) {
|
||
return;
|
||
}
|
||
|
||
fetch('/notifications/unread_status', {
|
||
method: 'GET',
|
||
headers: {
|
||
'Accept': 'application/json'
|
||
}
|
||
})
|
||
.then(function(response) {
|
||
if (!response.ok) {
|
||
throw new Error('status request failed');
|
||
}
|
||
return response.json();
|
||
})
|
||
.then(function(data) {
|
||
if (!data || !data.ok) {
|
||
return;
|
||
}
|
||
|
||
const unreadCount = Number(data.unread_count || 0);
|
||
updateNotificationDots(unreadCount);
|
||
|
||
if (unreadCount > lastUnreadCount && window.location.pathname !== notificationsPagePath) {
|
||
const latest = data.latest_unread || {};
|
||
showBrowserNotification(latest.title || 'Neue Nachricht', latest.message || 'Es gibt neue Benachrichtigungen.');
|
||
}
|
||
|
||
lastUnreadCount = unreadCount;
|
||
})
|
||
.catch(function() {
|
||
// Silent fail; polling retries automatically.
|
||
});
|
||
}
|
||
|
||
updateNotificationDots(lastUnreadCount);
|
||
if (username) {
|
||
window.setInterval(pollNotificationStatus, 30000);
|
||
}
|
||
|
||
function showOnboarding(){
|
||
if (onboardingOverlay) {
|
||
onboardingOverlay.style.display = 'flex';
|
||
}
|
||
}
|
||
|
||
function hideOnboarding(){
|
||
if (onboardingOverlay) {
|
||
onboardingOverlay.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
if (onboardingKey && !isTutorialPage && !isLoginPage) {
|
||
const decision = localStorage.getItem(onboardingKey);
|
||
if (!decision || decision === 'later') {
|
||
showOnboarding();
|
||
}
|
||
}
|
||
|
||
document.getElementById('onboarding-start')?.addEventListener('click', function(){
|
||
if (onboardingKey) {
|
||
localStorage.setItem(onboardingKey, 'started');
|
||
}
|
||
window.location.href = {{ url_for('tutorial_page')|tojson }};
|
||
});
|
||
|
||
document.getElementById('onboarding-later')?.addEventListener('click', function(){
|
||
if (onboardingKey) {
|
||
localStorage.setItem(onboardingKey, 'later');
|
||
}
|
||
hideOnboarding();
|
||
});
|
||
|
||
document.getElementById('onboarding-dismiss')?.addEventListener('click', function(){
|
||
if (onboardingKey) {
|
||
localStorage.setItem(onboardingKey, 'dismissed');
|
||
}
|
||
hideOnboarding();
|
||
});
|
||
|
||
const navList = document.getElementById('inventoryNavList');
|
||
const navOverflowAnchor = document.getElementById('inv-nav-overflow-anchor');
|
||
const libraryNavList = document.getElementById('libraryNavList');
|
||
const libNavOverflowAnchor = document.getElementById('lib-nav-overflow-anchor');
|
||
|
||
function initNavbarOverflow(navList, navOverflowAnchor) {
|
||
if (!navList || !navOverflowAnchor) return;
|
||
|
||
const navRoot = navList.closest('nav.navbar');
|
||
const navCollapse = navList.closest('.navbar-collapse');
|
||
const navContainer = navList.closest('.container-fluid') || navList.parentElement;
|
||
|
||
function applyCompactMode() {
|
||
if (!navRoot || !navContainer) return;
|
||
const width = navContainer.clientWidth || window.innerWidth;
|
||
navRoot.classList.remove('nav-compact', 'nav-ultra-compact');
|
||
|
||
if (window.innerWidth < 992) {
|
||
return;
|
||
}
|
||
|
||
if (width < 1220) {
|
||
navRoot.classList.add('nav-compact');
|
||
}
|
||
if (width < 1080) {
|
||
navRoot.classList.add('nav-ultra-compact');
|
||
}
|
||
}
|
||
|
||
function collectTopLevelNavSources() {
|
||
if (!navList) return [];
|
||
return Array.from(navList.children).filter(function(li){
|
||
if (!(li instanceof HTMLElement)) return false;
|
||
if (!li.classList.contains('nav-item')) return false;
|
||
if (li.id === navOverflowAnchor.id) return false;
|
||
if (li.dataset.overflowControl === 'true') return false;
|
||
if (li.dataset.navFixed === 'true') return false;
|
||
return li.style.display !== 'none';
|
||
});
|
||
}
|
||
|
||
function clearOverflowControls() {
|
||
if (!navList) return;
|
||
navList.querySelectorAll('[data-overflow-control="true"]').forEach(function(node){
|
||
node.remove();
|
||
});
|
||
}
|
||
|
||
function restoreAllNavItems() {
|
||
if (!navList) return;
|
||
navList.querySelectorAll('li.nav-item').forEach(function(li){
|
||
if (li.id === navOverflowAnchor.id) return;
|
||
if (li.dataset.overflowControl === 'true') return;
|
||
li.style.display = '';
|
||
});
|
||
clearOverflowControls();
|
||
}
|
||
|
||
function createOverflowControl() {
|
||
const li = document.createElement('li');
|
||
li.className = 'nav-item dropdown';
|
||
li.dataset.overflowControl = 'true';
|
||
|
||
const toggleId = navOverflowAnchor.id + '-toggle';
|
||
const menuId = navOverflowAnchor.id + '-menu';
|
||
|
||
li.innerHTML =
|
||
'<a class="nav-link dropdown-toggle" href="#" id="' + toggleId + '" role="button" data-bs-toggle="dropdown" aria-expanded="false" aria-controls="' + menuId + '">⋮ Weitere</a>' +
|
||
'<ul class="dropdown-menu" aria-labelledby="' + toggleId + '" id="' + menuId + '"></ul>';
|
||
|
||
return li;
|
||
}
|
||
|
||
function getNavCandidatePriority(item) {
|
||
if (!(item instanceof HTMLElement)) {
|
||
return -1;
|
||
}
|
||
|
||
const topLink = item.querySelector(':scope > a.nav-link');
|
||
if (!topLink) {
|
||
return 10;
|
||
}
|
||
|
||
if (topLink.classList.contains('nav-active')) {
|
||
return 1000;
|
||
}
|
||
|
||
if (topLink.classList.contains('nav-priority-link')) {
|
||
return 900;
|
||
}
|
||
|
||
if (topLink.classList.contains('quick-link-pill')) {
|
||
return 700;
|
||
}
|
||
|
||
if (item.classList.contains('dropdown')) {
|
||
return 300;
|
||
}
|
||
|
||
return 500;
|
||
}
|
||
|
||
function pickNextNavItemToHide(candidates) {
|
||
if (!Array.isArray(candidates) || candidates.length === 0) {
|
||
return null;
|
||
}
|
||
|
||
let selected = candidates[0];
|
||
let selectedIndex = 0;
|
||
let selectedPriority = getNavCandidatePriority(selected);
|
||
|
||
for (let i = 1; i < candidates.length; i += 1) {
|
||
const candidate = candidates[i];
|
||
const candidatePriority = getNavCandidatePriority(candidate);
|
||
if (candidatePriority < selectedPriority || (candidatePriority === selectedPriority && i > selectedIndex)) {
|
||
selected = candidate;
|
||
selectedIndex = i;
|
||
selectedPriority = candidatePriority;
|
||
}
|
||
}
|
||
|
||
return selected;
|
||
}
|
||
|
||
function appendSourceToOverflowMenu(sourceItem, menu) {
|
||
const topLink = sourceItem.querySelector(':scope > a.nav-link');
|
||
if (!topLink) return;
|
||
|
||
const childMenu = sourceItem.querySelector(':scope > ul.dropdown-menu');
|
||
const sourceLabel = topLink.textContent.trim();
|
||
|
||
if (!childMenu) {
|
||
const entry = document.createElement('li');
|
||
entry.innerHTML = '<a class="dropdown-item" href="' + topLink.getAttribute('href') + '">' + sourceLabel + '</a>';
|
||
menu.appendChild(entry);
|
||
return;
|
||
}
|
||
|
||
const header = document.createElement('li');
|
||
header.innerHTML = '<h6 class="dropdown-header">' + sourceLabel + '</h6>';
|
||
menu.appendChild(header);
|
||
|
||
childMenu.querySelectorAll(':scope > li > a.dropdown-item').forEach(function(subLink){
|
||
const subEntry = document.createElement('li');
|
||
subEntry.innerHTML = '<a class="dropdown-item" style="padding-left: 2.5rem;" href="' + subLink.getAttribute('href') + '">' + subLink.textContent.trim() + '</a>';
|
||
menu.appendChild(subEntry);
|
||
});
|
||
|
||
const divider = document.createElement('li');
|
||
divider.innerHTML = '<hr class="dropdown-divider">';
|
||
menu.appendChild(divider);
|
||
}
|
||
|
||
function rebuildOverflowControl(hiddenSources) {
|
||
clearOverflowControls();
|
||
if (!navList || !navOverflowAnchor || hiddenSources.length === 0) return;
|
||
|
||
const control = createOverflowControl();
|
||
const menu = control.querySelector('ul.dropdown-menu');
|
||
const controlLink = control.querySelector(':scope > a.nav-link');
|
||
if (controlLink) {
|
||
controlLink.textContent = '⋮ Weitere (' + hiddenSources.length + ')';
|
||
}
|
||
|
||
hiddenSources.forEach(function(source){
|
||
appendSourceToOverflowMenu(source, menu);
|
||
});
|
||
|
||
const trailingDivider = menu.querySelector('li:last-child .dropdown-divider');
|
||
if (trailingDivider) {
|
||
trailingDivider.parentElement.remove();
|
||
}
|
||
|
||
navList.insertBefore(control, navOverflowAnchor);
|
||
}
|
||
|
||
function adaptNavbarByWidth() {
|
||
if (!navList || !navOverflowAnchor) return;
|
||
|
||
applyCompactMode();
|
||
|
||
if (window.innerWidth < 992) {
|
||
restoreAllNavItems();
|
||
return;
|
||
}
|
||
|
||
restoreAllNavItems();
|
||
|
||
const hiddenSources = [];
|
||
let candidates = collectTopLevelNavSources();
|
||
|
||
while (navList.scrollWidth > navList.clientWidth && candidates.length > 0) {
|
||
const toHide = pickNextNavItemToHide(candidates);
|
||
if (!toHide) {
|
||
break;
|
||
}
|
||
toHide.style.display = 'none';
|
||
hiddenSources.unshift(toHide);
|
||
candidates = collectTopLevelNavSources();
|
||
}
|
||
|
||
rebuildOverflowControl(hiddenSources);
|
||
|
||
candidates = collectTopLevelNavSources();
|
||
while (navList.scrollWidth > navList.clientWidth && candidates.length > 0) {
|
||
const toHide = pickNextNavItemToHide(candidates);
|
||
if (!toHide) {
|
||
break;
|
||
}
|
||
toHide.style.display = 'none';
|
||
hiddenSources.unshift(toHide);
|
||
rebuildOverflowControl(hiddenSources);
|
||
candidates = collectTopLevelNavSources();
|
||
}
|
||
}
|
||
|
||
let resizeTimer = null;
|
||
function debounceAdapt() {
|
||
if (resizeTimer) {
|
||
window.clearTimeout(resizeTimer);
|
||
}
|
||
resizeTimer = window.setTimeout(adaptNavbarByWidth, 120);
|
||
}
|
||
|
||
adaptNavbarByWidth();
|
||
window.addEventListener('resize', debounceAdapt);
|
||
|
||
if (navCollapse) {
|
||
navCollapse.addEventListener('shown.bs.collapse', debounceAdapt);
|
||
navCollapse.addEventListener('hidden.bs.collapse', debounceAdapt);
|
||
}
|
||
|
||
if ('ResizeObserver' in window && navContainer) {
|
||
const resizeObserver = new ResizeObserver(function() {
|
||
debounceAdapt();
|
||
});
|
||
resizeObserver.observe(navContainer);
|
||
}
|
||
}
|
||
|
||
// Initialize overflow control for inventory navbar
|
||
if (navList && navOverflowAnchor) {
|
||
initNavbarOverflow(navList, navOverflowAnchor);
|
||
}
|
||
|
||
// Initialize overflow control for library navbar
|
||
if (libraryNavList && libNavOverflowAnchor) {
|
||
initNavbarOverflow(libraryNavList, libNavOverflowAnchor);
|
||
}
|
||
})();
|
||
</script>
|
||
<!-- Mobile compatibility scripts -->
|
||
<script src="{{ url_for('static', filename='js/mobile_compatibility.js') }}"></script>
|
||
<script src="{{ url_for('static', filename='js/ios_fixes.js') }}"></script>
|
||
|
||
<!-- Push Notifications Script -->
|
||
<script src="{{ url_for('static', filename='js/push-notifications.js') }}"></script>
|
||
</body>
|
||
</html>
|