diff --git a/backend/locale/en/LC_MESSAGES/django.mo b/backend/locale/en/LC_MESSAGES/django.mo
index 46923b3..1f6f36c 100644
Binary files a/backend/locale/en/LC_MESSAGES/django.mo and b/backend/locale/en/LC_MESSAGES/django.mo differ
diff --git a/backend/locale/en/LC_MESSAGES/django.po b/backend/locale/en/LC_MESSAGES/django.po
index 8758ccb..8bc10ab 100644
--- a/backend/locale/en/LC_MESSAGES/django.po
+++ b/backend/locale/en/LC_MESSAGES/django.po
@@ -2,13 +2,243 @@ msgid ""
msgstr ""
"Project-Id-Version: tubco-portal\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2026-03-26 10:38+0000\n"
+"POT-Creation-Date: 2026-03-26 10:55+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"
+#: workflows/app_registry.py:32 workflows/models.py:261 workflows/models.py:342
+#: workflows/templates/workflows/onboarding_form.html:25
+#: workflows/templates/workflows/requests_dashboard.html:68
+#: workflows/templates/workflows/requests_dashboard.html:131
+msgid "Onboarding"
+msgstr "Onboarding"
+
+#: workflows/app_registry.py:33
+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."
+
+#: workflows/app_registry.py:34
+msgid "Onboarding starten"
+msgstr "Start onboarding"
+
+#: workflows/app_registry.py:36
+msgid "Mehrschritt-Formular"
+msgstr "Multi-step form"
+
+#: workflows/app_registry.py:36
+msgid "E-Mail Routing"
+msgstr "Email routing"
+
+#: workflows/app_registry.py:43 workflows/models.py:262 workflows/models.py:343
+#: workflows/templates/workflows/requests_dashboard.html:78
+#: workflows/templates/workflows/requests_dashboard.html:132
+msgid "Offboarding"
+msgstr "Offboarding"
+
+#: workflows/app_registry.py:44
+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."
+
+#: workflows/app_registry.py:45
+msgid "Offboarding starten"
+msgstr "Start offboarding"
+
+#: workflows/app_registry.py:47
+msgid "Profile-Suche"
+msgstr "Profile search"
+
+#: workflows/app_registry.py:47
+msgid "Hardware-Liste"
+msgstr "Hardware list"
+
+#: workflows/app_registry.py:47
+msgid "IT-Rückgabe"
+msgstr "IT return"
+
+#: workflows/app_registry.py:54
+#: workflows/templates/workflows/requests_dashboard.html:4
+#: workflows/templates/workflows/requests_dashboard.html:33
+msgid "Anfragen Dashboard"
+msgstr "Requests Dashboard"
+
+#: workflows/app_registry.py:55
+msgid ""
+"Status, Suchfunktion, PDF-Links und Verlauf aller Onboarding-/Offboarding-"
+"Anfragen."
+msgstr ""
+"Status, search, PDF links, and history of all onboarding/offboarding "
+"requests."
+
+#: workflows/app_registry.py:56
+msgid "Dashboard öffnen"
+msgstr "Open dashboard"
+
+#: workflows/app_registry.py:59
+msgid "Suche"
+msgstr "Search"
+
+#: workflows/app_registry.py:59
+#: workflows/templates/workflows/backup_recovery.html:40
+#: workflows/templates/workflows/onboarding_intro_session.html:37
+#: workflows/templates/workflows/request_timeline.html:70
+#: workflows/templates/workflows/requests_dashboard.html:136
+#: workflows/templates/workflows/welcome_emails.html:85
+msgid "Status"
+msgstr "Status"
+
+#: workflows/app_registry.py:59
+msgid "PDF Zugriff"
+msgstr "PDF access"
+
+#: workflows/app_registry.py:65
+#: workflows/templates/workflows/branding_settings.html:4
+#: workflows/templates/workflows/branding_settings.html:12
+msgid "Branding"
+msgstr "Branding"
+
+#: workflows/app_registry.py:66
+msgid "Logo, Portalname, Farben und PDF-Briefkopf verwalten."
+msgstr "Manage logo, portal name, colors, and PDF letterhead."
+
+#: workflows/app_registry.py:67 workflows/app_registry.py:76
+#: workflows/app_registry.py:85 workflows/app_registry.py:94
+#: workflows/app_registry.py:103 workflows/app_registry.py:112
+#: workflows/app_registry.py:121 workflows/app_registry.py:130
+#: workflows/app_registry.py:139 workflows/app_registry.py:148
+#: workflows/app_registry.py:157
+msgid "Öffnen"
+msgstr "Open"
+
+#: workflows/app_registry.py:74
+#: workflows/templates/workflows/app_registry.html:4
+#: workflows/templates/workflows/app_registry.html:12
+msgid "App Registry"
+msgstr ""
+
+#: workflows/app_registry.py:75
+msgid "Apps zentral aktivieren, sortieren und für Kundenauftritte vorbereiten."
+msgstr ""
+
+#: workflows/app_registry.py:83
+msgid "Integrationen"
+msgstr "Integrations"
+
+#: workflows/app_registry.py:84
+msgid "Nextcloud- und E-Mail-Setup."
+msgstr "Nextcloud and email setup."
+
+#: workflows/app_registry.py:92
+#: workflows/templates/workflows/user_management.html:4
+#: workflows/templates/workflows/user_management.html:14
+msgid "Benutzer & Rollen"
+msgstr "Users & roles"
+
+#: workflows/app_registry.py:93
+msgid "Benutzer anlegen, Rollen zuweisen und Zugriffe steuern."
+msgstr "Create users, assign roles, and control access."
+
+#: workflows/app_registry.py:101 workflows/templates/workflows/audit_log.html:4
+#: workflows/templates/workflows/audit_log.html:15
+msgid "Audit Log"
+msgstr ""
+
+#: workflows/app_registry.py:102
+msgid "Wichtige Admin-Aktionen nachvollziehen und prüfen."
+msgstr ""
+
+#: workflows/app_registry.py:110
+#: workflows/templates/workflows/backup_recovery.html:4
+#: workflows/templates/workflows/backup_recovery.html:12
+msgid "Backup & Recovery"
+msgstr "Backup & Recovery"
+
+#: workflows/app_registry.py:111
+msgid "Backups erstellen und sicher verifizieren."
+msgstr ""
+
+#: workflows/app_registry.py:119
+#: workflows/templates/workflows/welcome_emails.html:4
+msgid "Welcome E-Mails"
+msgstr "Welcome Emails"
+
+#: workflows/app_registry.py:120
+msgid "Geplante Welcome Mails verwalten."
+msgstr "Manage scheduled welcome emails."
+
+#: workflows/app_registry.py:128
+#: workflows/templates/workflows/form_builder.html:4
+#: workflows/templates/workflows/form_builder.html:14
+msgid "Form Builder"
+msgstr "Form Builder"
+
+#: workflows/app_registry.py:129
+msgid "Felder, Schritte und Optionen verwalten."
+msgstr "Manage fields, steps, and options."
+
+#: workflows/app_registry.py:137
+#: workflows/templates/workflows/intro_builder.html:4
+#: workflows/templates/workflows/intro_builder.html:17
+msgid "Einweisungs-Builder"
+msgstr "Introduction Builder"
+
+#: workflows/app_registry.py:138
+msgid "Checklistenpunkte für das Einweisungsprotokoll konfigurieren."
+msgstr "Configure checklist items for the introduction protocol."
+
+#: workflows/app_registry.py:146 workflows/templates/workflows/handbook.html:4
+#: workflows/templates/workflows/handbook.html:15
+msgid "Handbook"
+msgstr "Handbook"
+
+#: workflows/app_registry.py:147
+msgid "Project wiki and developer documentation in one place."
+msgstr "Project wiki and developer documentation in one place."
+
+#: workflows/app_registry.py:155
+msgid "Django Admin"
+msgstr "Django Admin"
+
+#: workflows/app_registry.py:156
+msgid "Vollständige Datenverwaltung."
+msgstr "Full data management."
+
+#: workflows/app_registry.py:165 workflows/models.py:68
+msgid "Apps"
+msgstr "Apps"
+
+#: workflows/app_registry.py:166
+msgid "Wählen Sie den gewünschten Prozess."
+msgstr "Choose the desired process."
+
+#: workflows/app_registry.py:171 workflows/models.py:69
+msgid "Platform Apps"
+msgstr ""
+
+#: workflows/app_registry.py:172
+#, fuzzy
+#| msgid "Konfiguration, Tests und Steuerung."
+msgid "Produktweite Konfiguration und Produktsteuerung."
+msgstr "Configuration, tests, and controls."
+
+#: workflows/app_registry.py:177 workflows/models.py:70
+msgid "Admin Apps"
+msgstr "Admin Apps"
+
+#: workflows/app_registry.py:178
+msgid "Konfiguration, Tests und Steuerung."
+msgstr "Configuration, tests, and controls."
+
#: workflows/backup_ops.py:141
msgid "Remote Backup ist deaktiviert."
msgstr ""
@@ -55,40 +285,40 @@ msgstr ""
msgid "Remote Backup in Nextcloud konnte nicht gelöscht werden."
msgstr ""
-#: workflows/forms.py:103 workflows/forms.py:128
+#: workflows/forms.py:104 workflows/forms.py:129
#: workflows/templates/workflows/user_management.html:72
#: workflows/templates/workflows/user_management.html:170
msgid "Benutzername"
msgstr ""
-#: workflows/forms.py:104
+#: workflows/forms.py:105
msgid "Passwort"
msgstr "Password"
-#: workflows/forms.py:108 workflows/forms.py:129
+#: workflows/forms.py:109 workflows/forms.py:130
#, fuzzy
#| msgid "E-Mail"
msgid "E-Mail-Adresse"
msgstr "Email"
-#: workflows/forms.py:113 workflows/templates/workflows/user_management.html:77
+#: workflows/forms.py:114 workflows/templates/workflows/user_management.html:77
#: workflows/templates/workflows/user_management.html:108
msgid "Neues Passwort"
msgstr "New password"
-#: workflows/forms.py:119
+#: workflows/forms.py:120
msgid "Neues Passwort bestätigen"
msgstr "Confirm new password"
-#: workflows/forms.py:126
+#: workflows/forms.py:127
msgid "Vorname"
msgstr ""
-#: workflows/forms.py:127
+#: workflows/forms.py:128
msgid "Nachname"
msgstr ""
-#: workflows/forms.py:130 workflows/templates/workflows/user_management.html:74
+#: workflows/forms.py:131 workflows/templates/workflows/user_management.html:74
#: workflows/templates/workflows/user_management.html:93
#: workflows/templates/workflows/user_management.html:171
#, fuzzy
@@ -96,318 +326,319 @@ msgstr ""
msgid "Rolle"
msgstr "Role:"
-#: workflows/forms.py:144
+#: workflows/forms.py:145
msgid "Dieser Benutzername ist bereits vergeben."
msgstr "This username is already taken."
-#: workflows/forms.py:153 workflows/views.py:567
+#: workflows/forms.py:154 workflows/views.py:616
msgid "Ungültige Rolle."
msgstr "Invalid role."
-#: workflows/forms.py:155 workflows/views.py:570
+#: workflows/forms.py:156 workflows/views.py:619
msgid "Nur Platform Owner dürfen diese Rolle vergeben."
msgstr ""
-#: workflows/forms.py:186
+#: workflows/forms.py:188
msgid "Portal-Titel"
msgstr "Portal title"
-#: workflows/forms.py:187
+#: workflows/forms.py:189
msgid "Firmenname"
msgstr "Company name"
-#: workflows/forms.py:188
+#: workflows/forms.py:190
+#, fuzzy
+#| msgid "Firmenname"
+msgid "Firmen-Domain"
+msgstr "Company name"
+
+#: workflows/forms.py:191
msgid "Support-E-Mail"
msgstr "Support email"
-#: workflows/forms.py:189
+#: workflows/forms.py:192
msgid "Standardsprache"
msgstr "Default language"
-#: workflows/forms.py:190
+#: workflows/forms.py:193
msgid "Logo"
msgstr "Logo"
-#: workflows/forms.py:191
+#: workflows/forms.py:194
msgid "PDF-Briefkopf"
msgstr "PDF letterhead"
-#: workflows/forms.py:192
+#: workflows/forms.py:195
msgid "Primärfarbe"
msgstr "Primary color"
-#: workflows/forms.py:193
+#: workflows/forms.py:196
msgid "Sekundärfarbe"
msgstr "Secondary color"
-#: workflows/forms.py:207
+#: workflows/forms.py:210
msgid "Das Logo darf maximal 5 MB groß sein."
msgstr ""
-#: workflows/forms.py:215
+#: workflows/forms.py:218
msgid "Der PDF-Briefkopf darf maximal 10 MB groß sein."
msgstr ""
-#: workflows/forms.py:458
+#: workflows/forms.py:357 workflows/forms.py:542
+#, python-format
+msgid "Bitte nutzen Sie das Format name@%(domain)s."
+msgstr ""
+
+#: workflows/forms.py:379 workflows/forms.py:556
+#, python-format
+msgid "Bitte verwenden Sie eine @%(domain)s E-Mail-Adresse."
+msgstr ""
+
+#: workflows/forms.py:464
#, python-format
msgid ""
"Das Übergabedatum muss mindestens %(days)s Tage in der Zukunft liegen "
"(frühestens %(date)s)."
msgstr ""
-#: workflows/models.py:90 workflows/views.py:199
+#: workflows/models.py:139 workflows/views.py:200
msgid "Eingereicht"
msgstr "Submitted"
-#: workflows/models.py:91 workflows/views.py:200
+#: workflows/models.py:140 workflows/views.py:201
msgid "In Bearbeitung"
msgstr "Processing"
-#: workflows/models.py:92 workflows/models.py:407 workflows/views.py:201
+#: workflows/models.py:141 workflows/models.py:456 workflows/views.py:202
msgid "Abgeschlossen"
msgstr "Completed"
-#: workflows/models.py:93 workflows/models.py:347
+#: workflows/models.py:142 workflows/models.py:396
#: workflows/templates/workflows/backup_recovery.html:70
#: workflows/templates/workflows/requests_dashboard.html:222
-#: workflows/templates/workflows/welcome_emails.html:108 workflows/views.py:202
+#: workflows/templates/workflows/welcome_emails.html:108 workflows/views.py:203
msgid "Fehlgeschlagen"
msgstr "Failed"
-#: workflows/models.py:100
+#: workflows/models.py:149
msgid "Herr"
msgstr ""
-#: workflows/models.py:100
+#: workflows/models.py:149
msgid "Frau"
msgstr ""
-#: workflows/models.py:100
+#: workflows/models.py:149
msgid "Divers"
msgstr ""
-#: workflows/models.py:110
+#: workflows/models.py:159
msgid "befristet"
msgstr ""
-#: workflows/models.py:110
+#: workflows/models.py:159
msgid "unbefristet"
msgstr ""
-#: workflows/models.py:173
+#: workflows/models.py:222
#: workflows/templates/workflows/onboarding_intro_session.html:28
#: workflows/templates/workflows/requests_dashboard.html:145
msgid "Abteilung"
msgstr "Department"
-#: workflows/models.py:174
+#: workflows/models.py:223
msgid "Geräte"
msgstr ""
-#: workflows/models.py:175
+#: workflows/models.py:224
msgid "Software"
msgstr ""
-#: workflows/models.py:176
+#: workflows/models.py:225
#, fuzzy
#| msgid "Vorgänge"
msgid "Zugänge"
msgstr "Requests"
-#: workflows/models.py:177
+#: workflows/models.py:226
msgid "Workspace-Gruppen"
msgstr ""
-#: workflows/models.py:178
+#: workflows/models.py:227
msgid "Ressourcen"
msgstr ""
-#: workflows/models.py:179
+#: workflows/models.py:228
msgid "Telefonnummern"
msgstr ""
-#: workflows/models.py:205
+#: workflows/models.py:254
msgid "Automatisch"
msgstr ""
-#: workflows/models.py:206 workflows/views.py:94
+#: workflows/models.py:255 workflows/views.py:95
msgid "Stammdaten"
msgstr "Master data"
-#: workflows/models.py:207 workflows/views.py:95
+#: workflows/models.py:256 workflows/views.py:96
msgid "Vertrag"
msgstr "Contract"
-#: workflows/models.py:208 workflows/views.py:96
+#: workflows/models.py:257 workflows/views.py:97
msgid "IT-Setup"
msgstr "IT setup"
-#: workflows/models.py:209 workflows/views.py:97
+#: workflows/models.py:258 workflows/views.py:98
msgid "Abschluss"
msgstr "Finish"
-#: workflows/models.py:212 workflows/models.py:293
-#: workflows/templates/workflows/home.html:62
-#: workflows/templates/workflows/onboarding_form.html:25
-#: workflows/templates/workflows/requests_dashboard.html:68
-#: workflows/templates/workflows/requests_dashboard.html:131
-msgid "Onboarding"
-msgstr "Onboarding"
-
-#: workflows/models.py:213 workflows/models.py:294
-#: workflows/templates/workflows/home.html:78
-#: workflows/templates/workflows/requests_dashboard.html:78
-#: workflows/templates/workflows/requests_dashboard.html:132
-msgid "Offboarding"
-msgstr "Offboarding"
-
-#: workflows/models.py:251
+#: workflows/models.py:300
#, fuzzy
#| msgid "Onboarding"
msgid "Onboarding: IT"
msgstr "Onboarding"
-#: workflows/models.py:252
+#: workflows/models.py:301
#, fuzzy
#| msgid "Offboarding-Anfrage speichern"
msgid "Onboarding: Allgemeine Info"
msgstr "Save offboarding request"
-#: workflows/models.py:253
+#: workflows/models.py:302
#, fuzzy
#| msgid "Onboarding starten"
msgid "Onboarding: Visitenkarte"
msgstr "Start onboarding"
-#: workflows/models.py:254
+#: workflows/models.py:303
#, fuzzy
#| msgid "Onboarding"
msgid "Onboarding: HR Works"
msgstr "Onboarding"
-#: workflows/models.py:255
+#: workflows/models.py:304
#, fuzzy
#| msgid "Onboarding starten"
msgid "Onboarding: Schlüssel"
msgstr "Start onboarding"
-#: workflows/models.py:256
+#: workflows/models.py:305
msgid "Onboarding: Referenz Anfordernde Person"
msgstr ""
-#: workflows/models.py:257
+#: workflows/models.py:306
#, fuzzy
#| msgid "Welcome E-Mails"
msgid "Onboarding: Welcome E-Mail"
msgstr "Welcome Emails"
-#: workflows/models.py:258
+#: workflows/models.py:307
#, fuzzy
#| msgid "Offboarding"
msgid "Offboarding: IT"
msgstr "Offboarding"
-#: workflows/models.py:259
+#: workflows/models.py:308
#, fuzzy
#| msgid "Offboarding-Anfrage speichern"
msgid "Offboarding: Allgemeine Info"
msgstr "Save offboarding request"
-#: workflows/models.py:260
+#: workflows/models.py:309
#, fuzzy
#| msgid "Offboarding starten"
msgid "Offboarding: HR Works Deaktivierung"
msgstr "Start offboarding"
-#: workflows/models.py:261
+#: workflows/models.py:310
msgid "Offboarding: Referenz Anfordernde Person"
msgstr ""
-#: workflows/models.py:297
+#: workflows/models.py:346
msgid "Immer"
msgstr ""
-#: workflows/models.py:298 workflows/models.py:376
+#: workflows/models.py:347 workflows/models.py:425
msgid "Enthält"
msgstr ""
-#: workflows/models.py:299 workflows/models.py:377
+#: workflows/models.py:348 workflows/models.py:426
msgid "Ist gleich"
msgstr ""
-#: workflows/models.py:300
+#: workflows/models.py:349
msgid "Ist aktiv/Ja"
msgstr ""
-#: workflows/models.py:301
+#: workflows/models.py:350
#, fuzzy
#| msgid "inaktiv"
msgid "Ist inaktiv/Nein"
msgstr "inactive"
-#: workflows/models.py:343
+#: workflows/models.py:392
#: workflows/templates/workflows/welcome_emails.html:100
msgid "Geplant"
msgstr "Scheduled"
-#: workflows/models.py:344
+#: workflows/models.py:393
#: workflows/templates/workflows/welcome_emails.html:102
msgid "Pausiert"
msgstr "Paused"
-#: workflows/models.py:345
+#: workflows/models.py:394
#: workflows/templates/workflows/welcome_emails.html:104
msgid "Abgebrochen"
msgstr "Cancelled"
-#: workflows/models.py:346
+#: workflows/models.py:395
#: workflows/templates/workflows/welcome_emails.html:106
msgid "Gesendet"
msgstr "Sent"
-#: workflows/models.py:369 workflows/tasks.py:576
+#: workflows/models.py:418 workflows/tasks.py:576
msgid "Geräte und Arbeitsplatz"
msgstr "Devices and workplace"
-#: workflows/models.py:370 workflows/tasks.py:577
+#: workflows/models.py:419 workflows/tasks.py:577
msgid "Konten und Berechtigungen"
msgstr "Accounts and permissions"
-#: workflows/models.py:371 workflows/tasks.py:578
+#: workflows/models.py:420 workflows/tasks.py:578
msgid "Software und Tools"
msgstr "Software and tools"
-#: workflows/models.py:372 workflows/tasks.py:579
+#: workflows/models.py:421 workflows/tasks.py:579
msgid "Prozesse und Hinweise"
msgstr "Processes and notes"
-#: workflows/models.py:375
+#: workflows/models.py:424
msgid "Immer anzeigen"
msgstr "Always show"
-#: workflows/models.py:378
+#: workflows/models.py:427
msgid "Ist Ja / aktiv"
msgstr "Is yes / active"
-#: workflows/models.py:379
+#: workflows/models.py:428
msgid "Ist Nein / inaktiv"
msgstr "Is no / inactive"
-#: workflows/models.py:406
+#: workflows/models.py:455
msgid "Entwurf"
msgstr "Draft"
-#: workflows/models.py:426
+#: workflows/models.py:475
#, fuzzy
#| msgid "Nextcloud:"
msgid "Nextcloud"
msgstr "Nextcloud:"
-#: workflows/models.py:427
+#: workflows/models.py:476
msgid "S3"
msgstr ""
-#: workflows/models.py:428
+#: workflows/models.py:477
msgid "NFS"
msgstr ""
@@ -646,12 +877,80 @@ msgstr ""
msgid "Link anfordern"
msgstr "Request link"
-#: workflows/templates/workflows/audit_log.html:4
-#: workflows/templates/workflows/audit_log.html:15
-#: workflows/templates/workflows/home.html:148
-msgid "Audit Log"
+#: workflows/templates/workflows/app_registry.html:13
+msgid ""
+"Apps zentral steuern, für Kunden vorbereiten und ohne Template-Eingriffe auf "
+"der Landing Page ausspielen."
msgstr ""
+#: workflows/templates/workflows/app_registry.html:19
+msgid ""
+"Sicherheit bleibt codebasiert: Sichtbarkeit und Reihenfolge sind hier "
+"steuerbar, Berechtigungen weiterhin über Rollen und Capabilities."
+msgstr ""
+
+#: workflows/templates/workflows/app_registry.html:20
+#, fuzzy
+#| msgid "Produktion"
+msgid "Produktkern"
+msgstr "Production"
+
+#: workflows/templates/workflows/app_registry.html:28
+msgid "Key"
+msgstr ""
+
+#: workflows/templates/workflows/app_registry.html:29
+#: workflows/templates/workflows/form_builder.html:91
+#: workflows/templates/workflows/integrations_setup.html:263
+#: workflows/templates/workflows/intro_builder.html:65
+#: workflows/templates/workflows/user_management.html:75
+msgid "Aktiv"
+msgstr "Active"
+
+#: workflows/templates/workflows/app_registry.html:30
+#, fuzzy
+#| msgid "Eingereicht"
+msgid "Bereich"
+msgstr "Submitted"
+
+#: workflows/templates/workflows/app_registry.html:31
+#, fuzzy
+#| msgid "Reihenfolge speichern"
+msgid "Reihenfolge"
+msgstr "Save order"
+
+#: workflows/templates/workflows/app_registry.html:32
+msgid "Titel DE"
+msgstr ""
+
+#: workflows/templates/workflows/app_registry.html:33
+msgid "Titel EN"
+msgstr ""
+
+#: workflows/templates/workflows/app_registry.html:34
+#, fuzzy
+#| msgid "Aktion"
+msgid "Aktion DE"
+msgstr "Action"
+
+#: workflows/templates/workflows/app_registry.html:35
+#, fuzzy
+#| msgid "Aktion"
+msgid "Aktion EN"
+msgstr "Action"
+
+#: workflows/templates/workflows/app_registry.html:78
+msgid ""
+"Empfehlung: Produktweite Apps sparsam halten, kundenbezogene Prozesse unter "
+"Apps oder Admin Apps einordnen."
+msgstr ""
+
+#: workflows/templates/workflows/app_registry.html:79
+#, fuzzy
+#| msgid "Regeln speichern"
+msgid "App Registry speichern"
+msgstr "Save rules"
+
#: workflows/templates/workflows/audit_log.html:16
msgid "Nachvollziehbarkeit aller wichtigen Admin-Aktionen im Portal."
msgstr ""
@@ -745,12 +1044,6 @@ msgstr ""
msgid "Noch keine Audit-Einträge vorhanden."
msgstr "No requests available yet."
-#: workflows/templates/workflows/backup_recovery.html:4
-#: workflows/templates/workflows/backup_recovery.html:12
-#: workflows/templates/workflows/home.html:155
-msgid "Backup & Recovery"
-msgstr "Backup & Recovery"
-
#: workflows/templates/workflows/backup_recovery.html:13
msgid ""
"Datenbank- und Media-Backups erstellen und vorhandene Bundles sicher "
@@ -803,15 +1096,6 @@ msgstr "Created"
msgid "Verifiziert"
msgstr "Verified"
-#: workflows/templates/workflows/backup_recovery.html:40
-#: workflows/templates/workflows/home.html:99
-#: workflows/templates/workflows/onboarding_intro_session.html:37
-#: workflows/templates/workflows/request_timeline.html:70
-#: workflows/templates/workflows/requests_dashboard.html:136
-#: workflows/templates/workflows/welcome_emails.html:85
-msgid "Status"
-msgstr "Status"
-
#: workflows/templates/workflows/backup_recovery.html:41
msgid "Inhalt"
msgstr "Contents"
@@ -914,39 +1198,39 @@ msgstr "Action in progress"
msgid "Die Aktion wird im aktuellen Tab ausgeführt."
msgstr "The action is running in the current tab."
-#: workflows/templates/workflows/branding_settings.html:4
-#: workflows/templates/workflows/branding_settings.html:12
-#: workflows/templates/workflows/home.html:118
-msgid "Branding"
-msgstr "Branding"
-
#: workflows/templates/workflows/branding_settings.html:13
msgid "Portalname, Firmenauftritt, Logo und PDF-Briefkopf zentral verwalten."
msgstr ""
"Manage portal name, company branding, logo, and PDF letterhead centrally."
-#: workflows/templates/workflows/branding_settings.html:48
+#: workflows/templates/workflows/branding_settings.html:32
+msgid ""
+"Wird für E-Mail-Vorschläge und Domain-bezogene Standardtexte verwendet, z. "
+"B. tub.co."
+msgstr ""
+
+#: workflows/templates/workflows/branding_settings.html:53
msgid "Erlaubte Formate: SVG, PNG, JPG, JPEG, WEBP. Maximal 5 MB."
msgstr ""
-#: workflows/templates/workflows/branding_settings.html:51
+#: workflows/templates/workflows/branding_settings.html:56
msgid "Aktuelles Logo:"
msgstr "Current logo:"
-#: workflows/templates/workflows/branding_settings.html:51
-#: workflows/templates/workflows/branding_settings.html:60
+#: workflows/templates/workflows/branding_settings.html:56
+#: workflows/templates/workflows/branding_settings.html:65
msgid "öffnen"
msgstr "open"
-#: workflows/templates/workflows/branding_settings.html:57
+#: workflows/templates/workflows/branding_settings.html:62
msgid "Erlaubtes Format: PDF. Maximal 10 MB."
msgstr ""
-#: workflows/templates/workflows/branding_settings.html:60
+#: workflows/templates/workflows/branding_settings.html:65
msgid "Aktueller Briefkopf:"
msgstr "Current letterhead:"
-#: workflows/templates/workflows/branding_settings.html:65
+#: workflows/templates/workflows/branding_settings.html:70
msgid ""
"TUBCO bleibt als Standard erhalten, bis hier Werte geändert oder Dateien "
"hochgeladen werden."
@@ -954,16 +1238,10 @@ msgstr ""
"TUBCO remains the default until values are changed or files are uploaded "
"here."
-#: workflows/templates/workflows/branding_settings.html:66
+#: workflows/templates/workflows/branding_settings.html:71
msgid "Branding speichern"
msgstr "Save branding"
-#: workflows/templates/workflows/form_builder.html:4
-#: workflows/templates/workflows/form_builder.html:14
-#: workflows/templates/workflows/home.html:169
-msgid "Form Builder"
-msgstr "Form Builder"
-
#: workflows/templates/workflows/form_builder.html:15
msgid "Felder per Drag-and-Drop sortieren und pro Schritt gruppieren."
msgstr "Sort fields by drag and drop and group them by step."
@@ -1020,13 +1298,6 @@ msgstr "Sort order"
msgid "Label (EN)"
msgstr "Label (EN)"
-#: workflows/templates/workflows/form_builder.html:91
-#: workflows/templates/workflows/integrations_setup.html:263
-#: workflows/templates/workflows/intro_builder.html:65
-#: workflows/templates/workflows/user_management.html:75
-msgid "Aktiv"
-msgstr "Active"
-
#: workflows/templates/workflows/form_builder.html:100
msgid "Ziehen zum Sortieren"
msgstr "Drag to reorder"
@@ -1083,12 +1354,6 @@ msgstr "No field configurations available."
msgid "Feldtexte speichern"
msgstr "Save field text"
-#: workflows/templates/workflows/handbook.html:4
-#: workflows/templates/workflows/handbook.html:15
-#: workflows/templates/workflows/home.html:181
-msgid "Handbook"
-msgstr "Handbook"
-
#: workflows/templates/workflows/handbook.html:17
msgid ""
"Single documentation entry point for both operational knowledge and long-"
@@ -1263,181 +1528,7 @@ msgstr "Production"
msgid "PDF + E-Mail Workflow bereit"
msgstr "PDF + Email Workflow Ready"
-#: workflows/templates/workflows/home.html:55
-msgid "Apps"
-msgstr "Apps"
-
-#: workflows/templates/workflows/home.html:56
-msgid "Wählen Sie den gewünschten Prozess."
-msgstr "Choose the desired process."
-
-#: workflows/templates/workflows/home.html:63
-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."
-
-#: workflows/templates/workflows/home.html:65
-msgid "Mehrschritt-Formular"
-msgstr "Multi-step form"
-
-#: workflows/templates/workflows/home.html:67
-msgid "E-Mail Routing"
-msgstr "Email routing"
-
-#: workflows/templates/workflows/home.html:71
-msgid "Onboarding starten"
-msgstr "Start onboarding"
-
-#: workflows/templates/workflows/home.html:79
-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."
-
-#: workflows/templates/workflows/home.html:81
-msgid "Profile-Suche"
-msgstr "Profile search"
-
-#: workflows/templates/workflows/home.html:82
-msgid "Hardware-Liste"
-msgstr "Hardware list"
-
-#: workflows/templates/workflows/home.html:83
-msgid "IT-Rückgabe"
-msgstr "IT return"
-
-#: workflows/templates/workflows/home.html:87
-msgid "Offboarding starten"
-msgstr "Start offboarding"
-
-#: workflows/templates/workflows/home.html:95
-#: workflows/templates/workflows/requests_dashboard.html:4
-#: workflows/templates/workflows/requests_dashboard.html:33
-msgid "Anfragen Dashboard"
-msgstr "Requests Dashboard"
-
-#: workflows/templates/workflows/home.html:96
-msgid ""
-"Status, Suchfunktion, PDF-Links und Verlauf aller Onboarding-/Offboarding-"
-"Anfragen."
-msgstr ""
-"Status, search, PDF links, and history of all onboarding/offboarding "
-"requests."
-
-#: workflows/templates/workflows/home.html:98
-msgid "Suche"
-msgstr "Search"
-
-#: workflows/templates/workflows/home.html:100
-msgid "PDF Zugriff"
-msgstr "PDF access"
-
-#: workflows/templates/workflows/home.html:104
-msgid "Dashboard öffnen"
-msgstr "Open dashboard"
-
-#: workflows/templates/workflows/home.html:113
-msgid "Platform Apps"
-msgstr ""
-
-#: workflows/templates/workflows/home.html:114
-#, fuzzy
-#| msgid "Konfiguration, Tests und Steuerung."
-msgid "Produktweite Konfiguration und Produktsteuerung."
-msgstr "Configuration, tests, and controls."
-
-#: workflows/templates/workflows/home.html:119
-msgid "Logo, Portalname, Farben und PDF-Briefkopf verwalten."
-msgstr "Manage logo, portal name, colors, and PDF letterhead."
-
-#: workflows/templates/workflows/home.html:120
-#: workflows/templates/workflows/home.html:136
-#: workflows/templates/workflows/home.html:143
-#: workflows/templates/workflows/home.html:150
-#: workflows/templates/workflows/home.html:157
-#: workflows/templates/workflows/home.html:164
-#: workflows/templates/workflows/home.html:171
-#: workflows/templates/workflows/home.html:176
-#: workflows/templates/workflows/home.html:183
-#: workflows/templates/workflows/home.html:190
-msgid "Öffnen"
-msgstr "Open"
-
-#: workflows/templates/workflows/home.html:128
-msgid "Admin Apps"
-msgstr "Admin Apps"
-
-#: workflows/templates/workflows/home.html:129
-msgid "Konfiguration, Tests und Steuerung."
-msgstr "Configuration, tests, and controls."
-
-#: workflows/templates/workflows/home.html:134
-msgid "Integrationen"
-msgstr "Integrations"
-
-#: workflows/templates/workflows/home.html:135
-msgid "Nextcloud- und E-Mail-Setup."
-msgstr "Nextcloud and email setup."
-
-#: workflows/templates/workflows/home.html:141
-#: workflows/templates/workflows/user_management.html:4
-#: workflows/templates/workflows/user_management.html:14
-msgid "Benutzer & Rollen"
-msgstr "Users & roles"
-
-#: workflows/templates/workflows/home.html:142
-msgid "Benutzer anlegen, Rollen zuweisen und Zugriffe steuern."
-msgstr "Create users, assign roles, and control access."
-
-#: workflows/templates/workflows/home.html:149
-msgid "Wichtige Admin-Aktionen nachvollziehen und prüfen."
-msgstr ""
-
-#: workflows/templates/workflows/home.html:156
-msgid "Backups erstellen und sicher verifizieren."
-msgstr ""
-
-#: workflows/templates/workflows/home.html:162
-#: workflows/templates/workflows/welcome_emails.html:4
-msgid "Welcome E-Mails"
-msgstr "Welcome Emails"
-
-#: workflows/templates/workflows/home.html:163
-msgid "Geplante Welcome Mails verwalten."
-msgstr "Manage scheduled welcome emails."
-
-#: workflows/templates/workflows/home.html:170
-msgid "Felder, Schritte und Optionen verwalten."
-msgstr "Manage fields, steps, and options."
-
-#: workflows/templates/workflows/home.html:174
-#: workflows/templates/workflows/intro_builder.html:4
-#: workflows/templates/workflows/intro_builder.html:17
-msgid "Einweisungs-Builder"
-msgstr "Introduction Builder"
-
-#: workflows/templates/workflows/home.html:175
-msgid "Checklistenpunkte für das Einweisungsprotokoll konfigurieren."
-msgstr "Configure checklist items for the introduction protocol."
-
-#: workflows/templates/workflows/home.html:182
-msgid "Project wiki and developer documentation in one place."
-msgstr "Project wiki and developer documentation in one place."
-
-#: workflows/templates/workflows/home.html:188
-msgid "Django Admin"
-msgstr "Django Admin"
-
-#: workflows/templates/workflows/home.html:189
-msgid "Vollständige Datenverwaltung."
-msgstr "Full data management."
-
-#: workflows/templates/workflows/home.html:197
+#: workflows/templates/workflows/home.html:94
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."
@@ -1842,7 +1933,9 @@ msgid "Mitarbeitende suchen (Name oder E-Mail)"
msgstr "Search employees (name or email)"
#: workflows/templates/workflows/offboarding_form.html:31
-msgid "z. B. max.mustermann@tub.co"
+#, fuzzy, python-format
+#| msgid "z. B. max.mustermann@tub.co"
+msgid "z. B. max.mustermann@%(domain)s"
msgstr "e.g. john.doe@tub.co"
#: workflows/templates/workflows/offboarding_form.html:33
@@ -2002,7 +2095,7 @@ msgid "Dienstliche E-Mail"
msgstr "Work email"
#: workflows/templates/workflows/onboarding_intro_session.html:31
-#: workflows/views.py:820
+#: workflows/views.py:869
msgid "Vertragsbeginn"
msgstr "Contract start"
@@ -2651,244 +2744,256 @@ msgstr "Resume"
msgid "Keine geplanten Welcome E-Mails vorhanden."
msgstr "No scheduled welcome emails available."
-#: workflows/views.py:94
+#: workflows/views.py:95
msgid "Person, Rolle, Abteilung"
msgstr "Person, role, department"
-#: workflows/views.py:95
+#: workflows/views.py:96
msgid "Beschäftigung und Termine"
msgstr "Employment and dates"
-#: workflows/views.py:96
+#: workflows/views.py:97
msgid "Geräte, Software und Zugänge"
msgstr "Devices, software, and access"
-#: workflows/views.py:97
+#: workflows/views.py:98
msgid "Notizen und Freigabe"
msgstr "Notes and approval"
-#: workflows/views.py:128 workflows/views.py:906 workflows/views.py:911
+#: workflows/views.py:129 workflows/views.py:955 workflows/views.py:960
msgid "Sie haben keine Berechtigung für diese Aktion."
msgstr "You do not have permission for this action."
-#: workflows/views.py:209
+#: workflows/views.py:210
#, fuzzy
#| msgid "Vorgänge"
msgid "Vorgänge gelöscht"
msgstr "Requests"
-#: workflows/views.py:210
+#: workflows/views.py:211
msgid "Vorgang gelöscht"
msgstr ""
-#: workflows/views.py:211
+#: workflows/views.py:212
msgid "Vorgang erneut angestoßen"
msgstr ""
-#: workflows/views.py:212
+#: workflows/views.py:213
#, fuzzy
#| msgid "Einweisung"
msgid "Einweisungs-PDF erzeugt"
msgstr "Introduction"
-#: workflows/views.py:213
+#: workflows/views.py:214
#, fuzzy
#| msgid "Live-Protokoll erzeugen"
msgid "Live-Protokoll erzeugt"
msgstr "Generate live protocol"
-#: workflows/views.py:214
+#: workflows/views.py:215
#, fuzzy
#| msgid "Einweisung wurde zurückgesetzt."
msgid "Einweisung zurückgesetzt"
msgstr "Introduction was reset."
-#: workflows/views.py:215
+#: workflows/views.py:216
#, fuzzy
#| msgid "Einweisung wurde als Entwurf gespeichert."
msgid "Einweisung als Entwurf gespeichert"
msgstr "Introduction was saved as draft."
-#: workflows/views.py:216
+#: workflows/views.py:217
#, fuzzy
#| msgid "Einweisung wurde als abgeschlossen gespeichert."
msgid "Einweisung abgeschlossen"
msgstr "Introduction was saved as completed."
-#: workflows/views.py:217
+#: workflows/views.py:218
msgid "Formularoption gelöscht"
msgstr ""
-#: workflows/views.py:218
+#: workflows/views.py:219
#, fuzzy
#| msgid "Optionen speichern"
msgid "Formularoptionen gespeichert"
msgstr "Save options"
-#: workflows/views.py:219
+#: workflows/views.py:220
#, fuzzy
#| msgid "Feldtexte speichern"
msgid "Feldtexte gespeichert"
msgstr "Save field text"
-#: workflows/views.py:220
+#: workflows/views.py:221
#, fuzzy
#| msgid "Offboarding-Anfrage speichern"
msgid "Formularlayout gespeichert"
msgstr "Save offboarding request"
-#: workflows/views.py:221
+#: workflows/views.py:222
msgid "Einweisungs-Checkpunkt gelöscht"
msgstr ""
-#: workflows/views.py:222
+#: workflows/views.py:223
msgid "Einweisungs-Checkpunkt hinzugefügt"
msgstr ""
-#: workflows/views.py:223
+#: workflows/views.py:224
#, fuzzy
#| msgid "Checkliste speichern"
msgid "Einweisungs-Checkliste gespeichert"
msgstr "Save checklist"
-#: workflows/views.py:224
+#: workflows/views.py:225
#, fuzzy
#| msgid "Welcome E-Mails"
msgid "Welcome E-Mail sofort ausgelöst"
msgstr "Welcome Emails"
-#: workflows/views.py:225
+#: workflows/views.py:226
#, fuzzy
#| msgid "Welcome-Einstellungen speichern"
msgid "Welcome E-Mail Einstellungen gespeichert"
msgstr "Save welcome settings"
-#: workflows/views.py:226
+#: workflows/views.py:227
msgid "Welcome E-Mail Sammelaktion ausgeführt"
msgstr ""
-#: workflows/views.py:227
+#: workflows/views.py:228
#, fuzzy
#| msgid "Welcome E-Mails"
msgid "Welcome E-Mail pausiert"
msgstr "Welcome Emails"
-#: workflows/views.py:228
+#: workflows/views.py:229
#, fuzzy
#| msgid "Welcome E-Mails"
msgid "Welcome E-Mail fortgesetzt"
msgstr "Welcome Emails"
-#: workflows/views.py:229
+#: workflows/views.py:230
#, fuzzy
#| msgid "Welcome E-Mails"
msgid "Welcome E-Mail abgebrochen"
msgstr "Welcome Emails"
-#: workflows/views.py:230
+#: workflows/views.py:231
#, fuzzy
#| msgid "SMTP-Test"
msgid "SMTP-Test gesendet"
msgstr "SMTP test"
-#: workflows/views.py:231
+#: workflows/views.py:232
#, fuzzy
#| msgid "Nextcloud-Test"
msgid "Nextcloud-Testupload ausgeführt"
msgstr "Nextcloud test"
-#: workflows/views.py:232
+#: workflows/views.py:233
#, fuzzy
#| msgid "Nextcloud schalten"
msgid "Nextcloud-Modus umgeschaltet"
msgstr "Toggle Nextcloud"
-#: workflows/views.py:233
+#: workflows/views.py:234
msgid "E-Mail-Modus umgeschaltet"
msgstr ""
-#: workflows/views.py:234
+#: workflows/views.py:235
#, fuzzy
#| msgid "Integrationen Setup"
msgid "Integrationen gespeichert"
msgstr "Integrations Setup"
-#: workflows/views.py:235
+#: workflows/views.py:236
#, fuzzy
#| msgid "Welcome-Einstellungen speichern"
msgid "Nextcloud-Einstellungen gespeichert"
msgstr "Save welcome settings"
-#: workflows/views.py:236
+#: workflows/views.py:237
#, fuzzy
#| msgid "Welcome-Einstellungen speichern"
msgid "Mail-Einstellungen gespeichert"
msgstr "Save welcome settings"
-#: workflows/views.py:237
+#: workflows/views.py:238
#, fuzzy
#| msgid "E-Mail Routing & Vorlagen speichern"
msgid "E-Mail-Routing gespeichert"
msgstr "Save email routing & templates"
-#: workflows/views.py:238
+#: workflows/views.py:239
#, fuzzy
#| msgid "Offboarding-Anfrage speichern"
msgid "Benachrichtigungsregeln gespeichert"
msgstr "Save offboarding request"
-#: workflows/views.py:239
+#: workflows/views.py:240
#, fuzzy
#| msgid "Anfrage gespeichert"
msgid "Benutzer erstellt"
msgstr "Request saved"
-#: workflows/views.py:240
+#: workflows/views.py:241
msgid "Benutzer aktualisiert"
msgstr ""
-#: workflows/views.py:241
+#: workflows/views.py:242
msgid "Passwort-Reset-Link versendet"
msgstr ""
-#: workflows/views.py:242
+#: workflows/views.py:243
#, fuzzy
#| msgid "Benutzerübersicht"
msgid "Benutzer gelöscht"
msgstr "User overview"
-#: workflows/views.py:243
+#: workflows/views.py:244
#, fuzzy
#| msgid "Anfrage gespeichert"
msgid "Backup erstellt"
msgstr "Request saved"
-#: workflows/views.py:244
+#: workflows/views.py:245
msgid "Backup verifiziert"
msgstr ""
-#: workflows/views.py:245
+#: workflows/views.py:246
#, fuzzy
#| msgid "Anfrage gespeichert"
msgid "Backup gelöscht"
msgstr "Request saved"
-#: workflows/views.py:246
+#: workflows/views.py:247
#, fuzzy
#| msgid "Welcome-Einstellungen speichern"
msgid "Backup-Einstellungen gespeichert"
msgstr "Save welcome settings"
-#: workflows/views.py:436
+#: workflows/views.py:248
+#, fuzzy
+#| msgid "Anfrage gespeichert"
+msgid "App-Registry gespeichert"
+msgstr "Request saved"
+
+#: workflows/views.py:386
+#, fuzzy
+#| msgid "Anfrage gespeichert"
+msgid "App-Registry gespeichert."
+msgstr "Request saved"
+
+#: workflows/views.py:485
msgid "Für diesen Benutzer ist keine E-Mail-Adresse hinterlegt."
msgstr ""
-#: workflows/views.py:445
+#: workflows/views.py:494
#, python-format
msgid "Zugangseinladung für %(username)s"
msgstr ""
-#: workflows/views.py:447
+#: workflows/views.py:496
#, python-format
msgid ""
"Hallo %(name)s,\n"
@@ -2901,12 +3006,12 @@ msgid ""
"Ihrem Administrator."
msgstr ""
-#: workflows/views.py:458
+#: workflows/views.py:507
#, python-format
msgid "Passwort zurücksetzen für %(username)s"
msgstr ""
-#: workflows/views.py:460
+#: workflows/views.py:509
#, python-format
msgid ""
"Hallo %(name)s,\n"
@@ -2919,7 +3024,7 @@ msgid ""
"ignorieren."
msgstr ""
-#: workflows/views.py:498
+#: workflows/views.py:547
#, fuzzy
#| msgid ""
#| "Benutzer konnte nicht erstellt werden. Bitte prüfen Sie die Eingaben."
@@ -2927,23 +3032,23 @@ msgid ""
"Branding konnte nicht gespeichert werden. Bitte prüfen Sie die Eingaben."
msgstr "User could not be created. Please check the input."
-#: workflows/views.py:524
+#: workflows/views.py:573
#, fuzzy
#| msgid "Offboarding-Anfrage speichern"
msgid "Portal-Branding wurde gespeichert."
msgstr "Save offboarding request"
-#: workflows/views.py:540
+#: workflows/views.py:589
msgid "Benutzer konnte nicht erstellt werden. Bitte prüfen Sie die Eingaben."
msgstr "User could not be created. Please check the input."
-#: workflows/views.py:553
+#: workflows/views.py:602
#, fuzzy, python-format
#| msgid "Benutzer wurde erstellt: %(username)s"
msgid "Benutzer wurde erstellt und eingeladen: %(username)s"
msgstr "User created: %(username)s"
-#: workflows/views.py:575
+#: workflows/views.py:624
#, fuzzy
#| msgid ""
#| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren "
@@ -2954,14 +3059,14 @@ msgid ""
msgstr ""
"The currently signed-in super admin cannot lock or downgrade themselves here."
-#: workflows/views.py:578
+#: workflows/views.py:627
msgid ""
"Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren oder "
"herabstufen."
msgstr ""
"The currently signed-in super admin cannot lock or downgrade themselves here."
-#: workflows/views.py:581
+#: workflows/views.py:630
#, fuzzy
#| msgid ""
#| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren "
@@ -2972,7 +3077,7 @@ msgid ""
msgstr ""
"The currently signed-in super admin cannot lock or downgrade themselves here."
-#: workflows/views.py:584
+#: workflows/views.py:633
#, fuzzy
#| msgid ""
#| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren "
@@ -2983,18 +3088,18 @@ msgid ""
msgstr ""
"The currently signed-in super admin cannot lock or downgrade themselves here."
-#: workflows/views.py:601
+#: workflows/views.py:650
#, python-format
msgid "Benutzer wurde aktualisiert: %(username)s"
msgstr "User updated: %(username)s"
-#: workflows/views.py:623
+#: workflows/views.py:672
#, fuzzy, python-format
#| msgid "Benutzer wurde erstellt: %(username)s"
msgid "Passwort-Reset-Link wurde versendet: %(username)s"
msgstr "User created: %(username)s"
-#: workflows/views.py:635
+#: workflows/views.py:684
#, fuzzy
#| msgid ""
#| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren "
@@ -3004,7 +3109,7 @@ msgid ""
msgstr ""
"The currently signed-in super admin cannot lock or downgrade themselves here."
-#: workflows/views.py:638
+#: workflows/views.py:687
#, fuzzy
#| msgid ""
#| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren "
@@ -3014,7 +3119,7 @@ msgid ""
msgstr ""
"The currently signed-in super admin cannot lock or downgrade themselves here."
-#: workflows/views.py:641
+#: workflows/views.py:690
#, fuzzy
#| msgid ""
#| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren "
@@ -3023,7 +3128,7 @@ msgid "Der letzte aktive Platform Owner kann nicht gelöscht werden."
msgstr ""
"The currently signed-in super admin cannot lock or downgrade themselves here."
-#: workflows/views.py:644
+#: workflows/views.py:693
#, fuzzy
#| msgid ""
#| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren "
@@ -3032,121 +3137,121 @@ msgid "Der letzte aktive Super Admin kann nicht gelöscht werden."
msgstr ""
"The currently signed-in super admin cannot lock or downgrade themselves here."
-#: workflows/views.py:657
+#: workflows/views.py:706
#, fuzzy, python-format
#| msgid "Benutzer wurde erstellt: %(username)s"
msgid "Benutzer wurde gelöscht: %(username)s"
msgstr "User created: %(username)s"
-#: workflows/views.py:744
+#: workflows/views.py:793
#, python-format
msgid "Backup wurde erstellt: %(name)s"
msgstr ""
-#: workflows/views.py:746
+#: workflows/views.py:795
#, python-format
msgid "Backup konnte nicht erstellt werden: %(error)s"
msgstr ""
-#: workflows/views.py:762
+#: workflows/views.py:811
#, python-format
msgid "Backup wurde verifiziert: %(name)s"
msgstr ""
-#: workflows/views.py:764
+#: workflows/views.py:813
#, python-format
msgid "Backup-Verifikation fehlgeschlagen: %(error)s"
msgstr ""
-#: workflows/views.py:780
+#: workflows/views.py:829
#, python-format
msgid "Backup wurde gelöscht: %(name)s"
msgstr ""
-#: workflows/views.py:782
+#: workflows/views.py:831
#, python-format
msgid "Backup konnte nicht gelöscht werden: %(error)s"
msgstr ""
-#: workflows/views.py:808
+#: workflows/views.py:857
#, fuzzy
#| msgid "Anfrage gespeichert"
msgid "Anfrage erstellt"
msgstr "Request saved"
-#: workflows/views.py:810
+#: workflows/views.py:859
#, fuzzy, python-format
#| msgid "Sitzungsstatus"
msgid "Status: %(status)s"
msgstr "Session status"
-#: workflows/views.py:822
+#: workflows/views.py:871
#, fuzzy
#| msgid "Geplant für"
msgid "Geplanter Start"
msgstr "Scheduled for"
-#: workflows/views.py:832
+#: workflows/views.py:881
msgid "Geräteübergabe / Hardware-Abholung"
msgstr ""
-#: workflows/views.py:834
+#: workflows/views.py:883
msgid "Geplanter Hardware-Termin"
msgstr ""
-#: workflows/views.py:843
+#: workflows/views.py:892
#, fuzzy
#| msgid "Noch nicht verfügbar"
msgid "PDF verfügbar"
msgstr "Not available yet"
-#: workflows/views.py:869
+#: workflows/views.py:918
#, fuzzy
#| msgid "Einweisung"
msgid "Einweisungssitzung"
msgstr "Introduction"
-#: workflows/views.py:881
+#: workflows/views.py:930
#, fuzzy
#| msgid "Welcome E-Mails"
msgid "Welcome E-Mail"
msgstr "Welcome Emails"
-#: workflows/views.py:920
+#: workflows/views.py:969
msgid "Keine Einträge ausgewählt."
msgstr "No entries selected."
-#: workflows/views.py:963
+#: workflows/views.py:1012
#, python-format
msgid "%(count)s Eintrag/Einträge gelöscht."
msgstr "%(count)s entry/entries deleted."
-#: workflows/views.py:965
+#: workflows/views.py:1014
#, python-format
msgid "%(count)s Auswahl(en) konnten nicht verarbeitet werden."
msgstr "%(count)s selection(s) could not be processed."
-#: workflows/views.py:967
+#: workflows/views.py:1016
msgid "Keine passenden Einträge gefunden."
msgstr "No matching entries found."
-#: workflows/views.py:1194
+#: workflows/views.py:1244
msgid "Einweisungs- und Übergabeprotokoll wurde erzeugt."
msgstr "Introduction and handover protocol was generated."
-#: workflows/views.py:1211
+#: workflows/views.py:1261
msgid "Einweisungsprotokoll aus Live-Status wurde erzeugt."
msgstr "Introduction protocol from live status was generated."
-#: workflows/views.py:1240
+#: workflows/views.py:1290
msgid "Einweisung wurde zurückgesetzt."
msgstr "Introduction was reset."
-#: workflows/views.py:1254
+#: workflows/views.py:1304
msgid "Einweisung wurde als abgeschlossen gespeichert."
msgstr "Introduction was saved as completed."
-#: workflows/views.py:1267
+#: workflows/views.py:1317
msgid "Einweisung wurde als Entwurf gespeichert."
msgstr "Introduction was saved as draft."
diff --git a/backend/workflows/admin.py b/backend/workflows/admin.py
index d142cb2..31a851f 100644
--- a/backend/workflows/admin.py
+++ b/backend/workflows/admin.py
@@ -3,7 +3,7 @@ from django.conf import settings
from django import forms
from .emailing import send_system_email
-from .models import AdminAuditLog, EmployeeProfile, FormFieldConfig, FormOption, IntroChecklistItem, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, PortalBranding, ScheduledWelcomeEmail, SystemEmailConfig, WorkflowConfig
+from .models import AdminAuditLog, EmployeeProfile, FormFieldConfig, FormOption, IntroChecklistItem, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, PortalAppConfig, PortalBranding, ScheduledWelcomeEmail, SystemEmailConfig, WorkflowConfig
@admin.register(EmployeeProfile)
@@ -25,6 +25,15 @@ class PortalBrandingAdmin(admin.ModelAdmin):
list_display = ('name', 'portal_title', 'company_name', 'support_email', 'default_language', 'updated_at')
+@admin.register(PortalAppConfig)
+class PortalAppConfigAdmin(admin.ModelAdmin):
+ list_display = ('key', 'section', 'sort_order', 'is_enabled', 'updated_at')
+ list_filter = ('section', 'is_enabled')
+ search_fields = ('key', 'title_override', 'title_override_en')
+ ordering = ('section', 'sort_order', 'key')
+ list_editable = ('section', 'sort_order', 'is_enabled')
+
+
@admin.register(OnboardingRequest)
class OnboardingRequestAdmin(admin.ModelAdmin):
list_display = ('id', 'full_name', 'work_email', 'department', 'contract_start', 'created_at')
diff --git a/backend/workflows/app_registry.py b/backend/workflows/app_registry.py
new file mode 100644
index 0000000..73290e9
--- /dev/null
+++ b/backend/workflows/app_registry.py
@@ -0,0 +1,255 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+
+from django.urls import reverse
+from django.utils.translation import gettext_lazy as _
+
+from .models import PortalAppConfig
+from .roles import user_has_capability
+
+
+@dataclass(frozen=True)
+class AppDefinition:
+ key: str
+ section: str
+ route_name: str
+ title: object
+ description: object
+ action_label: object
+ capability: str | None = None
+ accent: str = ''
+ accent_label: str = 'APP'
+ style_variant: str = ''
+ tags: tuple[object, ...] = ()
+
+
+APP_DEFINITIONS: tuple[AppDefinition, ...] = (
+ AppDefinition(
+ key='onboarding',
+ section=PortalAppConfig.SECTION_APP,
+ route_name='onboarding_create',
+ title=_('Onboarding'),
+ description=_('Neue Mitarbeitende erfassen, PDF mit Briefkopf erstellen, Benachrichtigungen senden und in Nextcloud ablegen.'),
+ action_label=_('Onboarding starten'),
+ accent='ON',
+ tags=(_('Mehrschritt-Formular'), 'PDF', _('E-Mail Routing')),
+ style_variant='primary',
+ ),
+ AppDefinition(
+ key='offboarding',
+ section=PortalAppConfig.SECTION_APP,
+ route_name='offboarding_create',
+ title=_('Offboarding'),
+ description=_('Mitarbeitende suchen, Daten vorbefüllen, Offboarding-Dokumente erzeugen und Rückgabe-Prozess starten.'),
+ action_label=_('Offboarding starten'),
+ accent='OFF',
+ tags=(_('Profile-Suche'), _('Hardware-Liste'), _('IT-Rückgabe')),
+ style_variant='red',
+ ),
+ AppDefinition(
+ key='requests_dashboard',
+ section=PortalAppConfig.SECTION_APP,
+ route_name='requests_dashboard',
+ title=_('Anfragen Dashboard'),
+ description=_('Status, Suchfunktion, PDF-Links und Verlauf aller Onboarding-/Offboarding-Anfragen.'),
+ action_label=_('Dashboard öffnen'),
+ capability='access_requests_dashboard',
+ accent='APP',
+ tags=(_('Suche'), _('Status'), _('PDF Zugriff')),
+ ),
+ AppDefinition(
+ key='branding',
+ section=PortalAppConfig.SECTION_PLATFORM,
+ route_name='portal_branding_page',
+ title=_('Branding'),
+ description=_('Logo, Portalname, Farben und PDF-Briefkopf verwalten.'),
+ action_label=_('Öffnen'),
+ capability='manage_product_branding',
+ ),
+ AppDefinition(
+ key='app_registry',
+ section=PortalAppConfig.SECTION_PLATFORM,
+ route_name='portal_app_registry_page',
+ title=_('App Registry'),
+ description=_('Apps zentral aktivieren, sortieren und für Kundenauftritte vorbereiten.'),
+ action_label=_('Öffnen'),
+ capability='manage_app_registry',
+ ),
+ AppDefinition(
+ key='integrations',
+ section=PortalAppConfig.SECTION_ADMIN,
+ route_name='integrations_setup_page',
+ title=_('Integrationen'),
+ description=_('Nextcloud- und E-Mail-Setup.'),
+ action_label=_('Öffnen'),
+ capability='manage_integrations',
+ ),
+ AppDefinition(
+ key='users',
+ section=PortalAppConfig.SECTION_ADMIN,
+ route_name='user_management_page',
+ title=_('Benutzer & Rollen'),
+ description=_('Benutzer anlegen, Rollen zuweisen und Zugriffe steuern.'),
+ action_label=_('Öffnen'),
+ capability='manage_users',
+ ),
+ AppDefinition(
+ key='audit_log',
+ section=PortalAppConfig.SECTION_ADMIN,
+ route_name='audit_log_page',
+ title=_('Audit Log'),
+ description=_('Wichtige Admin-Aktionen nachvollziehen und prüfen.'),
+ action_label=_('Öffnen'),
+ capability='view_audit_log',
+ ),
+ AppDefinition(
+ key='backups',
+ section=PortalAppConfig.SECTION_ADMIN,
+ route_name='backup_recovery_page',
+ title=_('Backup & Recovery'),
+ description=_('Backups erstellen und sicher verifizieren.'),
+ action_label=_('Öffnen'),
+ capability='manage_backups',
+ ),
+ AppDefinition(
+ key='welcome_emails',
+ section=PortalAppConfig.SECTION_ADMIN,
+ route_name='welcome_emails_page',
+ title=_('Welcome E-Mails'),
+ description=_('Geplante Welcome Mails verwalten.'),
+ action_label=_('Öffnen'),
+ capability='manage_welcome_emails',
+ ),
+ AppDefinition(
+ key='form_builder',
+ section=PortalAppConfig.SECTION_ADMIN,
+ route_name='form_builder_page',
+ title=_('Form Builder'),
+ description=_('Felder, Schritte und Optionen verwalten.'),
+ action_label=_('Öffnen'),
+ capability='manage_builders',
+ ),
+ AppDefinition(
+ key='intro_builder',
+ section=PortalAppConfig.SECTION_ADMIN,
+ route_name='intro_builder_page',
+ title=_('Einweisungs-Builder'),
+ description=_('Checklistenpunkte für das Einweisungsprotokoll konfigurieren.'),
+ action_label=_('Öffnen'),
+ capability='manage_builders',
+ ),
+ AppDefinition(
+ key='handbook',
+ section=PortalAppConfig.SECTION_ADMIN,
+ route_name='handbook_page',
+ title=_('Handbook'),
+ description=_('Project wiki and developer documentation in one place.'),
+ action_label=_('Öffnen'),
+ capability='view_docs',
+ ),
+ AppDefinition(
+ key='django_admin',
+ section=PortalAppConfig.SECTION_ADMIN,
+ route_name='admin:index',
+ title=_('Django Admin'),
+ description=_('Vollständige Datenverwaltung.'),
+ action_label=_('Öffnen'),
+ capability='access_django_admin_link',
+ ),
+)
+
+
+SECTION_META = {
+ PortalAppConfig.SECTION_APP: {
+ 'title': _('Apps'),
+ 'subtitle': _('Wählen Sie den gewünschten Prozess.'),
+ 'css_class': 'section-head-primary',
+ 'grid_class': 'apps-grid',
+ },
+ PortalAppConfig.SECTION_PLATFORM: {
+ 'title': _('Platform Apps'),
+ 'subtitle': _('Produktweite Konfiguration und Produktsteuerung.'),
+ 'css_class': 'section-head-platform',
+ 'grid_class': 'admin-grid',
+ },
+ PortalAppConfig.SECTION_ADMIN: {
+ 'title': _('Admin Apps'),
+ 'subtitle': _('Konfiguration, Tests und Steuerung.'),
+ 'css_class': 'section-head-admin',
+ 'grid_class': 'admin-grid',
+ },
+}
+
+
+def ensure_portal_app_configs() -> None:
+ for index, definition in enumerate(APP_DEFINITIONS):
+ PortalAppConfig.objects.get_or_create(
+ key=definition.key,
+ defaults={
+ 'section': definition.section,
+ 'sort_order': index,
+ 'is_enabled': True,
+ },
+ )
+
+
+def get_portal_app_registry_rows() -> list[dict[str, object]]:
+ ensure_portal_app_configs()
+ config_map = {config.key: config for config in PortalAppConfig.objects.all()}
+ rows: list[dict[str, object]] = []
+ for index, definition in enumerate(APP_DEFINITIONS):
+ config = config_map[definition.key]
+ rows.append(
+ {
+ 'definition': definition,
+ 'config': config,
+ 'default_section': definition.section,
+ 'default_sort_order': index,
+ }
+ )
+ return rows
+
+
+def build_portal_app_sections(user) -> list[dict[str, object]]:
+ ensure_portal_app_configs()
+ config_map = {config.key: config for config in PortalAppConfig.objects.all()}
+ grouped: dict[str, list[dict[str, object]]] = {key: [] for key in SECTION_META}
+
+ for definition in APP_DEFINITIONS:
+ config = config_map.get(definition.key)
+ if not config or not config.is_enabled:
+ continue
+ if definition.capability and not user_has_capability(user, definition.capability):
+ continue
+ grouped[config.section].append(
+ {
+ 'key': definition.key,
+ 'href': reverse(definition.route_name),
+ 'title': config.translated_title_override() or str(definition.title),
+ 'description': config.translated_description_override() or str(definition.description),
+ 'action_label': config.translated_action_label_override() or str(definition.action_label),
+ 'accent': definition.accent,
+ 'accent_label': definition.accent_label,
+ 'style_variant': definition.style_variant,
+ 'tags': [str(tag) for tag in definition.tags],
+ 'sort_order': config.sort_order,
+ }
+ )
+
+ sections: list[dict[str, object]] = []
+ for section_key, meta in SECTION_META.items():
+ apps = sorted(grouped.get(section_key, []), key=lambda item: (item['sort_order'], item['title']))
+ if not apps:
+ continue
+ sections.append(
+ {
+ 'key': section_key,
+ 'title': str(meta['title']),
+ 'subtitle': str(meta['subtitle']),
+ 'css_class': meta['css_class'],
+ 'grid_class': meta['grid_class'],
+ 'apps': apps,
+ }
+ )
+ return sections
diff --git a/backend/workflows/branding.py b/backend/workflows/branding.py
index b25a984..46d4ae6 100644
--- a/backend/workflows/branding.py
+++ b/backend/workflows/branding.py
@@ -14,6 +14,7 @@ def get_portal_branding() -> PortalBranding:
defaults={
'portal_title': 'TUBCO Onboarding & Offboarding Portal',
'company_name': 'TUBCO',
+ 'company_domain': 'tub.co',
'support_email': 'info@tub.co',
'default_language': 'de',
'primary_color': '#000078',
@@ -23,6 +24,12 @@ def get_portal_branding() -> PortalBranding:
return branding
+def get_company_email_domain() -> str:
+ branding = get_portal_branding()
+ domain = (branding.company_domain or '').strip().lower().lstrip('@')
+ return domain or 'tub.co'
+
+
def get_portal_logo_url() -> str:
branding = get_portal_branding()
if branding.logo_image:
@@ -51,6 +58,7 @@ def get_branding_context() -> dict[str, object]:
'portal_branding': branding,
'portal_title': branding.portal_title,
'portal_company_name': branding.company_name,
+ 'portal_email_domain': get_company_email_domain(),
'portal_support_email': branding.support_email,
'portal_default_language': branding.default_language,
'portal_primary_color': branding.primary_color,
@@ -67,6 +75,7 @@ def get_branding_email_copy() -> dict[str, str]:
portal_title = (branding.portal_title or f'{company_name} Portal').strip()
return {
'company_name': company_name,
+ 'company_domain': get_company_email_domain(),
'portal_title': portal_title,
'support_email': (branding.support_email or '').strip(),
}
@@ -78,7 +87,9 @@ def get_default_notification_templates() -> dict[str, dict[str, str]]:
from .tasks import DEFAULT_NOTIFICATION_TEMPLATES
templates = deepcopy(DEFAULT_NOTIFICATION_TEMPLATES)
- company_name = get_branding_email_copy()['company_name']
+ branding_copy = get_branding_email_copy()
+ company_name = branding_copy['company_name']
+ support_email = branding_copy['support_email'] or f"it@{branding_copy['company_domain']}"
welcome = templates.get('onboarding_welcome')
if welcome:
welcome['subject'] = f'Willkommen bei {company_name}, {{ VORNAME }}'
@@ -89,7 +100,7 @@ def get_default_notification_templates() -> dict[str, dict[str, str]]:
'Wir freuen uns sehr, dass du ab dem {{ CONTRACT_START }} unser Team in der Abteilung {{ DEPARTMENT }} verstärkst.\n\n'
'Deine dienstliche E-Mail-Adresse lautet: {{ EMAIL }}.\n'
'Im Anhang findest du deine Onboarding-Unterlagen als PDF.\n\n'
- 'Wenn du Fragen hast, melde dich gerne jederzeit.\n\n'
+ f'Wenn du Fragen hast, melde dich gerne jederzeit unter {support_email}.\n\n'
'Viele Grüße\n'
f'{company_name} IT'
)
@@ -99,7 +110,7 @@ def get_default_notification_templates() -> dict[str, dict[str, str]]:
'We are very happy that you will join our {{ DEPARTMENT }} team starting on {{ CONTRACT_START }}.\n\n'
'Your work email address is: {{ EMAIL }}.\n'
'You will find your onboarding documents attached as a PDF.\n\n'
- 'If you have any questions, feel free to contact us anytime.\n\n'
+ f'If you have any questions, feel free to contact {support_email}.\n\n'
'Best regards,\n'
f'{company_name} IT'
)
diff --git a/backend/workflows/forms.py b/backend/workflows/forms.py
index d5ca11b..acd6060 100644
--- a/backend/workflows/forms.py
+++ b/backend/workflows/forms.py
@@ -6,6 +6,7 @@ from django.contrib.auth.forms import AuthenticationForm, PasswordResetForm, Set
from django.utils import timezone
from django.utils.translation import get_language, gettext as _, gettext_lazy
+from .branding import get_company_email_domain
from .form_builder import apply_form_field_config
from .models import EmployeeProfile, FormOption, OffboardingRequest, OnboardingRequest, PortalBranding, WorkflowConfig
from .roles import ROLE_ADMIN, ROLE_GROUP_NAMES, ROLE_IT_STAFF, ROLE_LABELS, ROLE_PLATFORM_OWNER, ROLE_STAFF, ROLE_SUPER_ADMIN, assign_user_role
@@ -175,6 +176,7 @@ class PortalBrandingForm(forms.ModelForm):
fields = [
'portal_title',
'company_name',
+ 'company_domain',
'support_email',
'default_language',
'logo_image',
@@ -185,6 +187,7 @@ class PortalBrandingForm(forms.ModelForm):
labels = {
'portal_title': gettext_lazy('Portal-Titel'),
'company_name': gettext_lazy('Firmenname'),
+ 'company_domain': gettext_lazy('Firmen-Domain'),
'support_email': gettext_lazy('Support-E-Mail'),
'default_language': gettext_lazy('Standardsprache'),
'logo_image': gettext_lazy('Logo'),
@@ -343,6 +346,7 @@ class OnboardingRequestForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
self.requester_email = (kwargs.pop('requester_email', '') or '').strip().lower()
super().__init__(*args, **kwargs)
+ self.email_domain = get_company_email_domain()
config = WorkflowConfig.objects.order_by('id').first()
self.handover_lead_days = max(0, int(getattr(config, 'device_handover_lead_days', 5) or 5))
@@ -350,6 +354,7 @@ class OnboardingRequestForm(forms.ModelForm):
self.fields['handover_date'].widget.attrs['min'] = minimum_handover_date.isoformat()
self.fields['full_name'].label = 'Name'
+ self.fields['work_email'].help_text = _('Bitte nutzen Sie das Format name@%(domain)s.') % {'domain': self.email_domain}
full_name_initial = (self.initial.get('full_name') or '').strip()
if full_name_initial and not self.initial.get('first_name') and not self.initial.get('last_name'):
name_parts = full_name_initial.split()
@@ -369,8 +374,9 @@ class OnboardingRequestForm(forms.ModelForm):
value = (self.cleaned_data.get('work_email') or '').strip().lower()
if not value:
return value
- if not value.endswith('@tub.co'):
- raise forms.ValidationError('Bitte verwenden Sie eine @tub.co E-Mail-Adresse.')
+ expected_suffix = f'@{self.email_domain}'
+ if self.email_domain and not value.endswith(expected_suffix):
+ raise forms.ValidationError(_('Bitte verwenden Sie eine @%(domain)s E-Mail-Adresse.') % {'domain': self.email_domain})
return value
def clean_signature_image(self):
@@ -531,11 +537,21 @@ class OffboardingRequestForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
prefill_profile = kwargs.pop('prefill_profile', None)
super().__init__(*args, **kwargs)
+ self.email_domain = get_company_email_domain()
self.fields['full_name'].label = 'Vorname und Nachname'
- self.fields['work_email'].help_text = ''
+ self.fields['work_email'].help_text = _('Bitte nutzen Sie das Format name@%(domain)s.') % {'domain': self.email_domain}
if prefill_profile:
self.fields['full_name'].initial = prefill_profile.full_name
self.fields['work_email'].initial = prefill_profile.work_email
self.fields['department'].initial = prefill_profile.department
self.fields['job_title'].initial = prefill_profile.job_title
apply_form_field_config('offboarding', self)
+
+ def clean_work_email(self):
+ value = (self.cleaned_data.get('work_email') or '').strip().lower()
+ if not value:
+ return value
+ expected_suffix = f'@{self.email_domain}'
+ if self.email_domain and not value.endswith(expected_suffix):
+ raise forms.ValidationError(_('Bitte verwenden Sie eine @%(domain)s E-Mail-Adresse.') % {'domain': self.email_domain})
+ return value
diff --git a/backend/workflows/migrations/0038_portalappconfig.py b/backend/workflows/migrations/0038_portalappconfig.py
new file mode 100644
index 0000000..a2e0875
--- /dev/null
+++ b/backend/workflows/migrations/0038_portalappconfig.py
@@ -0,0 +1,35 @@
+# Generated by Django 5.1.5 on 2026-03-26 10:48
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('workflows', '0037_alter_portalbranding_logo_image_and_more'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='PortalAppConfig',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('key', models.CharField(max_length=80, unique=True)),
+ ('section', models.CharField(choices=[('app', 'Apps'), ('platform', 'Platform Apps'), ('admin', 'Admin Apps')], default='app', max_length=20)),
+ ('sort_order', models.PositiveIntegerField(default=0)),
+ ('is_enabled', models.BooleanField(default=True)),
+ ('title_override', models.CharField(blank=True, max_length=255)),
+ ('title_override_en', models.CharField(blank=True, max_length=255)),
+ ('description_override', models.TextField(blank=True)),
+ ('description_override_en', models.TextField(blank=True)),
+ ('action_label_override', models.CharField(blank=True, max_length=255)),
+ ('action_label_override_en', models.CharField(blank=True, max_length=255)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ],
+ options={
+ 'verbose_name': 'Portal App',
+ 'verbose_name_plural': 'Portal Apps',
+ 'ordering': ['section', 'sort_order', 'key'],
+ },
+ ),
+ ]
diff --git a/backend/workflows/migrations/0039_portalbranding_company_domain.py b/backend/workflows/migrations/0039_portalbranding_company_domain.py
new file mode 100644
index 0000000..de83381
--- /dev/null
+++ b/backend/workflows/migrations/0039_portalbranding_company_domain.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.1.5 on 2026-03-26 10:55
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('workflows', '0038_portalappconfig'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='portalbranding',
+ name='company_domain',
+ field=models.CharField(blank=True, default='tub.co', max_length=120),
+ ),
+ ]
diff --git a/backend/workflows/models.py b/backend/workflows/models.py
index f986b51..7108549 100644
--- a/backend/workflows/models.py
+++ b/backend/workflows/models.py
@@ -29,6 +29,7 @@ class PortalBranding(models.Model):
name = models.CharField(max_length=80, default='Default', unique=True)
portal_title = models.CharField(max_length=255, default='TUBCO Onboarding & Offboarding Portal')
company_name = models.CharField(max_length=255, default='TUBCO')
+ company_domain = models.CharField(max_length=120, blank=True, default='tub.co')
support_email = models.EmailField(blank=True, default='info@tub.co')
default_language = models.CharField(
max_length=10,
@@ -59,6 +60,54 @@ class PortalBranding(models.Model):
return self.portal_title or self.company_name or self.name
+class PortalAppConfig(models.Model):
+ SECTION_APP = 'app'
+ SECTION_PLATFORM = 'platform'
+ SECTION_ADMIN = 'admin'
+ SECTION_CHOICES = [
+ (SECTION_APP, _('Apps')),
+ (SECTION_PLATFORM, _('Platform Apps')),
+ (SECTION_ADMIN, _('Admin Apps')),
+ ]
+
+ key = models.CharField(max_length=80, unique=True)
+ section = models.CharField(max_length=20, choices=SECTION_CHOICES, default=SECTION_APP)
+ sort_order = models.PositiveIntegerField(default=0)
+ is_enabled = models.BooleanField(default=True)
+ title_override = models.CharField(max_length=255, blank=True)
+ title_override_en = models.CharField(max_length=255, blank=True)
+ description_override = models.TextField(blank=True)
+ description_override_en = models.TextField(blank=True)
+ action_label_override = models.CharField(max_length=255, blank=True)
+ action_label_override_en = models.CharField(max_length=255, blank=True)
+ updated_at = models.DateTimeField(auto_now=True)
+
+ class Meta:
+ ordering = ['section', 'sort_order', 'key']
+ verbose_name = 'Portal App'
+ verbose_name_plural = 'Portal Apps'
+
+ def __str__(self) -> str:
+ return self.key
+
+ def _translated_value(self, field_name: str, language_code: str | None = None) -> str:
+ lang = (language_code or get_language() or 'de').split('-')[0]
+ if lang == 'en':
+ english_value = (getattr(self, f'{field_name}_en', '') or '').strip()
+ if english_value:
+ return english_value
+ return (getattr(self, field_name, '') or '').strip()
+
+ def translated_title_override(self, language_code: str | None = None) -> str:
+ return self._translated_value('title_override', language_code)
+
+ def translated_description_override(self, language_code: str | None = None) -> str:
+ return self._translated_value('description_override', language_code)
+
+ def translated_action_label_override(self, language_code: str | None = None) -> str:
+ return self._translated_value('action_label_override', language_code)
+
+
class AdminAuditLog(models.Model):
actor = models.ForeignKey(
settings.AUTH_USER_MODEL,
diff --git a/backend/workflows/roles.py b/backend/workflows/roles.py
index 890d2d0..e8841e5 100644
--- a/backend/workflows/roles.py
+++ b/backend/workflows/roles.py
@@ -29,6 +29,7 @@ ROLE_LABELS = {
CAPABILITIES = {
'manage_users': {ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN},
'manage_product_branding': {ROLE_PLATFORM_OWNER},
+ 'manage_app_registry': {ROLE_PLATFORM_OWNER},
'access_requests_dashboard': {ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF},
'run_intro_session': {ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF},
'generate_intro_pdfs': {ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF},
@@ -123,6 +124,7 @@ def template_role_context(user) -> dict[str, object]:
'role_key': role_key,
'role_label': str(ROLE_LABELS[role_key]),
'can_manage_product_branding': user_has_capability(user, 'manage_product_branding'),
+ 'can_manage_app_registry': user_has_capability(user, 'manage_app_registry'),
'can_manage_users': user_has_capability(user, 'manage_users'),
'can_access_requests_dashboard': user_has_capability(user, 'access_requests_dashboard'),
'can_run_intro_session': user_has_capability(user, 'run_intro_session'),
diff --git a/backend/workflows/static/workflows/css/admin_tools.css b/backend/workflows/static/workflows/css/admin_tools.css
index 014198d..ada691d 100644
--- a/backend/workflows/static/workflows/css/admin_tools.css
+++ b/backend/workflows/static/workflows/css/admin_tools.css
@@ -35,6 +35,12 @@ textarea { min-height: 120px; font-family: ui-monospace, SFMono-Regular, Menlo,
table { width: 100%; border-collapse: collapse; font-size: 14px; }
th, td { border: 1px solid #dce5f1; padding: 8px; text-align: left; vertical-align: top; }
th { background: #f6f9ff; color: #334155; }
+.app-registry-wrap textarea { min-height: 86px; }
+.app-registry-table input[type="text"],
+.app-registry-table input[type="number"],
+.app-registry-table select,
+.app-registry-table textarea { min-width: 160px; }
+.app-registry-table td { background: rgba(255,255,255,0.9); }
.template-block { border: 1px solid #d8e3f0; border-radius: 10px; background: #fff; padding: 10px; margin-top: 10px; }
.template-title, .rule-title { margin: 0 0 8px; color: #24344e; font-weight: 700; font-size: 14px; }
.rule-card { margin-top: 12px; border: 1px solid #d8e3f0; border-radius: 12px; padding: 10px; background: #fff; }
diff --git a/backend/workflows/static/workflows/js/offboarding_form.js b/backend/workflows/static/workflows/js/offboarding_form.js
index c7dec47..bccbae1 100644
--- a/backend/workflows/static/workflows/js/offboarding_form.js
+++ b/backend/workflows/static/workflows/js/offboarding_form.js
@@ -22,6 +22,8 @@
const fullName = byName('full_name');
const workEmail = byName('work_email');
+ const form = fullName ? fullName.closest('form') : null;
+ const emailDomain = (((form && form.dataset.emailDomain) || 'tub.co') + '').replace(/^@+/, '').trim();
if (!fullName || !workEmail) return;
let lastSuggested = '';
@@ -31,7 +33,7 @@
const lastName = extractLastName(fullName.value);
const slug = slugifyForEmail(lastName);
if (!slug) return;
- const suggestion = slug + '@tub.co';
+ const suggestion = slug + '@' + emailDomain;
const current = (workEmail.value || '').trim();
if (!userEditedEmail || current === '' || current === lastSuggested) {
workEmail.value = suggestion;
diff --git a/backend/workflows/static/workflows/js/onboarding_form.js b/backend/workflows/static/workflows/js/onboarding_form.js
index 8ae6960..8a0ce0b 100644
--- a/backend/workflows/static/workflows/js/onboarding_form.js
+++ b/backend/workflows/static/workflows/js/onboarding_form.js
@@ -5,6 +5,7 @@
const btnNext = document.getElementById('btn-next');
const btnSubmit = document.getElementById('btn-submit');
const form = document.getElementById('onboarding-form');
+ const emailDomain = ((form && form.dataset.emailDomain) || 'tub.co').replace(/^@+/, '').trim();
let current = 0;
form.setAttribute('novalidate', 'novalidate');
@@ -82,7 +83,7 @@
function suggestEmail() {
const slug = slugifyForEmail(lastName.value);
if (!slug) return;
- const suggestion = slug + '@tub.co';
+ const suggestion = slug + '@' + emailDomain;
if (!userEditedEmail || workEmail.value === '' || workEmail.value === lastSuggested) {
workEmail.value = suggestion;
lastSuggested = suggestion;
diff --git a/backend/workflows/tasks.py b/backend/workflows/tasks.py
index 9e804b8..9adfd4b 100644
--- a/backend/workflows/tasks.py
+++ b/backend/workflows/tasks.py
@@ -101,7 +101,7 @@ DEFAULT_NOTIFICATION_TEMPLATES = {
'Vertragsbeginn: {{ CONTRACT_START }}\n'
'E-Mail-Adresse: {{ EMAIL }}\n\n'
'{% if PDF_LINK %}In 2 Minuten findest du alle Infos über den Mitarbeiter als PDF unter diesem Link: {{ PDF_LINK }}\n\n{% endif %}'
- 'Falls du noch irgendwelche anderen Informationen benötigen solltest, kannst du dich bei der it@tub.co melden!\n\n'
+ 'Falls du noch irgendwelche anderen Informationen benötigen solltest, kannst du dich bei {{ SUPPORT_EMAIL }} melden!\n\n'
'Vielen Dank und schöne Grüße,\n'
'Die IT.'
),
@@ -114,7 +114,7 @@ DEFAULT_NOTIFICATION_TEMPLATES = {
'Contract start: {{ CONTRACT_START }}\n'
'Email address: {{ EMAIL }}\n\n'
'{% if PDF_LINK %}You will find the employee PDF here in about 2 minutes: {{ PDF_LINK }}\n\n{% endif %}'
- 'If you need any other information, please contact it@tub.co.\n\n'
+ 'If you need any other information, please contact {{ SUPPORT_EMAIL }}.\n\n'
'Thank you and best regards,\n'
'IT'
),
@@ -1176,6 +1176,7 @@ def process_onboarding_request(onboarding_request_id: int) -> None:
request_obj.last_error = ''
request_obj.save(update_fields=['processing_status', 'last_error'])
try:
+ branding_copy = get_branding_email_copy()
it_email, general_info_email, business_card_email, hr_works_email, key_email = _resolve_workflow_emails()
salutation = (request_obj.get_gender_display() or '').strip()
display_name = f"{salutation} {request_obj.full_name}".strip()
@@ -1204,6 +1205,7 @@ def process_onboarding_request(onboarding_request_id: int) -> None:
'CONTRACT_START': request_obj.contract_start,
'EMAIL': request_obj.work_email,
'REQUESTED_BY': request_obj.onboarded_by_email or '-',
+ 'SUPPORT_EMAIL': branding_copy['support_email'] or f"it@{branding_copy['company_domain']}",
'BUSINESS_CARD_NAME': request_obj.business_card_name or display_name,
'BUSINESS_CARD_TITLE': request_obj.business_card_title or '-',
'BUSINESS_CARD_EMAIL': request_obj.business_card_email or request_obj.work_email,
@@ -1285,6 +1287,7 @@ def process_offboarding_request(offboarding_request_id: int) -> None:
request_obj.last_error = ''
request_obj.save(update_fields=['processing_status', 'last_error'])
try:
+ branding_copy = get_branding_email_copy()
it_email, general_info_email, _, hr_works_email, _ = _resolve_workflow_emails()
pdf_path = _generate_offboarding_pdf(request_obj)
@@ -1297,6 +1300,7 @@ def process_offboarding_request(offboarding_request_id: int) -> None:
'LAST_WORKING_DAY': request_obj.last_working_day,
'REQUESTED_BY': request_obj.requested_by_email,
'EMAIL': request_obj.work_email,
+ 'SUPPORT_EMAIL': branding_copy['support_email'] or f"it@{branding_copy['company_domain']}",
}
_send_templated_email(
diff --git a/backend/workflows/templates/workflows/app_registry.html b/backend/workflows/templates/workflows/app_registry.html
new file mode 100644
index 0000000..dea7d93
--- /dev/null
+++ b/backend/workflows/templates/workflows/app_registry.html
@@ -0,0 +1,83 @@
+{% extends 'workflows/base_shell.html' %}
+{% load static i18n %}
+
+{% block title %}{% trans "App Registry" %}{% endblock %}
+
+{% block extra_css %}
+
+{% endblock %}
+
+{% block shell_body %}
+{% include 'workflows/includes/app_header.html' with header_show_home=1 header_show_lang=1 header_inside_shell=1 %}
+
{% trans "App Registry" %}
+{% trans "Apps zentral steuern, für Kunden vorbereiten und ohne Template-Eingriffe auf der Landing Page ausspielen." %}
+
+{% include 'workflows/includes/messages.html' %}
+
+
+{% endblock %}
diff --git a/backend/workflows/templates/workflows/branding_settings.html b/backend/workflows/templates/workflows/branding_settings.html
index 4f9138e..c7380d4 100644
--- a/backend/workflows/templates/workflows/branding_settings.html
+++ b/backend/workflows/templates/workflows/branding_settings.html
@@ -26,6 +26,11 @@
{{ form.company_name.label }}
{{ form.company_name }}
+
+
{{ form.company_domain.label }}
+ {{ form.company_domain }}
+
{% trans "Wird für E-Mail-Vorschläge und Domain-bezogene Standardtexte verwendet, z. B. tub.co." %}
+
{{ form.support_email.label }}
{{ form.support_email }}
diff --git a/backend/workflows/templates/workflows/developer_handbook.html b/backend/workflows/templates/workflows/developer_handbook.html
index 76e4552..5066ee8 100644
--- a/backend/workflows/templates/workflows/developer_handbook.html
+++ b/backend/workflows/templates/workflows/developer_handbook.html
@@ -102,12 +102,13 @@ docker compose exec -T web python manage.py check
Role and Permission Model
- Stable Django group names: Super Admin, Admin, IT Staff, Staff.
+ Stable Django group names: Platform Owner, Super Admin, Admin, IT Staff, Staff.
Groups are created automatically through a post_migrate hook in workflows.signals.
Capability checks are centralized in workflows.roles.CAPABILITIES.
Use _require_capability(...) in views instead of flat is_staff checks.
Templates receive permission flags from workflows.context_processors.role_context.
- Super-admin-only user management lives at /admin-tools/users/ and is the preferred path for normal role assignment, account activation, invitation mail dispatch, password-reset mail dispatch, and controlled user deletion.
+ Platform Owner is the product-level role. Company roles remain Super Admin, Admin, IT Staff, and Staff.
+ User management lives at /admin-tools/users/ and is the preferred path for normal role assignment, account activation, invitation mail dispatch, password-reset mail dispatch, and controlled user deletion.
Backward-compatibility rule: authenticated legacy users with is_staff=True but no explicit role group currently fall back to the Admin capability set.
superuser accounts resolve to Super Admin.
When adding a new operational page or action, define the capability in roles.py, gate the view, and hide the UI affordance when the capability is absent.
@@ -180,6 +181,15 @@ docker compose exec -T web django-admin compilemessages
User invitation emails and welcome-template fallbacks also use the configured branding defaults.
+
10b) App Registry
+
+ Registry definitions live in workflows/app_registry.py.
+ DB overrides live in PortalAppConfig.
+ The landing page now renders from registry data instead of hardcoded cards.
+ Security remains code-based: app visibility/order is configurable, but access still depends on role capabilities in roles.py.
+ Management UI: /admin-tools/apps/ for Platform Owner.
+
+
11) Builder Architecture
Form Builder
diff --git a/backend/workflows/templates/workflows/home.html b/backend/workflows/templates/workflows/home.html
index 8f194c0..c9387e0 100644
--- a/backend/workflows/templates/workflows/home.html
+++ b/backend/workflows/templates/workflows/home.html
@@ -51,147 +51,44 @@
{% include 'workflows/includes/messages.html' %}
-
-
{% trans "Apps" %}
-
{% trans "Wählen Sie den gewünschten Prozess." %}
-
-
-
-
-
-
{% trans "Onboarding" %}
-
{% trans "Neue Mitarbeitende erfassen, PDF mit Briefkopf erstellen, Benachrichtigungen senden und in Nextcloud ablegen." %}
-
-{% trans "Mehrschritt-Formular" %}
- PDF
-{% trans "E-Mail Routing" %}
-
-
-
-
-
-
-
-
-
{% trans "Offboarding" %}
-
{% trans "Mitarbeitende suchen, Daten vorbefüllen, Offboarding-Dokumente erzeugen und Rückgabe-Prozess starten." %}
-
-{% trans "Profile-Suche" %}
-{% trans "Hardware-Liste" %}
-{% trans "IT-Rückgabe" %}
-
-
-
-
-
- {% if can_access_requests_dashboard %}
-
-
-
-
{% trans "Anfragen Dashboard" %}
-
{% trans "Status, Suchfunktion, PDF-Links und Verlauf aller Onboarding-/Offboarding-Anfragen." %}
-
-{% trans "Suche" %}
-{% trans "Status" %}
-{% trans "PDF Zugriff" %}
-
-
-
-
- {% endif %}
-
-
- {% if can_manage_product_branding %}
+ {% for section in portal_app_sections %}
+ {% if not forloop.first %}
-
-
-
-{% trans "Branding" %}
-{% trans "Logo, Portalname, Farben und PDF-Briefkopf verwalten." %}
-{% trans "Öffnen" %}
-
-
{% endif %}
-
- {% if can_manage_users or can_manage_integrations or can_view_audit_log or can_manage_backups or can_manage_welcome_emails or can_manage_builders or can_view_docs or can_access_django_admin_link %}
-
-
-
{% trans "Admin Apps" %}
-
{% trans "Konfiguration, Tests und Steuerung." %}
+
+
{{ section.title }}
+
{{ section.subtitle }}
-
- {% if can_manage_integrations %}
+
+ {% for app in section.apps %}
+ {% if section.key == 'app' %}
+
+
+
+
{{ app.title }}
+
{{ app.description }}
+ {% if app.tags %}
+
+ {% for tag in app.tags %}
+ {{ tag }}
+ {% endfor %}
+
+ {% endif %}
+
+
+
+ {% else %}
- {% endif %}
- {% if can_manage_users %}
-
-{% trans "Benutzer & Rollen" %}
-{% trans "Benutzer anlegen, Rollen zuweisen und Zugriffe steuern." %}
-{% trans "Öffnen" %}
-
- {% endif %}
- {% if can_view_audit_log %}
-
-{% trans "Audit Log" %}
-{% trans "Wichtige Admin-Aktionen nachvollziehen und prüfen." %}
-{% trans "Öffnen" %}
-
- {% endif %}
- {% if can_manage_backups %}
-
-{% trans "Backup & Recovery" %}
-{% trans "Backups erstellen und sicher verifizieren." %}
-{% trans "Öffnen" %}
-
- {% endif %}
- {% if can_manage_welcome_emails %}
-
-{% trans "Welcome E-Mails" %}
-{% trans "Geplante Welcome Mails verwalten." %}
-{% trans "Öffnen" %}
-
- {% endif %}
- {% if can_manage_builders %}
-
-{% trans "Form Builder" %}
-{% trans "Felder, Schritte und Optionen verwalten." %}
-{% trans "Öffnen" %}
-
-
-{% trans "Einweisungs-Builder" %}
-{% trans "Checklistenpunkte für das Einweisungsprotokoll konfigurieren." %}
-{% trans "Öffnen" %}
-
- {% endif %}
- {% if can_view_docs %}
-
-{% trans "Handbook" %}
-{% trans "Project wiki and developer documentation in one place." %}
-{% trans "Öffnen" %}
-
- {% endif %}
- {% if can_access_django_admin_link %}
-
{% endif %}
+ {% endfor %}
- {% endif %}
+ {% endfor %}
-
+
{% csrf_token %}
{% for field in form.visible_fields %}
@@ -71,4 +71,3 @@
{% block extra_scripts %}
{% endblock %}
-
diff --git a/backend/workflows/templates/workflows/onboarding_form.html b/backend/workflows/templates/workflows/onboarding_form.html
index b6d7edd..9a2953a 100644
--- a/backend/workflows/templates/workflows/onboarding_form.html
+++ b/backend/workflows/templates/workflows/onboarding_form.html
@@ -42,7 +42,7 @@
{% trans "Bitte prüfen Sie die markierten Felder. Ungültige Eingaben wurden erkannt." %}
{% endif %}
-
+
{% csrf_token %}
{% for section in onboarding_sections %}
@@ -165,4 +165,3 @@
{% block extra_scripts %}
{% endblock %}
-
diff --git a/backend/workflows/templates/workflows/project_wiki.html b/backend/workflows/templates/workflows/project_wiki.html
index 3810443..923c016 100644
--- a/backend/workflows/templates/workflows/project_wiki.html
+++ b/backend/workflows/templates/workflows/project_wiki.html
@@ -179,6 +179,7 @@
Einweisungs-Builder: manage custom checklist items for the intro PDF and live introduction checklist, including section, visibility, and conditional display logic.
Integrations: Nextcloud, SMTP, default routing addresses, notification rules, workflow rules, and remote backup target settings.
Branding: portal title, company name, logo, support email, default language, PDF letterhead, and basic brand colors.
+ App Registry: platform-level registry for enabling, ordering, and relabeling landing-page apps without editing the home template.
Benutzer & Rollen: super-admin-only page for creating users, assigning roles, activating/deactivating access, sending access or password-reset links by email, and deleting accounts when appropriate.
Welcome Emails: scheduled jobs, pause/resume/cancel/trigger now.
Audit Log: staff-only trace of important admin changes such as builder edits, settings updates, PDF generation, welcome-email operations, and request deletions. Supports filtering by action, user, and date range.
diff --git a/backend/workflows/urls.py b/backend/workflows/urls.py
index 2b7afca..708af95 100644
--- a/backend/workflows/urls.py
+++ b/backend/workflows/urls.py
@@ -32,6 +32,8 @@ urlpatterns = [
path('admin-tools/handbook/', views.handbook_page, name='handbook_page'),
path('admin-tools/branding/', views.portal_branding_page, name='portal_branding_page'),
path('admin-tools/branding/save/', views.save_portal_branding, name='save_portal_branding'),
+ path('admin-tools/apps/', views.portal_app_registry_page, name='portal_app_registry_page'),
+ path('admin-tools/apps/save/', views.save_portal_app_registry, name='save_portal_app_registry'),
path('admin-tools/users/', views.user_management_page, name='user_management_page'),
path('admin-tools/users/create/', views.create_user_from_admin, name='create_user_from_admin'),
path('admin-tools/users//update/', views.update_user_from_admin, name='update_user_from_admin'),
diff --git a/backend/workflows/views.py b/backend/workflows/views.py
index d966876..f4330f2 100644
--- a/backend/workflows/views.py
+++ b/backend/workflows/views.py
@@ -24,8 +24,9 @@ from django.utils.translation import gettext as _, gettext_lazy
from django.utils.translation import get_language, override
from django.urls import reverse
+from .app_registry import build_portal_app_sections, get_portal_app_registry_rows
from .backup_ops import create_backup_bundle, delete_backup_bundle, list_backup_bundles, verify_backup_bundle
-from .branding import get_branding_email_copy, get_default_notification_templates
+from .branding import get_branding_email_copy, get_company_email_domain, get_default_notification_templates
from .forms import OffboardingRequestForm, OnboardingRequestForm, PortalBrandingForm, UserManagementCreateForm
from .form_builder import (
DEFAULT_FIELD_ORDER,
@@ -35,7 +36,7 @@ from .form_builder import (
ONBOARDING_PAGE_ORDER,
ensure_form_field_configs,
)
-from .models import AdminAuditLog, EmployeeProfile, FormFieldConfig, FormOption, IntroChecklistItem, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, PortalBranding, ScheduledWelcomeEmail, SystemEmailConfig, WorkflowConfig
+from .models import AdminAuditLog, EmployeeProfile, FormFieldConfig, FormOption, IntroChecklistItem, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, PortalAppConfig, PortalBranding, ScheduledWelcomeEmail, SystemEmailConfig, WorkflowConfig
from .emailing import send_system_email
from .roles import ROLE_GROUP_NAMES, ROLE_LABELS, ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN, assign_user_role, get_user_role_key, get_user_role_label, user_has_capability
from .services import get_email_test_redirect, is_email_test_mode, is_nextcloud_enabled, upload_to_nextcloud
@@ -244,6 +245,7 @@ def _audit_action_label(action: str) -> str:
'backup_verified': _('Backup verifiziert'),
'backup_deleted': _('Backup gelöscht'),
'backup_settings_saved': _('Backup-Einstellungen gespeichert'),
+ 'portal_app_registry_saved': _('App-Registry gespeichert'),
}
return labels.get(action, action.replace('_', ' ').strip().capitalize())
@@ -334,10 +336,57 @@ def home(request):
'email_test_mode': is_email_test_mode(),
'workflow_config': config,
'role_label': get_user_role_label(request.user),
+ 'portal_app_sections': build_portal_app_sections(request.user),
},
)
+@_require_capability('manage_app_registry')
+def portal_app_registry_page(request):
+ return render(
+ request,
+ 'workflows/app_registry.html',
+ {
+ 'rows': get_portal_app_registry_rows(),
+ 'section_choices': _translate_choice_list(PortalAppConfig.SECTION_CHOICES),
+ },
+ )
+
+
+@_require_capability('manage_app_registry')
+@require_POST
+def save_portal_app_registry(request):
+ rows = get_portal_app_registry_rows()
+ for row in rows:
+ config = row['config']
+ key = config.key
+ config.section = (request.POST.get(f'section__{key}') or config.section).strip()
+ if config.section not in dict(PortalAppConfig.SECTION_CHOICES):
+ config.section = row['default_section']
+ config.is_enabled = request.POST.get(f'is_enabled__{key}') == 'on'
+ try:
+ config.sort_order = int((request.POST.get(f'sort_order__{key}') or '').strip() or row['default_sort_order'])
+ except ValueError:
+ config.sort_order = row['default_sort_order']
+ config.title_override = (request.POST.get(f'title_override__{key}') or '').strip()
+ config.title_override_en = (request.POST.get(f'title_override_en__{key}') or '').strip()
+ config.description_override = (request.POST.get(f'description_override__{key}') or '').strip()
+ config.description_override_en = (request.POST.get(f'description_override_en__{key}') or '').strip()
+ config.action_label_override = (request.POST.get(f'action_label_override__{key}') or '').strip()
+ config.action_label_override_en = (request.POST.get(f'action_label_override_en__{key}') or '').strip()
+ config.save()
+
+ _audit(
+ request,
+ 'portal_app_registry_saved',
+ target_type='portal_app_registry',
+ target_label='Portal App Registry',
+ details={'updated_apps': len(rows)},
+ )
+ messages.success(request, _('App-Registry gespeichert.'))
+ return redirect('portal_app_registry_page')
+
+
def _user_management_rows():
user_model = get_user_model()
role_order = {
@@ -1170,6 +1219,7 @@ def onboarding_create(request):
'legal_text': legal_text,
'saved': request.GET.get('saved') == '1',
'saved_request_id': request.GET.get('id', ''),
+ 'portal_email_domain': get_company_email_domain(),
},
)
@@ -1331,7 +1381,8 @@ def offboarding_create(request):
if selected_profile:
obj.employee_profile = selected_profile
requester_email = (request.user.email or '').strip().lower()
- if requester_email and requester_email.endswith('@tub.co'):
+ company_suffix = f"@{get_company_email_domain()}"
+ if requester_email and requester_email.endswith(company_suffix):
obj.requested_by_email = requester_email
else:
obj.requested_by_email = settings.DEFAULT_FROM_EMAIL
@@ -1353,6 +1404,7 @@ def offboarding_create(request):
'search_query': search_query,
'saved': request.GET.get('saved') == '1',
'saved_request_id': request.GET.get('id', ''),
+ 'portal_email_domain': get_company_email_domain(),
},
)