snapshot: preserve admin i18n and language stability hardening

This commit is contained in:
Md Bayazid Bostame
2026-03-24 13:06:39 +01:00
parent ec00ae8b2e
commit 5265b8f3e2
13 changed files with 1366 additions and 547 deletions

View File

@@ -47,6 +47,9 @@ Notes:
- notification rule custom subjects and bodies in DE/EN - notification rule custom subjects and bodies in DE/EN
- welcome email subject and body in DE/EN - welcome email subject and body in DE/EN
- request language capture on onboarding/offboarding to choose the correct email language - request language capture on onboarding/offboarding to choose the correct email language
- Language stability hardening is in place:
- onboarding/offboarding request records normalize `preferred_language` at model-save time
- both request tables now have a database default of `de`
- Several generated PDF business text blocks are still not fully bilingual yet. - Several generated PDF business text blocks are still not fully bilingual yet.
- CI now validates that translation catalogs compile successfully on push and pull request. - CI now validates that translation catalogs compile successfully on push and pull request.

File diff suppressed because it is too large Load Diff

View File

@@ -326,6 +326,8 @@ class OnboardingRequestForm(forms.ModelForm):
def save(self, commit=True): def save(self, commit=True):
instance = super().save(commit=False) instance = super().save(commit=False)
if not (instance.preferred_language or '').strip():
instance.preferred_language = (get_language() or 'de').split('-')[0]
instance.group_mailboxes_required = self.cleaned_data.get('group_mailboxes_required_choice') == 'ja' instance.group_mailboxes_required = self.cleaned_data.get('group_mailboxes_required_choice') == 'ja'
instance.additional_software_needed = self.cleaned_data.get('additional_software_needed_choice') == 'ja' instance.additional_software_needed = self.cleaned_data.get('additional_software_needed_choice') == 'ja'

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.1.5 on 2026-03-24 12:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('workflows', '0030_notificationrule_custom_body_en_and_more'),
]
operations = [
migrations.AlterField(
model_name='offboardingrequest',
name='preferred_language',
field=models.CharField(blank=True, db_default='de', default='de', max_length=10),
),
migrations.AlterField(
model_name='onboardingrequest',
name='preferred_language',
field=models.CharField(blank=True, db_default='de', default='de', max_length=10),
),
]

View File

@@ -2,6 +2,11 @@ from django.db import models
from django.utils.translation import get_language from django.utils.translation import get_language
def _normalized_language_code(value: str | None) -> str:
lang = (value or '').strip().split('-')[0].lower()
return lang or 'de'
class EmployeeProfile(models.Model): class EmployeeProfile(models.Model):
full_name = models.CharField(max_length=255) full_name = models.CharField(max_length=255)
first_name = models.CharField(max_length=100) first_name = models.CharField(max_length=100)
@@ -80,12 +85,16 @@ class OnboardingRequest(models.Model):
generated_pdf_path = models.CharField(max_length=500, blank=True) generated_pdf_path = models.CharField(max_length=500, blank=True)
intro_pdf_path = models.CharField(max_length=500, blank=True) intro_pdf_path = models.CharField(max_length=500, blank=True)
preferred_language = models.CharField(max_length=10, blank=True, default='de') preferred_language = models.CharField(max_length=10, blank=True, default='de', db_default='de')
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
def __str__(self) -> str: def __str__(self) -> str:
return f"Onboarding #{self.id} - {self.full_name}" return f"Onboarding #{self.id} - {self.full_name}"
def save(self, *args, **kwargs):
self.preferred_language = _normalized_language_code(self.preferred_language)
super().save(*args, **kwargs)
class FormOption(models.Model): class FormOption(models.Model):
CATEGORY_CHOICES = [ CATEGORY_CHOICES = [
@@ -422,9 +431,13 @@ class OffboardingRequest(models.Model):
signature = models.CharField(max_length=255, blank=True, verbose_name='Unterschrift (Name)') signature = models.CharField(max_length=255, blank=True, verbose_name='Unterschrift (Name)')
requested_by_email = models.EmailField(verbose_name='E-Mail der anfordernden Person') requested_by_email = models.EmailField(verbose_name='E-Mail der anfordernden Person')
requested_by_name = models.CharField(max_length=255, blank=True, verbose_name='Name der anfordernden Person') requested_by_name = models.CharField(max_length=255, blank=True, verbose_name='Name der anfordernden Person')
preferred_language = models.CharField(max_length=10, blank=True, default='de') preferred_language = models.CharField(max_length=10, blank=True, default='de', db_default='de')
generated_pdf_path = models.CharField(max_length=500, blank=True) generated_pdf_path = models.CharField(max_length=500, blank=True)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
def __str__(self) -> str: def __str__(self) -> str:
return f"Offboarding #{self.id} - {self.full_name}" return f"Offboarding #{self.id} - {self.full_name}"
def save(self, *args, **kwargs):
self.preferred_language = _normalized_language_code(self.preferred_language)
super().save(*args, **kwargs)

View File

@@ -124,10 +124,11 @@ docker compose exec -T web django-admin compilemessages</code></pre>
<ul> <ul>
<li><code>gettext</code> is installed in the Docker image.</li> <li><code>gettext</code> is installed in the Docker image.</li>
<li>Do not use custom ad hoc <code>.mo</code> compilation anymore.</li> <li>Do not use custom ad hoc <code>.mo</code> compilation anymore.</li>
<li>Phase 1 bilingual support covers fixed UI.</li> <li>Phase 1 bilingual support covers fixed UI, including the main admin-tool labels/buttons.</li>
<li>Phase 2 covers builder-driven labels and checklist items with explicit German and English fields.</li> <li>Phase 2 covers builder-driven labels and checklist items with explicit German and English fields.</li>
<li>Email phase covers <code>NotificationTemplate</code>, <code>NotificationRule</code>, and the welcome-email settings UI with explicit DE/EN subject/body fields.</li> <li>Email phase covers <code>NotificationTemplate</code>, <code>NotificationRule</code>, and the welcome-email settings UI with explicit DE/EN subject/body fields.</li>
<li>Request records now persist <code>preferred_language</code> so workflow emails can render in the submitter's active UI language with German fallback.</li> <li>Request records now persist <code>preferred_language</code> so workflow emails can render in the submitter's active UI language with German fallback.</li>
<li><code>preferred_language</code> is normalized in model <code>save()</code> and also has a DB default of <code>de</code>, so alternate creation paths cannot insert null values.</li>
</ul> </ul>
<h2 id="pdf">7) PDF Pipeline</h2> <h2 id="pdf">7) PDF Pipeline</h2>
@@ -154,6 +155,7 @@ docker compose exec -T web django-admin compilemessages</code></pre>
<li>Admin-configured overrides live in <code>NotificationTemplate</code> and <code>NotificationRule</code>.</li> <li>Admin-configured overrides live in <code>NotificationTemplate</code> and <code>NotificationRule</code>.</li>
<li>Both models now support explicit DE/EN content fields. Prefer filling both languages in the frontend integrations UI instead of embedding mixed-language copy into a single template.</li> <li>Both models now support explicit DE/EN content fields. Prefer filling both languages in the frontend integrations UI instead of embedding mixed-language copy into a single template.</li>
<li>Welcome-email configuration under Admin Apps has the same DE/EN split and follows the same runtime language selection.</li> <li>Welcome-email configuration under Admin Apps has the same DE/EN split and follows the same runtime language selection.</li>
<li>The request objects passed into email tasks always carry a normalized <code>preferred_language</code> value.</li>
<li>Mail sending uses Celery tasks and supports test mode redirection.</li> <li>Mail sending uses Celery tasks and supports test mode redirection.</li>
<li>MailHog is the local verification path when test mode is active.</li> <li>MailHog is the local verification path when test mode is active.</li>
</ul> </ul>

View File

