Compare commits

..

5 Commits

5 changed files with 527 additions and 37 deletions
+69
View File
@@ -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
View File
@@ -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) {
+3 -4
View File
@@ -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>
-5
View File
@@ -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 -->
+5
View File
@@ -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>