From fdc27f2123b2cd14d21ac2f7b7d262f9084a12c7 Mon Sep 17 00:00:00 2001 From: Md Bayazid Bostame Date: Fri, 27 Mar 2026 13:21:25 +0100 Subject: [PATCH] snapshot: preserve custom field parity across forms timeline and pdf --- backend/locale/en/LC_MESSAGES/django.po | 1309 ++++++++++------- backend/workflows/form_builder.py | 142 +- backend/workflows/forms.py | 27 +- ...ingrequest_custom_field_values_and_more.py | 63 + backend/workflows/models.py | 78 + backend/workflows/pdf_sections.py | 21 + .../workflows/static/workflows/css/home.css | 109 ++ .../templates/workflows/form_builder.html | 141 +- .../workflows/templates/workflows/home.html | 51 + .../templates/workflows/job_monitor.html | 39 + .../templates/workflows/offboarding_form.html | 8 + .../templates/workflows/onboarding_form.html | 4 +- .../templates/workflows/request_timeline.html | 22 +- .../tests/test_form_builder_admin.py | 58 +- .../workflows/tests/test_observability_ui.py | 123 ++ .../workflows/tests/test_offboarding_flow.py | 36 +- .../workflows/tests/test_onboarding_flow.py | 167 ++- backend/workflows/tests/test_pdf_sections.py | 31 +- .../workflows/tests/test_request_timeline.py | 73 + backend/workflows/views.py | 337 ++++- 20 files changed, 2294 insertions(+), 545 deletions(-) create mode 100644 backend/workflows/migrations/0055_offboardingrequest_custom_field_values_and_more.py create mode 100644 backend/workflows/tests/test_observability_ui.py create mode 100644 backend/workflows/tests/test_request_timeline.py diff --git a/backend/locale/en/LC_MESSAGES/django.po b/backend/locale/en/LC_MESSAGES/django.po index ca8e780..28ef53a 100644 --- a/backend/locale/en/LC_MESSAGES/django.po +++ b/backend/locale/en/LC_MESSAGES/django.po @@ -2,15 +2,15 @@ msgid "" msgstr "" "Project-Id-Version: tubco-portal\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-03-27 11:06+0000\n" +"POT-Creation-Date: 2026-03-27 12:07+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:521 -#: workflows/models.py:588 +#: workflows/app_registry.py:35 workflows/models.py:485 workflows/models.py:524 +#: workflows/models.py:552 workflows/models.py:582 workflows/models.py:690 #: workflows/templates/workflows/onboarding_form.html:25 #: workflows/templates/workflows/requests_dashboard.html:68 #: workflows/templates/workflows/requests_dashboard.html:131 @@ -37,7 +37,8 @@ msgstr "Multi-step form" msgid "E-Mail Routing" msgstr "Email routing" -#: workflows/app_registry.py:46 workflows/models.py:483 workflows/models.py:589 +#: workflows/app_registry.py:46 workflows/models.py:486 workflows/models.py:525 +#: workflows/models.py:583 workflows/models.py:691 #: workflows/templates/workflows/requests_dashboard.html:78 #: workflows/templates/workflows/requests_dashboard.html:132 msgid "Offboarding" @@ -93,8 +94,8 @@ msgstr "Search" #: workflows/app_registry.py:62 #: workflows/templates/workflows/app_registry.html:32 #: workflows/templates/workflows/backup_recovery.html:72 -#: workflows/templates/workflows/job_monitor.html:29 -#: workflows/templates/workflows/job_monitor.html:50 +#: workflows/templates/workflows/job_monitor.html:68 +#: workflows/templates/workflows/job_monitor.html:89 #: workflows/templates/workflows/onboarding_intro_session.html:37 #: workflows/templates/workflows/request_timeline.html:70 #: workflows/templates/workflows/requests_dashboard.html:136 @@ -127,11 +128,12 @@ 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/form_builder.html:86 +#: workflows/templates/workflows/form_builder.html:155 +#: workflows/templates/workflows/form_builder.html:327 +#: workflows/templates/workflows/form_builder.html:337 +#: workflows/templates/workflows/form_builder.html:412 +#: workflows/templates/workflows/form_builder.html:463 #: workflows/templates/workflows/includes/app_header.html:57 msgid "Öffnen" msgstr "Open" @@ -390,222 +392,218 @@ msgstr "" msgid "Remote Backup in Nextcloud konnte nicht gelöscht werden." msgstr "" -#: workflows/forms.py:106 workflows/forms.py:473 +#: workflows/forms.py:112 workflows/forms.py:476 #: workflows/templates/workflows/user_management.html:72 #: workflows/templates/workflows/user_management.html:170 msgid "Benutzername" msgstr "" -#: workflows/forms.py:108 +#: workflows/forms.py:114 msgid "Passwort" msgstr "Password" -#: workflows/forms.py:114 +#: workflows/forms.py:120 msgid "Benutzername oder Passwort sind nicht korrekt." msgstr "" -#: workflows/forms.py:115 +#: workflows/forms.py:121 #, fuzzy #| msgid "Deaktivieren" msgid "Dieses Konto ist deaktiviert." msgstr "Disabled" -#: workflows/forms.py:141 workflows/forms.py:304 workflows/forms.py:356 +#: workflows/forms.py:147 workflows/forms.py:307 workflows/forms.py:359 msgid "TOTP-Code" msgstr "" -#: workflows/forms.py:147 workflows/forms.py:362 +#: workflows/forms.py:153 workflows/forms.py:365 msgid "Recovery-Code" msgstr "" -#: workflows/forms.py:154 workflows/forms.py:325 workflows/forms.py:382 +#: workflows/forms.py:160 workflows/forms.py:328 workflows/forms.py:385 msgid "Der TOTP-Code ist ungültig." msgstr "" -#: workflows/forms.py:155 +#: workflows/forms.py:161 msgid "Bitte geben Sie Ihren TOTP-Code ein." msgstr "" -#: workflows/forms.py:182 workflows/forms.py:246 workflows/forms.py:474 +#: workflows/forms.py:188 workflows/forms.py:249 workflows/forms.py:477 #, fuzzy #| msgid "E-Mail" msgid "E-Mail-Adresse" msgstr "Email" -#: workflows/forms.py:187 workflows/forms.py:206 +#: workflows/forms.py:193 workflows/forms.py:212 #: workflows/templates/workflows/user_management.html:77 #: workflows/templates/workflows/user_management.html:108 msgid "Neues Passwort" msgstr "New password" -#: workflows/forms.py:193 workflows/forms.py:212 +#: workflows/forms.py:199 workflows/forms.py:218 msgid "Neues Passwort bestätigen" msgstr "Confirm new password" -#: workflows/forms.py:201 workflows/forms.py:299 workflows/forms.py:331 +#: workflows/forms.py:207 workflows/forms.py:302 workflows/forms.py:334 #, fuzzy #| msgid "Neues Passwort" msgid "Aktuelles Passwort" msgstr "New password" -#: workflows/forms.py:223 workflows/templates/workflows/account_profile.html:36 +#: workflows/forms.py:229 workflows/templates/workflows/account_profile.html:36 #: workflows/templates/workflows/includes/app_header.html:79 msgid "Profilbild" msgstr "" -#: workflows/forms.py:239 -msgid "Das Profilbild darf maximal 5 MB groß sein." -msgstr "" - -#: workflows/forms.py:244 workflows/forms.py:471 +#: workflows/forms.py:247 workflows/forms.py:474 #: workflows/templates/workflows/account_profile.html:116 msgid "Vorname" msgstr "" -#: workflows/forms.py:245 workflows/forms.py:472 +#: workflows/forms.py:248 workflows/forms.py:475 #: workflows/templates/workflows/account_profile.html:120 msgid "Nachname" msgstr "" -#: workflows/forms.py:247 +#: workflows/forms.py:250 #: workflows/templates/workflows/account_profile.html:124 msgid "Telefon" msgstr "" -#: workflows/forms.py:248 +#: workflows/forms.py:251 #: workflows/templates/workflows/account_profile.html:128 msgid "Mobil" msgstr "" -#: workflows/forms.py:249 +#: workflows/forms.py:252 #: workflows/templates/workflows/account_profile.html:132 #, fuzzy #| msgid "Produktion" msgid "Position" msgstr "Production" -#: workflows/forms.py:250 workflows/models.py:443 +#: workflows/forms.py:253 workflows/models.py:444 #: workflows/templates/workflows/account_profile.html:136 #: workflows/templates/workflows/onboarding_intro_session.html:28 #: workflows/templates/workflows/requests_dashboard.html:145 msgid "Abteilung" msgstr "Department" -#: workflows/forms.py:251 +#: workflows/forms.py:254 #: workflows/templates/workflows/account_profile.html:140 msgid "Standort" msgstr "" -#: workflows/forms.py:253 +#: workflows/forms.py:256 #: workflows/templates/workflows/account_profile.html:144 #, fuzzy #| msgid "Einweisung" msgid "Hinweise" msgstr "Introduction" -#: workflows/forms.py:317 workflows/forms.py:344 +#: workflows/forms.py:320 workflows/forms.py:347 msgid "Das aktuelle Passwort ist nicht korrekt." msgstr "" -#: workflows/forms.py:323 +#: workflows/forms.py:326 msgid "Bitte geben Sie einen gültigen TOTP-Code ein." msgstr "" -#: workflows/forms.py:350 +#: workflows/forms.py:353 #, fuzzy #| msgid "Deaktivieren" msgid "TOTP ist für dieses Konto nicht aktiv." msgstr "Disabled" -#: workflows/forms.py:378 +#: workflows/forms.py:381 msgid "Bitte geben Sie einen gültigen TOTP-Code oder Recovery-Code ein." msgstr "" -#: workflows/forms.py:385 +#: workflows/forms.py:388 msgid "Der Recovery-Code ist ungültig." msgstr "" -#: workflows/forms.py:390 +#: workflows/forms.py:393 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Onboarding erfolgreich" msgstr "Save offboarding request" -#: workflows/forms.py:391 +#: workflows/forms.py:394 #, fuzzy #| msgid "Fehlgeschlagen" msgid "Onboarding fehlgeschlagen" msgstr "Failed" -#: workflows/forms.py:392 +#: workflows/forms.py:395 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Offboarding erfolgreich" msgstr "Save offboarding request" -#: workflows/forms.py:393 +#: workflows/forms.py:396 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Offboarding fehlgeschlagen" msgstr "Save offboarding request" -#: workflows/forms.py:394 +#: workflows/forms.py:397 #, fuzzy #| msgid "Eingereicht" msgid "Backup erfolgreich" msgstr "Submitted" -#: workflows/forms.py:395 workflows/views.py:1387 +#: workflows/forms.py:398 workflows/views.py:1448 #, fuzzy #| msgid "Fehlgeschlagen" msgid "Backup fehlgeschlagen" msgstr "Failed" -#: workflows/forms.py:396 +#: workflows/forms.py:399 #, fuzzy #| msgid "Welcome E-Mails" msgid "Welcome E-Mail erfolgreich" msgstr "Welcome Emails" -#: workflows/forms.py:397 +#: workflows/forms.py:400 #, fuzzy #| msgid "Welcome E-Mails" msgid "Welcome E-Mail fehlgeschlagen" msgstr "Welcome Emails" -#: workflows/forms.py:398 +#: workflows/forms.py:401 #, fuzzy #| msgid "Einweisung" msgid "Trial-Hinweise" msgstr "Introduction" -#: workflows/forms.py:399 +#: workflows/forms.py:402 #, fuzzy #| msgid "Einweisung" msgid "System-Hinweise" msgstr "Introduction" -#: workflows/forms.py:415 +#: workflows/forms.py:418 #, fuzzy #| msgid "Workflow-Regeln" msgid "Workflow" msgstr "Workflow rules" -#: workflows/forms.py:416 workflows/views.py:1544 +#: workflows/forms.py:419 workflows/views.py:1605 #, fuzzy #| msgid "Welcome E-Mails" msgid "Welcome E-Mail" msgstr "Welcome Emails" -#: workflows/forms.py:417 workflows/templates/workflows/handbook.html:21 +#: workflows/forms.py:420 workflows/templates/workflows/handbook.html:21 msgid "Operations" msgstr "Operations" -#: workflows/forms.py:418 +#: workflows/forms.py:421 msgid "Platform" msgstr "" -#: workflows/forms.py:475 workflows/templates/workflows/user_management.html:74 +#: workflows/forms.py:478 workflows/templates/workflows/user_management.html:74 #: workflows/templates/workflows/user_management.html:93 #: workflows/templates/workflows/user_management.html:171 #, fuzzy @@ -613,207 +611,195 @@ msgstr "" msgid "Rolle" msgstr "Role:" -#: workflows/forms.py:489 +#: workflows/forms.py:492 msgid "Dieser Benutzername ist bereits vergeben." msgstr "This username is already taken." -#: workflows/forms.py:498 workflows/views.py:1196 +#: workflows/forms.py:501 workflows/views.py:1257 msgid "Ungültige Rolle." msgstr "Invalid role." -#: workflows/forms.py:500 workflows/views.py:1199 +#: workflows/forms.py:503 workflows/views.py:1260 msgid "Nur Platform Owner dürfen diese Rolle vergeben." msgstr "" -#: workflows/forms.py:539 +#: workflows/forms.py:542 msgid "Portal-Titel" msgstr "Portal title" -#: workflows/forms.py:540 +#: workflows/forms.py:543 msgid "Firmenname" msgstr "Company name" -#: workflows/forms.py:541 +#: workflows/forms.py:544 #, fuzzy #| msgid "Firmenname" msgid "Firmen-Domain" msgstr "Company name" -#: workflows/forms.py:542 +#: workflows/forms.py:545 msgid "Support-E-Mail" msgstr "Support email" -#: workflows/forms.py:543 +#: workflows/forms.py:546 msgid "Absender-Anzeigename" msgstr "" -#: workflows/forms.py:544 +#: workflows/forms.py:547 msgid "Login-Untertitel" msgstr "" -#: workflows/forms.py:545 +#: workflows/forms.py:548 msgid "Footer-Text DE" msgstr "" -#: workflows/forms.py:546 +#: workflows/forms.py:549 msgid "Footer-Text EN" msgstr "" -#: workflows/forms.py:547 +#: workflows/forms.py:550 msgid "Rechtlicher Hinweis DE" msgstr "" -#: workflows/forms.py:548 +#: workflows/forms.py:551 msgid "Rechtlicher Hinweis EN" msgstr "" -#: workflows/forms.py:549 +#: workflows/forms.py:552 msgid "Standardsprache" msgstr "Default language" -#: workflows/forms.py:550 +#: workflows/forms.py:553 msgid "Logo" msgstr "Logo" -#: workflows/forms.py:551 +#: workflows/forms.py:554 msgid "PDF-Briefkopf" msgstr "PDF letterhead" -#: workflows/forms.py:552 +#: workflows/forms.py:555 msgid "Favicon" msgstr "" -#: workflows/forms.py:553 +#: workflows/forms.py:556 #: workflows/templates/workflows/branding_settings.html:89 #: workflows/templates/workflows/branding_settings.html:162 msgid "Primärfarbe" msgstr "Primary color" -#: workflows/forms.py:554 +#: workflows/forms.py:557 #: workflows/templates/workflows/branding_settings.html:90 #: workflows/templates/workflows/branding_settings.html:163 msgid "Sekundärfarbe" msgstr "Secondary color" -#: workflows/forms.py:571 -msgid "Das Logo darf maximal 5 MB groß sein." -msgstr "" - -#: workflows/forms.py:579 -msgid "Der PDF-Briefkopf darf maximal 10 MB groß sein." -msgstr "" - -#: workflows/forms.py:587 -msgid "Das Favicon darf maximal 2 MB groß sein." -msgstr "" - -#: workflows/forms.py:611 +#: workflows/forms.py:605 #, fuzzy #| msgid "Firmenname" msgid "Rechtlicher Firmenname" msgstr "Company name" -#: workflows/forms.py:612 +#: workflows/forms.py:606 msgid "Straße und Hausnummer" msgstr "" -#: workflows/forms.py:613 +#: workflows/forms.py:607 msgid "Postleitzahl" msgstr "" -#: workflows/forms.py:614 +#: workflows/forms.py:608 msgid "Stadt" msgstr "" -#: workflows/forms.py:615 +#: workflows/forms.py:609 msgid "Land" msgstr "" -#: workflows/forms.py:616 workflows/templates/workflows/base_shell.html:64 +#: workflows/forms.py:610 workflows/templates/workflows/base_shell.html:64 msgid "Website" msgstr "" -#: workflows/forms.py:617 +#: workflows/forms.py:611 msgid "Impressum-URL" msgstr "" -#: workflows/forms.py:618 +#: workflows/forms.py:612 msgid "Datenschutz-URL" msgstr "" -#: workflows/forms.py:619 +#: workflows/forms.py:613 msgid "HR-Kontakt" msgstr "" -#: workflows/forms.py:620 +#: workflows/forms.py:614 msgid "IT-Kontakt" msgstr "" -#: workflows/forms.py:621 +#: workflows/forms.py:615 #, fuzzy #| msgid "Operations" msgid "Operations-Kontakt" msgstr "Operations" -#: workflows/forms.py:622 +#: workflows/forms.py:616 msgid "Zentrale Telefonnummer" msgstr "" -#: workflows/forms.py:623 +#: workflows/forms.py:617 msgid "USt-IdNr." msgstr "" -#: workflows/forms.py:624 +#: workflows/forms.py:618 msgid "Register- oder Handelsnummer" msgstr "" -#: workflows/forms.py:641 +#: workflows/forms.py:635 msgid "Trial-Modus aktiv" msgstr "" -#: workflows/forms.py:642 +#: workflows/forms.py:636 msgid "Trial-Beginn" msgstr "" -#: workflows/forms.py:643 +#: workflows/forms.py:637 msgid "Trial-Ende" msgstr "" -#: workflows/forms.py:644 +#: workflows/forms.py:638 msgid "Produktive Integrationen begrenzen" msgstr "" -#: workflows/forms.py:645 +#: workflows/forms.py:639 msgid "Cleanup nach Ablauf zulassen" msgstr "" -#: workflows/forms.py:646 +#: workflows/forms.py:640 msgid "Banner-Text DE" msgstr "" -#: workflows/forms.py:647 +#: workflows/forms.py:641 msgid "Banner-Text EN" msgstr "" -#: workflows/forms.py:667 +#: workflows/forms.py:661 msgid "Bitte ein Trial-Ende festlegen." msgstr "" -#: workflows/forms.py:669 +#: workflows/forms.py:663 msgid "Das Trial-Ende muss nach dem Trial-Beginn liegen." msgstr "" -#: workflows/forms.py:808 workflows/forms.py:993 +#: workflows/forms.py:802 workflows/forms.py:964 #, python-format msgid "Bitte nutzen Sie das Format name@%(domain)s." msgstr "" -#: workflows/forms.py:830 workflows/forms.py:1007 +#: workflows/forms.py:825 workflows/forms.py:979 #, python-format msgid "Bitte verwenden Sie eine @%(domain)s E-Mail-Adresse." msgstr "" -#: workflows/forms.py:915 +#: workflows/forms.py:881 #, python-format msgid "" "Das Übergabedatum muss mindestens %(days)s Tage in der Zukunft liegen " @@ -879,39 +865,40 @@ msgstr "Submitted" msgid "Warnung" msgstr "" -#: workflows/models.py:132 workflows/templates/workflows/job_monitor.html:53 +#: workflows/models.py:132 workflows/templates/workflows/job_monitor.html:39 +#: workflows/templates/workflows/job_monitor.html:92 msgid "Fehler" msgstr "" -#: workflows/models.py:308 workflows/views.py:640 +#: workflows/models.py:308 workflows/views.py:695 #, fuzzy #| msgid "Gesamtbestand" msgid "Gestartet" msgstr "Total records" -#: workflows/models.py:309 workflows/views.py:640 +#: workflows/models.py:309 workflows/views.py:695 #, fuzzy #| msgid "Eingereicht" msgid "Erfolgreich" msgstr "Submitted" -#: workflows/models.py:310 workflows/models.py:363 workflows/models.py:642 +#: workflows/models.py:310 workflows/models.py:363 workflows/models.py:744 #: workflows/templates/workflows/backup_recovery.html:102 #: workflows/templates/workflows/requests_dashboard.html:222 -#: workflows/templates/workflows/welcome_emails.html:108 workflows/views.py:436 -#: workflows/views.py:640 +#: workflows/templates/workflows/welcome_emails.html:108 workflows/views.py:451 +#: workflows/views.py:695 msgid "Fehlgeschlagen" msgstr "Failed" -#: workflows/models.py:360 workflows/views.py:433 +#: workflows/models.py:360 workflows/views.py:448 msgid "Eingereicht" msgstr "Submitted" -#: workflows/models.py:361 workflows/views.py:434 +#: workflows/models.py:361 workflows/views.py:449 msgid "In Bearbeitung" msgstr "Processing" -#: workflows/models.py:362 workflows/models.py:702 workflows/views.py:435 +#: workflows/models.py:362 workflows/models.py:804 workflows/views.py:450 msgid "Abgeschlossen" msgstr "Completed" @@ -935,200 +922,231 @@ msgstr "" msgid "unbefristet" msgstr "" -#: workflows/models.py:444 +#: workflows/models.py:445 msgid "Geräte" msgstr "" -#: workflows/models.py:445 +#: workflows/models.py:446 msgid "Software" msgstr "" -#: workflows/models.py:446 +#: workflows/models.py:447 #, fuzzy #| msgid "Vorgänge" msgid "Zugänge" msgstr "Requests" -#: workflows/models.py:447 +#: workflows/models.py:448 msgid "Workspace-Gruppen" msgstr "" -#: workflows/models.py:448 +#: workflows/models.py:449 msgid "Ressourcen" msgstr "" -#: workflows/models.py:449 +#: workflows/models.py:450 msgid "Telefonnummern" msgstr "" -#: workflows/models.py:475 +#: workflows/models.py:476 msgid "Automatisch" msgstr "" -#: workflows/models.py:476 workflows/models.py:524 workflows/views.py:128 +#: workflows/models.py:477 workflows/models.py:528 workflows/models.py:586 +#: workflows/views.py:125 msgid "Stammdaten" msgstr "Master data" -#: workflows/models.py:477 workflows/models.py:525 workflows/views.py:129 +#: workflows/models.py:478 workflows/models.py:529 workflows/models.py:587 +#: workflows/views.py:126 msgid "Vertrag" msgstr "Contract" -#: workflows/models.py:478 workflows/models.py:526 workflows/views.py:130 +#: workflows/models.py:479 workflows/models.py:530 workflows/models.py:588 +#: workflows/views.py:127 msgid "IT-Setup" msgstr "IT setup" -#: workflows/models.py:479 workflows/models.py:527 workflows/views.py:131 -#: workflows/views.py:566 +#: workflows/models.py:480 workflows/models.py:531 workflows/models.py:589 +#: workflows/views.py:128 workflows/views.py:581 msgid "Abschluss" msgstr "Finish" -#: workflows/models.py:546 +#: workflows/models.py:481 workflows/models.py:532 workflows/models.py:590 +#: workflows/views.py:579 +#, fuzzy +#| msgid "Mitarbeiter" +msgid "Mitarbeitende" +msgstr "Staff" + +#: workflows/models.py:482 workflows/models.py:533 workflows/models.py:591 +#: workflows/views.py:580 +msgid "Austritt" +msgstr "" + +#: workflows/models.py:576 +msgid "Text" +msgstr "" + +#: workflows/models.py:577 +msgid "Mehrzeilig" +msgstr "" + +#: workflows/models.py:578 workflows/templates/workflows/welcome_emails.html:80 +msgid "Auswahl" +msgstr "Select" + +#: workflows/models.py:579 +msgid "Checkbox" +msgstr "" + +#: workflows/models.py:648 #, fuzzy #| msgid "Onboarding" msgid "Onboarding: IT" msgstr "Onboarding" -#: workflows/models.py:547 +#: workflows/models.py:649 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Onboarding: Allgemeine Info" msgstr "Save offboarding request" -#: workflows/models.py:548 +#: workflows/models.py:650 #, fuzzy #| msgid "Onboarding starten" msgid "Onboarding: Visitenkarte" msgstr "Start onboarding" -#: workflows/models.py:549 +#: workflows/models.py:651 #, fuzzy #| msgid "Onboarding" msgid "Onboarding: HR Works" msgstr "Onboarding" -#: workflows/models.py:550 +#: workflows/models.py:652 #, fuzzy #| msgid "Onboarding starten" msgid "Onboarding: Schlüssel" msgstr "Start onboarding" -#: workflows/models.py:551 +#: workflows/models.py:653 msgid "Onboarding: Referenz Anfordernde Person" msgstr "" -#: workflows/models.py:552 +#: workflows/models.py:654 #, fuzzy #| msgid "Welcome E-Mails" msgid "Onboarding: Welcome E-Mail" msgstr "Welcome Emails" -#: workflows/models.py:553 +#: workflows/models.py:655 #, fuzzy #| msgid "Offboarding" msgid "Offboarding: IT" msgstr "Offboarding" -#: workflows/models.py:554 +#: workflows/models.py:656 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Offboarding: Allgemeine Info" msgstr "Save offboarding request" -#: workflows/models.py:555 +#: workflows/models.py:657 #, fuzzy #| msgid "Offboarding starten" msgid "Offboarding: HR Works Deaktivierung" msgstr "Start offboarding" -#: workflows/models.py:556 +#: workflows/models.py:658 msgid "Offboarding: Referenz Anfordernde Person" msgstr "" -#: workflows/models.py:592 +#: workflows/models.py:694 msgid "Immer" msgstr "" -#: workflows/models.py:593 workflows/models.py:671 +#: workflows/models.py:695 workflows/models.py:773 msgid "Enthält" msgstr "" -#: workflows/models.py:594 workflows/models.py:672 +#: workflows/models.py:696 workflows/models.py:774 msgid "Ist gleich" msgstr "" -#: workflows/models.py:595 +#: workflows/models.py:697 msgid "Ist aktiv/Ja" msgstr "" -#: workflows/models.py:596 +#: workflows/models.py:698 #, fuzzy #| msgid "inaktiv" msgid "Ist inaktiv/Nein" msgstr "inactive" -#: workflows/models.py:638 +#: workflows/models.py:740 #: workflows/templates/workflows/welcome_emails.html:100 msgid "Geplant" msgstr "Scheduled" -#: workflows/models.py:639 +#: workflows/models.py:741 #: workflows/templates/workflows/welcome_emails.html:102 msgid "Pausiert" msgstr "Paused" -#: workflows/models.py:640 +#: workflows/models.py:742 #: workflows/templates/workflows/welcome_emails.html:104 msgid "Abgebrochen" msgstr "Cancelled" -#: workflows/models.py:641 +#: workflows/models.py:743 #: workflows/templates/workflows/welcome_emails.html:106 msgid "Gesendet" msgstr "Sent" -#: workflows/models.py:664 workflows/tasks.py:627 +#: workflows/models.py:766 workflows/tasks.py:628 msgid "Geräte und Arbeitsplatz" msgstr "Devices and workplace" -#: workflows/models.py:665 workflows/tasks.py:628 +#: workflows/models.py:767 workflows/tasks.py:629 msgid "Konten und Berechtigungen" msgstr "Accounts and permissions" -#: workflows/models.py:666 workflows/tasks.py:629 +#: workflows/models.py:768 workflows/tasks.py:630 msgid "Software und Tools" msgstr "Software and tools" -#: workflows/models.py:667 workflows/tasks.py:630 +#: workflows/models.py:769 workflows/tasks.py:631 msgid "Prozesse und Hinweise" msgstr "Processes and notes" -#: workflows/models.py:670 +#: workflows/models.py:772 msgid "Immer anzeigen" msgstr "Always show" -#: workflows/models.py:673 +#: workflows/models.py:775 msgid "Ist Ja / aktiv" msgstr "Is yes / active" -#: workflows/models.py:674 +#: workflows/models.py:776 msgid "Ist Nein / inaktiv" msgstr "Is no / inactive" -#: workflows/models.py:701 +#: workflows/models.py:803 msgid "Entwurf" msgstr "Draft" -#: workflows/models.py:721 +#: workflows/models.py:823 #, fuzzy #| msgid "Nextcloud:" msgid "Nextcloud" msgstr "Nextcloud:" -#: workflows/models.py:722 +#: workflows/models.py:824 msgid "S3" msgstr "" -#: workflows/models.py:723 +#: workflows/models.py:825 msgid "NFS" msgstr "" @@ -1155,137 +1173,137 @@ msgstr "IT Staff" msgid "Mitarbeiter" msgstr "Staff" -#: workflows/tasks.py:270 +#: workflows/tasks.py:271 #, fuzzy, python-format #| msgid "Welcome E-Mails" msgid "Welcome E-Mail gesendet: %(name)s" msgstr "Welcome Emails" -#: workflows/tasks.py:272 +#: workflows/tasks.py:273 #, fuzzy, python-format #| msgid "Fehlgeschlagen" msgid "Welcome E-Mail fehlgeschlagen: %(name)s" msgstr "Failed" -#: workflows/tasks.py:643 +#: workflows/tasks.py:644 #, python-format msgid "%(item)s übergeben und Grundfunktionen erklärt" msgstr "%(item)s handed over and basic functions explained" -#: workflows/tasks.py:645 +#: workflows/tasks.py:646 #, python-format msgid "%(item)s gezeigt bzw. Nutzung erklärt" msgstr "%(item)s shown or usage explained" -#: workflows/tasks.py:647 +#: workflows/tasks.py:648 #, python-format msgid "Telefonnummer / Direktwahl erklärt: %(value)s" msgstr "Phone number / direct extension explained: %(value)s" -#: workflows/tasks.py:649 +#: workflows/tasks.py:650 msgid "Arbeitsplatz, Geräte und allgemeine Nutzung besprochen" msgstr "Workplace, devices, and general usage reviewed" -#: workflows/tasks.py:651 +#: workflows/tasks.py:652 #, python-format msgid "%(item)s Zugang erklärt" msgstr "%(item)s access explained" -#: workflows/tasks.py:652 +#: workflows/tasks.py:653 #, python-format msgid "%(item)s Gruppe / Berechtigung erläutert" msgstr "%(item)s group / permission explained" -#: workflows/tasks.py:654 +#: workflows/tasks.py:655 #, python-format msgid "Dienstliche E-Mail-Adresse erläutert: %(value)s" msgstr "Work email address explained: %(value)s" -#: workflows/tasks.py:656 +#: workflows/tasks.py:657 #, python-format msgid "Gruppenpostfach erklärt: %(item)s" msgstr "Group mailbox explained: %(item)s" -#: workflows/tasks.py:658 +#: workflows/tasks.py:659 msgid "Zugänge, Konten und Anmeldelogik besprochen" msgstr "Accesses, accounts, and login logic reviewed" -#: workflows/tasks.py:660 +#: workflows/tasks.py:661 #, python-format msgid "%(item)s Einführung durchgeführt" msgstr "%(item)s introduction completed" -#: workflows/tasks.py:661 +#: workflows/tasks.py:662 #, python-format msgid "%(item)s zusätzlich besprochen" msgstr "%(item)s discussed additionally" -#: workflows/tasks.py:663 +#: workflows/tasks.py:664 msgid "Benötigte Standardsoftware und tägliche Nutzung erklärt" msgstr "Required standard software and daily usage explained" -#: workflows/tasks.py:666 +#: workflows/tasks.py:667 msgid "Passwortregeln und sicherer Umgang besprochen" msgstr "Password rules and secure handling reviewed" -#: workflows/tasks.py:667 +#: workflows/tasks.py:668 msgid "Dateiablage, Nextcloud und Freigaben erklärt" msgstr "File storage, Nextcloud, and sharing explained" -#: workflows/tasks.py:668 +#: workflows/tasks.py:669 msgid "Kommunikationswege und Support-Prozess erklärt" msgstr "Communication channels and support process explained" -#: workflows/tasks.py:671 +#: workflows/tasks.py:672 #, python-format msgid "%(item)s als zusätzliche Ausstattung besprochen" msgstr "%(item)s discussed as additional equipment" -#: workflows/tasks.py:673 +#: workflows/tasks.py:674 #, python-format msgid "Zusätzlicher Zugang besprochen: %(item)s" msgstr "Additional access discussed: %(item)s" -#: workflows/tasks.py:675 +#: workflows/tasks.py:676 #, python-format msgid "Übergabe-/Nachfolgekontext besprochen: %(value)s" msgstr "Handover / successor context reviewed: %(value)s" -#: workflows/tasks.py:1363 +#: workflows/tasks.py:1367 #, fuzzy, python-format #| msgid "Einweisung wurde als abgeschlossen gespeichert." msgid "Onboarding abgeschlossen: %(name)s" msgstr "Introduction was saved as completed." -#: workflows/tasks.py:1364 +#: workflows/tasks.py:1368 msgid "Die Onboarding-Anfrage wurde erfolgreich verarbeitet." msgstr "" -#: workflows/tasks.py:1375 +#: workflows/tasks.py:1379 #, fuzzy, python-format #| msgid "Fehlgeschlagen" msgid "Onboarding fehlgeschlagen: %(name)s" msgstr "Failed" -#: workflows/tasks.py:1464 +#: workflows/tasks.py:1468 #, fuzzy, python-format #| msgid "Offboarding-Anfrage speichern" msgid "Offboarding abgeschlossen: %(name)s" msgstr "Save offboarding request" -#: workflows/tasks.py:1465 +#: workflows/tasks.py:1469 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Die Offboarding-Anfrage wurde erfolgreich verarbeitet." msgstr "Save offboarding request" -#: workflows/tasks.py:1476 +#: workflows/tasks.py:1480 #, fuzzy, python-format #| msgid "Offboarding-Anfrage speichern" msgid "Offboarding fehlgeschlagen: %(name)s" msgstr "Save offboarding request" -#: workflows/tasks.py:1551 +#: workflows/tasks.py:1555 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Die geplante Welcome E-Mail wurde erfolgreich versendet." @@ -1525,7 +1543,9 @@ 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:308 +#: workflows/templates/workflows/form_builder.html:275 +#: workflows/templates/workflows/form_builder.html:376 +#: workflows/templates/workflows/form_builder.html:507 #: workflows/templates/workflows/integrations_setup.html:263 #: workflows/templates/workflows/intro_builder.html:65 #: workflows/templates/workflows/trial_management.html:28 @@ -1714,7 +1734,9 @@ msgstr "Last updated" #: workflows/templates/workflows/app_registry.html:4 #: workflows/templates/workflows/app_registry.html:103 -#: workflows/templates/workflows/form_builder.html:304 +#: workflows/templates/workflows/form_builder.html:372 +#: workflows/templates/workflows/form_builder.html:482 +#: workflows/templates/workflows/form_builder.html:503 #: workflows/templates/workflows/intro_builder.html:58 msgid "Sortierung" msgstr "Sort order" @@ -1746,8 +1768,8 @@ msgstr "Search by name or email" #: workflows/templates/workflows/app_registry.html:34 #: workflows/templates/workflows/app_registry.html:43 #: workflows/templates/workflows/audit_log.html:25 -#: workflows/templates/workflows/job_monitor.html:22 -#: workflows/templates/workflows/job_monitor.html:31 +#: workflows/templates/workflows/job_monitor.html:61 +#: workflows/templates/workflows/job_monitor.html:70 #: workflows/templates/workflows/requests_dashboard.html:130 #: workflows/templates/workflows/requests_dashboard.html:138 #: workflows/templates/workflows/requests_dashboard.html:147 @@ -1952,7 +1974,7 @@ msgid "Bis Datum" msgstr "" #: workflows/templates/workflows/audit_log.html:44 -#: workflows/templates/workflows/job_monitor.html:38 +#: workflows/templates/workflows/job_monitor.html:77 msgid "Filtern" msgstr "" @@ -1967,6 +1989,7 @@ msgid "Zeit" msgstr "" #: workflows/templates/workflows/audit_log.html:55 +#: workflows/templates/workflows/form_builder.html:502 #: workflows/templates/workflows/request_timeline.html:62 #: workflows/templates/workflows/requests_dashboard.html:128 #: workflows/templates/workflows/requests_dashboard.html:188 @@ -1974,7 +1997,8 @@ msgid "Typ" msgstr "Type" #: workflows/templates/workflows/audit_log.html:56 -#: workflows/templates/workflows/job_monitor.html:51 +#: workflows/templates/workflows/job_monitor.html:38 +#: workflows/templates/workflows/job_monitor.html:90 msgid "Ziel" msgstr "" @@ -2080,6 +2104,7 @@ msgid "" msgstr "Create database and media backups and verify existing bundles safely." #: workflows/templates/workflows/backup_recovery.html:20 +#: workflows/templates/workflows/home.html:65 #, fuzzy #| msgid "Trial-Status" msgid "Backup-Status" @@ -2201,8 +2226,10 @@ msgid "Backup-Bundle wirklich löschen?" msgstr "Delete this backup bundle?" #: workflows/templates/workflows/backup_recovery.html:133 -#: workflows/templates/workflows/form_builder.html:309 -#: workflows/templates/workflows/form_builder.html:324 +#: workflows/templates/workflows/form_builder.html:377 +#: workflows/templates/workflows/form_builder.html:392 +#: workflows/templates/workflows/form_builder.html:508 +#: workflows/templates/workflows/form_builder.html:550 #: workflows/templates/workflows/integrations_setup.html:265 #: workflows/templates/workflows/intro_builder.html:66 #: workflows/templates/workflows/intro_builder.html:102 @@ -2352,7 +2379,7 @@ msgid "Regeln" msgstr "Rule name" #: workflows/templates/workflows/form_builder.html:38 -#: workflows/templates/workflows/form_builder.html:257 +#: workflows/templates/workflows/form_builder.html:325 #, fuzzy #| msgid "Optionen verwalten" msgid "Optionen & Texte" @@ -2372,212 +2399,333 @@ msgstr "" msgid "Aktuell ausgeblendet" msgstr "Hidden" -#: workflows/templates/workflows/form_builder.html:56 +#: workflows/templates/workflows/form_builder.html:55 +#: workflows/templates/workflows/form_builder.html:462 +msgid "Eigene Felder" +msgstr "" + +#: workflows/templates/workflows/form_builder.html:60 #, fuzzy #| msgid "Abschnitt" msgid "Versteckte Abschnitte" msgstr "Section" -#: workflows/templates/workflows/form_builder.html:63 -msgid "Presets" +#: workflows/templates/workflows/form_builder.html:70 +msgid "Vorlage anwenden" msgstr "" -#: workflows/templates/workflows/form_builder.html:80 +#: workflows/templates/workflows/form_builder.html:76 +msgid "Anwenden" +msgstr "" + +#: workflows/templates/workflows/form_builder.html:84 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 +#: workflows/templates/workflows/form_builder.html:95 +#: workflows/templates/workflows/form_builder.html:126 +#: workflows/templates/workflows/form_builder.html:207 #, 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 +#: workflows/templates/workflows/form_builder.html:101 msgid "Keine sichtbaren Felder." msgstr "" -#: workflows/templates/workflows/form_builder.html:110 +#: workflows/templates/workflows/form_builder.html:114 #, fuzzy #| msgid "Reihenfolge speichern" msgid "Struktur & Reihenfolge" msgstr "Save order" -#: workflows/templates/workflows/form_builder.html:112 +#: workflows/templates/workflows/form_builder.html:116 #, 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 +#: workflows/templates/workflows/form_builder.html:136 +#: workflows/templates/workflows/form_builder.html:182 +#: workflows/templates/workflows/form_builder.html:231 msgid "Fix" msgstr "Fixed" -#: workflows/templates/workflows/form_builder.html:133 -#: workflows/templates/workflows/form_builder.html:180 -#: workflows/templates/workflows/form_builder.html:229 +#: workflows/templates/workflows/form_builder.html:137 +#: workflows/templates/workflows/form_builder.html:184 +#: workflows/templates/workflows/form_builder.html:233 msgid "Ausgeblendet" msgstr "Hidden" -#: 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 +#: workflows/templates/workflows/form_builder.html:138 +#: workflows/templates/workflows/form_builder.html:222 +#: workflows/templates/workflows/form_builder.html:225 +#: workflows/templates/workflows/form_builder.html:235 +#: workflows/templates/workflows/form_builder.html:484 +#: workflows/templates/workflows/form_builder.html:506 msgid "Pflicht" msgstr "Required" -#: workflows/templates/workflows/form_builder.html:149 +#: workflows/templates/workflows/form_builder.html:153 #, fuzzy #| msgid "Sicherheitsregeln" msgid "Sichtbarkeit & Regeln" msgstr "Safety rules" -#: workflows/templates/workflows/form_builder.html:159 +#: workflows/templates/workflows/form_builder.html:163 #, fuzzy #| msgid "Abschnitt" msgid "Abschnitte steuern" msgstr "Section" -#: workflows/templates/workflows/form_builder.html:168 +#: workflows/templates/workflows/form_builder.html:172 #, 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 +#: workflows/templates/workflows/form_builder.html:183 +#: workflows/templates/workflows/form_builder.html:218 msgid "Sichtbar" msgstr "" -#: workflows/templates/workflows/form_builder.html:187 +#: workflows/templates/workflows/form_builder.html:191 #, fuzzy #| msgid "Regeln speichern" msgid "Abschnittsregeln speichern" msgstr "Save rules" -#: workflows/templates/workflows/form_builder.html:194 +#: workflows/templates/workflows/form_builder.html:198 #, fuzzy #| msgid "Feldtexte verwalten" msgid "Feldregeln verwalten" msgstr "Manage field text" -#: workflows/templates/workflows/form_builder.html:220 +#: workflows/templates/workflows/form_builder.html:224 #, fuzzy #| msgid "Standardsprache" msgid "Standard" msgstr "Default language" -#: workflows/templates/workflows/form_builder.html:222 +#: workflows/templates/workflows/form_builder.html:226 #: workflows/templates/workflows/user_management.html:109 msgid "Optional" msgstr "Optional" -#: workflows/templates/workflows/form_builder.html:233 +#: workflows/templates/workflows/form_builder.html:237 msgid "Flexibel" msgstr "" -#: workflows/templates/workflows/form_builder.html:238 +#: workflows/templates/workflows/form_builder.html:242 #, fuzzy #| msgid "Keine Feldkonfigurationen verfügbar." msgid "Keine Feldregeln verfügbar." msgstr "No field configurations available." -#: workflows/templates/workflows/form_builder.html:245 +#: workflows/templates/workflows/form_builder.html:249 #, 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:257 +msgid "Bedingte Logik" +msgstr "" -#: workflows/templates/workflows/form_builder.html:279 -msgid "Kategorie" -msgstr "Category" +#: workflows/templates/workflows/form_builder.html:282 +#, python-format +msgid "Bedingung %(number)s" +msgstr "" -#: 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:293 -msgid "Label (EN, optional)" -msgstr "Label (EN, optional)" - -#: workflows/templates/workflows/form_builder.html:294 -msgid "Technischer Wert (optional)" -msgstr "Technical value (optional)" - -#: workflows/templates/workflows/form_builder.html:295 -msgid "Option hinzufügen" -msgstr "Add option" - -#: 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:317 -msgid "Ziehen zum Sortieren" -msgstr "Drag to reorder" - -#: workflows/templates/workflows/form_builder.html:324 -msgid "Option wirklich löschen?" -msgstr "Delete this option?" - -#: 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:334 -msgid "Optionen speichern" -msgstr "Save options" - -#: workflows/templates/workflows/form_builder.html:343 -msgid "Feldtexte verwalten" -msgstr "Manage field text" - -#: workflows/templates/workflows/form_builder.html:354 +#: workflows/templates/workflows/form_builder.html:285 +#: workflows/templates/workflows/form_builder.html:422 msgid "Feld" msgstr "Field" -#: workflows/templates/workflows/form_builder.html:357 +#: workflows/templates/workflows/form_builder.html:287 +msgid "Keine" +msgstr "" + +#: workflows/templates/workflows/form_builder.html:294 +#: workflows/templates/workflows/integrations_setup.html:221 +#: workflows/templates/workflows/integrations_setup.html:292 +#: workflows/templates/workflows/intro_builder.html:63 +msgid "Operator" +msgstr "Operator" + +#: workflows/templates/workflows/form_builder.html:302 +#: workflows/templates/workflows/intro_builder.html:64 +msgid "Wert" +msgstr "Value" + +#: workflows/templates/workflows/form_builder.html:303 +msgid "wird ignoriert" +msgstr "" + +#: workflows/templates/workflows/form_builder.html:312 +#, fuzzy +#| msgid "Branding speichern" +msgid "Bedingte Logik speichern" +msgstr "Save branding" + +#: workflows/templates/workflows/form_builder.html:336 +msgid "Optionen verwalten" +msgstr "Manage options" + +#: workflows/templates/workflows/form_builder.html:347 +msgid "Kategorie" +msgstr "Category" + +#: workflows/templates/workflows/form_builder.html:360 +#: workflows/templates/workflows/form_builder.html:373 +#: workflows/templates/workflows/form_builder.html:423 +#: workflows/templates/workflows/form_builder.html:470 +#: workflows/templates/workflows/form_builder.html:504 +msgid "Label (DE)" +msgstr "Label (DE)" + +#: workflows/templates/workflows/form_builder.html:361 +#: workflows/templates/workflows/form_builder.html:471 +msgid "Label (EN, optional)" +msgstr "Label (EN, optional)" + +#: workflows/templates/workflows/form_builder.html:362 +msgid "Technischer Wert (optional)" +msgstr "Technical value (optional)" + +#: workflows/templates/workflows/form_builder.html:363 +msgid "Option hinzufügen" +msgstr "Add option" + +#: workflows/templates/workflows/form_builder.html:374 +#: workflows/templates/workflows/form_builder.html:424 +#: workflows/templates/workflows/form_builder.html:505 +msgid "Label (EN)" +msgstr "Label (EN)" + +#: workflows/templates/workflows/form_builder.html:385 +msgid "Ziehen zum Sortieren" +msgstr "Drag to reorder" + +#: workflows/templates/workflows/form_builder.html:392 +msgid "Option wirklich löschen?" +msgstr "Delete this option?" + +#: workflows/templates/workflows/form_builder.html:396 +msgid "Keine Optionen in dieser Kategorie." +msgstr "No options in this category." + +#: workflows/templates/workflows/form_builder.html:402 +msgid "Optionen speichern" +msgstr "Save options" + +#: workflows/templates/workflows/form_builder.html:411 +msgid "Feldtexte verwalten" +msgstr "Manage field text" + +#: workflows/templates/workflows/form_builder.html:425 +#: workflows/templates/workflows/form_builder.html:539 msgid "Hilfetext (DE)" msgstr "Help text (DE)" -#: workflows/templates/workflows/form_builder.html:358 +#: workflows/templates/workflows/form_builder.html:426 +#: workflows/templates/workflows/form_builder.html:544 msgid "Hilfetext (EN)" msgstr "Help text (EN)" -#: workflows/templates/workflows/form_builder.html:372 +#: workflows/templates/workflows/form_builder.html:440 msgid "Fallback: Standardlabel" msgstr "Fallback: default label" -#: workflows/templates/workflows/form_builder.html:373 +#: workflows/templates/workflows/form_builder.html:441 msgid "English label" msgstr "English label" -#: workflows/templates/workflows/form_builder.html:374 +#: workflows/templates/workflows/form_builder.html:442 msgid "Optionaler Hilfetext" msgstr "Optional help text" -#: workflows/templates/workflows/form_builder.html:375 +#: workflows/templates/workflows/form_builder.html:443 msgid "Optional English help text" msgstr "Optional English help text" -#: workflows/templates/workflows/form_builder.html:378 +#: workflows/templates/workflows/form_builder.html:446 msgid "Keine Feldkonfigurationen verfügbar." msgstr "No field configurations available." -#: workflows/templates/workflows/form_builder.html:385 +#: workflows/templates/workflows/form_builder.html:453 msgid "Feldtexte speichern" msgstr "Save field text" +#: workflows/templates/workflows/form_builder.html:487 +#, fuzzy +#| msgid "Hilfetext (DE)" +msgid "Hilfetext (DE, optional)" +msgstr "Help text (DE)" + +#: workflows/templates/workflows/form_builder.html:488 +#, fuzzy +#| msgid "Hilfetext (EN)" +msgid "Hilfetext (EN, optional)" +msgstr "Help text (EN)" + +#: workflows/templates/workflows/form_builder.html:489 +msgid "Optionen (eine pro Zeile, optional: wert|Label)" +msgstr "" + +#: workflows/templates/workflows/form_builder.html:490 +msgid "Optionen EN (eine pro Zeile, optional: value|Label)" +msgstr "" + +#: workflows/templates/workflows/form_builder.html:491 +#, fuzzy +#| msgid "Neue Regel hinzufügen" +msgid "Eigenes Feld hinzufügen" +msgstr "Add new rule" + +#: workflows/templates/workflows/form_builder.html:500 +#, fuzzy +#| msgid "Onboarding starten" +msgid "Schlüssel" +msgstr "Start onboarding" + +#: workflows/templates/workflows/form_builder.html:501 +#: workflows/templates/workflows/intro_builder.html:29 +#: workflows/templates/workflows/intro_builder.html:59 +msgid "Abschnitt" +msgstr "Section" + +#: workflows/templates/workflows/form_builder.html:540 +#, fuzzy +#| msgid "Aktion" +msgid "Optionen (DE)" +msgstr "Action" + +#: workflows/templates/workflows/form_builder.html:545 +#, fuzzy +#| msgid "Aktion" +msgid "Optionen (EN)" +msgstr "Action" + +#: workflows/templates/workflows/form_builder.html:550 +#, fuzzy +#| msgid "Option wirklich löschen?" +msgid "Eigenes Feld wirklich löschen?" +msgstr "Delete this option?" + +#: workflows/templates/workflows/form_builder.html:554 +#, fuzzy +#| msgid "Keine geplanten Welcome E-Mails vorhanden." +msgid "Keine eigenen Felder vorhanden." +msgstr "No scheduled welcome emails available." + +#: workflows/templates/workflows/form_builder.html:561 +#, fuzzy +#| msgid "Feldtexte speichern" +msgid "Eigene Felder speichern" +msgstr "Save field text" + #: workflows/templates/workflows/handbook.html:17 msgid "" "Single documentation entry point for both operational knowledge and long-" @@ -2745,7 +2893,48 @@ msgstr "Production" msgid "PDF + E-Mail Workflow bereit" msgstr "PDF + Email Workflow Ready" -#: workflows/templates/workflows/home.html:80 +#: workflows/templates/workflows/home.html:44 +#, fuzzy +#| msgid "Operations" +msgid "Operations Overview" +msgstr "Operations" + +#: workflows/templates/workflows/home.html:45 +msgid "Letzte Laufzeit- und Backup-Signale auf einen Blick." +msgstr "" + +#: workflows/templates/workflows/home.html:51 +#: workflows/templates/workflows/job_monitor.html:20 +#, fuzzy +#| msgid "Fehlgeschlagen" +msgid "Fehlgeschlagene Jobs (24h)" +msgstr "Failed" + +#: workflows/templates/workflows/home.html:55 +#: workflows/templates/workflows/job_monitor.html:24 +#, fuzzy +#| msgid "Eingereicht" +msgid "Erfolgreiche Jobs (24h)" +msgstr "Submitted" + +#: workflows/templates/workflows/home.html:59 +#: workflows/templates/workflows/job_monitor.html:28 +msgid "Offene Starts (24h)" +msgstr "" + +#: workflows/templates/workflows/home.html:74 +#, fuzzy +#| msgid "Letzte Anmeldung" +msgid "Letzte Fehler" +msgstr "Last login" + +#: workflows/templates/workflows/home.html:75 +#, fuzzy +#| msgid "Dashboard öffnen" +msgid "Job Monitor öffnen" +msgstr "Open dashboard" + +#: workflows/templates/workflows/home.html:131 msgid "Tipp: Die letzten Vorgänge sehen Sie jederzeit im Anfragen Dashboard." msgstr "Tip: You can always see the latest requests in the Requests Dashboard." @@ -2947,12 +3136,6 @@ msgstr "Event" msgid "Feldname" msgstr "Field name" -#: workflows/templates/workflows/integrations_setup.html:221 -#: workflows/templates/workflows/integrations_setup.html:292 -#: workflows/templates/workflows/intro_builder.html:63 -msgid "Operator" -msgstr "Operator" - #: workflows/templates/workflows/integrations_setup.html:229 #: workflows/templates/workflows/integrations_setup.html:300 msgid "Vergleichswert" @@ -3075,11 +3258,6 @@ msgstr "" msgid "Checklistenpunkte für das Einweisungs- und Übergabeprotokoll verwalten." msgstr "Manage checklist items for the introduction and handover protocol." -#: workflows/templates/workflows/intro_builder.html:29 -#: workflows/templates/workflows/intro_builder.html:59 -msgid "Abschnitt" -msgstr "Section" - #: workflows/templates/workflows/intro_builder.html:37 #: workflows/templates/workflows/intro_builder.html:60 msgid "Checklistenpunkt (DE)" @@ -3115,10 +3293,6 @@ msgstr "Checklist item (EN)" msgid "Feld-Bedingung" msgstr "Field condition" -#: workflows/templates/workflows/intro_builder.html:64 -msgid "Wert" -msgstr "Value" - #: workflows/templates/workflows/intro_builder.html:99 msgid "z. B. HR Works" msgstr "e.g. HR Works" @@ -3147,20 +3321,26 @@ msgstr "Save checklist" msgid "Asynchrone Aufgaben, Fehler und letzte Worker-Läufe zentral prüfen." msgstr "" -#: workflows/templates/workflows/job_monitor.html:20 -#: workflows/templates/workflows/job_monitor.html:49 +#: workflows/templates/workflows/job_monitor.html:37 +#, fuzzy +#| msgid "Fehlgeschlagen" +msgid "Zuletzt fehlgeschlagen" +msgstr "Failed" + +#: workflows/templates/workflows/job_monitor.html:59 +#: workflows/templates/workflows/job_monitor.html:88 msgid "Task" msgstr "" -#: workflows/templates/workflows/job_monitor.html:48 +#: workflows/templates/workflows/job_monitor.html:87 msgid "Start" msgstr "" -#: workflows/templates/workflows/job_monitor.html:52 +#: workflows/templates/workflows/job_monitor.html:91 msgid "Task ID" msgstr "" -#: workflows/templates/workflows/job_monitor.html:67 +#: workflows/templates/workflows/job_monitor.html:106 #, fuzzy #| msgid "Noch keine Vorgänge vorhanden." msgid "Noch keine Task-Läufe vorhanden." @@ -3209,7 +3389,7 @@ msgstr "Search" msgid "Vorbefüllt aus:" msgstr "Prefilled from:" -#: workflows/templates/workflows/offboarding_form.html:74 +#: workflows/templates/workflows/offboarding_form.html:82 msgid "Offboarding-Anfrage speichern" msgstr "Save offboarding request" @@ -3286,42 +3466,42 @@ msgid "" "Bitte prüfen Sie die markierten Felder. Ungültige Eingaben wurden erkannt." msgstr "Please check the highlighted fields. Invalid input was detected." -#: workflows/templates/workflows/onboarding_form.html:75 -#: workflows/templates/workflows/onboarding_form.html:77 -#: workflows/templates/workflows/onboarding_form.html:114 -#: workflows/templates/workflows/onboarding_form.html:116 +#: workflows/templates/workflows/onboarding_form.html:76 +#: workflows/templates/workflows/onboarding_form.html:78 +#: workflows/templates/workflows/onboarding_form.html:119 +#: workflows/templates/workflows/onboarding_form.html:121 #: workflows/templates/workflows/welcome_emails.html:65 msgid "Alle auswählen" msgstr "Select all" -#: workflows/templates/workflows/onboarding_form.html:76 -#: workflows/templates/workflows/onboarding_form.html:115 +#: workflows/templates/workflows/onboarding_form.html:77 +#: workflows/templates/workflows/onboarding_form.html:120 #, fuzzy #| msgid "Auswahl löschen" msgid "Auswahl aufheben" msgstr "Delete selection" -#: workflows/templates/workflows/onboarding_form.html:138 +#: workflows/templates/workflows/onboarding_form.html:143 msgid "Keine konfigurierten Felder in diesem Schritt." msgstr "No configured fields in this step." -#: workflows/templates/workflows/onboarding_form.html:143 +#: workflows/templates/workflows/onboarding_form.html:148 msgid "Fast geschafft. Bitte Abschlussdaten prüfen und die Anfrage absenden." msgstr "Almost done. Please review the final details and submit the request." -#: workflows/templates/workflows/onboarding_form.html:155 +#: workflows/templates/workflows/onboarding_form.html:160 msgid "Zurück" msgstr "Back" -#: workflows/templates/workflows/onboarding_form.html:156 +#: workflows/templates/workflows/onboarding_form.html:161 msgid "Weiter" msgstr "Next" -#: workflows/templates/workflows/onboarding_form.html:157 +#: workflows/templates/workflows/onboarding_form.html:162 msgid "Wird gesendet..." msgstr "" -#: workflows/templates/workflows/onboarding_form.html:157 +#: workflows/templates/workflows/onboarding_form.html:162 msgid "Onboarding-Anfrage absenden" msgstr "Submit onboarding request" @@ -3351,7 +3531,7 @@ msgid "Dienstliche E-Mail" msgstr "Work email" #: workflows/templates/workflows/onboarding_intro_session.html:31 -#: workflows/views.py:1483 +#: workflows/views.py:1544 msgid "Vertragsbeginn" msgstr "Contract start" @@ -4131,10 +4311,6 @@ msgstr "Send now" msgid "Bulk ausführen" msgstr "Run bulk action" -#: workflows/templates/workflows/welcome_emails.html:80 -msgid "Auswahl" -msgstr "Select" - #: workflows/templates/workflows/welcome_emails.html:84 msgid "Geplant für" msgstr "Scheduled for" @@ -4151,350 +4327,424 @@ msgstr "Resume" msgid "Keine geplanten Welcome E-Mails vorhanden." msgstr "No scheduled welcome emails available." -#: workflows/views.py:128 +#: workflows/upload_validation.py:75 +msgid "Bitte ein PNG-, JPG-, WEBP- oder SVG-Bild hochladen." +msgstr "" + +#: workflows/upload_validation.py:76 +msgid "Das Profilbild darf maximal 5 MB groß sein." +msgstr "" + +#: workflows/upload_validation.py:77 +#, fuzzy +#| msgid "Passwort konnte nicht gespeichert werden" +msgid "Die Bilddatei konnte nicht gelesen werden." +msgstr "Password could not be saved" + +#: workflows/upload_validation.py:95 +msgid "Bitte ein SVG-, PNG-, JPG- oder WEBP-Bild hochladen." +msgstr "" + +#: workflows/upload_validation.py:96 +msgid "Das Logo darf maximal 5 MB groß sein." +msgstr "" + +#: workflows/upload_validation.py:97 +#, fuzzy +#| msgid "Passwort konnte nicht gespeichert werden" +msgid "Die Logo-Datei konnte nicht gelesen werden." +msgstr "Password could not be saved" + +#: workflows/upload_validation.py:114 +msgid "Bitte eine ICO-, PNG-, SVG- oder WEBP-Datei hochladen." +msgstr "" + +#: workflows/upload_validation.py:115 +msgid "Das Favicon darf maximal 2 MB groß sein." +msgstr "" + +#: workflows/upload_validation.py:116 +#, fuzzy +#| msgid "Passwort konnte nicht gespeichert werden" +msgid "Die Favicon-Datei konnte nicht gelesen werden." +msgstr "Password could not be saved" + +#: workflows/upload_validation.py:126 +msgid "Bitte eine gültige PDF-Datei hochladen." +msgstr "" + +#: workflows/upload_validation.py:127 +msgid "Der PDF-Briefkopf darf maximal 10 MB groß sein." +msgstr "" + +#: workflows/upload_validation.py:128 +#, fuzzy +#| msgid "Passwort konnte nicht gespeichert werden" +msgid "Die PDF-Datei konnte nicht gelesen werden." +msgstr "Password could not be saved" + +#: workflows/upload_validation.py:144 +msgid "Bitte eine PNG- oder JPG-Datei hochladen." +msgstr "" + +#: workflows/upload_validation.py:145 +msgid "Die Signatur-Datei ist zu groß (max. 4 MB)." +msgstr "" + +#: workflows/upload_validation.py:146 +#, fuzzy +#| msgid "Passwort konnte nicht gespeichert werden" +msgid "Die Signatur-Datei konnte nicht gelesen werden." +msgstr "Password could not be saved" + +#: workflows/views.py:125 msgid "Person, Rolle, Abteilung" msgstr "Person, role, department" -#: workflows/views.py:129 +#: workflows/views.py:126 msgid "Beschäftigung und Termine" msgstr "Employment and dates" -#: workflows/views.py:130 +#: workflows/views.py:127 msgid "Geräte, Software und Zugänge" msgstr "Devices, software, and access" -#: workflows/views.py:131 +#: workflows/views.py:128 msgid "Notizen und Freigabe" msgstr "Notes and approval" -#: workflows/views.py:264 +#: workflows/views.py:132 +#, fuzzy +#| msgid "Deaktivieren" +msgid "ist aktiviert" +msgstr "Disabled" + +#: workflows/views.py:133 +msgid "ist gleich" +msgstr "" + +#: workflows/views.py:134 +msgid "ist nicht gleich" +msgstr "" + +#: workflows/views.py:279 #, fuzzy #| msgid "Lokal gespeichert" msgid "Profilbild gespeichert." msgstr "Stored locally" -#: workflows/views.py:266 +#: workflows/views.py:281 #, fuzzy #| msgid "Passwort konnte nicht gespeichert werden" msgid "Profilbild konnte nicht gespeichert werden." msgstr "Password could not be saved" -#: workflows/views.py:272 +#: workflows/views.py:287 #, fuzzy #| msgid "Lokal gespeichert" msgid "Profildaten gespeichert." msgstr "Stored locally" -#: workflows/views.py:274 +#: workflows/views.py:289 #, fuzzy #| msgid "Passwort konnte nicht gespeichert werden" msgid "Profildaten konnten nicht gespeichert werden." msgstr "Password could not be saved" -#: workflows/views.py:280 +#: workflows/views.py:295 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Benachrichtigungseinstellungen gespeichert." msgstr "Save offboarding request" -#: workflows/views.py:282 +#: workflows/views.py:297 #, fuzzy #| msgid "Passwort konnte nicht gespeichert werden" msgid "Benachrichtigungseinstellungen konnten nicht gespeichert werden." msgstr "Password could not be saved" -#: workflows/views.py:291 +#: workflows/views.py:306 #, fuzzy #| msgid "Deaktivieren" msgid "TOTP wurde aktiviert." msgstr "Disabled" -#: workflows/views.py:293 +#: workflows/views.py:308 #, fuzzy #| msgid "Passwort konnte nicht gespeichert werden" msgid "TOTP konnte nicht aktiviert werden." msgstr "Password could not be saved" -#: workflows/views.py:300 +#: workflows/views.py:315 msgid "TOTP wurde deaktiviert." msgstr "" -#: workflows/views.py:302 +#: workflows/views.py:317 #, fuzzy #| msgid "Passwort konnte nicht gespeichert werden" msgid "TOTP konnte nicht deaktiviert werden." msgstr "Password could not be saved" -#: workflows/views.py:311 +#: workflows/views.py:326 msgid "Recovery-Codes wurden neu erzeugt." msgstr "" -#: workflows/views.py:313 +#: workflows/views.py:328 #, fuzzy #| msgid "Passwort konnte nicht gespeichert werden" msgid "Recovery-Codes konnten nicht neu erzeugt werden." msgstr "Password could not be saved" -#: workflows/views.py:362 workflows/views.py:1569 workflows/views.py:1574 +#: workflows/views.py:377 workflows/views.py:1630 workflows/views.py:1635 msgid "Sie haben keine Berechtigung für diese Aktion." msgstr "You do not have permission for this action." -#: workflows/views.py:443 +#: workflows/views.py:458 #, fuzzy #| msgid "Vorgänge" msgid "Vorgänge gelöscht" msgstr "Requests" -#: workflows/views.py:444 +#: workflows/views.py:459 msgid "Vorgang gelöscht" msgstr "" -#: workflows/views.py:445 +#: workflows/views.py:460 msgid "Vorgang erneut angestoßen" msgstr "" -#: workflows/views.py:446 +#: workflows/views.py:461 #, fuzzy #| msgid "Einweisung" msgid "Einweisungs-PDF erzeugt" msgstr "Introduction" -#: workflows/views.py:447 +#: workflows/views.py:462 #, fuzzy #| msgid "Live-Protokoll erzeugen" msgid "Live-Protokoll erzeugt" msgstr "Generate live protocol" -#: workflows/views.py:448 +#: workflows/views.py:463 #, fuzzy #| msgid "Einweisung wurde zurückgesetzt." msgid "Einweisung zurückgesetzt" msgstr "Introduction was reset." -#: workflows/views.py:449 +#: workflows/views.py:464 #, fuzzy #| msgid "Einweisung wurde als Entwurf gespeichert." msgid "Einweisung als Entwurf gespeichert" msgstr "Introduction was saved as draft." -#: workflows/views.py:450 +#: workflows/views.py:465 #, fuzzy #| msgid "Einweisung wurde als abgeschlossen gespeichert." msgid "Einweisung abgeschlossen" msgstr "Introduction was saved as completed." -#: workflows/views.py:451 +#: workflows/views.py:466 msgid "Formularoption gelöscht" msgstr "" -#: workflows/views.py:452 +#: workflows/views.py:467 #, fuzzy #| msgid "Optionen speichern" msgid "Formularoptionen gespeichert" msgstr "Save options" -#: workflows/views.py:453 +#: workflows/views.py:468 #, fuzzy #| msgid "Feldtexte speichern" msgid "Feldtexte gespeichert" msgstr "Save field text" -#: workflows/views.py:454 +#: workflows/views.py:469 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Formularlayout gespeichert" msgstr "Save offboarding request" -#: workflows/views.py:455 +#: workflows/views.py:470 msgid "Einweisungs-Checkpunkt gelöscht" msgstr "" -#: workflows/views.py:456 +#: workflows/views.py:471 msgid "Einweisungs-Checkpunkt hinzugefügt" msgstr "" -#: workflows/views.py:457 +#: workflows/views.py:472 #, fuzzy #| msgid "Checkliste speichern" msgid "Einweisungs-Checkliste gespeichert" msgstr "Save checklist" -#: workflows/views.py:458 +#: workflows/views.py:473 #, fuzzy #| msgid "Welcome E-Mails" msgid "Welcome E-Mail sofort ausgelöst" msgstr "Welcome Emails" -#: workflows/views.py:459 +#: workflows/views.py:474 #, fuzzy #| msgid "Welcome-Einstellungen speichern" msgid "Welcome E-Mail Einstellungen gespeichert" msgstr "Save welcome settings" -#: workflows/views.py:460 +#: workflows/views.py:475 msgid "Welcome E-Mail Sammelaktion ausgeführt" msgstr "" -#: workflows/views.py:461 +#: workflows/views.py:476 #, fuzzy #| msgid "Welcome E-Mails" msgid "Welcome E-Mail pausiert" msgstr "Welcome Emails" -#: workflows/views.py:462 +#: workflows/views.py:477 #, fuzzy #| msgid "Welcome E-Mails" msgid "Welcome E-Mail fortgesetzt" msgstr "Welcome Emails" -#: workflows/views.py:463 +#: workflows/views.py:478 #, fuzzy #| msgid "Welcome E-Mails" msgid "Welcome E-Mail abgebrochen" msgstr "Welcome Emails" -#: workflows/views.py:464 +#: workflows/views.py:479 #, fuzzy #| msgid "SMTP-Test" msgid "SMTP-Test gesendet" msgstr "SMTP test" -#: workflows/views.py:465 +#: workflows/views.py:480 #, fuzzy #| msgid "Nextcloud-Test" msgid "Nextcloud-Testupload ausgeführt" msgstr "Nextcloud test" -#: workflows/views.py:466 +#: workflows/views.py:481 #, fuzzy #| msgid "Nextcloud schalten" msgid "Nextcloud-Modus umgeschaltet" msgstr "Toggle Nextcloud" -#: workflows/views.py:467 +#: workflows/views.py:482 msgid "E-Mail-Modus umgeschaltet" msgstr "" -#: workflows/views.py:468 +#: workflows/views.py:483 #, fuzzy #| msgid "Integrationen Setup" msgid "Integrationen gespeichert" msgstr "Integrations Setup" -#: workflows/views.py:469 +#: workflows/views.py:484 #, fuzzy #| msgid "Welcome-Einstellungen speichern" msgid "Nextcloud-Einstellungen gespeichert" msgstr "Save welcome settings" -#: workflows/views.py:470 +#: workflows/views.py:485 #, fuzzy #| msgid "Welcome-Einstellungen speichern" msgid "Mail-Einstellungen gespeichert" msgstr "Save welcome settings" -#: workflows/views.py:471 +#: workflows/views.py:486 #, fuzzy #| msgid "E-Mail Routing & Vorlagen speichern" msgid "E-Mail-Routing gespeichert" msgstr "Save email routing & templates" -#: workflows/views.py:472 +#: workflows/views.py:487 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Benachrichtigungsregeln gespeichert" msgstr "Save offboarding request" -#: workflows/views.py:473 +#: workflows/views.py:488 #, fuzzy #| msgid "Anfrage gespeichert" msgid "Benutzer erstellt" msgstr "Request saved" -#: workflows/views.py:474 +#: workflows/views.py:489 msgid "Benutzer aktualisiert" msgstr "" -#: workflows/views.py:475 +#: workflows/views.py:490 msgid "Passwort-Reset-Link versendet" msgstr "" -#: workflows/views.py:476 +#: workflows/views.py:491 #, fuzzy #| msgid "Benutzerübersicht" msgid "Benutzer gelöscht" msgstr "User overview" -#: workflows/views.py:477 +#: workflows/views.py:492 #, fuzzy #| msgid "Anfrage gespeichert" msgid "Backup erstellt" msgstr "Request saved" -#: workflows/views.py:478 +#: workflows/views.py:493 msgid "Backup verifiziert" msgstr "" -#: workflows/views.py:479 +#: workflows/views.py:494 #, fuzzy #| msgid "Anfrage gespeichert" msgid "Backup gelöscht" msgstr "Request saved" -#: workflows/views.py:480 +#: workflows/views.py:495 #, fuzzy #| msgid "Welcome-Einstellungen speichern" msgid "Backup-Einstellungen gespeichert" msgstr "Save welcome settings" -#: workflows/views.py:481 +#: workflows/views.py:496 #, fuzzy #| msgid "Anfrage gespeichert" msgid "App-Registry gespeichert" msgstr "Request saved" -#: workflows/views.py:564 -#, fuzzy -#| msgid "Mitarbeiter" -msgid "Mitarbeitende" -msgstr "Staff" - -#: workflows/views.py:564 +#: workflows/views.py:579 #, 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 +#: workflows/views.py:580 msgid "Letzter Arbeitstag" msgstr "" -#: workflows/views.py:566 +#: workflows/views.py:581 #, fuzzy #| msgid "Einweisung wurde als abgeschlossen gespeichert." msgid "Hinweise und Abschlussnotizen" msgstr "Introduction was saved as completed." -#: workflows/views.py:683 +#: workflows/views.py:744 #, fuzzy #| msgid "Anfrage gespeichert" msgid "App-Registry gespeichert." msgstr "Request saved" -#: workflows/views.py:782 +#: workflows/views.py:843 msgid "Für diesen Benutzer ist keine E-Mail-Adresse hinterlegt." msgstr "" -#: workflows/views.py:791 +#: workflows/views.py:852 #, python-format msgid "Zugangseinladung für %(username)s" msgstr "" -#: workflows/views.py:793 +#: workflows/views.py:854 #, python-format msgid "" "Hallo %(name)s,\n" @@ -4507,12 +4757,12 @@ msgid "" "Ihrem Administrator." msgstr "" -#: workflows/views.py:804 +#: workflows/views.py:865 #, python-format msgid "Passwort zurücksetzen für %(username)s" msgstr "" -#: workflows/views.py:806 +#: workflows/views.py:867 #, python-format msgid "" "Hallo %(name)s,\n" @@ -4525,7 +4775,7 @@ msgid "" "ignorieren." msgstr "" -#: workflows/views.py:857 +#: workflows/views.py:918 #, fuzzy #| msgid "" #| "Benutzer konnte nicht erstellt werden. Bitte prüfen Sie die Eingaben." @@ -4533,69 +4783,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:885 +#: workflows/views.py:946 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Portal-Branding wurde gespeichert." msgstr "Save offboarding request" -#: workflows/views.py:902 +#: workflows/views.py:963 msgid "Identität" msgstr "" -#: workflows/views.py:903 +#: workflows/views.py:964 msgid "Titel, Firmenname und zentrale Spracheinstellungen." msgstr "" -#: workflows/views.py:907 +#: workflows/views.py:968 msgid "" "Wird für E-Mail-Vorschläge und Domain-bezogene Standardtexte verwendet, z. " "B. workdock.de." msgstr "" -#: workflows/views.py:912 +#: workflows/views.py:973 msgid "Farben & Erscheinungsbild" msgstr "" -#: workflows/views.py:913 +#: workflows/views.py:974 msgid "Zentrale visuelle Markenwerte und Browser-Icon." msgstr "" -#: workflows/views.py:917 +#: workflows/views.py:978 msgid "Erlaubte Formate: SVG, PNG, JPG, JPEG, WEBP. Maximal 5 MB." msgstr "" -#: workflows/views.py:918 +#: workflows/views.py:979 msgid "Erlaubte Formate: ICO, PNG, SVG, WEBP. Maximal 2 MB." msgstr "" -#: workflows/views.py:923 +#: workflows/views.py:984 #, fuzzy #| msgid "Produktion" msgid "Kommunikation" msgstr "Production" -#: workflows/views.py:924 +#: workflows/views.py:985 msgid "Absender, Support und PDF-Branding für ausgehende Kommunikation." msgstr "" -#: workflows/views.py:928 +#: workflows/views.py:989 msgid "Wird für ausgehende System-E-Mails als Anzeigename verwendet." msgstr "" -#: workflows/views.py:929 +#: workflows/views.py:990 msgid "Erlaubtes Format: PDF. Maximal 10 MB." msgstr "" -#: workflows/views.py:934 +#: workflows/views.py:995 msgid "Footer & Rechtliches" msgstr "" -#: workflows/views.py:935 +#: workflows/views.py:996 msgid "Gemeinsame Footer-Texte und rechtliche Hinweise für die Shell." msgstr "" -#: workflows/views.py:989 +#: workflows/views.py:1050 #, fuzzy #| msgid "" #| "Benutzer konnte nicht erstellt werden. Bitte prüfen Sie die Eingaben." @@ -4604,53 +4854,53 @@ msgid "" "Eingaben." msgstr "User could not be created. Please check the input." -#: workflows/views.py:1018 +#: workflows/views.py:1079 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Firmenkonfiguration wurde gespeichert." msgstr "Save offboarding request" -#: workflows/views.py:1035 +#: workflows/views.py:1096 #, fuzzy #| msgid "Firmenname" msgid "Firmenprofil" msgstr "Company name" -#: workflows/views.py:1036 +#: workflows/views.py:1097 msgid "Rechtlicher Name und zentrale Stammdaten der Firma." msgstr "" -#: workflows/views.py:1041 +#: workflows/views.py:1102 msgid "Adresse & Register" msgstr "" -#: workflows/views.py:1042 +#: workflows/views.py:1103 msgid "Anschrift sowie optionale Register- und Steuerangaben." msgstr "" -#: workflows/views.py:1047 +#: workflows/views.py:1108 msgid "Kontaktpunkte" msgstr "" -#: workflows/views.py:1048 +#: workflows/views.py:1109 msgid "Zentrale Ansprechpartner für HR, IT und Operations." msgstr "" -#: workflows/views.py:1053 +#: workflows/views.py:1114 msgid "Recht & Öffentlichkeit" msgstr "" -#: workflows/views.py:1054 +#: workflows/views.py:1115 msgid "Öffentliche Links für Website, Impressum und Datenschutz." msgstr "" -#: workflows/views.py:1056 +#: workflows/views.py:1117 msgid "" "Diese Links können später im Portal-Footer oder in öffentlichen Seiten " "verwendet werden." msgstr "" -#: workflows/views.py:1096 +#: workflows/views.py:1157 #, fuzzy #| msgid "" #| "Benutzer konnte nicht erstellt werden. Bitte prüfen Sie die Eingaben." @@ -4659,54 +4909,54 @@ msgid "" "Eingaben." msgstr "Trial configuration could not be saved. Please check the input." -#: workflows/views.py:1128 +#: workflows/views.py:1189 #, fuzzy #| msgid "Trial abgelaufen" msgid "Trial ist abgelaufen" msgstr "Trial expired" -#: workflows/views.py:1129 +#: workflows/views.py:1190 msgid "" "Der Trial-Zeitraum ist überschritten. Nicht-Platform-Owner werden jetzt " "blockiert." msgstr "" -#: workflows/views.py:1137 +#: workflows/views.py:1198 msgid "Trial läuft bald ab" msgstr "" -#: workflows/views.py:1138 +#: workflows/views.py:1199 #, python-format msgid "Der Trial endet am %(date)s." msgstr "" -#: workflows/views.py:1146 +#: workflows/views.py:1207 #, fuzzy #| msgid "Trial-Modus" msgid "Trial-Modus deaktiviert" msgstr "Trial mode" -#: workflows/views.py:1147 +#: workflows/views.py:1208 #, fuzzy #| msgid "Nextcloud schalten" msgid "Der Trial-Modus wurde ausgeschaltet." msgstr "Toggle Nextcloud" -#: workflows/views.py:1152 +#: workflows/views.py:1213 msgid "Trial-Konfiguration wurde gespeichert." msgstr "Trial configuration was saved." -#: workflows/views.py:1169 +#: workflows/views.py:1230 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:1182 +#: workflows/views.py:1243 #, 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:1204 +#: workflows/views.py:1265 #, fuzzy #| msgid "" #| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " @@ -4717,14 +4967,14 @@ msgid "" msgstr "" "The currently signed-in super admin cannot lock or downgrade themselves here." -#: workflows/views.py:1207 +#: workflows/views.py:1268 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:1210 +#: workflows/views.py:1271 #, fuzzy #| msgid "" #| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " @@ -4735,7 +4985,7 @@ msgid "" msgstr "" "The currently signed-in super admin cannot lock or downgrade themselves here." -#: workflows/views.py:1213 +#: workflows/views.py:1274 #, fuzzy #| msgid "" #| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " @@ -4746,18 +4996,18 @@ msgid "" msgstr "" "The currently signed-in super admin cannot lock or downgrade themselves here." -#: workflows/views.py:1230 +#: workflows/views.py:1291 #, python-format msgid "Benutzer wurde aktualisiert: %(username)s" msgstr "User updated: %(username)s" -#: workflows/views.py:1252 +#: workflows/views.py:1313 #, 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:1264 +#: workflows/views.py:1325 #, fuzzy #| msgid "" #| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " @@ -4767,7 +5017,7 @@ msgid "" msgstr "" "The currently signed-in super admin cannot lock or downgrade themselves here." -#: workflows/views.py:1267 +#: workflows/views.py:1328 #, fuzzy #| msgid "" #| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " @@ -4777,7 +5027,7 @@ msgid "" msgstr "" "The currently signed-in super admin cannot lock or downgrade themselves here." -#: workflows/views.py:1270 +#: workflows/views.py:1331 #, fuzzy #| msgid "" #| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " @@ -4786,7 +5036,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:1273 +#: workflows/views.py:1334 #, fuzzy #| msgid "" #| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " @@ -4795,189 +5045,261 @@ 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:1286 +#: workflows/views.py:1347 #, fuzzy, python-format #| msgid "Benutzer wurde erstellt: %(username)s" msgid "Benutzer wurde gelöscht: %(username)s" msgstr "User created: %(username)s" -#: workflows/views.py:1377 +#: workflows/views.py:1438 #, fuzzy, python-format #| msgid "Anfrage gespeichert" msgid "Backup erstellt: %(name)s" msgstr "Request saved" -#: workflows/views.py:1378 +#: workflows/views.py:1439 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Das Backup-Bundle wurde erfolgreich erstellt." msgstr "Save offboarding request" -#: workflows/views.py:1383 +#: workflows/views.py:1444 #, python-format msgid "Backup wurde erstellt: %(name)s" msgstr "" -#: workflows/views.py:1393 +#: workflows/views.py:1454 #, python-format msgid "Backup konnte nicht erstellt werden: %(error)s" msgstr "" -#: workflows/views.py:1411 +#: workflows/views.py:1472 #, fuzzy, python-format #| msgid "Backup wird verifiziert" msgid "Backup verifiziert: %(name)s" msgstr "Backup is being verified" -#: workflows/views.py:1412 +#: workflows/views.py:1473 #, fuzzy #| msgid "Backup wird verifiziert" msgid "Das Backup wurde erfolgreich verifiziert." msgstr "Backup is being verified" -#: workflows/views.py:1417 +#: workflows/views.py:1478 #, python-format msgid "Backup wurde verifiziert: %(name)s" msgstr "" -#: workflows/views.py:1421 +#: workflows/views.py:1482 #, fuzzy #| msgid "Fehlgeschlagen" msgid "Backup-Verifikation fehlgeschlagen" msgstr "Failed" -#: workflows/views.py:1427 +#: workflows/views.py:1488 #, python-format msgid "Backup-Verifikation fehlgeschlagen: %(error)s" msgstr "" -#: workflows/views.py:1443 +#: workflows/views.py:1504 #, python-format msgid "Backup wurde gelöscht: %(name)s" msgstr "" -#: workflows/views.py:1445 +#: workflows/views.py:1506 #, python-format msgid "Backup konnte nicht gelöscht werden: %(error)s" msgstr "" -#: workflows/views.py:1471 +#: workflows/views.py:1532 #, fuzzy #| msgid "Anfrage gespeichert" msgid "Anfrage erstellt" msgstr "Request saved" -#: workflows/views.py:1473 +#: workflows/views.py:1534 #, fuzzy, python-format #| msgid "Sitzungsstatus" msgid "Status: %(status)s" msgstr "Session status" -#: workflows/views.py:1485 +#: workflows/views.py:1546 #, fuzzy #| msgid "Geplant für" msgid "Geplanter Start" msgstr "Scheduled for" -#: workflows/views.py:1495 +#: workflows/views.py:1556 msgid "Geräteübergabe / Hardware-Abholung" msgstr "" -#: workflows/views.py:1497 +#: workflows/views.py:1558 msgid "Geplanter Hardware-Termin" msgstr "" -#: workflows/views.py:1506 +#: workflows/views.py:1567 #, fuzzy #| msgid "Noch nicht verfügbar" msgid "PDF verfügbar" msgstr "Not available yet" -#: workflows/views.py:1532 +#: workflows/views.py:1593 #, fuzzy #| msgid "Einweisung" msgid "Einweisungssitzung" msgstr "Introduction" -#: workflows/views.py:1583 +#: workflows/views.py:1644 msgid "Keine Einträge ausgewählt." msgstr "No entries selected." -#: workflows/views.py:1626 +#: workflows/views.py:1687 #, python-format msgid "%(count)s Eintrag/Einträge gelöscht." msgstr "%(count)s entry/entries deleted." -#: workflows/views.py:1628 +#: workflows/views.py:1689 #, python-format msgid "%(count)s Auswahl(en) konnten nicht verarbeitet werden." msgstr "%(count)s selection(s) could not be processed." -#: workflows/views.py:1630 +#: workflows/views.py:1691 msgid "Keine passenden Einträge gefunden." msgstr "No matching entries found." -#: workflows/views.py:1863 +#: workflows/views.py:1926 msgid "Einweisungs- und Übergabeprotokoll wurde erzeugt." msgstr "Introduction and handover protocol was generated." -#: workflows/views.py:1880 +#: workflows/views.py:1943 msgid "Einweisungsprotokoll aus Live-Status wurde erzeugt." msgstr "Introduction protocol from live status was generated." -#: workflows/views.py:1909 +#: workflows/views.py:1972 msgid "Einweisung wurde zurückgesetzt." msgstr "Introduction was reset." -#: workflows/views.py:1923 +#: workflows/views.py:1986 msgid "Einweisung wurde als abgeschlossen gespeichert." msgstr "Introduction was saved as completed." -#: workflows/views.py:1936 +#: workflows/views.py:1999 msgid "Einweisung wurde als Entwurf gespeichert." msgstr "Introduction was saved as draft." -#: workflows/views.py:2907 +#: workflows/views.py:2560 +msgid "Visitenkarten-Details" +msgstr "" + +#: workflows/views.py:2561 +#, fuzzy +#| msgid "Vertragsbeginn" +msgid "Vertragsende" +msgstr "Contract start" + +#: workflows/views.py:2562 +#, fuzzy +#| msgid "Gruppenpostfach erklärt: %(item)s" +msgid "Gruppenpostfächer" +msgstr "Group mailbox explained: %(item)s" + +#: workflows/views.py:2563 +msgid "Zusätzliche Hardware" +msgstr "" + +#: workflows/views.py:2564 +msgid "Zusätzliche Software" +msgstr "" + +#: workflows/views.py:2565 +#, fuzzy +#| msgid "Zusätzlicher Zugang besprochen: %(item)s" +msgid "Zusätzliche Zugänge" +msgstr "Additional access discussed: %(item)s" + +#: workflows/views.py:2566 +#, fuzzy +#| msgid "Reihenfolge speichern" +msgid "Nachfolge" +msgstr "Save order" + +#: workflows/views.py:2567 +msgid "Direktwahl" +msgstr "" + +#: workflows/views.py:2570 +msgid "Steuert die Detailfelder für Visitenkarten." +msgstr "" + +#: workflows/views.py:2571 +msgid "Steuert das Enddatum bei befristeter Beschäftigung." +msgstr "" + +#: workflows/views.py:2572 +msgid "Steuert das Freitextfeld für Gruppenpostfächer." +msgstr "" + +#: workflows/views.py:2573 +msgid "Steuert zusätzliche Hardware-Felder." +msgstr "" + +#: workflows/views.py:2574 +msgid "Steuert zusätzliche Software-Felder." +msgstr "" + +#: workflows/views.py:2575 +msgid "Steuert zusätzliche Zugangsangaben." +msgstr "" + +#: workflows/views.py:2576 +msgid "Steuert Nachfolge- und Übernahmefelder." +msgstr "" + +#: workflows/views.py:2577 +msgid "Steuert die manuelle Direktwahl." +msgstr "" + +#: workflows/views.py:3171 #, fuzzy #| msgid "SMTP-Test starten" msgid "SMTP-Test erfolgreich" msgstr "Run SMTP test" -#: workflows/views.py:2908 +#: workflows/views.py:3172 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Die SMTP-Testmail wurde erfolgreich gesendet." msgstr "Save offboarding request" -#: workflows/views.py:2917 +#: workflows/views.py:3181 #, fuzzy #| msgid "SMTP-Test" msgid "SMTP-Test fehlgeschlagen" msgstr "SMTP test" -#: workflows/views.py:2923 +#: workflows/views.py:3187 #, 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:2948 +#: workflows/views.py:3212 #, fuzzy #| msgid "Nextcloud-Test starten" msgid "Nextcloud-Test erfolgreich" msgstr "Run Nextcloud test" -#: workflows/views.py:2949 +#: workflows/views.py:3213 msgid "Der Testupload nach Nextcloud war erfolgreich." msgstr "" -#: workflows/views.py:2959 workflows/views.py:2969 +#: workflows/views.py:3223 workflows/views.py:3233 #, fuzzy #| msgid "Nextcloud-Test starten" msgid "Nextcloud-Test fehlgeschlagen" msgstr "Run Nextcloud test" -#: workflows/views.py:2960 +#: workflows/views.py:3224 msgid "Der Testupload nach Nextcloud ist fehlgeschlagen." msgstr "" @@ -4998,24 +5320,11 @@ msgstr "" #~ msgid "Setup-Link" #~ msgstr "Setup Mail" -#~ msgid "Branding speichern" -#~ msgstr "Save branding" - #, fuzzy #~| msgid "Optionen speichern" #~ msgid "Firmenkonfiguration speichern" #~ msgstr "Save options" -#, fuzzy -#~| msgid "Aktion" -#~ msgid "Aktion DE" -#~ msgstr "Action" - -#, fuzzy -#~| msgid "Aktion" -#~ msgid "Aktion EN" -#~ msgstr "Action" - #, fuzzy #~| msgid "Produktion" #~ msgid "Product Owner" diff --git a/backend/workflows/form_builder.py b/backend/workflows/form_builder.py index fa2094a..43bb7a0 100644 --- a/backend/workflows/form_builder.py +++ b/backend/workflows/form_builder.py @@ -1,7 +1,9 @@ from collections import OrderedDict +from django import forms +from django.utils.text import slugify from django.utils.translation import get_language -from .models import FormConditionalRuleConfig, FormFieldConfig, FormSectionConfig +from .models import FormConditionalRuleConfig, FormCustomFieldConfig, FormFieldConfig, FormSectionConfig DEFAULT_FIELD_ORDER = { @@ -164,6 +166,8 @@ DEFAULT_CONDITIONAL_RULES = { }, } +CUSTOM_FIELD_PREFIX = 'custom__' + def get_section_order(form_type: str) -> list[str]: if form_type == 'onboarding': @@ -193,6 +197,94 @@ def get_default_conditional_rules(form_type: str) -> dict[str, dict]: return DEFAULT_CONDITIONAL_RULES.get(form_type, {}) +def custom_field_target_key(field_key: str) -> str: + return f'custom__{field_key}' + + +def is_custom_field_target_key(target_key: str) -> bool: + return target_key.startswith(CUSTOM_FIELD_PREFIX) + + +def custom_field_form_name(field_key: str) -> str: + return f'{CUSTOM_FIELD_PREFIX}{field_key}' + + +def is_custom_field_name(field_name: str) -> bool: + return field_name.startswith(CUSTOM_FIELD_PREFIX) + + +def custom_field_key_from_name(field_name: str) -> str: + return field_name[len(CUSTOM_FIELD_PREFIX):] if is_custom_field_name(field_name) else field_name + + +def build_custom_field_key(label: str) -> str: + return slugify(label).replace('-', '_')[:60] or 'custom_field' + + +def get_custom_field_configs(form_type: str, include_inactive: bool = False): + qs = FormCustomFieldConfig.objects.filter(form_type=form_type) + if not include_inactive: + qs = qs.filter(is_active=True) + return list(qs.order_by('sort_order', 'field_key')) + + +def add_custom_form_fields(form_type: str, form, initial_values: dict | None = None) -> None: + language_code = get_language() + initial_values = initial_values or {} + field_page_keys = getattr(form, '_field_page_keys', {}) + sort_map = {} + for cfg in get_custom_field_configs(form_type): + field_name = custom_field_form_name(cfg.field_key) + initial = initial_values.get(cfg.field_key) + if cfg.field_type == FormCustomFieldConfig.FIELD_TYPE_TEXTAREA: + field = forms.CharField( + label=cfg.translated_label(language_code), + help_text=cfg.translated_help_text(language_code), + required=cfg.is_required, + initial=initial, + widget=forms.Textarea(attrs={'rows': 3}), + ) + elif cfg.field_type == FormCustomFieldConfig.FIELD_TYPE_SELECT: + field = forms.ChoiceField( + label=cfg.translated_label(language_code), + help_text=cfg.translated_help_text(language_code), + required=cfg.is_required, + initial=initial or '', + choices=[('', '--')] + cfg.translated_select_options(language_code), + ) + elif cfg.field_type == FormCustomFieldConfig.FIELD_TYPE_CHECKBOX: + field = forms.BooleanField( + label=cfg.translated_label(language_code), + help_text=cfg.translated_help_text(language_code), + required=False, + initial=bool(initial), + ) + else: + field = forms.CharField( + label=cfg.translated_label(language_code), + help_text=cfg.translated_help_text(language_code), + required=cfg.is_required, + initial=initial, + ) + form.fields[field_name] = field + field_page_keys[field_name] = cfg.section_key + sort_map[field_name] = cfg.sort_order + + if form.fields: + core_configs = ensure_form_field_configs(form_type, [name for name in form.fields.keys() if not is_custom_field_name(name)]) + for name in form.fields.keys(): + if is_custom_field_name(name): + continue + cfg = core_configs.get(name) + if cfg: + sort_map[name] = cfg.sort_order + form.fields = OrderedDict( + (name, form.fields[name]) + for name in sorted(form.fields.keys(), key=lambda name: (sort_map.get(name, 9999), name)) + ) + form._field_page_keys = field_page_keys + + def _default_sort(form_type: str, field_name: str) -> int: ordered = DEFAULT_FIELD_ORDER.get(form_type, []) if field_name in ordered: @@ -253,21 +345,28 @@ def ensure_form_section_configs(form_type: str) -> dict[str, FormSectionConfig]: def ensure_form_conditional_rule_configs(form_type: str) -> dict[str, FormConditionalRuleConfig]: defaults = get_default_conditional_rules(form_type) - if not defaults: + if form_type != 'onboarding' and not defaults: return {} + custom_targets = { + custom_field_target_key(cfg.field_key): {'clauses': []} + for cfg in get_custom_field_configs(form_type) + if form_type == 'onboarding' + } + target_defaults = dict(defaults) + target_defaults.update(custom_targets) existing = { cfg.target_key: cfg for cfg in FormConditionalRuleConfig.objects.filter(form_type=form_type) } - missing = [key for key in defaults.keys() if key not in existing] + missing = [key for key in target_defaults.keys() if key not in existing] if missing: FormConditionalRuleConfig.objects.bulk_create( [ FormConditionalRuleConfig( form_type=form_type, target_key=key, - clauses=defaults[key].get('clauses', []), - is_active=True, + clauses=target_defaults[key].get('clauses', []), + is_active=bool(target_defaults[key].get('clauses')), ) for key in missing ], @@ -280,6 +379,39 @@ def ensure_form_conditional_rule_configs(form_type: str) -> dict[str, FormCondit return existing +def evaluate_conditional_clauses(cleaned_data: dict, clauses: list[dict]) -> bool: + def clause_result(clause: dict) -> bool: + field_name = (clause.get('field') or '').strip() + operator = (clause.get('operator') or '').strip() + if not field_name or not operator: + return False + value = cleaned_data.get(field_name) + if operator == 'checked': + return bool(value) is bool(clause.get('value')) + normalized = '' if value is None else str(value).strip() + expected = '' if clause.get('value') is None else str(clause.get('value')).strip() + if operator == 'equals': + return normalized == expected + if operator == 'not_equals': + return normalized != expected + return False + + active_clauses = [clause for clause in (clauses or []) if clause.get('field') and clause.get('operator')] + return bool(active_clauses) and all(clause_result(clause) for clause in active_clauses) + + +def hidden_custom_field_names(form_type: str, cleaned_data: dict) -> set[str]: + if form_type != 'onboarding': + return set() + hidden = set() + for target_key, cfg in ensure_form_conditional_rule_configs(form_type).items(): + if not cfg.is_active or not is_custom_field_target_key(target_key): + continue + if not evaluate_conditional_clauses(cleaned_data, list(cfg.clauses or [])): + hidden.add(target_key) + return hidden + + def apply_form_field_config(form_type: str, form) -> None: field_names = list(form.fields.keys()) configs = _ensure_configs(form_type, field_names) diff --git a/backend/workflows/forms.py b/backend/workflows/forms.py index cd5a126..837ef1c 100644 --- a/backend/workflows/forms.py +++ b/backend/workflows/forms.py @@ -7,7 +7,7 @@ from django.utils import timezone from django.utils.translation import get_language, gettext as _, gettext_lazy from .branding import get_company_email_domain -from .form_builder import apply_form_field_config +from .form_builder import add_custom_form_fields, apply_form_field_config, custom_field_key_from_name, hidden_custom_field_names, is_custom_field_name from .models import EmployeeProfile, FormOption, OffboardingRequest, OnboardingRequest, PortalBranding, PortalCompanyConfig, PortalTrialConfig, UserProfile, WorkflowConfig from .roles import ROLE_ADMIN, ROLE_GROUP_NAMES, ROLE_IT_STAFF, ROLE_LABELS, ROLE_PLATFORM_OWNER, ROLE_STAFF, ROLE_SUPER_ADMIN, assign_user_role, user_has_capability from .totp import normalize_recovery_code, normalize_totp_token, verify_totp_token @@ -814,6 +814,7 @@ class OnboardingRequestForm(forms.ModelForm): self.fields['needed_resources_multi'].choices = self._choices_from_options('resource', RESOURCE_CHOICES) self.fields['signature_image'].required = False apply_form_field_config('onboarding', self) + add_custom_form_fields('onboarding', self, getattr(self.instance, 'custom_field_values', None)) def clean_work_email(self): value = (self.cleaned_data.get('work_email') or '').strip().lower() @@ -883,6 +884,11 @@ class OnboardingRequestForm(forms.ModelForm): }, ) + hidden_custom = hidden_custom_field_names('onboarding', cleaned) + for field_name in hidden_custom: + cleaned[field_name] = False if self.fields.get(field_name) and self.fields[field_name].widget.input_type == 'checkbox' else '' + self._errors.pop(field_name, None) + return cleaned def save(self, commit=True): @@ -922,6 +928,11 @@ class OnboardingRequestForm(forms.ModelForm): instance.agreement = 'accepted' if self.cleaned_data.get('agreement_confirm') else '' instance.onboarded_by_email = self.requester_email + instance.custom_field_values = { + custom_field_key_from_name(name): self.cleaned_data.get(name) + for name in self.fields.keys() + if is_custom_field_name(name) + } if commit: instance.save() @@ -962,6 +973,7 @@ class OffboardingRequestForm(forms.ModelForm): self.fields['department'].initial = prefill_profile.department self.fields['job_title'].initial = prefill_profile.job_title apply_form_field_config('offboarding', self) + add_custom_form_fields('offboarding', self, getattr(self.instance, 'custom_field_values', None)) def clean_work_email(self): value = (self.cleaned_data.get('work_email') or '').strip().lower() @@ -971,3 +983,16 @@ class OffboardingRequestForm(forms.ModelForm): if self.email_domain and not value.endswith(expected_suffix): raise forms.ValidationError(_('Bitte verwenden Sie eine @%(domain)s E-Mail-Adresse.') % {'domain': self.email_domain}) return value + + def save(self, commit=True): + instance = super().save(commit=False) + if not (instance.preferred_language or '').strip(): + instance.preferred_language = (get_language() or 'de').split('-')[0] + instance.custom_field_values = { + custom_field_key_from_name(name): self.cleaned_data.get(name) + for name in self.fields.keys() + if is_custom_field_name(name) + } + if commit: + instance.save() + return instance diff --git a/backend/workflows/migrations/0055_offboardingrequest_custom_field_values_and_more.py b/backend/workflows/migrations/0055_offboardingrequest_custom_field_values_and_more.py new file mode 100644 index 0000000..c1cf8a1 --- /dev/null +++ b/backend/workflows/migrations/0055_offboardingrequest_custom_field_values_and_more.py @@ -0,0 +1,63 @@ +# Generated by Django 5.1.5 on 2026-03-27 12:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('workflows', '0054_formconditionalruleconfig'), + ] + + operations = [ + migrations.AddField( + model_name='offboardingrequest', + name='custom_field_values', + field=models.JSONField(blank=True, default=dict), + ), + migrations.AddField( + model_name='onboardingrequest', + name='custom_field_values', + field=models.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='formfieldconfig', + name='page_key', + field=models.CharField(blank=True, choices=[('', 'Automatisch'), ('stammdaten', 'Stammdaten'), ('vertrag', 'Vertrag'), ('itsetup', 'IT-Setup'), ('abschluss', 'Abschluss'), ('mitarbeitende', 'Mitarbeitende'), ('austritt', 'Austritt')], default='', max_length=20), + ), + migrations.AlterField( + model_name='formsectionconfig', + name='form_type', + field=models.CharField(choices=[('onboarding', 'Onboarding'), ('offboarding', 'Offboarding')], max_length=20), + ), + migrations.AlterField( + model_name='formsectionconfig', + name='section_key', + field=models.CharField(choices=[('stammdaten', 'Stammdaten'), ('vertrag', 'Vertrag'), ('itsetup', 'IT-Setup'), ('abschluss', 'Abschluss'), ('mitarbeitende', 'Mitarbeitende'), ('austritt', 'Austritt')], max_length=20), + ), + migrations.CreateModel( + name='FormCustomFieldConfig', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('form_type', models.CharField(choices=[('onboarding', 'Onboarding'), ('offboarding', 'Offboarding')], max_length=20)), + ('field_key', models.SlugField(max_length=80)), + ('section_key', models.CharField(choices=[('stammdaten', 'Stammdaten'), ('vertrag', 'Vertrag'), ('itsetup', 'IT-Setup'), ('abschluss', 'Abschluss'), ('mitarbeitende', 'Mitarbeitende'), ('austritt', 'Austritt')], max_length=20)), + ('sort_order', models.PositiveIntegerField(default=0)), + ('field_type', models.CharField(choices=[('text', 'Text'), ('textarea', 'Mehrzeilig'), ('select', 'Auswahl'), ('checkbox', 'Checkbox')], default='text', max_length=20)), + ('is_active', models.BooleanField(default=True)), + ('is_required', models.BooleanField(default=False)), + ('label', models.CharField(max_length=255)), + ('label_en', models.CharField(blank=True, max_length=255)), + ('help_text', models.TextField(blank=True)), + ('help_text_en', models.TextField(blank=True)), + ('select_options', models.TextField(blank=True, help_text='Eine Option pro Zeile. Optional: wert|Label')), + ('select_options_en', models.TextField(blank=True, help_text='Eine Option pro Zeile. Optional: value|Label')), + ], + options={ + 'verbose_name': 'Benutzerdefiniertes Formularfeld', + 'verbose_name_plural': 'Benutzerdefinierte Formularfelder', + 'ordering': ['form_type', 'section_key', 'sort_order', 'field_key'], + 'unique_together': {('form_type', 'field_key')}, + }, + ), + ] diff --git a/backend/workflows/models.py b/backend/workflows/models.py index 57ceb76..a406bab 100644 --- a/backend/workflows/models.py +++ b/backend/workflows/models.py @@ -422,6 +422,7 @@ class OnboardingRequest(models.Model): verbose_name='Personalisierter Text für PDF', help_text='Optionaler individueller Textblock im Onboarding PDF.', ) + custom_field_values = models.JSONField(default=dict, blank=True) generated_pdf_path = models.CharField(max_length=500, blank=True) intro_pdf_path = models.CharField(max_length=500, blank=True) @@ -566,6 +567,82 @@ class FormConditionalRuleConfig(models.Model): return f'{self.form_type}: {self.target_key}' +class FormCustomFieldConfig(models.Model): + FIELD_TYPE_TEXT = 'text' + FIELD_TYPE_TEXTAREA = 'textarea' + FIELD_TYPE_SELECT = 'select' + FIELD_TYPE_CHECKBOX = 'checkbox' + FIELD_TYPE_CHOICES = [ + (FIELD_TYPE_TEXT, _('Text')), + (FIELD_TYPE_TEXTAREA, _('Mehrzeilig')), + (FIELD_TYPE_SELECT, _('Auswahl')), + (FIELD_TYPE_CHECKBOX, _('Checkbox')), + ] + FORM_CHOICES = [ + ('onboarding', _('Onboarding')), + ('offboarding', _('Offboarding')), + ] + SECTION_CHOICES = [ + ('stammdaten', _('Stammdaten')), + ('vertrag', _('Vertrag')), + ('itsetup', _('IT-Setup')), + ('abschluss', _('Abschluss')), + ('mitarbeitende', _('Mitarbeitende')), + ('austritt', _('Austritt')), + ] + + form_type = models.CharField(max_length=20, choices=FORM_CHOICES) + field_key = models.SlugField(max_length=80) + section_key = models.CharField(max_length=20, choices=SECTION_CHOICES) + sort_order = models.PositiveIntegerField(default=0) + field_type = models.CharField(max_length=20, choices=FIELD_TYPE_CHOICES, default=FIELD_TYPE_TEXT) + is_active = models.BooleanField(default=True) + is_required = models.BooleanField(default=False) + label = models.CharField(max_length=255) + label_en = models.CharField(max_length=255, blank=True) + help_text = models.TextField(blank=True) + help_text_en = models.TextField(blank=True) + select_options = models.TextField(blank=True, help_text='Eine Option pro Zeile. Optional: wert|Label') + select_options_en = models.TextField(blank=True, help_text='Eine Option pro Zeile. Optional: value|Label') + + class Meta: + ordering = ['form_type', 'section_key', 'sort_order', 'field_key'] + unique_together = ('form_type', 'field_key') + verbose_name = 'Benutzerdefiniertes Formularfeld' + verbose_name_plural = 'Benutzerdefinierte Formularfelder' + + def __str__(self) -> str: + return f'{self.form_type}: {self.label}' + + def translated_label(self, language_code: str | None = None) -> str: + lang = (language_code or get_language() or 'de').split('-')[0] + if lang == 'en' and self.label_en.strip(): + return self.label_en.strip() + return self.label.strip() + + def translated_help_text(self, language_code: str | None = None) -> str: + lang = (language_code or get_language() or 'de').split('-')[0] + if lang == 'en' and self.help_text_en.strip(): + return self.help_text_en.strip() + return self.help_text.strip() + + def translated_select_options(self, language_code: str | None = None) -> list[tuple[str, str]]: + lang = (language_code or get_language() or 'de').split('-')[0] + raw = self.select_options_en if lang == 'en' and self.select_options_en.strip() else self.select_options + options = [] + for line in (raw or '').splitlines(): + line = line.strip() + if not line: + continue + if '|' in line: + value, label = [part.strip() for part in line.split('|', 1)] + else: + value = label = line + if value: + options.append((value, label or value)) + return options + + class NotificationTemplate(models.Model): TEMPLATE_CHOICES = [ ('onboarding_it', _('Onboarding: IT')), @@ -844,6 +921,7 @@ class OffboardingRequest(models.Model): requested_by_name = models.CharField(max_length=255, blank=True, verbose_name='Name der anfordernden Person') preferred_language = models.CharField(max_length=10, blank=True, default='de', db_default='de') generated_pdf_path = models.CharField(max_length=500, blank=True) + custom_field_values = models.JSONField(default=dict, blank=True) processing_status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='submitted') last_error = models.TextField(blank=True) created_at = models.DateTimeField(auto_now_add=True) diff --git a/backend/workflows/pdf_sections.py b/backend/workflows/pdf_sections.py index 948fc84..cff2221 100644 --- a/backend/workflows/pdf_sections.py +++ b/backend/workflows/pdf_sections.py @@ -9,6 +9,7 @@ from django.utils.translation import override from .form_builder import ( LOCKED_FIELD_RULES, LOCKED_SECTION_RULES, + get_custom_field_configs, ensure_form_field_configs, ensure_form_section_configs, get_default_page_map, @@ -16,6 +17,7 @@ from .form_builder import ( get_section_order, ) from .forms import OffboardingRequestForm, OnboardingRequestForm +from .models import FormCustomFieldConfig PDF_SECTION_TITLES = { "onboarding": { @@ -251,6 +253,25 @@ def build_pdf_sections(form_type: str, request_obj, language_code: str | None = } ) + custom_values = getattr(request_obj, 'custom_field_values', {}) or {} + for cfg in get_custom_field_configs(form_type): + if cfg.section_key not in sections: + continue + raw_value = custom_values.get(cfg.field_key) + if cfg.field_type == FormCustomFieldConfig.FIELD_TYPE_CHECKBOX: + raw_value = _yes_no_text(language_code)[0] if raw_value else '' + sections[cfg.section_key]["fields"].append( + { + "name": f"custom__{cfg.field_key}", + "label": cfg.translated_label(language_code), + "help_text": cfg.translated_help_text(language_code), + "kind": _field_kind(raw_value), + "value": raw_value, + "is_empty": _is_empty_value(raw_value), + "is_locked": False, + } + ) + not_available = _not_available_text(language_code) result = [] for section in sections.values(): diff --git a/backend/workflows/static/workflows/css/home.css b/backend/workflows/static/workflows/css/home.css index 11817de..af00405 100644 --- a/backend/workflows/static/workflows/css/home.css +++ b/backend/workflows/static/workflows/css/home.css @@ -149,6 +149,114 @@ line-height: 1.55; } + .ops-overview-card { + margin-bottom: 20px; + border: 1px solid var(--line); + border-radius: 18px; + background: + radial-gradient(120% 120% at 100% 0%, rgba(0, 0, 120, 0.08), rgba(0, 0, 120, 0)), + linear-gradient(180deg, rgba(255,255,255,0.98), rgba(246,250,255,0.95)); + padding: 18px; + box-shadow: inset 0 1px 0 rgba(255,255,255,0.92); + } + + .ops-overview-head h2 { + margin: 0; + color: #17345e; + font-size: 20px; + } + + .ops-overview-head p { + margin: 4px 0 0; + color: var(--muted); + font-size: 14px; + } + + .ops-overview-grid { + display: grid; + grid-template-columns: repeat(4, minmax(180px, 1fr)); + gap: 12px; + margin-top: 14px; + } + + .ops-stat-card { + border: 1px solid #dce6f2; + border-radius: 16px; + background: rgba(255,255,255,0.86); + padding: 14px; + display: grid; + gap: 6px; + } + + .ops-stat-label { + color: #60738d; + font-size: 12px; + font-weight: 700; + } + + .ops-stat-card strong { + color: #17345e; + font-size: 22px; + line-height: 1.1; + } + + .ops-stat-card strong.is-error { + color: #a32020; + } + + .ops-failure-list { + margin-top: 16px; + display: grid; + gap: 10px; + } + + .ops-failure-head { + display: flex; + justify-content: space-between; + align-items: center; + gap: 10px; + flex-wrap: wrap; + } + + .ops-failure-head h3 { + margin: 0; + color: #17345e; + font-size: 16px; + } + + .ops-failure-items { + display: grid; + gap: 10px; + } + + .ops-failure-item { + border: 1px solid #ead9d9; + border-radius: 14px; + background: rgba(255,248,248,0.92); + padding: 12px; + display: grid; + gap: 4px; + } + + .ops-failure-item strong { + color: #7f1d1d; + font-size: 14px; + } + + .ops-failure-item span { + color: #6f5b5b; + font-size: 13px; + } + + .ops-failure-item code { + color: #6a1f1f; + background: rgba(255,255,255,0.6); + border-radius: 8px; + padding: 6px 8px; + font-size: 12px; + overflow-wrap: anywhere; + } + .status-row { margin-top: 16px; display: flex; @@ -605,6 +713,7 @@ @media (max-width: 1080px) { .hero-grid { grid-template-columns: 1fr; } + .ops-overview-grid { grid-template-columns: 1fr 1fr; } .apps-grid { grid-template-columns: 1fr 1fr; } .admin-grid { grid-template-columns: 1fr 1fr; } } diff --git a/backend/workflows/templates/workflows/form_builder.html b/backend/workflows/templates/workflows/form_builder.html index f14536a..b032696 100644 --- a/backend/workflows/templates/workflows/form_builder.html +++ b/backend/workflows/templates/workflows/form_builder.html @@ -51,6 +51,10 @@ {% trans "Aktuell ausgeblendet" %} {{ builder_summary.hidden_field_count }} +
+ {% trans "Eigene Felder" %} + {{ builder_summary.custom_field_count }} +
{% if form_type == 'onboarding' %}
{% trans "Versteckte Abschnitte" %} @@ -129,6 +133,7 @@
{{ item.field_name }}
+ {% if item.is_custom %}{% trans "Eigen" %}{% endif %} {% if item.locked %}{% trans "Fix" %}{% endif %} {% if not item.is_visible %}{% endif %} {% if item.is_required %}{% trans "Pflicht" %}{% endif %} @@ -451,22 +456,134 @@
+ +
+ +
+

