snapshot: preserve role-aware notification preferences and operational alerts

This commit is contained in:
Md Bayazid Bostame
2026-03-27 11:26:57 +01:00
parent fe3a8933fd
commit aa54f41731
25 changed files with 2958 additions and 633 deletions

View File

@@ -18,7 +18,11 @@
<div class="account-hero-copy">
<span class="account-kicker">{% trans "Konto" %}</span>
<h1>{% trans "Profil" %}</h1>
<p>{% trans "Ihre aktuelle Workdock-Kontoübersicht und wichtige Sicherheitsaktionen." %}</p>
<p>{% trans "Ihre aktuelle Kontoübersicht und wichtige Sicherheitsaktionen." %}</p>
<p class="account-hero-submeta">
{% trans "Letzte Anmeldung" %}:
<strong>{% if account_user.last_login %}{{ account_user.last_login|date:"d.m.Y H:i" }}{% else %}-{% endif %}</strong>
</p>
</div>
</section>
@@ -53,24 +57,36 @@
<h2>{{ account_user.get_full_name|default:account_user.username }}</h2>
<p>{{ account_user.email|default:account_user.username }}</p>
</div>
<div class="account-profile-meta">
<div>
<span>{% trans "Position" %}</span>
<strong>{{ account_user_profile.job_title|default:"-" }}</strong>
</div>
<div>
<span>{% trans "Abteilung" %}</span>
<strong>{{ account_user_profile.department|default:"-" }}</strong>
</div>
<div>
<span>{% trans "Letzte Anmeldung" %}</span>
<strong>{% if account_user.last_login %}{{ account_user.last_login|date:"d.m.Y H:i" }}{% else %}-{% endif %}</strong>
</div>
<div class="account-nav">
<button
class="account-nav-item is-active"
type="button"
data-account-tab="details"
aria-expanded="true"
>
{% trans "Kontodaten" %}
</button>
<button
class="account-nav-item"
type="button"
data-account-tab="security"
aria-expanded="false"
>
{% trans "Sicherheit & Aktionen" %}
</button>
<button
class="account-nav-item"
type="button"
data-account-tab="notifications"
aria-expanded="false"
>
{% trans "Benachrichtigungen" %}
</button>
</div>
</aside>
<div class="account-main">
<section class="account-panel">
<section class="account-panel" data-account-panel="details">
<div class="account-panel-head">
<div>
<h2>{% trans "Kontodaten" %}</h2>
@@ -156,13 +172,92 @@
</form>
</section>
<section class="account-panel">
<section class="account-panel" data-account-panel="notifications" hidden>
<div class="account-panel-head">
<div>
<h2>{% trans "Benachrichtigungen" %}</h2>
<p>{% trans "Legen Sie fest, welche Workflow-Ereignisse im Header als Benachrichtigung erscheinen sollen." %}</p>
</div>
<button
class="btn btn-secondary account-inline-edit-trigger"
type="button"
data-account-edit-toggle="notifications"
aria-expanded="{% if notifications_edit_open %}true{% else %}false{% endif %}"
>
{% trans "Bearbeiten" %}
</button>
</div>
<div class="account-inline-view{% if notifications_edit_open %} is-hidden{% endif %}" data-account-edit-view="notifications">
{% for group in notification_preference_groups %}
<section class="account-notification-group">
<h3>{{ group.label }}</h3>
<div class="account-notification-pref-grid">
{% for field in group.fields %}
<div class="account-notification-pref-item">
<span>{{ field.label }}</span>
<strong>{% if field.value %}{% trans "Aktiv" %}{% else %}{% trans "Aus" %}{% endif %}</strong>
</div>
{% endfor %}
</div>
</section>
{% endfor %}
</div>
<form
class="account-inline-form{% if not notifications_edit_open %} is-hidden{% endif %}"
method="post"
data-account-edit-form="notifications"
>
{% csrf_token %}
<input type="hidden" name="account_form" value="notification_preferences" />
{% for group in notification_preference_groups %}
<section class="account-notification-group">
<h3>{{ group.label }}</h3>
<div class="account-notification-pref-grid account-notification-pref-grid-edit">
{% for field in group.fields %}
<label class="account-notification-pref-toggle{% if field.errors %} has-error{% endif %}" for="{{ field.id_for_label }}">
<span class="account-notification-pref-copy">
<strong>{{ field.label }}</strong>
<small>
{% if field.name == 'onboarding_success' %}{% trans "Benachrichtigung nach erfolgreich abgeschlossenem Onboarding." %}
{% elif field.name == 'onboarding_failure' %}{% trans "Benachrichtigung wenn ein Onboarding fehlschlägt." %}
{% elif field.name == 'offboarding_success' %}{% trans "Benachrichtigung nach erfolgreich abgeschlossenem Offboarding." %}
{% elif field.name == 'offboarding_failure' %}{% trans "Benachrichtigung wenn ein Offboarding fehlschlägt." %}
{% elif field.name == 'backup_success' %}{% trans "Benachrichtigung bei erfolgreicher Backup-Erstellung oder Verifikation." %}
{% elif field.name == 'backup_failure' %}{% trans "Benachrichtigung wenn Backup-Aktionen fehlschlagen." %}
{% elif field.name == 'welcome_email_success' %}{% trans "Benachrichtigung wenn eine geplante Welcome E-Mail erfolgreich gesendet wurde." %}
{% elif field.name == 'welcome_email_failure' %}{% trans "Benachrichtigung wenn eine geplante Welcome E-Mail fehlschlägt." %}
{% elif field.name == 'trial_alerts' %}{% trans "Hinweise zu Trial-Ablauf, Ablaufdatum oder Deaktivierung." %}
{% else %}{% trans "Hinweise aus Systemtests wie SMTP oder Nextcloud." %}{% endif %}
</small>
</span>
<span class="account-toggle-control">
{{ field }}
<span class="account-toggle-slider" aria-hidden="true"></span>
</span>
</label>
{% if field.errors %}
<div class="account-form-error">{{ field.errors|join:", " }}</div>
{% endif %}
{% endfor %}
</div>
</section>
{% endfor %}
<div class="account-inline-actions">
<button class="btn btn-primary" type="submit">{% trans "Speichern" %}</button>
<button class="btn btn-secondary" type="button" data-account-edit-cancel="notifications">{% trans "Abbrechen" %}</button>
</div>
</form>
</section>
<section class="account-panel" data-account-panel="security" hidden>
<div class="account-panel-head">
<h2>{% trans "Sicherheit & Aktionen" %}</h2>
</div>
<div class="account-security-overview">
<div class="account-security-item">
<div class="account-security-item{% if account_user_profile.totp_enabled %} account-security-item-active{% endif %}">
<span>{% trans "TOTP" %}</span>
<strong>{% if account_user_profile.totp_enabled %}{% trans "Aktiv" %}{% else %}{% trans "Aus" %}{% endif %}</strong>
<p>
@@ -198,60 +293,97 @@
<h3>{% trans "Zwei-Faktor-Authentifizierung" %}</h3>
<p>{% trans "Aktivieren Sie TOTP mit einer Authenticator-App. Standardmäßig bleibt es ausgeschaltet." %}</p>
</div>
<span class="account-chip{% if not account_user_profile.totp_enabled %} account-chip-muted{% endif %}">
{% if account_user_profile.totp_enabled %}
{% trans "Aktiv" %}
{% else %}
{% trans "Aus" %}
{% endif %}
</span>
</div>
{% if account_user_profile.totp_enabled %}
<div class="account-detail-grid">
<div class="account-detail">
<span>{% trans "Status" %}</span>
<div class="account-totp-status-row">
<div class="account-totp-status-copy">
<strong>{% trans "TOTP ist aktiviert." %}</strong>
<p>{% trans "Bestätigt am" %}: {% if account_user_profile.totp_confirmed_at %}{{ account_user_profile.totp_confirmed_at|date:"d.m.Y H:i" }}{% else %}-{% endif %}</p>
</div>
<div class="account-detail">
<span>{% trans "Bestätigt am" %}</span>
<strong>{% if account_user_profile.totp_confirmed_at %}{{ account_user_profile.totp_confirmed_at|date:"d.m.Y H:i" }}{% else %}-{% endif %}</strong>
</div>
<button
class="btn btn-secondary"
type="button"
data-disable-toggle
aria-expanded="{% if totp_disable_form.errors %}true{% else %}false{% endif %}"
>
{% trans "TOTP deaktivieren" %}
</button>
</div>
<form class="account-totp-form" method="post">
<form class="account-totp-form{% if not totp_disable_form.errors %} is-hidden{% endif %}" method="post" data-disable-form>
{% csrf_token %}
<input type="hidden" name="account_form" value="totp_disable" />
<div class="account-form-grid">
{% for field in totp_disable_form %}
<div class="account-form-field{% if field.errors %} has-error{% endif %}">
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{{ field }}
{% if field.errors %}
<div class="account-form-error">{{ field.errors|join:", " }}</div>
<div class="account-form-field">
<label for="{{ totp_disable_form.current_password.id_for_label }}">{{ totp_disable_form.current_password.label }}</label>
{{ totp_disable_form.current_password }}
{% if totp_disable_form.current_password.errors %}
<div class="account-form-error">{{ totp_disable_form.current_password.errors|join:", " }}</div>
{% endif %}
</div>
{% endfor %}
{% if totp_disable_form.non_field_errors %}
<div class="account-form-field account-form-field-wide">
<div class="account-form-error">{{ totp_disable_form.non_field_errors|join:", " }}</div>
</div>
{% endif %}
</div>
<div class="account-inline-actions">
<button class="btn btn-secondary" type="submit">{% trans "TOTP deaktivieren" %}</button>
<button class="btn btn-secondary" type="submit">{% trans "Deaktivierung bestätigen" %}</button>
<button class="btn btn-secondary" type="button" data-disable-cancel>{% trans "Abbrechen" %}</button>
</div>
</form>
<form class="account-totp-form" method="post">
{% csrf_token %}
<input type="hidden" name="account_form" value="totp_regenerate_codes" />
<div class="account-form-grid">
{% for field in totp_regenerate_form %}
<div class="account-form-field{% if field.errors %} has-error{% endif %}">
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{{ field }}
{% if field.errors %}
<div class="account-form-error">{{ field.errors|join:", " }}</div>
<div class="account-totp-action-row">
<div class="account-totp-action-copy">
<strong>{% trans "Recovery-Codes neu erzeugen" %}</strong>
<p>{% trans "Neue Recovery-Codes sollten nur erzeugt werden, wenn die bisherigen Codes nicht mehr sicher sind." %}</p>
</div>
<button
class="btn btn-primary"
type="button"
data-regenerate-toggle
aria-expanded="{% if totp_regenerate_form.errors %}true{% else %}false{% endif %}"
>
{% trans "Recovery-Codes neu erzeugen" %}
</button>
</div>
<div class="account-form-grid{% if not totp_regenerate_form.errors %} is-hidden{% endif %}" data-regenerate-form>
<div class="account-form-field{% if totp_regenerate_form.verification_code.errors %} has-error{% endif %}">
<label for="{{ totp_regenerate_form.verification_code.id_for_label }}">{{ totp_regenerate_form.verification_code.label }}</label>
{{ totp_regenerate_form.verification_code }}
{% if totp_regenerate_form.verification_code.errors %}
<div class="account-form-error">{{ totp_regenerate_form.verification_code.errors|join:", " }}</div>
{% endif %}
</div>
{% endfor %}
<div class="account-form-field account-form-field-wide">
<button
class="btn btn-secondary account-recovery-toggle"
type="button"
data-recovery-toggle
aria-expanded="{% if totp_regenerate_form.recovery_code.value %}true{% else %}false{% endif %}"
aria-controls="account-regenerate-recovery-code"
>
{% trans "Stattdessen Recovery-Code verwenden" %}
</button>
</div>
<div class="account-form-field account-form-field-wide{% if not totp_regenerate_form.recovery_code.value %} is-hidden{% endif %}" id="account-regenerate-recovery-code" data-recovery-field>
<label for="{{ totp_regenerate_form.recovery_code.id_for_label }}">{{ totp_regenerate_form.recovery_code.label }}</label>
{{ totp_regenerate_form.recovery_code }}
{% if totp_regenerate_form.recovery_code.errors %}
<div class="account-form-error">{{ totp_regenerate_form.recovery_code.errors|join:", " }}</div>
{% endif %}
</div>
{% if totp_regenerate_form.non_field_errors %}
<div class="account-form-field account-form-field-wide">
<div class="account-form-error">{{ totp_regenerate_form.non_field_errors|join:", " }}</div>
</div>
{% endif %}
</div>
<div class="account-inline-actions">
<button class="btn btn-primary" type="submit">{% trans "Recovery-Codes neu erzeugen" %}</button>
<div class="account-inline-actions{% if not totp_regenerate_form.errors %} is-hidden{% endif %}" data-regenerate-actions>
<button class="btn btn-primary" type="submit">{% trans "Erzeugung bestätigen" %}</button>
<button class="btn btn-secondary" type="button" data-regenerate-cancel>{% trans "Abbrechen" %}</button>
</div>
</form>
{% else %}
@@ -309,8 +441,11 @@
<h3>{% trans "Recovery-Codes" %}</h3>
<p>{% trans "Diese Codes werden nur jetzt im Klartext angezeigt. Jeden Code können Sie genau einmal verwenden." %}</p>
</div>
<button class="btn btn-secondary" type="button" data-recovery-download>
{% trans "Herunterladen" %}
</button>
</div>
<div class="account-recovery-grid">
<div class="account-recovery-grid" data-recovery-codes>
{% for code in totp_recovery_codes %}
<div class="account-recovery-code">{{ code }}</div>
{% endfor %}
@@ -328,27 +463,68 @@
{% block extra_scripts %}
<script>
(function () {
var toggle = document.querySelector('[data-account-edit-toggle="details"]');
var cancel = document.querySelector('[data-account-edit-cancel="details"]');
var view = document.querySelector('[data-account-edit-view="details"]');
var form = document.querySelector('[data-account-edit-form="details"]');
var editConfigs = ['details', 'notifications'].map(function (key) {
return {
key: key,
toggle: document.querySelector('[data-account-edit-toggle="' + key + '"]'),
cancel: document.querySelector('[data-account-edit-cancel="' + key + '"]'),
view: document.querySelector('[data-account-edit-view="' + key + '"]'),
form: document.querySelector('[data-account-edit-form="' + key + '"]')
};
});
var secretToggle = document.querySelector('[data-secret-toggle]');
var secretBody = document.querySelector('[data-secret-body]');
var secretIcon = document.querySelector('[data-secret-toggle-icon]');
if (!toggle || !cancel || !view || !form) return;
function setMode(editing) {
view.classList.toggle('is-hidden', editing);
form.classList.toggle('is-hidden', !editing);
toggle.setAttribute('aria-expanded', editing ? 'true' : 'false');
}
toggle.addEventListener('click', function () {
setMode(true);
var recoveryToggle = document.querySelector('[data-recovery-toggle]');
var recoveryField = document.querySelector('[data-recovery-field]');
var disableToggle = document.querySelector('[data-disable-toggle]');
var disableForm = document.querySelector('[data-disable-form]');
var disableCancel = document.querySelector('[data-disable-cancel]');
var regenerateToggle = document.querySelector('[data-regenerate-toggle]');
var regenerateForm = document.querySelector('[data-regenerate-form]');
var regenerateActions = document.querySelector('[data-regenerate-actions]');
var regenerateCancel = document.querySelector('[data-regenerate-cancel]');
var recoveryDownload = document.querySelector('[data-recovery-download]');
var recoveryCodes = document.querySelector('[data-recovery-codes]');
var tabButtons = document.querySelectorAll('[data-account-tab]');
var panels = document.querySelectorAll('[data-account-panel]');
editConfigs.forEach(function (config) {
if (!config.toggle || !config.cancel || !config.view || !config.form) return;
function setMode(editing) {
config.view.classList.toggle('is-hidden', editing);
config.form.classList.toggle('is-hidden', !editing);
config.toggle.setAttribute('aria-expanded', editing ? 'true' : 'false');
}
config.toggle.addEventListener('click', function () {
setMode(true);
});
config.cancel.addEventListener('click', function () {
setMode(false);
});
});
cancel.addEventListener('click', function () {
setMode(false);
function setTab(tabKey) {
tabButtons.forEach(function (button) {
var isActive = button.getAttribute('data-account-tab') === tabKey;
button.classList.toggle('is-active', isActive);
button.setAttribute('aria-expanded', isActive ? 'true' : 'false');
});
panels.forEach(function (panel) {
var shouldShow = panel.getAttribute('data-account-panel') === tabKey;
panel.hidden = !shouldShow;
if (shouldShow) {
panel.classList.remove('is-entering');
window.requestAnimationFrame(function () {
panel.classList.add('is-entering');
});
}
});
}
tabButtons.forEach(function (button) {
button.addEventListener('click', function () {
setTab(button.getAttribute('data-account-tab'));
});
});
if (secretToggle && secretBody) {
@@ -361,6 +537,74 @@
}
});
}
if (recoveryToggle && recoveryField) {
recoveryToggle.addEventListener('click', function () {
var isOpen = recoveryToggle.getAttribute('aria-expanded') === 'true';
recoveryToggle.setAttribute('aria-expanded', isOpen ? 'false' : 'true');
recoveryField.classList.toggle('is-hidden', isOpen);
});
}
if (disableToggle && disableForm) {
disableToggle.addEventListener('click', function () {
var isOpen = disableToggle.getAttribute('aria-expanded') === 'true';
disableToggle.setAttribute('aria-expanded', isOpen ? 'false' : 'true');
disableForm.classList.toggle('is-hidden', isOpen);
});
}
if (disableCancel && disableToggle && disableForm) {
disableCancel.addEventListener('click', function () {
disableToggle.setAttribute('aria-expanded', 'false');
disableForm.classList.add('is-hidden');
});
}
if (regenerateToggle && regenerateForm && regenerateActions) {
regenerateToggle.addEventListener('click', function () {
var isOpen = regenerateToggle.getAttribute('aria-expanded') === 'true';
regenerateToggle.setAttribute('aria-expanded', isOpen ? 'false' : 'true');
regenerateForm.classList.toggle('is-hidden', isOpen);
regenerateActions.classList.toggle('is-hidden', isOpen);
});
}
if (regenerateCancel && regenerateToggle && regenerateForm && regenerateActions) {
regenerateCancel.addEventListener('click', function () {
regenerateToggle.setAttribute('aria-expanded', 'false');
regenerateForm.classList.add('is-hidden');
regenerateActions.classList.add('is-hidden');
});
}
if (recoveryDownload && recoveryCodes) {
recoveryDownload.addEventListener('click', function () {
var codes = Array.from(recoveryCodes.querySelectorAll('.account-recovery-code')).map(function (node) {
return node.textContent.trim();
}).filter(Boolean);
if (!codes.length) return;
var blob = new Blob([codes.join('\n') + '\n'], { type: 'text/plain;charset=utf-8' });
var url = URL.createObjectURL(blob);
var link = document.createElement('a');
link.href = url;
link.download = 'workdock-recovery-codes.txt';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
});
}
if ({{ account_edit_open|yesno:"true,false" }}) {
setTab('details');
} else if ({{ notifications_edit_open|yesno:"true,false" }}) {
setTab('notifications');
} else if ({{ totp_edit_open|yesno:"true,false" }} || {{ totp_disable_form.errors|yesno:"true,false" }} || {{ totp_regenerate_form.errors|yesno:"true,false" }}) {
setTab('security');
} else {
setTab('details');
}
}());
</script>
{% endblock %}

View File

@@ -14,10 +14,21 @@
{% block shell_body %}
<section class="login-shell-body">
<div class="login-card">
{% if login_step == 'totp' %}
<h1>{% trans "Zwei-Faktor-Prüfung" %}</h1>
<p>{% trans "Geben Sie Ihren TOTP-Code ein, um die Anmeldung abzuschließen." %}</p>
{% if login_totp_user %}
<div class="login-step-caption">
<strong>{{ login_totp_user.get_full_name|default:login_totp_user.username }}</strong>
<span>{{ login_totp_user.email|default:login_totp_user.username }}</span>
</div>
{% endif %}
{% else %}
<h1>{% trans "Anmeldung" %}</h1>
<p>{{ portal_login_subtitle }}</p>
{% endif %}
<form method="post" action="/accounts/login/">
<form method="post" action="{% if login_step == 'totp' %}/accounts/login/totp/{% else %}/accounts/login/{% endif %}">
{% csrf_token %}
{% if next %}
<input type="hidden" name="next" value="{{ next }}" />
@@ -25,23 +36,54 @@
{% if form.errors %}
<div class="app-alert app-alert-error" role="alert" aria-live="assertive">
<div class="app-alert-body">
{% if login_step == 'totp' %}
<strong>{% trans "Code ungültig" %}</strong><br />
<span>{% trans "Der eingegebene TOTP- oder Recovery-Code ist nicht korrekt. Bitte versuchen Sie es erneut." %}</span>
{% else %}
<strong>{% trans "Anmeldung fehlgeschlagen" %}</strong><br />
<span>{% trans "Anmeldedaten oder TOTP-Code sind nicht korrekt. Bitte versuchen Sie es erneut." %}</span>
<span>{% trans "Benutzername oder Passwort sind nicht korrekt. Bitte versuchen Sie es erneut." %}</span>
{% endif %}
</div>
</div>
{% endif %}
{% if login_step == 'totp' %}
<div class="field{% if form.otp_code.errors or form.errors %} has-error{% endif %}">
{{ form.otp_code.label_tag }}{{ form.otp_code }}
</div>
<div class="login-recovery-toggle-row">
<button
class="btn btn-secondary btn-inline-toggle"
type="button"
data-toggle-target="login-recovery-box"
aria-expanded="{% if show_recovery_code %}true{% else %}false{% endif %}"
>
{% trans "Recovery-Code verwenden" %}
</button>
</div>
<div class="login-recovery-box{% if not show_recovery_code %} is-hidden{% endif %}" id="login-recovery-box">
<div class="field{% if form.recovery_code.errors %} has-error{% endif %}">
{{ form.recovery_code.label_tag }}{{ form.recovery_code }}
<div class="mini">{% trans "Nutzen Sie stattdessen einen einmaligen Recovery-Code." %}</div>
</div>
</div>
<button class="btn btn-primary" type="submit">{% trans "Code prüfen" %}</button>
<a class="login-back-link" href="/accounts/login/">{% trans "Zurück zur Anmeldung" %}</a>
{% else %}
<div class="field{% if form.username.errors or form.errors %} has-error{% endif %}">{{ form.username.label_tag }}{{ form.username }}</div>
<div class="field{% if form.password.errors or form.errors %} has-error{% endif %}">{{ form.password.label_tag }}{{ form.password }}</div>
<div class="field{% if form.otp_code.errors %} has-error{% endif %}">
{{ form.otp_code.label_tag }}{{ form.otp_code }}
<div class="mini">{% trans "Nur erforderlich, wenn TOTP für Ihr Konto aktiviert ist." %}</div>
</div>
<div class="field{% if form.recovery_code.errors %} has-error{% endif %}">
{{ form.recovery_code.label_tag }}{{ form.recovery_code }}
<div class="mini">{% trans "Alternativ können Sie einen einmaligen Recovery-Code verwenden." %}</div>
</div>
<button class="btn btn-primary" type="submit">{% trans "Anmelden" %}</button>
{% endif %}
</form>
</div>
</section>
<script>
document.querySelectorAll('[data-toggle-target]').forEach((trigger) => {
trigger.addEventListener('click', () => {
const target = document.getElementById(trigger.getAttribute('data-toggle-target'));
if (!target) return;
const hidden = target.classList.toggle('is-hidden');
trigger.setAttribute('aria-expanded', hidden ? 'false' : 'true');
});
});
</script>
{% endblock %}

View File

@@ -20,6 +20,58 @@
<a class="btn btn-secondary" href="/">{% trans "Zur Startseite" %}</a>
{% endif %}
{% if request.user.is_authenticated %}
<details class="app-notification-menu">
<summary class="app-notification-trigger" aria-label="{% trans 'Benachrichtigungen' %}">
<span class="app-notification-bell" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" focusable="false" aria-hidden="true">
<path d="M12 4.25a4.25 4.25 0 0 0-4.25 4.25v2.04c0 .83-.24 1.65-.69 2.35l-1.2 1.88a1.5 1.5 0 0 0 1.27 2.31h9.74a1.5 1.5 0 0 0 1.27-2.31l-1.2-1.88a4.34 4.34 0 0 1-.69-2.35V8.5A4.25 4.25 0 0 0 12 4.25Z" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.75 18.25a2.25 2.25 0 0 0 4.5 0" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</span>
{% if header_unread_notification_count %}
<span class="app-notification-count">{{ header_unread_notification_count }}</span>
{% endif %}
</summary>
<div class="app-notification-panel">
<div class="app-notification-panel-head">
<strong>{% trans "Benachrichtigungen" %}</strong>
{% if header_unread_notification_count %}
<form method="post" action="{% url 'mark_all_notifications_read' %}">
{% csrf_token %}
<input type="hidden" name="next" value="{{ request.get_full_path }}" />
<button type="submit">{% trans "Alle als gelesen" %}</button>
</form>
{% endif %}
</div>
{% if header_notifications %}
<div class="app-notification-list">
{% for notification in header_notifications %}
<article class="app-notification-item app-notification-{{ notification.level }}{% if notification.is_unread %} is-unread{% endif %}">
<div class="app-notification-copy">
<strong>{{ notification.title }}</strong>
{% if notification.body %}<p>{{ notification.body|truncatechars:140 }}</p>{% endif %}
<span>{{ notification.created_at|date:"d.m.Y H:i" }}</span>
</div>
<div class="app-notification-actions">
{% if notification.link_url %}
<a href="{{ notification.link_url }}">{% trans "Öffnen" %}</a>
{% endif %}
{% if notification.is_unread %}
<form method="post" action="{% url 'mark_notification_read' notification.id %}">
{% csrf_token %}
<input type="hidden" name="next" value="{{ request.get_full_path }}" />
<button type="submit">{% trans "Gelesen" %}</button>
</form>
{% endif %}
</div>
</article>
{% endfor %}
</div>
{% else %}
<div class="app-notification-empty">{% trans "Keine Benachrichtigungen vorhanden." %}</div>
{% endif %}
</div>
</details>
<details class="app-user-menu">
<summary class="app-user-trigger">
<span class="app-user-avatar" aria-hidden="true">