From e929e7509bd3d64ac58b5f67d32c02374b0a4e24 Mon Sep 17 00:00:00 2001 From: Md Bayazid Bostame Date: Fri, 27 Mar 2026 12:30:10 +0100 Subject: [PATCH] snapshot: preserve dynamic form builder parity and presets --- backend/locale/en/LC_MESSAGES/django.mo | Bin 35430 -> 35298 bytes backend/locale/en/LC_MESSAGES/django.po | 643 +++++++++------ backend/workflows/form_builder.py | 213 ++++- .../migrations/0053_formsectionconfig.py | 37 + backend/workflows/models.py | 25 + .../static/workflows/css/form_builder.css | 761 +++++++++++++++--- .../static/workflows/css/offboarding_form.css | 6 + .../templates/workflows/form_builder.html | 514 +++++++++--- .../templates/workflows/offboarding_form.html | 20 +- .../tests/test_form_builder_admin.py | 80 +- .../workflows/tests/test_onboarding_flow.py | 33 +- backend/workflows/views.py | 270 ++++++- 12 files changed, 2097 insertions(+), 505 deletions(-) create mode 100644 backend/workflows/migrations/0053_formsectionconfig.py diff --git a/backend/locale/en/LC_MESSAGES/django.mo b/backend/locale/en/LC_MESSAGES/django.mo index c9e43614907a9a468e70d2cea9f81d3e14258113..56fa243a2f8ce8a6a0da851d4c43ee4b57c91218 100644 GIT binary patch delta 9194 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 V{bBsY;N7QFLL2W6pYUAd{{Xq@Mce=Y delta 9320 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 Yae=F7LhfXppI($(kg)UB$!U@Q1H5`^uK)l5 diff --git a/backend/locale/en/LC_MESSAGES/django.po b/backend/locale/en/LC_MESSAGES/django.po index 469ca79..ca8e780 100644 --- a/backend/locale/en/LC_MESSAGES/django.po +++ b/backend/locale/en/LC_MESSAGES/django.po @@ -2,14 +2,15 @@ msgid "" msgstr "" "Project-Id-Version: tubco-portal\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-03-27 10:24+0000\n" +"POT-Creation-Date: 2026-03-27 11:06+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:35 workflows/models.py:482 workflows/models.py:563 +#: workflows/app_registry.py:35 workflows/models.py:482 workflows/models.py:521 +#: workflows/models.py:588 #: workflows/templates/workflows/onboarding_form.html:25 #: workflows/templates/workflows/requests_dashboard.html:68 #: workflows/templates/workflows/requests_dashboard.html:131 @@ -36,7 +37,7 @@ msgstr "Multi-step form" msgid "E-Mail Routing" msgstr "Email routing" -#: workflows/app_registry.py:46 workflows/models.py:483 workflows/models.py:564 +#: workflows/app_registry.py:46 workflows/models.py:483 workflows/models.py:589 #: workflows/templates/workflows/requests_dashboard.html:78 #: workflows/templates/workflows/requests_dashboard.html:132 msgid "Offboarding" @@ -126,6 +127,11 @@ msgstr "" #: workflows/app_registry.py:142 workflows/app_registry.py:151 #: workflows/app_registry.py:160 workflows/app_registry.py:169 #: workflows/app_registry.py:178 workflows/app_registry.py:187 +#: workflows/templates/workflows/form_builder.html:82 +#: workflows/templates/workflows/form_builder.html:151 +#: workflows/templates/workflows/form_builder.html:259 +#: workflows/templates/workflows/form_builder.html:269 +#: workflows/templates/workflows/form_builder.html:344 #: workflows/templates/workflows/includes/app_header.html:57 msgid "Öffnen" msgstr "Open" @@ -219,7 +225,7 @@ msgstr "Manage scheduled welcome emails." #: workflows/app_registry.py:158 #: workflows/templates/workflows/form_builder.html:4 -#: workflows/templates/workflows/form_builder.html:14 +#: workflows/templates/workflows/form_builder.html:16 msgid "Form Builder" msgstr "Form Builder" @@ -549,7 +555,7 @@ msgstr "Save offboarding request" msgid "Backup erfolgreich" msgstr "Submitted" -#: workflows/forms.py:395 workflows/views.py:1348 +#: workflows/forms.py:395 workflows/views.py:1387 #, fuzzy #| msgid "Fehlgeschlagen" msgid "Backup fehlgeschlagen" @@ -585,7 +591,7 @@ msgstr "Introduction" msgid "Workflow" msgstr "Workflow rules" -#: workflows/forms.py:416 workflows/views.py:1505 +#: workflows/forms.py:416 workflows/views.py:1544 #, fuzzy #| msgid "Welcome E-Mails" msgid "Welcome E-Mail" @@ -611,11 +617,11 @@ msgstr "Role:" msgid "Dieser Benutzername ist bereits vergeben." msgstr "This username is already taken." -#: workflows/forms.py:498 workflows/views.py:1157 +#: workflows/forms.py:498 workflows/views.py:1196 msgid "Ungültige Rolle." msgstr "Invalid role." -#: workflows/forms.py:500 workflows/views.py:1160 +#: workflows/forms.py:500 workflows/views.py:1199 msgid "Nur Platform Owner dürfen diese Rolle vergeben." msgstr "" @@ -877,35 +883,35 @@ msgstr "" msgid "Fehler" msgstr "" -#: workflows/models.py:308 workflows/views.py:601 +#: workflows/models.py:308 workflows/views.py:640 #, fuzzy #| msgid "Gesamtbestand" msgid "Gestartet" msgstr "Total records" -#: workflows/models.py:309 workflows/views.py:601 +#: workflows/models.py:309 workflows/views.py:640 #, fuzzy #| msgid "Eingereicht" msgid "Erfolgreich" msgstr "Submitted" -#: workflows/models.py:310 workflows/models.py:363 workflows/models.py:617 +#: workflows/models.py:310 workflows/models.py:363 workflows/models.py:642 #: workflows/templates/workflows/backup_recovery.html:102 #: workflows/templates/workflows/requests_dashboard.html:222 -#: workflows/templates/workflows/welcome_emails.html:108 workflows/views.py:427 -#: workflows/views.py:601 +#: workflows/templates/workflows/welcome_emails.html:108 workflows/views.py:436 +#: workflows/views.py:640 msgid "Fehlgeschlagen" msgstr "Failed" -#: workflows/models.py:360 workflows/views.py:424 +#: workflows/models.py:360 workflows/views.py:433 msgid "Eingereicht" msgstr "Submitted" -#: workflows/models.py:361 workflows/views.py:425 +#: workflows/models.py:361 workflows/views.py:434 msgid "In Bearbeitung" msgstr "Processing" -#: workflows/models.py:362 workflows/models.py:677 workflows/views.py:426 +#: workflows/models.py:362 workflows/models.py:702 workflows/views.py:435 msgid "Abgeschlossen" msgstr "Completed" @@ -959,169 +965,170 @@ msgstr "" msgid "Automatisch" msgstr "" -#: workflows/models.py:476 workflows/views.py:119 +#: workflows/models.py:476 workflows/models.py:524 workflows/views.py:128 msgid "Stammdaten" msgstr "Master data" -#: workflows/models.py:477 workflows/views.py:120 +#: workflows/models.py:477 workflows/models.py:525 workflows/views.py:129 msgid "Vertrag" msgstr "Contract" -#: workflows/models.py:478 workflows/views.py:121 +#: workflows/models.py:478 workflows/models.py:526 workflows/views.py:130 msgid "IT-Setup" msgstr "IT setup" -#: workflows/models.py:479 workflows/views.py:122 +#: workflows/models.py:479 workflows/models.py:527 workflows/views.py:131 +#: workflows/views.py:566 msgid "Abschluss" msgstr "Finish" -#: workflows/models.py:521 +#: workflows/models.py:546 #, fuzzy #| msgid "Onboarding" msgid "Onboarding: IT" msgstr "Onboarding" -#: workflows/models.py:522 +#: workflows/models.py:547 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Onboarding: Allgemeine Info" msgstr "Save offboarding request" -#: workflows/models.py:523 +#: workflows/models.py:548 #, fuzzy #| msgid "Onboarding starten" msgid "Onboarding: Visitenkarte" msgstr "Start onboarding" -#: workflows/models.py:524 +#: workflows/models.py:549 #, fuzzy #| msgid "Onboarding" msgid "Onboarding: HR Works" msgstr "Onboarding" -#: workflows/models.py:525 +#: workflows/models.py:550 #, fuzzy #| msgid "Onboarding starten" msgid "Onboarding: Schlüssel" msgstr "Start onboarding" -#: workflows/models.py:526 +#: workflows/models.py:551 msgid "Onboarding: Referenz Anfordernde Person" msgstr "" -#: workflows/models.py:527 +#: workflows/models.py:552 #, fuzzy #| msgid "Welcome E-Mails" msgid "Onboarding: Welcome E-Mail" msgstr "Welcome Emails" -#: workflows/models.py:528 +#: workflows/models.py:553 #, fuzzy #| msgid "Offboarding" msgid "Offboarding: IT" msgstr "Offboarding" -#: workflows/models.py:529 +#: workflows/models.py:554 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Offboarding: Allgemeine Info" msgstr "Save offboarding request" -#: workflows/models.py:530 +#: workflows/models.py:555 #, fuzzy #| msgid "Offboarding starten" msgid "Offboarding: HR Works Deaktivierung" msgstr "Start offboarding" -#: workflows/models.py:531 +#: workflows/models.py:556 msgid "Offboarding: Referenz Anfordernde Person" msgstr "" -#: workflows/models.py:567 +#: workflows/models.py:592 msgid "Immer" msgstr "" -#: workflows/models.py:568 workflows/models.py:646 +#: workflows/models.py:593 workflows/models.py:671 msgid "Enthält" msgstr "" -#: workflows/models.py:569 workflows/models.py:647 +#: workflows/models.py:594 workflows/models.py:672 msgid "Ist gleich" msgstr "" -#: workflows/models.py:570 +#: workflows/models.py:595 msgid "Ist aktiv/Ja" msgstr "" -#: workflows/models.py:571 +#: workflows/models.py:596 #, fuzzy #| msgid "inaktiv" msgid "Ist inaktiv/Nein" msgstr "inactive" -#: workflows/models.py:613 +#: workflows/models.py:638 #: workflows/templates/workflows/welcome_emails.html:100 msgid "Geplant" msgstr "Scheduled" -#: workflows/models.py:614 +#: workflows/models.py:639 #: workflows/templates/workflows/welcome_emails.html:102 msgid "Pausiert" msgstr "Paused" -#: workflows/models.py:615 +#: workflows/models.py:640 #: workflows/templates/workflows/welcome_emails.html:104 msgid "Abgebrochen" msgstr "Cancelled" -#: workflows/models.py:616 +#: workflows/models.py:641 #: workflows/templates/workflows/welcome_emails.html:106 msgid "Gesendet" msgstr "Sent" -#: workflows/models.py:639 workflows/tasks.py:627 +#: workflows/models.py:664 workflows/tasks.py:627 msgid "Geräte und Arbeitsplatz" msgstr "Devices and workplace" -#: workflows/models.py:640 workflows/tasks.py:628 +#: workflows/models.py:665 workflows/tasks.py:628 msgid "Konten und Berechtigungen" msgstr "Accounts and permissions" -#: workflows/models.py:641 workflows/tasks.py:629 +#: workflows/models.py:666 workflows/tasks.py:629 msgid "Software und Tools" msgstr "Software and tools" -#: workflows/models.py:642 workflows/tasks.py:630 +#: workflows/models.py:667 workflows/tasks.py:630 msgid "Prozesse und Hinweise" msgstr "Processes and notes" -#: workflows/models.py:645 +#: workflows/models.py:670 msgid "Immer anzeigen" msgstr "Always show" -#: workflows/models.py:648 +#: workflows/models.py:673 msgid "Ist Ja / aktiv" msgstr "Is yes / active" -#: workflows/models.py:649 +#: workflows/models.py:674 msgid "Ist Nein / inaktiv" msgstr "Is no / inactive" -#: workflows/models.py:676 +#: workflows/models.py:701 msgid "Entwurf" msgstr "Draft" -#: workflows/models.py:696 +#: workflows/models.py:721 #, fuzzy #| msgid "Nextcloud:" msgid "Nextcloud" msgstr "Nextcloud:" -#: workflows/models.py:697 +#: workflows/models.py:722 msgid "S3" msgstr "" -#: workflows/models.py:698 +#: workflows/models.py:723 msgid "NFS" msgstr "" @@ -1518,7 +1525,7 @@ msgstr "" #: workflows/templates/workflows/account_profile.html:262 #: workflows/templates/workflows/app_registry.html:35 #: workflows/templates/workflows/app_registry.html:84 -#: workflows/templates/workflows/form_builder.html:91 +#: workflows/templates/workflows/form_builder.html:308 #: workflows/templates/workflows/integrations_setup.html:263 #: workflows/templates/workflows/intro_builder.html:65 #: workflows/templates/workflows/trial_management.html:28 @@ -1707,7 +1714,7 @@ msgstr "Last updated" #: workflows/templates/workflows/app_registry.html:4 #: workflows/templates/workflows/app_registry.html:103 -#: workflows/templates/workflows/form_builder.html:87 +#: workflows/templates/workflows/form_builder.html:304 #: workflows/templates/workflows/intro_builder.html:58 msgid "Sortierung" msgstr "Sort order" @@ -1838,6 +1845,7 @@ msgid "Platzierung" msgstr "Sort order" #: workflows/templates/workflows/app_registry.html:166 +#: workflows/templates/workflows/form_builder.html:36 #, fuzzy #| msgid "Reihenfolge speichern" msgid "Reihenfolge" @@ -2193,8 +2201,8 @@ msgid "Backup-Bundle wirklich löschen?" msgstr "Delete this backup bundle?" #: workflows/templates/workflows/backup_recovery.html:133 -#: workflows/templates/workflows/form_builder.html:92 -#: workflows/templates/workflows/form_builder.html:107 +#: workflows/templates/workflows/form_builder.html:309 +#: workflows/templates/workflows/form_builder.html:324 #: workflows/templates/workflows/integrations_setup.html:265 #: workflows/templates/workflows/intro_builder.html:66 #: workflows/templates/workflows/intro_builder.html:102 @@ -2324,109 +2332,249 @@ msgid "" msgstr "" #: 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." +msgid "Deployment Configuration" +msgstr "" -#: workflows/templates/workflows/form_builder.html:29 +#: workflows/templates/workflows/form_builder.html:27 msgid "Reihenfolge speichern" msgstr "Save order" -#: workflows/templates/workflows/form_builder.html:46 +#: workflows/templates/workflows/form_builder.html:35 +#, fuzzy +#| msgid "Eingereicht" +msgid "Bereiche" +msgstr "Submitted" + +#: workflows/templates/workflows/form_builder.html:37 +#, fuzzy +#| msgid "Regelname" +msgid "Regeln" +msgstr "Rule name" + +#: workflows/templates/workflows/form_builder.html:38 +#: workflows/templates/workflows/form_builder.html:257 +#, fuzzy +#| msgid "Optionen verwalten" +msgid "Optionen & Texte" +msgstr "Manage options" + +#: workflows/templates/workflows/form_builder.html:43 +msgid "Fixe Kernfelder" +msgstr "" + +#: workflows/templates/workflows/form_builder.html:47 +msgid "Konfigurierbar" +msgstr "" + +#: workflows/templates/workflows/form_builder.html:51 +#, fuzzy +#| msgid "Ausgeblendet" +msgid "Aktuell ausgeblendet" +msgstr "Hidden" + +#: workflows/templates/workflows/form_builder.html:56 +#, fuzzy +#| msgid "Abschnitt" +msgid "Versteckte Abschnitte" +msgstr "Section" + +#: workflows/templates/workflows/form_builder.html:63 +msgid "Presets" +msgstr "" + +#: workflows/templates/workflows/form_builder.html:80 +msgid "Live-Vorschau" +msgstr "" + +#: workflows/templates/workflows/form_builder.html:91 +#: workflows/templates/workflows/form_builder.html:122 +#: workflows/templates/workflows/form_builder.html:203 +#, fuzzy, python-format +#| msgid "Keine konfigurierten Felder in diesem Schritt." +msgid "%(count)s Feld/Felder" +msgstr "No configured fields in this step." + +#: workflows/templates/workflows/form_builder.html:97 +msgid "Keine sichtbaren Felder." +msgstr "" + +#: workflows/templates/workflows/form_builder.html:110 +#, fuzzy +#| msgid "Reihenfolge speichern" +msgid "Struktur & Reihenfolge" +msgstr "Save order" + +#: workflows/templates/workflows/form_builder.html:112 +#, fuzzy +#| msgid "öffnen" +msgid "Geöffnet" +msgstr "open" + +#: workflows/templates/workflows/form_builder.html:132 +#: workflows/templates/workflows/form_builder.html:178 +#: workflows/templates/workflows/form_builder.html:227 msgid "Fix" msgstr "Fixed" -#: workflows/templates/workflows/form_builder.html:47 +#: workflows/templates/workflows/form_builder.html:133 +#: workflows/templates/workflows/form_builder.html:180 +#: workflows/templates/workflows/form_builder.html:229 msgid "Ausgeblendet" msgstr "Hidden" -#: workflows/templates/workflows/form_builder.html:48 +#: workflows/templates/workflows/form_builder.html:134 +#: workflows/templates/workflows/form_builder.html:218 +#: workflows/templates/workflows/form_builder.html:221 +#: workflows/templates/workflows/form_builder.html:231 msgid "Pflicht" msgstr "Required" -#: workflows/templates/workflows/form_builder.html:59 +#: workflows/templates/workflows/form_builder.html:149 +#, fuzzy +#| msgid "Sicherheitsregeln" +msgid "Sichtbarkeit & Regeln" +msgstr "Safety rules" + +#: workflows/templates/workflows/form_builder.html:159 +#, fuzzy +#| msgid "Abschnitt" +msgid "Abschnitte steuern" +msgstr "Section" + +#: workflows/templates/workflows/form_builder.html:168 +#, fuzzy, python-format +#| msgid "Keine konfigurierten Felder in diesem Schritt." +msgid "%(count)s Feld/Felder in diesem Abschnitt." +msgstr "No configured fields in this step." + +#: workflows/templates/workflows/form_builder.html:179 +#: workflows/templates/workflows/form_builder.html:214 +msgid "Sichtbar" +msgstr "" + +#: workflows/templates/workflows/form_builder.html:187 +#, fuzzy +#| msgid "Regeln speichern" +msgid "Abschnittsregeln speichern" +msgstr "Save rules" + +#: workflows/templates/workflows/form_builder.html:194 +#, fuzzy +#| msgid "Feldtexte verwalten" +msgid "Feldregeln verwalten" +msgstr "Manage field text" + +#: workflows/templates/workflows/form_builder.html:220 +#, fuzzy +#| msgid "Standardsprache" +msgid "Standard" +msgstr "Default language" + +#: workflows/templates/workflows/form_builder.html:222 +#: workflows/templates/workflows/user_management.html:109 +msgid "Optional" +msgstr "Optional" + +#: workflows/templates/workflows/form_builder.html:233 +msgid "Flexibel" +msgstr "" + +#: workflows/templates/workflows/form_builder.html:238 +#, fuzzy +#| msgid "Keine Feldkonfigurationen verfügbar." +msgid "Keine Feldregeln verfügbar." +msgstr "No field configurations available." + +#: workflows/templates/workflows/form_builder.html:245 +#, fuzzy +#| msgid "Regeln speichern" +msgid "Feldregeln speichern" +msgstr "Save rules" + +#: workflows/templates/workflows/form_builder.html:268 msgid "Optionen verwalten" msgstr "Manage options" -#: workflows/templates/workflows/form_builder.html:62 +#: workflows/templates/workflows/form_builder.html:279 msgid "Kategorie" msgstr "Category" -#: workflows/templates/workflows/form_builder.html:75 -#: workflows/templates/workflows/form_builder.html:88 -#: workflows/templates/workflows/form_builder.html:133 +#: workflows/templates/workflows/form_builder.html:292 +#: workflows/templates/workflows/form_builder.html:305 +#: workflows/templates/workflows/form_builder.html:355 msgid "Label (DE)" msgstr "Label (DE)" -#: workflows/templates/workflows/form_builder.html:76 +#: workflows/templates/workflows/form_builder.html:293 msgid "Label (EN, optional)" msgstr "Label (EN, optional)" -#: workflows/templates/workflows/form_builder.html:77 +#: workflows/templates/workflows/form_builder.html:294 msgid "Technischer Wert (optional)" msgstr "Technical value (optional)" -#: workflows/templates/workflows/form_builder.html:78 +#: workflows/templates/workflows/form_builder.html:295 msgid "Option hinzufügen" msgstr "Add option" -#: workflows/templates/workflows/form_builder.html:89 -#: workflows/templates/workflows/form_builder.html:134 +#: workflows/templates/workflows/form_builder.html:306 +#: workflows/templates/workflows/form_builder.html:356 msgid "Label (EN)" msgstr "Label (EN)" -#: workflows/templates/workflows/form_builder.html:100 +#: workflows/templates/workflows/form_builder.html:317 msgid "Ziehen zum Sortieren" msgstr "Drag to reorder" -#: workflows/templates/workflows/form_builder.html:107 +#: workflows/templates/workflows/form_builder.html:324 msgid "Option wirklich löschen?" msgstr "Delete this option?" -#: workflows/templates/workflows/form_builder.html:111 +#: workflows/templates/workflows/form_builder.html:328 msgid "Keine Optionen in dieser Kategorie." msgstr "No options in this category." -#: workflows/templates/workflows/form_builder.html:117 +#: workflows/templates/workflows/form_builder.html:334 msgid "Optionen speichern" msgstr "Save options" -#: workflows/templates/workflows/form_builder.html:124 +#: workflows/templates/workflows/form_builder.html:343 msgid "Feldtexte verwalten" msgstr "Manage field text" -#: workflows/templates/workflows/form_builder.html:132 +#: workflows/templates/workflows/form_builder.html:354 msgid "Feld" msgstr "Field" -#: workflows/templates/workflows/form_builder.html:135 +#: workflows/templates/workflows/form_builder.html:357 msgid "Hilfetext (DE)" msgstr "Help text (DE)" -#: workflows/templates/workflows/form_builder.html:136 +#: workflows/templates/workflows/form_builder.html:358 msgid "Hilfetext (EN)" msgstr "Help text (EN)" -#: workflows/templates/workflows/form_builder.html:146 +#: workflows/templates/workflows/form_builder.html:372 msgid "Fallback: Standardlabel" msgstr "Fallback: default label" -#: workflows/templates/workflows/form_builder.html:147 +#: workflows/templates/workflows/form_builder.html:373 msgid "English label" msgstr "English label" -#: workflows/templates/workflows/form_builder.html:148 +#: workflows/templates/workflows/form_builder.html:374 msgid "Optionaler Hilfetext" msgstr "Optional help text" -#: workflows/templates/workflows/form_builder.html:149 +#: workflows/templates/workflows/form_builder.html:375 msgid "Optional English help text" msgstr "Optional English help text" -#: workflows/templates/workflows/form_builder.html:152 +#: workflows/templates/workflows/form_builder.html:378 msgid "Keine Feldkonfigurationen verfügbar." msgstr "No field configurations available." -#: workflows/templates/workflows/form_builder.html:158 +#: workflows/templates/workflows/form_builder.html:385 msgid "Feldtexte speichern" msgstr "Save field text" @@ -3061,7 +3209,7 @@ msgstr "Search" msgid "Vorbefüllt aus:" msgstr "Prefilled from:" -#: workflows/templates/workflows/offboarding_form.html:64 +#: workflows/templates/workflows/offboarding_form.html:74 msgid "Offboarding-Anfrage speichern" msgstr "Save offboarding request" @@ -3203,7 +3351,7 @@ msgid "Dienstliche E-Mail" msgstr "Work email" #: workflows/templates/workflows/onboarding_intro_session.html:31 -#: workflows/views.py:1444 +#: workflows/views.py:1483 msgid "Vertragsbeginn" msgstr "Contract start" @@ -3849,10 +3997,6 @@ msgstr "Change roles, block access, or set a new password." msgid "Sie selbst" msgstr "You" -#: workflows/templates/workflows/user_management.html:109 -msgid "Optional" -msgstr "Optional" - #: workflows/templates/workflows/user_management.html:119 msgid "Reset-Link senden" msgstr "" @@ -4007,324 +4151,350 @@ msgstr "Resume" msgid "Keine geplanten Welcome E-Mails vorhanden." msgstr "No scheduled welcome emails available." -#: workflows/views.py:119 +#: workflows/views.py:128 msgid "Person, Rolle, Abteilung" msgstr "Person, role, department" -#: workflows/views.py:120 +#: workflows/views.py:129 msgid "Beschäftigung und Termine" msgstr "Employment and dates" -#: workflows/views.py:121 +#: workflows/views.py:130 msgid "Geräte, Software und Zugänge" msgstr "Devices, software, and access" -#: workflows/views.py:122 +#: workflows/views.py:131 msgid "Notizen und Freigabe" msgstr "Notes and approval" -#: workflows/views.py:255 +#: workflows/views.py:264 #, fuzzy #| msgid "Lokal gespeichert" msgid "Profilbild gespeichert." msgstr "Stored locally" -#: workflows/views.py:257 +#: workflows/views.py:266 #, fuzzy #| msgid "Passwort konnte nicht gespeichert werden" msgid "Profilbild konnte nicht gespeichert werden." msgstr "Password could not be saved" -#: workflows/views.py:263 +#: workflows/views.py:272 #, fuzzy #| msgid "Lokal gespeichert" msgid "Profildaten gespeichert." msgstr "Stored locally" -#: workflows/views.py:265 +#: workflows/views.py:274 #, fuzzy #| msgid "Passwort konnte nicht gespeichert werden" msgid "Profildaten konnten nicht gespeichert werden." msgstr "Password could not be saved" -#: workflows/views.py:271 +#: workflows/views.py:280 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Benachrichtigungseinstellungen gespeichert." msgstr "Save offboarding request" -#: workflows/views.py:273 +#: workflows/views.py:282 #, fuzzy #| msgid "Passwort konnte nicht gespeichert werden" msgid "Benachrichtigungseinstellungen konnten nicht gespeichert werden." msgstr "Password could not be saved" -#: workflows/views.py:282 +#: workflows/views.py:291 #, fuzzy #| msgid "Deaktivieren" msgid "TOTP wurde aktiviert." msgstr "Disabled" -#: workflows/views.py:284 +#: workflows/views.py:293 #, fuzzy #| msgid "Passwort konnte nicht gespeichert werden" msgid "TOTP konnte nicht aktiviert werden." msgstr "Password could not be saved" -#: workflows/views.py:291 +#: workflows/views.py:300 msgid "TOTP wurde deaktiviert." msgstr "" -#: workflows/views.py:293 +#: workflows/views.py:302 #, fuzzy #| msgid "Passwort konnte nicht gespeichert werden" msgid "TOTP konnte nicht deaktiviert werden." msgstr "Password could not be saved" -#: workflows/views.py:302 +#: workflows/views.py:311 msgid "Recovery-Codes wurden neu erzeugt." msgstr "" -#: workflows/views.py:304 +#: workflows/views.py:313 #, fuzzy #| msgid "Passwort konnte nicht gespeichert werden" msgid "Recovery-Codes konnten nicht neu erzeugt werden." msgstr "Password could not be saved" -#: workflows/views.py:353 workflows/views.py:1530 workflows/views.py:1535 +#: workflows/views.py:362 workflows/views.py:1569 workflows/views.py:1574 msgid "Sie haben keine Berechtigung für diese Aktion." msgstr "You do not have permission for this action." -#: workflows/views.py:434 +#: workflows/views.py:443 #, fuzzy #| msgid "Vorgänge" msgid "Vorgänge gelöscht" msgstr "Requests" -#: workflows/views.py:435 +#: workflows/views.py:444 msgid "Vorgang gelöscht" msgstr "" -#: workflows/views.py:436 +#: workflows/views.py:445 msgid "Vorgang erneut angestoßen" msgstr "" -#: workflows/views.py:437 +#: workflows/views.py:446 #, fuzzy #| msgid "Einweisung" msgid "Einweisungs-PDF erzeugt" msgstr "Introduction" -#: workflows/views.py:438 +#: workflows/views.py:447 #, fuzzy #| msgid "Live-Protokoll erzeugen" msgid "Live-Protokoll erzeugt" msgstr "Generate live protocol" -#: workflows/views.py:439 +#: workflows/views.py:448 #, fuzzy #| msgid "Einweisung wurde zurückgesetzt." msgid "Einweisung zurückgesetzt" msgstr "Introduction was reset." -#: workflows/views.py:440 +#: workflows/views.py:449 #, fuzzy #| msgid "Einweisung wurde als Entwurf gespeichert." msgid "Einweisung als Entwurf gespeichert" msgstr "Introduction was saved as draft." -#: workflows/views.py:441 +#: workflows/views.py:450 #, fuzzy #| msgid "Einweisung wurde als abgeschlossen gespeichert." msgid "Einweisung abgeschlossen" msgstr "Introduction was saved as completed." -#: workflows/views.py:442 +#: workflows/views.py:451 msgid "Formularoption gelöscht" msgstr "" -#: workflows/views.py:443 +#: workflows/views.py:452 #, fuzzy #| msgid "Optionen speichern" msgid "Formularoptionen gespeichert" msgstr "Save options" -#: workflows/views.py:444 +#: workflows/views.py:453 #, fuzzy #| msgid "Feldtexte speichern" msgid "Feldtexte gespeichert" msgstr "Save field text" -#: workflows/views.py:445 +#: workflows/views.py:454 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Formularlayout gespeichert" msgstr "Save offboarding request" -#: workflows/views.py:446 +#: workflows/views.py:455 msgid "Einweisungs-Checkpunkt gelöscht" msgstr "" -#: workflows/views.py:447 +#: workflows/views.py:456 msgid "Einweisungs-Checkpunkt hinzugefügt" msgstr "" -#: workflows/views.py:448 +#: workflows/views.py:457 #, fuzzy #| msgid "Checkliste speichern" msgid "Einweisungs-Checkliste gespeichert" msgstr "Save checklist" -#: workflows/views.py:449 +#: workflows/views.py:458 #, fuzzy #| msgid "Welcome E-Mails" msgid "Welcome E-Mail sofort ausgelöst" msgstr "Welcome Emails" -#: workflows/views.py:450 +#: workflows/views.py:459 #, fuzzy #| msgid "Welcome-Einstellungen speichern" msgid "Welcome E-Mail Einstellungen gespeichert" msgstr "Save welcome settings" -#: workflows/views.py:451 +#: workflows/views.py:460 msgid "Welcome E-Mail Sammelaktion ausgeführt" msgstr "" -#: workflows/views.py:452 +#: workflows/views.py:461 #, fuzzy #| msgid "Welcome E-Mails" msgid "Welcome E-Mail pausiert" msgstr "Welcome Emails" -#: workflows/views.py:453 +#: workflows/views.py:462 #, fuzzy #| msgid "Welcome E-Mails" msgid "Welcome E-Mail fortgesetzt" msgstr "Welcome Emails" -#: workflows/views.py:454 +#: workflows/views.py:463 #, fuzzy #| msgid "Welcome E-Mails" msgid "Welcome E-Mail abgebrochen" msgstr "Welcome Emails" -#: workflows/views.py:455 +#: workflows/views.py:464 #, fuzzy #| msgid "SMTP-Test" msgid "SMTP-Test gesendet" msgstr "SMTP test" -#: workflows/views.py:456 +#: workflows/views.py:465 #, fuzzy #| msgid "Nextcloud-Test" msgid "Nextcloud-Testupload ausgeführt" msgstr "Nextcloud test" -#: workflows/views.py:457 +#: workflows/views.py:466 #, fuzzy #| msgid "Nextcloud schalten" msgid "Nextcloud-Modus umgeschaltet" msgstr "Toggle Nextcloud" -#: workflows/views.py:458 +#: workflows/views.py:467 msgid "E-Mail-Modus umgeschaltet" msgstr "" -#: workflows/views.py:459 +#: workflows/views.py:468 #, fuzzy #| msgid "Integrationen Setup" msgid "Integrationen gespeichert" msgstr "Integrations Setup" -#: workflows/views.py:460 +#: workflows/views.py:469 #, fuzzy #| msgid "Welcome-Einstellungen speichern" msgid "Nextcloud-Einstellungen gespeichert" msgstr "Save welcome settings" -#: workflows/views.py:461 +#: workflows/views.py:470 #, fuzzy #| msgid "Welcome-Einstellungen speichern" msgid "Mail-Einstellungen gespeichert" msgstr "Save welcome settings" -#: workflows/views.py:462 +#: workflows/views.py:471 #, fuzzy #| msgid "E-Mail Routing & Vorlagen speichern" msgid "E-Mail-Routing gespeichert" msgstr "Save email routing & templates" -#: workflows/views.py:463 +#: workflows/views.py:472 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Benachrichtigungsregeln gespeichert" msgstr "Save offboarding request" -#: workflows/views.py:464 +#: workflows/views.py:473 #, fuzzy #| msgid "Anfrage gespeichert" msgid "Benutzer erstellt" msgstr "Request saved" -#: workflows/views.py:465 +#: workflows/views.py:474 msgid "Benutzer aktualisiert" msgstr "" -#: workflows/views.py:466 +#: workflows/views.py:475 msgid "Passwort-Reset-Link versendet" msgstr "" -#: workflows/views.py:467 +#: workflows/views.py:476 #, fuzzy #| msgid "Benutzerübersicht" msgid "Benutzer gelöscht" msgstr "User overview" -#: workflows/views.py:468 +#: workflows/views.py:477 #, fuzzy #| msgid "Anfrage gespeichert" msgid "Backup erstellt" msgstr "Request saved" -#: workflows/views.py:469 +#: workflows/views.py:478 msgid "Backup verifiziert" msgstr "" -#: workflows/views.py:470 +#: workflows/views.py:479 #, fuzzy #| msgid "Anfrage gespeichert" msgid "Backup gelöscht" msgstr "Request saved" -#: workflows/views.py:471 +#: workflows/views.py:480 #, fuzzy #| msgid "Welcome-Einstellungen speichern" msgid "Backup-Einstellungen gespeichert" msgstr "Save welcome settings" -#: workflows/views.py:472 +#: workflows/views.py:481 #, fuzzy #| msgid "Anfrage gespeichert" msgid "App-Registry gespeichert" msgstr "Request saved" -#: workflows/views.py:644 +#: workflows/views.py:564 +#, fuzzy +#| msgid "Mitarbeiter" +msgid "Mitarbeitende" +msgstr "Staff" + +#: workflows/views.py:564 +#, fuzzy +#| msgid "Person, Rolle, Abteilung" +msgid "Person, Rolle und Bereich" +msgstr "Person, role, department" + +#: workflows/views.py:565 +msgid "Austritt" +msgstr "" + +#: workflows/views.py:565 +msgid "Letzter Arbeitstag" +msgstr "" + +#: workflows/views.py:566 +#, fuzzy +#| msgid "Einweisung wurde als abgeschlossen gespeichert." +msgid "Hinweise und Abschlussnotizen" +msgstr "Introduction was saved as completed." + +#: workflows/views.py:683 #, fuzzy #| msgid "Anfrage gespeichert" msgid "App-Registry gespeichert." msgstr "Request saved" -#: workflows/views.py:743 +#: workflows/views.py:782 msgid "Für diesen Benutzer ist keine E-Mail-Adresse hinterlegt." msgstr "" -#: workflows/views.py:752 +#: workflows/views.py:791 #, python-format msgid "Zugangseinladung für %(username)s" msgstr "" -#: workflows/views.py:754 +#: workflows/views.py:793 #, python-format msgid "" "Hallo %(name)s,\n" @@ -4337,12 +4507,12 @@ msgid "" "Ihrem Administrator." msgstr "" -#: workflows/views.py:765 +#: workflows/views.py:804 #, python-format msgid "Passwort zurücksetzen für %(username)s" msgstr "" -#: workflows/views.py:767 +#: workflows/views.py:806 #, python-format msgid "" "Hallo %(name)s,\n" @@ -4355,7 +4525,7 @@ msgid "" "ignorieren." msgstr "" -#: workflows/views.py:818 +#: workflows/views.py:857 #, fuzzy #| msgid "" #| "Benutzer konnte nicht erstellt werden. Bitte prüfen Sie die Eingaben." @@ -4363,69 +4533,69 @@ 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:846 +#: workflows/views.py:885 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Portal-Branding wurde gespeichert." msgstr "Save offboarding request" -#: workflows/views.py:863 +#: workflows/views.py:902 msgid "Identität" msgstr "" -#: workflows/views.py:864 +#: workflows/views.py:903 msgid "Titel, Firmenname und zentrale Spracheinstellungen." msgstr "" -#: workflows/views.py:868 +#: workflows/views.py:907 msgid "" "Wird für E-Mail-Vorschläge und Domain-bezogene Standardtexte verwendet, z. " "B. workdock.de." msgstr "" -#: workflows/views.py:873 +#: workflows/views.py:912 msgid "Farben & Erscheinungsbild" msgstr "" -#: workflows/views.py:874 +#: workflows/views.py:913 msgid "Zentrale visuelle Markenwerte und Browser-Icon." msgstr "" -#: workflows/views.py:878 +#: workflows/views.py:917 msgid "Erlaubte Formate: SVG, PNG, JPG, JPEG, WEBP. Maximal 5 MB." msgstr "" -#: workflows/views.py:879 +#: workflows/views.py:918 msgid "Erlaubte Formate: ICO, PNG, SVG, WEBP. Maximal 2 MB." msgstr "" -#: workflows/views.py:884 +#: workflows/views.py:923 #, fuzzy #| msgid "Produktion" msgid "Kommunikation" msgstr "Production" -#: workflows/views.py:885 +#: workflows/views.py:924 msgid "Absender, Support und PDF-Branding für ausgehende Kommunikation." msgstr "" -#: workflows/views.py:889 +#: workflows/views.py:928 msgid "Wird für ausgehende System-E-Mails als Anzeigename verwendet." msgstr "" -#: workflows/views.py:890 +#: workflows/views.py:929 msgid "Erlaubtes Format: PDF. Maximal 10 MB." msgstr "" -#: workflows/views.py:895 +#: workflows/views.py:934 msgid "Footer & Rechtliches" msgstr "" -#: workflows/views.py:896 +#: workflows/views.py:935 msgid "Gemeinsame Footer-Texte und rechtliche Hinweise für die Shell." msgstr "" -#: workflows/views.py:950 +#: workflows/views.py:989 #, fuzzy #| msgid "" #| "Benutzer konnte nicht erstellt werden. Bitte prüfen Sie die Eingaben." @@ -4434,53 +4604,53 @@ msgid "" "Eingaben." msgstr "User could not be created. Please check the input." -#: workflows/views.py:979 +#: workflows/views.py:1018 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Firmenkonfiguration wurde gespeichert." msgstr "Save offboarding request" -#: workflows/views.py:996 +#: workflows/views.py:1035 #, fuzzy #| msgid "Firmenname" msgid "Firmenprofil" msgstr "Company name" -#: workflows/views.py:997 +#: workflows/views.py:1036 msgid "Rechtlicher Name und zentrale Stammdaten der Firma." msgstr "" -#: workflows/views.py:1002 +#: workflows/views.py:1041 msgid "Adresse & Register" msgstr "" -#: workflows/views.py:1003 +#: workflows/views.py:1042 msgid "Anschrift sowie optionale Register- und Steuerangaben." msgstr "" -#: workflows/views.py:1008 +#: workflows/views.py:1047 msgid "Kontaktpunkte" msgstr "" -#: workflows/views.py:1009 +#: workflows/views.py:1048 msgid "Zentrale Ansprechpartner für HR, IT und Operations." msgstr "" -#: workflows/views.py:1014 +#: workflows/views.py:1053 msgid "Recht & Öffentlichkeit" msgstr "" -#: workflows/views.py:1015 +#: workflows/views.py:1054 msgid "Öffentliche Links für Website, Impressum und Datenschutz." msgstr "" -#: workflows/views.py:1017 +#: workflows/views.py:1056 msgid "" "Diese Links können später im Portal-Footer oder in öffentlichen Seiten " "verwendet werden." msgstr "" -#: workflows/views.py:1057 +#: workflows/views.py:1096 #, fuzzy #| msgid "" #| "Benutzer konnte nicht erstellt werden. Bitte prüfen Sie die Eingaben." @@ -4489,54 +4659,54 @@ msgid "" "Eingaben." msgstr "Trial configuration could not be saved. Please check the input." -#: workflows/views.py:1089 +#: workflows/views.py:1128 #, fuzzy #| msgid "Trial abgelaufen" msgid "Trial ist abgelaufen" msgstr "Trial expired" -#: workflows/views.py:1090 +#: workflows/views.py:1129 msgid "" "Der Trial-Zeitraum ist überschritten. Nicht-Platform-Owner werden jetzt " "blockiert." msgstr "" -#: workflows/views.py:1098 +#: workflows/views.py:1137 msgid "Trial läuft bald ab" msgstr "" -#: workflows/views.py:1099 +#: workflows/views.py:1138 #, python-format msgid "Der Trial endet am %(date)s." msgstr "" -#: workflows/views.py:1107 +#: workflows/views.py:1146 #, fuzzy #| msgid "Trial-Modus" msgid "Trial-Modus deaktiviert" msgstr "Trial mode" -#: workflows/views.py:1108 +#: workflows/views.py:1147 #, fuzzy #| msgid "Nextcloud schalten" msgid "Der Trial-Modus wurde ausgeschaltet." msgstr "Toggle Nextcloud" -#: workflows/views.py:1113 +#: workflows/views.py:1152 msgid "Trial-Konfiguration wurde gespeichert." msgstr "Trial configuration was saved." -#: workflows/views.py:1130 +#: workflows/views.py:1169 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:1143 +#: workflows/views.py:1182 #, 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:1165 +#: workflows/views.py:1204 #, fuzzy #| msgid "" #| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " @@ -4547,14 +4717,14 @@ msgid "" msgstr "" "The currently signed-in super admin cannot lock or downgrade themselves here." -#: workflows/views.py:1168 +#: workflows/views.py:1207 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:1171 +#: workflows/views.py:1210 #, fuzzy #| msgid "" #| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " @@ -4565,7 +4735,7 @@ msgid "" msgstr "" "The currently signed-in super admin cannot lock or downgrade themselves here." -#: workflows/views.py:1174 +#: workflows/views.py:1213 #, fuzzy #| msgid "" #| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " @@ -4576,18 +4746,18 @@ msgid "" msgstr "" "The currently signed-in super admin cannot lock or downgrade themselves here." -#: workflows/views.py:1191 +#: workflows/views.py:1230 #, python-format msgid "Benutzer wurde aktualisiert: %(username)s" msgstr "User updated: %(username)s" -#: workflows/views.py:1213 +#: workflows/views.py:1252 #, 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:1225 +#: workflows/views.py:1264 #, fuzzy #| msgid "" #| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " @@ -4597,7 +4767,7 @@ msgid "" msgstr "" "The currently signed-in super admin cannot lock or downgrade themselves here." -#: workflows/views.py:1228 +#: workflows/views.py:1267 #, fuzzy #| msgid "" #| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " @@ -4607,7 +4777,7 @@ msgid "" msgstr "" "The currently signed-in super admin cannot lock or downgrade themselves here." -#: workflows/views.py:1231 +#: workflows/views.py:1270 #, fuzzy #| msgid "" #| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " @@ -4616,7 +4786,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:1234 +#: workflows/views.py:1273 #, fuzzy #| msgid "" #| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " @@ -4625,192 +4795,195 @@ 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:1247 +#: workflows/views.py:1286 #, fuzzy, python-format #| msgid "Benutzer wurde erstellt: %(username)s" msgid "Benutzer wurde gelöscht: %(username)s" msgstr "User created: %(username)s" -#: workflows/views.py:1338 +#: workflows/views.py:1377 #, fuzzy, python-format #| msgid "Anfrage gespeichert" msgid "Backup erstellt: %(name)s" msgstr "Request saved" -#: workflows/views.py:1339 +#: workflows/views.py:1378 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Das Backup-Bundle wurde erfolgreich erstellt." msgstr "Save offboarding request" -#: workflows/views.py:1344 +#: workflows/views.py:1383 #, python-format msgid "Backup wurde erstellt: %(name)s" msgstr "" -#: workflows/views.py:1354 +#: workflows/views.py:1393 #, python-format msgid "Backup konnte nicht erstellt werden: %(error)s" msgstr "" -#: workflows/views.py:1372 +#: workflows/views.py:1411 #, fuzzy, python-format #| msgid "Backup wird verifiziert" msgid "Backup verifiziert: %(name)s" msgstr "Backup is being verified" -#: workflows/views.py:1373 +#: workflows/views.py:1412 #, fuzzy #| msgid "Backup wird verifiziert" msgid "Das Backup wurde erfolgreich verifiziert." msgstr "Backup is being verified" -#: workflows/views.py:1378 +#: workflows/views.py:1417 #, python-format msgid "Backup wurde verifiziert: %(name)s" msgstr "" -#: workflows/views.py:1382 +#: workflows/views.py:1421 #, fuzzy #| msgid "Fehlgeschlagen" msgid "Backup-Verifikation fehlgeschlagen" msgstr "Failed" -#: workflows/views.py:1388 +#: workflows/views.py:1427 #, python-format msgid "Backup-Verifikation fehlgeschlagen: %(error)s" msgstr "" -#: workflows/views.py:1404 +#: workflows/views.py:1443 #, python-format msgid "Backup wurde gelöscht: %(name)s" msgstr "" -#: workflows/views.py:1406 +#: workflows/views.py:1445 #, python-format msgid "Backup konnte nicht gelöscht werden: %(error)s" msgstr "" -#: workflows/views.py:1432 +#: workflows/views.py:1471 #, fuzzy #| msgid "Anfrage gespeichert" msgid "Anfrage erstellt" msgstr "Request saved" -#: workflows/views.py:1434 +#: workflows/views.py:1473 #, fuzzy, python-format #| msgid "Sitzungsstatus" msgid "Status: %(status)s" msgstr "Session status" -#: workflows/views.py:1446 +#: workflows/views.py:1485 #, fuzzy #| msgid "Geplant für" msgid "Geplanter Start" msgstr "Scheduled for" -#: workflows/views.py:1456 +#: workflows/views.py:1495 msgid "Geräteübergabe / Hardware-Abholung" msgstr "" -#: workflows/views.py:1458 +#: workflows/views.py:1497 msgid "Geplanter Hardware-Termin" msgstr "" -#: workflows/views.py:1467 +#: workflows/views.py:1506 #, fuzzy #| msgid "Noch nicht verfügbar" msgid "PDF verfügbar" msgstr "Not available yet" -#: workflows/views.py:1493 +#: workflows/views.py:1532 #, fuzzy #| msgid "Einweisung" msgid "Einweisungssitzung" msgstr "Introduction" -#: workflows/views.py:1544 +#: workflows/views.py:1583 msgid "Keine Einträge ausgewählt." msgstr "No entries selected." -#: workflows/views.py:1587 +#: workflows/views.py:1626 #, python-format msgid "%(count)s Eintrag/Einträge gelöscht." msgstr "%(count)s entry/entries deleted." -#: workflows/views.py:1589 +#: workflows/views.py:1628 #, python-format msgid "%(count)s Auswahl(en) konnten nicht verarbeitet werden." msgstr "%(count)s selection(s) could not be processed." -#: workflows/views.py:1591 +#: workflows/views.py:1630 msgid "Keine passenden Einträge gefunden." msgstr "No matching entries found." -#: workflows/views.py:1819 +#: workflows/views.py:1863 msgid "Einweisungs- und Übergabeprotokoll wurde erzeugt." msgstr "Introduction and handover protocol was generated." -#: workflows/views.py:1836 +#: workflows/views.py:1880 msgid "Einweisungsprotokoll aus Live-Status wurde erzeugt." msgstr "Introduction protocol from live status was generated." -#: workflows/views.py:1865 +#: workflows/views.py:1909 msgid "Einweisung wurde zurückgesetzt." msgstr "Introduction was reset." -#: workflows/views.py:1879 +#: workflows/views.py:1923 msgid "Einweisung wurde als abgeschlossen gespeichert." msgstr "Introduction was saved as completed." -#: workflows/views.py:1892 +#: workflows/views.py:1936 msgid "Einweisung wurde als Entwurf gespeichert." msgstr "Introduction was saved as draft." -#: workflows/views.py:2677 +#: workflows/views.py:2907 #, fuzzy #| msgid "SMTP-Test starten" msgid "SMTP-Test erfolgreich" msgstr "Run SMTP test" -#: workflows/views.py:2678 +#: workflows/views.py:2908 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Die SMTP-Testmail wurde erfolgreich gesendet." msgstr "Save offboarding request" -#: workflows/views.py:2687 +#: workflows/views.py:2917 #, fuzzy #| msgid "SMTP-Test" msgid "SMTP-Test fehlgeschlagen" msgstr "SMTP test" -#: workflows/views.py:2693 +#: workflows/views.py:2923 #, fuzzy, python-format #| msgid "Passwort konnte nicht gespeichert werden" msgid "SMTP-Testmail konnte nicht gesendet werden: %(error)s" msgstr "Password could not be saved" -#: workflows/views.py:2718 +#: workflows/views.py:2948 #, fuzzy #| msgid "Nextcloud-Test starten" msgid "Nextcloud-Test erfolgreich" msgstr "Run Nextcloud test" -#: workflows/views.py:2719 +#: workflows/views.py:2949 msgid "Der Testupload nach Nextcloud war erfolgreich." msgstr "" -#: workflows/views.py:2729 workflows/views.py:2739 +#: workflows/views.py:2959 workflows/views.py:2969 #, fuzzy #| msgid "Nextcloud-Test starten" msgid "Nextcloud-Test fehlgeschlagen" msgstr "Run Nextcloud test" -#: workflows/views.py:2730 +#: workflows/views.py:2960 msgid "Der Testupload nach Nextcloud ist fehlgeschlagen." msgstr "" +#~ msgid "Felder per Drag-and-Drop sortieren und pro Schritt gruppieren." +#~ msgstr "Sort fields by drag and drop and group them by step." + #~ msgid "Direkte Aktionen für Ihr Workdock-Konto." #~ msgstr "Direct actions for your Workdock account." diff --git a/backend/workflows/form_builder.py b/backend/workflows/form_builder.py index d7a03cf..a366a38 100644 --- a/backend/workflows/form_builder.py +++ b/backend/workflows/form_builder.py @@ -1,7 +1,7 @@ from collections import OrderedDict from django.utils.translation import get_language -from .models import FormFieldConfig +from .models import FormFieldConfig, FormSectionConfig DEFAULT_FIELD_ORDER = { @@ -58,18 +58,29 @@ DEFAULT_FIELD_ORDER = { } ONBOARDING_PAGE_ORDER = ['stammdaten', 'vertrag', 'itsetup', 'abschluss'] +OFFBOARDING_PAGE_ORDER = ['mitarbeitende', 'austritt', 'abschluss'] ONBOARDING_PAGE_LABELS = { 'stammdaten': '1. Stammdaten', 'vertrag': '2. Vertrag', 'itsetup': '3. IT-Setup', 'abschluss': '4. Abschluss', } +OFFBOARDING_PAGE_LABELS = { + 'mitarbeitende': '1. Mitarbeitende', + 'austritt': '2. Austritt', + 'abschluss': '3. Abschluss', +} LOCKED_FIELD_RULES = { 'onboarding': {'full_name', 'work_email', 'contract_start', 'agreement_confirm'}, 'offboarding': {'full_name', 'work_email', 'last_working_day'}, } +LOCKED_SECTION_RULES = { + 'onboarding': {'stammdaten', 'vertrag', 'abschluss'}, + 'offboarding': {'mitarbeitende', 'austritt'}, +} + ONBOARDING_DEFAULT_PAGE = { 'first_name': 'stammdaten', 'last_name': 'stammdaten', @@ -112,6 +123,38 @@ ONBOARDING_DEFAULT_PAGE = { 'onboarded_by_email': 'abschluss', 'agreement_confirm': 'abschluss', } +OFFBOARDING_DEFAULT_PAGE = { + 'full_name': 'mitarbeitende', + 'work_email': 'mitarbeitende', + 'department': 'mitarbeitende', + 'job_title': 'mitarbeitende', + 'last_working_day': 'austritt', + 'notes': 'abschluss', +} + + +def get_section_order(form_type: str) -> list[str]: + if form_type == 'onboarding': + return ONBOARDING_PAGE_ORDER + if form_type == 'offboarding': + return OFFBOARDING_PAGE_ORDER + return [] + + +def get_section_labels(form_type: str) -> dict[str, str]: + if form_type == 'onboarding': + return ONBOARDING_PAGE_LABELS + if form_type == 'offboarding': + return OFFBOARDING_PAGE_LABELS + return {} + + +def get_default_page_map(form_type: str) -> dict[str, str]: + if form_type == 'onboarding': + return ONBOARDING_DEFAULT_PAGE + if form_type == 'offboarding': + return OFFBOARDING_DEFAULT_PAGE + return {} def _default_sort(form_type: str, field_name: str) -> int: @@ -134,7 +177,7 @@ def _ensure_configs(form_type: str, field_names: list[str]) -> dict[str, FormFie form_type=form_type, field_name=name, sort_order=_default_sort(form_type, name), - page_key=ONBOARDING_DEFAULT_PAGE.get(name, '') if form_type == 'onboarding' else '', + page_key=get_default_page_map(form_type).get(name, ''), ) for name in missing_names ], @@ -151,10 +194,34 @@ def ensure_form_field_configs(form_type: str, field_names: list[str]) -> dict[st return _ensure_configs(form_type, field_names) +def ensure_form_section_configs(form_type: str) -> dict[str, FormSectionConfig]: + section_order = get_section_order(form_type) + if not section_order: + return {} + existing = { + cfg.section_key: cfg + for cfg in FormSectionConfig.objects.filter(form_type=form_type) + } + missing = [key for key in section_order if key not in existing] + if missing: + FormSectionConfig.objects.bulk_create( + [FormSectionConfig(form_type=form_type, section_key=key, is_visible=True) for key in missing], + ignore_conflicts=True, + ) + existing = { + cfg.section_key: cfg + for cfg in FormSectionConfig.objects.filter(form_type=form_type) + } + return existing + + def apply_form_field_config(form_type: str, form) -> None: field_names = list(form.fields.keys()) configs = _ensure_configs(form_type, field_names) + section_configs = ensure_form_section_configs(form_type) locked = LOCKED_FIELD_RULES.get(form_type, set()) + locked_sections = LOCKED_SECTION_RULES.get(form_type, set()) + default_page_map = get_default_page_map(form_type) language_code = get_language() for field_name, field in list(form.fields.items()): @@ -173,7 +240,14 @@ def apply_form_field_config(form_type: str, form) -> None: if field_name not in locked and cfg.is_required is not None: field.required = cfg.is_required - if field_name not in locked and not cfg.is_visible: + section_key = cfg.page_key or default_page_map.get(field_name, '') + section_hidden = ( + form_type in {'onboarding', 'offboarding'} + and section_key not in locked_sections + and section_key in section_configs + and not section_configs[section_key].is_visible + ) + if field_name not in locked and (not cfg.is_visible or section_hidden): form.fields.pop(field_name, None) ordered_items = sorted( @@ -184,9 +258,138 @@ def apply_form_field_config(form_type: str, form) -> None: ), ) form.fields = OrderedDict(ordered_items) - if form_type == 'onboarding': + if form_type in {'onboarding', 'offboarding'}: form._field_page_keys = { - name: (configs[name].page_key or ONBOARDING_DEFAULT_PAGE.get(name, 'abschluss')) + name: (configs[name].page_key or default_page_map.get(name, '')) for name in form.fields.keys() if name in configs } + + +FORM_PRESETS = { + 'onboarding': { + 'standard': { + 'label': 'Standard', + 'sections': { + 'stammdaten': True, + 'vertrag': True, + 'itsetup': True, + 'abschluss': True, + }, + 'fields': {}, + }, + 'lean': { + 'label': 'Lean', + 'sections': { + 'stammdaten': True, + 'vertrag': True, + 'itsetup': False, + 'abschluss': True, + }, + 'fields': { + 'gender': {'is_visible': False}, + 'order_business_cards': {'is_visible': False}, + 'business_card_name': {'is_visible': False}, + 'business_card_title': {'is_visible': False}, + 'business_card_email': {'is_visible': False}, + 'business_card_phone': {'is_visible': False}, + 'employment_end_date': {'is_visible': False}, + 'group_mailboxes_required_choice': {'is_visible': False}, + 'group_mailboxes': {'is_visible': False}, + 'additional_notes': {'is_required': False}, + }, + }, + 'it_heavy': { + 'label': 'IT-heavy', + 'sections': { + 'stammdaten': True, + 'vertrag': True, + 'itsetup': True, + 'abschluss': True, + }, + 'fields': { + 'needed_devices_multi': {'is_required': True}, + 'needed_software_multi': {'is_required': True}, + 'needed_accesses_multi': {'is_required': True}, + 'needed_workspace_groups_multi': {'is_required': True}, + 'needed_resources_multi': {'is_required': True}, + 'additional_hardware_needed_choice': {'is_visible': True}, + 'additional_software_needed_choice': {'is_visible': True}, + 'additional_access_needed_choice': {'is_visible': True}, + 'successor_required_choice': {'is_visible': True}, + }, + }, + }, + 'offboarding': { + 'standard': { + 'label': 'Standard', + 'sections': { + 'mitarbeitende': True, + 'austritt': True, + 'abschluss': True, + }, + 'fields': {}, + }, + 'lean': { + 'label': 'Lean', + 'sections': { + 'mitarbeitende': True, + 'austritt': True, + 'abschluss': False, + }, + 'fields': { + 'department': {'is_visible': False}, + 'job_title': {'is_visible': False}, + 'notes': {'is_visible': False}, + }, + }, + 'hr_heavy': { + 'label': 'HR-heavy', + 'sections': { + 'mitarbeitende': True, + 'austritt': True, + 'abschluss': True, + }, + 'fields': { + 'department': {'is_visible': True, 'is_required': True}, + 'job_title': {'is_visible': True, 'is_required': True}, + 'notes': {'is_visible': True, 'is_required': True}, + }, + }, + }, +} + + +def apply_form_preset(form_type: str, preset_key: str) -> bool: + preset = FORM_PRESETS.get(form_type, {}).get(preset_key) + if not preset: + return False + + locked_fields = LOCKED_FIELD_RULES.get(form_type, set()) + locked_sections = LOCKED_SECTION_RULES.get(form_type, set()) + default_names = list(DEFAULT_FIELD_ORDER.get(form_type, [])) + ensure_form_field_configs(form_type, default_names) + section_configs = ensure_form_section_configs(form_type) + + for section_key, is_visible in preset.get('sections', {}).items(): + cfg = section_configs.get(section_key) + if not cfg or section_key in locked_sections: + continue + cfg.is_visible = bool(is_visible) + cfg.save(update_fields=['is_visible']) + + for cfg in FormFieldConfig.objects.filter(form_type=form_type): + if cfg.field_name in locked_fields: + cfg.is_visible = True + cfg.is_required = None + else: + cfg.is_visible = True + cfg.is_required = None + override = preset.get('fields', {}).get(cfg.field_name, {}) + if 'is_visible' in override: + cfg.is_visible = bool(override['is_visible']) + if 'is_required' in override: + cfg.is_required = override['is_required'] + cfg.save(update_fields=['is_visible', 'is_required']) + + return True diff --git a/backend/workflows/migrations/0053_formsectionconfig.py b/backend/workflows/migrations/0053_formsectionconfig.py new file mode 100644 index 0000000..0a4c6ce --- /dev/null +++ b/backend/workflows/migrations/0053_formsectionconfig.py @@ -0,0 +1,37 @@ +from django.db import migrations, models + + +def seed_onboarding_sections(apps, schema_editor): + FormSectionConfig = apps.get_model('workflows', 'FormSectionConfig') + for section_key in ['stammdaten', 'vertrag', 'itsetup', 'abschluss']: + FormSectionConfig.objects.get_or_create( + form_type='onboarding', + section_key=section_key, + defaults={'is_visible': True}, + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ('workflows', '0052_userprofile_notification_preferences'), + ] + + operations = [ + migrations.CreateModel( + name='FormSectionConfig', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('form_type', models.CharField(choices=[('onboarding', 'Onboarding')], max_length=20)), + ('section_key', models.CharField(choices=[('stammdaten', 'Stammdaten'), ('vertrag', 'Vertrag'), ('itsetup', 'IT-Setup'), ('abschluss', 'Abschluss')], max_length=20)), + ('is_visible', models.BooleanField(default=True)), + ], + options={ + 'verbose_name': 'Formularabschnitt-Konfiguration', + 'verbose_name_plural': 'Formularabschnitt-Konfigurationen', + 'ordering': ['form_type', 'section_key'], + 'unique_together': {('form_type', 'section_key')}, + }, + ), + migrations.RunPython(seed_onboarding_sections, migrations.RunPython.noop), + ] diff --git a/backend/workflows/models.py b/backend/workflows/models.py index 963315f..96c18a9 100644 --- a/backend/workflows/models.py +++ b/backend/workflows/models.py @@ -516,6 +516,31 @@ class FormFieldConfig(models.Model): return self.help_text_override.strip() +class FormSectionConfig(models.Model): + FORM_CHOICES = [ + ('onboarding', _('Onboarding')), + ] + SECTION_CHOICES = [ + ('stammdaten', _('Stammdaten')), + ('vertrag', _('Vertrag')), + ('itsetup', _('IT-Setup')), + ('abschluss', _('Abschluss')), + ] + + form_type = models.CharField(max_length=20, choices=FORM_CHOICES) + section_key = models.CharField(max_length=20, choices=SECTION_CHOICES) + is_visible = models.BooleanField(default=True) + + class Meta: + ordering = ['form_type', 'section_key'] + unique_together = ('form_type', 'section_key') + verbose_name = 'Formularabschnitt-Konfiguration' + verbose_name_plural = 'Formularabschnitt-Konfigurationen' + + def __str__(self) -> str: + return f'{self.form_type}: {self.section_key}' + + class NotificationTemplate(models.Model): TEMPLATE_CHOICES = [ ('onboarding_it', _('Onboarding: IT')), diff --git a/backend/workflows/static/workflows/css/form_builder.css b/backend/workflows/static/workflows/css/form_builder.css index 8de88a6..fce3a2e 100644 --- a/backend/workflows/static/workflows/css/form_builder.css +++ b/backend/workflows/static/workflows/css/form_builder.css @@ -1,72 +1,98 @@ body { margin: 0; - font-family: Arial, sans-serif; - background: #f4f7fb; - color: #1f2937; + font-family: "IBM Plex Sans", "Segoe UI", sans-serif; + background: + radial-gradient(circle at top right, rgba(0, 120, 255, 0.08), transparent 28%), + linear-gradient(180deg, #eef4fb 0%, #f7f9fc 100%); + color: #142033; } .shell { - width: min(1280px, 94%); - margin: 20px auto 28px; - background: #ffffff; - border: 1px solid #d8e2f0; - border-radius: 14px; - padding: 18px; - box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08); + width: min(1320px, 94%); + margin: 20px auto 32px; + background: rgba(255, 255, 255, 0.92); + border: 1px solid rgba(191, 204, 222, 0.8); + border-radius: 20px; + padding: 20px; + box-shadow: 0 22px 60px rgba(15, 23, 42, 0.08); + backdrop-filter: blur(12px); } -.topbar { +.builder-hero, +.builder-panel, +.builder-stat-card, +.section-rule-card, +.field-card, +.options-panel { + animation: builderFadeIn 0.32s ease; +} + +.builder-hero { display: flex; - align-items: center; + align-items: flex-end; justify-content: space-between; - gap: 12px; - margin-bottom: 10px; + gap: 20px; + padding: 8px 0 6px; } -.brand-logo { - width: 190px; - max-width: 100%; - height: auto; - display: block; +.builder-hero-copy { + max-width: 760px; } -.header h1 { - margin: 0; - font-size: 28px; -} - -.header p { - margin: 6px 0 0; - color: #64748b; -} - -.toolbar { - margin-top: 14px; - display: flex; +.builder-eyebrow { + display: inline-flex; align-items: center; gap: 8px; + margin-bottom: 10px; + padding: 6px 11px; + border-radius: 999px; + background: #e9f2ff; + color: #174ea6; + font-size: 12px; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.builder-hero h1 { + margin: 0; + font-size: clamp(30px, 4vw, 40px); + line-height: 1.02; +} + +.builder-hero-actions { + display: flex; + align-items: center; + gap: 10px; flex-wrap: wrap; + justify-content: flex-end; } .tab { - border: 1px solid #cbd5e1; + border: 1px solid #c6d1e1; border-radius: 999px; - padding: 8px 14px; + padding: 9px 15px; text-decoration: none; - color: #1f2937; - background: #f8fafc; - font-weight: 600; + color: #1c2a41; + background: #f8fbff; + font-weight: 700; + transition: transform 0.18s ease, border-color 0.18s ease, background 0.18s ease; +} + +.tab:hover { + transform: translateY(-1px); + border-color: #9db4d2; } .tab.active { - background: #000078; + background: linear-gradient(135deg, #0f3b7a 0%, #1759b8 100%); color: #ffffff; - border-color: #000078; + border-color: #1759b8; } .status { min-height: 22px; - margin: 10px 0 8px; + margin: 14px 0 10px; color: #334155; font-size: 14px; } @@ -83,11 +109,401 @@ body { color: #166534; } +.builder-overview { + margin-top: 12px; + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; +} + +.builder-stat-card, +.builder-panel, +.options-panel { + border: 1px solid rgba(201, 212, 226, 0.95); + border-radius: 18px; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(247, 250, 255, 0.98)); + box-shadow: 0 10px 24px rgba(15, 23, 42, 0.05); +} + +.builder-stat-card { + padding: 14px 16px; + transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease; +} + +.builder-stat-card:hover { + transform: translateY(-2px); + border-color: rgba(146, 170, 199, 0.95); + box-shadow: 0 16px 28px rgba(15, 23, 42, 0.08); +} + +.builder-stat-label { + display: block; + color: #65758f; + font-size: 12px; + font-weight: 700; + letter-spacing: 0.03em; + text-transform: uppercase; +} + +.builder-stat-card strong { + display: block; + margin-top: 6px; + font-size: 28px; + line-height: 1; + color: #101c30; +} + +.builder-panel-head h2, +.options-head h2 { + margin: 0; + color: #142033; +} + +.mini { + color: #61718a; + font-size: 13px; + line-height: 1.55; +} + +.builder-quicknav { + margin-top: 12px; + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.builder-quicknav a { + display: inline-flex; + align-items: center; + min-height: 34px; + padding: 0 12px; + border: 1px solid #d1dbea; + border-radius: 999px; + background: #f7fbff; + color: #304159; + font-size: 13px; + font-weight: 700; + text-decoration: none; + transition: transform 0.18s ease, border-color 0.18s ease, background-color 0.18s ease, box-shadow 0.18s ease; +} + +.builder-quicknav a:hover { + transform: translateY(-1px); + border-color: #adc2dd; + background: #ffffff; + box-shadow: 0 10px 18px rgba(15, 23, 42, 0.06); +} + +.builder-preset-bar { + margin-top: 12px; + display: flex; + justify-content: flex-end; +} + +.builder-preset-form { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; +} + +.builder-preset-label { + color: #61718a; + font-size: 12px; + font-weight: 800; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.builder-preset-form select { + min-height: 36px; + padding: 0 12px; + border: 1px solid #cfdbeb; + border-radius: 999px; + background: linear-gradient(180deg, #ffffff, #f6faff); + color: #24405f; + font-size: 13px; + font-weight: 700; +} + +.builder-panel { + margin-top: 14px; + padding: 16px; +} + +.builder-panel-head, +.options-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 14px; + margin-bottom: 12px; +} + +.options-head-inline { + margin-bottom: 14px; +} + +.builder-accordion { + padding: 0; + overflow: hidden; +} + +.nested-accordion { + padding: 0; + overflow: hidden; +} + +.nested-accordion-summary { + list-style: none; + cursor: pointer; + padding: 14px 16px; + transition: background-color 0.18s ease; +} + +.nested-accordion-summary::-webkit-details-marker { + display: none; +} + +.nested-accordion-summary .options-head { + margin-bottom: 0; +} + +.nested-accordion-summary:hover { + background: rgba(242, 247, 255, 0.72); +} + +.nested-accordion[open] .nested-accordion-summary { + border-bottom: 1px solid rgba(201, 212, 226, 0.8); +} + +.nested-accordion:not([open]) .builder-panel-toggle::after { + transform: rotate(-90deg); +} + +.nested-accordion[open] .builder-panel-toggle { + color: #194ea7; +} + +.nested-accordion-body { + padding: 14px 16px 16px; +} + +.nested-accordion[open] .nested-accordion-body { + animation: builderReveal 0.24s ease; +} + +.builder-panel-summary { + list-style: none; + cursor: pointer; + padding: 14px 16px; + transition: background-color 0.18s ease; +} + +.builder-panel-summary::-webkit-details-marker { + display: none; +} + +.builder-panel-summary .builder-panel-head { + margin-bottom: 0; +} + +.builder-panel-summary:hover { + background: rgba(242, 247, 255, 0.72); +} + +.builder-panel-toggle { + display: inline-flex; + align-items: center; + gap: 8px; + color: #5f7089; + font-size: 12px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.builder-panel-toggle::after { + content: "▾"; + font-size: 13px; + line-height: 1; + transition: transform 0.18s ease; +} + +.builder-accordion:not([open]) .builder-panel-toggle::after { + transform: rotate(-90deg); +} + +.builder-accordion[open] .builder-panel-toggle { + color: #194ea7; +} + +.builder-accordion[open] .builder-panel-toggle::before { + content: ""; +} + +.builder-accordion[open] .builder-panel-summary { + border-bottom: 1px solid rgba(201, 212, 226, 0.8); +} + +.builder-panel-body { + padding: 14px 16px 16px; +} + +.builder-accordion[open] .builder-panel-body { + animation: builderReveal 0.24s ease; +} + +.builder-rule-layout { + display: grid; + grid-template-columns: minmax(300px, 0.85fr) minmax(0, 1.15fr); + gap: 14px; +} + +.builder-stack-layout { + display: grid; + gap: 14px; +} + +.preview-shell { + display: grid; + gap: 12px; +} + +.preview-section { + border: 1px solid #d7e0ec; + border-radius: 14px; + background: linear-gradient(180deg, #f8fbff 0%, #ffffff 100%); + overflow: hidden; +} + +.preview-section-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding: 12px 14px; + border-bottom: 1px solid #dfe7f1; + background: #f2f7ff; +} + +.preview-section-head h3 { + margin: 0; + font-size: 15px; + color: #142033; +} + +.preview-chip-list { + display: flex; + flex-wrap: wrap; + gap: 8px; + padding: 14px; +} + +.preview-chip { + display: inline-flex; + align-items: center; + min-height: 32px; + padding: 0 10px; + border: 1px solid #d8e1ec; + border-radius: 999px; + background: #ffffff; + color: #304159; + font-size: 13px; + font-weight: 700; +} + +.preview-chip.is-locked { + background: #eef2ff; + border-color: #c7d2fe; + color: #3730a3; +} + +.field-rule-groups { + display: grid; + gap: 12px; +} + +.field-rule-group { + border: 1px solid #d7e0ec; + border-radius: 14px; + background: linear-gradient(180deg, #f8fbff 0%, #ffffff 100%); + overflow: hidden; + transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease; +} + +.field-rule-group:hover { + transform: translateY(-1px); + border-color: #bfd0e4; + box-shadow: 0 14px 26px rgba(15, 23, 42, 0.06); +} + +.field-rule-group-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding: 12px 14px; + border-bottom: 1px solid #dfe7f1; + background: #f2f7ff; +} + +.field-rule-group-head h3 { + margin: 0; + font-size: 15px; + color: #142033; +} + +.field-rule-list { + display: grid; +} + +.field-rule-row { + display: grid; + grid-template-columns: minmax(220px, 1.4fr) 120px 160px 120px; + gap: 12px; + align-items: center; + padding: 12px 14px; + border-top: 1px solid #edf2f7; + transition: background-color 0.18s ease; +} + +.field-rule-row:first-child { + border-top: 0; +} + +.field-rule-row:hover { + background: rgba(246, 250, 255, 0.92); +} + +.field-rule-main strong { + display: block; + color: #162133; +} + +.field-rule-control { + display: grid; + gap: 6px; + color: #5f7089; + font-size: 12px; + font-weight: 700; +} + +.field-rule-control input[type='checkbox'] { + width: 16px; + height: 16px; +} + +.field-rule-status { + display: flex; + justify-content: flex-end; +} + .columns { - margin-top: 8px; display: grid; grid-template-columns: repeat(4, minmax(220px, 1fr)); - gap: 10px; + gap: 12px; } .columns.single { @@ -95,28 +511,47 @@ body { } .column { - border: 1px solid #d4dce7; - border-radius: 12px; - background: #f9fbff; + border: 1px solid #d7e0ec; + border-radius: 16px; + background: linear-gradient(180deg, #f7faff 0%, #fdfefe 100%); display: flex; flex-direction: column; min-height: 460px; + overflow: hidden; + transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease; } -.column h2 { +.column:hover { + transform: translateY(-2px); + border-color: #bfd0e4; + box-shadow: 0 16px 28px rgba(15, 23, 42, 0.07); +} + +.column-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding: 13px 14px; + border-bottom: 1px solid #deE7f1; + background: linear-gradient(180deg, #eef5ff, #f7fbff); +} + +.column-head h3 { margin: 0; - padding: 10px 12px; - border-bottom: 1px solid #dbe3ee; font-size: 16px; - color: #0f172a; - background: #edf3fb; - border-radius: 12px 12px 0 0; +} + +.column-count { + color: #5c6d87; + font-size: 12px; + font-weight: 700; } .dropzone { - padding: 10px; + padding: 12px; display: grid; - gap: 8px; + gap: 10px; align-content: start; min-height: 140px; flex: 1; @@ -127,14 +562,22 @@ body { } .field-card { - background: #ffffff; - border: 1px solid #d3dbe8; - border-radius: 10px; - padding: 9px 10px; + background: rgba(255, 255, 255, 0.96); + border: 1px solid #d7dfeb; + border-radius: 14px; + padding: 11px 12px; display: flex; justify-content: space-between; - gap: 8px; + align-items: flex-start; + gap: 10px; cursor: move; + transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease; +} + +.field-card:hover { + transform: translateY(-1px); + border-color: #b9cadf; + box-shadow: 0 12px 20px rgba(15, 23, 42, 0.06); } .field-card.dragging { @@ -145,13 +588,20 @@ body { font-weight: 700; font-size: 14px; color: #0f172a; + overflow-wrap: anywhere; } .field-name { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; color: #64748b; - margin-top: 2px; + margin-top: 3px; + overflow-wrap: anywhere; +} + +.field-main { + min-width: 0; + flex: 1 1 auto; } .badges { @@ -159,13 +609,15 @@ body { gap: 6px; align-items: center; flex-wrap: wrap; + justify-content: flex-end; + flex: 0 0 auto; } .badge { font-size: 11px; border-radius: 999px; border: 1px solid #d1d5db; - padding: 2px 7px; + padding: 3px 8px; background: #f8fafc; color: #334155; } @@ -189,24 +641,7 @@ body { } .options-panel { - margin-top: 16px; - border: 1px solid #d4dce7; - border-radius: 12px; - background: #ffffff; - padding: 12px; -} - -.options-head { - display: flex; - align-items: center; - justify-content: space-between; - gap: 10px; - margin-bottom: 10px; -} - -.options-head h2 { - margin: 0; - font-size: 18px; + padding: 14px; } .category-switch { @@ -215,23 +650,22 @@ body { gap: 8px; } -.category-switch select { +.category-switch select, +.option-table select, +.add-option-form input, +.option-table input[type='text'] { border: 1px solid #cbd5e1; - border-radius: 8px; - padding: 6px 8px; + border-radius: 10px; + padding: 8px 10px; + box-sizing: border-box; + background: #fff; } .add-option-form { display: grid; grid-template-columns: minmax(180px, 1fr) minmax(180px, 1fr) minmax(180px, 1fr) auto; gap: 8px; - margin-bottom: 10px; -} - -.add-option-form input { - border: 1px solid #cbd5e1; - border-radius: 8px; - padding: 8px 9px; + margin-bottom: 12px; } .option-table-wrap { @@ -246,21 +680,22 @@ body { .option-table th, .option-table td { border: 1px solid #e2e8f0; - padding: 7px 8px; + padding: 8px 9px; text-align: left; vertical-align: top; } .option-table th { - background: #f8fafc; + background: #f8fbff; + color: #3d4c63; } -.option-table input[type='text'] { - width: 100%; - border: 1px solid #cbd5e1; - border-radius: 7px; - padding: 6px 8px; - box-sizing: border-box; +.option-table-group-row th { + background: #eef5ff; + color: #17335e; + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.04em; } .option-row { @@ -279,7 +714,7 @@ body { width: 28px; height: 28px; border: 1px solid #cbd5e1; - border-radius: 6px; + border-radius: 8px; background: #f8fafc; color: #475569; font-size: 14px; @@ -288,28 +723,146 @@ body { } .options-actions { - margin-top: 10px; + margin-top: 12px; display: flex; justify-content: flex-end; } -@media (max-width: 1120px) { +.section-rule-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 10px; +} + +.section-rule-card { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 13px 14px; + border: 1px solid #d6e0ec; + border-radius: 14px; + background: linear-gradient(180deg, #f9fbff, #ffffff); +} + +.section-rule-card.is-locked { + background: linear-gradient(180deg, #f4f7fb, #fafcff); +} + +.section-rule-copy { + display: grid; + gap: 4px; +} + +.section-rule-copy strong { + color: #0f172a; + font-size: 14px; +} + +.section-rule-copy span, +.mini { + color: #64748b; + font-size: 12px; + line-height: 1.45; +} + +.section-rule-toggle { + display: inline-flex; + align-items: center; + gap: 8px; +} + +@keyframes builderFadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes builderReveal { + from { + opacity: 0; + transform: translateY(-4px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@media (prefers-reduced-motion: reduce) { + .builder-hero, + .builder-panel, + .builder-stat-card, + .section-rule-card, + .field-card, + .options-panel { + animation: none; + } + + .field-card, + .tab, + .builder-stat-card, + .builder-quicknav a, + .builder-panel-summary, + .field-rule-group, + .field-rule-row, + .column { + transition: none; + } + + .builder-accordion[open] .builder-panel-body { + animation: none; + } +} + +@media (max-width: 1220px) { + .builder-overview { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .builder-rule-layout, .columns { grid-template-columns: repeat(2, minmax(220px, 1fr)); } } +@media (max-width: 900px) { + .builder-hero, + .builder-panel-head, + .options-head { + flex-direction: column; + align-items: flex-start; + } + + .builder-hero-actions { + justify-content: flex-start; + } + + .builder-rule-layout { + grid-template-columns: 1fr; + } +} + @media (max-width: 760px) { + .builder-overview, .columns { grid-template-columns: 1fr; } + .field-rule-row { + grid-template-columns: 1fr; + } + + .field-rule-status { + justify-content: flex-start; + } + .add-option-form { grid-template-columns: 1fr; } - - .options-head { - flex-direction: column; - align-items: flex-start; - } } diff --git a/backend/workflows/static/workflows/css/offboarding_form.css b/backend/workflows/static/workflows/css/offboarding_form.css index 34a1a12..3c2a507 100644 --- a/backend/workflows/static/workflows/css/offboarding_form.css +++ b/backend/workflows/static/workflows/css/offboarding_form.css @@ -14,6 +14,7 @@ body { .card { background: linear-gradient(180deg, #ffffff, #fbfcff); border: 1px solid #d9dcf3; border-radius: 14px; padding: 18px; margin-bottom: 14px; box-shadow: 0 10px 24px rgba(0, 0, 120, 0.08); } .wrap-body .card:last-child { margin-bottom: 0; } h1 { margin-top: 0; color: #000078; } +h2 { margin: 0; color: #17335e; font-size: 18px; } .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } .field { margin-bottom: 12px; } .field-full { grid-column: 1 / -1; } @@ -21,6 +22,11 @@ label { display: block; font-weight: 600; margin-bottom: 6px; } input, textarea { width: 100%; min-height: 44px; padding: 9px 11px; box-sizing: border-box; border: 1px solid #d4dbf7; border-radius: 10px; background: #fff; } textarea { min-height: 120px; resize: vertical; } .hint { color: #64748b; font-size: 12px; margin-top: 4px; } +.offboarding-sections { display: grid; gap: 14px; margin-bottom: 14px; } +.offboarding-section-card { border: 1px solid #dbe5f1; border-radius: 14px; background: linear-gradient(180deg, #f9fbff, #ffffff); overflow: hidden; box-shadow: 0 8px 20px rgba(15, 23, 42, 0.05); } +.offboarding-section-head { padding: 14px 16px; border-bottom: 1px solid #e2e8f4; background: #f2f7ff; } +.offboarding-section-head p { margin: 4px 0 0; color: #5f7089; font-size: 13px; } +.offboarding-section-card .grid { padding: 16px; } .results a { display: inline-block; margin: 4px 8px 4px 0; padding: 6px 8px; border: 1px solid #d4dbf7; border-radius: 6px; text-decoration: none; color: #000078; background: #f7f8ff; } .errorlist { color: #b91c1c; margin: 4px 0; } .popup-backdrop { position: fixed; inset: 0; background: rgba(15, 23, 42, 0.38); display: none; align-items: center; justify-content: center; z-index: 1000; } diff --git a/backend/workflows/templates/workflows/form_builder.html b/backend/workflows/templates/workflows/form_builder.html index 58697b3..2e1cef1 100644 --- a/backend/workflows/templates/workflows/form_builder.html +++ b/backend/workflows/templates/workflows/form_builder.html @@ -10,153 +10,399 @@ {% block shell_body %} {% include 'workflows/includes/app_header.html' with header_show_home=1 header_inside_shell=1 %} -
-

{% trans "Form Builder" %}

-

{% trans "Felder per Drag-and-Drop sortieren und pro Schritt gruppieren." %}

-
+
+
+ {% trans "Deployment Configuration" %} +

{% trans "Form Builder" %}

+
+
+ {% for key, label in form_types %} + + {{ label }} + + {% endfor %} + +
+
{% include 'workflows/includes/messages.html' %} -
- {% for key, label in form_types %} - - {{ label }} - - {% endfor %} - -
-
-
- {% for column in columns %} -
-

{{ column.title }}

-
- {% for item in column.items %} -
-
-
{{ item.label }}
-
{{ item.field_name }}
+ + +
+
+ {% trans "Fixe Kernfelder" %} + {{ builder_summary.locked_field_count }} +
+
+ {% trans "Konfigurierbar" %} + {{ builder_summary.configurable_field_count }} +
+
+ {% trans "Aktuell ausgeblendet" %} + {{ builder_summary.hidden_field_count }} +
+ {% if form_type == 'onboarding' %} +
+ {% trans "Versteckte Abschnitte" %} + {{ builder_summary.hidden_section_count }} +
+ {% endif %} +
+ +
+
+ {% csrf_token %} + + + + +
+
+ +
+ +
+
+

{% trans "Live-Vorschau" %}

+
+ {% trans "Öffnen" %} +
+
+
+
+ {% for section in preview_sections %} +
+
+

{{ section.title }}

+ {% blocktrans trimmed with count=section.items|length %}{{ count }} Feld/Felder{% endblocktrans %}
-
- {% if item.locked %}{% trans "Fix" %}{% endif %} - {% if not item.is_visible %}{% endif %} - {% if item.is_required %}{% trans "Pflicht" %}{% endif %} +
+ {% for item in section.items %} + {{ item.label }} + {% empty %} + {% trans "Keine sichtbaren Felder." %} + {% endfor %}
-
+
{% endfor %}
- - {% endfor %} - + + -
-
-

{% trans "Optionen verwalten" %}

-
- - - -
+
+
+ {% endfor %} - -
- {% csrf_token %} - - - - - - -
- -
- {% csrf_token %} -
- - - - - - - - - - - - - {% for item in option_items %} - - - - - - - - - {% empty %} - - {% endfor %} - -
{% trans "Sortierung" %}{% trans "Label (DE)" %}{% trans "Label (EN)" %}Value{% trans "Aktiv" %}{% trans "Löschen" %}
- - ⋮⋮ - - -
{% trans "Keine Optionen in dieser Kategorie." %}
-
-
- -
-
- - -
-
-

{% trans "Feldtexte verwalten" %}

-
- {% csrf_token %} -
- - - - - - - - - - - - {% for item in field_text_items %} - - - - - - - - {% empty %} - + + +
+ +
+
+

{% trans "Sichtbarkeit & Regeln" %}

+
+ {% trans "Öffnen" %} +
+
+
+ +
+
+
+

{% trans "Abschnitte steuern" %}

+
+ + {% csrf_token %} +
+ {% for section in section_rule_items %} + {% endfor %} -
-
{% trans "Feld" %}{% trans "Label (DE)" %}{% trans "Label (EN)" %}{% trans "Hilfetext (DE)" %}{% trans "Hilfetext (EN)" %}
- - {{ item.field_name }} -
{% trans "Keine Feldkonfigurationen verfügbar." %}
+
+
+ +
+
+
+ +
+
+

{% trans "Feldregeln verwalten" %}

+
+
+ {% csrf_token %} +
+ {% for group in field_rule_groups %} +
+
+

{{ group.title }}

+ {% blocktrans trimmed with count=group.items|length %}{{ count }} Feld/Felder{% endblocktrans %} +
+
+ {% for item in group.items %} +
+
+ + {{ item.label }} +
{{ item.field_name }}
+
+ + +
+ {% if item.locked %} + {% trans "Fix" %} + {% elif not item.is_visible %} + + {% elif item.is_required %} + {% trans "Pflicht" %} + {% else %} + {% trans "Flexibel" %} + {% endif %} +
+
+ {% empty %} +
{% trans "Keine Feldregeln verfügbar." %}
+ {% endfor %} +
+
+ {% endfor %} +
+
+ +
+
+
+ + + + +
+ +
+
+

{% trans "Optionen & Texte" %}

+
+ {% trans "Öffnen" %}
-
- -
- - +
+
+ +
+
+ +
+

{% trans "Optionen verwalten" %}

+ {% trans "Öffnen" %} +
+
+
+
+
+ + + + + + +
+
+ +
+ {% csrf_token %} + + + + + + +
+ +
+ {% csrf_token %} +
+ + + + + + + + + + + + + {% for item in option_items %} + + + + + + + + + {% empty %} + + {% endfor %} + +
{% trans "Sortierung" %}{% trans "Label (DE)" %}{% trans "Label (EN)" %}Value{% trans "Aktiv" %}{% trans "Löschen" %}
+ + ⋮⋮ + + +
{% trans "Keine Optionen in dieser Kategorie." %}
+
+
+ +
+
+
+
+ +
+ +
+

{% trans "Feldtexte verwalten" %}

+ {% trans "Öffnen" %} +
+
+
+
+ {% csrf_token %} +
+ + + + + + + + + + + + {% for group in field_text_groups %} + + + + {% for item in group.items %} + + + + + + + + {% empty %} + + {% endfor %} + {% endfor %} + +
{% trans "Feld" %}{% trans "Label (DE)" %}{% trans "Label (EN)" %}{% trans "Hilfetext (DE)" %}{% trans "Hilfetext (EN)" %}
{{ group.title }}
+ + {{ item.field_name }} +
{% trans "Keine Feldkonfigurationen verfügbar." %}
+
+
+ +
+
+
+
+
+
+
+ + {% endblock %} diff --git a/backend/workflows/templates/workflows/offboarding_form.html b/backend/workflows/templates/workflows/offboarding_form.html index 9846e9c..489ea0d 100644 --- a/backend/workflows/templates/workflows/offboarding_form.html +++ b/backend/workflows/templates/workflows/offboarding_form.html @@ -49,19 +49,29 @@
{% csrf_token %} -
- {% for field in form.visible_fields %} - {% if field.name != 'search_query' %} +
+ {% for section in offboarding_sections %} +
+
+
+

{{ section.title }}

+

{{ section.subtitle }}

+
+
+
+ {% for field in section.fields %}
{{ field.label_tag }} {{ field }} {% if field.help_text %}
{{ field.help_text }}
{% endif %} {{ field.errors }}
- {% endif %} + {% endfor %} +
+
{% endfor %}
- +
diff --git a/backend/workflows/tests/test_form_builder_admin.py b/backend/workflows/tests/test_form_builder_admin.py index b37a472..ab75efd 100644 --- a/backend/workflows/tests/test_form_builder_admin.py +++ b/backend/workflows/tests/test_form_builder_admin.py @@ -3,7 +3,7 @@ import json from django.contrib.auth import get_user_model from django.test import TestCase -from workflows.models import FormFieldConfig, FormOption +from workflows.models import FormFieldConfig, FormOption, FormSectionConfig class FormBuilderAdminTests(TestCase): @@ -94,3 +94,81 @@ class FormBuilderAdminTests(TestCase): self.assertEqual(response.status_code, 302) self.assertTrue(FormOption.objects.filter(category='device', label='Tablet').exists()) + + def test_staff_can_save_field_rules(self): + self.client.force_login(self.staff) + self.client.get('/admin-tools/form-builder/?form_type=onboarding', HTTP_HOST='localhost') + department = FormFieldConfig.objects.get(form_type='onboarding', field_name='department') + contract_start = FormFieldConfig.objects.get(form_type='onboarding', field_name='contract_start') + + response = self.client.post( + '/admin-tools/form-builder/?form_type=onboarding&option_category=device', + data={ + 'builder_action': 'save_field_rules', + 'field_rule_ids': [str(department.id), str(contract_start.id)], + f'is_required_{department.id}': 'required', + f'is_visible_{contract_start.id}': 'on', + }, + HTTP_HOST='localhost', + ) + + self.assertEqual(response.status_code, 302) + department.refresh_from_db() + contract_start.refresh_from_db() + self.assertEqual(department.is_required, True) + self.assertEqual(contract_start.is_required, None) + + def test_staff_can_save_section_rules_with_locked_sections_preserved(self): + self.client.force_login(self.staff) + self.client.get('/admin-tools/form-builder/?form_type=onboarding', HTTP_HOST='localhost') + + response = self.client.post( + '/admin-tools/form-builder/?form_type=onboarding&option_category=device', + data={ + 'builder_action': 'save_section_rules', + }, + HTTP_HOST='localhost', + ) + + self.assertEqual(response.status_code, 302) + itsetup = FormSectionConfig.objects.get(form_type='onboarding', section_key='itsetup') + stammdaten = FormSectionConfig.objects.get(form_type='onboarding', section_key='stammdaten') + self.assertEqual(itsetup.is_visible, False) + self.assertEqual(stammdaten.is_visible, True) + + def test_apply_onboarding_lean_preset_updates_section_and_field_rules(self): + self.client.force_login(self.staff) + response = self.client.post( + '/admin-tools/form-builder/?form_type=onboarding&option_category=device', + data={ + 'builder_action': 'apply_preset', + 'preset_key': 'lean', + }, + HTTP_HOST='localhost', + ) + + self.assertEqual(response.status_code, 302) + itsetup = FormSectionConfig.objects.get(form_type='onboarding', section_key='itsetup') + gender = FormFieldConfig.objects.get(form_type='onboarding', field_name='gender') + contract_start = FormFieldConfig.objects.get(form_type='onboarding', field_name='contract_start') + self.assertEqual(itsetup.is_visible, False) + self.assertEqual(gender.is_visible, False) + self.assertEqual(contract_start.is_required, None) + + def test_apply_offboarding_hr_heavy_preset_updates_fields(self): + self.client.force_login(self.staff) + response = self.client.post( + '/admin-tools/form-builder/?form_type=offboarding&option_category=device', + data={ + 'builder_action': 'apply_preset', + 'preset_key': 'hr_heavy', + }, + HTTP_HOST='localhost', + ) + + self.assertEqual(response.status_code, 302) + notes = FormFieldConfig.objects.get(form_type='offboarding', field_name='notes') + department = FormFieldConfig.objects.get(form_type='offboarding', field_name='department') + self.assertEqual(notes.is_visible, True) + self.assertEqual(notes.is_required, True) + self.assertEqual(department.is_required, True) diff --git a/backend/workflows/tests/test_onboarding_flow.py b/backend/workflows/tests/test_onboarding_flow.py index 0d84c1a..a91a714 100644 --- a/backend/workflows/tests/test_onboarding_flow.py +++ b/backend/workflows/tests/test_onboarding_flow.py @@ -4,7 +4,7 @@ from django.contrib.auth import get_user_model from django.test import TestCase from workflows.branding import get_company_email_domain -from workflows.models import FormFieldConfig, OnboardingRequest +from workflows.models import FormFieldConfig, FormSectionConfig, OnboardingRequest class OnboardingFlowTests(TestCase): @@ -112,3 +112,34 @@ class OnboardingFlowTests(TestCase): self.assertContains(response, 'Dieses Feld ist zwingend erforderlich.') self.assertFalse(OnboardingRequest.objects.filter(work_email=f'lina.leer@{self.company_domain}').exists()) mock_delay.assert_not_called() + + @patch('workflows.views.process_onboarding_request.delay') + def test_hidden_itsetup_section_is_removed_from_form_and_submission(self, mock_delay): + FormSectionConfig.objects.update_or_create( + form_type='onboarding', + section_key='itsetup', + defaults={'is_visible': False}, + ) + + response = self.client.get('/onboarding/new/', HTTP_HOST='localhost') + self.assertEqual(response.status_code, 200) + self.assertNotContains(response, '3. IT-Setup') + + payload = { + 'first_name': 'Nora', + 'last_name': 'Neutral', + 'gender': 'frau', + 'job_title': 'Consultant', + 'department': 'IT-Service', + 'work_email': f'nora.section@{self.company_domain}', + 'contract_start': '2026-11-01', + 'employment_type': 'unbefristet', + 'group_mailboxes_required_choice': 'nein', + 'agreement_confirm': 'on', + } + + submit_response = self.client.post('/onboarding/new/', payload, HTTP_HOST='localhost') + + self.assertEqual(submit_response.status_code, 302) + self.assertTrue(OnboardingRequest.objects.filter(work_email=f'nora.section@{self.company_domain}').exists()) + mock_delay.assert_called_once() diff --git a/backend/workflows/views.py b/backend/workflows/views.py index 621c1be..0b0233c 100644 --- a/backend/workflows/views.py +++ b/backend/workflows/views.py @@ -37,13 +37,22 @@ from .branding import get_branding_email_copy, get_company_email_domain, get_def from .forms import AccountAvatarForm, AccountDetailsForm, AccountNotificationPreferencesForm, AccountTOTPDisableForm, AccountTOTPEnableForm, AccountTOTPRegenerateRecoveryCodesForm, AppLoginForm, AppTOTPChallengeForm, OffboardingRequestForm, OnboardingRequestForm, PortalBrandingForm, PortalCompanyConfigForm, PortalTrialConfigForm, UserManagementCreateForm from .form_builder import ( DEFAULT_FIELD_ORDER, + FORM_PRESETS, LOCKED_FIELD_RULES, + LOCKED_SECTION_RULES, + OFFBOARDING_PAGE_LABELS, + OFFBOARDING_PAGE_ORDER, ONBOARDING_DEFAULT_PAGE, ONBOARDING_PAGE_LABELS, ONBOARDING_PAGE_ORDER, ensure_form_field_configs, + ensure_form_section_configs, + get_default_page_map, + get_section_labels, + get_section_order, + apply_form_preset, ) -from .models import AdminAuditLog, AsyncTaskLog, EmployeeProfile, FormFieldConfig, FormOption, IntroChecklistItem, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, PortalAppConfig, PortalBranding, PortalCompanyConfig, PortalTrialConfig, ScheduledWelcomeEmail, SystemEmailConfig, UserNotification, UserProfile, WorkflowConfig +from .models import AdminAuditLog, AsyncTaskLog, EmployeeProfile, FormFieldConfig, FormOption, FormSectionConfig, IntroChecklistItem, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, PortalAppConfig, PortalBranding, PortalCompanyConfig, PortalTrialConfig, ScheduledWelcomeEmail, SystemEmailConfig, UserNotification, UserProfile, WorkflowConfig from .emailing import send_system_email from .notifications import notify_user 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 @@ -531,13 +540,14 @@ def _section_for_block(block: dict, field_pages: dict[str, str]) -> str: return field_pages.get(fields[0].name, 'abschluss') -def _build_onboarding_sections(blocks: list[dict], field_pages: dict[str, str]) -> list[dict]: +def _build_onboarding_sections(blocks: list[dict], field_pages: dict[str, str], visible_section_keys: set[str] | None = None) -> list[dict]: grouped = {key: [] for key in ONBOARDING_SECTION_ORDER} for block in blocks: section_key = _section_for_block(block, field_pages) if section_key not in grouped: section_key = 'abschluss' grouped[section_key].append(block) + visible_keys = visible_section_keys or set(ONBOARDING_SECTION_ORDER) return [ { 'key': key, @@ -546,6 +556,35 @@ def _build_onboarding_sections(blocks: list[dict], field_pages: dict[str, str]) 'blocks': grouped[key], } for key in ONBOARDING_SECTION_ORDER + if key in visible_keys + ] + + +OFFBOARDING_SECTION_META = { + 'mitarbeitende': {'title': gettext_lazy('Mitarbeitende'), 'subtitle': gettext_lazy('Person, Rolle und Bereich')}, + 'austritt': {'title': gettext_lazy('Austritt'), 'subtitle': gettext_lazy('Letzter Arbeitstag')}, + 'abschluss': {'title': gettext_lazy('Abschluss'), 'subtitle': gettext_lazy('Hinweise und Abschlussnotizen')}, +} + + +def _build_offboarding_sections(form, visible_section_keys: set[str] | None = None) -> list[dict]: + field_pages = getattr(form, '_field_page_keys', {}) + grouped = {key: [] for key in OFFBOARDING_PAGE_ORDER} + for field_name in form.fields.keys(): + section_key = field_pages.get(field_name, 'abschluss') + if section_key not in grouped: + section_key = 'abschluss' + grouped[section_key].append(form[field_name]) + visible_keys = visible_section_keys or set(OFFBOARDING_PAGE_ORDER) + return [ + { + 'key': key, + 'title': OFFBOARDING_SECTION_META[key]['title'], + 'subtitle': OFFBOARDING_SECTION_META[key]['subtitle'], + 'fields': grouped[key], + } + for key in OFFBOARDING_PAGE_ORDER + if key in visible_keys and grouped[key] ] @@ -1780,7 +1819,12 @@ def onboarding_create(request): onboarding_blocks = _build_onboarding_layout(form) field_pages = getattr(form, '_field_page_keys', {}) - onboarding_sections = _build_onboarding_sections(onboarding_blocks, field_pages) + section_configs = ensure_form_section_configs('onboarding') + visible_section_keys = { + key for key in ONBOARDING_SECTION_ORDER + if key in LOCKED_SECTION_RULES.get('onboarding', set()) or section_configs.get(key, None) is None or section_configs[key].is_visible + } + onboarding_sections = _build_onboarding_sections(onboarding_blocks, field_pages, visible_section_keys=visible_section_keys) return render( request, @@ -1969,6 +2013,14 @@ def offboarding_create(request): else: form = OffboardingRequestForm(prefill_profile=selected_profile, initial={'search_query': search_query}) + field_pages = getattr(form, '_field_page_keys', {}) + section_configs = ensure_form_section_configs('offboarding') + visible_section_keys = { + key for key in OFFBOARDING_PAGE_ORDER + if key in LOCKED_SECTION_RULES.get('offboarding', set()) or section_configs.get(key, None) is None or section_configs[key].is_visible + } + offboarding_sections = _build_offboarding_sections(form, visible_section_keys=visible_section_keys) + return render( request, 'workflows/offboarding_form.html', @@ -1980,6 +2032,7 @@ def offboarding_create(request): 'saved': request.GET.get('saved') == '1', 'saved_request_id': request.GET.get('id', ''), 'portal_email_domain': get_company_email_domain(), + 'offboarding_sections': offboarding_sections, }, ) @@ -1997,6 +2050,9 @@ def offboarding_success(request, request_id: int): def form_builder_page(request): language_code = get_language() form_type = request.GET.get('form_type', 'onboarding') + anchor = (request.GET.get('anchor') or '').strip() + active_panel = (request.GET.get('panel') or '').strip() + active_subpanel = (request.GET.get('subpanel') or '').strip() if form_type not in DEFAULT_FIELD_ORDER: form_type = 'onboarding' option_category = request.GET.get('option_category', 'department') @@ -2017,7 +2073,7 @@ def form_builder_page(request): option.delete() _audit(request, 'form_option_deleted', target_type='form_option', target_id=deleted_id, target_label=deleted_label) messages.success(request, 'Option wurde gelöscht.') - return redirect(f"/admin-tools/form-builder/?form_type={form_type}&option_category={option_category}") + return redirect(f"/admin-tools/form-builder/?form_type={form_type}&option_category={option_category}&panel=builder-content&subpanel=options#builder-content") action = request.POST.get('builder_action', '') if action == 'add_option': @@ -2068,7 +2124,7 @@ def form_builder_page(request): option.save(update_fields=['label', 'label_en', 'value', 'is_active', 'sort_order']) except IntegrityError: messages.error(request, f'Doppelte Bezeichnung in Kategorie: {next_label}') - return redirect(f"/admin-tools/form-builder/?form_type={form_type}&option_category={option.category}") + return redirect(f"/admin-tools/form-builder/?form_type={form_type}&option_category={option.category}&panel=builder-content&subpanel=options#builder-content") option_category = option.category _audit(request, 'form_options_saved', target_type='form_option', target_label=option_category, details={'count': len(option_ids)}) messages.success(request, 'Optionen wurden gespeichert.') @@ -2087,7 +2143,67 @@ def form_builder_page(request): _audit(request, 'form_field_texts_saved', target_type='form_config', target_label=form_type, details={'count': len(field_ids)}) messages.success(request, 'Feldtexte wurden gespeichert.') - return redirect(f"/admin-tools/form-builder/?form_type={form_type}&option_category={option_category}") + elif action == 'save_field_rules': + field_ids = request.POST.getlist('field_rule_ids') + locked_fields = LOCKED_FIELD_RULES.get(form_type, set()) + updated = 0 + for raw_id in field_ids: + cfg = FormFieldConfig.objects.filter(id=raw_id, form_type=form_type).first() + if not cfg: + continue + if cfg.field_name in locked_fields: + cfg.is_visible = True + cfg.is_required = None + else: + cfg.is_visible = request.POST.get(f'is_visible_{cfg.id}') == 'on' + required_mode = (request.POST.get(f'is_required_{cfg.id}') or '').strip() + cfg.is_required = True if required_mode == 'required' else False if required_mode == 'optional' else None + cfg.save(update_fields=['is_visible', 'is_required']) + updated += 1 + _audit(request, 'form_field_rules_saved', target_type='form_config', target_label=form_type, details={'count': updated}) + messages.success(request, 'Feldregeln wurden gespeichert.') + + elif action == 'save_section_rules' and form_type in {'onboarding', 'offboarding'}: + section_configs = ensure_form_section_configs(form_type) + locked_sections = LOCKED_SECTION_RULES.get(form_type, set()) + updated = 0 + for section_key, cfg in section_configs.items(): + if section_key in locked_sections: + if not cfg.is_visible: + cfg.is_visible = True + cfg.save(update_fields=['is_visible']) + continue + cfg.is_visible = request.POST.get(f'section_visible_{section_key}') == 'on' + cfg.save(update_fields=['is_visible']) + updated += 1 + _audit(request, 'form_section_rules_saved', target_type='form_config', target_label=form_type, details={'count': updated}) + messages.success(request, 'Abschnittsregeln wurden gespeichert.') + + elif action == 'apply_preset': + preset_key = (request.POST.get('preset_key') or '').strip() + if apply_form_preset(form_type, preset_key): + active_panel = 'builder-preview' + _audit(request, 'form_preset_applied', target_type='form_config', target_label=form_type, details={'preset': preset_key}) + messages.success(request, 'Preset wurde angewendet.') + else: + messages.error(request, 'Preset konnte nicht angewendet werden.') + + if action in {'add_option', 'save_options', 'save_field_texts'}: + active_panel = 'builder-content' + if action in {'add_option', 'save_options'}: + active_subpanel = 'options' + elif action == 'save_field_texts': + active_subpanel = 'field-texts' + elif action in {'save_field_rules', 'save_section_rules'}: + active_panel = 'builder-rules' + redirect_target = f"/admin-tools/form-builder/?form_type={form_type}&option_category={option_category}" + if active_panel: + redirect_target += f"&panel={active_panel}" + if active_subpanel: + redirect_target += f"&subpanel={active_subpanel}" + if anchor == 'builder-content' or active_panel == 'builder-content': + redirect_target += "#builder-content" + return redirect(redirect_target) default_names = list(DEFAULT_FIELD_ORDER.get(form_type, [])) existing_names = list( @@ -2100,12 +2216,17 @@ def form_builder_page(request): default_names.append(name) ensure_form_field_configs(form_type, default_names) + section_configs = ensure_form_section_configs(form_type) + section_order = get_section_order(form_type) + section_labels = get_section_labels(form_type) + default_page_map = get_default_page_map(form_type) configs = list( FormFieldConfig.objects.filter(form_type=form_type).order_by('sort_order', 'field_name') ) labels = _form_field_labels(form_type) locked = LOCKED_FIELD_RULES.get(form_type, set()) + locked_sections = LOCKED_SECTION_RULES.get(form_type, set()) if form_type == 'onboarding': columns = [ @@ -2131,27 +2252,127 @@ def form_builder_page(request): 'is_visible': cfg.is_visible, 'is_required': cfg.is_required, 'locked': cfg.field_name in locked, + 'page_key': page_key, } ) else: columns = [ { - 'key': 'all', - 'title': 'Offboarding Felder', - 'items': [ - { - 'field_name': cfg.field_name, - 'label': cfg.translated_label_override(language_code) or labels.get(cfg.field_name, cfg.field_name), - 'label_de': cfg.label_override or labels.get(cfg.field_name, cfg.field_name), - 'label_en': cfg.label_override_en, - 'is_visible': cfg.is_visible, - 'is_required': cfg.is_required, - 'locked': cfg.field_name in locked, - } - for cfg in configs - ], + 'key': key, + 'title': section_labels.get(key, key), + 'items': [], } + for key in section_order ] + column_by_key = {c['key']: c for c in columns} + fallback = section_order[-1] if section_order else 'all' + for cfg in configs: + page_key = cfg.page_key or default_page_map.get(cfg.field_name, fallback) + if page_key not in column_by_key: + page_key = fallback + column_by_key[page_key]['items'].append( + { + 'field_name': cfg.field_name, + 'label': cfg.translated_label_override(language_code) or labels.get(cfg.field_name, cfg.field_name), + 'label_de': cfg.label_override or labels.get(cfg.field_name, cfg.field_name), + 'label_en': cfg.label_override_en, + 'is_visible': cfg.is_visible, + 'is_required': cfg.is_required, + 'locked': cfg.field_name in locked, + 'page_key': page_key, + } + ) + + section_rule_items = [] + if section_order: + fallback_section = section_order[-1] if section_order else '' + for key in section_order: + cfg = section_configs.get(key) + section_rule_items.append( + { + 'key': key, + 'title': section_labels.get(key, key), + 'is_visible': True if not cfg else cfg.is_visible, + 'locked': key in locked_sections, + 'field_count': len([c for c in configs if (c.page_key or default_page_map.get(c.field_name, fallback_section)) == key]), + } + ) + + field_rule_items = [] + for cfg in configs: + page_key = cfg.page_key or default_page_map.get(cfg.field_name, section_order[-1] if section_order else '') + field_rule_items.append( + { + 'id': cfg.id, + 'field_name': cfg.field_name, + 'label': cfg.translated_label_override(language_code) or labels.get(cfg.field_name, cfg.field_name), + 'page_key': page_key, + 'page_label': section_labels.get(page_key, page_key) if section_order else '', + 'is_visible': cfg.is_visible, + 'is_required': cfg.is_required, + 'locked': cfg.field_name in locked, + } + ) + + field_rule_groups = [] + if section_order: + grouped_rules = {key: [] for key in section_order} + for item in field_rule_items: + grouped_rules.setdefault(item['page_key'], []).append(item) + for key in section_order: + field_rule_groups.append( + { + 'key': key, + 'title': section_labels.get(key, key), + 'items': grouped_rules.get(key, []), + } + ) + + field_text_groups = [] + if section_order: + grouped_texts = {key: [] for key in section_order} + for cfg in configs: + page_key = cfg.page_key or default_page_map.get(cfg.field_name, section_order[-1] if section_order else '') + grouped_texts.setdefault(page_key, []).append(cfg) + for key in section_order: + field_text_groups.append( + { + 'key': key, + 'title': section_labels.get(key, key), + 'items': grouped_texts.get(key, []), + } + ) + + preview_sections = [] + if section_order: + field_rule_group_map = {group['key']: group['items'] for group in field_rule_groups} + for key in section_order: + section_cfg = section_configs.get(key) + section_locked = key in locked_sections + section_visible = True if section_locked or not section_cfg else section_cfg.is_visible + visible_items = [ + item for item in field_rule_group_map.get(key, []) + if item['locked'] or item['is_visible'] + ] + if section_visible: + preview_sections.append( + { + 'key': key, + 'title': section_labels.get(key, key), + 'items': visible_items, + } + ) + + locked_field_count = len([item for item in field_rule_items if item['locked']]) + hidden_field_count = len([item for item in field_rule_items if not item['is_visible']]) + configurable_field_count = len(field_rule_items) - locked_field_count + hidden_section_count = len([item for item in section_rule_items if not item['is_visible']]) if section_rule_items else 0 + builder_summary = { + 'locked_field_count': locked_field_count, + 'configurable_field_count': configurable_field_count, + 'hidden_field_count': hidden_field_count, + 'hidden_section_count': hidden_section_count, + } return render( request, @@ -2164,6 +2385,15 @@ def form_builder_page(request): 'selected_option_category': option_category, 'option_items': FormOption.objects.filter(category=option_category).order_by('sort_order', 'label'), 'field_text_items': configs, + 'field_rule_items': field_rule_items, + 'field_rule_groups': field_rule_groups, + 'field_text_groups': field_text_groups, + 'preview_sections': preview_sections, + 'section_rule_items': section_rule_items, + 'builder_summary': builder_summary, + 'active_panel': active_panel, + 'active_subpanel': active_subpanel, + 'available_presets': FORM_PRESETS.get(form_type, {}), }, )