snapshot: preserve totp account security baseline
This commit is contained in:
@@ -180,12 +180,100 @@
|
||||
<span>{% trans "Aktualisieren Sie Ihr Passwort direkt im Konto." %}</span>
|
||||
</a>
|
||||
|
||||
<div class="account-action-card{% if account_user_profile.totp_enabled %}{% else %} account-action-card-muted{% endif %}">
|
||||
<strong>{% trans "TOTP" %}</strong>
|
||||
<span>
|
||||
{% if account_user_profile.totp_enabled %}
|
||||
{% trans "Zweiter Faktor ist aktiv und wird bei der Anmeldung geprüft." %}
|
||||
{% else %}
|
||||
{% trans "Standardmäßig deaktiviert. Kann hier jederzeit aktiviert werden." %}
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="account-action-card account-action-card-muted">
|
||||
<strong>{% trans "Sitzung" %}</strong>
|
||||
<span>{% trans "Sie können sich jederzeit sicher vom aktuellen Gerät abmelden." %}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="account-totp-card">
|
||||
<div class="account-panel-head">
|
||||
<div>
|
||||
<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>
|
||||
<strong>{% trans "TOTP ist aktiviert." %}</strong>
|
||||
</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>
|
||||
</div>
|
||||
<form class="account-totp-form" method="post">
|
||||
{% 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>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="account-inline-actions">
|
||||
<button class="btn btn-secondary" type="submit">{% trans "TOTP deaktivieren" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="account-detail-grid">
|
||||
<div class="account-detail">
|
||||
<span>{% trans "Manueller Schlüssel" %}</span>
|
||||
<strong class="account-secret">{{ totp_pending_secret }}</strong>
|
||||
</div>
|
||||
<div class="account-detail">
|
||||
<span>{% trans "Setup-Link" %}</span>
|
||||
<strong class="account-secret">{{ totp_otpauth_uri }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mini">{% trans "Wenn Ihre App keinen QR-Code scannen kann, tragen Sie den Schlüssel oder den otpauth-Link manuell ein." %}</p>
|
||||
<form class="account-totp-form" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="account_form" value="totp_enable" />
|
||||
<div class="account-form-grid">
|
||||
{% for field in totp_enable_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>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="account-inline-actions">
|
||||
<button class="btn btn-primary" type="submit">{% trans "TOTP aktivieren" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="account-actions">
|
||||
<a class="btn btn-primary" href="{% url 'password_change' %}">{% trans "Passwort ändern" %}</a>
|
||||
<form method="post" action="{% url 'logout' %}">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends 'workflows/base_shell.html' %}
|
||||
{% load static i18n %}
|
||||
{% trans "Ungespeicherte Änderungen" as dirty_state_label %}
|
||||
{% trans "Sortierung" as sort_label %}
|
||||
|
||||
{% block title %}{% trans "App Registry" %}{% endblock %}
|
||||
|
||||
@@ -40,150 +41,184 @@
|
||||
<label for="app-registry-section">{% trans "Bereich" %}</label>
|
||||
<select id="app-registry-section">
|
||||
<option value="all">{% trans "Alle" %}</option>
|
||||
<option value="apps">{% trans "Apps" %}</option>
|
||||
<option value="platform_apps">{% trans "Platform Apps" %}</option>
|
||||
<option value="admin_apps">{% trans "Admin Apps" %}</option>
|
||||
<option value="app">{% trans "Apps" %}</option>
|
||||
<option value="platform">{% trans "Platform Apps" %}</option>
|
||||
<option value="admin">{% trans "Admin Apps" %}</option>
|
||||
</select>
|
||||
</div>
|
||||
</section>
|
||||
<div class="app-registry-cards">
|
||||
{% for row in rows %}
|
||||
<details class="app-registry-card{% if not row.config.is_enabled %} is-disabled{% endif %}" data-app-card
|
||||
data-key="{{ row.config.key|lower }}"
|
||||
data-title="{{ row.definition.title|lower }}"
|
||||
data-enabled="{% if row.config.is_enabled %}1{% else %}0{% endif %}"
|
||||
data-platform-only="{% if not row.config.visible_to_super_admin and not row.config.visible_to_admin and not row.config.visible_to_it_staff and not row.config.visible_to_staff %}1{% else %}0{% endif %}"
|
||||
data-section="{{ row.config.section }}">
|
||||
<summary class="app-registry-summary">
|
||||
<div class="app-registry-summary-main">
|
||||
<div class="app-registry-card-title-row">
|
||||
<h2>{{ row.definition.title }}</h2>
|
||||
{% if row.config.is_enabled %}
|
||||
<span class="badge sent">{% trans "Aktiv" %}</span>
|
||||
<div class="hint" id="app-registry-reorder-hint">{% trans "Für eine verlässliche Reihenfolge bitte ohne aktive Filter umsortieren." %}</div>
|
||||
<div class="app-registry-groups">
|
||||
{% for section_key, section_label in section_choices %}
|
||||
<section class="app-registry-group" data-app-group="{{ section_key }}">
|
||||
<div class="app-registry-group-head">
|
||||
<div>
|
||||
<h2>{{ section_label }}</h2>
|
||||
<p class="mini">
|
||||
{% if section_key == 'platform' %}
|
||||
{% trans "Produktweite Steuerung und nur für die Platform sichtbare Oberflächen." %}
|
||||
{% elif section_key == 'admin' %}
|
||||
{% trans "Administrative Apps für Kundenrollen mit erhöhter Verantwortung." %}
|
||||
{% else %}
|
||||
<span class="badge cancelled">{% trans "Deaktiviert" %}</span>
|
||||
{% trans "Operative Apps, die im täglichen Einsatz auf der Landing Page erscheinen." %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="mini">{{ row.config.key }}</div>
|
||||
<p class="app-registry-card-copy">{{ row.definition.description }}</p>
|
||||
<p class="mini">{% trans "Empfohlener Standardzugriff:" %} {{ row.default_visibility_summary }}</p>
|
||||
</p>
|
||||
</div>
|
||||
<div class="app-registry-summary-meta">
|
||||
<span class="badge scheduled">
|
||||
{% if row.config.section == 'platform_apps' %}
|
||||
{% trans "Platform Apps" %}
|
||||
{% elif row.config.section == 'admin_apps' %}
|
||||
{% trans "Admin Apps" %}
|
||||
{% else %}
|
||||
{% trans "Apps" %}
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="badge">{% trans "Sortierung" %}: {{ row.config.sort_order }}</span>
|
||||
{% if not row.config.visible_to_super_admin and not row.config.visible_to_admin and not row.config.visible_to_it_staff and not row.config.visible_to_staff %}
|
||||
<span class="badge paused">{% trans "Platform only" %}</span>
|
||||
{% elif row.config.visible_to_super_admin and row.config.visible_to_admin and row.config.visible_to_it_staff and row.config.visible_to_staff %}
|
||||
<span class="badge sent">{% trans "Alle Firmenrollen" %}</span>
|
||||
{% else %}
|
||||
<span class="badge scheduled">
|
||||
{% if row.config.visible_to_super_admin %}{% trans "Super Admin" %}{% endif %}
|
||||
{% if row.config.visible_to_admin %}{% if row.config.visible_to_super_admin %} + {% endif %}{% trans "Admin" %}{% endif %}
|
||||
{% if row.config.visible_to_it_staff %}{% if row.config.visible_to_super_admin or row.config.visible_to_admin %} + {% endif %}{% trans "IT Staff" %}{% endif %}
|
||||
{% if row.config.visible_to_staff %}{% if row.config.visible_to_super_admin or row.config.visible_to_admin or row.config.visible_to_it_staff %} + {% endif %}{% trans "Staff" %}{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</summary>
|
||||
|
||||
<div class="app-registry-card-grid">
|
||||
<section class="app-registry-panel">
|
||||
<h3>{% trans "Verfügbarkeit" %}</h3>
|
||||
<div class="check-row app-registry-checks">
|
||||
<label>
|
||||
<input type="checkbox" name="is_enabled__{{ row.config.key }}" {% if row.config.is_enabled %}checked{% endif %} />
|
||||
<span>{% trans "App aktiviert" %}</span>
|
||||
</label>
|
||||
</div>
|
||||
<p class="hint">{% trans "Deaktivierte Apps erscheinen nicht auf der Landing Page, selbst wenn Rollen sie sehen dürften." %}</p>
|
||||
</section>
|
||||
|
||||
<section class="app-registry-panel">
|
||||
<h3>{% trans "Sichtbarkeit nach Rolle" %}</h3>
|
||||
<div class="check-row app-registry-checks">
|
||||
<label>
|
||||
<input type="checkbox" name="visible_to_super_admin__{{ row.config.key }}" {% if row.config.visible_to_super_admin %}checked{% endif %} />
|
||||
<span>{% trans "Super Admin" %}</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" name="visible_to_admin__{{ row.config.key }}" {% if row.config.visible_to_admin %}checked{% endif %} />
|
||||
<span>{% trans "Admin" %}</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" name="visible_to_it_staff__{{ row.config.key }}" {% if row.config.visible_to_it_staff %}checked{% endif %} />
|
||||
<span>{% trans "IT Staff" %}</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" name="visible_to_staff__{{ row.config.key }}" {% if row.config.visible_to_staff %}checked{% endif %} />
|
||||
<span>{% trans "Staff" %}</span>
|
||||
</label>
|
||||
</div>
|
||||
<p class="hint">{% trans "Wenn keine Firmenrolle aktiv ist, bleibt die App nur für die Platform sichtbar." %}</p>
|
||||
</section>
|
||||
|
||||
<section class="app-registry-panel">
|
||||
<h3>{% trans "Platzierung" %}</h3>
|
||||
<div class="grid">
|
||||
<div class="field">
|
||||
<label for="section__{{ row.config.key }}">{% trans "Bereich" %}</label>
|
||||
<select id="section__{{ row.config.key }}" name="section__{{ row.config.key }}">
|
||||
{% for value, label in section_choices %}
|
||||
<option value="{{ value }}"{% if row.config.section == value %} selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="sort_order__{{ row.config.key }}">{% trans "Reihenfolge" %}</label>
|
||||
<input id="sort_order__{{ row.config.key }}" type="number" name="sort_order__{{ row.config.key }}" value="{{ row.config.sort_order }}" min="0" step="1" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="app-registry-panel app-registry-copy-panel">
|
||||
<h3>{% trans "Bezeichnungen & Texte" %}</h3>
|
||||
<div class="grid lang-pairs">
|
||||
<div class="lang-block">
|
||||
<h4>{% trans "Deutsch" %}</h4>
|
||||
<div class="field">
|
||||
<label for="title_override__{{ row.config.key }}">{% trans "Titel" %}</label>
|
||||
<input id="title_override__{{ row.config.key }}" type="text" name="title_override__{{ row.config.key }}" value="{{ row.config.title_override }}" placeholder="{{ row.definition.title }}" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="description_override__{{ row.config.key }}">{% trans "Beschreibung" %}</label>
|
||||
<textarea id="description_override__{{ row.config.key }}" name="description_override__{{ row.config.key }}" rows="3" placeholder="{{ row.definition.description }}">{{ row.config.description_override }}</textarea>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="action_label_override__{{ row.config.key }}">{% trans "Aktionslabel" %}</label>
|
||||
<input id="action_label_override__{{ row.config.key }}" type="text" name="action_label_override__{{ row.config.key }}" value="{{ row.config.action_label_override }}" placeholder="{{ row.definition.action_label }}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="lang-block">
|
||||
<h4>{% trans "English" %}</h4>
|
||||
<div class="field">
|
||||
<label for="title_override_en__{{ row.config.key }}">{% trans "Title" %}</label>
|
||||
<input id="title_override_en__{{ row.config.key }}" type="text" name="title_override_en__{{ row.config.key }}" value="{{ row.config.title_override_en }}" placeholder="{{ row.definition.title }}" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="description_override_en__{{ row.config.key }}">{% trans "Description" %}</label>
|
||||
<textarea id="description_override_en__{{ row.config.key }}" name="description_override_en__{{ row.config.key }}" rows="3" placeholder="{{ row.definition.description }}">{{ row.config.description_override_en }}</textarea>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="action_label_override_en__{{ row.config.key }}">{% trans "Action label" %}</label>
|
||||
<input id="action_label_override_en__{{ row.config.key }}" type="text" name="action_label_override_en__{{ row.config.key }}" value="{{ row.config.action_label_override_en }}" placeholder="{{ row.definition.action_label }}" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<span class="badge scheduled" data-app-group-count>{{ section_label }}</span>
|
||||
</div>
|
||||
</details>
|
||||
<div class="app-registry-group-body" data-app-group-body="{{ section_key }}">
|
||||
{% for row in rows %}
|
||||
{% if row.config.section == section_key %}
|
||||
<details class="app-registry-card{% if not row.config.is_enabled %} is-disabled{% endif %}" data-app-card draggable="true"
|
||||
data-key="{{ row.config.key|lower }}"
|
||||
data-title="{{ row.definition.title|lower }}"
|
||||
data-enabled="{% if row.config.is_enabled %}1{% else %}0{% endif %}"
|
||||
data-platform-only="{% if not row.config.visible_to_super_admin and not row.config.visible_to_admin and not row.config.visible_to_it_staff and not row.config.visible_to_staff %}1{% else %}0{% endif %}"
|
||||
data-section="{{ row.config.section }}">
|
||||
<summary class="app-registry-summary">
|
||||
<span class="app-registry-drag-handle" title="{% trans 'Ziehen zum Umordnen' %}" aria-hidden="true">⋮⋮</span>
|
||||
<div class="app-registry-summary-main">
|
||||
<div class="app-registry-card-title-row">
|
||||
<h2>{{ row.definition.title }}</h2>
|
||||
{% if row.config.is_enabled %}
|
||||
<span class="badge sent">{% trans "Aktiv" %}</span>
|
||||
{% else %}
|
||||
<span class="badge cancelled">{% trans "Deaktiviert" %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="mini">{{ row.config.key }}</div>
|
||||
<p class="app-registry-card-copy">{{ row.definition.description }}</p>
|
||||
<p class="mini">{% trans "Empfohlener Standardzugriff:" %} {{ row.default_visibility_summary }}</p>
|
||||
</div>
|
||||
<div class="app-registry-summary-meta">
|
||||
<span class="badge scheduled">
|
||||
{% if row.config.section == 'platform' %}
|
||||
{% trans "Platform Apps" %}
|
||||
{% elif row.config.section == 'admin' %}
|
||||
{% trans "Admin Apps" %}
|
||||
{% else %}
|
||||
{% trans "Apps" %}
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="badge" data-sort-badge>{% trans "Sortierung" %}: {{ row.config.sort_order }}</span>
|
||||
{% if not row.config.visible_to_super_admin and not row.config.visible_to_admin and not row.config.visible_to_it_staff and not row.config.visible_to_staff %}
|
||||
<span class="badge paused">{% trans "Platform only" %}</span>
|
||||
{% elif row.config.visible_to_super_admin and row.config.visible_to_admin and row.config.visible_to_it_staff and row.config.visible_to_staff %}
|
||||
<span class="badge sent">{% trans "Alle Firmenrollen" %}</span>
|
||||
{% else %}
|
||||
<span class="badge scheduled">
|
||||
{% if row.config.visible_to_super_admin %}{% trans "Super Admin" %}{% endif %}
|
||||
{% if row.config.visible_to_admin %}{% if row.config.visible_to_super_admin %} + {% endif %}{% trans "Admin" %}{% endif %}
|
||||
{% if row.config.visible_to_it_staff %}{% if row.config.visible_to_super_admin or row.config.visible_to_admin %} + {% endif %}{% trans "IT Staff" %}{% endif %}
|
||||
{% if row.config.visible_to_staff %}{% if row.config.visible_to_super_admin or row.config.visible_to_admin or row.config.visible_to_it_staff %} + {% endif %}{% trans "Staff" %}{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</summary>
|
||||
|
||||
<div class="app-registry-card-grid">
|
||||
<section class="app-registry-panel">
|
||||
<h3>{% trans "Verfügbarkeit" %}</h3>
|
||||
<div class="check-row app-registry-checks">
|
||||
<label>
|
||||
<input type="checkbox" name="is_enabled__{{ row.config.key }}" {% if row.config.is_enabled %}checked{% endif %} />
|
||||
<span>{% trans "App aktiviert" %}</span>
|
||||
</label>
|
||||
</div>
|
||||
<p class="hint">{% trans "Deaktivierte Apps erscheinen nicht auf der Landing Page, selbst wenn Rollen sie sehen dürften." %}</p>
|
||||
</section>
|
||||
|
||||
<section class="app-registry-panel">
|
||||
<h3>{% trans "Sichtbarkeit nach Rolle" %}</h3>
|
||||
<div class="check-row app-registry-checks">
|
||||
<label>
|
||||
<input type="checkbox" name="visible_to_super_admin__{{ row.config.key }}" {% if row.config.visible_to_super_admin %}checked{% endif %} />
|
||||
<span>{% trans "Super Admin" %}</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" name="visible_to_admin__{{ row.config.key }}" {% if row.config.visible_to_admin %}checked{% endif %} />
|
||||
<span>{% trans "Admin" %}</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" name="visible_to_it_staff__{{ row.config.key }}" {% if row.config.visible_to_it_staff %}checked{% endif %} />
|
||||
<span>{% trans "IT Staff" %}</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" name="visible_to_staff__{{ row.config.key }}" {% if row.config.visible_to_staff %}checked{% endif %} />
|
||||
<span>{% trans "Staff" %}</span>
|
||||
</label>
|
||||
</div>
|
||||
<p class="hint">{% trans "Wenn keine Firmenrolle aktiv ist, bleibt die App nur für die Platform sichtbar." %}</p>
|
||||
</section>
|
||||
|
||||
<section class="app-registry-panel">
|
||||
<h3>{% trans "Platzierung" %}</h3>
|
||||
<div class="grid">
|
||||
<div class="field">
|
||||
<label for="section__{{ row.config.key }}">{% trans "Bereich" %}</label>
|
||||
<select id="section__{{ row.config.key }}" name="section__{{ row.config.key }}">
|
||||
{% for value, label in section_choices %}
|
||||
<option value="{{ value }}"{% if row.config.section == value %} selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="sort_order__{{ row.config.key }}">{% trans "Reihenfolge" %}</label>
|
||||
<input
|
||||
id="sort_order__{{ row.config.key }}"
|
||||
type="number"
|
||||
name="sort_order__{{ row.config.key }}"
|
||||
value="{{ row.config.sort_order }}"
|
||||
min="0"
|
||||
step="1"
|
||||
data-sort-order-input
|
||||
/>
|
||||
<div class="hint">{% trans "Wird per Drag-and-drop und Bereichswechsel dynamisch neu nummeriert." %}</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="app-registry-panel app-registry-copy-panel">
|
||||
<h3>{% trans "Bezeichnungen & Texte" %}</h3>
|
||||
<div class="grid lang-pairs">
|
||||
<div class="lang-block">
|
||||
<h4>{% trans "Deutsch" %}</h4>
|
||||
<div class="field">
|
||||
<label for="title_override__{{ row.config.key }}">{% trans "Titel" %}</label>
|
||||
<input id="title_override__{{ row.config.key }}" type="text" name="title_override__{{ row.config.key }}" value="{{ row.config.title_override }}" placeholder="{{ row.definition.title }}" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="description_override__{{ row.config.key }}">{% trans "Beschreibung" %}</label>
|
||||
<textarea id="description_override__{{ row.config.key }}" name="description_override__{{ row.config.key }}" rows="3" placeholder="{{ row.definition.description }}">{{ row.config.description_override }}</textarea>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="action_label_override__{{ row.config.key }}">{% trans "Aktionslabel" %}</label>
|
||||
<input id="action_label_override__{{ row.config.key }}" type="text" name="action_label_override__{{ row.config.key }}" value="{{ row.config.action_label_override }}" placeholder="{{ row.definition.action_label }}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="lang-block">
|
||||
<h4>{% trans "English" %}</h4>
|
||||
<div class="field">
|
||||
<label for="title_override_en__{{ row.config.key }}">{% trans "Title" %}</label>
|
||||
<input id="title_override_en__{{ row.config.key }}" type="text" name="title_override_en__{{ row.config.key }}" value="{{ row.config.title_override_en }}" placeholder="{{ row.definition.title }}" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="description_override_en__{{ row.config.key }}">{% trans "Description" %}</label>
|
||||
<textarea id="description_override_en__{{ row.config.key }}" name="description_override_en__{{ row.config.key }}" rows="3" placeholder="{{ row.definition.description }}">{{ row.config.description_override_en }}</textarea>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="action_label_override_en__{{ row.config.key }}">{% trans "Action label" %}</label>
|
||||
<input id="action_label_override_en__{{ row.config.key }}" type="text" name="action_label_override_en__{{ row.config.key }}" value="{{ row.config.action_label_override_en }}" placeholder="{{ row.definition.action_label }}" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</details>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="app-registry-savebar">
|
||||
@@ -204,13 +239,46 @@
|
||||
const stateSelect = document.getElementById('app-registry-state');
|
||||
const sectionSelect = document.getElementById('app-registry-section');
|
||||
const cards = Array.from(document.querySelectorAll('[data-app-card]'));
|
||||
const groups = Array.from(document.querySelectorAll('[data-app-group]'));
|
||||
const groupBodies = Array.from(document.querySelectorAll('[data-app-group-body]'));
|
||||
const form = document.querySelector('form[action$="app-registry/save/"], form[action*="save_portal_app_registry"]') || document.querySelector('.stack-form');
|
||||
const dirtyState = document.getElementById('app-registry-dirty-state');
|
||||
const reorderHint = document.getElementById('app-registry-reorder-hint');
|
||||
const SECTION_ORDER = ['app', 'platform', 'admin'];
|
||||
let draggedCard = null;
|
||||
|
||||
function cardsInDomOrder() {
|
||||
return groupBodies.flatMap((body) => Array.from(body.querySelectorAll('[data-app-card]')));
|
||||
}
|
||||
|
||||
function syncGroupContainers() {
|
||||
cards.forEach((card) => {
|
||||
const sectionField = card.querySelector('select[name^="section__"]');
|
||||
const targetSection = sectionField ? sectionField.value : card.dataset.section;
|
||||
const targetBody = document.querySelector(`[data-app-group-body="${targetSection}"]`);
|
||||
if (targetBody && card.parentElement !== targetBody) {
|
||||
targetBody.appendChild(card);
|
||||
}
|
||||
card.dataset.section = targetSection;
|
||||
});
|
||||
}
|
||||
|
||||
function updateGroupVisibility() {
|
||||
groups.forEach((group) => {
|
||||
const visibleCards = group.querySelectorAll('[data-app-card]:not([hidden])').length;
|
||||
group.hidden = visibleCards === 0;
|
||||
const countBadge = group.querySelector('[data-app-group-count]');
|
||||
if (countBadge) {
|
||||
countBadge.textContent = String(visibleCards);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function applyFilters() {
|
||||
const query = (searchInput?.value || '').trim().toLowerCase();
|
||||
const state = stateSelect?.value || 'all';
|
||||
const section = sectionSelect?.value || 'all';
|
||||
const filterActive = Boolean(query) || state !== 'all' || section !== 'all';
|
||||
|
||||
cards.forEach((card) => {
|
||||
const matchesQuery = !query || card.dataset.key.includes(query) || card.dataset.title.includes(query);
|
||||
@@ -221,20 +289,116 @@
|
||||
(state === 'platform_only' && card.dataset.platformOnly === '1');
|
||||
const matchesSection = section === 'all' || card.dataset.section === section;
|
||||
card.hidden = !(matchesQuery && matchesState && matchesSection);
|
||||
card.draggable = !filterActive;
|
||||
card.classList.toggle('drag-disabled', filterActive);
|
||||
});
|
||||
|
||||
if (reorderHint) {
|
||||
reorderHint.hidden = !filterActive;
|
||||
}
|
||||
updateGroupVisibility();
|
||||
}
|
||||
|
||||
function normalizeSortOrders() {
|
||||
const grouped = new Map();
|
||||
const orderedCards = cardsInDomOrder();
|
||||
SECTION_ORDER.forEach((key) => grouped.set(key, []));
|
||||
|
||||
orderedCards.forEach((card) => {
|
||||
const sectionField = card.querySelector('select[name^="section__"]');
|
||||
const sortInput = card.querySelector('[data-sort-order-input]');
|
||||
const sectionKey = sectionField ? sectionField.value : card.dataset.section;
|
||||
const sortValue = sortInput ? parseInt(sortInput.value || '0', 10) : 0;
|
||||
if (!grouped.has(sectionKey)) grouped.set(sectionKey, []);
|
||||
grouped.get(sectionKey).push({
|
||||
card,
|
||||
sortInput,
|
||||
sortValue: Number.isNaN(sortValue) ? 0 : sortValue,
|
||||
title: (card.dataset.title || '').toLowerCase(),
|
||||
});
|
||||
});
|
||||
|
||||
grouped.forEach((items, sectionKey) => {
|
||||
items
|
||||
.sort((a, b) => {
|
||||
const domOrder = orderedCards.indexOf(a.card) - orderedCards.indexOf(b.card);
|
||||
return (a.sortValue - b.sortValue) || domOrder || a.title.localeCompare(b.title);
|
||||
})
|
||||
.forEach((item, index) => {
|
||||
if (item.sortInput) item.sortInput.value = index;
|
||||
item.card.dataset.section = sectionKey;
|
||||
const badge = item.card.querySelector('[data-sort-badge]');
|
||||
if (badge) badge.textContent = `{{ sort_label|escapejs }}: ${index}`;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function markDirty() {
|
||||
if (!dirtyState) return;
|
||||
dirtyState.textContent = "{{ dirty_state_label|escapejs }}";
|
||||
function closestCardAfterPointer(container, clientY) {
|
||||
const siblingCards = Array.from(container.querySelectorAll('[data-app-card]:not(.is-dragging)'));
|
||||
return siblingCards.find((card) => {
|
||||
const rect = card.getBoundingClientRect();
|
||||
return clientY < rect.top + rect.height / 2;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
searchInput?.addEventListener('input', applyFilters);
|
||||
stateSelect?.addEventListener('change', applyFilters);
|
||||
sectionSelect?.addEventListener('change', applyFilters);
|
||||
form?.addEventListener('input', markDirty);
|
||||
form?.addEventListener('change', markDirty);
|
||||
function markDirty() {
|
||||
if (dirtyState) {
|
||||
dirtyState.textContent = '{{ dirty_state_label|escapejs }}';
|
||||
}
|
||||
}
|
||||
|
||||
if (form) {
|
||||
form.addEventListener('change', (event) => {
|
||||
if (event.target.matches('select[name^="section__"], [data-sort-order-input]')) {
|
||||
syncGroupContainers();
|
||||
normalizeSortOrders();
|
||||
}
|
||||
markDirty();
|
||||
applyFilters();
|
||||
});
|
||||
|
||||
form.addEventListener('input', () => {
|
||||
markDirty();
|
||||
});
|
||||
}
|
||||
|
||||
cards.forEach((card) => {
|
||||
card.addEventListener('dragstart', () => {
|
||||
if (card.classList.contains('drag-disabled')) return;
|
||||
draggedCard = card;
|
||||
card.classList.add('is-dragging');
|
||||
});
|
||||
|
||||
card.addEventListener('dragend', () => {
|
||||
card.classList.remove('is-dragging');
|
||||
draggedCard = null;
|
||||
normalizeSortOrders();
|
||||
markDirty();
|
||||
applyFilters();
|
||||
});
|
||||
});
|
||||
|
||||
groupBodies.forEach((body) => {
|
||||
body.addEventListener('dragover', (event) => {
|
||||
if (!draggedCard || draggedCard.classList.contains('drag-disabled')) return;
|
||||
event.preventDefault();
|
||||
const nextCard = closestCardAfterPointer(body, event.clientY);
|
||||
if (nextCard) {
|
||||
body.insertBefore(draggedCard, nextCard);
|
||||
} else {
|
||||
body.appendChild(draggedCard);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
[searchInput, stateSelect, sectionSelect].forEach((control) => {
|
||||
if (!control) return;
|
||||
control.addEventListener('input', applyFilters);
|
||||
control.addEventListener('change', applyFilters);
|
||||
});
|
||||
|
||||
syncGroupContainers();
|
||||
normalizeSortOrders();
|
||||
applyFilters();
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -26,12 +26,16 @@
|
||||
<div class="app-alert app-alert-error" role="alert" aria-live="assertive">
|
||||
<div class="app-alert-body">
|
||||
<strong>{% trans "Anmeldung fehlgeschlagen" %}</strong><br />
|
||||
<span>{% trans "Benutzername oder Passwort sind nicht korrekt. Bitte versuchen Sie es erneut." %}</span>
|
||||
<span>{% trans "Anmeldedaten oder TOTP-Code sind nicht korrekt. Bitte versuchen Sie es erneut." %}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="field{% if form.errors %} has-error{% endif %}">{{ form.username.label_tag }}{{ form.username }}</div>
|
||||
<div class="field{% if form.errors %} has-error{% endif %}">{{ form.password.label_tag }}{{ form.password }}</div>
|
||||
<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>
|
||||
<button class="btn btn-primary" type="submit">{% trans "Anmelden" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -15,71 +15,66 @@
|
||||
{% include 'workflows/includes/messages.html' %}
|
||||
|
||||
<section class="card">
|
||||
<form method="post" action="{% url 'save_portal_branding' %}" enctype="multipart/form-data" class="stack-form">
|
||||
{% csrf_token %}
|
||||
<div class="branding-sections">
|
||||
<section class="branding-block">
|
||||
<div class="branding-block-head">
|
||||
<h2>{% trans "Identität" %}</h2>
|
||||
<p>{% trans "Titel, Firmenname und zentrale Spracheinstellungen." %}</p>
|
||||
<div class="branding-sections">
|
||||
{% for section in branding_sections %}
|
||||
<section class="branding-block branding-inline-block" data-branding-section="{{ section.key }}">
|
||||
<div class="branding-block-head branding-inline-head">
|
||||
<div>
|
||||
<h2>{{ section.title }}</h2>
|
||||
<p>{{ section.subtitle }}</p>
|
||||
</div>
|
||||
<div class="grid two">
|
||||
<div class="field">
|
||||
<label for="{{ form.portal_title.id_for_label }}">{{ form.portal_title.label }}</label>
|
||||
{{ form.portal_title }}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="{{ form.company_name.id_for_label }}">{{ form.company_name.label }}</label>
|
||||
{{ form.company_name }}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="{{ form.company_domain.id_for_label }}">{{ form.company_domain.label }}</label>
|
||||
{{ form.company_domain }}
|
||||
<div class="hint">{% trans "Wird für E-Mail-Vorschläge und Domain-bezogene Standardtexte verwendet, z. B. tub.co." %}</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="{{ form.default_language.id_for_label }}">{{ form.default_language.label }}</label>
|
||||
{{ form.default_language }}
|
||||
</div>
|
||||
<div class="field field-full">
|
||||
<label for="{{ form.login_subtitle.id_for_label }}">{{ form.login_subtitle.label }}</label>
|
||||
{{ form.login_subtitle }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<button
|
||||
class="btn btn-secondary branding-inline-trigger"
|
||||
type="button"
|
||||
data-branding-edit-toggle="{{ section.key }}"
|
||||
aria-expanded="{% if editing_branding_section == section.key %}true{% else %}false{% endif %}"
|
||||
>
|
||||
{% trans "Bearbeiten" %}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<section class="branding-block">
|
||||
<div class="branding-block-head">
|
||||
<h2>{% trans "Farben & Erscheinungsbild" %}</h2>
|
||||
<p>{% trans "Zentrale visuelle Markenwerte und Browser-Icon." %}</p>
|
||||
<div class="branding-inline-view{% if editing_branding_section == section.key %} is-hidden{% endif %}" data-branding-edit-view="{{ section.key }}">
|
||||
{% if section.key == 'legal' %}
|
||||
<div class="grid two lang-pairs">
|
||||
<div class="lang-block">
|
||||
<h3>{% trans "Deutsch" %}</h3>
|
||||
{% for row in section.rows|slice:":2" %}
|
||||
<div class="field{% if row.is_full %} field-full{% endif %}">
|
||||
<label>{{ row.label }}</label>
|
||||
<div class="branding-inline-value">{{ row.value|default:"-" }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="lang-block">
|
||||
<h3>{% trans "English" %}</h3>
|
||||
{% for row in section.rows|slice:"2:" %}
|
||||
<div class="field{% if row.is_full %} field-full{% endif %}">
|
||||
<label>{{ row.label }}</label>
|
||||
<div class="branding-inline-value">{{ row.value|default:"-" }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="grid two">
|
||||
<div class="field">
|
||||
<label for="{{ form.primary_color.id_for_label }}">{{ form.primary_color.label }}</label>
|
||||
{{ form.primary_color }}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="{{ form.secondary_color.id_for_label }}">{{ form.secondary_color.label }}</label>
|
||||
{{ form.secondary_color }}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="{{ form.logo_image.id_for_label }}">{{ form.logo_image.label }}</label>
|
||||
{{ form.logo_image }}
|
||||
<div class="hint">{% trans "Erlaubte Formate: SVG, PNG, JPG, JPEG, WEBP. Maximal 5 MB." %}</div>
|
||||
{% for error in form.logo_image.errors %}<div class="hint">{{ error }}</div>{% endfor %}
|
||||
{% if branding.logo_image %}
|
||||
<div class="hint">{% trans "Aktuelles Logo:" %} <a href="{{ branding.logo_image.url }}" target="_blank" rel="noopener">{% trans "öffnen" %}</a></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="{{ form.favicon_image.id_for_label }}">{{ form.favicon_image.label }}</label>
|
||||
{{ form.favicon_image }}
|
||||
<div class="hint">{% trans "Erlaubte Formate: ICO, PNG, SVG, WEBP. Maximal 2 MB." %}</div>
|
||||
{% for error in form.favicon_image.errors %}<div class="hint">{{ error }}</div>{% endfor %}
|
||||
{% if branding.favicon_image %}
|
||||
<div class="hint">{% trans "Aktuelles Favicon:" %} <a href="{{ branding.favicon_image.url }}" target="_blank" rel="noopener">{% trans "öffnen" %}</a></div>
|
||||
{% endif %}
|
||||
{% for row in section.rows %}
|
||||
<div class="field{% if row.is_full %} field-full{% endif %}">
|
||||
<label>{{ row.label }}</label>
|
||||
<div class="branding-inline-value">
|
||||
{% if row.is_file %}
|
||||
{% if row.value %}
|
||||
<a href="{{ row.value.url }}" target="_blank" rel="noopener">{% trans "öffnen" %}</a>
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ row.value|default:"-" }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if row.hint %}<div class="hint">{{ row.hint }}</div>{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if section.key == 'appearance' %}
|
||||
<div class="field field-full">
|
||||
<div class="branding-preview" id="branding-preview" data-default-logo="{{ portal_logo_url }}">
|
||||
<div class="branding-preview-shell">
|
||||
@@ -101,78 +96,121 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<section class="branding-block">
|
||||
<div class="branding-block-head">
|
||||
<h2>{% trans "Kommunikation" %}</h2>
|
||||
<p>{% trans "Absender, Support und PDF-Branding für ausgehende Kommunikation." %}</p>
|
||||
</div>
|
||||
<div class="grid two">
|
||||
<div class="field">
|
||||
<label for="{{ form.support_email.id_for_label }}">{{ form.support_email.label }}</label>
|
||||
{{ form.support_email }}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="{{ form.sender_display_name.id_for_label }}">{{ form.sender_display_name.label }}</label>
|
||||
{{ form.sender_display_name }}
|
||||
<div class="hint">{% trans "Wird für ausgehende System-E-Mails als Anzeigename verwendet." %}</div>
|
||||
</div>
|
||||
<div class="field field-full">
|
||||
<label for="{{ form.pdf_letterhead.id_for_label }}">{{ form.pdf_letterhead.label }}</label>
|
||||
{{ form.pdf_letterhead }}
|
||||
<div class="hint">{% trans "Erlaubtes Format: PDF. Maximal 10 MB." %}</div>
|
||||
{% for error in form.pdf_letterhead.errors %}<div class="hint">{{ error }}</div>{% endfor %}
|
||||
{% if branding.pdf_letterhead %}
|
||||
<div class="hint">{% trans "Aktueller Briefkopf:" %} <a href="{{ branding.pdf_letterhead.url }}" target="_blank" rel="noopener">{% trans "öffnen" %}</a></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<form method="post" action="{% url 'save_portal_branding' %}" enctype="multipart/form-data" class="branding-inline-form{% if editing_branding_section != section.key %} is-hidden{% endif %}" data-branding-edit-form="{{ section.key }}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="section_key" value="{{ section.key }}" />
|
||||
|
||||
<section class="branding-block">
|
||||
<div class="branding-block-head">
|
||||
<h2>{% trans "Footer & Rechtliches" %}</h2>
|
||||
<p>{% trans "Gemeinsame Footer-Texte und rechtliche Hinweise für die Shell." %}</p>
|
||||
</div>
|
||||
{% if section.key == 'legal' %}
|
||||
<div class="grid two lang-pairs">
|
||||
<div class="lang-block">
|
||||
<h3>{% trans "Deutsch" %}</h3>
|
||||
<div class="field">
|
||||
<label for="{{ form.footer_text.id_for_label }}">{{ form.footer_text.label }}</label>
|
||||
{{ form.footer_text }}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="{{ form.legal_notice.id_for_label }}">{{ form.legal_notice.label }}</label>
|
||||
{{ form.legal_notice }}
|
||||
{% for row in section.rows|slice:":2" %}
|
||||
<div class="field{% if row.is_full %} field-full{% endif %}{% if row.bound_field.errors %} has-error{% endif %}">
|
||||
<label for="{{ row.bound_field.id_for_label }}">{{ row.label }}</label>
|
||||
{{ row.bound_field }}
|
||||
{% if row.bound_field.errors %}<div class="branding-inline-error">{{ row.bound_field.errors|join:", " }}</div>{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="lang-block">
|
||||
<h3>{% trans "English" %}</h3>
|
||||
<div class="field">
|
||||
<label for="{{ form.footer_text_en.id_for_label }}">{{ form.footer_text_en.label }}</label>
|
||||
{{ form.footer_text_en }}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="{{ form.legal_notice_en.id_for_label }}">{{ form.legal_notice_en.label }}</label>
|
||||
{{ form.legal_notice_en }}
|
||||
{% for row in section.rows|slice:"2:" %}
|
||||
<div class="field{% if row.is_full %} field-full{% endif %}{% if row.bound_field.errors %} has-error{% endif %}">
|
||||
<label for="{{ row.bound_field.id_for_label }}">{{ row.label }}</label>
|
||||
{{ row.bound_field }}
|
||||
{% if row.bound_field.errors %}<div class="branding-inline-error">{{ row.bound_field.errors|join:", " }}</div>{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="grid two">
|
||||
{% for row in section.rows %}
|
||||
<div class="field{% if row.is_full %} field-full{% endif %}{% if row.bound_field.errors %} has-error{% endif %}">
|
||||
<label for="{{ row.bound_field.id_for_label }}">{{ row.label }}</label>
|
||||
{{ row.bound_field }}
|
||||
{% if row.hint %}<div class="hint">{{ row.hint }}</div>{% endif %}
|
||||
{% if row.is_file and row.value %}
|
||||
<div class="hint">
|
||||
{% if row.name == 'logo_image' %}{% trans "Aktuelles Logo:" %}
|
||||
{% elif row.name == 'favicon_image' %}{% trans "Aktuelles Favicon:" %}
|
||||
{% elif row.name == 'pdf_letterhead' %}{% trans "Aktueller Briefkopf:" %}
|
||||
{% endif %}
|
||||
<a href="{{ row.value.url }}" target="_blank" rel="noopener">{% trans "öffnen" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if row.bound_field.errors %}<div class="branding-inline-error">{{ row.bound_field.errors|join:", " }}</div>{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if section.key == 'appearance' %}
|
||||
<div class="field field-full">
|
||||
<div class="branding-preview" id="branding-preview" data-default-logo="{{ portal_logo_url }}">
|
||||
<div class="branding-preview-shell">
|
||||
<div class="branding-preview-header">
|
||||
<img class="branding-preview-logo" id="branding-preview-logo" src="{{ portal_logo_url }}" alt="{{ portal_company_name }} Logo" />
|
||||
<div class="branding-preview-copy">
|
||||
<strong id="branding-preview-company">{{ branding.company_name }}</strong>
|
||||
<span id="branding-preview-title">{{ branding.portal_title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="branding-preview-band">
|
||||
<span class="branding-preview-chip" id="branding-preview-primary">{% trans "Primärfarbe" %}</span>
|
||||
<span class="branding-preview-chip branding-preview-chip-secondary" id="branding-preview-secondary">{% trans "Sekundärfarbe" %}</span>
|
||||
</div>
|
||||
<div class="branding-preview-footer">
|
||||
<div class="branding-preview-footer-main" id="branding-preview-footer">{{ branding.footer_text|default:branding.portal_title }}</div>
|
||||
<div class="branding-preview-footer-legal" id="branding-preview-legal">{{ branding.legal_notice }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="branding-inline-actions">
|
||||
<button class="btn btn-primary" type="submit">{% trans "Speichern" %}</button>
|
||||
<button class="btn btn-secondary" type="button" data-branding-edit-cancel="{{ section.key }}">{% trans "Abbrechen" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
{% endfor %}
|
||||
|
||||
<div class="toolbar" style="margin-top:1.25rem;">
|
||||
<div class="hint">{% trans "Die aktuell gesetzte Deployment-Branding bleibt erhalten, bis hier Werte geändert oder Dateien hochgeladen werden." %}</div>
|
||||
<button class="btn btn-primary" type="submit">{% trans "Branding speichern" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script>
|
||||
(() => {
|
||||
function setBrandingMode(key, editing) {
|
||||
const view = document.querySelector('[data-branding-edit-view="' + key + '"]');
|
||||
const form = document.querySelector('[data-branding-edit-form="' + key + '"]');
|
||||
const toggle = document.querySelector('[data-branding-edit-toggle="' + key + '"]');
|
||||
if (!view || !form || !toggle) return;
|
||||
view.classList.toggle('is-hidden', editing);
|
||||
form.classList.toggle('is-hidden', !editing);
|
||||
toggle.setAttribute('aria-expanded', editing ? 'true' : 'false');
|
||||
}
|
||||
|
||||
document.querySelectorAll('[data-branding-edit-toggle]').forEach((button) => {
|
||||
const key = button.getAttribute('data-branding-edit-toggle');
|
||||
button.addEventListener('click', () => setBrandingMode(key, true));
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-branding-edit-cancel]').forEach((button) => {
|
||||
const key = button.getAttribute('data-branding-edit-cancel');
|
||||
button.addEventListener('click', () => setBrandingMode(key, false));
|
||||
});
|
||||
|
||||
const byId = (id) => document.getElementById(id);
|
||||
const title = byId('{{ form.portal_title.id_for_label }}');
|
||||
const company = byId('{{ form.company_name.id_for_label }}');
|
||||
|
||||
@@ -15,106 +15,75 @@
|
||||
{% include 'workflows/includes/messages.html' %}
|
||||
|
||||
<section class="branding-sections">
|
||||
<form method="post" action="{% url 'save_portal_company_config' %}" class="stack-form">
|
||||
{% csrf_token %}
|
||||
|
||||
<section class="branding-block">
|
||||
<div class="branding-block-head">
|
||||
<h2>{% trans "Firmenprofil" %}</h2>
|
||||
<p>{% trans "Rechtlicher Name und zentrale Stammdaten der Firma." %}</p>
|
||||
{% for section in company_config_sections %}
|
||||
<section class="branding-block company-inline-block" data-company-section="{{ section.key }}">
|
||||
<div class="branding-block-head company-inline-head">
|
||||
<div>
|
||||
<h2>{{ section.title }}</h2>
|
||||
<p>{{ section.subtitle }}</p>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<div class="field">
|
||||
<label for="{{ form.legal_company_name.id_for_label }}">{{ form.legal_company_name.label }}</label>
|
||||
{{ form.legal_company_name }}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="{{ form.phone_number.id_for_label }}">{{ form.phone_number.label }}</label>
|
||||
{{ form.phone_number }}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="{{ form.website_url.id_for_label }}">{{ form.website_url.label }}</label>
|
||||
{{ form.website_url }}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="{{ form.country.id_for_label }}">{{ form.country.label }}</label>
|
||||
{{ form.country }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="branding-block">
|
||||
<div class="branding-block-head">
|
||||
<h2>{% trans "Adresse & Register" %}</h2>
|
||||
<p>{% trans "Anschrift sowie optionale Register- und Steuerangaben." %}</p>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<div class="field field-full">
|
||||
<label for="{{ form.street_address.id_for_label }}">{{ form.street_address.label }}</label>
|
||||
{{ form.street_address }}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="{{ form.postal_code.id_for_label }}">{{ form.postal_code.label }}</label>
|
||||
{{ form.postal_code }}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="{{ form.city.id_for_label }}">{{ form.city.label }}</label>
|
||||
{{ form.city }}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="{{ form.registration_number.id_for_label }}">{{ form.registration_number.label }}</label>
|
||||
{{ form.registration_number }}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="{{ form.vat_id.id_for_label }}">{{ form.vat_id.label }}</label>
|
||||
{{ form.vat_id }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="branding-block">
|
||||
<div class="branding-block-head">
|
||||
<h2>{% trans "Kontaktpunkte" %}</h2>
|
||||
<p>{% trans "Zentrale Ansprechpartner für HR, IT und Operations." %}</p>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<div class="field">
|
||||
<label for="{{ form.hr_contact_email.id_for_label }}">{{ form.hr_contact_email.label }}</label>
|
||||
{{ form.hr_contact_email }}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="{{ form.it_contact_email.id_for_label }}">{{ form.it_contact_email.label }}</label>
|
||||
{{ form.it_contact_email }}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="{{ form.operations_contact_email.id_for_label }}">{{ form.operations_contact_email.label }}</label>
|
||||
{{ form.operations_contact_email }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="branding-block">
|
||||
<div class="branding-block-head">
|
||||
<h2>{% trans "Recht & Öffentlichkeit" %}</h2>
|
||||
<p>{% trans "Öffentliche Links für Website, Impressum und Datenschutz." %}</p>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<div class="field">
|
||||
<label for="{{ form.imprint_url.id_for_label }}">{{ form.imprint_url.label }}</label>
|
||||
{{ form.imprint_url }}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="{{ form.privacy_url.id_for_label }}">{{ form.privacy_url.label }}</label>
|
||||
{{ form.privacy_url }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="hint">{% trans "Diese Links können später im Portal-Footer oder in öffentlichen Seiten verwendet werden." %}</div>
|
||||
</section>
|
||||
|
||||
<div class="toolbar" style="margin-top:1rem;">
|
||||
<div class="hint">{% trans "Diese Ebene ist bewusst von Branding getrennt: Hier geht es um strukturierte Firmendaten, nicht um visuelle Gestaltung." %}</div>
|
||||
<button class="btn btn-primary" type="submit">{% trans "Firmenkonfiguration speichern" %}</button>
|
||||
<button class="btn btn-secondary company-inline-trigger" type="button" data-company-edit-toggle="{{ section.key }}" aria-expanded="{% if editing_company_section == section.key %}true{% else %}false{% endif %}">{% trans "Bearbeiten" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="company-inline-view{% if editing_company_section == section.key %} is-hidden{% endif %}" data-company-edit-view="{{ section.key }}">
|
||||
<div class="grid">
|
||||
{% for row in section.rows %}
|
||||
<div class="field{% if row.name == 'street_address' %} field-full{% endif %}">
|
||||
<label>{{ row.label }}</label>
|
||||
<div class="company-inline-value">{{ row.value|default:"-" }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if section.hint %}<div class="hint">{{ section.hint }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
<form method="post" action="{% url 'save_portal_company_config' %}" class="company-inline-form{% if editing_company_section != section.key %} is-hidden{% endif %}" data-company-edit-form="{{ section.key }}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="section_key" value="{{ section.key }}" />
|
||||
<div class="grid">
|
||||
{% for row in section.rows %}
|
||||
<div class="field{% if row.name == 'street_address' %} field-full{% endif %}{% if row.bound_field.errors %} has-error{% endif %}">
|
||||
<label for="{{ row.bound_field.id_for_label }}">{{ row.label }}</label>
|
||||
{{ row.bound_field }}
|
||||
{% if row.bound_field.errors %}<div class="company-inline-error">{{ row.bound_field.errors|join:", " }}</div>{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if section.hint %}<div class="hint">{{ section.hint }}</div>{% endif %}
|
||||
<div class="company-inline-actions">
|
||||
<button class="btn btn-primary" type="submit">{% trans "Speichern" %}</button>
|
||||
<button class="btn btn-secondary" type="button" data-company-edit-cancel="{{ section.key }}">{% trans "Abbrechen" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
{% endfor %}
|
||||
|
||||
<div class="toolbar" style="margin-top:1rem;">
|
||||
<div class="hint">{% trans "Diese Ebene ist bewusst von Branding getrennt: Hier geht es um strukturierte Firmendaten, nicht um visuelle Gestaltung." %}</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script>
|
||||
(function () {
|
||||
function setMode(key, editing) {
|
||||
var view = document.querySelector('[data-company-edit-view="' + key + '"]');
|
||||
var form = document.querySelector('[data-company-edit-form="' + key + '"]');
|
||||
var toggle = document.querySelector('[data-company-edit-toggle="' + key + '"]');
|
||||
if (!view || !form || !toggle) return;
|
||||
view.classList.toggle('is-hidden', editing);
|
||||
form.classList.toggle('is-hidden', !editing);
|
||||
toggle.setAttribute('aria-expanded', editing ? 'true' : 'false');
|
||||
}
|
||||
document.querySelectorAll('[data-company-edit-toggle]').forEach(function (button) {
|
||||
var key = button.getAttribute('data-company-edit-toggle');
|
||||
button.addEventListener('click', function () { setMode(key, true); });
|
||||
});
|
||||
document.querySelectorAll('[data-company-edit-cancel]').forEach(function (button) {
|
||||
var key = button.getAttribute('data-company-edit-cancel');
|
||||
button.addEventListener('click', function () { setMode(key, false); });
|
||||
});
|
||||
}());
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
|
||||
{% block title %}{{ portal_title }}{% endblock %}
|
||||
|
||||
|
||||
{% block shell_header %}
|
||||
{% include 'workflows/includes/app_header.html' with header_show_lang=1 header_inside_shell=1 %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<link rel="stylesheet" href="{% static 'workflows/css/home.css' %}" />
|
||||
{% endblock %}
|
||||
|
||||
{% block shell_body %}
|
||||
{% include 'workflows/includes/app_header.html' with header_show_lang=1 %}
|
||||
|
||||
<div class="hero">
|
||||
<div class="hero-grid">
|
||||
<div class="hero-card">
|
||||
|
||||
Reference in New Issue
Block a user