diff --git a/backend/locale/en/LC_MESSAGES/django.mo b/backend/locale/en/LC_MESSAGES/django.mo index f69d1a6..46923b3 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 af32b22..8758ccb 100644 --- a/backend/locale/en/LC_MESSAGES/django.po +++ b/backend/locale/en/LC_MESSAGES/django.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: tubco-portal\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-03-26 09:25+0000\n" +"POT-Creation-Date: 2026-03-26 10:38+0000\n" "PO-Revision-Date: 2026-03-24 00:00+0000\n" "Language: en\n" "MIME-Version: 1.0\n" @@ -57,6 +57,7 @@ msgstr "" #: workflows/forms.py:103 workflows/forms.py:128 #: workflows/templates/workflows/user_management.html:72 +#: workflows/templates/workflows/user_management.html:170 msgid "Benutzername" msgstr "" @@ -89,118 +90,163 @@ msgstr "" #: workflows/forms.py:130 workflows/templates/workflows/user_management.html:74 #: workflows/templates/workflows/user_management.html:93 +#: workflows/templates/workflows/user_management.html:171 #, fuzzy #| msgid "Rolle:" msgid "Rolle" msgstr "Role:" -#: workflows/forms.py:143 +#: workflows/forms.py:144 msgid "Dieser Benutzername ist bereits vergeben." msgstr "This username is already taken." -#: workflows/forms.py:152 workflows/views.py:472 +#: workflows/forms.py:153 workflows/views.py:567 msgid "Ungültige Rolle." msgstr "Invalid role." -#: workflows/forms.py:408 +#: workflows/forms.py:155 workflows/views.py:570 +msgid "Nur Platform Owner dürfen diese Rolle vergeben." +msgstr "" + +#: workflows/forms.py:186 +msgid "Portal-Titel" +msgstr "Portal title" + +#: workflows/forms.py:187 +msgid "Firmenname" +msgstr "Company name" + +#: workflows/forms.py:188 +msgid "Support-E-Mail" +msgstr "Support email" + +#: workflows/forms.py:189 +msgid "Standardsprache" +msgstr "Default language" + +#: workflows/forms.py:190 +msgid "Logo" +msgstr "Logo" + +#: workflows/forms.py:191 +msgid "PDF-Briefkopf" +msgstr "PDF letterhead" + +#: workflows/forms.py:192 +msgid "Primärfarbe" +msgstr "Primary color" + +#: workflows/forms.py:193 +msgid "Sekundärfarbe" +msgstr "Secondary color" + +#: workflows/forms.py:207 +msgid "Das Logo darf maximal 5 MB groß sein." +msgstr "" + +#: workflows/forms.py:215 +msgid "Der PDF-Briefkopf darf maximal 10 MB groß sein." +msgstr "" + +#: workflows/forms.py:458 #, python-format msgid "" "Das Übergabedatum muss mindestens %(days)s Tage in der Zukunft liegen " "(frühestens %(date)s)." msgstr "" -#: workflows/models.py:55 workflows/views.py:199 +#: workflows/models.py:90 workflows/views.py:199 msgid "Eingereicht" msgstr "Submitted" -#: workflows/models.py:56 workflows/views.py:200 +#: workflows/models.py:91 workflows/views.py:200 msgid "In Bearbeitung" msgstr "Processing" -#: workflows/models.py:57 workflows/models.py:372 workflows/views.py:201 +#: workflows/models.py:92 workflows/models.py:407 workflows/views.py:201 msgid "Abgeschlossen" msgstr "Completed" -#: workflows/models.py:58 workflows/models.py:312 +#: workflows/models.py:93 workflows/models.py:347 #: 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 msgid "Fehlgeschlagen" msgstr "Failed" -#: workflows/models.py:65 +#: workflows/models.py:100 msgid "Herr" msgstr "" -#: workflows/models.py:65 +#: workflows/models.py:100 msgid "Frau" msgstr "" -#: workflows/models.py:65 +#: workflows/models.py:100 msgid "Divers" msgstr "" -#: workflows/models.py:75 +#: workflows/models.py:110 msgid "befristet" msgstr "" -#: workflows/models.py:75 +#: workflows/models.py:110 msgid "unbefristet" msgstr "" -#: workflows/models.py:138 +#: workflows/models.py:173 #: workflows/templates/workflows/onboarding_intro_session.html:28 #: workflows/templates/workflows/requests_dashboard.html:145 msgid "Abteilung" msgstr "Department" -#: workflows/models.py:139 +#: workflows/models.py:174 msgid "Geräte" msgstr "" -#: workflows/models.py:140 +#: workflows/models.py:175 msgid "Software" msgstr "" -#: workflows/models.py:141 +#: workflows/models.py:176 #, fuzzy #| msgid "Vorgänge" msgid "Zugänge" msgstr "Requests" -#: workflows/models.py:142 +#: workflows/models.py:177 msgid "Workspace-Gruppen" msgstr "" -#: workflows/models.py:143 +#: workflows/models.py:178 msgid "Ressourcen" msgstr "" -#: workflows/models.py:144 +#: workflows/models.py:179 msgid "Telefonnummern" msgstr "" -#: workflows/models.py:170 +#: workflows/models.py:205 msgid "Automatisch" msgstr "" -#: workflows/models.py:171 workflows/views.py:94 +#: workflows/models.py:206 workflows/views.py:94 msgid "Stammdaten" msgstr "Master data" -#: workflows/models.py:172 workflows/views.py:95 +#: workflows/models.py:207 workflows/views.py:95 msgid "Vertrag" msgstr "Contract" -#: workflows/models.py:173 workflows/views.py:96 +#: workflows/models.py:208 workflows/views.py:96 msgid "IT-Setup" msgstr "IT setup" -#: workflows/models.py:174 workflows/views.py:97 +#: workflows/models.py:209 workflows/views.py:97 msgid "Abschluss" msgstr "Finish" -#: workflows/models.py:177 workflows/models.py:258 +#: 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 @@ -208,259 +254,263 @@ msgstr "Finish" msgid "Onboarding" msgstr "Onboarding" -#: workflows/models.py:178 workflows/models.py:259 +#: 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:216 +#: workflows/models.py:251 #, fuzzy #| msgid "Onboarding" msgid "Onboarding: IT" msgstr "Onboarding" -#: workflows/models.py:217 +#: workflows/models.py:252 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Onboarding: Allgemeine Info" msgstr "Save offboarding request" -#: workflows/models.py:218 +#: workflows/models.py:253 #, fuzzy #| msgid "Onboarding starten" msgid "Onboarding: Visitenkarte" msgstr "Start onboarding" -#: workflows/models.py:219 +#: workflows/models.py:254 #, fuzzy #| msgid "Onboarding" msgid "Onboarding: HR Works" msgstr "Onboarding" -#: workflows/models.py:220 +#: workflows/models.py:255 #, fuzzy #| msgid "Onboarding starten" msgid "Onboarding: Schlüssel" msgstr "Start onboarding" -#: workflows/models.py:221 +#: workflows/models.py:256 msgid "Onboarding: Referenz Anfordernde Person" msgstr "" -#: workflows/models.py:222 +#: workflows/models.py:257 #, fuzzy #| msgid "Welcome E-Mails" msgid "Onboarding: Welcome E-Mail" msgstr "Welcome Emails" -#: workflows/models.py:223 +#: workflows/models.py:258 #, fuzzy #| msgid "Offboarding" msgid "Offboarding: IT" msgstr "Offboarding" -#: workflows/models.py:224 +#: workflows/models.py:259 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Offboarding: Allgemeine Info" msgstr "Save offboarding request" -#: workflows/models.py:225 +#: workflows/models.py:260 #, fuzzy #| msgid "Offboarding starten" msgid "Offboarding: HR Works Deaktivierung" msgstr "Start offboarding" -#: workflows/models.py:226 +#: workflows/models.py:261 msgid "Offboarding: Referenz Anfordernde Person" msgstr "" -#: workflows/models.py:262 +#: workflows/models.py:297 msgid "Immer" msgstr "" -#: workflows/models.py:263 workflows/models.py:341 +#: workflows/models.py:298 workflows/models.py:376 msgid "Enthält" msgstr "" -#: workflows/models.py:264 workflows/models.py:342 +#: workflows/models.py:299 workflows/models.py:377 msgid "Ist gleich" msgstr "" -#: workflows/models.py:265 +#: workflows/models.py:300 msgid "Ist aktiv/Ja" msgstr "" -#: workflows/models.py:266 +#: workflows/models.py:301 #, fuzzy #| msgid "inaktiv" msgid "Ist inaktiv/Nein" msgstr "inactive" -#: workflows/models.py:308 +#: workflows/models.py:343 #: workflows/templates/workflows/welcome_emails.html:100 msgid "Geplant" msgstr "Scheduled" -#: workflows/models.py:309 +#: workflows/models.py:344 #: workflows/templates/workflows/welcome_emails.html:102 msgid "Pausiert" msgstr "Paused" -#: workflows/models.py:310 +#: workflows/models.py:345 #: workflows/templates/workflows/welcome_emails.html:104 msgid "Abgebrochen" msgstr "Cancelled" -#: workflows/models.py:311 +#: workflows/models.py:346 #: workflows/templates/workflows/welcome_emails.html:106 msgid "Gesendet" msgstr "Sent" -#: workflows/models.py:334 workflows/tasks.py:575 +#: workflows/models.py:369 workflows/tasks.py:576 msgid "Geräte und Arbeitsplatz" msgstr "Devices and workplace" -#: workflows/models.py:335 workflows/tasks.py:576 +#: workflows/models.py:370 workflows/tasks.py:577 msgid "Konten und Berechtigungen" msgstr "Accounts and permissions" -#: workflows/models.py:336 workflows/tasks.py:577 +#: workflows/models.py:371 workflows/tasks.py:578 msgid "Software und Tools" msgstr "Software and tools" -#: workflows/models.py:337 workflows/tasks.py:578 +#: workflows/models.py:372 workflows/tasks.py:579 msgid "Prozesse und Hinweise" msgstr "Processes and notes" -#: workflows/models.py:340 +#: workflows/models.py:375 msgid "Immer anzeigen" msgstr "Always show" -#: workflows/models.py:343 +#: workflows/models.py:378 msgid "Ist Ja / aktiv" msgstr "Is yes / active" -#: workflows/models.py:344 +#: workflows/models.py:379 msgid "Ist Nein / inaktiv" msgstr "Is no / inactive" -#: workflows/models.py:371 +#: workflows/models.py:406 msgid "Entwurf" msgstr "Draft" -#: workflows/models.py:391 +#: workflows/models.py:426 #, fuzzy #| msgid "Nextcloud:" msgid "Nextcloud" msgstr "Nextcloud:" -#: workflows/models.py:392 +#: workflows/models.py:427 msgid "S3" msgstr "" -#: workflows/models.py:393 +#: workflows/models.py:428 msgid "NFS" msgstr "" -#: workflows/roles.py:20 +#: workflows/roles.py:22 +msgid "Platform Owner" +msgstr "" + +#: workflows/roles.py:23 msgid "Super Admin" msgstr "Super Admin" -#: workflows/roles.py:21 +#: workflows/roles.py:24 msgid "Admin" msgstr "Admin" -#: workflows/roles.py:22 +#: workflows/roles.py:25 msgid "IT Staff" msgstr "IT Staff" -#: workflows/roles.py:23 +#: workflows/roles.py:26 msgid "Mitarbeiter" msgstr "Staff" -#: workflows/tasks.py:591 +#: workflows/tasks.py:592 #, python-format msgid "%(item)s übergeben und Grundfunktionen erklärt" msgstr "%(item)s handed over and basic functions explained" -#: workflows/tasks.py:593 +#: workflows/tasks.py:594 #, python-format msgid "%(item)s gezeigt bzw. Nutzung erklärt" msgstr "%(item)s shown or usage explained" -#: workflows/tasks.py:595 +#: workflows/tasks.py:596 #, python-format msgid "Telefonnummer / Direktwahl erklärt: %(value)s" msgstr "Phone number / direct extension explained: %(value)s" -#: workflows/tasks.py:597 +#: workflows/tasks.py:598 msgid "Arbeitsplatz, Geräte und allgemeine Nutzung besprochen" msgstr "Workplace, devices, and general usage reviewed" -#: workflows/tasks.py:599 +#: workflows/tasks.py:600 #, python-format msgid "%(item)s Zugang erklärt" msgstr "%(item)s access explained" -#: workflows/tasks.py:600 +#: workflows/tasks.py:601 #, python-format msgid "%(item)s Gruppe / Berechtigung erläutert" msgstr "%(item)s group / permission explained" -#: workflows/tasks.py:602 +#: workflows/tasks.py:603 #, python-format msgid "Dienstliche E-Mail-Adresse erläutert: %(value)s" msgstr "Work email address explained: %(value)s" -#: workflows/tasks.py:604 +#: workflows/tasks.py:605 #, python-format msgid "Gruppenpostfach erklärt: %(item)s" msgstr "Group mailbox explained: %(item)s" -#: workflows/tasks.py:606 +#: workflows/tasks.py:607 msgid "Zugänge, Konten und Anmeldelogik besprochen" msgstr "Accesses, accounts, and login logic reviewed" -#: workflows/tasks.py:608 +#: workflows/tasks.py:609 #, python-format msgid "%(item)s Einführung durchgeführt" msgstr "%(item)s introduction completed" -#: workflows/tasks.py:609 +#: workflows/tasks.py:610 #, python-format msgid "%(item)s zusätzlich besprochen" msgstr "%(item)s discussed additionally" -#: workflows/tasks.py:611 +#: workflows/tasks.py:612 msgid "Benötigte Standardsoftware und tägliche Nutzung erklärt" msgstr "Required standard software and daily usage explained" -#: workflows/tasks.py:614 +#: workflows/tasks.py:615 msgid "Passwortregeln und sicherer Umgang besprochen" msgstr "Password rules and secure handling reviewed" -#: workflows/tasks.py:615 +#: workflows/tasks.py:616 msgid "Dateiablage, Nextcloud und Freigaben erklärt" msgstr "File storage, Nextcloud, and sharing explained" -#: workflows/tasks.py:616 +#: workflows/tasks.py:617 msgid "Kommunikationswege und Support-Prozess erklärt" msgstr "Communication channels and support process explained" -#: workflows/tasks.py:619 +#: workflows/tasks.py:620 #, python-format msgid "%(item)s als zusätzliche Ausstattung besprochen" msgstr "%(item)s discussed as additional equipment" -#: workflows/tasks.py:621 +#: workflows/tasks.py:622 #, python-format msgid "Zusätzlicher Zugang besprochen: %(item)s" msgstr "Additional access discussed: %(item)s" -#: workflows/tasks.py:623 +#: workflows/tasks.py:624 #, python-format msgid "Übergabe-/Nachfolgekontext besprochen: %(value)s" msgstr "Handover / successor context reviewed: %(value)s" @@ -507,13 +557,15 @@ msgstr "Password saved" msgid "" "Ihr Passwort wurde erfolgreich gesetzt. Sie können sich jetzt mit Ihrem " "Benutzerkonto anmelden." -msgstr "Your password has been set successfully. You can now sign in with your account." +msgstr "" +"Your password has been set successfully. You can now sign in with your " +"account." #: workflows/templates/registration/password_reset_complete.html:19 #: workflows/templates/registration/password_reset_confirm.html:41 #: workflows/templates/registration/password_reset_done.html:19 #: workflows/templates/workflows/auth/password_reset_complete.html:19 -#: workflows/templates/workflows/auth/password_reset_confirm.html:44 +#: workflows/templates/workflows/auth/password_reset_confirm.html:43 #: workflows/templates/workflows/auth/password_reset_done.html:19 msgid "Zur Anmeldung" msgstr "Back to sign in" @@ -541,17 +593,17 @@ msgid "Bitte prüfen Sie die beiden Passwortfelder und versuchen Sie es erneut." msgstr "Please check both password fields and try again." #: workflows/templates/registration/password_reset_confirm.html:36 -#: workflows/templates/workflows/auth/password_reset_confirm.html:39 +#: workflows/templates/workflows/auth/password_reset_confirm.html:38 msgid "Passwort speichern" msgstr "Save password" #: workflows/templates/registration/password_reset_confirm.html:39 -#: workflows/templates/workflows/auth/password_reset_confirm.html:42 +#: workflows/templates/workflows/auth/password_reset_confirm.html:41 msgid "Link ungültig" msgstr "Invalid link" #: workflows/templates/registration/password_reset_confirm.html:40 -#: workflows/templates/workflows/auth/password_reset_confirm.html:43 +#: workflows/templates/workflows/auth/password_reset_confirm.html:42 msgid "" "Dieser Link ist nicht mehr gültig. Bitte fordern Sie einen neuen Passwort-" "Link an." @@ -569,7 +621,9 @@ msgstr "Email sent" msgid "" "Wenn ein passendes Konto existiert, wurde ein Passwort-Link an die " "hinterlegte E-Mail-Adresse verschickt." -msgstr "If a matching account exists, a password link has been sent to the stored email address." +msgstr "" +"If a matching account exists, a password link has been sent to the stored " +"email address." #: workflows/templates/registration/password_reset_form.html:4 #: workflows/templates/registration/password_reset_form.html:17 @@ -583,7 +637,9 @@ msgstr "Reset password" msgid "" "Geben Sie Ihre E-Mail-Adresse ein. Wenn ein Konto vorhanden ist, erhalten " "Sie einen Passwort-Link." -msgstr "Enter your email address. If an account exists, you will receive a password link." +msgstr "" +"Enter your email address. If an account exists, you will receive a password " +"link." #: workflows/templates/registration/password_reset_form.html:24 #: workflows/templates/workflows/auth/password_reset_form.html:25 @@ -592,7 +648,7 @@ msgstr "Request link" #: workflows/templates/workflows/audit_log.html:4 #: workflows/templates/workflows/audit_log.html:15 -#: workflows/templates/workflows/home.html:132 +#: workflows/templates/workflows/home.html:148 msgid "Audit Log" msgstr "" @@ -604,6 +660,7 @@ msgstr "" #: workflows/templates/workflows/audit_log.html:54 #: workflows/templates/workflows/backup_recovery.html:43 #: workflows/templates/workflows/requests_dashboard.html:193 +#: workflows/templates/workflows/user_management.html:156 #: workflows/templates/workflows/welcome_emails.html:87 msgid "Aktion" msgstr "Action" @@ -644,6 +701,7 @@ msgid "Zurücksetzen" msgstr "Reset" #: workflows/templates/workflows/audit_log.html:52 +#: workflows/templates/workflows/user_management.html:155 msgid "Zeit" msgstr "" @@ -659,6 +717,7 @@ msgid "Ziel" msgstr "" #: workflows/templates/workflows/audit_log.html:57 +#: workflows/templates/workflows/user_management.html:159 msgid "Details" msgstr "" @@ -688,7 +747,7 @@ msgstr "No requests available yet." #: workflows/templates/workflows/backup_recovery.html:4 #: workflows/templates/workflows/backup_recovery.html:12 -#: workflows/templates/workflows/home.html:139 +#: workflows/templates/workflows/home.html:155 msgid "Backup & Recovery" msgstr "Backup & Recovery" @@ -855,9 +914,53 @@ 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 +msgid "Erlaubte Formate: SVG, PNG, JPG, JPEG, WEBP. Maximal 5 MB." +msgstr "" + +#: workflows/templates/workflows/branding_settings.html:51 +msgid "Aktuelles Logo:" +msgstr "Current logo:" + +#: workflows/templates/workflows/branding_settings.html:51 +#: workflows/templates/workflows/branding_settings.html:60 +msgid "öffnen" +msgstr "open" + +#: workflows/templates/workflows/branding_settings.html:57 +msgid "Erlaubtes Format: PDF. Maximal 10 MB." +msgstr "" + +#: workflows/templates/workflows/branding_settings.html:60 +msgid "Aktueller Briefkopf:" +msgstr "Current letterhead:" + +#: workflows/templates/workflows/branding_settings.html:65 +msgid "" +"TUBCO bleibt als Standard erhalten, bis hier Werte geändert oder Dateien " +"hochgeladen werden." +msgstr "" +"TUBCO remains the default until values are changed or files are uploaded " +"here." + +#: workflows/templates/workflows/branding_settings.html:66 +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:153 +#: workflows/templates/workflows/home.html:169 msgid "Form Builder" msgstr "Form Builder" @@ -982,7 +1085,7 @@ msgstr "Save field text" #: workflows/templates/workflows/handbook.html:4 #: workflows/templates/workflows/handbook.html:15 -#: workflows/templates/workflows/home.html:165 +#: workflows/templates/workflows/home.html:181 msgid "Handbook" msgstr "Handbook" @@ -1103,12 +1206,6 @@ msgstr "" msgid "Open Release Checklist" msgstr "" -#: workflows/templates/workflows/home.html:4 -#: workflows/templates/workflows/home.html:35 -#: workflows/templates/workflows/requests_dashboard.html:303 -msgid "TUBCO Onboarding & Offboarding Portal" -msgstr "TUBCO Onboarding & Offboarding Portal" - #: workflows/templates/workflows/home.html:26 msgid "Abmelden" msgstr "Log out" @@ -1244,88 +1341,103 @@ msgstr "PDF access" msgid "Dashboard öffnen" msgstr "Open dashboard" -#: workflows/templates/workflows/home.html:112 -msgid "Admin Apps" -msgstr "Admin Apps" - #: workflows/templates/workflows/home.html:113 -msgid "Konfiguration, Tests und Steuerung." +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:118 -msgid "Integrationen" -msgstr "Integrations" - #: workflows/templates/workflows/home.html:119 -msgid "Nextcloud- und E-Mail-Setup." -msgstr "Nextcloud and email setup." +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:127 -#: workflows/templates/workflows/home.html:134 -#: workflows/templates/workflows/home.html:141 -#: workflows/templates/workflows/home.html:148 -#: workflows/templates/workflows/home.html:155 -#: workflows/templates/workflows/home.html:160 -#: workflows/templates/workflows/home.html:167 -#: workflows/templates/workflows/home.html:174 +#: 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:125 +#: 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:126 +#: 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:133 +#: workflows/templates/workflows/home.html:149 msgid "Wichtige Admin-Aktionen nachvollziehen und prüfen." msgstr "" -#: workflows/templates/workflows/home.html:140 +#: workflows/templates/workflows/home.html:156 msgid "Backups erstellen und sicher verifizieren." msgstr "" -#: workflows/templates/workflows/home.html:146 +#: 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:147 +#: workflows/templates/workflows/home.html:163 msgid "Geplante Welcome Mails verwalten." msgstr "Manage scheduled welcome emails." -#: workflows/templates/workflows/home.html:154 +#: workflows/templates/workflows/home.html:170 msgid "Felder, Schritte und Optionen verwalten." msgstr "Manage fields, steps, and options." -#: workflows/templates/workflows/home.html:158 +#: 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:159 +#: 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:166 +#: 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:172 +#: workflows/templates/workflows/home.html:188 msgid "Django Admin" msgstr "Django Admin" -#: workflows/templates/workflows/home.html:173 +#: workflows/templates/workflows/home.html:189 msgid "Vollständige Datenverwaltung." msgstr "Full data management." -#: workflows/templates/workflows/home.html:181 +#: workflows/templates/workflows/home.html:197 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." @@ -1890,7 +2002,7 @@ msgid "Dienstliche E-Mail" msgstr "Work email" #: workflows/templates/workflows/onboarding_intro_session.html:31 -#: workflows/views.py:708 +#: workflows/views.py:820 msgid "Vertragsbeginn" msgstr "Contract start" @@ -2151,6 +2263,7 @@ msgstr "" #: workflows/templates/workflows/request_timeline.html:74 #: workflows/templates/workflows/requests_dashboard.html:190 #: workflows/templates/workflows/user_management.html:73 +#: workflows/templates/workflows/user_management.html:172 msgid "E-Mail" msgstr "Email" @@ -2330,7 +2443,12 @@ msgid "Noch keine Vorgänge vorhanden." msgstr "No requests available yet." #: workflows/templates/workflows/user_management.html:15 -msgid "Super Admins verwalten Benutzerkonten, Rollen und den aktiven Zugriff." +#, fuzzy +#| msgid "" +#| "Super Admins verwalten Benutzerkonten, Rollen und den aktiven Zugriff." +msgid "" +"Platform Owner und Super Admins verwalten Benutzerkonten, Rollen und den " +"aktiven Zugriff." msgstr "Super admins manage user accounts, roles, and active access." #: workflows/templates/workflows/user_management.html:22 @@ -2394,13 +2512,59 @@ msgid "Es sind noch keine Benutzer vorhanden." msgstr "No users exist yet." #: workflows/templates/workflows/user_management.html:140 +#, fuzzy +#| msgid "" +#| "Hinweis: Der aktuell angemeldete Super Admin kann sich hier nicht selbst " +#| "deaktivieren oder auf eine niedrigere Rolle setzen." msgid "" -"Hinweis: Der aktuell angemeldete Super Admin kann sich hier nicht selbst " -"deaktivieren oder auf eine niedrigere Rolle setzen." +"Hinweis: Der letzte aktive Platform Owner oder Super Admin kann sich hier " +"nicht selbst entfernen oder auf eine niedrigere Rolle setzen." msgstr "" "Note: The currently signed-in super admin cannot deactivate themselves or " "assign a lower role here." +#: workflows/templates/workflows/user_management.html:146 +#, fuzzy +#| msgid "Benutzer anlegen" +msgid "Letzte Benutzeraktionen" +msgstr "Create user" + +#: workflows/templates/workflows/user_management.html:147 +msgid "Die letzten Änderungen an Benutzerkonten und Rollen." +msgstr "" + +#: workflows/templates/workflows/user_management.html:149 +msgid "Zum Audit Log" +msgstr "" + +#: workflows/templates/workflows/user_management.html:157 +#, fuzzy +#| msgid "Vorgänge" +msgid "Betroffen" +msgstr "Requests" + +#: workflows/templates/workflows/user_management.html:158 +msgid "Durch" +msgstr "" + +#: workflows/templates/workflows/user_management.html:173 +#, fuzzy +#| msgid "E-Mail versendet" +msgid "Einladung versendet" +msgstr "Email sent" + +#: workflows/templates/workflows/user_management.html:174 +#, fuzzy +#| msgid "Passwort gespeichert" +msgid "Passwort geändert" +msgstr "Password saved" + +#: workflows/templates/workflows/user_management.html:180 +#, fuzzy +#| msgid "Es sind noch keine Benutzer vorhanden." +msgid "Noch keine Benutzeraktionen vorhanden." +msgstr "No users exist yet." + #: workflows/templates/workflows/welcome_emails.html:14 msgid "Geplante Welcome E-Mails" msgstr "Scheduled welcome emails" @@ -2503,7 +2667,7 @@ msgstr "Devices, software, and access" msgid "Notizen und Freigabe" msgstr "Notes and approval" -#: workflows/views.py:128 workflows/views.py:794 workflows/views.py:799 +#: workflows/views.py:128 workflows/views.py:906 workflows/views.py:911 msgid "Sie haben keine Berechtigung für diese Aktion." msgstr "You do not have permission for this action." @@ -2715,22 +2879,21 @@ msgstr "Request saved" msgid "Backup-Einstellungen gespeichert" msgstr "Save welcome settings" -#: workflows/views.py:400 +#: workflows/views.py:436 msgid "Für diesen Benutzer ist keine E-Mail-Adresse hinterlegt." msgstr "" -#: workflows/views.py:408 +#: workflows/views.py:445 #, python-format msgid "Zugangseinladung für %(username)s" msgstr "" -#: workflows/views.py:410 +#: workflows/views.py:447 #, python-format msgid "" "Hallo %(name)s,\n" "\n" -"für Sie wurde ein Benutzerkonto im TUBCO Onboarding- und Offboarding-Portal " -"angelegt.\n" +"für Sie wurde ein Benutzerkonto im %(portal_title)s angelegt.\n" "Bitte öffnen Sie den folgenden Link, um Ihr Passwort zu setzen:\n" "%(url)s\n" "\n" @@ -2738,12 +2901,12 @@ msgid "" "Ihrem Administrator." msgstr "" -#: workflows/views.py:420 +#: workflows/views.py:458 #, python-format msgid "Passwort zurücksetzen für %(username)s" msgstr "" -#: workflows/views.py:422 +#: workflows/views.py:460 #, python-format msgid "" "Hallo %(name)s,\n" @@ -2756,24 +2919,60 @@ msgid "" "ignorieren." msgstr "" -#: workflows/views.py:445 +#: workflows/views.py:498 +#, fuzzy +#| msgid "" +#| "Benutzer konnte nicht erstellt werden. Bitte prüfen Sie die Eingaben." +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 +#, fuzzy +#| msgid "Offboarding-Anfrage speichern" +msgid "Portal-Branding wurde gespeichert." +msgstr "Save offboarding request" + +#: workflows/views.py:540 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:458 +#: workflows/views.py:553 #, 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:476 +#: workflows/views.py:575 +#, fuzzy +#| msgid "" +#| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " +#| "oder herabstufen." +msgid "" +"Der aktuell angemeldete Platform Owner kann sich hier nicht selbst sperren " +"oder herabstufen." +msgstr "" +"The currently signed-in super admin cannot lock or downgrade themselves here." + +#: workflows/views.py:578 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:479 +#: workflows/views.py:581 +#, fuzzy +#| msgid "" +#| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " +#| "oder herabstufen." +msgid "" +"Der letzte aktive Platform Owner kann nicht deaktiviert oder herabgestuft " +"werden." +msgstr "" +"The currently signed-in super admin cannot lock or downgrade themselves here." + +#: workflows/views.py:584 #, fuzzy #| msgid "" #| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " @@ -2784,18 +2983,28 @@ msgid "" msgstr "" "The currently signed-in super admin cannot lock or downgrade themselves here." -#: workflows/views.py:496 +#: workflows/views.py:601 #, python-format msgid "Benutzer wurde aktualisiert: %(username)s" msgstr "User updated: %(username)s" -#: workflows/views.py:518 +#: workflows/views.py:623 #, 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:529 +#: workflows/views.py:635 +#, fuzzy +#| msgid "" +#| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " +#| "oder herabstufen." +msgid "" +"Der aktuell angemeldete Platform Owner kann sich hier nicht selbst löschen." +msgstr "" +"The currently signed-in super admin cannot lock or downgrade themselves here." + +#: workflows/views.py:638 #, fuzzy #| msgid "" #| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " @@ -2805,7 +3014,16 @@ msgid "" msgstr "" "The currently signed-in super admin cannot lock or downgrade themselves here." -#: workflows/views.py:532 +#: workflows/views.py:641 +#, fuzzy +#| msgid "" +#| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " +#| "oder herabstufen." +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 #, fuzzy #| msgid "" #| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " @@ -2814,124 +3032,132 @@ 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:545 +#: workflows/views.py:657 #, fuzzy, python-format #| msgid "Benutzer wurde erstellt: %(username)s" msgid "Benutzer wurde gelöscht: %(username)s" msgstr "User created: %(username)s" -#: workflows/views.py:632 +#: workflows/views.py:744 #, python-format msgid "Backup wurde erstellt: %(name)s" msgstr "" -#: workflows/views.py:634 +#: workflows/views.py:746 #, python-format msgid "Backup konnte nicht erstellt werden: %(error)s" msgstr "" -#: workflows/views.py:650 +#: workflows/views.py:762 #, python-format msgid "Backup wurde verifiziert: %(name)s" msgstr "" -#: workflows/views.py:652 +#: workflows/views.py:764 #, python-format msgid "Backup-Verifikation fehlgeschlagen: %(error)s" msgstr "" -#: workflows/views.py:668 +#: workflows/views.py:780 #, python-format msgid "Backup wurde gelöscht: %(name)s" msgstr "" -#: workflows/views.py:670 +#: workflows/views.py:782 #, python-format msgid "Backup konnte nicht gelöscht werden: %(error)s" msgstr "" -#: workflows/views.py:696 +#: workflows/views.py:808 #, fuzzy #| msgid "Anfrage gespeichert" msgid "Anfrage erstellt" msgstr "Request saved" -#: workflows/views.py:698 +#: workflows/views.py:810 #, fuzzy, python-format #| msgid "Sitzungsstatus" msgid "Status: %(status)s" msgstr "Session status" -#: workflows/views.py:710 +#: workflows/views.py:822 #, fuzzy #| msgid "Geplant für" msgid "Geplanter Start" msgstr "Scheduled for" -#: workflows/views.py:720 +#: workflows/views.py:832 msgid "Geräteübergabe / Hardware-Abholung" msgstr "" -#: workflows/views.py:722 +#: workflows/views.py:834 msgid "Geplanter Hardware-Termin" msgstr "" -#: workflows/views.py:731 +#: workflows/views.py:843 #, fuzzy #| msgid "Noch nicht verfügbar" msgid "PDF verfügbar" msgstr "Not available yet" -#: workflows/views.py:757 +#: workflows/views.py:869 #, fuzzy #| msgid "Einweisung" msgid "Einweisungssitzung" msgstr "Introduction" -#: workflows/views.py:769 +#: workflows/views.py:881 #, fuzzy #| msgid "Welcome E-Mails" msgid "Welcome E-Mail" msgstr "Welcome Emails" -#: workflows/views.py:808 +#: workflows/views.py:920 msgid "Keine Einträge ausgewählt." msgstr "No entries selected." -#: workflows/views.py:851 +#: workflows/views.py:963 #, python-format msgid "%(count)s Eintrag/Einträge gelöscht." msgstr "%(count)s entry/entries deleted." -#: workflows/views.py:853 +#: workflows/views.py:965 #, python-format msgid "%(count)s Auswahl(en) konnten nicht verarbeitet werden." msgstr "%(count)s selection(s) could not be processed." -#: workflows/views.py:855 +#: workflows/views.py:967 msgid "Keine passenden Einträge gefunden." msgstr "No matching entries found." -#: workflows/views.py:1082 +#: workflows/views.py:1194 msgid "Einweisungs- und Übergabeprotokoll wurde erzeugt." msgstr "Introduction and handover protocol was generated." -#: workflows/views.py:1099 +#: workflows/views.py:1211 msgid "Einweisungsprotokoll aus Live-Status wurde erzeugt." msgstr "Introduction protocol from live status was generated." -#: workflows/views.py:1128 +#: workflows/views.py:1240 msgid "Einweisung wurde zurückgesetzt." msgstr "Introduction was reset." -#: workflows/views.py:1142 +#: workflows/views.py:1254 msgid "Einweisung wurde als abgeschlossen gespeichert." msgstr "Introduction was saved as completed." -#: workflows/views.py:1155 +#: workflows/views.py:1267 msgid "Einweisung wurde als Entwurf gespeichert." msgstr "Introduction was saved as draft." +#, fuzzy +#~| msgid "Produktion" +#~ msgid "Product Owner" +#~ msgstr "Production" + +#~ msgid "TUBCO Onboarding & Offboarding Portal" +#~ msgstr "TUBCO Onboarding & Offboarding Portal" + #~ msgid "Die Passwörter stimmen nicht überein." #~ msgstr "The passwords do not match." diff --git a/backend/workflows/admin.py b/backend/workflows/admin.py index 5895ee7..d142cb2 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, ScheduledWelcomeEmail, SystemEmailConfig, WorkflowConfig +from .models import AdminAuditLog, EmployeeProfile, FormFieldConfig, FormOption, IntroChecklistItem, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, PortalBranding, ScheduledWelcomeEmail, SystemEmailConfig, WorkflowConfig @admin.register(EmployeeProfile) @@ -20,6 +20,11 @@ class AdminAuditLogAdmin(admin.ModelAdmin): ordering = ('-created_at', '-id') +@admin.register(PortalBranding) +class PortalBrandingAdmin(admin.ModelAdmin): + list_display = ('name', 'portal_title', 'company_name', 'support_email', 'default_language', 'updated_at') + + @admin.register(OnboardingRequest) class OnboardingRequestAdmin(admin.ModelAdmin): list_display = ('id', 'full_name', 'work_email', 'department', 'contract_start', 'created_at') diff --git a/backend/workflows/branding.py b/backend/workflows/branding.py new file mode 100644 index 0000000..b25a984 --- /dev/null +++ b/backend/workflows/branding.py @@ -0,0 +1,106 @@ +from __future__ import annotations + +from pathlib import Path + +from django.conf import settings +from django.templatetags.static import static + +from .models import PortalBranding + + +def get_portal_branding() -> PortalBranding: + branding, _ = PortalBranding.objects.get_or_create( + name='Default', + defaults={ + 'portal_title': 'TUBCO Onboarding & Offboarding Portal', + 'company_name': 'TUBCO', + 'support_email': 'info@tub.co', + 'default_language': 'de', + 'primary_color': '#000078', + 'secondary_color': '#c0002b', + }, + ) + return branding + + +def get_portal_logo_url() -> str: + branding = get_portal_branding() + if branding.logo_image: + try: + return branding.logo_image.url + except ValueError: + pass + return static('workflows/img/tubco-logo.svg') + + +def get_portal_letterhead_path() -> Path: + branding = get_portal_branding() + if branding.pdf_letterhead: + try: + candidate = Path(branding.pdf_letterhead.path) + if candidate.exists(): + return candidate + except (ValueError, NotImplementedError): + pass + return settings.PDF_TEMPLATES_DIR / 'templates.pdf' + + +def get_branding_context() -> dict[str, object]: + branding = get_portal_branding() + return { + 'portal_branding': branding, + 'portal_title': branding.portal_title, + 'portal_company_name': branding.company_name, + 'portal_support_email': branding.support_email, + 'portal_default_language': branding.default_language, + 'portal_primary_color': branding.primary_color, + 'portal_secondary_color': branding.secondary_color, + 'portal_logo_url': get_portal_logo_url(), + 'portal_has_custom_logo': bool(branding.logo_image), + 'portal_has_custom_letterhead': bool(branding.pdf_letterhead), + } + + +def get_branding_email_copy() -> dict[str, str]: + branding = get_portal_branding() + company_name = (branding.company_name or 'TUBCO').strip() + portal_title = (branding.portal_title or f'{company_name} Portal').strip() + return { + 'company_name': company_name, + 'portal_title': portal_title, + 'support_email': (branding.support_email or '').strip(), + } + + +def get_default_notification_templates() -> dict[str, dict[str, str]]: + from copy import deepcopy + + from .tasks import DEFAULT_NOTIFICATION_TEMPLATES + + templates = deepcopy(DEFAULT_NOTIFICATION_TEMPLATES) + company_name = get_branding_email_copy()['company_name'] + welcome = templates.get('onboarding_welcome') + if welcome: + welcome['subject'] = f'Willkommen bei {company_name}, {{ VORNAME }}' + welcome['subject_en'] = f'Welcome to {company_name}, {{ VORNAME }}' + welcome['body'] = ( + 'Hallo {{ FULL_NAME }},\n\n' + f'herzlich willkommen bei {company_name}.\n' + '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' + 'Viele Grüße\n' + f'{company_name} IT' + ) + welcome['body_en'] = ( + 'Hello {{ FULL_NAME }},\n\n' + f'welcome to {company_name}.\n' + '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' + 'Best regards,\n' + f'{company_name} IT' + ) + return templates diff --git a/backend/workflows/context_processors.py b/backend/workflows/context_processors.py index 64bad7f..162e55b 100644 --- a/backend/workflows/context_processors.py +++ b/backend/workflows/context_processors.py @@ -1,5 +1,8 @@ +from .branding import get_branding_context from .roles import template_role_context def role_context(request): - return template_role_context(getattr(request, 'user', None)) + context = template_role_context(getattr(request, 'user', None)) + context.update(get_branding_context()) + return context diff --git a/backend/workflows/forms.py b/backend/workflows/forms.py index 2853db5..d5ca11b 100644 --- a/backend/workflows/forms.py +++ b/backend/workflows/forms.py @@ -7,8 +7,8 @@ from django.utils import timezone from django.utils.translation import get_language, gettext as _, gettext_lazy from .form_builder import apply_form_field_config -from .models import EmployeeProfile, FormOption, OffboardingRequest, OnboardingRequest, WorkflowConfig -from .roles import ROLE_ADMIN, ROLE_GROUP_NAMES, ROLE_IT_STAFF, ROLE_LABELS, ROLE_STAFF, ROLE_SUPER_ADMIN, assign_user_role +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 YES_NO_CHOICES = [('', '--'), ('ja', 'Ja'), ('nein', 'Nein')] @@ -129,12 +129,13 @@ class UserManagementCreateForm(forms.Form): email = forms.EmailField(label=_('E-Mail-Adresse')) role_key = forms.ChoiceField(label=_('Rolle')) - def __init__(self, *args, **kwargs): + def __init__(self, *args, include_product_owner: bool = False, **kwargs): super().__init__(*args, **kwargs) - self.fields['role_key'].choices = [ - (role_key, str(ROLE_LABELS[role_key])) - for role_key in (ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF, ROLE_STAFF) - ] + self.include_product_owner = include_product_owner + role_order = [ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF, ROLE_STAFF] + if include_product_owner: + role_order = [ROLE_PLATFORM_OWNER] + role_order + self.fields['role_key'].choices = [(role_key, str(ROLE_LABELS[role_key])) for role_key in role_order] def clean_username(self): username = (self.cleaned_data.get('username') or '').strip() @@ -150,6 +151,8 @@ class UserManagementCreateForm(forms.Form): role_key = (self.cleaned_data.get('role_key') or '').strip() if role_key not in ROLE_GROUP_NAMES: raise forms.ValidationError(_('Ungültige Rolle.')) + if role_key == ROLE_PLATFORM_OWNER and not self.include_product_owner: + raise forms.ValidationError(_('Nur Platform Owner dürfen diese Rolle vergeben.')) return role_key def save(self): @@ -166,6 +169,53 @@ class UserManagementCreateForm(forms.Form): return user +class PortalBrandingForm(forms.ModelForm): + class Meta: + model = PortalBranding + fields = [ + 'portal_title', + 'company_name', + 'support_email', + 'default_language', + 'logo_image', + 'pdf_letterhead', + 'primary_color', + 'secondary_color', + ] + labels = { + 'portal_title': gettext_lazy('Portal-Titel'), + 'company_name': gettext_lazy('Firmenname'), + 'support_email': gettext_lazy('Support-E-Mail'), + 'default_language': gettext_lazy('Standardsprache'), + 'logo_image': gettext_lazy('Logo'), + 'pdf_letterhead': gettext_lazy('PDF-Briefkopf'), + 'primary_color': gettext_lazy('Primärfarbe'), + 'secondary_color': gettext_lazy('Sekundärfarbe'), + } + widgets = { + 'primary_color': forms.TextInput(attrs={'type': 'color'}), + 'secondary_color': forms.TextInput(attrs={'type': 'color'}), + 'logo_image': forms.ClearableFileInput(attrs={'accept': '.svg,.png,.jpg,.jpeg,.webp'}), + 'pdf_letterhead': forms.ClearableFileInput(attrs={'accept': '.pdf'}), + } + + def clean_logo_image(self): + logo = self.cleaned_data.get('logo_image') + if not logo: + return logo + if getattr(logo, 'size', 0) > 5 * 1024 * 1024: + raise forms.ValidationError(_('Das Logo darf maximal 5 MB groß sein.')) + return logo + + def clean_pdf_letterhead(self): + letterhead = self.cleaned_data.get('pdf_letterhead') + if not letterhead: + return letterhead + if getattr(letterhead, 'size', 0) > 10 * 1024 * 1024: + raise forms.ValidationError(_('Der PDF-Briefkopf darf maximal 10 MB groß sein.')) + return letterhead + + class OnboardingRequestForm(forms.ModelForm): first_name = forms.CharField(label='Vorname', required=False) last_name = forms.CharField(label='Nachname', required=False) diff --git a/backend/workflows/management/commands/bootstrap_initial_users.py b/backend/workflows/management/commands/bootstrap_initial_users.py index 5adfb7f..aa35bce 100644 --- a/backend/workflows/management/commands/bootstrap_initial_users.py +++ b/backend/workflows/management/commands/bootstrap_initial_users.py @@ -1,7 +1,7 @@ from django.contrib.auth import get_user_model from django.core.management.base import BaseCommand -from workflows.roles import ROLE_STAFF, ROLE_SUPER_ADMIN, assign_user_role, ensure_role_groups +from workflows.roles import ROLE_PLATFORM_OWNER, ROLE_STAFF, assign_user_role, ensure_role_groups DEFAULT_USERS = [ { @@ -45,7 +45,7 @@ class Command(BaseCommand): is_superuser=item['is_superuser'], ) ensure_role_groups() - assign_user_role(user, ROLE_SUPER_ADMIN if item['username'] == 'admin_test' else ROLE_STAFF) + assign_user_role(user, ROLE_PLATFORM_OWNER if item['username'] == 'admin_test' else ROLE_STAFF) self.stdout.write(f'created {user.username}') self.stdout.write(self.style.SUCCESS('initial users created')) diff --git a/backend/workflows/migrations/0036_portalbranding.py b/backend/workflows/migrations/0036_portalbranding.py new file mode 100644 index 0000000..a06b44b --- /dev/null +++ b/backend/workflows/migrations/0036_portalbranding.py @@ -0,0 +1,33 @@ +# Generated by Django 5.1.5 on 2026-03-26 10:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('workflows', '0035_workflowconfig_remote_backup_enabled_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='PortalBranding', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(default='Default', max_length=80, unique=True)), + ('portal_title', models.CharField(default='TUBCO Onboarding & Offboarding Portal', max_length=255)), + ('company_name', models.CharField(default='TUBCO', max_length=255)), + ('support_email', models.EmailField(blank=True, default='info@tub.co', max_length=254)), + ('default_language', models.CharField(choices=[('de', 'Deutsch'), ('en', 'English')], default='de', max_length=10)), + ('logo_image', models.ImageField(blank=True, null=True, upload_to='branding/')), + ('pdf_letterhead', models.FileField(blank=True, null=True, upload_to='branding/')), + ('primary_color', models.CharField(blank=True, default='#000078', max_length=20)), + ('secondary_color', models.CharField(blank=True, default='#c0002b', max_length=20)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + options={ + 'verbose_name': 'Portal Branding', + 'verbose_name_plural': 'Portal Branding', + }, + ), + ] diff --git a/backend/workflows/migrations/0037_alter_portalbranding_logo_image_and_more.py b/backend/workflows/migrations/0037_alter_portalbranding_logo_image_and_more.py new file mode 100644 index 0000000..0fb23eb --- /dev/null +++ b/backend/workflows/migrations/0037_alter_portalbranding_logo_image_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 5.1.5 on 2026-03-26 10:25 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('workflows', '0036_portalbranding'), + ] + + operations = [ + migrations.AlterField( + model_name='portalbranding', + name='logo_image', + field=models.FileField(blank=True, null=True, upload_to='branding/', validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['svg', 'png', 'jpg', 'jpeg', 'webp'])]), + ), + migrations.AlterField( + model_name='portalbranding', + name='pdf_letterhead', + field=models.FileField(blank=True, null=True, upload_to='branding/', validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['pdf'])]), + ), + ] diff --git a/backend/workflows/models.py b/backend/workflows/models.py index 7f6498c..f986b51 100644 --- a/backend/workflows/models.py +++ b/backend/workflows/models.py @@ -1,4 +1,5 @@ from django.conf import settings +from django.core.validators import FileExtensionValidator from django.db import models from django.utils.translation import get_language from django.utils.translation import gettext_lazy as _ @@ -24,6 +25,40 @@ class EmployeeProfile(models.Model): return f"{self.full_name} <{self.work_email}>" +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') + support_email = models.EmailField(blank=True, default='info@tub.co') + default_language = models.CharField( + max_length=10, + choices=[('de', 'Deutsch'), ('en', 'English')], + default='de', + ) + logo_image = models.FileField( + upload_to='branding/', + blank=True, + null=True, + validators=[FileExtensionValidator(allowed_extensions=['svg', 'png', 'jpg', 'jpeg', 'webp'])], + ) + pdf_letterhead = models.FileField( + upload_to='branding/', + blank=True, + null=True, + validators=[FileExtensionValidator(allowed_extensions=['pdf'])], + ) + primary_color = models.CharField(max_length=20, blank=True, default='#000078') + secondary_color = models.CharField(max_length=20, blank=True, default='#c0002b') + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + verbose_name = 'Portal Branding' + verbose_name_plural = 'Portal Branding' + + def __str__(self) -> str: + return self.portal_title or self.company_name or self.name + + class AdminAuditLog(models.Model): actor = models.ForeignKey( settings.AUTH_USER_MODEL, diff --git a/backend/workflows/roles.py b/backend/workflows/roles.py index f487490..890d2d0 100644 --- a/backend/workflows/roles.py +++ b/backend/workflows/roles.py @@ -4,12 +4,14 @@ from django.contrib.auth import get_user_model from django.contrib.auth.models import Group from django.utils.translation import gettext_lazy as _ +ROLE_PLATFORM_OWNER = 'platform_owner' ROLE_SUPER_ADMIN = 'super_admin' ROLE_ADMIN = 'admin' ROLE_IT_STAFF = 'it_staff' ROLE_STAFF = 'staff' ROLE_GROUP_NAMES = { + ROLE_PLATFORM_OWNER: 'Platform Owner', ROLE_SUPER_ADMIN: 'Super Admin', ROLE_ADMIN: 'Admin', ROLE_IT_STAFF: 'IT Staff', @@ -17,6 +19,7 @@ ROLE_GROUP_NAMES = { } ROLE_LABELS = { + ROLE_PLATFORM_OWNER: _('Platform Owner'), ROLE_SUPER_ADMIN: _('Super Admin'), ROLE_ADMIN: _('Admin'), ROLE_IT_STAFF: _('IT Staff'), @@ -24,19 +27,20 @@ ROLE_LABELS = { } CAPABILITIES = { - 'manage_users': {ROLE_SUPER_ADMIN}, - 'access_requests_dashboard': {ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF}, - 'run_intro_session': {ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF}, - 'generate_intro_pdfs': {ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF}, - 'retry_requests': {ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF}, - 'delete_requests': {ROLE_SUPER_ADMIN, ROLE_ADMIN}, - 'manage_integrations': {ROLE_SUPER_ADMIN, ROLE_ADMIN}, - 'manage_welcome_emails': {ROLE_SUPER_ADMIN, ROLE_ADMIN}, - 'manage_builders': {ROLE_SUPER_ADMIN, ROLE_ADMIN}, - 'view_audit_log': {ROLE_SUPER_ADMIN, ROLE_ADMIN}, - 'manage_backups': {ROLE_SUPER_ADMIN, ROLE_ADMIN}, - 'view_docs': {ROLE_SUPER_ADMIN, ROLE_ADMIN}, - 'access_django_admin_link': {ROLE_SUPER_ADMIN}, + 'manage_users': {ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN}, + 'manage_product_branding': {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}, + 'retry_requests': {ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF}, + 'delete_requests': {ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN, ROLE_ADMIN}, + 'manage_integrations': {ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN, ROLE_ADMIN}, + 'manage_welcome_emails': {ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN, ROLE_ADMIN}, + 'manage_builders': {ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN, ROLE_ADMIN}, + 'view_audit_log': {ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN, ROLE_ADMIN}, + 'manage_backups': {ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN, ROLE_ADMIN}, + 'view_docs': {ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN, ROLE_ADMIN}, + 'access_django_admin_link': {ROLE_PLATFORM_OWNER}, } @@ -54,16 +58,17 @@ def assign_user_role(user, role_key: str) -> None: user.groups.remove(*role_groups) user.groups.add(Group.objects.get(name=ROLE_GROUP_NAMES[role_key])) + is_product_owner = role_key == ROLE_PLATFORM_OWNER is_super_admin = role_key == ROLE_SUPER_ADMIN - user.is_staff = is_super_admin - user.is_superuser = is_super_admin + user.is_staff = is_product_owner or is_super_admin + user.is_superuser = is_product_owner user.save(update_fields=['is_staff', 'is_superuser']) def ensure_bootstrap_role_assignments() -> None: user_model = get_user_model() bootstrap_roles = { - 'admin_test': ROLE_SUPER_ADMIN, + 'admin_test': ROLE_PLATFORM_OWNER, 'user_test': ROLE_STAFF, } role_group_names = set(ROLE_GROUP_NAMES.values()) @@ -72,6 +77,12 @@ def ensure_bootstrap_role_assignments() -> None: user = user_model.objects.get(username=username) except user_model.DoesNotExist: continue + if role_key == ROLE_PLATFORM_OWNER and not any( + get_user_role_key(existing_user) == ROLE_PLATFORM_OWNER + for existing_user in user_model.objects.all() + ): + assign_user_role(user, ROLE_PLATFORM_OWNER) + continue if user.groups.filter(name__in=role_group_names).exists(): continue assign_user_role(user, role_key) @@ -81,15 +92,15 @@ def get_user_role_key(user) -> str: if not getattr(user, 'is_authenticated', False): return ROLE_STAFF if getattr(user, 'is_superuser', False): - return ROLE_SUPER_ADMIN + return ROLE_PLATFORM_OWNER group_names = set(user.groups.values_list('name', flat=True)) - for role_key in (ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF, ROLE_STAFF): + for role_key in (ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF, ROLE_STAFF): if ROLE_GROUP_NAMES[role_key] in group_names: return role_key if getattr(user, 'is_staff', False): - return ROLE_ADMIN + return ROLE_SUPER_ADMIN return ROLE_STAFF @@ -111,6 +122,7 @@ def template_role_context(user) -> dict[str, object]: return { 'role_key': role_key, 'role_label': str(ROLE_LABELS[role_key]), + 'can_manage_product_branding': user_has_capability(user, 'manage_product_branding'), '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/home.css b/backend/workflows/static/workflows/css/home.css index a9976a1..11817de 100644 --- a/backend/workflows/static/workflows/css/home.css +++ b/backend/workflows/static/workflows/css/home.css @@ -362,6 +362,8 @@ gap: 10px; align-items: flex-end; flex-wrap: wrap; + position: relative; + padding-left: 16px; } .section-head h2 { @@ -376,6 +378,32 @@ font-size: 13px; } + .section-divider { + height: 1px; + margin: 24px 0 14px; + border-radius: 999px; + background: linear-gradient(90deg, rgba(0, 0, 120, 0.18), rgba(0, 0, 120, 0.05) 40%, rgba(140, 29, 29, 0.10)); + } + + .section-head::before { + content: ""; + position: absolute; + left: 0; + top: 2px; + width: 4px; + height: 34px; + border-radius: 999px; + background: linear-gradient(180deg, rgba(0, 0, 120, 0.95), rgba(0, 0, 120, 0.30)); + } + + .section-head-platform::before { + background: linear-gradient(180deg, rgba(140, 29, 29, 0.90), rgba(140, 29, 29, 0.28)); + } + + .section-head-admin::before { + background: linear-gradient(180deg, rgba(159, 118, 33, 0.92), rgba(159, 118, 33, 0.28)); + } + .apps-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); diff --git a/backend/workflows/tasks.py b/backend/workflows/tasks.py index 56c305d..9e804b8 100644 --- a/backend/workflows/tasks.py +++ b/backend/workflows/tasks.py @@ -13,6 +13,7 @@ from jinja2 import Template from pypdf import PageObject, PdfReader, PdfWriter from xhtml2pdf import pisa +from .branding import get_default_notification_templates, get_portal_letterhead_path from .models import EmployeeProfile, IntroChecklistItem, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, ScheduledWelcomeEmail, WorkflowConfig from .emailing import send_system_email from .services import upload_to_nextcloud @@ -678,7 +679,7 @@ def _render_notification_template(template_key: str, context: dict, language_cod subject_template = db_template.translated_subject_template(lang) body_template = db_template.translated_body_template(lang) else: - fallback = DEFAULT_NOTIFICATION_TEMPLATES[template_key] + fallback = get_default_notification_templates()[template_key] subject_template = fallback.get(f'subject_{lang}', '') or fallback['subject'] body_template = fallback.get(f'body_{lang}', '') or fallback['body'] @@ -865,7 +866,7 @@ def _generate_onboarding_pdf(request_obj: OnboardingRequest) -> Path: temp_pdf = settings.PDF_OUTPUT_DIR / f'_temp_onboarding_{safe_name}.pdf' template_path = settings.PDF_TEMPLATES_DIR / 'onboarding_template.html' - letterhead_path = settings.PDF_TEMPLATES_DIR / 'templates.pdf' + letterhead_path = get_portal_letterhead_path() devices = _split_multiline(request_obj.needed_devices) software = _split_multiline(request_obj.needed_software) @@ -998,7 +999,7 @@ def _generate_onboarding_intro_pdf(request_obj: OnboardingRequest, language_code temp_pdf = settings.PDF_OUTPUT_DIR / f'_temp_onboarding_intro_{safe_name}.pdf' template_path = settings.PDF_TEMPLATES_DIR / 'onboarding_intro_template.html' - letterhead_path = settings.PDF_TEMPLATES_DIR / 'templates.pdf' + letterhead_path = get_portal_letterhead_path() salutation = (request_obj.get_gender_display() or '').strip() display_name = f"{salutation} {request_obj.full_name}".strip() if salutation else request_obj.full_name @@ -1048,7 +1049,7 @@ def _generate_onboarding_intro_session_pdf( temp_pdf = settings.PDF_OUTPUT_DIR / f'_temp_onboarding_intro_session_{safe_name}_{version}.pdf' template_path = settings.PDF_TEMPLATES_DIR / 'onboarding_intro_session_pdf.html' - letterhead_path = settings.PDF_TEMPLATES_DIR / 'templates.pdf' + letterhead_path = get_portal_letterhead_path() salutation = (request_obj.get_gender_display() or '').strip() display_name = f"{salutation} {request_obj.full_name}".strip() if salutation else request_obj.full_name @@ -1109,7 +1110,7 @@ def _generate_offboarding_pdf(request_obj: OffboardingRequest) -> Path: temp_pdf = settings.PDF_OUTPUT_DIR / f'_temp_offboarding_{safe_name}.pdf' template_path = settings.PDF_TEMPLATES_DIR / 'offboarding_template.html' - letterhead_path = settings.PDF_TEMPLATES_DIR / 'templates.pdf' + letterhead_path = get_portal_letterhead_path() latest_onboarding = ( OnboardingRequest.objects.filter(work_email=request_obj.work_email) .order_by('-created_at') diff --git a/backend/workflows/templates/workflows/branding_settings.html b/backend/workflows/templates/workflows/branding_settings.html new file mode 100644 index 0000000..4f9138e --- /dev/null +++ b/backend/workflows/templates/workflows/branding_settings.html @@ -0,0 +1,70 @@ +{% extends 'workflows/base_shell.html' %} +{% load static i18n %} + +{% block title %}{% trans "Branding" %}{% 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 "Branding" %}