{% trans "Eigene Felder" %}

+ {% trans "Öffnen" %} +
+
+
+
+ {% csrf_token %} + + + + + + + + + + + + +
+ +
+ {% csrf_token %} +
+ + + + + + + + + + + + + + + + {% for group in custom_field_groups %} + + + + {% for item in group.items %} + + + + + + + + + + + + {% empty %} + + {% endfor %} + {% endfor %} + +
{% trans "Schlüssel" %}{% trans "Abschnitt" %}{% trans "Typ" %}{% trans "Sortierung" %}{% trans "Label (DE)" %}{% trans "Label (EN)" %}{% trans "Pflicht" %}{% trans "Aktiv" %}{% trans "Löschen" %}
{{ group.title }}
+ + {{ item.field_key }} + + + + + + + + + + + + + + +
{% trans "Keine eigenen Felder vorhanden." %}
+
+
+ +
+
+
+
- + + }); + })(); + {% endblock %} diff --git a/backend/workflows/templates/workflows/home.html b/backend/workflows/templates/workflows/home.html index 6329e7e..0aaa03b 100644 --- a/backend/workflows/templates/workflows/home.html +++ b/backend/workflows/templates/workflows/home.html @@ -37,6 +37,57 @@
{% include 'workflows/includes/messages.html' %} + {% if ops_summary.show %} +
+
+
+

