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

@@ -20,6 +20,21 @@ This is the standalone dockerized web application for the TUBCO onboarding and o
5. Open test mailbox: 5. Open test mailbox:
- `http://localhost:8025` - `http://localhost:8025`
## Translations
This project now uses Django's standard i18n workflow for long-term maintainability.
- Extract/update message catalogs:
- `docker compose exec -T web django-admin makemessages -l en`
- Compile translations:
- `docker compose exec -T web django-admin compilemessages`
- Add more languages the same way:
- `docker compose exec -T web django-admin makemessages -l de`
Notes:
- `gettext` is installed in the Docker image, so `compilemessages` works inside the container.
- Translation files live under `backend/locale/`.
- Core fixed UI is bilingual now; dynamic builder content and most PDF/email business text are not fully bilingual yet.
## Current implemented scope ## Current implemented scope
- Onboarding form with labels mapped from your CSV schema. - Onboarding form with labels mapped from your CSV schema.
- Stores requests in PostgreSQL. - Stores requests in PostgreSQL.

View File

@@ -6,7 +6,7 @@ ENV PYTHONUNBUFFERED=1
WORKDIR /app WORKDIR /app
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y --no-install-recommends build-essential netcat-openbsd \ && apt-get install -y --no-install-recommends build-essential netcat-openbsd gettext \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
RUN groupadd -g 1000 app && useradd -u 1000 -g app -m app RUN groupadd -g 1000 app && useradd -u 1000 -g app -m app

View File

@@ -38,6 +38,7 @@ INSTALLED_APPS = [
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
@@ -83,7 +84,12 @@ AUTH_PASSWORD_VALIDATORS = [
{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'}, {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
] ]
LANGUAGE_CODE = 'de-de' LANGUAGE_CODE = os.getenv('DJANGO_LANGUAGE_CODE', 'de')
LANGUAGES = [
('de', 'Deutsch'),
('en', 'English'),
]
LOCALE_PATHS = [BASE_DIR / 'locale']
TIME_ZONE = 'Europe/Berlin' TIME_ZONE = 'Europe/Berlin'
USE_I18N = True USE_I18N = True
USE_TZ = True USE_TZ = True

View File

@@ -5,6 +5,7 @@ from django.urls import include, path
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('i18n/', include('django.conf.urls.i18n')),
path('accounts/', include('django.contrib.auth.urls')), path('accounts/', include('django.contrib.auth.urls')),
path('', include('workflows.urls')), path('', include('workflows.urls')),
] ]

Binary file not shown.

View File