@@ -1,10 +1,10 @@
{% load static %} {% load static i18n %}
<!doctype html> <!doctype html>
<html lang="de"> <html lang="de">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Form Builder</title> <title>{% trans "Form Builder" %}</title>
<link rel="stylesheet" href="{% static 'workflows/css/buttons.css' %}" /> <link rel="stylesheet" href="{% static 'workflows/css/buttons.css' %}" />
<link rel="stylesheet" href="{% static 'workflows/css/form_builder.css' %}" /> <link rel="stylesheet" href="{% static 'workflows/css/form_builder.css' %}" />
</head> </head>
@@ -12,12 +12,12 @@
<div class="shell"> <div class="shell">
<div class="topbar"> <div class="topbar">
<img class="brand-logo" src="{% static 'workflows/img/tubco-logo.svg' %}" alt="TUB/CO Logo" /> <img class="brand-logo" src="{% static 'workflows/img/tubco-logo.svg' %}" alt="TUB/CO Logo" />
<a class="btn btn-secondary" href="/">Zur Startseite</a> <a class="btn btn-secondary" href="/">{% trans "Zur Startseite" %}</a>
</div> </div>
<header class="header"> <header class="header">
<h1>Form Builder</h1> <h1>{% trans "Form Builder" %}</h1>
<p>Felder per Drag-and-Drop sortieren und pro Schritt gruppieren.</p> <p>{% trans "Felder per Drag-and-Drop sortieren und pro Schritt gruppieren." %}</p>
</header> </header>
{% if messages %} {% if messages %}
@@ -35,7 +35,7 @@
{{ label }} {{ label }}
</a> </a>
{% endfor %} {% endfor %}
<button id="save-order" class="btn btn-primary" type="button">Reihenfolge speichern</button> <button id="save-order" class="btn btn-primary" type="button">{% trans "Reihenfolge speichern" %}</button>
</div> </div>
<div id="status-message" class="status" aria-live="polite"></div> <div id="status-message" class="status" aria-live="polite"></div>
@@ -52,9 +52,9 @@
<div class="field-name">{{ item.field_name }}</div> <div class="field-name">{{ item.field_name }}</div>
</div> </div>
<div class="badges"> <div class="badges">
{% if item.locked %}<span class="badge locked">Fix</span>{% endif %} {% if item.locked %}<span class="badge locked">{% trans "Fix" %}</span>{% endif %}
{% if not item.is_visible %}<span class="badge hidden">Hidden</span>{% endif %} {% if not item.is_visible %}<span class="badge hidden">{% trans "Ausgeblendet" %}</span>{% endif %}
{% if item.is_required %}<span class="badge required">Pflicht</span>{% endif %} {% if item.is_required %}<span class="badge required">{% trans "Pflicht" %}</span>{% endif %}
</div> </div>
</article> </article>
{% endfor %} {% endfor %}
@@ -65,10 +65,10 @@
<section class="options-panel"> <section class="options-panel">
<div class="options-head"> <div class="options-head">
<h2>Optionen verwalten</h2> <h2>{% trans "Optionen verwalten" %}</h2>
<form class="category-switch" method="get" action="/admin-tools/form-builder/"> <form class="category-switch" method="get" action="/admin-tools/form-builder/">
<input type="hidden" name="form_type" value="{{ form_type }}" /> <input type="hidden" name="form_type" value="{{ form_type }}" />
<label for="option_category">Kategorie</label> <label for="option_category">{% trans "Kategorie" %}</label>
<select id="option_category" name="option_category" onchange="this.form.submit()"> <select id="option_category" name="option_category" onchange="this.form.submit()">
{% for value, label in option_categories %} {% for value, label in option_categories %}
<option value="{{ value }}" {% if value == selected_option_category %}selected{% endif %}>{{ label }}</option> <option value="{{ value }}" {% if value == selected_option_category %}selected{% endif %}>{{ label }}</option>
@@ -81,10 +81,10 @@
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="builder_action" value="add_option" /> <input type="hidden" name="builder_action" value="add_option" />
<input type="hidden" name="category" value="{{ selected_option_category }}" /> <input type="hidden" name="category" value="{{ selected_option_category }}" />
<input type="text" name="label" placeholder="Label (DE)" required /> <input type="text" name="label" placeholder="{% trans 'Label (DE)' %}" required />
<input type="text" name="label_en" placeholder="Label (EN, optional)" /> <input type="text" name="label_en" placeholder="{% trans 'Label (EN, optional)' %}" />
<input type="text" name="value" placeholder="Technischer Wert (optional)" /> <input type="text" name="value" placeholder="{% trans 'Technischer Wert (optional)' %}" />
<button class="btn btn-primary" type="submit">Option hinzufügen</button> <button class="btn btn-primary" type="submit">{% trans "Option hinzufügen" %}</button>
</form> </form>
<form method="post" action="/admin-tools/form-builder/?form_type={{ form_type }}&option_category={{ selected_option_category }}"> <form method="post" action="/admin-tools/form-builder/?form_type={{ form_type }}&option_category={{ selected_option_category }}">
@@ -93,12 +93,12 @@
<table class="option-table"> <table class="option-table">
<thead> <thead>
<tr> <tr>
<th>Sortierung</th> <th>{% trans "Sortierung" %}</th>
<th>Label (DE)</th> <th>{% trans "Label (DE)" %}</th>
<th>Label (EN)</th> <th>{% trans "Label (EN)" %}</th>
<th>Value</th> <th>Value</th>
<th>Aktiv</th> <th>{% trans "Aktiv" %}</th>
<th>Löschen</th> <th>{% trans "Löschen" %}</th>
</tr> </tr>
</thead> </thead>
<tbody id="option-table-body"> <tbody id="option-table-body">
@@ -106,31 +106,31 @@
<tr class="option-row" draggable="true" data-option-row="1"> <tr class="option-row" draggable="true" data-option-row="1">
<td> <td>
<input type="hidden" name="option_ids" value="{{ item.id }}" /> <input type="hidden" name="option_ids" value="{{ item.id }}" />
<span class="drag-handle" title="Ziehen zum Sortieren">⋮⋮</span> <span class="drag-handle" title="{% trans 'Ziehen zum Sortieren' %}">⋮⋮</span>
</td> </td>
<td><input type="text" name="label_{{ item.id }}" value="{{ item.label }}" required /></td> <td><input type="text" name="label_{{ item.id }}" value="{{ item.label }}" required /></td>
<td><input type="text" name="label_en_{{ item.id }}" value="{{ item.label_en }}" /></td> <td><input type="text" name="label_en_{{ item.id }}" value="{{ item.label_en }}" /></td>
<td><input type="text" name="value_{{ item.id }}" value="{{ item.value }}" /></td> <td><input type="text" name="value_{{ item.id }}" value="{{ item.value }}" /></td>
<td><input type="checkbox" name="active_{{ item.id }}" {% if item.is_active %}checked{% endif %} /></td> <td><input type="checkbox" name="active_{{ item.id }}" {% if item.is_active %}checked{% endif %} /></td>
<td> <td>
<button class="btn btn-secondary" type="submit" name="delete_option_id" value="{{ item.id }}" onclick="return confirm('Option wirklich löschen?');">Löschen</button> <button class="btn btn-secondary" type="submit" name="delete_option_id" value="{{ item.id }}" onclick="return confirm('{% trans 'Option wirklich löschen?' %}');">{% trans "Löschen" %}</button>
</td> </td>
</tr> </tr>
{% empty %} {% empty %}
<tr><td colspan="6">Keine Optionen in dieser Kategorie.</td></tr> <tr><td colspan="6">{% trans "Keine Optionen in dieser Kategorie." %}</td></tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="options-actions"> <div class="options-actions">
<button class="btn btn-primary" type="submit" name="builder_action" value="save_options">Optionen speichern</button> <button class="btn btn-primary" type="submit" name="builder_action" value="save_options">{% trans "Optionen speichern" %}</button>
</div> </div>
</form> </form>
</section> </section>
<section class="options-panel"> <section class="options-panel">
<div class="options-head"> <div class="options-head">
<h2>Feldtexte verwalten</h2> <h2>{% trans "Feldtexte verwalten" %}</h2>
</div> </div>
<form method="post" action="/admin-tools/form-builder/?form_type={{ form_type }}&option_category={{ selected_option_category }}"> <form method="post" action="/admin-tools/form-builder/?form_type={{ form_type }}&option_category={{ selected_option_category }}">
{% csrf_token %} {% csrf_token %}
@@ -138,11 +138,11 @@
<table class="option-table"> <table class="option-table">
<thead> <thead>
<tr> <tr>
<th>Feld</th> <th>{% trans "Feld" %}</th>
<th>Label (DE)</th> <th>{% trans "Label (DE)" %}</th>
<th>Label (EN)</th> <th>{% trans "Label (EN)" %}</th>
<th>Hilfetext (DE)</th> <th>{% trans "Hilfetext (DE)" %}</th>
<th>Hilfetext (EN)</th> <th>{% trans "Hilfetext (EN)" %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -152,19 +152,19 @@
<input type="hidden" name="field_ids" value="{{ item.id }}" /> <input type="hidden" name="field_ids" value="{{ item.id }}" />
<strong>{{ item.field_name }}</strong> <strong>{{ item.field_name }}</strong>
</td> </td>
<td><input type="text" name="label_override_{{ item.id }}" value="{{ item.label_override }}" placeholder="Fallback: Standardlabel" /></td> <td><input type="text" name="label_override_{{ item.id }}" value="{{ item.label_override }}" placeholder="{% trans 'Fallback: Standardlabel' %}" /></td>
<td><input type="text" name="label_override_en_{{ item.id }}" value="{{ item.label_override_en }}" placeholder="English label" /></td> <td><input type="text" name="label_override_en_{{ item.id }}" value="{{ item.label_override_en }}" placeholder="{% trans 'English label' %}" /></td>
<td><input type="text" name="help_text_override_{{ item.id }}" value="{{ item.help_text_override }}" placeholder="Optionaler Hilfetext" /></td> <td><input type="text" name="help_text_override_{{ item.id }}" value="{{ item.help_text_override }}" placeholder="{% trans 'Optionaler Hilfetext' %}" /></td>
<td><input type="text" name="help_text_override_en_{{ item.id }}" value="{{ item.help_text_override_en }}" placeholder="Optional English help text" /></td> <td><input type="text" name="help_text_override_en_{{ item.id }}" value="{{ item.help_text_override_en }}" placeholder="{% trans 'Optional English help text' %}" /></td>
</tr> </tr>
{% empty %} {% empty %}
<tr><td colspan="5">Keine Feldkonfigurationen verfügbar.</td></tr> <tr><td colspan="5">{% trans "Keine Feldkonfigurationen verfügbar." %}</td></tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="options-actions"> <div class="options-actions">
<button class="btn btn-primary" type="submit" name="builder_action" value="save_field_texts">Feldtexte speichern</button> <button class="btn btn-primary" type="submit" name="builder_action" value="save_field_texts">{% trans "Feldtexte speichern" %}</button>
</div> </div>
</form> </form>
</section> </section>

View File

@@ -4,7 +4,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Handbook</title> <title>{% trans "Handbook" %}</title>
<link rel="stylesheet" href="{% static 'workflows/css/buttons.css' %}" /> <link rel="stylesheet" href="{% static 'workflows/css/buttons.css' %}" />
<style> <style>
body { margin: 0; font-family: Arial, sans-serif; background: #f4f8ff; color: #1b2b43; padding: 20px; } body { margin: 0; font-family: Arial, sans-serif; background: #f4f8ff; color: #1b2b43; padding: 20px; }
@@ -28,39 +28,39 @@
<div class="shell"> <div class="shell">
<img class="brand-logo" src="{% static 'workflows/img/tubco-logo.svg' %}" alt="TUB/CO Logo" /> <img class="brand-logo" src="{% static 'workflows/img/tubco-logo.svg' %}" alt="TUB/CO Logo" />
<div class="top"> <div class="top">
<h1>Handbook</h1> <h1>{% trans "Handbook" %}</h1>
<a class="btn btn-secondary" href="/">Back to Home</a> <a class="btn btn-secondary" href="/">{% trans "Back to Home" %}</a>
</div> </div>
<p class="sub">Single documentation entry point for both operational knowledge and long-term engineering knowledge.</p> <p class="sub">{% trans "Single documentation entry point for both operational knowledge and long-term engineering knowledge." %}</p>
<div class="grid"> <div class="grid">
<section class="card"> <section class="card">
<div class="eyebrow">Operations</div> <div class="eyebrow">{% trans "Operations" %}</div>
<h2>Project Wiki</h2> <h2>{% trans "Project Wiki" %}</h2>
<p>Operational and product-level documentation for onboarding, offboarding, PDFs, integrations, admin tools, and system behavior.</p> <p>{% trans "Operational and product-level documentation for onboarding, offboarding, PDFs, integrations, admin tools, and system behavior." %}</p>
<ul> <ul>
<li>workflow overview</li> <li>{% trans "workflow overview" %}</li>
<li>admin tools and system behavior</li> <li>{% trans "admin tools and system behavior" %}</li>
<li>integrations and operations</li> <li>{% trans "integrations and operations" %}</li>
<li>runbook and troubleshooting</li> <li>{% trans "runbook and troubleshooting" %}</li>
</ul> </ul>
<div class="actions"> <div class="actions">
<a class="btn btn-secondary" href="/admin-tools/wiki/">Open Project Wiki</a> <a class="btn btn-secondary" href="/admin-tools/wiki/">{% trans "Open Project Wiki" %}</a>
</div> </div>
</section> </section>
<section class="card"> <section class="card">
<div class="eyebrow">Engineering</div> <div class="eyebrow">{% trans "Engineering" %}</div>
<h2>Developer Handbook</h2> <h2>{% trans "Developer Handbook" %}</h2>
<p>Engineering documentation for architecture, local setup, Docker, migrations, translations, deployment, testing, and long-term maintenance.</p> <p>{% trans "Engineering documentation for architecture, local setup, Docker, migrations, translations, deployment, testing, and long-term maintenance." %}</p>
<ul> <ul>
<li>repository and service structure</li> <li>{% trans "repository and service structure" %}</li>
<li>Docker and migration workflow</li> <li>{% trans "Docker and migration workflow" %}</li>
<li>translation and builder architecture</li> <li>{% trans "translation and builder architecture" %}</li>
<li>deployment, security, and maintenance notes</li> <li>{% trans "deployment, security, and maintenance notes" %}</li>
</ul> </ul>
<div class="actions"> <div class="actions">
<a class="btn btn-secondary" href="/admin-tools/developer-handbook/">Open Developer Handbook</a> <a class="btn btn-secondary" href="/admin-tools/developer-handbook/">{% trans "Open Developer Handbook" %}</a>
</div> </div>
</section> </section>
</div> </div>

View File

@@ -1,10 +1,10 @@
{% load static %} {% load static i18n %}
<!doctype html> <!doctype html>
<html lang="de"> <html lang="de">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Integrationen Setup</title> <title>{% trans "Integrationen Setup" %}</title>
<link rel="stylesheet" href="{% static 'workflows/css/buttons.css' %}" /> <link rel="stylesheet" href="{% static 'workflows/css/buttons.css' %}" />
<style> <style>
body { margin: 0; font-family: Arial, sans-serif; background: #f4f8ff; color: #0f172a; padding: 20px; } body { margin: 0; font-family: Arial, sans-serif; background: #f4f8ff; color: #0f172a; padding: 20px; }
@@ -74,15 +74,15 @@
<div class="shell"> <div class="shell">
<div class="topbar"> <div class="topbar">
<img class="brand-logo" src="{% static 'workflows/img/tubco-logo.svg' %}" alt="TUB/CO Logo" /> <img class="brand-logo" src="{% static 'workflows/img/tubco-logo.svg' %}" alt="TUB/CO Logo" />
<a class="btn btn-secondary" href="/">Zur Startseite</a> <a class="btn btn-secondary" href="/">{% trans "Zur Startseite" %}</a>
</div> </div>
<h1>Integrationen Setup</h1> <h1>{% trans "Integrationen Setup" %}</h1>
<p class="sub">Verwalten Sie Nextcloud- und Mail-Konfiguration ohne Backend-Wechsel.</p> <p class="sub">{% trans "Verwalten Sie Nextcloud- und Mail-Konfiguration ohne Backend-Wechsel." %}</p>
<div class="switch"> <div class="switch">
<a class="tab {% if kind == 'nextcloud' %}active{% endif %}" href="/admin-tools/integrations/?kind=nextcloud">Setup Nextcloud</a> <a class="tab {% if kind == 'nextcloud' %}active{% endif %}" href="/admin-tools/integrations/?kind=nextcloud">{% trans "Setup Nextcloud" %}</a>
<a class="tab {% if kind == 'mail' %}active{% endif %}" href="/admin-tools/integrations/?kind=mail">Setup Mail</a> <a class="tab {% if kind == 'mail' %}active{% endif %}" href="/admin-tools/integrations/?kind=mail">{% trans "Setup Mail" %}</a>
<a class="tab {% if kind == 'emails' %}active{% endif %}" href="/admin-tools/integrations/?kind=emails">E-Mail Routing & Vorlagen</a> <a class="tab {% if kind == 'emails' %}active{% endif %}" href="/admin-tools/integrations/?kind=emails">{% trans "E-Mail Routing & Vorlagen" %}</a>
</div> </div>
{% if messages %} {% if messages %}
@@ -117,9 +117,9 @@
</div> </div>
</div> </div>
<div class="actions"> <div class="actions">
<button class="btn btn-primary" type="submit">Nextcloud speichern</button> <button class="btn btn-primary" type="submit">{% trans "Nextcloud speichern" %}</button>
</div> </div>
<div class="hint">Leeres Passwortfeld lässt das bestehende Passwort unverändert.</div> <div class="hint">{% trans "Leeres Passwortfeld lässt das bestehende Passwort unverändert." %}</div>
</form> </form>
{% endif %} {% endif %}
@@ -153,13 +153,13 @@
</div> </div>
</div> </div>
<div class="check-row"> <div class="check-row">
<label><input type="checkbox" name="smtp_use_ssl" {% if workflow_config.smtp_use_ssl %}checked{% endif %} /> SMTP SSL</label> <label><input type="checkbox" name="smtp_use_ssl" {% if workflow_config.smtp_use_ssl %}checked{% endif %} /> {% trans "SMTP SSL" %}</label>
<label><input type="checkbox" name="smtp_use_tls" {% if workflow_config.smtp_use_tls %}checked{% endif %} /> SMTP TLS</label> <label><input type="checkbox" name="smtp_use_tls" {% if workflow_config.smtp_use_tls %}checked{% endif %} /> {% trans "SMTP TLS" %}</label>
</div> </div>
<div class="actions"> <div class="actions">
<button class="btn btn-primary" type="submit">Mail speichern</button> <button class="btn btn-primary" type="submit">{% trans "Mail speichern" %}</button>
</div> </div>
<div class="hint">Leeres Passwortfeld lässt das bestehende Passwort unverändert.</div> <div class="hint">{% trans "Leeres Passwortfeld lässt das bestehende Passwort unverändert." %}</div>
</form> </form>
{% endif %} {% endif %}
@@ -168,27 +168,27 @@
{% csrf_token %} {% csrf_token %}
<div class="grid"> <div class="grid">
<div> <div>
<label for="it_onboarding_email">It onboarding email</label> <label for="it_onboarding_email">{% trans "It onboarding email" %}</label>
<input id="it_onboarding_email" name="it_onboarding_email" value="{{ workflow_config.it_onboarding_email }}" /> <input id="it_onboarding_email" name="it_onboarding_email" value="{{ workflow_config.it_onboarding_email }}" />
</div> </div>
<div> <div>
<label for="general_info_email">General info email</label> <label for="general_info_email">{% trans "General info email" %}</label>
<input id="general_info_email" name="general_info_email" value="{{ workflow_config.general_info_email }}" /> <input id="general_info_email" name="general_info_email" value="{{ workflow_config.general_info_email }}" />
</div> </div>
<div> <div>
<label for="business_card_email">Business card email</label> <label for="business_card_email">{% trans "Business card email" %}</label>
<input id="business_card_email" name="business_card_email" value="{{ workflow_config.business_card_email }}" /> <input id="business_card_email" name="business_card_email" value="{{ workflow_config.business_card_email }}" />
</div> </div>
<div> <div>
<label for="hr_works_email">Hr works email</label> <label for="hr_works_email">{% trans "Hr works email" %}</label>
<input id="hr_works_email" name="hr_works_email" value="{{ workflow_config.hr_works_email }}" /> <input id="hr_works_email" name="hr_works_email" value="{{ workflow_config.hr_works_email }}" />
</div> </div>
<div> <div>
<label for="key_notification_email">Key notification email</label> <label for="key_notification_email">{% trans "Key notification email" %}</label>
<input id="key_notification_email" name="key_notification_email" value="{{ workflow_config.key_notification_email }}" /> <input id="key_notification_email" name="key_notification_email" value="{{ workflow_config.key_notification_email }}" />
</div> </div>
</div> </div>
<div class="hint">Diese Empfänger werden für condition-based E-Mail Routing genutzt.</div> <div class="hint">{% trans "Diese Empfänger werden für condition-based E-Mail Routing genutzt." %}</div>
{% for tpl in templates %} {% for tpl in templates %}
<div class="template-block"> <div class="template-block">
@@ -215,25 +215,25 @@
{% endfor %} {% endfor %}
<div class="actions"> <div class="actions">
<button class="btn btn-primary" type="submit">E-Mail Routing & Vorlagen speichern</button> <button class="btn btn-primary" type="submit">{% trans "E-Mail Routing & Vorlagen speichern" %}</button>
</div> </div>
</form> </form>
<form class="rule-card" method="post" action="/admin-tools/integrations/save-rules/"> <form class="rule-card" method="post" action="/admin-tools/integrations/save-rules/">
{% csrf_token %} {% csrf_token %}
<p class="rule-title">Bedingungsregeln für zusätzliche E-Mails</p> <p class="rule-title">{% trans "Bedingungsregeln für zusätzliche E-Mails" %}</p>
<div class="hint">Zusätzliche Regeln laufen nach dem Standard-Routing.</div> <div class="hint">{% trans "Zusätzliche Regeln laufen nach dem Standard-Routing." %}</div>
{% for rule in notification_rules %} {% for rule in notification_rules %}
<div class="template-block"> <div class="template-block">
<input type="hidden" name="rule_ids" value="{{ rule.id }}" /> <input type="hidden" name="rule_ids" value="{{ rule.id }}" />
<div class="grid"> <div class="grid">
<div> <div>
<label for="name_{{ rule.id }}">Regelname</label> <label for="name_{{ rule.id }}">{% trans "Regelname" %}</label>
<input id="name_{{ rule.id }}" name="name_{{ rule.id }}" value="{{ rule.name }}" /> <input id="name_{{ rule.id }}" name="name_{{ rule.id }}" value="{{ rule.name }}" />
</div> </div>
<div> <div>
<label for="event_type_{{ rule.id }}">Event</label> <label for="event_type_{{ rule.id }}">{% trans "Event" %}</label>
<select id="event_type_{{ rule.id }}" name="event_type_{{ rule.id }}"> <select id="event_type_{{ rule.id }}" name="event_type_{{ rule.id }}">
{% for key, label in rule_event_choices %} {% for key, label in rule_event_choices %}
<option value="{{ key }}" {% if key == rule.event_type %}selected{% endif %}>{{ label }}</option> <option value="{{ key }}" {% if key == rule.event_type %}selected{% endif %}>{{ label }}</option>
@@ -241,11 +241,11 @@
</select> </select>
</div> </div>
<div> <div>
<label for="field_name_{{ rule.id }}">Feldname</label> <label for="field_name_{{ rule.id }}">{% trans "Feldname" %}</label>
<input id="field_name_{{ rule.id }}" name="field_name_{{ rule.id }}" value="{{ rule.field_name }}" placeholder="z. B. needed_devices" /> <input id="field_name_{{ rule.id }}" name="field_name_{{ rule.id }}" value="{{ rule.field_name }}" placeholder="z. B. needed_devices" />
</div> </div>
<div> <div>
<label for="operator_{{ rule.id }}">Operator</label> <label for="operator_{{ rule.id }}">{% trans "Operator" %}</label>
<select id="operator_{{ rule.id }}" name="operator_{{ rule.id }}"> <select id="operator_{{ rule.id }}" name="operator_{{ rule.id }}">
{% for key, label in rule_operator_choices %} {% for key, label in rule_operator_choices %}
<option value="{{ key }}" {% if key == rule.operator %}selected{% endif %}>{{ label }}</option> <option value="{{ key }}" {% if key == rule.operator %}selected{% endif %}>{{ label }}</option>
@@ -253,58 +253,58 @@
</select> </select>
</div> </div>
<div> <div>
<label for="expected_value_{{ rule.id }}">Vergleichswert</label> <label for="expected_value_{{ rule.id }}">{% trans "Vergleichswert" %}</label>
<input id="expected_value_{{ rule.id }}" name="expected_value_{{ rule.id }}" value="{{ rule.expected_value }}" placeholder="z. B. Schlüssel" /> <input id="expected_value_{{ rule.id }}" name="expected_value_{{ rule.id }}" value="{{ rule.expected_value }}" placeholder="z. B. Schlüssel" />
</div> </div>
<div> <div>
<label for="recipients_{{ rule.id }}">Empfänger</label> <label for="recipients_{{ rule.id }}">{% trans "Empfänger" %}</label>
<input id="recipients_{{ rule.id }}" name="recipients_{{ rule.id }}" value="{{ rule.recipients }}" placeholder="a@b.de, c@d.de" /> <input id="recipients_{{ rule.id }}" name="recipients_{{ rule.id }}" value="{{ rule.recipients }}" placeholder="a@b.de, c@d.de" />
</div> </div>
<div> <div>
<label for="template_key_{{ rule.id }}">Template Key (optional)</label> <label for="template_key_{{ rule.id }}">{% trans "Template Key (optional)" %}</label>
<select id="template_key_{{ rule.id }}" name="template_key_{{ rule.id }}"> <select id="template_key_{{ rule.id }}" name="template_key_{{ rule.id }}">
<option value="">-- Custom Betreff/Body verwenden --</option> <option value="">{% trans "-- Custom Betreff/Body verwenden --" %}</option>
{% for key, label in template_choices %} {% for key, label in template_choices %}
<option value="{{ key }}" {% if key == rule.template_key %}selected{% endif %}>{{ label }} ({{ key }})</option> <option value="{{ key }}" {% if key == rule.template_key %}selected{% endif %}>{{ label }} ({{ key }})</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
<div> <div>
<label for="custom_subject_{{ rule.id }}">Custom Subject (optional)</label> <label for="custom_subject_{{ rule.id }}">{% trans "Custom Subject (optional)" %}</label>
<input id="custom_subject_{{ rule.id }}" name="custom_subject_{{ rule.id }}" value="{{ rule.custom_subject }}" /> <input id="custom_subject_{{ rule.id }}" name="custom_subject_{{ rule.id }}" value="{{ rule.custom_subject }}" />
</div> </div>
<div> <div>
<label for="custom_body_{{ rule.id }}">Custom Body (optional)</label> <label for="custom_body_{{ rule.id }}">{% trans "Custom Body (optional)" %}</label>
<textarea id="custom_body_{{ rule.id }}" name="custom_body_{{ rule.id }}">{{ rule.custom_body }}</textarea> <textarea id="custom_body_{{ rule.id }}" name="custom_body_{{ rule.id }}">{{ rule.custom_body }}</textarea>
</div> </div>
<div> <div>
<label for="custom_subject_en_{{ rule.id }}">Custom Subject (EN, optional)</label> <label for="custom_subject_en_{{ rule.id }}">{% trans "Custom Subject (EN, optional)" %}</label>
<input id="custom_subject_en_{{ rule.id }}" name="custom_subject_en_{{ rule.id }}" value="{{ rule.custom_subject_en }}" /> <input id="custom_subject_en_{{ rule.id }}" name="custom_subject_en_{{ rule.id }}" value="{{ rule.custom_subject_en }}" />
</div> </div>
<div> <div>
<label for="custom_body_en_{{ rule.id }}">Custom Body (EN, optional)</label> <label for="custom_body_en_{{ rule.id }}">{% trans "Custom Body (EN, optional)" %}</label>
<textarea id="custom_body_en_{{ rule.id }}" name="custom_body_en_{{ rule.id }}">{{ rule.custom_body_en }}</textarea> <textarea id="custom_body_en_{{ rule.id }}" name="custom_body_en_{{ rule.id }}">{{ rule.custom_body_en }}</textarea>
</div> </div>
</div> </div>
<div class="check-row"> <div class="check-row">
<label><input type="checkbox" name="active_{{ rule.id }}" {% if rule.is_active %}checked{% endif %} /> Aktiv</label> <label><input type="checkbox" name="active_{{ rule.id }}" {% if rule.is_active %}checked{% endif %} /> {% trans "Aktiv" %}</label>
<label><input type="checkbox" name="include_pdf_{{ rule.id }}" {% if rule.include_pdf_attachment %}checked{% endif %} /> PDF anhängen</label> <label><input type="checkbox" name="include_pdf_{{ rule.id }}" {% if rule.include_pdf_attachment %}checked{% endif %} /> {% trans "PDF anhängen" %}</label>
<label><input type="checkbox" name="delete_{{ rule.id }}" /> Löschen</label> <label><input type="checkbox" name="delete_{{ rule.id }}" /> {% trans "Löschen" %}</label>
</div> </div>
</div> </div>
{% empty %} {% empty %}
<div class="hint">Noch keine zusätzlichen Regeln vorhanden.</div> <div class="hint">{% trans "Noch keine zusätzlichen Regeln vorhanden." %}</div>
{% endfor %} {% endfor %}
<div class="template-block"> <div class="template-block">
<p class="template-title">Neue Regel hinzufügen</p> <p class="template-title">{% trans "Neue Regel hinzufügen" %}</p>
<div class="grid"> <div class="grid">
<div> <div>
<label for="new_name">Regelname</label> <label for="new_name">{% trans "Regelname" %}</label>
<input id="new_name" name="new_name" placeholder="z. B. Extra Schlüssel-Mail" /> <input id="new_name" name="new_name" placeholder="z. B. Extra Schlüssel-Mail" />
</div> </div>
<div> <div>
<label for="new_event_type">Event</label> <label for="new_event_type">{% trans "Event" %}</label>
<select id="new_event_type" name="new_event_type"> <select id="new_event_type" name="new_event_type">
{% for key, label in rule_event_choices %} {% for key, label in rule_event_choices %}
<option value="{{ key }}">{{ label }}</option> <option value="{{ key }}">{{ label }}</option>
@@ -312,11 +312,11 @@
</select> </select>
</div> </div>
<div> <div>
<label for="new_field_name">Feldname</label> <label for="new_field_name">{% trans "Feldname" %}</label>
<input id="new_field_name" name="new_field_name" placeholder="z. B. needed_devices" /> <input id="new_field_name" name="new_field_name" placeholder="z. B. needed_devices" />
</div> </div>
<div> <div>
<label for="new_operator">Operator</label> <label for="new_operator">{% trans "Operator" %}</label>
<select id="new_operator" name="new_operator"> <select id="new_operator" name="new_operator">
{% for key, label in rule_operator_choices %} {% for key, label in rule_operator_choices %}
<option value="{{ key }}">{{ label }}</option> <option value="{{ key }}">{{ label }}</option>
@@ -324,46 +324,46 @@
</select> </select>
</div> </div>
<div> <div>
<label for="new_expected_value">Vergleichswert</label> <label for="new_expected_value">{% trans "Vergleichswert" %}</label>
<input id="new_expected_value" name="new_expected_value" placeholder="z. B. Schlüssel" /> <input id="new_expected_value" name="new_expected_value" placeholder="z. B. Schlüssel" />
</div> </div>
<div> <div>
<label for="new_recipients">Empfänger</label> <label for="new_recipients">{% trans "Empfänger" %}</label>
<input id="new_recipients" name="new_recipients" placeholder="a@b.de, c@d.de" /> <input id="new_recipients" name="new_recipients" placeholder="a@b.de, c@d.de" />
</div> </div>
<div> <div>
<label for="new_template_key">Template Key (optional)</label> <label for="new_template_key">{% trans "Template Key (optional)" %}</label>
<select id="new_template_key" name="new_template_key"> <select id="new_template_key" name="new_template_key">
<option value="">-- Custom Betreff/Body verwenden --</option> <option value="">{% trans "-- Custom Betreff/Body verwenden --" %}</option>
{% for key, label in template_choices %} {% for key, label in template_choices %}
<option value="{{ key }}">{{ label }} ({{ key }})</option> <option value="{{ key }}">{{ label }} ({{ key }})</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
<div> <div>
<label for="new_custom_subject">Custom Subject (optional)</label> <label for="new_custom_subject">{% trans "Custom Subject (optional)" %}</label>
<input id="new_custom_subject" name="new_custom_subject" /> <input id="new_custom_subject" name="new_custom_subject" />
</div> </div>
<div> <div>
<label for="new_custom_body">Custom Body (optional)</label> <label for="new_custom_body">{% trans "Custom Body (optional)" %}</label>
<textarea id="new_custom_body" name="new_custom_body"></textarea> <textarea id="new_custom_body" name="new_custom_body"></textarea>
</div> </div>
<div> <div>
<label for="new_custom_subject_en">Custom Subject (EN, optional)</label> <label for="new_custom_subject_en">{% trans "Custom Subject (EN, optional)" %}</label>
<input id="new_custom_subject_en" name="new_custom_subject_en" /> <input id="new_custom_subject_en" name="new_custom_subject_en" />
</div> </div>
<div> <div>
<label for="new_custom_body_en">Custom Body (EN, optional)</label> <label for="new_custom_body_en">{% trans "Custom Body (EN, optional)" %}</label>
<textarea id="new_custom_body_en" name="new_custom_body_en"></textarea> <textarea id="new_custom_body_en" name="new_custom_body_en"></textarea>
</div> </div>
</div> </div>
<div class="check-row"> <div class="check-row">
<label><input type="checkbox" name="new_include_pdf" /> PDF anhängen</label> <label><input type="checkbox" name="new_include_pdf" /> {% trans "PDF anhängen" %}</label>
</div> </div>
</div> </div>
<div class="actions"> <div class="actions">
<button class="btn btn-primary" type="submit">Regeln speichern</button> <button class="btn btn-primary" type="submit">{% trans "Regeln speichern" %}</button>
</div> </div>
</form> </form>
{% endif %} {% endif %}

View File

@@ -1,10 +1,10 @@
{% load static %} {% load static i18n %}
<!doctype html> <!doctype html>
<html lang="de"> <html lang="de">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Einweisungs-Builder</title> <title>{% trans "Einweisungs-Builder" %}</title>
<link rel="stylesheet" href="{% static 'workflows/css/buttons.css' %}" /> <link rel="stylesheet" href="{% static 'workflows/css/buttons.css' %}" />
<style> <style>
body { margin: 0; font-family: Arial, sans-serif; background: #f4f8ff; color: #1b2b43; padding: 20px; } body { margin: 0; font-family: Arial, sans-serif; background: #f4f8ff; color: #1b2b43; padding: 20px; }
@@ -36,15 +36,15 @@
<div class="shell"> <div class="shell">
<div class="topbar"> <div class="topbar">
<img class="brand-logo" src="{% static 'workflows/img/tubco-logo.svg' %}" alt="TUB/CO Logo" /> <img class="brand-logo" src="{% static 'workflows/img/tubco-logo.svg' %}" alt="TUB/CO Logo" />
<a class="btn btn-secondary" href="/">Zur Startseite</a> <a class="btn btn-secondary" href="/">{% trans "Zur Startseite" %}</a>
</div> </div>
<div class="toolbar"> <div class="toolbar">
<div> <div>
<h1>Einweisungs-Builder</h1> <h1>{% trans "Einweisungs-Builder" %}</h1>
<p class="sub">Checklistenpunkte für das Einweisungs- und Übergabeprotokoll verwalten.</p> <p class="sub">{% trans "Checklistenpunkte für das Einweisungs- und Übergabeprotokoll verwalten." %}</p>
</div> </div>
<a class="btn btn-secondary" href="/requests/">Zum Dashboard</a> <a class="btn btn-secondary" href="/requests/">{% trans "Zum Dashboard" %}</a>
</div> </div>
{% if messages %} {% if messages %}
@@ -58,7 +58,7 @@
<input type="hidden" name="builder_action" value="add_item" /> <input type="hidden" name="builder_action" value="add_item" />
<div class="grid"> <div class="grid">
<div class="field"> <div class="field">
<label for="section">Abschnitt</label> <label for="section">{% trans "Abschnitt" %}</label>
<select id="section" name="section"> <select id="section" name="section">
{% for value, label in section_choices %} {% for value, label in section_choices %}
<option value="{{ value }}">{{ label }}</option> <option value="{{ value }}">{{ label }}</option>
@@ -66,18 +66,18 @@
</select> </select>
</div> </div>
<div class="field"> <div class="field">
<label for="label">Checklistenpunkt (DE)</label> <label for="label">{% trans "Checklistenpunkt (DE)" %}</label>
<input id="label" name="label" placeholder="z. B. Nextcloud Ordnerstruktur erklärt" required /> <input id="label" name="label" placeholder="{% trans 'z. B. Nextcloud Ordnerstruktur erklärt' %}" required />
</div> </div>
<div class="field"> <div class="field">
<label for="label_en">Checklist item (EN)</label> <label for="label_en">{% trans "Checklist item (EN)" %}</label>
<input id="label_en" name="label_en" placeholder="e.g. Nextcloud folder structure explained" /> <input id="label_en" name="label_en" placeholder="{% trans 'e.g. Nextcloud folder structure explained' %}" />
</div> </div>
<div class="actions"> <div class="actions">
<button class="btn btn-primary" type="submit">Punkt hinzufügen</button> <button class="btn btn-primary" type="submit">{% trans "Punkt hinzufügen" %}</button>
</div> </div>
</div> </div>
<div class="hint">Bedingungen und Sortierung können anschließend in der Tabelle bearbeitet werden.</div> <div class="hint">{% trans "Bedingungen und Sortierung können anschließend in der Tabelle bearbeitet werden." %}</div>
</form> </form>
<form class="card" method="post" action="/admin-tools/intro-builder/"> <form class="card" method="post" action="/admin-tools/intro-builder/">
@@ -87,15 +87,15 @@
<table class="table-controls"> <table class="table-controls">
<thead> <thead>
<tr> <tr>
<th>Sortierung</th> <th>{% trans "Sortierung" %}</th>
<th>Abschnitt</th> <th>{% trans "Abschnitt" %}</th>
<th>Checklistenpunkt (DE)</th> <th>{% trans "Checklistenpunkt (DE)" %}</th>
<th>Checklistenpunkt (EN)</th> <th>{% trans "Checklistenpunkt (EN)" %}</th>
<th>Feld-Bedingung</th> <th>{% trans "Feld-Bedingung" %}</th>
<th>Operator</th> <th>{% trans "Operator" %}</th>
<th>Wert</th> <th>{% trans "Wert" %}</th>
<th>Aktiv</th> <th>{% trans "Aktiv" %}</th>
<th>Löschen</th> <th>{% trans "Löschen" %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -128,21 +128,21 @@
{% endfor %} {% endfor %}
</select> </select>
</td> </td>
<td><input type="text" name="value_{{ item.id }}" value="{{ item.condition_value }}" placeholder="z. B. HR Works" /></td> <td><input type="text" name="value_{{ item.id }}" value="{{ item.condition_value }}" placeholder="{% trans 'z. B. HR Works' %}" /></td>
<td><input type="checkbox" name="active_{{ item.id }}" {% if item.is_active %}checked{% endif %} /></td> <td><input type="checkbox" name="active_{{ item.id }}" {% if item.is_active %}checked{% endif %} /></td>
<td class="actions"> <td class="actions">
<button class="btn btn-secondary" type="submit" name="delete_item_id" value="{{ item.id }}" onclick="return confirm('Checklistenpunkt wirklich löschen?');">Löschen</button> <button class="btn btn-secondary" type="submit" name="delete_item_id" value="{{ item.id }}" onclick="return confirm('{% trans 'Checklistenpunkt wirklich löschen?' %}');">{% trans "Löschen" %}</button>
</td> </td>
</tr> </tr>
{% empty %} {% empty %}
<tr><td colspan="9">Noch keine benutzerdefinierten Checklistenpunkte angelegt. Solange die Liste leer ist, nutzt das System die integrierten Standardpunkte.</td></tr> <tr><td colspan="9">{% trans "Noch keine benutzerdefinierten Checklistenpunkte angelegt. Solange die Liste leer ist, nutzt das System die integrierten Standardpunkte." %}</td></tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="hint">Reihenfolge folgt derzeit der Tabellenreihenfolge beim Speichern.</div> <div class="hint">{% trans "Reihenfolge folgt derzeit der Tabellenreihenfolge beim Speichern." %}</div>
<div style="margin-top:12px;"> <div style="margin-top:12px;">
<button class="btn btn-primary" type="submit">Checkliste speichern</button> <button class="btn btn-primary" type="submit">{% trans "Checkliste speichern" %}</button>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -164,10 +164,11 @@
<h2 id="bilingual">8b) Bilingual Core UI</h2> <h2 id="bilingual">8b) Bilingual Core UI</h2>
<ul> <ul>
<li><strong>Current scope:</strong> the core user interface supports German and English switching for the main fixed UI pages.</li> <li><strong>Current scope:</strong> the core user interface supports German and English switching for the main fixed UI pages.</li>
<li><strong>Covered now:</strong> login, home, requests dashboard, onboarding form shell, offboarding form shell, and common status/messages in views.</li> <li><strong>Covered now:</strong> login, home, requests dashboard, onboarding form shell, offboarding form shell, common status/messages in views, and the main admin-tool labels/buttons for builders, integrations, welcome emails, and the handbook hub.</li>
<li><strong>Phase 2 added:</strong> dynamic Form Builder option labels, field label/help-text overrides, and intro-builder checklist item labels now support German and English values.</li> <li><strong>Phase 2 added:</strong> dynamic Form Builder option labels, field label/help-text overrides, and intro-builder checklist item labels now support German and English values.</li>
<li><strong>Email phase added:</strong> admin-managed notification template subject/body content, notification rule custom subject/body content, and welcome email subject/body content now support explicit DE/EN values.</li> <li><strong>Email phase added:</strong> admin-managed notification template subject/body content, notification rule custom subject/body content, and welcome email subject/body content now support explicit DE/EN values.</li>
<li><strong>Runtime selection:</strong> onboarding and offboarding requests store the UI language at submission time, and notification delivery uses that language with German fallback.</li> <li><strong>Runtime selection:</strong> onboarding and offboarding requests store the UI language at submission time, and notification delivery uses that language with German fallback.</li>
<li><strong>Stability hardening:</strong> request models normalize <code>preferred_language</code> on save, and the database schema also enforces a default of <code>de</code>. This prevents null-language inserts outside the main form/view path.</li>
<li><strong>Editing path:</strong> these DE/EN values are maintained directly in the frontend builder pages, not only in Django admin.</li> <li><strong>Editing path:</strong> these DE/EN values are maintained directly in the frontend builder pages, not only in Django admin.</li>
<li><strong>Not fully bilingual yet:</strong> several generated PDF/business text blocks still remain primarily single-language.</li> <li><strong>Not fully bilingual yet:</strong> several generated PDF/business text blocks still remain primarily single-language.</li>
<li><strong>Implementation:</strong> Django i18n with locale middleware, translation catalogs, and a DE/EN language switch in the main UI.</li> <li><strong>Implementation:</strong> Django i18n with locale middleware, translation catalogs, and a DE/EN language switch in the main UI.</li>

View File

@@ -1,10 +1,10 @@
{% load static %} {% load static i18n %}
<!doctype html> <!doctype html>
<html lang="de"> <html lang="de">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Welcome E-Mails</title> <title>{% trans "Welcome E-Mails" %}</title>
<link rel="stylesheet" href="{% static 'workflows/css/buttons.css' %}" /> <link rel="stylesheet" href="{% static 'workflows/css/buttons.css' %}" />
<style> <style>
body { margin: 0; font-family: Arial, sans-serif; background: #f4f8ff; color: #0f172a; padding: 20px; } body { margin: 0; font-family: Arial, sans-serif; background: #f4f8ff; color: #0f172a; padding: 20px; }
@@ -45,10 +45,10 @@
<div class="shell"> <div class="shell">
<div class="topbar"> <div class="topbar">
<img class="brand-logo" src="{% static 'workflows/img/tubco-logo.svg' %}" alt="TUB/CO Logo" /> <img class="brand-logo" src="{% static 'workflows/img/tubco-logo.svg' %}" alt="TUB/CO Logo" />
<a class="btn btn-secondary" href="/">Zur Startseite</a> <a class="btn btn-secondary" href="/">{% trans "Zur Startseite" %}</a>
</div> </div>
<h1>Geplante Welcome E-Mails</h1> <h1>{% trans "Geplante Welcome E-Mails" %}</h1>
<p class="sub">Welcome-Mails konfigurieren und geplante Mails steuern (sofort senden, pausieren, fortsetzen, abbrechen).</p> <p class="sub">{% trans "Welcome-Mails konfigurieren und geplante Mails steuern (sofort senden, pausieren, fortsetzen, abbrechen)." %}</p>
{% if messages %} {% if messages %}
{% for message in messages %} {% for message in messages %}
@@ -60,12 +60,12 @@
{% csrf_token %} {% csrf_token %}
<div class="grid"> <div class="grid">
<div> <div>
<label for="welcome_email_delay_days">Verzögerung in Tagen</label> <label for="welcome_email_delay_days">{% trans "Verzögerung in Tagen" %}</label>
<input id="welcome_email_delay_days" name="welcome_email_delay_days" type="number" min="0" step="1" value="{{ workflow_config.welcome_email_delay_days }}" /> <input id="welcome_email_delay_days" name="welcome_email_delay_days" type="number" min="0" step="1" value="{{ workflow_config.welcome_email_delay_days }}" />
</div> </div>
<div> <div>
<label for="welcome_sender_email">Absenderadresse (optional)</label> <label for="welcome_sender_email">{% trans "Absenderadresse (optional)" %}</label>
<input id="welcome_sender_email" name="welcome_sender_email" type="email" value="{{ workflow_config.welcome_sender_email }}" placeholder="Leer = System-Absender" /> <input id="welcome_sender_email" name="welcome_sender_email" type="email" value="{{ workflow_config.welcome_sender_email }}" placeholder="{% trans 'Leer = System-Absender' %}" />
</div> </div>
<div> <div>
<label for="welcome_subject">Welcome Subject (DE)</label> <label for="welcome_subject">Welcome Subject (DE)</label>
@@ -85,16 +85,16 @@
</div> </div>
</div> </div>
<div class="check-row"> <div class="check-row">
<label><input type="checkbox" name="welcome_include_pdf" {% if workflow_config.welcome_include_pdf %}checked{% endif %} /> Onboarding-PDF anhängen</label> <label><input type="checkbox" name="welcome_include_pdf" {% if workflow_config.welcome_include_pdf %}checked{% endif %} /> {% trans "Onboarding-PDF anhängen" %}</label>
</div> </div>
<div class="hint"> <div class="hint">
Verfügbare Keywords: {% trans "Verfügbare Keywords:" %}
{% for key in welcome_keywords %} {% for key in welcome_keywords %}
<code>{{ key }}</code>{% if not forloop.last %}, {% endif %} <code>{{ key }}</code>{% if not forloop.last %}, {% endif %}
{% endfor %} {% endfor %}
</div> </div>
<div class="actions"> <div class="actions">
<button class="btn btn-primary" type="submit">Welcome-Einstellungen speichern</button> <button class="btn btn-primary" type="submit">{% trans "Welcome-Einstellungen speichern" %}</button>
</div> </div>
</form> </form>
@@ -102,29 +102,29 @@
{% csrf_token %} {% csrf_token %}
<label style="display:inline-flex; align-items:center; gap:6px; margin:0;"> <label style="display:inline-flex; align-items:center; gap:6px; margin:0;">
<input type="checkbox" id="select-all-welcome" /> <input type="checkbox" id="select-all-welcome" />
Alle auswählen {% trans "Alle auswählen" %}
</label> </label>
<select name="bulk_action" id="bulk_action"> <select name="bulk_action" id="bulk_action">
<option value="pause">Pausieren</option> <option value="pause">{% trans "Pausieren" %}</option>
<option value="send_now">Sofort senden</option> <option value="send_now">{% trans "Sofort senden" %}</option>
<option value="delete">Löschen</option> <option value="delete">{% trans "Löschen" %}</option>
</select> </select>
<input type="hidden" name="selected_ids" id="selected_ids" /> <input type="hidden" name="selected_ids" id="selected_ids" />
<button class="btn btn-secondary" type="submit">Bulk ausführen</button> <button class="btn btn-secondary" type="submit">{% trans "Bulk ausführen" %}</button>
<span class="bulk-note"><span id="selected-count">0</span> ausgewählt</span> <span class="bulk-note"><span id="selected-count">0</span> {% trans "ausgewählt" %}</span>
</form> </form>
<table> <table>
<thead> <thead>
<tr> <tr>
<th class="select-col">Auswahl</th> <th class="select-col">{% trans "Auswahl" %}</th>
<th>ID</th> <th>ID</th>
<th>Mitarbeitende Person</th> <th>{% trans "Mitarbeitende Person" %}</th>
<th>Empfänger</th> <th>{% trans "Empfänger" %}</th>
<th>Geplant für</th> <th>{% trans "Geplant für" %}</th>
<th>Status</th> <th>{% trans "Status" %}</th>
<th>Gesendet am</th> <th>{% trans "Gesendet am" %}</th>
<th>Aktion</th> <th>{% trans "Aktion" %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -137,15 +137,15 @@
<td>{{ row.send_at|date:"Y-m-d H:i" }}</td> <td>{{ row.send_at|date:"Y-m-d H:i" }}</td>
<td> <td>
{% if row.status == 'scheduled' %} {% if row.status == 'scheduled' %}
<span class="badge scheduled">Geplant</span> <span class="badge scheduled">{% trans "Geplant" %}</span>
{% elif row.status == 'paused' %} {% elif row.status == 'paused' %}
<span class="badge paused">Pausiert</span> <span class="badge paused">{% trans "Pausiert" %}</span>
{% elif row.status == 'cancelled' %} {% elif row.status == 'cancelled' %}
<span class="badge cancelled">Abgebrochen</span> <span class="badge cancelled">{% trans "Abgebrochen" %}</span>
{% elif row.status == 'sent' %} {% elif row.status == 'sent' %}
<span class="badge sent">Gesendet</span> <span class="badge sent">{% trans "Gesendet" %}</span>
{% else %} {% else %}
<span class="badge failed">Fehlgeschlagen</span> <span class="badge failed">{% trans "Fehlgeschlagen" %}</span>
{% endif %} {% endif %}
</td> </td>
<td>{% if row.sent_at %}{{ row.sent_at|date:"Y-m-d H:i" }}{% else %}-{% endif %}</td> <td>{% if row.sent_at %}{{ row.sent_at|date:"Y-m-d H:i" }}{% else %}-{% endif %}</td>
@@ -154,31 +154,31 @@
{% if row.status != 'sent' and row.status != 'cancelled' %} {% if row.status != 'sent' and row.status != 'cancelled' %}
<form method="post" action="/admin-tools/welcome-emails/{{ row.id }}/trigger-now/" style="display:inline;"> <form method="post" action="/admin-tools/welcome-emails/{{ row.id }}/trigger-now/" style="display:inline;">
{% csrf_token %} {% csrf_token %}
<button class="btn btn-secondary" type="submit">Sofort senden</button> <button class="btn btn-secondary" type="submit">{% trans "Sofort senden" %}</button>
</form> </form>
{% endif %} {% endif %}
{% if row.status == 'scheduled' %} {% if row.status == 'scheduled' %}
<form method="post" action="/admin-tools/welcome-emails/{{ row.id }}/pause/" style="display:inline;"> <form method="post" action="/admin-tools/welcome-emails/{{ row.id }}/pause/" style="display:inline;">
{% csrf_token %} {% csrf_token %}
<button class="btn btn-secondary" type="submit">Pausieren</button> <button class="btn btn-secondary" type="submit">{% trans "Pausieren" %}</button>
</form> </form>
{% elif row.status == 'paused' %} {% elif row.status == 'paused' %}
<form method="post" action="/admin-tools/welcome-emails/{{ row.id }}/resume/" style="display:inline;"> <form method="post" action="/admin-tools/welcome-emails/{{ row.id }}/resume/" style="display:inline;">
{% csrf_token %} {% csrf_token %}
<button class="btn btn-secondary" type="submit">Fortsetzen</button> <button class="btn btn-secondary" type="submit">{% trans "Fortsetzen" %}</button>
</form> </form>
{% endif %} {% endif %}
{% if row.status != 'sent' and row.status != 'cancelled' %} {% if row.status != 'sent' and row.status != 'cancelled' %}
<form method="post" action="/admin-tools/welcome-emails/{{ row.id }}/cancel/" style="display:inline;"> <form method="post" action="/admin-tools/welcome-emails/{{ row.id }}/cancel/" style="display:inline;">
{% csrf_token %} {% csrf_token %}
<button class="btn btn-secondary" type="submit">Abbrechen</button> <button class="btn btn-secondary" type="submit">{% trans "Abbrechen" %}</button>
</form> </form>
{% endif %} {% endif %}
</div> </div>
</td> </td>
</tr> </tr>
{% empty %} {% empty %}
<tr><td colspan="8">Keine geplanten Welcome E-Mails vorhanden.</td></tr> <tr><td colspan="8">{% trans "Keine geplanten Welcome E-Mails vorhanden." %}</td></tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
@@ -221,17 +221,17 @@
syncState(); syncState();
const count = currentSelected().length; const count = currentSelected().length;
if (!count) { if (!count) {
alert('Bitte mindestens einen Welcome-Eintrag auswählen.'); alert('{% trans "Bitte mindestens einen Welcome-Eintrag auswählen." %}');
return false; return false;
} }
const action = bulkAction.value; const action = bulkAction.value;
if (action === 'delete') { if (action === 'delete') {
return confirm('Ausgewählte Welcome-Einträge wirklich löschen?'); return confirm('{% trans "Ausgewählte Welcome-Einträge wirklich löschen?" %}');
} }
if (action === 'pause') { if (action === 'pause') {
return confirm('Ausgewählte Welcome-Einträge pausieren?'); return confirm('{% trans "Ausgewählte Welcome-Einträge pausieren?" %}');
} }
return confirm('Ausgewählte Welcome-Einträge sofort senden?'); return confirm('{% trans "Ausgewählte Welcome-Einträge sofort senden?" %}');
}; };
syncState(); syncState();