{% trans "Operations Overview" %}

+

{% trans "Letzte Laufzeit- und Backup-Signale auf einen Blick." %}

+
+
+
+ {% if ops_summary.can_view_jobs %} +
+ {% trans "Fehlgeschlagene Jobs (24h)" %} + {{ ops_summary.failed_count_24h }} +
+
+ {% trans "Erfolgreiche Jobs (24h)" %} + {{ ops_summary.success_count_24h }} +
+
+ {% trans "Offene Starts (24h)" %} + {{ ops_summary.started_count_24h }} +
+ {% endif %} + {% if ops_summary.can_manage_backups and ops_summary.backup_health %} +
+ {% trans "Backup-Status" %} + {{ ops_summary.backup_health.label }} + {{ ops_summary.backup_health.summary }} +
+ {% endif %} +
+ {% if ops_summary.can_view_jobs and ops_summary.recent_failed_logs %} +
+
+

{% trans "Letzte Fehler" %}

+ {% trans "Job Monitor öffnen" %} +
+
+ {% for log in ops_summary.recent_failed_logs %} +
+ {{ log.task_name }} + {{ log.target_label|default:log.target_type }} + {{ log.error_message|truncatechars:120 }} +
+ {% endfor %} +
+
+ {% endif %} +
+ {% endif %} + {% for section in portal_app_sections %} {% if not forloop.first %} diff --git a/backend/workflows/templates/workflows/job_monitor.html b/backend/workflows/templates/workflows/job_monitor.html index 3828049..6e32a7b 100644 --- a/backend/workflows/templates/workflows/job_monitor.html +++ b/backend/workflows/templates/workflows/job_monitor.html @@ -14,6 +14,45 @@ {% include 'workflows/includes/messages.html' %} +
+
+
+ +
{{ job_summary.failed_count_24h }}
+
+
+ +
{{ job_summary.success_count_24h }}
+
+
+ +
{{ job_summary.started_count_24h }}
+
+
+ {% if job_summary.recent_failed %} +
+ + + + + + + + + + {% for log in job_summary.recent_failed %} + + + + + + {% endfor %} + +
{% trans "Zuletzt fehlgeschlagen" %}{% trans "Ziel" %}{% trans "Fehler" %}
{{ log.task_name }}{{ log.target_label|default:log.target_type }}{{ log.error_message|truncatechars:140 }}
+
+ {% endif %} +
+
diff --git a/backend/workflows/templates/workflows/offboarding_form.html b/backend/workflows/templates/workflows/offboarding_form.html index 489ea0d..300e96c 100644 --- a/backend/workflows/templates/workflows/offboarding_form.html +++ b/backend/workflows/templates/workflows/offboarding_form.html @@ -60,12 +60,20 @@
{% for field in section.fields %} + {% if field.field.widget.input_type == 'checkbox' %} +
+ {{ field }} {{ field.label_tag }} + {% if field.help_text %}
{{ field.help_text }}
{% endif %} + {{ field.errors }} +
+ {% else %}
{{ field.label_tag }} {{ field }} {% if field.help_text %}
{{ field.help_text }}
{% endif %} {{ field.errors }}
+ {% endif %} {% endfor %}
diff --git a/backend/workflows/templates/workflows/onboarding_form.html b/backend/workflows/templates/workflows/onboarding_form.html index 121cc05..0fdfb48 100644 --- a/backend/workflows/templates/workflows/onboarding_form.html +++ b/backend/workflows/templates/workflows/onboarding_form.html @@ -59,7 +59,7 @@ {% with field=block.field %} {% if field.is_hidden %} {{ field }} - {% elif field.name in onboarding_inline_checks %} + {% elif field.name in onboarding_inline_checks or field.field.widget.input_type == 'checkbox' %}
{{ field }} {{ field.label_tag }} {% if field.help_text %}
{{ field.help_text }}
{% endif %} @@ -102,7 +102,7 @@ {% for field in block.fields %} {% if field.is_hidden %} {{ field }} - {% elif field.name in onboarding_inline_checks %} + {% elif field.name in onboarding_inline_checks or field.field.widget.input_type == 'checkbox' %}
{{ field }} {{ field.label_tag }} {% if field.help_text %}
{{ field.help_text }}
{% endif %} diff --git a/backend/workflows/templates/workflows/request_timeline.html b/backend/workflows/templates/workflows/request_timeline.html index a0c0dc0..c59c416 100644 --- a/backend/workflows/templates/workflows/request_timeline.html +++ b/backend/workflows/templates/workflows/request_timeline.html @@ -37,9 +37,15 @@ .timeline-detail-row { display:grid; grid-template-columns:160px 1fr; gap:12px; font-size:13px; } .timeline-detail-row strong { color:#566886; } .timeline-detail-list { margin:0; padding-left:18px; color:#4f617f; } +.timeline-custom-fields { margin: 0 0 20px; padding: 18px 20px; border: 1px solid #d9e3f8; border-radius: 20px; background: linear-gradient(180deg,#ffffff 0%,#f7faff 100%); box-shadow: 0 18px 40px rgba(23,39,90,.08); } +.timeline-custom-fields h2 { margin: 0 0 14px; font-size: 18px; color: #20345f; } +.timeline-custom-grid { display:grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 12px 16px; } +.timeline-custom-item { padding: 12px 14px; border: 1px solid #d8e1f5; border-radius: 16px; background: #fff; } +.timeline-custom-item strong { display:block; margin-bottom: 4px; color:#566886; font-size:12px; letter-spacing:.05em; text-transform:uppercase; } +.timeline-custom-item span { color:#22324d; font-size:14px; line-height:1.45; } @media (max-width: 1160px) { .timeline-summary-grid { grid-template-columns:repeat(3, minmax(0,1fr)); } } @media (max-width: 820px) { .timeline-summary-grid { grid-template-columns:repeat(2, minmax(0,1fr)); } } -@media (max-width: 700px) { .timeline-summary-grid { grid-template-columns:1fr; } .timeline-head { flex-direction:column; } .timeline-stamp { white-space:normal; } .timeline-detail-row { grid-template-columns:1fr; } } +@media (max-width: 700px) { .timeline-summary-grid { grid-template-columns:1fr; } .timeline-custom-grid { grid-template-columns:1fr; } .timeline-head { flex-direction:column; } .timeline-stamp { white-space:normal; } .timeline-detail-row { grid-template-columns:1fr; } } {% endblock %} @@ -80,6 +86,20 @@
+ {% if custom_field_details %} +
+

{% trans "Benutzerdefinierte Felder" %}

+
+ {% for item in custom_field_details %} +
+ {{ item.label }} + {{ item.value }} +
+ {% endfor %} +
+
+ {% endif %} +
{% for row in timeline_rows %}
diff --git a/backend/workflows/tests/test_form_builder_admin.py b/backend/workflows/tests/test_form_builder_admin.py index 69ee28e..3620174 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 FormConditionalRuleConfig, FormFieldConfig, FormOption, FormSectionConfig +from workflows.models import FormConditionalRuleConfig, FormCustomFieldConfig, FormFieldConfig, FormOption, FormSectionConfig class FormBuilderAdminTests(TestCase): @@ -202,3 +202,59 @@ class FormBuilderAdminTests(TestCase): self.assertEqual(len(rule.clauses), 2) self.assertEqual(rule.clauses[0]['field'], 'successor_required_choice') self.assertEqual(rule.clauses[1]['operator'], 'not_equals') + + def test_staff_can_add_custom_field(self): + self.client.force_login(self.staff) + response = self.client.post( + '/admin-tools/form-builder/?form_type=onboarding&option_category=device', + data={ + 'builder_action': 'add_custom_field', + 'custom_label': 'Laptop-Tag', + 'custom_label_en': 'Laptop tag', + 'custom_section_key': 'itsetup', + 'custom_field_type': 'text', + 'custom_sort_order': '3', + 'custom_is_required': 'on', + }, + HTTP_HOST='localhost', + ) + + self.assertEqual(response.status_code, 302) + field = FormCustomFieldConfig.objects.get(form_type='onboarding', field_key='laptop_tag') + self.assertEqual(field.section_key, 'itsetup') + self.assertEqual(field.field_type, 'text') + self.assertEqual(field.is_required, True) + + def test_save_order_updates_custom_field_section_and_sort_order(self): + self.client.force_login(self.staff) + custom_field = FormCustomFieldConfig.objects.create( + form_type='onboarding', + field_key='laptop_tag', + section_key='itsetup', + sort_order=99, + field_type='text', + label='Laptop-Tag', + ) + self.client.get('/admin-tools/form-builder/?form_type=onboarding', HTTP_HOST='localhost') + + payload = { + 'form_type': 'onboarding', + 'columns': { + 'stammdaten': ['department'], + 'vertrag': ['contract_start'], + 'itsetup': ['custom__laptop_tag'], + 'abschluss': [], + }, + } + + response = self.client.post( + '/admin-tools/form-builder/save-order/', + data=json.dumps(payload), + content_type='application/json', + HTTP_HOST='localhost', + ) + + self.assertEqual(response.status_code, 200) + custom_field.refresh_from_db() + self.assertEqual(custom_field.section_key, 'itsetup') + self.assertEqual(custom_field.sort_order, 2) diff --git a/backend/workflows/tests/test_observability_ui.py b/backend/workflows/tests/test_observability_ui.py new file mode 100644 index 0000000..5c80698 --- /dev/null +++ b/backend/workflows/tests/test_observability_ui.py @@ -0,0 +1,123 @@ +from datetime import timedelta + +from django.contrib.auth import get_user_model +from django.test import Client, TestCase +from django.utils import timezone + +from workflows.models import AsyncTaskLog +from workflows.roles import ROLE_ADMIN, ROLE_STAFF, assign_user_role + + +class ObservabilityUITests(TestCase): + def setUp(self): + user_model = get_user_model() + self.admin = user_model.objects.create_user( + username='ops_admin', + email='ops-admin@example.com', + password='secret123', + ) + assign_user_role(self.admin, ROLE_ADMIN) + + self.staff = user_model.objects.create_user( + username='ops_staff', + email='ops-staff@example.com', + password='secret123', + ) + assign_user_role(self.staff, ROLE_STAFF) + + def _create_log(self, *, status: str, task_name: str, target_label: str, error_message: str = '') -> AsyncTaskLog: + log = AsyncTaskLog.objects.create( + task_name=task_name, + status=status, + target_type='request', + target_id=1, + target_label=target_label, + error_message=error_message, + ) + AsyncTaskLog.objects.filter(id=log.id).update( + started_at=timezone.now() - timedelta(hours=2), + finished_at=timezone.now() - timedelta(hours=1, minutes=45), + ) + return AsyncTaskLog.objects.get(id=log.id) + + def test_home_shows_operations_overview_for_admin(self): + self._create_log( + status='failed', + task_name='send_scheduled_welcome_email', + target_label='Request A', + error_message='smtp failed hard', + ) + self._create_log( + status='succeeded', + task_name='process_onboarding_request', + target_label='Request B', + ) + self._create_log( + status='started', + task_name='process_offboarding_request', + target_label='Request C', + ) + + client = Client() + client.force_login(self.admin) + response = client.get('/', HTTP_HOST='localhost') + + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'Operations Overview') + self.assertContains(response, 'Fehlgeschlagene Jobs (24h)') + self.assertContains(response, '1', html=True) + self.assertContains(response, 'send_scheduled_welcome_email') + self.assertContains(response, 'Backup-Status') + + def test_home_hides_operations_overview_for_staff(self): + self._create_log( + status='failed', + task_name='process_onboarding_request', + target_label='Request A', + error_message='pdf failed', + ) + + client = Client() + client.force_login(self.staff) + response = client.get('/', HTTP_HOST='localhost') + + self.assertEqual(response.status_code, 200) + self.assertNotContains(response, 'Operations Overview') + self.assertNotContains(response, 'Job Monitor öffnen') + + def test_job_monitor_summary_shows_recent_counts(self): + self._create_log( + status='failed', + task_name='process_onboarding_request', + target_label='Request A', + error_message='pdf failed', + ) + self._create_log( + status='succeeded', + task_name='process_offboarding_request', + target_label='Request B', + ) + self._create_log( + status='started', + task_name='send_scheduled_welcome_email', + target_label='Request C', + ) + + client = Client() + client.force_login(self.admin) + response = client.get('/admin-tools/jobs/', HTTP_HOST='localhost') + + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'Fehlgeschlagene Jobs (24h)') + self.assertContains(response, 'Erfolgreiche Jobs (24h)') + self.assertContains(response, 'Offene Starts (24h)') + self.assertContains(response, 'Zuletzt fehlgeschlagen') + self.assertContains(response, 'pdf failed') + + def test_job_monitor_requires_capability(self): + client = Client() + client.force_login(self.staff) + + response = client.get('/admin-tools/jobs/', HTTP_HOST='localhost') + + self.assertEqual(response.status_code, 302) diff --git a/backend/workflows/tests/test_offboarding_flow.py b/backend/workflows/tests/test_offboarding_flow.py index 6306a0b..df83b5f 100644 --- a/backend/workflows/tests/test_offboarding_flow.py +++ b/backend/workflows/tests/test_offboarding_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 EmployeeProfile, OffboardingRequest +from workflows.models import EmployeeProfile, FormCustomFieldConfig, OffboardingRequest class OffboardingFlowTests(TestCase): @@ -59,3 +59,37 @@ class OffboardingFlowTests(TestCase): self.assertEqual(obj.requested_by_email, f'operator@{self.company_domain}') self.assertEqual(obj.requested_by_name, 'Nina Admin') mock_delay.assert_called_once_with(obj.id) + + @patch('workflows.views.process_offboarding_request.delay') + def test_offboarding_custom_field_is_saved(self, mock_delay): + FormCustomFieldConfig.objects.create( + form_type='offboarding', + field_key='return_comment', + section_key='abschluss', + sort_order=0, + field_type='textarea', + is_active=True, + is_required=False, + label='Rückgabehinweis', + ) + + payload = { + 'full_name': self.profile.full_name, + 'work_email': self.profile.work_email, + 'department': self.profile.department, + 'job_title': self.profile.job_title, + 'last_working_day': '2026-12-31', + 'notes': 'Bitte Accounts sperren.', + 'custom__return_comment': 'Abholung durch IT am Freitag.', + } + + response = self.client.post( + f'/offboarding/new/?profile={self.profile.id}', + payload, + HTTP_HOST='localhost', + ) + + self.assertEqual(response.status_code, 302) + obj = OffboardingRequest.objects.get(work_email=self.profile.work_email) + self.assertEqual(obj.custom_field_values, {'return_comment': 'Abholung durch IT am Freitag.'}) + mock_delay.assert_called_once_with(obj.id) diff --git a/backend/workflows/tests/test_onboarding_flow.py b/backend/workflows/tests/test_onboarding_flow.py index c43bad6..8cad82c 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 FormConditionalRuleConfig, FormFieldConfig, FormSectionConfig, OnboardingRequest +from workflows.models import FormConditionalRuleConfig, FormCustomFieldConfig, FormFieldConfig, FormSectionConfig, OnboardingRequest class OnboardingFlowTests(TestCase): @@ -171,3 +171,168 @@ class OnboardingFlowTests(TestCase): self.assertEqual(response.status_code, 200) self.assertIn('employment-end-box', html) self.assertIn('"value": "unbefristet"', html) + + def test_onboarding_custom_field_uses_combined_order(self): + FormCustomFieldConfig.objects.create( + form_type='onboarding', + field_key='office_location', + section_key='stammdaten', + sort_order=1, + field_type='text', + is_active=True, + label='Bürostandort', + ) + FormFieldConfig.objects.update_or_create( + form_type='onboarding', + field_name='gender', + defaults={'sort_order': 2, 'page_key': 'stammdaten'}, + ) + + response = self.client.get('/onboarding/new/', HTTP_HOST='localhost') + html = response.content.decode('utf-8') + + self.assertLess(html.index('Bürostandort'), html.index('Anrede')) + + @patch('workflows.views.process_onboarding_request.delay') + def test_onboarding_custom_field_is_rendered_and_saved(self, mock_delay): + FormCustomFieldConfig.objects.create( + form_type='onboarding', + field_key='office_location', + section_key='stammdaten', + sort_order=0, + field_type='text', + is_active=True, + is_required=True, + label='Bürostandort', + ) + + response = self.client.get('/onboarding/new/', HTTP_HOST='localhost') + self.assertContains(response, 'Bürostandort') + + payload = { + 'first_name': 'Mara', + 'last_name': 'Muster', + 'gender': 'frau', + 'job_title': 'Consultant', + 'department': 'IT-Service', + 'work_email': f'mara.muster@{self.company_domain}', + 'contract_start': '2026-11-01', + 'employment_type': 'unbefristet', + 'group_mailboxes_required_choice': 'nein', + 'additional_hardware_needed_choice': 'nein', + 'additional_software_needed_choice': 'nein', + 'additional_access_needed_choice': 'nein', + 'successor_required_choice': 'nein', + 'inherit_phone_number_choice': 'nein', + 'custom__office_location': 'Berlin Mitte', + 'agreement_confirm': 'on', + } + + submit_response = self.client.post('/onboarding/new/', payload, HTTP_HOST='localhost') + + self.assertEqual(submit_response.status_code, 302) + obj = OnboardingRequest.objects.get(work_email=f'mara.muster@{self.company_domain}') + self.assertEqual(obj.custom_field_values, {'office_location': 'Berlin Mitte'}) + mock_delay.assert_called_once_with(obj.id) + + @patch('workflows.views.process_onboarding_request.delay') + def test_hidden_required_custom_field_does_not_block_submission(self, mock_delay): + FormCustomFieldConfig.objects.create( + form_type='onboarding', + field_key='visitor_badge_name', + section_key='stammdaten', + sort_order=0, + field_type='text', + is_active=True, + is_required=True, + label='Besucherausweis', + ) + FormConditionalRuleConfig.objects.update_or_create( + form_type='onboarding', + target_key='custom__visitor_badge_name', + defaults={ + 'is_active': True, + 'clauses': [{'field': 'order_business_cards', 'operator': 'checked', 'value': True}], + }, + ) + + response = self.client.get('/onboarding/new/', HTTP_HOST='localhost') + html = response.content.decode('utf-8') + self.assertIn('custom__visitor_badge_name', html) + self.assertIn('"custom__visitor_badge_name"', html) + + payload = { + 'first_name': 'Lea', + 'last_name': 'Leicht', + 'gender': 'frau', + 'job_title': 'Consultant', + 'department': 'IT-Service', + 'work_email': f'lea.leicht@{self.company_domain}', + 'contract_start': '2026-11-01', + 'employment_type': 'unbefristet', + 'group_mailboxes_required_choice': 'nein', + 'additional_hardware_needed_choice': 'nein', + 'additional_software_needed_choice': 'nein', + 'additional_access_needed_choice': 'nein', + 'successor_required_choice': 'nein', + 'inherit_phone_number_choice': 'nein', + 'agreement_confirm': 'on', + } + + submit_response = self.client.post('/onboarding/new/', payload, HTTP_HOST='localhost') + + self.assertEqual(submit_response.status_code, 302) + obj = OnboardingRequest.objects.get(work_email=f'lea.leicht@{self.company_domain}') + self.assertEqual(obj.custom_field_values, {'visitor_badge_name': ''}) + mock_delay.assert_called_once_with(obj.id) + + @patch('workflows.views.process_onboarding_request.delay') + def test_visible_required_custom_field_blocks_submission(self, mock_delay): + FormCustomFieldConfig.objects.create( + form_type='onboarding', + field_key='visitor_badge_name', + section_key='stammdaten', + sort_order=0, + field_type='text', + is_active=True, + is_required=True, + label='Besucherausweis', + ) + FormConditionalRuleConfig.objects.update_or_create( + form_type='onboarding', + target_key='custom__visitor_badge_name', + defaults={ + 'is_active': True, + 'clauses': [{'field': 'order_business_cards', 'operator': 'checked', 'value': True}], + }, + ) + + payload = { + 'first_name': 'Lia', + 'last_name': 'Laut', + 'gender': 'frau', + 'job_title': 'Consultant', + 'department': 'IT-Service', + 'work_email': f'lia.laut@{self.company_domain}', + 'contract_start': '2026-11-01', + 'employment_type': 'unbefristet', + 'order_business_cards': 'on', + 'business_card_name': 'Lia Laut', + 'business_card_title': 'Consultant', + 'business_card_email': f'lia.laut@{self.company_domain}', + 'business_card_phone': '030 123456', + 'group_mailboxes_required_choice': 'nein', + 'additional_hardware_needed_choice': 'nein', + 'additional_software_needed_choice': 'nein', + 'additional_access_needed_choice': 'nein', + 'successor_required_choice': 'nein', + 'inherit_phone_number_choice': 'nein', + 'agreement_confirm': 'on', + } + + response = self.client.post('/onboarding/new/', payload, HTTP_HOST='localhost') + + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'Besucherausweis') + self.assertFalse(OnboardingRequest.objects.filter(work_email=f'lia.laut@{self.company_domain}').exists()) + mock_delay.assert_not_called() diff --git a/backend/workflows/tests/test_pdf_sections.py b/backend/workflows/tests/test_pdf_sections.py index 9d4321e..80ee418 100644 --- a/backend/workflows/tests/test_pdf_sections.py +++ b/backend/workflows/tests/test_pdf_sections.py @@ -1,6 +1,6 @@ from django.test import TestCase -from workflows.models import FormFieldConfig, FormSectionConfig, OffboardingRequest, OnboardingRequest +from workflows.models import FormCustomFieldConfig, FormFieldConfig, FormSectionConfig, OffboardingRequest, OnboardingRequest from workflows.pdf_sections import build_pdf_sections @@ -94,3 +94,32 @@ class PDFSectionBuilderTests(TestCase): self.assertIn('last_working_day', [field['name'] for field in austritt['fields']]) date_field = next(field for field in austritt['fields'] if field['name'] == 'last_working_day') self.assertTrue(date_field['display_value']) + + def test_custom_fields_are_included_in_pdf_sections(self): + FormCustomFieldConfig.objects.create( + form_type='onboarding', + field_key='office_location', + section_key='stammdaten', + sort_order=0, + field_type='text', + is_active=True, + label='Bürostandort', + ) + request_obj = OnboardingRequest.objects.create( + full_name='Max Mustermann', + gender='herr', + job_title='Consultant', + department='IT-Service', + work_email='max.mustermann@workdock.de', + contract_start='2026-11-01', + employment_type='unbefristet', + agreement='accepted', + custom_field_values={'office_location': 'Berlin Mitte'}, + ) + + sections = build_pdf_sections('onboarding', request_obj, 'de') + stammdaten = next(section for section in sections if section['key'] == 'stammdaten') + custom_field = next(field for field in stammdaten['fields'] if field['name'] == 'custom__office_location') + + self.assertEqual(custom_field['label'], 'Bürostandort') + self.assertEqual(custom_field['display_value'], 'Berlin Mitte') diff --git a/backend/workflows/tests/test_request_timeline.py b/backend/workflows/tests/test_request_timeline.py new file mode 100644 index 0000000..389e3be --- /dev/null +++ b/backend/workflows/tests/test_request_timeline.py @@ -0,0 +1,73 @@ +from django.contrib.auth import get_user_model +from django.test import TestCase + +from workflows.models import FormCustomFieldConfig, OffboardingRequest, OnboardingRequest +from workflows.roles import ROLE_ADMIN, assign_user_role + + +class RequestTimelineCustomFieldTests(TestCase): + def setUp(self): + user_model = get_user_model() + self.user = user_model.objects.create_user( + username='timeline_admin', + email='timeline-admin@example.com', + password='secret123', + ) + assign_user_role(self.user, ROLE_ADMIN) + self.client.force_login(self.user) + + def test_onboarding_timeline_renders_custom_field_values(self): + FormCustomFieldConfig.objects.create( + form_type='onboarding', + field_key='office_location', + section_key='stammdaten', + sort_order=0, + field_type='text', + is_active=True, + label='Bürostandort', + ) + obj = OnboardingRequest.objects.create( + full_name='Max Mustermann', + gender='herr', + job_title='Consultant', + department='IT-Service', + work_email='max.mustermann@workdock.de', + contract_start='2026-11-01', + employment_type='unbefristet', + agreement='accepted', + custom_field_values={'office_location': 'Berlin Mitte'}, + ) + + response = self.client.get(f'/requests/timeline/onboarding/{obj.id}/', HTTP_HOST='localhost') + + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'Benutzerdefinierte Felder') + self.assertContains(response, 'Bürostandort') + self.assertContains(response, 'Berlin Mitte') + + def test_offboarding_timeline_renders_custom_field_values(self): + FormCustomFieldConfig.objects.create( + form_type='offboarding', + field_key='return_comment', + section_key='abschluss', + sort_order=0, + field_type='textarea', + is_active=True, + label='Rückgabehinweis', + ) + obj = OffboardingRequest.objects.create( + full_name='Lara Beispiel', + work_email='lara.beispiel@workdock.de', + department='IT-Service', + job_title='Engineer', + last_working_day='2026-12-31', + requested_by_email='admin@workdock.de', + custom_field_values={'return_comment': 'Abholung durch IT am Freitag.'}, + ) + + response = self.client.get(f'/requests/timeline/offboarding/{obj.id}/', HTTP_HOST='localhost') + + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'Benutzerdefinierte Felder') + self.assertContains(response, 'Rückgabehinweis') + self.assertContains(response, 'Abholung durch IT am Freitag.') diff --git a/backend/workflows/views.py b/backend/workflows/views.py index e44bbc7..ad2422d 100644 --- a/backend/workflows/views.py +++ b/backend/workflows/views.py @@ -10,6 +10,7 @@ from django.conf import settings from django.db import connection from django.db import IntegrityError from django.db.models import Q +from django.db.models import Count from django.shortcuts import get_object_or_404, redirect, render from django.contrib import messages from django.contrib.auth import get_user_model, login as auth_login @@ -46,15 +47,18 @@ from .form_builder import ( ONBOARDING_DEFAULT_PAGE, ONBOARDING_PAGE_LABELS, ONBOARDING_PAGE_ORDER, + build_custom_field_key, + custom_field_target_key, ensure_form_field_configs, ensure_form_conditional_rule_configs, ensure_form_section_configs, + get_custom_field_configs, get_default_page_map, get_section_labels, get_section_order, apply_form_preset, ) -from .models import AdminAuditLog, AsyncTaskLog, EmployeeProfile, FormConditionalRuleConfig, FormFieldConfig, FormOption, FormSectionConfig, IntroChecklistItem, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, PortalAppConfig, PortalBranding, PortalCompanyConfig, PortalTrialConfig, ScheduledWelcomeEmail, SystemEmailConfig, UserNotification, UserProfile, WorkflowConfig +from .models import AdminAuditLog, AsyncTaskLog, EmployeeProfile, FormConditionalRuleConfig, FormCustomFieldConfig, 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 @@ -105,8 +109,6 @@ ONBOARDING_GROUPS = { 'phone-box': ['phone_number_choice'], } -ONBOARDING_HIDDEN_BY_DEFAULT = set(DEFAULT_CONDITIONAL_RULES.get('onboarding', {}).keys()) - ONBOARDING_INLINE_CHECKS = {'order_business_cards', 'agreement_confirm'} ONBOARDING_CHECKBOX_LISTS = { 'needed_devices_multi', @@ -144,6 +146,10 @@ def _normalized_conditional_rule_payload(form_type: str) -> dict[str, dict]: return payload +def _active_conditional_target_keys(form_type: str) -> set[str]: + return set(_normalized_conditional_rule_payload(form_type).keys()) + + def healthz(request): db_ok = True try: @@ -450,6 +456,36 @@ def _request_status_label(status_key: str, language_code: str | None = None) -> return labels.get(status_key, status_key) +def _request_custom_field_details(obj, kind: str, language_code: str | None = None) -> list[dict[str, str]]: + form_type = 'onboarding' if kind == 'onboarding' else 'offboarding' + language_code = ((language_code or getattr(obj, 'preferred_language', '') or get_language() or 'de').split('-')[0]).lower() + values = getattr(obj, 'custom_field_values', {}) or {} + rows = [] + yes_label = 'Ja' if language_code == 'de' else 'Yes' + for cfg in get_custom_field_configs(form_type, include_inactive=True): + raw_value = values.get(cfg.field_key) + if raw_value in (None, '', False, []): + continue + if isinstance(raw_value, bool): + display_value = str(yes_label) if raw_value else '' + elif isinstance(raw_value, list): + display_value = ', '.join(str(item).strip() for item in raw_value if str(item).strip()) + else: + display_value = str(raw_value).strip() + if not display_value: + continue + rows.append( + { + 'label': cfg.translated_label(language_code), + 'value': display_value, + 'section': cfg.section_key, + 'sort_order': cfg.sort_order, + } + ) + rows.sort(key=lambda item: (item['section'], item['sort_order'], item['label'])) + return rows + + def _audit_action_label(action: str) -> str: labels = { 'requests_deleted': _('Vorgänge gelöscht'), @@ -505,6 +541,7 @@ def _build_onboarding_layout(form) -> list[dict]: for group_id, group_fields in ONBOARDING_GROUPS.items(): for name in group_fields: group_by_field[name] = group_id + conditional_target_keys = _active_conditional_target_keys('onboarding') rendered_groups = set() consumed = set() @@ -529,7 +566,7 @@ def _build_onboarding_layout(form) -> list[dict]: { 'kind': 'group', 'id': group_id, - 'hidden_default': group_id in ONBOARDING_HIDDEN_BY_DEFAULT, + 'hidden_default': group_id in conditional_target_keys, 'fields': group_fields, } ) @@ -537,6 +574,18 @@ def _build_onboarding_layout(form) -> list[dict]: consumed.update([f.name for f in group_fields]) continue + if field_name.startswith('custom__') and field_name in conditional_target_keys: + blocks.append( + { + 'kind': 'group', + 'id': field_name, + 'hidden_default': True, + 'fields': [form[field_name]], + } + ) + consumed.add(field_name) + continue + blocks.append({'kind': 'field', 'field': form[field_name]}) consumed.add(field_name) @@ -600,10 +649,42 @@ def _build_offboarding_sections(form, visible_section_keys: set[str] | None = No ] +def _ops_summary_for_user(user) -> dict[str, object]: + can_view_jobs = user_has_capability(user, 'view_job_monitor') + can_manage_backups = user_has_capability(user, 'manage_backups') + summary: dict[str, object] = { + 'show': can_view_jobs or can_manage_backups, + 'can_view_jobs': can_view_jobs, + 'can_manage_backups': can_manage_backups, + 'failed_count_24h': 0, + 'started_count_24h': 0, + 'success_count_24h': 0, + 'recent_failed_logs': [], + 'backup_health': latest_backup_health_snapshot() if can_manage_backups else None, + } + if not can_view_jobs: + return summary + + since = timezone.now() - timedelta(hours=24) + logs = AsyncTaskLog.objects.filter(started_at__gte=since) + counts = { + row['status']: row['count'] + for row in logs.values('status').annotate(count=Count('id')) + } + summary['failed_count_24h'] = counts.get('failed', 0) + summary['started_count_24h'] = counts.get('started', 0) + summary['success_count_24h'] = counts.get('succeeded', 0) + summary['recent_failed_logs'] = list( + AsyncTaskLog.objects.filter(status='failed').order_by('-started_at', '-id')[:5] + ) + return summary + + @login_required def home(request): config, _ = WorkflowConfig.objects.get_or_create(name='Default') role_key = get_user_role_key(request.user) + ops_summary = _ops_summary_for_user(request.user) return render( request, 'workflows/home.html', @@ -614,6 +695,7 @@ def home(request): 'role_label': get_user_role_label(request.user), 'role_key': role_key, 'portal_app_sections': build_portal_app_sections(request.user), + 'ops_summary': ops_summary, }, ) @@ -641,6 +723,13 @@ def job_monitor_page(request): logs = logs.filter(task_name=task_filter) logs = logs.order_by('-started_at', '-id')[:200] task_names = list(AsyncTaskLog.objects.order_by('task_name').values_list('task_name', flat=True).distinct()) + since = timezone.now() - timedelta(hours=24) + recent_logs = AsyncTaskLog.objects.filter(started_at__gte=since) + counts = { + row['status']: row['count'] + for row in recent_logs.values('status').annotate(count=Count('id')) + } + recent_failed = list(AsyncTaskLog.objects.filter(status='failed').order_by('-started_at', '-id')[:5]) return render( request, 'workflows/job_monitor.html', @@ -650,6 +739,12 @@ def job_monitor_page(request): 'task_filter': task_filter, 'task_names': task_names, 'status_choices': [('started', _('Gestartet')), ('succeeded', _('Erfolgreich')), ('failed', _('Fehlgeschlagen'))], + 'job_summary': { + 'started_count_24h': counts.get('started', 0), + 'success_count_24h': counts.get('succeeded', 0), + 'failed_count_24h': counts.get('failed', 0), + 'recent_failed': recent_failed, + }, }, ) @@ -1469,6 +1564,7 @@ def request_timeline_page(request, kind: str, request_id: int): return redirect('requests_dashboard') request_label = _request_target_label(obj, kind) + custom_field_details = _request_custom_field_details(obj, kind, getattr(request, 'LANGUAGE_CODE', None)) audit_rows = list( AdminAuditLog.objects.select_related('actor') .filter(target_type__in=[kind, 'request']) @@ -1483,6 +1579,7 @@ def request_timeline_page(request, kind: str, request_id: int): 'title': _('Anfrage erstellt'), 'summary': request_label, 'meta': _('Status: %(status)s') % {'status': obj.get_processing_status_display()}, + 'details': {item['label']: item['value'] for item in custom_field_details}, } ] @@ -1569,6 +1666,7 @@ def request_timeline_page(request, kind: str, request_id: int): 'request_obj': obj, 'request_label': request_label, 'timeline_rows': timeline_rows, + 'custom_field_details': custom_field_details, 'contract_start': getattr(obj, 'contract_start', None), 'handover_date': getattr(obj, 'handover_date', None), }, @@ -2076,6 +2174,7 @@ def form_builder_page(request): if request.method == 'POST': delete_option_id = request.POST.get('delete_option_id', '').strip() + delete_custom_field_id = request.POST.get('delete_custom_field_id', '').strip() if delete_option_id: option = FormOption.objects.filter(id=delete_option_id).first() if not option: @@ -2088,6 +2187,17 @@ def form_builder_page(request): _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}&panel=builder-content&subpanel=options#builder-content") + if delete_custom_field_id: + custom_field = FormCustomFieldConfig.objects.filter(id=delete_custom_field_id, form_type=form_type).first() + if not custom_field: + messages.error(request, 'Benutzerdefiniertes Feld nicht gefunden.') + else: + deleted_label = custom_field.label + deleted_id = custom_field.id + custom_field.delete() + _audit(request, 'form_custom_field_deleted', target_type='form_custom_field', target_id=deleted_id, target_label=deleted_label) + messages.success(request, 'Benutzerdefiniertes Feld wurde gelöscht.') + return redirect(f"/admin-tools/form-builder/?form_type={form_type}&option_category={option_category}&panel=builder-content&subpanel=custom-fields#builder-content") action = request.POST.get('builder_action', '') if action == 'add_option': @@ -2157,6 +2267,91 @@ 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.') + elif action == 'add_custom_field': + label = (request.POST.get('custom_label') or '').strip() + label_en = (request.POST.get('custom_label_en') or '').strip() + section_key = (request.POST.get('custom_section_key') or '').strip() + field_type = (request.POST.get('custom_field_type') or '').strip() + sort_order_raw = (request.POST.get('custom_sort_order') or '').strip() + help_text = (request.POST.get('custom_help_text') or '').strip() + help_text_en = (request.POST.get('custom_help_text_en') or '').strip() + select_options = (request.POST.get('custom_select_options') or '').strip() + select_options_en = (request.POST.get('custom_select_options_en') or '').strip() + section_choices = {key for key in get_section_order(form_type)} + field_type_choices = {key for key, _ in FormCustomFieldConfig.FIELD_TYPE_CHOICES} + if not label: + messages.error(request, 'Bitte eine Bezeichnung für das benutzerdefinierte Feld angeben.') + elif section_key not in section_choices: + messages.error(request, 'Ungültiger Abschnitt für das benutzerdefinierte Feld.') + elif field_type not in field_type_choices: + messages.error(request, 'Ungültiger Feldtyp.') + elif field_type == FormCustomFieldConfig.FIELD_TYPE_SELECT and not select_options: + messages.error(request, 'Auswahlfelder benötigen mindestens eine Option.') + else: + field_key_base = build_custom_field_key(label) + field_key = field_key_base + suffix = 2 + while FormCustomFieldConfig.objects.filter(form_type=form_type, field_key=field_key).exists(): + field_key = f'{field_key_base}_{suffix}' + suffix += 1 + try: + sort_order = int(sort_order_raw or 0) + except ValueError: + sort_order = 0 + FormCustomFieldConfig.objects.create( + form_type=form_type, + field_key=field_key, + section_key=section_key, + sort_order=max(0, sort_order), + field_type=field_type, + is_active=True, + is_required=request.POST.get('custom_is_required') == 'on', + label=label, + label_en=label_en, + help_text=help_text, + help_text_en=help_text_en, + select_options=select_options, + select_options_en=select_options_en, + ) + _audit(request, 'form_custom_field_added', target_type='form_custom_field', target_label=label, details={'form_type': form_type, 'field_type': field_type, 'section_key': section_key}) + messages.success(request, 'Benutzerdefiniertes Feld wurde hinzugefügt.') + + elif action == 'save_custom_fields': + custom_ids = request.POST.getlist('custom_field_ids') + updated = 0 + section_choices = {key for key in get_section_order(form_type)} + field_type_choices = {key for key, _ in FormCustomFieldConfig.FIELD_TYPE_CHOICES} + for raw_id in custom_ids: + cfg = FormCustomFieldConfig.objects.filter(id=raw_id, form_type=form_type).first() + if not cfg: + continue + field_type = (request.POST.get(f'custom_field_type_{cfg.id}') or '').strip() + section_key = (request.POST.get(f'custom_section_key_{cfg.id}') or '').strip() + try: + sort_order = int((request.POST.get(f'custom_sort_order_{cfg.id}') or '').strip() or cfg.sort_order) + except ValueError: + sort_order = cfg.sort_order + cfg.label = (request.POST.get(f'custom_label_{cfg.id}') or '').strip() or cfg.label + cfg.label_en = (request.POST.get(f'custom_label_en_{cfg.id}') or '').strip() + cfg.help_text = (request.POST.get(f'custom_help_text_{cfg.id}') or '').strip() + cfg.help_text_en = (request.POST.get(f'custom_help_text_en_{cfg.id}') or '').strip() + cfg.is_required = request.POST.get(f'custom_is_required_{cfg.id}') == 'on' + cfg.is_active = request.POST.get(f'custom_is_active_{cfg.id}') == 'on' + if field_type in field_type_choices: + cfg.field_type = field_type + if section_key in section_choices: + cfg.section_key = section_key + cfg.sort_order = max(0, sort_order) + cfg.select_options = (request.POST.get(f'custom_select_options_{cfg.id}') or '').strip() + cfg.select_options_en = (request.POST.get(f'custom_select_options_en_{cfg.id}') or '').strip() + if cfg.field_type == FormCustomFieldConfig.FIELD_TYPE_SELECT and not cfg.select_options: + messages.error(request, f'Auswahlfeld "{cfg.label}" benötigt mindestens eine Option.') + return redirect(f"/admin-tools/form-builder/?form_type={form_type}&option_category={option_category}&panel=builder-content&subpanel=custom-fields#builder-content") + cfg.save() + updated += 1 + _audit(request, 'form_custom_fields_saved', target_type='form_custom_field', target_label=form_type, details={'count': updated}) + messages.success(request, 'Benutzerdefinierte Felder wurden gespeichert.') + elif action == 'save_field_rules': field_ids = request.POST.getlist('field_rule_ids') locked_fields = LOCKED_FIELD_RULES.get(form_type, set()) @@ -2223,12 +2418,14 @@ def form_builder_page(request): else: messages.error(request, 'Preset konnte nicht angewendet werden.') - if action in {'add_option', 'save_options', 'save_field_texts'}: + if action in {'add_option', 'save_options', 'save_field_texts', 'add_custom_field', 'save_custom_fields'}: 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 {'add_custom_field', 'save_custom_fields'}: + active_subpanel = 'custom-fields' elif action in {'save_field_rules', 'save_section_rules', 'save_conditional_rules'}: active_panel = 'builder-rules' redirect_target = f"/admin-tools/form-builder/?form_type={form_type}&option_category={option_category}" @@ -2263,6 +2460,7 @@ def form_builder_page(request): labels = _form_field_labels(form_type) locked = LOCKED_FIELD_RULES.get(form_type, set()) locked_sections = LOCKED_SECTION_RULES.get(form_type, set()) + custom_field_configs = list(FormCustomFieldConfig.objects.filter(form_type=form_type).order_by('section_key', 'sort_order', 'field_key')) if form_type == 'onboarding': columns = [ @@ -2289,8 +2487,30 @@ def form_builder_page(request): 'is_required': cfg.is_required, 'locked': cfg.field_name in locked, 'page_key': page_key, + 'is_custom': False, + 'sort_order': cfg.sort_order, } ) + for cfg in custom_field_configs: + page_key = cfg.section_key or fallback + if page_key not in column_by_key: + page_key = fallback + column_by_key[page_key]['items'].append( + { + 'field_name': f'custom__{cfg.field_key}', + 'label': cfg.translated_label(language_code), + 'label_de': cfg.label, + 'label_en': cfg.label_en, + 'is_visible': cfg.is_active, + 'is_required': cfg.is_required, + 'locked': False, + 'page_key': page_key, + 'is_custom': True, + 'sort_order': cfg.sort_order, + } + ) + for column in columns: + column['items'].sort(key=lambda item: (item.get('sort_order', 9999), item['field_name'])) else: columns = [ { @@ -2316,8 +2536,30 @@ def form_builder_page(request): 'is_required': cfg.is_required, 'locked': cfg.field_name in locked, 'page_key': page_key, + 'is_custom': False, + 'sort_order': cfg.sort_order, } ) + for cfg in custom_field_configs: + page_key = cfg.section_key or fallback + if page_key not in column_by_key: + page_key = fallback + column_by_key[page_key]['items'].append( + { + 'field_name': f'custom__{cfg.field_key}', + 'label': cfg.translated_label(language_code), + 'label_de': cfg.label, + 'label_en': cfg.label_en, + 'is_visible': cfg.is_active, + 'is_required': cfg.is_required, + 'locked': False, + 'page_key': page_key, + 'is_custom': True, + 'sort_order': cfg.sort_order, + } + ) + for column in columns: + column['items'].sort(key=lambda item: (item.get('sort_order', 9999), item['field_name'])) section_rule_items = [] if section_order: @@ -2350,6 +2592,20 @@ def form_builder_page(request): } ) + custom_field_groups = [] + if section_order: + grouped_custom = {key: [] for key in section_order} + for cfg in custom_field_configs: + grouped_custom.setdefault(cfg.section_key, []).append(cfg) + for key in section_order: + custom_field_groups.append( + { + 'key': key, + 'title': section_labels.get(key, key), + 'items': grouped_custom.get(key, []), + } + ) + field_rule_groups = [] if section_order: grouped_rules = {key: [] for key in section_order} @@ -2393,6 +2649,8 @@ def form_builder_page(request): 'inherit_phone_number_choice', ]: conditional_field_choices.append((field_name, labels.get(field_name, field_name))) + for cfg in custom_field_configs: + conditional_field_choices.append((f'custom__{cfg.field_key}', cfg.translated_label(language_code))) conditional_target_titles = { 'business-card-box': _('Visitenkarten-Details'), 'employment-end-box': _('Vertragsende'), @@ -2417,16 +2675,26 @@ def form_builder_page(request): clauses = list(cfg.clauses or []) while len(clauses) < 2: clauses.append({'field': '', 'operator': 'equals', 'value': ''}) + if target_key.startswith('custom__'): + custom_field_key = target_key.replace('custom__', '', 1) + custom_field = next((item for item in custom_field_configs if item.field_key == custom_field_key), None) + target_title = custom_field.translated_label(language_code) if custom_field else target_key + target_description = _('Steuert die Sichtbarkeit dieses benutzerdefinierten Feldes.') + target_fields = [target_title] + else: + target_title = conditional_target_titles.get(target_key, target_key) + target_description = conditional_target_descriptions.get(target_key, '') + target_fields = [labels.get(name, name) for name in ONBOARDING_GROUPS.get(target_key, [])] conditional_rule_items.append( { 'target_key': target_key, - 'title': conditional_target_titles.get(target_key, target_key), - 'description': conditional_target_descriptions.get(target_key, ''), + 'title': target_title, + 'description': target_description, 'is_active': cfg.is_active, 'clauses': clauses[:2], 'field_choices': conditional_field_choices, 'operator_choices': CONDITIONAL_RULE_OPERATOR_CHOICES, - 'target_fields': [labels.get(name, name) for name in ONBOARDING_GROUPS.get(target_key, [])], + 'target_fields': target_fields, } ) @@ -2441,6 +2709,16 @@ def form_builder_page(request): item for item in field_rule_group_map.get(key, []) if item['locked'] or item['is_visible'] ] + visible_items.extend( + [ + { + 'label': cfg.translated_label(language_code), + 'locked': False, + } + for cfg in custom_field_configs + if cfg.section_key == key and cfg.is_active + ] + ) if section_visible: preview_sections.append( { @@ -2459,6 +2737,7 @@ def form_builder_page(request): 'configurable_field_count': configurable_field_count, 'hidden_field_count': hidden_field_count, 'hidden_section_count': hidden_section_count, + 'custom_field_count': len([cfg for cfg in custom_field_configs if cfg.is_active]), } return render( @@ -2479,6 +2758,8 @@ def form_builder_page(request): 'section_rule_items': section_rule_items, 'builder_summary': builder_summary, 'conditional_rule_items': conditional_rule_items, + 'custom_field_groups': custom_field_groups, + 'custom_field_type_choices': _translate_choice_list(FormCustomFieldConfig.FIELD_TYPE_CHOICES), 'active_panel': active_panel, 'active_subpanel': active_subpanel, 'available_presets': FORM_PRESETS.get(form_type, {}), @@ -2921,22 +3202,24 @@ def form_builder_save_order(request): form_type = payload.get('form_type') if form_type not in DEFAULT_FIELD_ORDER: return JsonResponse({'ok': False, 'error': 'Ungültiger Formulartyp.'}, status=400) + default_page_map = get_default_page_map(form_type) columns = payload.get('columns') if not isinstance(columns, dict): return JsonResponse({'ok': False, 'error': 'Spalten-Daten fehlen.'}, status=400) configs = list(FormFieldConfig.objects.filter(form_type=form_type).order_by('sort_order', 'field_name')) - allowed_names = {cfg.field_name for cfg in configs} + custom_configs = list(FormCustomFieldConfig.objects.filter(form_type=form_type).order_by('sort_order', 'field_key')) + allowed_names = {cfg.field_name for cfg in configs} | {f'custom__{cfg.field_key}' for cfg in custom_configs} seen = set() - ordered_names = [] if form_type == 'onboarding': allowed_columns = ONBOARDING_PAGE_ORDER else: - allowed_columns = ['all'] + allowed_columns = OFFBOARDING_PAGE_ORDER name_to_cfg = {cfg.field_name: cfg for cfg in configs} + custom_name_to_cfg = {f'custom__{cfg.field_key}': cfg for cfg in custom_configs} sort_order = 0 for column_key in allowed_columns: @@ -2950,14 +3233,15 @@ def form_builder_save_order(request): if name not in allowed_names or name in seen: continue seen.add(name) - ordered_names.append(name) - cfg = name_to_cfg[name] - cfg.sort_order = sort_order - sort_order += 1 - if form_type == 'onboarding': + if name in name_to_cfg: + cfg = name_to_cfg[name] + cfg.sort_order = sort_order cfg.page_key = column_key else: - cfg.page_key = '' + cfg = custom_name_to_cfg[name] + cfg.sort_order = sort_order + cfg.section_key = column_key + sort_order += 1 missing = [cfg.field_name for cfg in configs if cfg.field_name not in seen] for name in missing: @@ -2967,11 +3251,24 @@ def form_builder_save_order(request): if form_type == 'onboarding': cfg.page_key = cfg.page_key or ONBOARDING_DEFAULT_PAGE.get(name, 'abschluss') else: - cfg.page_key = '' + cfg.page_key = cfg.page_key or default_page_map.get(name, OFFBOARDING_PAGE_ORDER[-1]) + + missing_custom = [name for name in custom_name_to_cfg.keys() if name not in seen] + for name in missing_custom: + cfg = custom_name_to_cfg[name] + cfg.sort_order = sort_order + sort_order += 1 + if form_type == 'onboarding': + cfg.section_key = cfg.section_key or 'abschluss' + else: + cfg.section_key = cfg.section_key or OFFBOARDING_PAGE_ORDER[-1] FormFieldConfig.objects.bulk_update(configs, ['sort_order', 'page_key']) - _audit(request, 'form_layout_saved', target_type='form_config', target_label=form_type, details={'saved_count': len(configs)}) - return JsonResponse({'ok': True, 'saved_count': len(configs)}) + if custom_configs: + FormCustomFieldConfig.objects.bulk_update(custom_configs, ['sort_order', 'section_key']) + saved_count = len(configs) + len(custom_configs) + _audit(request, 'form_layout_saved', target_type='form_config', target_label=form_type, details={'saved_count': saved_count}) + return JsonResponse({'ok': True, 'saved_count': saved_count}) @_require_capability('manage_integrations')