@@ -0,0 +1,403 @@
msgid ""
msgstr ""
"Project-Id-Version: tubco-portal\n"
"POT-Creation-Date: 2026-03-24 00:00+0000\n"
"PO-Revision-Date: 2026-03-24 00:00+0000\n"
"Language: en\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
msgid "%(count)s Auswahl(en) konnten nicht verarbeitet werden."
msgstr "%(count)s selection(s) could not be processed."
msgid "%(count)s Eintrag/Einträge gelöscht."
msgstr "%(count)s entry/entries deleted."
msgid "%(count)s Einträge sichtbar"
msgstr "%(count)s entries visible"
msgid "14 Tage"
msgstr "14 days"
msgid "Abmelden"
msgstr "Log out"
msgid "Abschluss"
msgstr "Finish"
msgid "Admin"
msgstr "Admin"
msgid "Admin Apps"
msgstr "Admin Apps"
msgid "Aktion"
msgstr "Action"
msgid "Aktivität 14 Tage"
msgstr "Activity 14 Days"
msgid "Aktivitätsverlauf"
msgstr "Activity Timeline"
msgid "Alle Vorgänge, durchsuchbar und mit Dokumenten verknüpft."
msgstr "All requests, searchable and linked with documents."
msgid "Alle erfassten Onboarding-Vorgänge im aktuellen System."
msgstr "All onboarding requests captured in the current system."
msgid "Anfrage gespeichert"
msgstr "Request saved"
msgid "Anfragen Dashboard"
msgstr "Requests Dashboard"
msgid "Anmelden"
msgstr "Sign in"
msgid "Anmeldung"
msgstr "Sign in"
msgid "Anmeldung fehlgeschlagen. Bitte Zugangsdaten prüfen."
msgstr "Login failed. Please check your credentials."
msgid "Apps"
msgstr "Apps"
msgid "Austritte und Rückgaben in derselben Prozessübersicht."
msgstr "Departures and returns in the same process overview."
msgid "Auswahl löschen"
msgstr "Delete selection"
msgid "Beschäftigung und Termine"
msgstr "Employment and dates"
msgid "Bitte melden Sie sich mit Ihrem Benutzerkonto an."
msgstr "Please sign in with your user account."
msgid "Bitte prüfen Sie die markierten Felder. Ungültige Eingaben wurden erkannt."
msgstr "Please check the highlighted fields. Invalid input was detected."
msgid "Checklistenpunkte für das Einweisungsprotokoll konfigurieren."
msgstr "Configure checklist items for the introduction protocol."
msgid "Dashboard öffnen"
msgstr "Open dashboard"
msgid "Datensätze können direkt in der Tabelle gefiltert, geöffnet, geprüft oder gelöscht werden."
msgstr "Records can be filtered, opened, checked, or deleted directly in the table."
msgid "Die letzten 14 Tage in einer kompakten Ansicht über alle Onboarding- und Offboarding-Vorgänge."
msgstr "The last 14 days in a compact view across all onboarding and offboarding requests."
msgid "Django Admin"
msgstr "Django Admin"
msgid "Dokument"
msgstr "Document"
msgid "Dokumentation, Architektur und Runbook."
msgstr "Documentation, architecture, and runbook."
msgid "Dokumente, Status und Einweisungsaktionen in einer verdichteten Arbeitsansicht."
msgstr "Documents, status, and introduction actions in a condensed work view."
msgid "E-Mail"
msgstr "Email"
msgid "E-Mail Routing"
msgstr "Email routing"
msgid "E-Mail:"
msgstr "Email:"
msgid "Einweisung"
msgstr "Introduction"
msgid "Einweisung wurde als Entwurf gespeichert."
msgstr "Introduction was saved as draft."
msgid "Einweisung wurde als abgeschlossen gespeichert."
msgstr "Introduction was saved as completed."
msgid "Einweisung wurde zurückgesetzt."
msgstr "Introduction was reset."
msgid "Einweisung öffnen"
msgstr "Open introduction"
msgid "Einweisungs- und Übergabeprotokoll wurde erzeugt."
msgstr "Introduction and handover protocol was generated."
msgid "Einweisungs-Builder"
msgstr "Introduction Builder"
msgid "Einweisungsprotokoll aus Live-Status wurde erzeugt."
msgstr "Introduction protocol from live status was generated."
msgid "Erstellt"
msgstr "Created"
msgid "Fast geschafft. Bitte Abschlussdaten prüfen und die Anfrage absenden."
msgstr "Almost done. Please review the final details and submit the request."
msgid "Felder, Schritte und Optionen verwalten."
msgstr "Manage fields, steps, and options."
msgid "Form Builder"
msgstr "Form Builder"
msgid "Geplante Welcome Mails verwalten."
msgstr "Manage scheduled welcome emails."
msgid "Geräte, Software und Zugänge"
msgstr "Devices, software, and access"
msgid "Gesamtbestand"
msgstr "Total records"
msgid "Hardware-Liste"
msgstr "Hardware list"
msgid "IT-Rückgabe"
msgstr "IT return"
msgid "IT-Setup"
msgstr "IT setup"
msgid "Integrationen"
msgstr "Integrations"
msgid "Keine Einträge ausgewählt."
msgstr "No entries selected."
msgid "Keine konfigurierten Felder in diesem Schritt."
msgstr "No configured fields in this step."
msgid "Keine passenden Einträge gefunden."
msgstr "No matching entries found."
msgid "Konfiguration, Tests und Steuerung."
msgstr "Configuration, tests, and controls."
msgid "Live-Protokoll"
msgstr "Live protocol"
msgid "Live-Protokoll öffnen"
msgstr "Open live protocol"
msgid "Löschen"
msgstr "Delete"
msgid "Mehrschritt-Formular"
msgstr "Multi-step form"
msgid "Mehrseitiges Formular mit konfigurierbaren Feldern aus dem Admin."
msgstr "Multi-page form with configurable fields from the admin."
msgid "Mitarbeitende suchen (Name oder E-Mail)"
msgstr "Search employees (name or email)"
msgid "Mitarbeitende suchen, Daten vorbefüllen, Offboarding-Dokumente erzeugen und Rückgabe-Prozess starten."
msgstr "Search employees, prefill data, generate offboarding documents, and start the return process."
msgid "Mitarbeiter"
msgstr "Staff"
msgid "Nach Name oder E-Mail suchen"
msgstr "Search by name or email"
msgid "Neu erzeugen"
msgstr "Regenerate"
msgid "Neue Mitarbeitende erfassen, PDF mit Briefkopf erstellen, Benachrichtigungen senden und in Nextcloud ablegen."
msgstr "Capture new employees, generate a PDF with letterhead, send notifications, and store it in Nextcloud."
msgid "Nextcloud- und E-Mail-Setup."
msgstr "Nextcloud and email setup."
msgid "Nextcloud:"
msgstr "Nextcloud:"
msgid "Nicht relevant"
msgstr "Not relevant"
msgid "Noch keine Vorgänge vorhanden."
msgstr "No requests available yet."
msgid "Noch nicht verfügbar"
msgstr "Not available yet"
msgid "Notizen und Freigabe"
msgstr "Notes and approval"
msgid "Offboarding"
msgstr "Offboarding"
msgid "Offboarding starten"
msgstr "Start offboarding"
msgid "Offboarding wurde erfolgreich gespeichert (ID: %(request_id)s). Das PDF wird im Hintergrund erzeugt."
msgstr "Offboarding was saved successfully (ID: %(request_id)s). The PDF is being generated in the background."
msgid "Offboarding-Anfrage"
msgstr "Offboarding request"
msgid "Offboarding-Anfrage speichern"
msgstr "Save offboarding request"
msgid "Onboarding"
msgstr "Onboarding"
msgid "Onboarding + Offboarding"
msgstr "Onboarding + Offboarding"
msgid "Onboarding starten"
msgstr "Start onboarding"
msgid "Onboarding wurde erfolgreich gespeichert (ID: %(request_id)s). Das PDF wird im Hintergrund erzeugt."
msgstr "Onboarding was saved successfully (ID: %(request_id)s). The PDF is being generated in the background."
msgid "Onboarding-Anfrage"
msgstr "Onboarding request"
msgid "Onboarding-Anfrage absenden"
msgstr "Submit onboarding request"
msgid "Operations Console"
msgstr "Operations Console"
msgid "PDF + E-Mail Workflow bereit"
msgstr "PDF + Email Workflow Ready"
msgid "PDF Zugriff"
msgstr "PDF access"
msgid "PDF erzeugen"
msgstr "Generate PDF"
msgid "PDF öffnen"
msgstr "Open PDF"
msgid "PDFs + Live-Protokolle"
msgstr "PDFs + live protocols"
msgid "Person"
msgstr "Person"
msgid "Person, Rolle, Abteilung"
msgstr "Person, role, department"
msgid "Produktion"
msgstr "Production"
msgid "Profile-Suche"
msgstr "Profile search"
msgid "Projekt Wiki"
msgstr "Project Wiki"
msgid "Rolle:"
msgstr "Role:"
msgid "Schließen"
msgstr "Close"
msgid "Sie haben keine Berechtigung für diese Aktion."
msgstr "You do not have permission for this action."
msgid "Stammdaten"
msgstr "Master data"
msgid "Standard-Einweisungs-PDF"
msgstr "Standard introduction PDF"
msgid "Standard-PDF öffnen"
msgstr "Open standard PDF"
msgid "Status"
msgstr "Status"
msgid "Status, Suchfunktion, PDF-Links und Verlauf aller Onboarding-/Offboarding-Anfragen."
msgstr "Status, search, PDF links, and history of all onboarding/offboarding requests."
msgid "Status:"
msgstr "Status:"
msgid "Steuert Onboarding- und Offboarding-Prozesse an einem Ort. Die Oberfläche priorisiert Kennzahlen, Aktivität und direkte Aktionen in der Vorgangsliste."
msgstr "Controls onboarding and offboarding processes in one place. The interface prioritizes metrics, activity, and direct actions in the request list."
msgid "Suche"
msgstr "Search"
msgid "Suche + Bulk-Aktionen"
msgstr "Search + bulk actions"
msgid "Suchen"
msgstr "Search"
msgid "TUBCO Onboarding & Offboarding Portal"
msgstr "TUBCO Onboarding & Offboarding Portal"
msgid "Testmodus"
msgstr "Test mode"
msgid "Tipp: Die letzten Vorgänge sehen Sie jederzeit im Anfragen Dashboard."
msgstr "Tip: You can always see the latest requests in the Requests Dashboard."
msgid "Typ"
msgstr "Type"
msgid "Vertrag"
msgstr "Contract"
msgid "Vollständige Datenverwaltung."
msgstr "Full data management."
msgid "Vorbefüllt aus:"
msgstr "Prefilled from:"
msgid "Vorgänge"
msgstr "Requests"
msgid "Weiter"
msgstr "Next"
msgid "Welcome E-Mails"
msgstr "Welcome Emails"
msgid "Wählen Sie den gewünschten Prozess."
msgstr "Choose the desired process."
msgid "Zeitraum des visuellen Aktivitätsverlaufs in dieser Übersicht."
msgstr "Time span of the visual activity timeline in this overview."
msgid "Zentrale Arbeitsfläche für Anfragen, PDF-Generierung, E-Mail-Workflows und Ablage in Nextcloud."
msgstr "Central workspace for requests, PDF generation, email workflows, and storage in Nextcloud."
msgid "Zur Startseite"
msgstr "Back to home"
msgid "Zurück"
msgstr "Back"
msgid "Zurücksetzen"
msgstr "Reset"
msgid "aktiv"
msgstr "active"
msgid "ausgewählt"
msgstr "selected"
msgid "inaktiv"
msgstr "inactive"
msgid "z. B. max.mustermann@tub.co"
msgstr "e.g. john.doe@tub.co"
msgid "Öffnen"
msgstr "Open"

