snapshot: preserve bilingual core UI and gettext workflow state

This commit is contained in:
Md Bayazid Bostame
2026-03-24 11:27:49 +01:00
parent 396bb058ad
commit 0f285aa2cf
13 changed files with 650 additions and 156 deletions

View File

@@ -1,10 +1,11 @@
{% load static %}
{% load static i18n %}
{% get_current_language as CURRENT_LANGUAGE %}
<!doctype html>
<html lang="de">
<html lang="{{ CURRENT_LANGUAGE }}">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Anmeldung</title>
<title>{% trans "Anmeldung" %}</title>
<link rel="stylesheet" href="{% static 'workflows/css/buttons.css' %}" />
<style>
body { margin: 0; font-family: Arial, sans-serif; min-height: 100vh; display: grid; place-items: center; background: linear-gradient(160deg, #eef6ff, #fff3f3); }
@@ -17,22 +18,34 @@
input { width: 100%; padding: 10px; box-sizing: border-box; border: 1px solid #cbd5e1; border-radius: 8px; }
.btn { width: 100%; }
.errorlist { color: #b91c1c; margin: 6px 0; }
.card-head { display:flex; justify-content:space-between; gap:12px; align-items:flex-start; }
.lang-switch { display:flex; gap:6px; }
.lang-btn { border:1px solid #d9e3f0; background:#f8fbff; color:#1f3a5f; border-radius:999px; padding:6px 10px; font-size:12px; font-weight:700; cursor:pointer; }
.lang-btn.active { background:#000078; border-color:#000078; color:#fff; }
</style>
</head>
<body>
<div class="card">
<img class="logo" src="https://tub.co/media/site/0856bfc615-1750234287/tubco-wortbildmarke.svg" alt="TUB/CO Logo" />
<h1>Anmeldung</h1>
<p>Bitte melden Sie sich mit Ihrem Benutzerkonto an.</p>
<div class="card-head">
<img class="logo" src="https://tub.co/media/site/0856bfc615-1750234287/tubco-wortbildmarke.svg" alt="TUB/CO Logo" />
<form method="post" action="{% url 'set_language' %}" class="lang-switch">
{% csrf_token %}
<input type="hidden" name="next" value="{{ request.get_full_path }}" />
<button class="lang-btn {% if CURRENT_LANGUAGE == 'de' %}active{% endif %}" type="submit" name="language" value="de">DE</button>
<button class="lang-btn {% if CURRENT_LANGUAGE == 'en' %}active{% endif %}" type="submit" name="language" value="en">EN</button>
</form>
</div>
<h1>{% trans "Anmeldung" %}</h1>
<p>{% trans "Bitte melden Sie sich mit Ihrem Benutzerkonto an." %}</p>
<form method="post" action="/accounts/login/">
{% csrf_token %}
{% if form.errors %}
<div class="errorlist">Anmeldung fehlgeschlagen. Bitte Zugangsdaten prüfen.</div>
<div class="errorlist">{% trans "Anmeldung fehlgeschlagen. Bitte Zugangsdaten prüfen." %}</div>
{% endif %}
<div class="field">{{ form.username.label_tag }}{{ form.username }}</div>
<div class="field">{{ form.password.label_tag }}{{ form.password }}</div>
<button class="btn btn-primary" type="submit">Anmelden</button>
<button class="btn btn-primary" type="submit">{% trans "Anmelden" %}</button>
</form>
</div>
</body>

View File

@@ -1,10 +1,11 @@
{% load static %}
{% load static i18n %}
{% get_current_language as CURRENT_LANGUAGE %}
<!doctype html>
<html lang="de">
<html lang="{{ CURRENT_LANGUAGE }}">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>TUBCO Onboarding & Offboarding Portal</title>
<title>{% trans "TUBCO Onboarding & Offboarding Portal" %}</title>
<link rel="stylesheet" href="{% static 'workflows/css/buttons.css' %}" />
<style>
:root {
@@ -68,7 +69,11 @@
gap: 8px;
flex-wrap: wrap;
justify-content: flex-end;
align-items: center;
}
.lang-switch { display:flex; gap:6px; }
.lang-btn { border:1px solid var(--line); background:#f8fbff; color:#1f3a5f; border-radius:999px; padding:6px 10px; font-size:12px; font-weight:700; cursor:pointer; }
.lang-btn.active { background:var(--brand-blue); border-color:var(--brand-blue); color:#fff; }
.hero {
padding: 24px;
@@ -433,9 +438,15 @@
<div class="topbar">
<img class="brand-logo" src="https://tub.co/media/site/0856bfc615-1750234287/tubco-wortbildmarke.svg" alt="TUB/CO Logo" />
<div class="quick-actions">
<form method="post" action="{% url 'set_language' %}" class="lang-switch">
{% csrf_token %}
<input type="hidden" name="next" value="{{ request.get_full_path }}" />
<button class="lang-btn {% if CURRENT_LANGUAGE == 'de' %}active{% endif %}" type="submit" name="language" value="de">DE</button>
<button class="lang-btn {% if CURRENT_LANGUAGE == 'en' %}active{% endif %}" type="submit" name="language" value="en">EN</button>
</form>
<form method="post" action="/accounts/logout/" style="display:inline;">
{% csrf_token %}
<button class="btn btn-secondary" type="submit">Abmelden</button>
<button class="btn btn-secondary" type="submit">{% trans "Abmelden" %}</button>
</form>
</div>
</div>
@@ -443,18 +454,18 @@
<div class="hero">
<div class="hero-grid">
<div class="hero-card">
<span class="eyebrow">Operations Console</span>
<h1>TUBCO Onboarding & Offboarding Portal</h1>
<p>Zentrale Arbeitsfläche für Anfragen, PDF-Generierung, E-Mail-Workflows und Ablage in Nextcloud.</p>
<span class="eyebrow">{% trans "Operations Console" %}</span>
<h1>{% trans "TUBCO Onboarding & Offboarding Portal" %}</h1>
<p>{% trans "Zentrale Arbeitsfläche für Anfragen, PDF-Generierung, E-Mail-Workflows und Ablage in Nextcloud." %}</p>
<div class="status-row">
<span class="status-pill">Rolle: {% if request.user.is_staff %}Admin{% else %}Mitarbeiter{% endif %}</span>
<span class="status-pill">{% trans "Rolle:" %} {% if request.user.is_staff %}{% trans "Admin" %}{% else %}{% trans "Mitarbeiter" %}{% endif %}</span>
<span class="status-pill {% if nextcloud_enabled %}ok{% else %}warn{% endif %}">
Nextcloud: {% if nextcloud_enabled %}aktiv{% else %}inaktiv{% endif %}
{% trans "Nextcloud:" %} {% if nextcloud_enabled %}{% trans "aktiv" %}{% else %}{% trans "inaktiv" %}{% endif %}
</span>
<span class="status-pill {% if email_test_mode %}warn{% else %}ok{% endif %}">
E-Mail: {% if email_test_mode %}Testmodus{% else %}Produktion{% endif %}
{% trans "E-Mail:" %} {% if email_test_mode %}{% trans "Testmodus" %}{% else %}{% trans "Produktion" %}{% endif %}
</span>
<span class="status-pill">PDF + Email Workflow Ready</span>
<span class="status-pill">{% trans "PDF + E-Mail Workflow bereit" %}</span>
</div>
</div>
</div>
@@ -468,94 +479,94 @@
{% endif %}
<div class="section-head">
<h2>Apps</h2>
<p>Wählen Sie den gewünschten Prozess.</p>
<h2>{% trans "Apps" %}</h2>
<p>{% trans "Wählen Sie den gewünschten Prozess." %}</p>
</div>
<div class="apps-grid">
<section class="app-card primary">
<div>
<div class="top-line"><div class="accent">ON</div></div>
<h3 class="app-title">Onboarding</h3>
<p class="app-text">Neue Mitarbeitende erfassen, PDF mit Briefkopf erstellen, Benachrichtigungen senden und in Nextcloud ablegen.</p>
<h3 class="app-title">{% trans "Onboarding" %}</h3>
<p class="app-text">{% trans "Neue Mitarbeitende erfassen, PDF mit Briefkopf erstellen, Benachrichtigungen senden und in Nextcloud ablegen." %}</p>
<div class="tag-row">
<span class="tag">Mehrschritt-Formular</span>
<span class="tag">{% trans "Mehrschritt-Formular" %}</span>
<span class="tag">PDF</span>
<span class="tag">E-Mail Routing</span>
<span class="tag">{% trans "E-Mail Routing" %}</span>
</div>
</div>
<div class="card-actions">
<a class="btn btn-primary" href="/onboarding/new/">Onboarding starten</a>
<a class="btn btn-primary" href="/onboarding/new/">{% trans "Onboarding starten" %}</a>
</div>
</section>
<section class="app-card red">
<div>
<div class="top-line"><div class="accent red">OFF</div></div>
<h3 class="app-title">Offboarding</h3>
<p class="app-text">Mitarbeitende suchen, Daten vorbefüllen, Offboarding-Dokumente erzeugen und Rückgabe-Prozess starten.</p>
<h3 class="app-title">{% trans "Offboarding" %}</h3>
<p class="app-text">{% trans "Mitarbeitende suchen, Daten vorbefüllen, Offboarding-Dokumente erzeugen und Rückgabe-Prozess starten." %}</p>
<div class="tag-row">
<span class="tag">Profile-Suche</span>
<span class="tag">Hardware-Liste</span>
<span class="tag">IT-Rückgabe</span>
<span class="tag">{% trans "Profile-Suche" %}</span>
<span class="tag">{% trans "Hardware-Liste" %}</span>
<span class="tag">{% trans "IT-Rückgabe" %}</span>
</div>
</div>
<div class="card-actions">
<a class="btn btn-primary" href="/offboarding/new/">Offboarding starten</a>
<a class="btn btn-primary" href="/offboarding/new/">{% trans "Offboarding starten" %}</a>
</div>
</section>
<section class="app-card">
<div>
<div class="top-line"><div class="accent">APP</div></div>
<h3 class="app-title">Anfragen Dashboard</h3>
<p class="app-text">Status, Suchfunktion, PDF-Links und Verlauf aller Onboarding-/Offboarding-Anfragen.</p>
<h3 class="app-title">{% trans "Anfragen Dashboard" %}</h3>
<p class="app-text">{% trans "Status, Suchfunktion, PDF-Links und Verlauf aller Onboarding-/Offboarding-Anfragen." %}</p>
<div class="tag-row">
<span class="tag">Suche</span>
<span class="tag">Status</span>
<span class="tag">PDF Zugriff</span>
<span class="tag">{% trans "Suche" %}</span>
<span class="tag">{% trans "Status" %}</span>
<span class="tag">{% trans "PDF Zugriff" %}</span>
</div>
</div>
<div class="card-actions">
<a class="btn btn-secondary" href="/requests/">Dashboard öffnen</a>
<a class="btn btn-secondary" href="/requests/">{% trans "Dashboard öffnen" %}</a>
</div>
</section>
</div>
{% if request.user.is_staff %}
<div class="section-head">
<h2>Admin Apps</h2>
<p>Konfiguration, Tests und Steuerung.</p>
<h2>{% trans "Admin Apps" %}</h2>
<p>{% trans "Konfiguration, Tests und Steuerung." %}</p>
</div>
<div class="admin-grid">
<section class="admin-card">
<h3>Form Builder</h3>
<p>Felder, Schritte und Optionen verwalten.</p>
<a class="btn btn-secondary" href="/admin-tools/form-builder/">Öffnen</a>
<h3>{% trans "Form Builder" %}</h3>
<p>{% trans "Felder, Schritte und Optionen verwalten." %}</p>
<a class="btn btn-secondary" href="/admin-tools/form-builder/">{% trans "Öffnen" %}</a>
</section>
<section class="admin-card">
<h3>Einweisungs-Builder</h3>
<p>Checklistenpunkte für das Einweisungsprotokoll konfigurieren.</p>
<a class="btn btn-secondary" href="/admin-tools/intro-builder/">Öffnen</a>
<h3>{% trans "Einweisungs-Builder" %}</h3>
<p>{% trans "Checklistenpunkte für das Einweisungsprotokoll konfigurieren." %}</p>
<a class="btn btn-secondary" href="/admin-tools/intro-builder/">{% trans "Öffnen" %}</a>
</section>
<section class="admin-card">
<h3>Projekt Wiki</h3>
<p>Dokumentation, Architektur und Runbook.</p>
<a class="btn btn-secondary" href="/admin-tools/wiki/">Öffnen</a>
<h3>{% trans "Projekt Wiki" %}</h3>
<p>{% trans "Dokumentation, Architektur und Runbook." %}</p>
<a class="btn btn-secondary" href="/admin-tools/wiki/">{% trans "Öffnen" %}</a>
</section>
<section class="admin-card">
<h3>Integrationen</h3>
<p>Nextcloud- und E-Mail-Setup.</p>
<a class="btn btn-secondary" href="/admin-tools/integrations/?kind=nextcloud">Öffnen</a>
<h3>{% trans "Integrationen" %}</h3>
<p>{% trans "Nextcloud- und E-Mail-Setup." %}</p>
<a class="btn btn-secondary" href="/admin-tools/integrations/?kind=nextcloud">{% trans "Öffnen" %}</a>
</section>
<section class="admin-card">
<h3>Welcome E-Mails</h3>
<p>Geplante Welcome Mails verwalten.</p>
<a class="btn btn-secondary" href="/admin-tools/welcome-emails/">Öffnen</a>
<h3>{% trans "Welcome E-Mails" %}</h3>
<p>{% trans "Geplante Welcome Mails verwalten." %}</p>
<a class="btn btn-secondary" href="/admin-tools/welcome-emails/">{% trans "Öffnen" %}</a>
</section>
<section class="admin-card">
<h3>Django Admin</h3>
<p>Vollständige Datenverwaltung.</p>
<a class="btn btn-secondary" href="/admin/">Öffnen</a>
<h3>{% trans "Django Admin" %}</h3>
<p>{% trans "Vollständige Datenverwaltung." %}</p>
<a class="btn btn-secondary" href="/admin/">{% trans "Öffnen" %}</a>
</section>
<section class="admin-card">
<h3>SMTP Einstellungen</h3>
@@ -600,7 +611,7 @@
{% endif %}
<div class="footer-note">
Tipp: Die letzten Vorgänge sehen Sie jederzeit im Anfragen Dashboard.
{% trans "Tipp: Die letzten Vorgänge sehen Sie jederzeit im Anfragen Dashboard." %}
</div>
</main>
</div>

View File

@@ -1,33 +1,41 @@
{% load static %}
{% load static i18n %}
{% get_current_language as CURRENT_LANGUAGE %}
<!doctype html>
<html lang="de">
<html lang="{{ CURRENT_LANGUAGE }}">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Offboarding-Anfrage</title>
<title>{% trans "Offboarding-Anfrage" %}</title>
<link rel="stylesheet" href="{% static 'workflows/css/buttons.css' %}" />
<link rel="stylesheet" href="{% static 'workflows/css/offboarding_form.css' %}" />
</head>
<body>
<div id="saved-popup" class="popup-backdrop {% if saved %}show{% endif %}">
<div class="popup">
<h3>Anfrage gespeichert</h3>
<p>Offboarding wurde erfolgreich gespeichert (ID: {{ saved_request_id }}). Das PDF wird im Hintergrund erzeugt.</p>
<button class="btn btn-secondary" type="button" onclick="document.getElementById('saved-popup').classList.remove('show')">Schließen</button>
<h3>{% trans "Anfrage gespeichert" %}</h3>
<p>{% blocktrans trimmed with request_id=saved_request_id %}Offboarding wurde erfolgreich gespeichert (ID: {{ request_id }}). Das PDF wird im Hintergrund erzeugt.{% endblocktrans %}</p>
<button class="btn btn-secondary" type="button" onclick="document.getElementById('saved-popup').classList.remove('show')">{% trans "Schließen" %}</button>
</div>
</div>
<div class="wrap">
<img class="brand-logo" src="https://tub.co/media/site/0856bfc615-1750234287/tubco-wortbildmarke.svg" alt="TUB/CO Logo" />
<div class="top-link"><a class="btn btn-secondary" href="/">Zur Startseite</a></div>
<div class="top-link" style="display:flex; gap:8px; align-items:center;">
<form method="post" action="{% url 'set_language' %}" class="lang-switch" style="display:flex; gap:6px;">
{% csrf_token %}
<input type="hidden" name="next" value="{{ request.get_full_path }}" />
<button class="btn btn-secondary" type="submit" name="language" value="de">DE</button>
<button class="btn btn-secondary" type="submit" name="language" value="en">EN</button>
</form>
<a class="btn btn-secondary" href="/">{% trans "Zur Startseite" %}</a></div>
<div class="card">
<h1>Offboarding-Anfrage</h1>
<h1>{% trans "Offboarding-Anfrage" %}</h1>
<form method="get" action="/offboarding/new/">
<div class="field">
<label for="q">Mitarbeitende suchen (Name oder E-Mail)</label>
<input id="q" name="q" value="{{ search_query }}" placeholder="z. B. max.mustermann@tub.co" />
<label for="q">{% trans "Mitarbeitende suchen (Name oder E-Mail)" %}</label>
<input id="q" name="q" value="{{ search_query }}" placeholder="{% trans "z. B. max.mustermann@tub.co" %}" />
</div>
<button class="btn btn-primary" type="submit">Suchen</button>
<button class="btn btn-primary" type="submit">{% trans "Suchen" %}</button>
</form>
{% if search_results %}
@@ -39,7 +47,7 @@
{% endif %}
{% if selected_profile %}
<p style="margin-top:10px; color:#2563eb;">Vorbefüllt aus: <strong>{{ selected_profile.full_name }}</strong> ({{ selected_profile.work_email }})</p>
<p style="margin-top:10px; color:#2563eb;">{% trans "Vorbefüllt aus:" %} <strong>{{ selected_profile.full_name }}</strong> ({{ selected_profile.work_email }})</p>
{% endif %}
</div>
@@ -58,7 +66,7 @@
{% endif %}
{% endfor %}
</div>
<button class="btn btn-primary" type="submit">Offboarding-Anfrage speichern</button>
<button class="btn btn-primary" type="submit">{% trans "Offboarding-Anfrage speichern" %}</button>
</form>
</div>
</div>

View File

@@ -1,31 +1,40 @@
{% load static %}
{% load static i18n %}
{% get_current_language as CURRENT_LANGUAGE %}
<!doctype html>
<html lang="de">
<html lang="{{ CURRENT_LANGUAGE }}">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Onboarding-Anfrage</title>
<title>{% trans "Onboarding-Anfrage" %}</title>
<link rel="stylesheet" href="{% static 'workflows/css/buttons.css' %}" />
<link rel="stylesheet" href="{% static 'workflows/css/onboarding_form.css' %}" />
</head>
<body>
<div id="saved-popup" class="popup-backdrop {% if saved %}show{% endif %}">
<div class="popup">
<h3>Anfrage gespeichert</h3>
<p>Onboarding wurde erfolgreich gespeichert (ID: {{ saved_request_id }}). Das PDF wird im Hintergrund erzeugt.</p>
<button class="btn btn-secondary" type="button" onclick="document.getElementById('saved-popup').classList.remove('show')">Schließen</button>
<h3>{% trans "Anfrage gespeichert" %}</h3>
<p>{% blocktrans trimmed with request_id=saved_request_id %}Onboarding wurde erfolgreich gespeichert (ID: {{ request_id }}). Das PDF wird im Hintergrund erzeugt.{% endblocktrans %}</p>
<button class="btn btn-secondary" type="button" onclick="document.getElementById('saved-popup').classList.remove('show')">{% trans "Schließen" %}</button>
</div>
</div>
<div class="top-wrap">
<img class="brand-logo" src="https://tub.co/media/site/0856bfc615-1750234287/tubco-wortbildmarke.svg" alt="TUB/CO Logo" />
<div class="top-link"><a class="btn btn-secondary" href="/">Zur Startseite</a></div>
<div class="top-link" style="display:flex; gap:8px; align-items:center;">
<form method="post" action="{% url 'set_language' %}" class="lang-switch" style="display:flex; gap:6px;">
{% csrf_token %}
<input type="hidden" name="next" value="{{ request.get_full_path }}" />
<button class="btn btn-secondary" type="submit" name="language" value="de">DE</button>
<button class="btn btn-secondary" type="submit" name="language" value="en">EN</button>
</form>
<a class="btn btn-secondary" href="/">{% trans "Zur Startseite" %}</a>
</div>
</div>
<div class="shell">
<aside class="panel">
<h1>Onboarding</h1>
<p class="sub">Mehrseitiges Formular mit konfigurierbaren Feldern aus dem Admin.</p>
<h1>{% trans "Onboarding" %}</h1>
<p class="sub">{% trans "Mehrseitiges Formular mit konfigurierbaren Feldern aus dem Admin." %}</p>
<ol class="step-list">
{% for section in onboarding_sections %}
<li class="step-item {% if forloop.first %}active{% endif %}" data-nav-step="{{ forloop.counter }}" role="button" tabindex="0" aria-label="{{ section.title }}">
@@ -41,7 +50,7 @@
<main class="main">
{% if form.errors %}
<div class="error-banner">Bitte prüfen Sie die markierten Felder. Ungültige Eingaben wurden erkannt.</div>
<div class="error-banner">{% trans "Bitte prüfen Sie die markierten Felder. Ungültige Eingaben wurden erkannt." %}</div>
{% endif %}
<form method="post" id="onboarding-form" enctype="multipart/form-data">
@@ -101,12 +110,12 @@
{% endif %}
{% endfor %}
{% if not section.blocks %}
<div class="field field-full empty-step">Keine konfigurierten Felder in diesem Schritt.</div>
<div class="field field-full empty-step">{% trans "Keine konfigurierten Felder in diesem Schritt." %}</div>
{% endif %}
{% if section.key == 'abschluss' %}
<div class="field-full finish-note">
Fast geschafft. Bitte Abschlussdaten prüfen und die Anfrage absenden.
{% trans "Fast geschafft. Bitte Abschlussdaten prüfen und die Anfrage absenden." %}
</div>
<div class="field-full">
<div class="legal">{{ legal_text }}</div>
@@ -118,9 +127,9 @@
{% endfor %}
<div class="actions">
<button class="btn btn-secondary" type="button" id="btn-prev">Zurück</button>
<button class="btn btn-primary" type="button" id="btn-next">Weiter</button>
<button type="submit" id="btn-submit" class="btn btn-primary hidden">Onboarding-Anfrage absenden</button>
<button class="btn btn-secondary" type="button" id="btn-prev">{% trans "Zurück" %}</button>
<button class="btn btn-primary" type="button" id="btn-next">{% trans "Weiter" %}</button>
<button type="submit" id="btn-submit" class="btn btn-primary hidden">{% trans "Onboarding-Anfrage absenden" %}</button>
</div>
</form>
</main>

View File

@@ -161,6 +161,22 @@
<li>Use SMTP test action before switching to production mode.</li>
</ul>
<h2 id="bilingual">8b) Bilingual Core UI</h2>
<ul>
<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>Not fully bilingual yet:</strong> dynamic Form Builder content, intro-builder item labels, admin-configured email templates, and most generated PDF/business text 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>
</ul>
<h3>Translation Workflow</h3>
<ul>
<li>The long-term translation path uses Django's standard <code>makemessages</code> and <code>compilemessages</code> workflow.</li>
<li><code>gettext</code> is installed in the Docker image so translations can be compiled inside the running container.</li>
<li>Translation catalogs live under <code>/backend/locale/</code>.</li>
<li>The earlier ad hoc Python-based <code>.mo</code> compilation path should no longer be used for ongoing maintenance.</li>
</ul>
<h2 id="admin">9) Admin Apps (Home)</h2>
<ul>
<li><strong>Form Builder:</strong> manage field visibility/order/options.</li>

View File

@@ -1,10 +1,11 @@
{% load static %}
{% load static i18n %}
{% get_current_language as CURRENT_LANGUAGE %}
<!doctype html>
<html lang="de">
<html lang="{{ CURRENT_LANGUAGE }}">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Anfragen Dashboard</title>
<title>{% trans "Anfragen Dashboard" %}</title>
<link rel="stylesheet" href="{% static 'workflows/css/buttons.css' %}" />
<style>
:root {
@@ -81,7 +82,11 @@
gap: 8px;
flex-wrap: wrap;
justify-content: flex-end;
align-items: center;
}
.lang-switch { display:flex; gap:6px; }
.lang-btn { border:1px solid var(--line); background:#f8fbff; color:#1f3a5f; border-radius:999px; padding:6px 10px; font-size:12px; font-weight:700; cursor:pointer; }
.lang-btn.active { background:var(--brand-blue); border-color:var(--brand-blue); color:#fff; }
.hero {
padding: 0;
@@ -875,7 +880,13 @@
<img class="brand-logo" src="https://tub.co/media/site/0856bfc615-1750234287/tubco-wortbildmarke.svg" alt="TUB/CO Logo" />
</div>
<div class="quick-actions">
<a class="btn btn-secondary" href="/">Zur Startseite</a>
<form method="post" action="{% url 'set_language' %}" class="lang-switch">
{% csrf_token %}
<input type="hidden" name="next" value="{{ request.get_full_path }}" />
<button class="lang-btn {% if CURRENT_LANGUAGE == 'de' %}active{% endif %}" type="submit" name="language" value="de">DE</button>
<button class="lang-btn {% if CURRENT_LANGUAGE == 'en' %}active{% endif %}" type="submit" name="language" value="en">EN</button>
</form>
<a class="btn btn-secondary" href="/">{% trans "Zur Startseite" %}</a>
</div>
</div>
@@ -883,22 +894,22 @@
<section class="hero">
<div class="hero-grid">
<div class="hero-card">
<span class="eyebrow">Operations Console</span>
<h1>Anfragen Dashboard</h1>
<p>Steuert Onboarding- und Offboarding-Prozesse an einem Ort. Die Oberfläche priorisiert Kennzahlen, Aktivität und direkte Aktionen in der Vorgangsliste.</p>
<span class="eyebrow">{% trans "Operations Console" %}</span>
<h1>{% trans "Anfragen Dashboard" %}</h1>
<p>{% trans "Steuert Onboarding- und Offboarding-Prozesse an einem Ort. Die Oberfläche priorisiert Kennzahlen, Aktivität und direkte Aktionen in der Vorgangsliste." %}</p>
<div class="hero-pills">
<span class="hero-pill">Onboarding + Offboarding</span>
<span class="hero-pill">PDFs + Live-Protokolle</span>
<span class="hero-pill">Suche + Bulk-Aktionen</span>
<span class="hero-pill">{% trans "Onboarding + Offboarding" %}</span>
<span class="hero-pill">{% trans "PDFs + Live-Protokolle" %}</span>
<span class="hero-pill">{% trans "Suche + Bulk-Aktionen" %}</span>
</div>
</div>
<article class="chart-card activity-card">
<div class="section-head">
<div>
<h2>Aktivitätsverlauf</h2>
<p>Die letzten 14 Tage in einer kompakten Ansicht über alle Onboarding- und Offboarding-Vorgänge.</p>
<h2>{% trans "Aktivitätsverlauf" %}</h2>
<p>{% trans "Die letzten 14 Tage in einer kompakten Ansicht über alle Onboarding- und Offboarding-Vorgänge." %}</p>
</div>
<span class="eyebrow">14 Tage</span>
<span class="eyebrow">{% trans "14 Tage" %}</span>
</div>
<div class="chart" aria-label="Aktivitätsverlauf der letzten 14 Tage">
{% for point in chart_points %}
@@ -919,42 +930,42 @@
<article class="stat-card">
<div class="stat-head">
<div>
<p class="stat-title">Onboarding</p>
<p class="stat-title">{% trans "Onboarding" %}</p>
<div class="stat-value">{{ onboarding_total }}</div>
</div>
<div class="stat-kicker">ON</div>
</div>
<div class="stat-foot">Alle erfassten Onboarding-Vorgänge im aktuellen System.</div>
<div class="stat-foot">{% trans "Alle erfassten Onboarding-Vorgänge im aktuellen System." %}</div>
</article>
<article class="stat-card red">
<div class="stat-head">
<div>
<p class="stat-title">Offboarding</p>
<p class="stat-title">{% trans "Offboarding" %}</p>
<div class="stat-value">{{ offboarding_total }}</div>
</div>
<div class="stat-kicker">OFF</div>
</div>
<div class="stat-foot">Austritte und Rückgaben in derselben Prozessübersicht.</div>
<div class="stat-foot">{% trans "Austritte und Rückgaben in derselben Prozessübersicht." %}</div>
</article>
<article class="stat-card gold">
<div class="stat-head">
<div>
<p class="stat-title">Gesamtbestand</p>
<p class="stat-title">{% trans "Gesamtbestand" %}</p>
<div class="stat-value">{{ combined_total }}</div>
</div>
<div class="stat-kicker">Σ</div>
</div>
<div class="stat-foot">Alle Vorgänge, durchsuchbar und mit Dokumenten verknüpft.</div>
<div class="stat-foot">{% trans "Alle Vorgänge, durchsuchbar und mit Dokumenten verknüpft." %}</div>
</article>
<article class="stat-card ink">
<div class="stat-head">
<div>
<p class="stat-title">Aktivität 14 Tage</p>
<p class="stat-title">{% trans "Aktivität 14 Tage" %}</p>
<div class="stat-value">{{ chart_points|length }}</div>
</div>
<div class="stat-kicker">D</div>
</div>
<div class="stat-foot">Zeitraum des visuellen Aktivitätsverlaufs in dieser Übersicht.</div>
<div class="stat-foot">{% trans "Zeitraum des visuellen Aktivitätsverlaufs in dieser Übersicht." %}</div>
</article>
</section>
</section>
@@ -969,34 +980,34 @@
<section class="table-card">
<div class="table-head">
<div>
<h2>Vorgänge</h2>
<p>Dokumente, Status und Einweisungsaktionen in einer verdichteten Arbeitsansicht.</p>
<h2>{% trans "Vorgänge" %}</h2>
<p>{% trans "Dokumente, Status und Einweisungsaktionen in einer verdichteten Arbeitsansicht." %}</p>
</div>
<div class="table-head-meta">{{ rows|length }} Einträge sichtbar</div>
<div class="table-head-meta">{% blocktrans with count=rows|length trimmed %}{{ count }} Einträge sichtbar{% endblocktrans %}</div>
</div>
<div class="table-controls">
<div class="table-controls-grid">
<div class="control-stack">
<form method="get" action="/requests/" class="search-form">
<div class="search-box">
<input type="search" name="q" value="{{ search_query }}" placeholder="Nach Name oder E-Mail suchen" aria-label="Nach Name oder E-Mail suchen" />
<input type="search" name="q" value="{{ search_query }}" placeholder="{% trans "Nach Name oder E-Mail suchen" %}" aria-label="{% trans "Nach Name oder E-Mail suchen" %}" />
</div>
<div class="intro-actions">
<button class="btn btn-primary" type="submit">Suchen</button>
<button class="btn btn-primary" type="submit">{% trans "Suchen" %}</button>
{% if search_query %}
<a class="btn btn-secondary" href="/requests/">Zurücksetzen</a>
<a class="btn btn-secondary" href="/requests/">{% trans "Zurücksetzen" %}</a>
{% endif %}
</div>
</form>
<div class="search-help">Datensätze können direkt in der Tabelle gefiltert, geöffnet, geprüft oder gelöscht werden.</div>
<div class="search-help">{% trans "Datensätze können direkt in der Tabelle gefiltert, geöffnet, geprüft oder gelöscht werden." %}</div>
</div>
{% if request.user.is_staff %}
<div class="control-stack">
<form method="post" action="/requests/" id="bulk-delete-form" onsubmit="return confirm('Ausgewählte Einträge wirklich löschen?');">
{% csrf_token %}
<div class="bulk-toolbar">
<span class="bulk-info"><span id="selected-count">0</span> ausgewählt</span>
<button class="btn btn-secondary" type="submit">Auswahl löschen</button>
<span class="bulk-info"><span id="selected-count">0</span> {% trans "ausgewählt" %}</span>
<button class="btn btn-secondary" type="submit">{% trans "Auswahl löschen" %}</button>
</div>
</form>
</div>
@@ -1008,13 +1019,13 @@
<thead>
<tr>
{% if request.user.is_staff %}<th class="select-col"><input type="checkbox" id="select-all" aria-label="Alle auswählen" /></th>{% endif %}
<th>Typ</th>
<th>Person</th>
<th>E-Mail</th>
<th>Erstellt</th>
<th>Dokument</th>
{% if request.user.is_staff %}<th>Einweisung</th>{% endif %}
{% if request.user.is_staff %}<th>Aktion</th>{% endif %}
<th>{% trans "Typ" %}</th>
<th>{% trans "Person" %}</th>
<th>{% trans "E-Mail" %}</th>
<th>{% trans "Erstellt" %}</th>
<th>{% trans "Dokument" %}</th>
{% if request.user.is_staff %}<th>{% trans "Einweisung" %}</th>{% endif %}
{% if request.user.is_staff %}<th>{% trans "Aktion" %}</th>{% endif %}
</tr>
</thead>
<tbody>
@@ -1039,63 +1050,63 @@
<td>{{ row.created_at|date:"Y-m-d H:i" }}</td>
<td>
{% if row.pdf_url %}
<a class="doc-link" href="{{ row.pdf_url }}" target="_blank" rel="noopener">PDF öffnen</a>
<a class="doc-link" href="{{ row.pdf_url }}" target="_blank" rel="noopener">{% trans "PDF öffnen" %}</a>
{% else %}
<span class="person-meta">Noch nicht verfügbar</span>
<span class="person-meta">{% trans "Noch nicht verfügbar" %}</span>
{% endif %}
</td>
{% if request.user.is_staff %}
<td class="actions-cell intro-panel">
{% if row.kind_slug == 'onboarding' %}
<details>
<summary class="intro-toggle">Einweisung</summary>
<summary class="intro-toggle">{% trans "Einweisung" %}</summary>
<div class="intro-menu">
<div class="intro-group">
<div class="intro-group-title">Live-Protokoll</div>
<div class="intro-group-title">{% trans "Live-Protokoll" %}</div>
<div class="intro-actions">
<a class="btn btn-secondary" href="/requests/onboarding/{{ row.id }}/intro-session/">Einweisung öffnen</a>
<a class="btn btn-secondary" href="/requests/onboarding/{{ row.id }}/intro-session/">{% trans "Einweisung öffnen" %}</a>
{% if row.intro_session and row.intro_session.exported_pdf_url %}
<a class="btn btn-secondary" href="{{ row.intro_session.exported_pdf_url }}" target="_blank" rel="noopener">Live-Protokoll öffnen</a>
<a class="btn btn-secondary" href="{{ row.intro_session.exported_pdf_url }}" target="_blank" rel="noopener">{% trans "Live-Protokoll öffnen" %}</a>
{% endif %}
</div>
</div>
<div class="intro-group">
<div class="intro-group-title">Standard-Einweisungs-PDF</div>
<div class="intro-group-title">{% trans "Standard-Einweisungs-PDF" %}</div>
<div class="intro-actions">
{% if row.intro_pdf_url %}
<form method="post" action="/requests/onboarding/{{ row.id }}/intro-pdf/generate/" class="inline-form">
{% csrf_token %}
<button class="btn btn-secondary" type="submit">Neu erzeugen</button>
<button class="btn btn-secondary" type="submit">{% trans "Neu erzeugen" %}</button>
</form>
<a class="btn btn-secondary" href="{{ row.intro_pdf_url }}" target="_blank" rel="noopener">Standard-PDF öffnen</a>
<a class="btn btn-secondary" href="{{ row.intro_pdf_url }}" target="_blank" rel="noopener">{% trans "Standard-PDF öffnen" %}</a>
{% else %}
<form method="post" action="/requests/onboarding/{{ row.id }}/intro-pdf/generate/" class="inline-form">
{% csrf_token %}
<button class="btn btn-secondary" type="submit">PDF erzeugen</button>
<button class="btn btn-secondary" type="submit">{% trans "PDF erzeugen" %}</button>
</form>
{% endif %}
</div>
</div>
</div>
{% if row.intro_session %}
<div class="intro-meta">Status: {{ row.intro_session.get_status_display }}</div>
<div class="intro-meta">{% trans "Status:" %} {{ row.intro_session.get_status_display }}</div>
{% endif %}
</details>
{% else %}
<span class="person-meta">Nicht relevant</span>
<span class="person-meta">{% trans "Nicht relevant" %}</span>
{% endif %}
</td>
<td class="actions-cell">
<form method="post" action="/requests/" class="inline-delete" onsubmit="return confirm('Eintrag wirklich löschen?');">
{% csrf_token %}
<button class="btn btn-secondary" type="submit" name="single_delete" value="{{ row.kind_slug }}:{{ row.id }}">Löschen</button>
<button class="btn btn-secondary" type="submit" name="single_delete" value="{{ row.kind_slug }}:{{ row.id }}">{% trans "Löschen" %}</button>
</form>
</td>
{% endif %}
</tr>
{% empty %}
<tr>
<td colspan="{% if request.user.is_staff %}8{% else %}5{% endif %}" class="empty-state">Noch keine Vorgänge vorhanden.</td>
<td colspan="{% if request.user.is_staff %}8{% else %}5{% endif %}" class="empty-state">{% trans "Noch keine Vorgänge vorhanden." %}</td>
</tr>
{% endfor %}
</tbody>
@@ -1104,8 +1115,8 @@
</section>
</section>
<div class="footer-bar">
<div class="footer-note">TUBCO Onboarding & Offboarding Portal</div>
<a class="btn btn-secondary" href="/">Zur Startseite</a>
<div class="footer-note">{% trans "TUBCO Onboarding & Offboarding Portal" %}</div>
<a class="btn btn-secondary" href="/">{% trans "Zur Startseite" %}</a>
</div>
</div>
{% if request.user.is_staff %}

View File

@@ -15,6 +15,7 @@ from django.http import JsonResponse
from django.views.decorators.http import require_POST
from django.views.decorators.csrf import ensure_csrf_cookie
from django.utils import timezone
from django.utils.translation import gettext as _, gettext_lazy
from .forms import OffboardingRequestForm, OnboardingRequestForm
from .form_builder import (
@@ -71,10 +72,10 @@ ONBOARDING_CHECKBOX_LISTS = {
}
ONBOARDING_SECTION_ORDER = ['stammdaten', 'vertrag', 'itsetup', 'abschluss']
ONBOARDING_SECTION_META = {
'stammdaten': {'title': 'Stammdaten', 'subtitle': 'Person, Rolle, Abteilung'},
'vertrag': {'title': 'Vertrag', 'subtitle': 'Beschäftigung und Termine'},
'itsetup': {'title': 'IT-Setup', 'subtitle': 'Geräte, Software und Zugänge'},
'abschluss': {'title': 'Abschluss', 'subtitle': 'Notizen und Freigabe'},
'stammdaten': {'title': gettext_lazy('Stammdaten'), 'subtitle': gettext_lazy('Person, Rolle, Abteilung')},
'vertrag': {'title': gettext_lazy('Vertrag'), 'subtitle': gettext_lazy('Beschäftigung und Termine')},
'itsetup': {'title': gettext_lazy('IT-Setup'), 'subtitle': gettext_lazy('Geräte, Software und Zugänge')},
'abschluss': {'title': gettext_lazy('Abschluss'), 'subtitle': gettext_lazy('Notizen und Freigabe')},
}
@@ -218,7 +219,7 @@ def project_wiki_page(request):
def requests_dashboard(request):
if request.method == 'POST':
if not request.user.is_staff:
messages.error(request, 'Sie haben keine Berechtigung für diese Aktion.')
messages.error(request, _('Sie haben keine Berechtigung für diese Aktion.'))
return redirect('requests_dashboard')
selected = request.POST.getlist('selected_requests')
@@ -227,7 +228,7 @@ def requests_dashboard(request):
selected = [single_delete]
if not selected:
messages.warning(request, 'Keine Einträge ausgewählt.')
messages.warning(request, _('Keine Einträge ausgewählt.'))
return redirect('requests_dashboard')
deleted_count = 0
@@ -256,11 +257,11 @@ def requests_dashboard(request):
deleted_count += 1
if deleted_count:
messages.success(request, f'{deleted_count} Eintrag/Einträge gelöscht.')
messages.success(request, _('%(count)s Eintrag/Einträge gelöscht.') % {'count': deleted_count})
if invalid_count:
messages.warning(request, f'{invalid_count} Auswahl(en) konnten nicht verarbeitet werden.')
messages.warning(request, _('%(count)s Auswahl(en) konnten nicht verarbeitet werden.') % {'count': invalid_count})
if not deleted_count and not invalid_count:
messages.info(request, 'Keine passenden Einträge gefunden.')
messages.info(request, _('Keine passenden Einträge gefunden.'))
return redirect('requests_dashboard')
search_query = request.GET.get('q', '').strip()
@@ -418,7 +419,7 @@ def generate_onboarding_intro_pdf(request, request_id: int):
pdf_path = _generate_onboarding_intro_pdf(obj)
obj.intro_pdf_path = str(pdf_path)
obj.save(update_fields=['intro_pdf_path'])
messages.success(request, 'Einweisungs- und Übergabeprotokoll wurde erzeugt.')
messages.success(request, _('Einweisungs- und Übergabeprotokoll wurde erzeugt.'))
return redirect('requests_dashboard')
@@ -431,7 +432,7 @@ def generate_onboarding_intro_session_pdf(request, request_id: int):
pdf_path = _generate_onboarding_intro_session_pdf(session, admin_signature_name=_display_user_name(request.user))
session.exported_pdf_path = str(pdf_path)
session.save(update_fields=['exported_pdf_path'])
messages.success(request, 'Einweisungsprotokoll aus Live-Status wurde erzeugt.')
messages.success(request, _('Einweisungsprotokoll aus Live-Status wurde erzeugt.'))
return redirect('onboarding_intro_session_page', request_id=request_id)
@@ -460,18 +461,18 @@ def onboarding_intro_session_page(request, request_id: int):
session.completed_by_name = ''
session.exported_pdf_path = ''
session.save(update_fields=['checklist_state', 'notes', 'status', 'completed_at', 'completed_by_name', 'exported_pdf_path'])
messages.success(request, 'Einweisung wurde zurückgesetzt.')
messages.success(request, _('Einweisung wurde zurückgesetzt.'))
return redirect('onboarding_intro_session_page', request_id=request_id)
if action == 'complete':
session.status = 'completed'
session.completed_at = timezone.now()
session.completed_by_name = _display_user_name(request.user)
messages.success(request, 'Einweisung wurde als abgeschlossen gespeichert.')
messages.success(request, _('Einweisung wurde als abgeschlossen gespeichert.'))
else:
session.status = 'draft'
session.completed_at = None
session.completed_by_name = ''
messages.success(request, 'Einweisung wurde als Entwurf gespeichert.')
messages.success(request, _('Einweisung wurde als Entwurf gespeichert.'))
session.save()
return redirect('onboarding_intro_session_page', request_id=request_id)