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:
- `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
- Onboarding form with labels mapped from your CSV schema.
- Stores requests in PostgreSQL.

View File

@@ -6,7 +6,7 @@ ENV PYTHONUNBUFFERED=1
WORKDIR /app
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/*
RUN groupadd -g 1000 app && useradd -u 1000 -g app -m app

View File

@@ -38,6 +38,7 @@ INSTALLED_APPS = [
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
@@ -83,7 +84,12 @@ AUTH_PASSWORD_VALIDATORS = [
{'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'
USE_I18N = True
USE_TZ = True

View File

@@ -5,6 +5,7 @@ from django.urls import include, path
urlpatterns = [
path('admin/', admin.site.urls),
path('i18n/', include('django.conf.urls.i18n')),
path('accounts/', include('django.contrib.auth.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>
<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)