Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ec7f32994c | |||
| 8358bf257f | |||
| 193495d6a2 | |||
| 5afcaa5e19 | |||
| 3aa44b64b8 |
+69
@@ -7878,6 +7878,75 @@ def mark_all_notifications_read():
|
||||
return redirect(url_for('notifications_view'))
|
||||
|
||||
|
||||
@app.route('/notifications/unread_status', methods=['GET'])
|
||||
def notifications_unread_status():
|
||||
"""Return unread notification count and latest unread message metadata."""
|
||||
if 'username' not in session:
|
||||
return jsonify({'ok': False, 'error': 'not_authenticated'}), 401
|
||||
|
||||
username = session['username']
|
||||
is_admin_user = False
|
||||
try:
|
||||
is_admin_user = us.check_admin(username)
|
||||
except Exception:
|
||||
is_admin_user = False
|
||||
|
||||
client = None
|
||||
try:
|
||||
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
|
||||
db = client[MONGODB_DB]
|
||||
|
||||
visibility_query = {
|
||||
'$or': [
|
||||
{'Audience': 'user', 'TargetUser': username},
|
||||
]
|
||||
}
|
||||
if is_admin_user:
|
||||
visibility_query['$or'].append({'Audience': 'admin'})
|
||||
|
||||
unread_query = {
|
||||
'$and': [
|
||||
visibility_query,
|
||||
{'ReadBy': {'$ne': username}},
|
||||
]
|
||||
}
|
||||
|
||||
unread_count = db['notifications'].count_documents(unread_query)
|
||||
latest_unread = db['notifications'].find_one(
|
||||
unread_query,
|
||||
{
|
||||
'Title': 1,
|
||||
'Message': 1,
|
||||
'CreatedAt': 1,
|
||||
'Type': 1,
|
||||
'Severity': 1,
|
||||
},
|
||||
sort=[('CreatedAt', -1)]
|
||||
)
|
||||
|
||||
latest_payload = None
|
||||
if latest_unread:
|
||||
latest_payload = {
|
||||
'title': latest_unread.get('Title', 'Benachrichtigung'),
|
||||
'message': latest_unread.get('Message', ''),
|
||||
'created_at': latest_unread.get('CreatedAt').isoformat() if isinstance(latest_unread.get('CreatedAt'), datetime.datetime) else '',
|
||||
'type': latest_unread.get('Type', ''),
|
||||
'severity': latest_unread.get('Severity', 'info'),
|
||||
}
|
||||
|
||||
return jsonify({
|
||||
'ok': True,
|
||||
'unread_count': unread_count,
|
||||
'latest_unread': latest_payload,
|
||||
})
|
||||
except Exception as exc:
|
||||
app.logger.warning(f"Could not fetch unread notification status for {username}: {exc}")
|
||||
return jsonify({'ok': False, 'error': 'status_fetch_failed'}), 500
|
||||
finally:
|
||||
if client:
|
||||
client.close()
|
||||
|
||||
|
||||
@app.route('/admin/damaged_items')
|
||||
def admin_damaged_items():
|
||||
"""Dedicated admin management window for damaged items."""
|
||||
|
||||
+450
-28
@@ -63,6 +63,9 @@
|
||||
.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-brand {
|
||||
@@ -82,6 +85,65 @@
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.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);
|
||||
@@ -255,6 +317,27 @@
|
||||
.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 {
|
||||
@@ -363,6 +446,7 @@
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
{% set current_path = request.path %}
|
||||
<!-- Module Selector Bar -->
|
||||
{% if 'username' in session %}
|
||||
<div class="module-selector-bar" id="moduleBar">
|
||||
@@ -396,28 +480,28 @@
|
||||
<div class="collapse navbar-collapse" id="inventoryNavContent">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0" id="inventoryNavList">
|
||||
<li class="nav-item" data-nav-fixed="true">
|
||||
<a class="nav-link" href="{{ url_for('home') }}">Artikel</a>
|
||||
<a class="nav-link {% if current_path == url_for('home') %}nav-active{% endif %}" href="{{ url_for('home') }}">Artikel</a>
|
||||
</li>
|
||||
{% if 'username' in session %}
|
||||
<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>
|
||||
<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 %}
|
||||
{% if 'username' in session and (session.get('admin', False) or is_admin) %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link nav-priority-link" href="{{ url_for('upload_admin') }}">➕ Hochladen</a>
|
||||
<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">
|
||||
⋯ Mehr
|
||||
<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 %}
|
||||
<li><a class="dropdown-item" href="{{ url_for('my_borrowed_items') }}">Meine Ausleihen</a></li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ url_for('notifications_view') }}">
|
||||
Benachrichtigungen
|
||||
{% if unread_notification_count and unread_notification_count > 0 %}
|
||||
<span class="notif-badge">{{ unread_notification_count }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
<li><a class="dropdown-item" href="{{ url_for('tutorial_page') }}">Tutorial</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
{% endif %}
|
||||
@@ -443,12 +527,28 @@
|
||||
</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">
|
||||
<button class="btn btn-secondary dropdown-toggle" type="button" id="invUserMenuDropdown" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<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">
|
||||
<li><a class="dropdown-item" href="{{ url_for('notifications_view') }}">Benachrichtigungen</a></li>
|
||||
<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>
|
||||
@@ -471,28 +571,28 @@
|
||||
<div class="collapse navbar-collapse" id="libraryNavContent">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0" id="libraryNavList">
|
||||
<li class="nav-item" data-nav-fixed="true">
|
||||
<a class="nav-link" href="{{ url_for('library_view') }}">Medien</a>
|
||||
<a class="nav-link {% if current_path == url_for('library_view') %}nav-active{% endif %}" href="{{ url_for('library_view') }}">Medien</a>
|
||||
</li>
|
||||
{% if 'username' in session %}
|
||||
<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>
|
||||
<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 %}
|
||||
{% if 'username' in session and (session.get('admin', False) or is_admin) %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link nav-priority-link" href="{{ url_for('library_admin') }}">📖 Hochladen</a>
|
||||
<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">
|
||||
⋯ Mehr
|
||||
<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 %}
|
||||
<li><a class="dropdown-item" href="{{ url_for('my_borrowed_items') }}">Meine Medien</a></li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ url_for('notifications_view') }}">
|
||||
Benachrichtigungen
|
||||
{% if unread_notification_count and unread_notification_count > 0 %}
|
||||
<span class="notif-badge">{{ unread_notification_count }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
<li><a class="dropdown-item" href="{{ url_for('tutorial_page') }}">Tutorial</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
{% endif %}
|
||||
@@ -517,12 +617,28 @@
|
||||
</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">
|
||||
<button class="btn btn-secondary dropdown-toggle" type="button" id="libUserMenuDropdown" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<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">
|
||||
<li><a class="dropdown-item" href="{{ url_for('notifications_view') }}">Benachrichtigungen</a></li>
|
||||
<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>
|
||||
@@ -631,6 +747,57 @@
|
||||
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">
|
||||
@@ -658,6 +825,38 @@
|
||||
</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){
|
||||
@@ -691,8 +890,231 @@
|
||||
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) {
|
||||
|
||||
@@ -18,16 +18,15 @@
|
||||
|
||||
<div class="impressum-section mb-4">
|
||||
<h3 class="mb-3">Kontaktinformationen</h3>
|
||||
<p><strong>Name:</strong> . ..</p>
|
||||
<p><strong>Name:</strong>Invario UG</p>
|
||||
<p><strong>Adresse:</strong> Musterstraße 123<br>12345 Musterstadt<br>Deutschland</p>
|
||||
<p><strong>E-Mail:</strong> <a href="mailto:kontakt@example.com">kontakt@example.com</a></p>
|
||||
<p><strong>Telefon:</strong> +49 123 456789</p>
|
||||
<p><strong>E-Mail:</strong> <a href="mailto:kontakt@example.com">info@invario.eu</a></p>
|
||||
</div>
|
||||
|
||||
<div class="impressum-section mb-4">
|
||||
<h3 class="mb-3">Verantwortlich für den Inhalt</h3>
|
||||
<p>(nach § 55 Abs. 2 RStV)</p>
|
||||
<p>...<br>
|
||||
<p>Invario UG<br>
|
||||
Musterstraße 123<br>
|
||||
12345 Musterstadt<br>
|
||||
Deutschland</p>
|
||||
|
||||
@@ -321,11 +321,6 @@
|
||||
<li><strong>Einwilligung:</strong> Falls private Daten der Nutzer erfasst werden, muss eine explizite
|
||||
Einwilligung vorliegen (Art. 6 Abs. 1 lit. a DSGVO).</li>
|
||||
</ol>
|
||||
<div class="license-exception-notice" style="background-color:#f8d7da; border-color:#f5c2c7; border-left-color:#dc3545;">
|
||||
<h3 style="color:#842029;">⚠️ Hinweis</h3>
|
||||
<p>Dieses Dokument stellt <strong>keine Rechtsberatung</strong> dar. Bitte konsultieren Sie im Zweifelsfall
|
||||
einen Fachanwalt für Datenschutzrecht.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /#pane-legal -->
|
||||
|
||||
|
||||
@@ -25,6 +25,11 @@
|
||||
<div>
|
||||
<div style="font-weight:800; color:#0f172a;">{{ n.title }}</div>
|
||||
<div style="font-size:0.9rem; color:#475569; margin-top:2px;">{{ n.message }}</div>
|
||||
{% if n.type == 'damage_reported' %}
|
||||
<div style="margin-top:8px;">
|
||||
<a class="btn btn-sm btn-outline-danger" href="{{ url_for('admin_damaged_items') }}">Zu Defekte-Items Verwaltung</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div style="font-size:0.78rem; color:#64748b; margin-top:6px;">
|
||||
{% if n.created_at %}{{ n.created_at.strftime('%d.%m.%Y %H:%M') }}{% else %}-{% endif %}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user