Implement notification system with user and admin views, including unread count and reminder functionality for damaged items

This commit is contained in:
2026-04-13 20:29:50 +02:00
parent e74798f252
commit 86d8f19313
4 changed files with 624 additions and 1 deletions
+388 -1
View File
@@ -436,17 +436,183 @@ def _prepare_invoice_pdf_payload(invoice_data, borrow_doc=None, item_doc=None):
'amount_text': amount_text or '-',
}
def _create_notification(db, *, audience, notif_type, title, message, target_user=None, reference=None, unique_key=None, severity='info'):
"""Create a notification entry with optional deduplication via unique_key."""
notifications_col = db['notifications']
if unique_key:
existing = notifications_col.find_one({'UniqueKey': unique_key}, {'_id': 1})
if existing:
return False
now = datetime.datetime.now()
payload = {
'Audience': audience,
'Type': notif_type,
'Title': title,
'Message': message,
'TargetUser': target_user,
'Reference': reference or {},
'UniqueKey': unique_key,
'Severity': severity,
'ReadBy': [],
'CreatedAt': now,
'UpdatedAt': now,
}
notifications_col.insert_one(payload)
return True
def _get_notifications_for_user(db, username, is_admin=False, limit=150):
"""Fetch notifications visible to the current user, newest first."""
query = {
'$or': [
{'Audience': 'user', 'TargetUser': username},
]
}
if is_admin:
query['$or'].append({'Audience': 'admin'})
cursor = db['notifications'].find(query).sort('CreatedAt', -1).limit(limit)
return list(cursor)
def _get_unread_notification_count(db, username, is_admin=False):
"""Count unread notifications for navbar badge rendering."""
query = {
'$and': [
{
'$or': [
{'Audience': 'user', 'TargetUser': username},
]
},
{'ReadBy': {'$ne': username}},
]
}
if is_admin:
query['$and'][0]['$or'].append({'Audience': 'admin'})
return db['notifications'].count_documents(query)
def _build_reminder_message(item_name, start_dt=None, end_dt=None):
"""Build a concise reminder text for borrowed items."""
start_text = start_dt.strftime('%d.%m.%Y %H:%M') if isinstance(start_dt, datetime.datetime) else '-'
end_text = end_dt.strftime('%d.%m.%Y %H:%M') if isinstance(end_dt, datetime.datetime) else '-'
return (
f"Bitte denke an die Rueckgabe von '{item_name}'. "
f"Ausleihe seit: {start_text}. "
f"Geplantes Ende: {end_text}."
)
def create_return_reminders():
"""Create one-time reminders for day-1 and planned-end events."""
now = datetime.datetime.now()
one_day_ago = now - datetime.timedelta(days=1)
client = None
try:
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
db = client[MONGODB_DB]
ausleihungen_col = db['ausleihungen']
items_col = db['items']
candidates = list(ausleihungen_col.find(
{
'Status': {'$in': ['active', 'planned']},
'User': {'$exists': True, '$ne': ''},
'$or': [
{'Start': {'$lte': one_day_ago}},
{'End': {'$lte': now}},
]
},
{'User': 1, 'Item': 1, 'Status': 1, 'Start': 1, 'End': 1}
))
item_ids = []
for entry in candidates:
item_id = entry.get('Item')
if item_id:
item_ids.append(item_id)
item_docs = {}
for raw_id in item_ids:
try:
doc = items_col.find_one({'_id': ObjectId(raw_id)}, {'Name': 1, 'Code_4': 1})
if doc:
item_docs[raw_id] = doc
except Exception:
continue
for borrow_doc in candidates:
borrow_id = str(borrow_doc.get('_id'))
username = str(borrow_doc.get('User', '')).strip()
if not borrow_id or not username:
continue
item_id = str(borrow_doc.get('Item', '')).strip()
item_doc = item_docs.get(item_id, {})
item_name = item_doc.get('Name') or f'Item {item_id}'
start_dt = borrow_doc.get('Start')
end_dt = borrow_doc.get('End')
if isinstance(start_dt, datetime.datetime) and start_dt <= one_day_ago:
_create_notification(
db,
audience='user',
notif_type='return_day_1',
title='Erinnerung: Rueckgabe nach 1 Tag',
message=_build_reminder_message(item_name, start_dt=start_dt, end_dt=end_dt),
target_user=username,
reference={'borrow_id': borrow_id, 'item_id': item_id, 'event': 'day_1'},
unique_key=f'reminder:day1:{borrow_id}',
severity='warning',
)
if isinstance(end_dt, datetime.datetime) and end_dt <= now:
_create_notification(
db,
audience='user',
notif_type='return_after_end',
title='Erinnerung: Geplante Ausleihe ist beendet',
message=_build_reminder_message(item_name, start_dt=start_dt, end_dt=end_dt),
target_user=username,
reference={'borrow_id': borrow_id, 'item_id': item_id, 'event': 'after_end'},
unique_key=f'reminder:end:{borrow_id}',
severity='warning',
)
except Exception as exc:
app.logger.warning(f"Reminder creation failed: {exc}")
finally:
if client:
client.close()
@app.context_processor
def inject_version():
"""Inject global template variables."""
is_admin = False
asset_version = _get_asset_version()
unread_notification_count = 0
if 'username' in session:
try:
is_admin = us.check_admin(session['username'])
except Exception:
is_admin = False
client = None
try:
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
db = client[MONGODB_DB]
unread_notification_count = _get_unread_notification_count(db, session['username'], is_admin=is_admin)
except Exception:
unread_notification_count = 0
finally:
if client:
client.close()
current_module = _get_current_module(request.path)
return {
@@ -456,7 +622,8 @@ def inject_version():
'school_periods': SCHOOL_PERIODS,
'library_module_enabled': cfg.LIBRARY_MODULE_ENABLED,
'student_cards_module_enabled': cfg.STUDENT_CARDS_MODULE_ENABLED,
'is_admin': is_admin
'is_admin': is_admin,
'unread_notification_count': unread_notification_count,
}
# Create necessary directories at startup
@@ -639,6 +806,7 @@ scheduler = BackgroundScheduler()
if cfg.SCHEDULER_ENABLED:
scheduler.add_job(func=create_daily_backup, trigger="interval", hours=cfg.BACKUP_INTERVAL_HOURS)
scheduler.add_job(func=update_appointment_statuses, trigger="interval", minutes=cfg.SCHEDULER_INTERVAL_MIN)
scheduler.add_job(func=create_return_reminders, trigger="interval", minutes=cfg.SCHEDULER_INTERVAL_MIN)
scheduler.start()
# Register shutdown handler to stop scheduler when app is terminated
@@ -4675,6 +4843,19 @@ def report_damage(id):
updated_item = items_col.find_one({'_id': ObjectId(id)}, {'DamageReports': 1})
damage_count = len(updated_item.get('DamageReports', [])) if updated_item else 0
_create_notification(
db,
audience='admin',
notif_type='damage_reported',
title='Defekt gemeldet',
message=(
f"Fuer das Item '{item_doc.get('Name', id)}' wurde ein Defekt gemeldet. "
f"Meldung von {session.get('username', '-')}: {description}"
),
reference={'item_id': id, 'damage_count': damage_count},
severity='danger',
)
# Best-effort system log entry for auditability
try:
logs_collection = db['system_logs']
@@ -7572,6 +7753,212 @@ def my_borrowed_items():
user_is_admin=user_is_admin
)
@app.route('/notifications')
def notifications_view():
"""Notification center for users and admins."""
if 'username' not in session:
flash('Bitte melden Sie sich an, um Benachrichtigungen zu sehen.', 'error')
return redirect(url_for('login'))
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]
notifications = _get_notifications_for_user(db, username, is_admin=is_admin_user, limit=200)
user_notifications = []
admin_notifications = []
for notif in notifications:
is_read = username in (notif.get('ReadBy') or [])
row = {
'id': str(notif.get('_id')),
'title': notif.get('Title', 'Benachrichtigung'),
'message': notif.get('Message', ''),
'severity': notif.get('Severity', 'info'),
'type': notif.get('Type', ''),
'created_at': notif.get('CreatedAt'),
'is_read': is_read,
'reference': notif.get('Reference', {}) or {},
}
if notif.get('Audience') == 'admin':
admin_notifications.append(row)
else:
user_notifications.append(row)
return render_template(
'notifications.html',
user_notifications=user_notifications,
admin_notifications=admin_notifications,
is_admin_user=is_admin_user,
library_module_enabled=cfg.LIBRARY_MODULE_ENABLED,
student_cards_module_enabled=cfg.STUDENT_CARDS_MODULE_ENABLED,
)
except Exception as exc:
app.logger.error(f"Error loading notifications: {exc}")
flash('Fehler beim Laden der Benachrichtigungen.', 'error')
return redirect(url_for('home'))
finally:
if client:
client.close()
@app.route('/notifications/mark_read/<notification_id>', methods=['POST'])
def mark_notification_read(notification_id):
"""Mark a single notification as read for the current user."""
if 'username' not in session:
return redirect(url_for('login'))
username = session['username']
client = None
try:
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
db = client[MONGODB_DB]
db['notifications'].update_one(
{'_id': ObjectId(notification_id)},
{
'$addToSet': {'ReadBy': username},
'$set': {'UpdatedAt': datetime.datetime.now()}
}
)
except Exception as exc:
app.logger.warning(f"Could not mark notification as read {notification_id}: {exc}")
finally:
if client:
client.close()
return redirect(url_for('notifications_view'))
@app.route('/notifications/mark_all_read', methods=['POST'])
def mark_all_notifications_read():
"""Mark all visible notifications as read for the current user."""
if 'username' not in session:
return redirect(url_for('login'))
username = session['username']
is_admin_user = False
try:
is_admin_user = us.check_admin(username)
except Exception:
is_admin_user = False
query = {
'$or': [
{'Audience': 'user', 'TargetUser': username},
]
}
if is_admin_user:
query['$or'].append({'Audience': 'admin'})
client = None
try:
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
db = client[MONGODB_DB]
db['notifications'].update_many(
query,
{
'$addToSet': {'ReadBy': username},
'$set': {'UpdatedAt': datetime.datetime.now()}
}
)
except Exception as exc:
app.logger.warning(f"Could not mark all notifications as read for {username}: {exc}")
finally:
if client:
client.close()
return redirect(url_for('notifications_view'))
@app.route('/admin/damaged_items')
def admin_damaged_items():
"""Dedicated admin management window for damaged items."""
if 'username' not in session or not us.check_admin(session['username']):
flash('Administratorrechte erforderlich.', 'error')
return redirect(url_for('login'))
client = None
try:
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
db = client[MONGODB_DB]
items_col = db['items']
ausleihungen_col = db['ausleihungen']
items = list(items_col.find(
{
'Deleted': {'$ne': True},
'$or': [
{'HasDamage': True},
{'Condition': 'destroyed'},
{'DamageReports.0': {'$exists': True}},
]
},
{
'Name': 1,
'Code_4': 1,
'ItemType': 1,
'Author': 1,
'ISBN': 1,
'Condition': 1,
'DamageReports': 1,
'DamageRepairs': 1,
'Verfuegbar': 1,
'User': 1,
'LastUpdated': 1,
}
).sort('LastUpdated', -1))
damaged_rows = []
for item_doc in items:
item_id = str(item_doc.get('_id'))
active_borrow = ausleihungen_col.find_one(
{'Item': item_id, 'Status': {'$in': ['active', 'planned']}},
{'_id': 1, 'User': 1, 'Status': 1, 'End': 1}
)
reports = item_doc.get('DamageReports', []) or []
latest_report = reports[0] if reports else {}
damaged_rows.append({
'id': item_id,
'name': item_doc.get('Name', ''),
'code': item_doc.get('Code_4', ''),
'item_type': item_doc.get('ItemType', ''),
'author': item_doc.get('Author', ''),
'isbn': item_doc.get('ISBN', ''),
'condition': item_doc.get('Condition', ''),
'available': bool(item_doc.get('Verfuegbar', False)),
'borrow_user': item_doc.get('User', ''),
'damage_count': len(reports),
'damage_reports': reports,
'latest_damage_description': latest_report.get('description', ''),
'latest_damage_by': latest_report.get('reported_by', ''),
'latest_damage_at': latest_report.get('reported_at'),
'active_borrow': active_borrow,
'last_updated': item_doc.get('LastUpdated'),
})
return render_template(
'admin_damaged_items.html',
damaged_items=damaged_rows,
library_module_enabled=cfg.LIBRARY_MODULE_ENABLED,
student_cards_module_enabled=cfg.STUDENT_CARDS_MODULE_ENABLED,
)
except Exception as exc:
app.logger.error(f"Error loading damaged-items admin view: {exc}")
flash('Fehler beim Laden der Defekte-Items-Verwaltung.', 'error')
return redirect(url_for('home_admin'))
finally:
if client:
client.close()
@app.route('/favicon.ico')
def favicon():
"""
+121
View File
@@ -0,0 +1,121 @@
{% extends "base.html" %}
{% block title %}Defekte Items verwalten{% endblock %}
{% block content %}
<div class="container" style="max-width: 1250px; margin: 18px auto 32px;">
<div style="display:flex; justify-content:space-between; align-items:flex-start; gap:12px; flex-wrap:wrap; margin-bottom:16px;">
<div>
<h1 style="margin:0;">Defekte Items</h1>
<p style="margin:6px 0 0; color:#64748b;">Eigenes Verwaltungsfenster fuer gemeldete Defekte mit schneller Reparatur-Funktion.</p>
</div>
<div style="display:flex; gap:8px; flex-wrap:wrap;">
<a class="btn btn-outline-secondary" href="{{ url_for('admin_borrowings') }}">Ausleihen</a>
{% if library_module_enabled %}
<a class="btn btn-outline-secondary" href="{{ url_for('library_loans_admin') }}">Bibliothek</a>
{% endif %}
</div>
</div>
<div style="background:#fff; border:1px solid #e2e8f0; border-radius:14px; padding:14px; margin-bottom:12px;">
<input id="damage-search" type="text" placeholder="Suche nach Name, Code, Typ, Benutzer oder Schaden..." style="width:100%; padding:10px 12px; border:1px solid #d1d5db; border-radius:10px;">
</div>
<div style="background:#fff; border:1px solid #e2e8f0; border-radius:14px; padding:0; overflow:hidden;">
<table id="damage-table" style="width:100%; border-collapse:collapse;">
<thead>
<tr style="background:#f8fafc;">
<th style="text-align:left; padding:11px; border-bottom:1px solid #e5e7eb;">Item</th>
<th style="text-align:left; padding:11px; border-bottom:1px solid #e5e7eb;">Status</th>
<th style="text-align:left; padding:11px; border-bottom:1px solid #e5e7eb;">Letzte Meldung</th>
<th style="text-align:left; padding:11px; border-bottom:1px solid #e5e7eb;">Ausleihe</th>
<th style="text-align:left; padding:11px; border-bottom:1px solid #e5e7eb;">Aktion</th>
</tr>
</thead>
<tbody>
{% for item in damaged_items %}
<tr class="damage-row" data-search="{{ (item.name ~ ' ' ~ item.code ~ ' ' ~ item.item_type ~ ' ' ~ item.borrow_user ~ ' ' ~ item.latest_damage_description)|lower }}">
<td style="padding:11px; border-bottom:1px solid #eef2f7; vertical-align:top;">
<div style="font-weight:700;">{{ item.name }}</div>
<div style="font-size:0.86rem; color:#64748b;">{{ item.code or '-' }} | {{ item.item_type or '-' }}</div>
{% if item.author %}<div style="font-size:0.82rem; color:#64748b;">{{ item.author }}</div>{% endif %}
{% if item.isbn %}<div style="font-size:0.82rem; color:#64748b;">ISBN: {{ item.isbn }}</div>{% endif %}
</td>
<td style="padding:11px; border-bottom:1px solid #eef2f7; vertical-align:top;">
<div style="display:inline-block; padding:3px 9px; border-radius:999px; background:#fee2e2; color:#991b1b; font-size:0.75rem; font-weight:700;">Defekt</div>
<div style="font-size:0.84rem; color:#475569; margin-top:6px;">Meldungen: {{ item.damage_count }}</div>
<div style="font-size:0.84rem; color:#475569;">Condition: {{ item.condition or '-' }}</div>
<div style="font-size:0.84rem; color:#475569;">Verfuegbar: {{ 'Ja' if item.available else 'Nein' }}</div>
</td>
<td style="padding:11px; border-bottom:1px solid #eef2f7; vertical-align:top;">
<div style="font-size:0.9rem; color:#1f2937;">{{ item.latest_damage_description or '-' }}</div>
<div style="font-size:0.82rem; color:#64748b; margin-top:4px;">
Gemeldet von {{ item.latest_damage_by or '-' }}
{% if item.latest_damage_at %} am {{ item.latest_damage_at.strftime('%d.%m.%Y %H:%M') }}{% endif %}
</div>
</td>
<td style="padding:11px; border-bottom:1px solid #eef2f7; vertical-align:top;">
{% if item.active_borrow %}
<div style="display:inline-block; padding:3px 9px; border-radius:999px; background:#dbeafe; color:#1d4ed8; font-size:0.75rem; font-weight:700;">Aktiv/Geplant</div>
<div style="font-size:0.84rem; color:#475569; margin-top:6px;">User: {{ item.active_borrow.User or item.borrow_user or '-' }}</div>
{% if item.active_borrow.End %}
<div style="font-size:0.84rem; color:#475569;">Ende: {{ item.active_borrow.End.strftime('%d.%m.%Y %H:%M') }}</div>
{% endif %}
{% else %}
<div style="display:inline-block; padding:3px 9px; border-radius:999px; background:#dcfce7; color:#166534; font-size:0.75rem; font-weight:700;">Keine offene Ausleihe</div>
{% endif %}
</td>
<td style="padding:11px; border-bottom:1px solid #eef2f7; vertical-align:top;">
<button class="btn btn-success btn-sm" onclick="repairDamage('{{ item.id }}', this)">Als repariert markieren</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if not damaged_items %}
<div style="padding:24px; color:#64748b; text-align:center;">Keine defekten Items vorhanden.</div>
{% endif %}
</div>
</div>
<script>
(function() {
const searchInput = document.getElementById('damage-search');
if (searchInput) {
searchInput.addEventListener('input', function() {
const q = (searchInput.value || '').toLowerCase();
document.querySelectorAll('.damage-row').forEach(function(row) {
const text = row.getAttribute('data-search') || '';
row.style.display = text.includes(q) ? '' : 'none';
});
});
}
})();
function repairDamage(itemId, button) {
if (!itemId) return;
if (!confirm('Alle offenen Defektmeldungen fuer dieses Item als repariert markieren?')) {
return;
}
button.disabled = true;
fetch('/mark_damage_repaired/' + itemId, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({})
})
.then(function(res) { return res.json(); })
.then(function(data) {
if (!data.success) {
throw new Error(data.message || 'Reparatur fehlgeschlagen');
}
window.location.reload();
})
.catch(function(err) {
button.disabled = false;
alert(err.message || 'Fehler beim Markieren als repariert.');
});
}
</script>
{% endblock %}
+34
View File
@@ -410,6 +410,14 @@
<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 %}
@@ -418,6 +426,7 @@
<li><a class="dropdown-item" href="{{ url_for('manage_filters') }}">Filter verwalten</a></li>
<li><a class="dropdown-item" href="{{ url_for('manage_locations') }}">Orte verwalten</a></li>
<li><a class="dropdown-item" href="{{ url_for('admin_borrowings') }}">Ausleihen</a></li>
<li><a class="dropdown-item" href="{{ url_for('admin_damaged_items') }}">Defekte Items</a></li>
<li><a class="dropdown-item" href="{{ url_for('admin_audit_dashboard') }}">Audit Dashboard</a></li>
<li><a class="dropdown-item" href="{{ url_for('logs') }}">Logs</a></li>
<li><hr class="dropdown-divider"></li>
@@ -476,12 +485,21 @@
<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 %}
{% if 'username' in session and (session.get('admin', False) or is_admin) %}
<li><h6 class="dropdown-header">Bibliotheks-Verwaltung</h6></li>
<li><a class="dropdown-item" href="{{ url_for('library_loans_admin') }}">Ausleihen</a></li>
<li><a class="dropdown-item" href="{{ url_for('admin_damaged_items') }}">Defekte Items</a></li>
{% if student_cards_module_enabled %}
<li><a class="dropdown-item" href="{{ url_for('student_cards_admin') }}">Bibliotheksausweis</a></li>
{% endif %}
@@ -597,6 +615,22 @@
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;
}
</style>
<div id="cookie-banner" role="dialog" aria-live="polite" aria-label="Cookie-Hinweis">
<div class="cb-inner">
+81
View File
@@ -0,0 +1,81 @@
{% extends "base.html" %}
{% block title %}Benachrichtigungen{% endblock %}
{% block content %}
<div class="container" style="max-width: 1080px; margin: 18px auto 32px;">
<div style="display:flex; justify-content:space-between; gap:12px; align-items:center; margin-bottom:16px; flex-wrap:wrap;">
<div>
<h1 style="margin:0;">Benachrichtigungen</h1>
<p style="margin:6px 0 0; color:#64748b;">Rueckgabe-Erinnerungen und wichtige Hinweise fuer Benutzer und Verwaltung.</p>
</div>
<form method="post" action="{{ url_for('mark_all_notifications_read') }}">
<button class="btn btn-outline-secondary" type="submit">Alle als gelesen markieren</button>
</form>
</div>
<div style="display:grid; grid-template-columns:1fr; gap:14px;">
<section style="background:#fff; border:1px solid #e2e8f0; border-radius:14px; padding:16px;">
<h2 style="margin:0 0 12px; font-size:1.15rem;">Meine Benachrichtigungen</h2>
{% if user_notifications %}
<div style="display:grid; gap:10px;">
{% for n in user_notifications %}
<article style="border:1px solid #e5e7eb; border-left:5px solid {% if n.severity == 'danger' %}#dc2626{% elif n.severity == 'warning' %}#d97706{% else %}#2563eb{% endif %}; border-radius:10px; padding:12px 12px 10px; background:{% if n.is_read %}#f8fafc{% else %}#ffffff{% endif %};">
<div style="display:flex; justify-content:space-between; gap:10px; align-items:flex-start; flex-wrap:wrap;">
<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>
<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>
</div>
{% if not n.is_read %}
<form method="post" action="{{ url_for('mark_notification_read', notification_id=n.id) }}">
<button class="btn btn-sm btn-primary" type="submit">Als gelesen</button>
</form>
{% else %}
<span style="font-size:0.8rem; color:#16a34a; font-weight:700;">Gelesen</span>
{% endif %}
</div>
</article>
{% endfor %}
</div>
{% else %}
<p style="color:#64748b; margin:0;">Keine Benachrichtigungen vorhanden.</p>
{% endif %}
</section>
{% if is_admin_user %}
<section style="background:#fff; border:1px solid #e2e8f0; border-radius:14px; padding:16px;">
<h2 style="margin:0 0 12px; font-size:1.15rem;">Admin-Benachrichtigungen</h2>
{% if admin_notifications %}
<div style="display:grid; gap:10px;">
{% for n in admin_notifications %}
<article style="border:1px solid #e5e7eb; border-left:5px solid {% if n.severity == 'danger' %}#dc2626{% elif n.severity == 'warning' %}#d97706{% else %}#2563eb{% endif %}; border-radius:10px; padding:12px 12px 10px; background:{% if n.is_read %}#f8fafc{% else %}#ffffff{% endif %};">
<div style="display:flex; justify-content:space-between; gap:10px; align-items:flex-start; flex-wrap:wrap;">
<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>
<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>
</div>
{% if not n.is_read %}
<form method="post" action="{{ url_for('mark_notification_read', notification_id=n.id) }}">
<button class="btn btn-sm btn-primary" type="submit">Als gelesen</button>
</form>
{% else %}
<span style="font-size:0.8rem; color:#16a34a; font-weight:700;">Gelesen</span>
{% endif %}
</div>
</article>
{% endfor %}
</div>
{% else %}
<p style="color:#64748b; margin:0;">Keine Admin-Benachrichtigungen vorhanden.</p>
{% endif %}
</section>
{% endif %}
</div>
</div>
{% endblock %}