+

{% trans "Portalname, Firmenauftritt, Logo und PDF-Briefkopf zentral verwalten." %}

+ +{% include 'workflows/includes/messages.html' %} + +
+
+ {% csrf_token %} +
+
+ + {{ form.portal_title }} +
+
+ + {{ form.company_name }} +
+
+ + {{ form.support_email }} +
+
+ + {{ form.default_language }} +
+
+ + {{ form.primary_color }} +
+
+ + {{ form.secondary_color }} +
+
+ + {{ form.logo_image }} +
{% trans "Erlaubte Formate: SVG, PNG, JPG, JPEG, WEBP. Maximal 5 MB." %}
+ {% for error in form.logo_image.errors %}
{{ error }}
{% endfor %} + {% if branding.logo_image %} +
{% trans "Aktuelles Logo:" %} {% trans "öffnen" %}
+ {% endif %} +
+
+ + {{ form.pdf_letterhead }} +
{% trans "Erlaubtes Format: PDF. Maximal 10 MB." %}
+ {% for error in form.pdf_letterhead.errors %}
{{ error }}
{% endfor %} + {% if branding.pdf_letterhead %} +
{% trans "Aktueller Briefkopf:" %} {% trans "öffnen" %}
+ {% endif %} +
+
+
+
{% trans "TUBCO bleibt als Standard erhalten, bis hier Werte geändert oder Dateien hochgeladen werden." %}
+ +
+
+
+{% endblock %} diff --git a/backend/workflows/templates/workflows/developer_handbook.html b/backend/workflows/templates/workflows/developer_handbook.html index 94841e4..76e4552 100644 --- a/backend/workflows/templates/workflows/developer_handbook.html +++ b/backend/workflows/templates/workflows/developer_handbook.html @@ -17,7 +17,7 @@ Project Wiki -

Engineering runbook for development, deployment, maintenance, and extension of the TUBCO Onboarding & Offboarding Portal.

+

Engineering runbook for development, deployment, maintenance, and extension of the current company portal deployment.

Overview @@ -133,7 +133,7 @@ docker compose exec -T web django-admin compilemessages

7) PDF Pipeline

-

10) Builder Architecture

+

10) Branding

+ + +

11) Builder Architecture

Form Builder