View File

@@ -1,10 +1,11 @@
{% load static %} {% load static i18n %}
{% get_current_language as CURRENT_LANGUAGE %}
<!doctype html> <!doctype html>
<html lang="de"> <html lang="{{ CURRENT_LANGUAGE }}">
<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>Anmeldung</title> <title>{% trans "Anmeldung" %}</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; min-height: 100vh; display: grid; place-items: center; background: linear-gradient(160deg, #eef6ff, #fff3f3); } 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; } input { width: 100%; padding: 10px; box-sizing: border-box; border: 1px solid #cbd5e1; border-radius: 8px; }
.btn { width: 100%; } .btn { width: 100%; }
.errorlist { color: #b91c1c; margin: 6px 0; } .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> </style>
</head> </head>
<body> <body>
<div class="card"> <div class="card">
<img class="logo" src="https://tub.co/media/site/0856bfc615-1750234287/tubco-wortbildmarke.svg" alt="TUB/CO Logo" /> <div class="card-head">
<h1>Anmeldung</h1> <img class="logo" src="https://tub.co/media/site/0856bfc615-1750234287/tubco-wortbildmarke.svg" alt="TUB/CO Logo" />
<p>Bitte melden Sie sich mit Ihrem Benutzerkonto an.</p> <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/"> <form method="post" action="/accounts/login/">
{% csrf_token %} {% csrf_token %}
{% if form.errors %} {% 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 %} {% endif %}
<div class="field">{{ form.username.label_tag }}{{ form.username }}</div> <div class="field">{{ form.username.label_tag }}{{ form.username }}</div>
<div class="field">{{ form.password.label_tag }}{{ form.password }}</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> </form>
</div> </div>
</body> </body>

View File

@@ -1,10 +1,11 @@
{% load static %} {% load static i18n %}
{% get_current_language as CURRENT_LANGUAGE %}
<!doctype html> <!doctype html>
<html lang="de"> <html lang="{{ CURRENT_LANGUAGE }}">
<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>TUBCO Onboarding & Offboarding Portal</title> <title>{% trans "TUBCO Onboarding & Offboarding Portal" %}</title>
<link rel="stylesheet" href="{% static 'workflows/css/buttons.css' %}" /> <link rel="stylesheet" href="{% static 'workflows/css/buttons.css' %}" />
<style> <style>
:root { :root {
@@ -68,7 +69,11 @@
gap: 8px; gap: 8px;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: flex-end; 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 { .hero {
padding: 24px; padding: 24px;
@@ -433,9 +438,15 @@
<div class="topbar"> <div class="topbar">
<img class="brand-logo" src="https://tub.co/media/site/0856bfc615-1750234287/tubco-wortbildmarke.svg" alt="TUB/CO Logo" /> <img class="brand-logo" src="https://tub.co/media/site/0856bfc615-1750234287/tubco-wortbildmarke.svg" alt="TUB/CO Logo" />
<div class="quick-actions"> <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;"> <form method="post" action="/accounts/logout/" style="display:inline;">
{% csrf_token %} {% csrf_token %}
<button class="btn btn-secondary" type="submit">Abmelden</button> <button class="btn btn-secondary" type="submit">{% trans "Abmelden" %}</button>
</form> </form>
</div> </div>
</div> </div>
@@ -443,18 +454,18 @@
<div class="hero"> <div class="hero">
<div class="hero-grid"> <div class="hero-grid">
<div class="hero-card"> <div class="hero-card">
<span class="eyebrow">Operations Console</span> <span class="eyebrow">{% trans "Operations Console" %}</span>
<h1>TUBCO Onboarding & Offboarding Portal</h1> <h1>{% trans "TUBCO Onboarding & Offboarding Portal" %}</h1>
<p>Zentrale Arbeitsfläche für Anfragen, PDF-Generierung, E-Mail-Workflows und Ablage in Nextcloud.</p> <p>{% trans "Zentrale Arbeitsfläche für Anfragen, PDF-Generierung, E-Mail-Workflows und Ablage in Nextcloud." %}</p>
<div class="status-row"> <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 %}"> <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>
<span class="status-pill {% if email_test_mode %}warn{% else %}ok{% endif %}"> <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>
<span class="status-pill">PDF + Email Workflow Ready</span> <span class="status-pill">{% trans "PDF + E-Mail Workflow bereit" %}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -468,94 +479,94 @@
{% endif %} {% endif %}
<div class="section-head"> <div class="section-head">
<h2>Apps</h2> <h2>{% trans "Apps" %}</h2>
<p>Wählen Sie den gewünschten Prozess.</p> <p>{% trans "Wählen Sie den gewünschten Prozess." %}</p>
</div> </div>
<div class="apps-grid"> <div class="apps-grid">
<section class="app-card primary"> <section class="app-card primary">
<div> <div>
<div class="top-line"><div class="accent">ON</div></div> <div class="top-line"><div class="accent">ON</div></div>
<h3 class="app-title">Onboarding</h3> <h3 class="app-title">{% trans "Onboarding" %}</h3>
<p class="app-text">Neue Mitarbeitende erfassen, PDF mit Briefkopf erstellen, Benachrichtigungen senden und in Nextcloud ablegen.</p> <p class="app-text">{% trans "Neue Mitarbeitende erfassen, PDF mit Briefkopf erstellen, Benachrichtigungen senden und in Nextcloud ablegen." %}</p>
<div class="tag-row"> <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">PDF</span>
<span class="tag">E-Mail Routing</span> <span class="tag">{% trans "E-Mail Routing" %}</span>
</div> </div>
</div> </div>
<div class="card-actions"> <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> </div>
</section> </section>
<section class="app-card red"> <section class="app-card red">
<div> <div>
<div class="top-line"><div class="accent red">OFF</div></div> <div class="top-line"><div class="accent red">OFF</div></div>
<h3 class="app-title">Offboarding</h3> <h3 class="app-title">{% trans "Offboarding" %}</h3>
<p class="app-text">Mitarbeitende suchen, Daten vorbefüllen, Offboarding-Dokumente erzeugen und Rückgabe-Prozess starten.</p> <p class="app-text">{% trans "Mitarbeitende suchen, Daten vorbefüllen, Offboarding-Dokumente erzeugen und Rückgabe-Prozess starten." %}</p>
<div class="tag-row"> <div class="tag-row">
<span class="tag">Profile-Suche</span> <span class="tag">{% trans "Profile-Suche" %}</span>
<span class="tag">Hardware-Liste</span> <span class="tag">{% trans "Hardware-Liste" %}</span>
<span class="tag">IT-Rückgabe</span> <span class="tag">{% trans "IT-Rückgabe" %}</span>
</div> </div>
</div> </div>
<div class="card-actions"> <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> </div>
</section> </section>
<section class="app-card"> <section class="app-card">
<div> <div>
<div class="top-line"><div class="accent">APP</div></div> <div class="top-line"><div class="accent">APP</div></div>
<h3 class="app-title">Anfragen Dashboard</h3> <h3 class="app-title">{% trans "Anfragen Dashboard" %}</h3>
<p class="app-text">Status, Suchfunktion, PDF-Links und Verlauf aller Onboarding-/Offboarding-Anfragen.</p> <p class="app-text">{% trans "Status, Suchfunktion, PDF-Links und Verlauf aller Onboarding-/Offboarding-Anfragen." %}</p>
<div class="tag-row"> <div class="tag-row">
<span class="tag">Suche</span> <span class="tag">{% trans "Suche" %}</span>
<span class="tag">Status</span> <span class="tag">{% trans "Status" %}</span>
<span class="tag">PDF Zugriff</span> <span class="tag">{% trans "PDF Zugriff" %}</span>
</div> </div>
</div> </div>
<div class="card-actions"> <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> </div>
</section> </section>
</div> </div>
{% if request.user.is_staff %} {% if request.user.is_staff %}
<div class="section-head"> <div class="section-head">
<h2>Admin Apps</h2> <h2>{% trans "Admin Apps" %}</h2>
<p>Konfiguration, Tests und Steuerung.</p> <p>{% trans "Konfiguration, Tests und Steuerung." %}</p>
</div> </div>
<div class="admin-grid"> <div class="admin-grid">
<section class="admin-card"> <section class="admin-card">
<h3>Form Builder</h3> <h3>{% trans "Form Builder" %}</h3>
<p>Felder, Schritte und Optionen verwalten.</p> <p>{% trans "Felder, Schritte und Optionen verwalten." %}</p>
<a class="btn btn-secondary" href="/admin-tools/form-builder/">Öffnen</a> <a class="btn btn-secondary" href="/admin-tools/form-builder/">{% trans "Öffnen" %}</a>
</section> </section>
<section class="admin-card"> <section class="admin-card">
<h3>Einweisungs-Builder</h3> <h3>{% trans "Einweisungs-Builder" %}</h3>
<p>Checklistenpunkte für das Einweisungsprotokoll konfigurieren.</p> <p>{% trans "Checklistenpunkte für das Einweisungsprotokoll konfigurieren." %}</p>
<a class="btn btn-secondary" href="/admin-tools/intro-builder/">Öffnen</a> <a class="btn btn-secondary" href="/admin-tools/intro-builder/">{% trans "Öffnen" %}</a>
</section> </section>
<section class="admin-card"> <section class="admin-card">
<h3>Projekt Wiki</h3> <h3>{% trans "Projekt Wiki" %}</h3>
<p>Dokumentation, Architektur und Runbook.</p> <p>{% trans "Dokumentation, Architektur und Runbook." %}</p>
<a class="btn btn-secondary" href="/admin-tools/wiki/">Öffnen</a> <a class="btn btn-secondary" href="/admin-tools/wiki/">{% trans "Öffnen" %}</a>
</section> </section>
<section class="admin-card"> <section class="admin-card">
<h3>Integrationen</h3> <h3>{% trans "Integrationen" %}</h3>
<p>Nextcloud- und E-Mail-Setup.</p> <p>{% trans "Nextcloud- und E-Mail-Setup." %}</p>
<a class="btn btn-secondary" href="/admin-tools/integrations/?kind=nextcloud">Öffnen</a> <a class="btn btn-secondary" href="/admin-tools/integrations/?kind=nextcloud">{% trans "Öffnen" %}</a>
</section> </section>
<section class="admin-card"> <section class="admin-card">
<h3>Welcome E-Mails</h3> <h3>{% trans "Welcome E-Mails" %}</h3>
<p>Geplante Welcome Mails verwalten.</p> <p>{% trans "Geplante Welcome Mails verwalten." %}</p>
<a class="btn btn-secondary" href="/admin-tools/welcome-emails/">Öffnen</a> <a class="btn btn-secondary" href="/admin-tools/welcome-emails/">{% trans "Öffnen" %}</a>
</section> </section>
<section class="admin-card"> <section class="admin-card">
<h3>Django Admin</h3> <h3>{% trans "Django Admin" %}</h3>
<p>Vollständige Datenverwaltung.</p> <p>{% trans "Vollständige Datenverwaltung." %}</p>
<a class="btn btn-secondary" href="/admin/">Öffnen</a> <a class="btn btn-secondary" href="/admin/">{% trans "Öffnen" %}</a>
</section> </section>
<section class="admin-card"> <section class="admin-card">
<h3>SMTP Einstellungen</h3> <h3>SMTP Einstellungen</h3>
@@ -600,7 +611,7 @@
{% endif %} {% endif %}
<div class="footer-note"> <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> </div>
</main> </main>
</div> </div>

View File

@@ -1,33 +1,41 @@
{% load static %} {% load static i18n %}
{% get_current_language as CURRENT_LANGUAGE %}
<!doctype html> <!doctype html>
<html lang="de"> <html lang="{{ CURRENT_LANGUAGE }}">
<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>Offboarding-Anfrage</title> <title>{% trans "Offboarding-Anfrage" %}</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/offboarding_form.css' %}" /> <link rel="stylesheet" href="{% static 'workflows/css/offboarding_form.css' %}" />
</head> </head>
<body> <body>
<div id="saved-popup" class="popup-backdrop {% if saved %}show{% endif %}"> <div id="saved-popup" class="popup-backdrop {% if saved %}show{% endif %}">
<div class="popup"> <div class="popup">
<h3>Anfrage gespeichert</h3> <h3>{% trans "Anfrage gespeichert" %}</h3>
<p>Offboarding wurde erfolgreich gespeichert (ID: {{ saved_request_id }}). Das PDF wird im Hintergrund erzeugt.</p> <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')">Schließen</button> <button class="btn btn-secondary" type="button" onclick="document.getElementById('saved-popup').classList.remove('show')">{% trans "Schließen" %}</button>
</div> </div>
</div> </div>
<div class="wrap"> <div class="wrap">
<img class="brand-logo" src="https://tub.co/media/site/0856bfc615-1750234287/tubco-wortbildmarke.svg" alt="TUB/CO Logo" /> <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"> <div class="card">
<h1>Offboarding-Anfrage</h1> <h1>{% trans "Offboarding-Anfrage" %}</h1>
<form method="get" action="/offboarding/new/"> <form method="get" action="/offboarding/new/">
<div class="field"> <div class="field">
<label for="q">Mitarbeitende suchen (Name oder E-Mail)</label> <label for="q">{% trans "Mitarbeitende suchen (Name oder E-Mail)" %}</label>
<input id="q" name="q" value="{{ search_query }}" placeholder="z. B. max.mustermann@tub.co" /> <input id="q" name="q" value="{{ search_query }}" placeholder="{% trans "z. B. max.mustermann@tub.co" %}" />
</div> </div>
<button class="btn btn-primary" type="submit">Suchen</button> <button class="btn btn-primary" type="submit">{% trans "Suchen" %}</button>
</form> </form>
{% if search_results %} {% if search_results %}
@@ -39,7 +47,7 @@
{% endif %} {% endif %}
{% if selected_profile %} {% 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 %} {% endif %}
</div> </div>
@@ -58,7 +66,7 @@
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</div> </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> </form>
</div> </div>
</div> </div>

View File

@@ -1,31 +1,40 @@
{% load static %} {% load static i18n %}
{% get_current_language as CURRENT_LANGUAGE %}
<!doctype html> <!doctype html>
<html lang="de"> <html lang="{{ CURRENT_LANGUAGE }}">
<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>Onboarding-Anfrage</title> <title>{% trans "Onboarding-Anfrage" %}</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/onboarding_form.css' %}" /> <link rel="stylesheet" href="{% static 'workflows/css/onboarding_form.css' %}" />
</head> </head>
<body> <body>
<div id="saved-popup" class="popup-backdrop {% if saved %}show{% endif %}"> <div id="saved-popup" class="popup-backdrop {% if saved %}show{% endif %}">
<div class="popup"> <div class="popup">
<h3>Anfrage gespeichert</h3> <h3>{% trans "Anfrage gespeichert" %}</h3>
<p>Onboarding wurde erfolgreich gespeichert (ID: {{ saved_request_id }}). Das PDF wird im Hintergrund erzeugt.</p> <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')">Schließen</button> <button class="btn btn-secondary" type="button" onclick="document.getElementById('saved-popup').classList.remove('show')">{% trans "Schließen" %}</button>
</div> </div>
</div> </div>
<div class="top-wrap"> <div class="top-wrap">
<img class="brand-logo" src="https://tub.co/media/site/0856bfc615-1750234287/tubco-wortbildmarke.svg" alt="TUB/CO Logo" /> <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>
<div class="shell"> <div class="shell">
<aside class="panel"> <aside class="panel">
<h1>Onboarding</h1> <h1>{% trans "Onboarding" %}</h1>
<p class="sub">Mehrseitiges Formular mit konfigurierbaren Feldern aus dem Admin.</p> <p class="sub">{% trans "Mehrseitiges Formular mit konfigurierbaren Feldern aus dem Admin." %}</p>
<ol class="step-list"> <ol class="step-list">
{% for section in onboarding_sections %} {% 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 }}"> <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"> <main class="main">
{% if form.errors %} {% 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 %} {% endif %}
<form method="post" id="onboarding-form" enctype="multipart/form-data"> <form method="post" id="onboarding-form" enctype="multipart/form-data">
@@ -101,12 +110,12 @@
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if not section.blocks %} {% 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 %} {% endif %}
{% if section.key == 'abschluss' %} {% if section.key == 'abschluss' %}
<div class="field-full finish-note"> <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>
<div class="field-full"> <div class="field-full">
<div class="legal">{{ legal_text }}</div> <div class="legal">{{ legal_text }}</div>
@@ -118,9 +127,9 @@
{% endfor %} {% endfor %}
<div class="actions"> <div class="actions">
<button class="btn btn-secondary" type="button" id="btn-prev">Zurück</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">Weiter</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">Onboarding-Anfrage absenden</button> <button type="submit" id="btn-submit" class="btn btn-primary hidden">{% trans "Onboarding-Anfrage absenden" %}</button>
</div> </div>
</form> </form>
</main> </main>

View File

@@ -161,6 +161,22 @@
<li>Use SMTP test action before switching to production mode.</li> <li>Use SMTP test action before switching to production mode.</li>
</ul> </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> <h2 id="admin">9) Admin Apps (Home)</h2>
<ul> <ul>
<li><strong>Form Builder:</strong> manage field visibility/order/options.</li> <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> <!doctype html>
<html lang="de"> <html lang="{{ CURRENT_LANGUAGE }}">
<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>Anfragen Dashboard</title> <title>{% trans "Anfragen Dashboard" %}</title>
<link rel="stylesheet" href="{% static 'workflows/css/buttons.css' %}" /> <link rel="stylesheet" href="{% static 'workflows/css/buttons.css' %}" />
<style> <style>
:root { :root {
@@ -81,7 +82,11 @@
gap: 8px; gap: 8px;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: flex-end; 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 { .hero {
padding: 0; 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" /> <img class="brand-logo" src="https://tub.co/media/site/0856bfc615-1750234287/tubco-wortbildmarke.svg" alt="TUB/CO Logo" />
</div> </div>
<div class="quick-actions"> <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>
</div> </div>
@@ -883,22 +894,22 @@
<section class="hero"> <section class="hero">
<div class="hero-grid"> <div class="hero-grid">
<div class="hero-card"> <div class="hero-card">
<span class="eyebrow">Operations Console</span> <span class="eyebrow">{% trans "Operations Console" %}</span>
<h1>Anfragen Dashboard</h1> <h1>{% trans "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> <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"> <div class="hero-pills">
<span class="hero-pill">Onboarding + Offboarding</span> <span class="hero-pill">{% trans "Onboarding + Offboarding" %}</span>
<span class="hero-pill">PDFs + Live-Protokolle</span> <span class="hero-pill">{% trans "PDFs + Live-Protokolle" %}</span>
<span class="hero-pill">Suche + Bulk-Aktionen</span> <span class="hero-pill">{% trans "Suche + Bulk-Aktionen" %}</span>
</div> </div>
</div> </div>
<article class="chart-card activity-card"> <article class="chart-card activity-card">
<div class="section-head"> <div class="section-head">
<div> <div>
<h2>Aktivitätsverlauf</h2> <h2>{% trans "Aktivitätsverlauf" %}</h2>
<p>Die letzten 14 Tage in einer kompakten Ansicht über alle Onboarding- und Offboarding-Vorgänge.</p> <p>{% trans "Die letzten 14 Tage in einer kompakten Ansicht über alle Onboarding- und Offboarding-Vorgänge." %}</p>
</div> </div>
<span class="eyebrow">14 Tage</span> <span class="eyebrow">{% trans "14 Tage" %}</span>
</div> </div>
<div class="chart" aria-label="Aktivitätsverlauf der letzten 14 Tage"> <div class="chart" aria-label="Aktivitätsverlauf der letzten 14 Tage">
{% for point in chart_points %} {% for point in chart_points %}
@@ -919,42 +930,42 @@
<article class="stat-card"> <article class="stat-card">
<div class="stat-head"> <div class="stat-head">
<div> <div>
<p class="stat-title">Onboarding</p> <p class="stat-title">{% trans "Onboarding" %}</p>
<div class="stat-value">{{ onboarding_total }}</div> <div class="stat-value">{{ onboarding_total }}</div>
</div> </div>
<div class="stat-kicker">ON</div> <div class="stat-kicker">ON</div>
</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>
<article class="stat-card red"> <article class="stat-card red">
<div class="stat-head"> <div class="stat-head">
<div> <div>
<p class="stat-title">Offboarding</p> <p class="stat-title">{% trans "Offboarding" %}</p>
<div class="stat-value">{{ offboarding_total }}</div> <div class="stat-value">{{ offboarding_total }}</div>
</div> </div>
<div class="stat-kicker">OFF</div> <div class="stat-kicker">OFF</div>
</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>
<article class="stat-card gold"> <article class="stat-card gold">
<div class="stat-head"> <div class="stat-head">
<div> <div>
<p class="stat-title">Gesamtbestand</p> <p class="stat-title">{% trans "Gesamtbestand" %}</p>
<div class="stat-value">{{ combined_total }}</div> <div class="stat-value">{{ combined_total }}</div>
</div> </div>
<div class="stat-kicker">Σ</div> <div class="stat-kicker">Σ</div>
</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>
<article class="stat-card ink"> <article class="stat-card ink">
<div class="stat-head"> <div class="stat-head">
<div> <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 class="stat-value">{{ chart_points|length }}</div>
</div> </div>
<div class="stat-kicker">D</div> <div class="stat-kicker">D</div>
</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> </article>
</section> </section>
</section> </section>
@@ -969,34 +980,34 @@
<section class="table-card"> <section class="table-card">
<div class="table-head"> <div class="table-head">
<div> <div>
<h2>Vorgänge</h2> <h2>{% trans "Vorgänge" %}</h2>
<p>Dokumente, Status und Einweisungsaktionen in einer verdichteten Arbeitsansicht.</p> <p>{% trans "Dokumente, Status und Einweisungsaktionen in einer verdichteten Arbeitsansicht." %}</p>
</div> </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>
<div class="table-controls"> <div class="table-controls">
<div class="table-controls-grid"> <div class="table-controls-grid">
<div class="control-stack"> <div class="control-stack">
<form method="get" action="/requests/" class="search-form"> <form method="get" action="/requests/" class="search-form">
<div class="search-box"> <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>
<div class="intro-actions"> <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 %} {% 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 %} {% endif %}
</div> </div>
</form> </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> </div>
{% if request.user.is_staff %} {% if request.user.is_staff %}
<div class="control-stack"> <div class="control-stack">
<form method="post" action="/requests/" id="bulk-delete-form" onsubmit="return confirm('Ausgewählte Einträge wirklich löschen?');"> <form method="post" action="/requests/" id="bulk-delete-form" onsubmit="return confirm('Ausgewählte Einträge wirklich löschen?');">
{% csrf_token %} {% csrf_token %}
<div class="bulk-toolbar"> <div class="bulk-toolbar">
<span class="bulk-info"><span id="selected-count">0</span> ausgewählt</span> <span class="bulk-info"><span id="selected-count">0</span> {% trans "ausgewählt" %}</span>
<button class="btn btn-secondary" type="submit">Auswahl löschen</button> <button class="btn btn-secondary" type="submit">{% trans "Auswahl löschen" %}</button>
</div> </div>
</form> </form>
</div> </div>
@@ -1008,13 +1019,13 @@
<thead> <thead>
<tr> <tr>
{% if request.user.is_staff %}<th class="select-col"><input type="checkbox" id="select-all" aria-label="Alle auswählen" /></th>{% endif %} {% 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>{% trans "Typ" %}</th>
<th>Person</th> <th>{% trans "Person" %}</th>
<th>E-Mail</th> <th>{% trans "E-Mail" %}</th>
<th>Erstellt</th> <th>{% trans "Erstellt" %}</th>
<th>Dokument</th> <th>{% trans "Dokument" %}</th>
{% if request.user.is_staff %}<th>Einweisung</th>{% endif %} {% if request.user.is_staff %}<th>{% trans "Einweisung" %}</th>{% endif %}
{% if request.user.is_staff %}<th>Aktion</th>{% endif %} {% if request.user.is_staff %}<th>{% trans "Aktion" %}</th>{% endif %}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -1039,63 +1050,63 @@
<td>{{ row.created_at|date:"Y-m-d H:i" }}</td> <td>{{ row.created_at|date:"Y-m-d H:i" }}</td>
<td> <td>
{% if row.pdf_url %} {% 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 %} {% else %}
<span class="person-meta">Noch nicht verfügbar</span> <span class="person-meta">{% trans "Noch nicht verfügbar" %}</span>
{% endif %} {% endif %}
</td> </td>
{% if request.user.is_staff %} {% if request.user.is_staff %}
<td class="actions-cell intro-panel"> <td class="actions-cell intro-panel">
{% if row.kind_slug == 'onboarding' %} {% if row.kind_slug == 'onboarding' %}
<details> <details>
<summary class="intro-toggle">Einweisung</summary> <summary class="intro-toggle">{% trans "Einweisung" %}</summary>
<div class="intro-menu"> <div class="intro-menu">
<div class="intro-group"> <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"> <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 %} {% 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 %} {% endif %}
</div> </div>
</div> </div>
<div class="intro-group"> <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"> <div class="intro-actions">
{% if row.intro_pdf_url %} {% if row.intro_pdf_url %}
<form method="post" action="/requests/onboarding/{{ row.id }}/intro-pdf/generate/" class="inline-form"> <form method="post" action="/requests/onboarding/{{ row.id }}/intro-pdf/generate/" class="inline-form">
{% csrf_token %} {% csrf_token %}
<button class="btn btn-secondary" type="submit">Neu erzeugen</button> <button class="btn btn-secondary" type="submit">{% trans "Neu erzeugen" %}</button>
</form> </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 %} {% else %}
<form method="post" action="/requests/onboarding/{{ row.id }}/intro-pdf/generate/" class="inline-form"> <form method="post" action="/requests/onboarding/{{ row.id }}/intro-pdf/generate/" class="inline-form">
{% csrf_token %} {% csrf_token %}
<button class="btn btn-secondary" type="submit">PDF erzeugen</button> <button class="btn btn-secondary" type="submit">{% trans "PDF erzeugen" %}</button>
</form> </form>
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div> </div>
{% if row.intro_session %} {% 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 %} {% endif %}
</details> </details>
{% else %} {% else %}
<span class="person-meta">Nicht relevant</span> <span class="person-meta">{% trans "Nicht relevant" %}</span>
{% endif %} {% endif %}
</td> </td>
<td class="actions-cell"> <td class="actions-cell">
<form method="post" action="/requests/" class="inline-delete" onsubmit="return confirm('Eintrag wirklich löschen?');"> <form method="post" action="/requests/" class="inline-delete" onsubmit="return confirm('Eintrag wirklich löschen?');">
{% csrf_token %} {% 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> </form>
</td> </td>
{% endif %} {% endif %}
</tr> </tr>
{% empty %} {% empty %}
<tr> <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> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@@ -1104,8 +1115,8 @@
</section> </section>
</section> </section>
<div class="footer-bar"> <div class="footer-bar">
<div class="footer-note">TUBCO Onboarding & Offboarding Portal</div> <div class="footer-note">{% trans "TUBCO Onboarding & Offboarding Portal" %}</div>
<a class="btn btn-secondary" href="/">Zur Startseite</a> <a class="btn btn-secondary" href="/">{% trans "Zur Startseite" %}</a>
</div> </div>
</div> </div>
{% if request.user.is_staff %} {% 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.http import require_POST
from django.views.decorators.csrf import ensure_csrf_cookie from django.views.decorators.csrf import ensure_csrf_cookie
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext as _, gettext_lazy
from .forms import OffboardingRequestForm, OnboardingRequestForm from .forms import OffboardingRequestForm, OnboardingRequestForm
from .form_builder import ( from .form_builder import (
@@ -71,10 +72,10 @@ ONBOARDING_CHECKBOX_LISTS = {
} }
ONBOARDING_SECTION_ORDER = ['stammdaten', 'vertrag', 'itsetup', 'abschluss'] ONBOARDING_SECTION_ORDER = ['stammdaten', 'vertrag', 'itsetup', 'abschluss']
ONBOARDING_SECTION_META = { ONBOARDING_SECTION_META = {
'stammdaten': {'title': 'Stammdaten', 'subtitle': 'Person, Rolle, Abteilung'}, 'stammdaten': {'title': gettext_lazy('Stammdaten'), 'subtitle': gettext_lazy('Person, Rolle, Abteilung')},
'vertrag': {'title': 'Vertrag', 'subtitle': 'Beschäftigung und Termine'}, 'vertrag': {'title': gettext_lazy('Vertrag'), 'subtitle': gettext_lazy('Beschäftigung und Termine')},
'itsetup': {'title': 'IT-Setup', 'subtitle': 'Geräte, Software und Zugänge'}, 'itsetup': {'title': gettext_lazy('IT-Setup'), 'subtitle': gettext_lazy('Geräte, Software und Zugänge')},
'abschluss': {'title': 'Abschluss', 'subtitle': 'Notizen und Freigabe'}, 'abschluss': {'title': gettext_lazy('Abschluss'), 'subtitle': gettext_lazy('Notizen und Freigabe')},
} }
@@ -218,7 +219,7 @@ def project_wiki_page(request):
def requests_dashboard(request): def requests_dashboard(request):
if request.method == 'POST': if request.method == 'POST':
if not request.user.is_staff: 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') return redirect('requests_dashboard')
selected = request.POST.getlist('selected_requests') selected = request.POST.getlist('selected_requests')
@@ -227,7 +228,7 @@ def requests_dashboard(request):
selected = [single_delete] selected = [single_delete]
if not selected: if not selected:
messages.warning(request, 'Keine Einträge ausgewählt.') messages.warning(request, _('Keine Einträge ausgewählt.'))
return redirect('requests_dashboard') return redirect('requests_dashboard')
deleted_count = 0 deleted_count = 0
@@ -256,11 +257,11 @@ def requests_dashboard(request):
deleted_count += 1 deleted_count += 1
if deleted_count: 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: 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: 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') return redirect('requests_dashboard')
search_query = request.GET.get('q', '').strip() 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) pdf_path = _generate_onboarding_intro_pdf(obj)
obj.intro_pdf_path = str(pdf_path) obj.intro_pdf_path = str(pdf_path)
obj.save(update_fields=['intro_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') 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)) pdf_path = _generate_onboarding_intro_session_pdf(session, admin_signature_name=_display_user_name(request.user))
session.exported_pdf_path = str(pdf_path) session.exported_pdf_path = str(pdf_path)
session.save(update_fields=['exported_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) 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.completed_by_name = ''
session.exported_pdf_path = '' session.exported_pdf_path = ''
session.save(update_fields=['checklist_state', 'notes', 'status', 'completed_at', 'completed_by_name', '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) return redirect('onboarding_intro_session_page', request_id=request_id)
if action == 'complete': if action == 'complete':
session.status = 'completed' session.status = 'completed'
session.completed_at = timezone.now() session.completed_at = timezone.now()
session.completed_by_name = _display_user_name(request.user) 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: else:
session.status = 'draft' session.status = 'draft'
session.completed_at = None session.completed_at = None
session.completed_by_name = '' session.completed_by_name = ''
messages.success(request, 'Einweisung wurde als Entwurf gespeichert.') messages.success(request, _('Einweisung wurde als Entwurf gespeichert.'))
session.save() session.save()
return redirect('onboarding_intro_session_page', request_id=request_id) return redirect('onboarding_intro_session_page', request_id=request_id)