diff --git a/PRODUCTIZATION_ROADMAP.md b/PRODUCTIZATION_ROADMAP.md index cb4480a..9d46875 100644 --- a/PRODUCTIZATION_ROADMAP.md +++ b/PRODUCTIZATION_ROADMAP.md @@ -79,7 +79,7 @@ Snapshot commit: ### Phase 1. Product Core Standardization -Status: next +Status: completed Purpose: @@ -117,8 +117,18 @@ Deliverables: - branding flow - PDF/letterhead override behavior +Delivered: + +- generic branding model and management UI +- shared branding context across shell/auth/pages +- configurable favicon, logo, sender display, footer/legal text, and PDF letterhead +- company-domain-driven email defaults and validation +- platform vs company admin separation for product-level controls + ### Phase 2. App Registry and Navigation +Status: completed + Purpose: - stop hardcoding app cards and app visibility in the homepage template @@ -129,9 +139,13 @@ Deliverables: - title / subtitle / icon / route / required capability / enabled flag - homepage and navigation driven by registry data - ability to enable/disable apps per deployment +- role-based app visibility and section grouping +- drag-and-drop ordering with filter-safe behavior ### Phase 3. Trial Mode Lifecycle +Status: completed + Purpose: - allow limited-time test environments for demos and sales @@ -145,9 +159,18 @@ Deliverables: - cleanup command / scheduled deletion - DB/media cleanup policy +Delivered: + +- platform-only trial management UI +- shared trial banner and expiry enforcement +- integration restriction during trial mode +- cleanup/verification management commands + ### Phase 4. New Business Apps -Only start after phases 1-3 are stable. +Status: next + +Only start after phases 1-3 are stable and the workflow regression suite is green. Candidate apps: @@ -191,22 +214,21 @@ These should move into configuration progressively, not all at once in one risky ## Immediate Next Slice -Implement first: +Implement next: -1. `PortalBranding` model -2. branding management page -3. shared branding context processor -4. replace header/logo/title references on: - - home - - shared header - - login/auth pages -5. make PDF letterhead configurable +1. restore and keep green the onboarding/offboarding regression suite +2. extend dynamic onboarding configuration: + - field visibility + - section visibility + - guarded required/optional controls +3. remove remaining hardcoded customer/product leakage from docs, fixtures, and fallback assets +4. continue security and observability hardening before the next business app -This is the first productization slice because it gives: +This is the next productization slice because it gives: -- generic portal identity -- customer-specific configurability -- a cleaner base for every future app +- reliable core workflow behavior +- safer deployment-neutral product defaults +- a configurable onboarding experience for future customers ## Guardrails diff --git a/backend/config/urls.py b/backend/config/urls.py index 81ef653..b783ae7 100644 --- a/backend/config/urls.py +++ b/backend/config/urls.py @@ -4,16 +4,18 @@ from django.contrib import admin from django.contrib.auth import views as auth_views from django.urls import include, path -from workflows.forms import AppAuthenticationForm, AppPasswordChangeForm, AppPasswordResetForm, AppSetPasswordForm +from workflows.forms import AppPasswordChangeForm, AppPasswordResetForm, AppSetPasswordForm +from workflows import views as workflow_views urlpatterns = [ path('admin/', admin.site.urls), path('i18n/', include('django.conf.urls.i18n')), path( 'accounts/login/', - auth_views.LoginView.as_view(template_name='workflows/auth/login.html', authentication_form=AppAuthenticationForm), + workflow_views.login_page, name='login', ), + path('accounts/login/totp/', workflow_views.login_totp_page, name='login_totp'), path( 'accounts/logout/', auth_views.LogoutView.as_view(), diff --git a/backend/locale/en/LC_MESSAGES/django.mo b/backend/locale/en/LC_MESSAGES/django.mo index 612fd69..c9e4361 100644 Binary files a/backend/locale/en/LC_MESSAGES/django.mo and b/backend/locale/en/LC_MESSAGES/django.mo differ diff --git a/backend/locale/en/LC_MESSAGES/django.po b/backend/locale/en/LC_MESSAGES/django.po index 2a65f22..469ca79 100644 --- a/backend/locale/en/LC_MESSAGES/django.po +++ b/backend/locale/en/LC_MESSAGES/django.po @@ -2,14 +2,14 @@ msgid "" msgstr "" "Project-Id-Version: tubco-portal\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-03-27 01:48+0000\n" +"POT-Creation-Date: 2026-03-27 10:24+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:409 workflows/models.py:490 +#: workflows/app_registry.py:35 workflows/models.py:482 workflows/models.py:563 #: workflows/templates/workflows/onboarding_form.html:25 #: workflows/templates/workflows/requests_dashboard.html:68 #: workflows/templates/workflows/requests_dashboard.html:131 @@ -36,7 +36,7 @@ msgstr "Multi-step form" msgid "E-Mail Routing" msgstr "Email routing" -#: workflows/app_registry.py:46 workflows/models.py:410 workflows/models.py:491 +#: workflows/app_registry.py:46 workflows/models.py:483 workflows/models.py:564 #: workflows/templates/workflows/requests_dashboard.html:78 #: workflows/templates/workflows/requests_dashboard.html:132 msgid "Offboarding" @@ -90,7 +90,6 @@ msgid "Suche" msgstr "Search" #: workflows/app_registry.py:62 -#: workflows/templates/workflows/account_profile.html:218 #: workflows/templates/workflows/app_registry.html:32 #: workflows/templates/workflows/backup_recovery.html:72 #: workflows/templates/workflows/job_monitor.html:29 @@ -127,6 +126,7 @@ 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/includes/app_header.html:57 msgid "Öffnen" msgstr "Open" @@ -263,7 +263,7 @@ msgstr "" msgid "Alle Firmenrollen" msgstr "" -#: workflows/app_registry.py:317 workflows/models.py:186 +#: workflows/app_registry.py:317 workflows/models.py:259 #: workflows/templates/workflows/app_registry.html:44 #: workflows/templates/workflows/app_registry.html:100 msgid "Apps" @@ -273,7 +273,7 @@ msgstr "Apps" msgid "Wählen Sie den gewünschten Prozess." msgstr "Choose the desired process." -#: workflows/app_registry.py:323 workflows/models.py:187 +#: workflows/app_registry.py:323 workflows/models.py:260 #: workflows/templates/workflows/app_registry.html:45 #: workflows/templates/workflows/app_registry.html:96 msgid "Platform Apps" @@ -285,7 +285,7 @@ msgstr "" msgid "Produktweite Konfiguration und Produktsteuerung." msgstr "Configuration, tests, and controls." -#: workflows/app_registry.py:329 workflows/models.py:188 +#: workflows/app_registry.py:329 workflows/models.py:261 #: workflows/templates/workflows/app_registry.html:46 #: workflows/templates/workflows/app_registry.html:98 msgid "Admin Apps" @@ -384,132 +384,222 @@ msgstr "" msgid "Remote Backup in Nextcloud konnte nicht gelöscht werden." msgstr "" -#: workflows/forms.py:106 workflows/forms.py:399 -#: workflows/templates/workflows/account_profile.html:66 +#: workflows/forms.py:106 workflows/forms.py:473 #: workflows/templates/workflows/user_management.html:72 #: workflows/templates/workflows/user_management.html:170 msgid "Benutzername" msgstr "" -#: workflows/forms.py:107 +#: workflows/forms.py:108 msgid "Passwort" msgstr "Password" -#: workflows/forms.py:109 workflows/forms.py:279 workflows/forms.py:311 -#: workflows/forms.py:357 +#: workflows/forms.py:114 +msgid "Benutzername oder Passwort sind nicht korrekt." +msgstr "" + +#: workflows/forms.py:115 +#, fuzzy +#| msgid "Deaktivieren" +msgid "Dieses Konto ist deaktiviert." +msgstr "Disabled" + +#: workflows/forms.py:141 workflows/forms.py:304 workflows/forms.py:356 msgid "TOTP-Code" msgstr "" -#: workflows/forms.py:115 workflows/forms.py:317 workflows/forms.py:363 +#: workflows/forms.py:147 workflows/forms.py:362 msgid "Recovery-Code" msgstr "" -#: workflows/forms.py:123 workflows/forms.py:300 workflows/forms.py:343 -#: workflows/forms.py:389 +#: workflows/forms.py:154 workflows/forms.py:325 workflows/forms.py:382 msgid "Der TOTP-Code ist ungültig." msgstr "" -#: workflows/forms.py:124 +#: workflows/forms.py:155 msgid "Bitte geben Sie Ihren TOTP-Code ein." msgstr "" -#: workflows/forms.py:157 workflows/forms.py:221 workflows/forms.py:400 +#: workflows/forms.py:182 workflows/forms.py:246 workflows/forms.py:474 #, fuzzy #| msgid "E-Mail" msgid "E-Mail-Adresse" msgstr "Email" -#: workflows/forms.py:162 workflows/forms.py:181 +#: workflows/forms.py:187 workflows/forms.py:206 #: workflows/templates/workflows/user_management.html:77 #: workflows/templates/workflows/user_management.html:108 msgid "Neues Passwort" msgstr "New password" -#: workflows/forms.py:168 workflows/forms.py:187 +#: workflows/forms.py:193 workflows/forms.py:212 msgid "Neues Passwort bestätigen" msgstr "Confirm new password" -#: workflows/forms.py:176 workflows/forms.py:274 workflows/forms.py:306 -#: workflows/forms.py:352 +#: workflows/forms.py:201 workflows/forms.py:299 workflows/forms.py:331 #, fuzzy #| msgid "Neues Passwort" msgid "Aktuelles Passwort" msgstr "New password" -#: workflows/forms.py:198 workflows/templates/workflows/account_profile.html:36 -#: workflows/templates/workflows/includes/app_header.html:27 +#: workflows/forms.py:223 workflows/templates/workflows/account_profile.html:36 +#: workflows/templates/workflows/includes/app_header.html:79 msgid "Profilbild" msgstr "" -#: workflows/forms.py:214 +#: workflows/forms.py:239 msgid "Das Profilbild darf maximal 5 MB groß sein." msgstr "" -#: workflows/forms.py:219 workflows/forms.py:397 -#: workflows/templates/workflows/account_profile.html:112 +#: workflows/forms.py:244 workflows/forms.py:471 +#: workflows/templates/workflows/account_profile.html:116 msgid "Vorname" msgstr "" -#: workflows/forms.py:220 workflows/forms.py:398 -#: workflows/templates/workflows/account_profile.html:116 +#: workflows/forms.py:245 workflows/forms.py:472 +#: workflows/templates/workflows/account_profile.html:120 msgid "Nachname" msgstr "" -#: workflows/forms.py:222 -#: workflows/templates/workflows/account_profile.html:120 +#: workflows/forms.py:247 +#: workflows/templates/workflows/account_profile.html:124 msgid "Telefon" msgstr "" -#: workflows/forms.py:223 -#: workflows/templates/workflows/account_profile.html:124 +#: workflows/forms.py:248 +#: workflows/templates/workflows/account_profile.html:128 msgid "Mobil" msgstr "" -#: workflows/forms.py:224 workflows/templates/workflows/account_profile.html:70 -#: workflows/templates/workflows/account_profile.html:128 +#: workflows/forms.py:249 +#: workflows/templates/workflows/account_profile.html:132 #, fuzzy #| msgid "Produktion" msgid "Position" msgstr "Production" -#: workflows/forms.py:225 workflows/models.py:370 -#: workflows/templates/workflows/account_profile.html:74 -#: workflows/templates/workflows/account_profile.html:132 +#: workflows/forms.py:250 workflows/models.py:443 +#: 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:226 -#: workflows/templates/workflows/account_profile.html:136 +#: workflows/forms.py:251 +#: workflows/templates/workflows/account_profile.html:140 msgid "Standort" msgstr "" -#: workflows/forms.py:228 -#: workflows/templates/workflows/account_profile.html:140 +#: workflows/forms.py:253 +#: workflows/templates/workflows/account_profile.html:144 #, fuzzy #| msgid "Einweisung" msgid "Hinweise" msgstr "Introduction" -#: workflows/forms.py:292 workflows/forms.py:331 workflows/forms.py:377 +#: workflows/forms.py:317 workflows/forms.py:344 msgid "Das aktuelle Passwort ist nicht korrekt." msgstr "" -#: workflows/forms.py:298 +#: workflows/forms.py:323 msgid "Bitte geben Sie einen gültigen TOTP-Code ein." msgstr "" -#: workflows/forms.py:339 workflows/forms.py:385 +#: workflows/forms.py:350 +#, fuzzy +#| msgid "Deaktivieren" +msgid "TOTP ist für dieses Konto nicht aktiv." +msgstr "Disabled" + +#: workflows/forms.py:378 msgid "Bitte geben Sie einen gültigen TOTP-Code oder Recovery-Code ein." msgstr "" -#: workflows/forms.py:346 workflows/forms.py:392 +#: workflows/forms.py:385 msgid "Der Recovery-Code ist ungültig." msgstr "" -#: workflows/forms.py:401 workflows/templates/workflows/account_profile.html:62 -#: workflows/templates/workflows/user_management.html:74 +#: workflows/forms.py:390 +#, fuzzy +#| msgid "Offboarding-Anfrage speichern" +msgid "Onboarding erfolgreich" +msgstr "Save offboarding request" + +#: workflows/forms.py:391 +#, fuzzy +#| msgid "Fehlgeschlagen" +msgid "Onboarding fehlgeschlagen" +msgstr "Failed" + +#: workflows/forms.py:392 +#, fuzzy +#| msgid "Offboarding-Anfrage speichern" +msgid "Offboarding erfolgreich" +msgstr "Save offboarding request" + +#: workflows/forms.py:393 +#, fuzzy +#| msgid "Offboarding-Anfrage speichern" +msgid "Offboarding fehlgeschlagen" +msgstr "Save offboarding request" + +#: workflows/forms.py:394 +#, fuzzy +#| msgid "Eingereicht" +msgid "Backup erfolgreich" +msgstr "Submitted" + +#: workflows/forms.py:395 workflows/views.py:1348 +#, fuzzy +#| msgid "Fehlgeschlagen" +msgid "Backup fehlgeschlagen" +msgstr "Failed" + +#: workflows/forms.py:396 +#, fuzzy +#| msgid "Welcome E-Mails" +msgid "Welcome E-Mail erfolgreich" +msgstr "Welcome Emails" + +#: workflows/forms.py:397 +#, fuzzy +#| msgid "Welcome E-Mails" +msgid "Welcome E-Mail fehlgeschlagen" +msgstr "Welcome Emails" + +#: workflows/forms.py:398 +#, fuzzy +#| msgid "Einweisung" +msgid "Trial-Hinweise" +msgstr "Introduction" + +#: workflows/forms.py:399 +#, fuzzy +#| msgid "Einweisung" +msgid "System-Hinweise" +msgstr "Introduction" + +#: workflows/forms.py:415 +#, fuzzy +#| msgid "Workflow-Regeln" +msgid "Workflow" +msgstr "Workflow rules" + +#: workflows/forms.py:416 workflows/views.py:1505 +#, fuzzy +#| msgid "Welcome E-Mails" +msgid "Welcome E-Mail" +msgstr "Welcome Emails" + +#: workflows/forms.py:417 workflows/templates/workflows/handbook.html:21 +msgid "Operations" +msgstr "Operations" + +#: workflows/forms.py:418 +msgid "Platform" +msgstr "" + +#: workflows/forms.py:475 workflows/templates/workflows/user_management.html:74 #: workflows/templates/workflows/user_management.html:93 #: workflows/templates/workflows/user_management.html:171 #, fuzzy @@ -517,207 +607,207 @@ msgstr "" msgid "Rolle" msgstr "Role:" -#: workflows/forms.py:415 +#: workflows/forms.py:489 msgid "Dieser Benutzername ist bereits vergeben." msgstr "This username is already taken." -#: workflows/forms.py:424 workflows/views.py:1020 +#: workflows/forms.py:498 workflows/views.py:1157 msgid "Ungültige Rolle." msgstr "Invalid role." -#: workflows/forms.py:426 workflows/views.py:1023 +#: workflows/forms.py:500 workflows/views.py:1160 msgid "Nur Platform Owner dürfen diese Rolle vergeben." msgstr "" -#: workflows/forms.py:465 +#: workflows/forms.py:539 msgid "Portal-Titel" msgstr "Portal title" -#: workflows/forms.py:466 +#: workflows/forms.py:540 msgid "Firmenname" msgstr "Company name" -#: workflows/forms.py:467 +#: workflows/forms.py:541 #, fuzzy #| msgid "Firmenname" msgid "Firmen-Domain" msgstr "Company name" -#: workflows/forms.py:468 +#: workflows/forms.py:542 msgid "Support-E-Mail" msgstr "Support email" -#: workflows/forms.py:469 +#: workflows/forms.py:543 msgid "Absender-Anzeigename" msgstr "" -#: workflows/forms.py:470 +#: workflows/forms.py:544 msgid "Login-Untertitel" msgstr "" -#: workflows/forms.py:471 +#: workflows/forms.py:545 msgid "Footer-Text DE" msgstr "" -#: workflows/forms.py:472 +#: workflows/forms.py:546 msgid "Footer-Text EN" msgstr "" -#: workflows/forms.py:473 +#: workflows/forms.py:547 msgid "Rechtlicher Hinweis DE" msgstr "" -#: workflows/forms.py:474 +#: workflows/forms.py:548 msgid "Rechtlicher Hinweis EN" msgstr "" -#: workflows/forms.py:475 +#: workflows/forms.py:549 msgid "Standardsprache" msgstr "Default language" -#: workflows/forms.py:476 +#: workflows/forms.py:550 msgid "Logo" msgstr "Logo" -#: workflows/forms.py:477 +#: workflows/forms.py:551 msgid "PDF-Briefkopf" msgstr "PDF letterhead" -#: workflows/forms.py:478 +#: workflows/forms.py:552 msgid "Favicon" msgstr "" -#: workflows/forms.py:479 +#: workflows/forms.py:553 #: workflows/templates/workflows/branding_settings.html:89 #: workflows/templates/workflows/branding_settings.html:162 msgid "Primärfarbe" msgstr "Primary color" -#: workflows/forms.py:480 +#: workflows/forms.py:554 #: workflows/templates/workflows/branding_settings.html:90 #: workflows/templates/workflows/branding_settings.html:163 msgid "Sekundärfarbe" msgstr "Secondary color" -#: workflows/forms.py:497 +#: workflows/forms.py:571 msgid "Das Logo darf maximal 5 MB groß sein." msgstr "" -#: workflows/forms.py:505 +#: workflows/forms.py:579 msgid "Der PDF-Briefkopf darf maximal 10 MB groß sein." msgstr "" -#: workflows/forms.py:513 +#: workflows/forms.py:587 msgid "Das Favicon darf maximal 2 MB groß sein." msgstr "" -#: workflows/forms.py:537 +#: workflows/forms.py:611 #, fuzzy #| msgid "Firmenname" msgid "Rechtlicher Firmenname" msgstr "Company name" -#: workflows/forms.py:538 +#: workflows/forms.py:612 msgid "Straße und Hausnummer" msgstr "" -#: workflows/forms.py:539 +#: workflows/forms.py:613 msgid "Postleitzahl" msgstr "" -#: workflows/forms.py:540 +#: workflows/forms.py:614 msgid "Stadt" msgstr "" -#: workflows/forms.py:541 +#: workflows/forms.py:615 msgid "Land" msgstr "" -#: workflows/forms.py:542 workflows/templates/workflows/base_shell.html:64 +#: workflows/forms.py:616 workflows/templates/workflows/base_shell.html:64 msgid "Website" msgstr "" -#: workflows/forms.py:543 +#: workflows/forms.py:617 msgid "Impressum-URL" msgstr "" -#: workflows/forms.py:544 +#: workflows/forms.py:618 msgid "Datenschutz-URL" msgstr "" -#: workflows/forms.py:545 +#: workflows/forms.py:619 msgid "HR-Kontakt" msgstr "" -#: workflows/forms.py:546 +#: workflows/forms.py:620 msgid "IT-Kontakt" msgstr "" -#: workflows/forms.py:547 +#: workflows/forms.py:621 #, fuzzy #| msgid "Operations" msgid "Operations-Kontakt" msgstr "Operations" -#: workflows/forms.py:548 +#: workflows/forms.py:622 msgid "Zentrale Telefonnummer" msgstr "" -#: workflows/forms.py:549 +#: workflows/forms.py:623 msgid "USt-IdNr." msgstr "" -#: workflows/forms.py:550 +#: workflows/forms.py:624 msgid "Register- oder Handelsnummer" msgstr "" -#: workflows/forms.py:567 +#: workflows/forms.py:641 msgid "Trial-Modus aktiv" msgstr "" -#: workflows/forms.py:568 +#: workflows/forms.py:642 msgid "Trial-Beginn" msgstr "" -#: workflows/forms.py:569 +#: workflows/forms.py:643 msgid "Trial-Ende" msgstr "" -#: workflows/forms.py:570 +#: workflows/forms.py:644 msgid "Produktive Integrationen begrenzen" msgstr "" -#: workflows/forms.py:571 +#: workflows/forms.py:645 msgid "Cleanup nach Ablauf zulassen" msgstr "" -#: workflows/forms.py:572 +#: workflows/forms.py:646 msgid "Banner-Text DE" msgstr "" -#: workflows/forms.py:573 +#: workflows/forms.py:647 msgid "Banner-Text EN" msgstr "" -#: workflows/forms.py:593 +#: workflows/forms.py:667 msgid "Bitte ein Trial-Ende festlegen." msgstr "" -#: workflows/forms.py:595 +#: workflows/forms.py:669 msgid "Das Trial-Ende muss nach dem Trial-Beginn liegen." msgstr "" -#: workflows/forms.py:734 workflows/forms.py:919 +#: workflows/forms.py:808 workflows/forms.py:993 #, python-format msgid "Bitte nutzen Sie das Format name@%(domain)s." msgstr "" -#: workflows/forms.py:756 workflows/forms.py:933 +#: workflows/forms.py:830 workflows/forms.py:1007 #, python-format msgid "Bitte verwenden Sie eine @%(domain)s E-Mail-Adresse." msgstr "" -#: workflows/forms.py:841 +#: workflows/forms.py:915 #, python-format msgid "" "Das Übergabedatum muss mindestens %(days)s Tage in der Zukunft liegen " @@ -763,257 +853,275 @@ msgid "" "an." msgstr "" -#: workflows/middleware.py:165 +#: workflows/middleware.py:171 msgid "" "Bitte bestätigen Sie Ihre Identität erneut, bevor Sie diese sensible Aktion " "ausführen." msgstr "" -#: workflows/models.py:235 workflows/views.py:493 +#: workflows/models.py:129 +msgid "Info" +msgstr "" + +#: workflows/models.py:130 +#, fuzzy +#| msgid "Eingereicht" +msgid "Erfolg" +msgstr "Submitted" + +#: workflows/models.py:131 +msgid "Warnung" +msgstr "" + +#: workflows/models.py:132 workflows/templates/workflows/job_monitor.html:53 +msgid "Fehler" +msgstr "" + +#: workflows/models.py:308 workflows/views.py:601 #, fuzzy #| msgid "Gesamtbestand" msgid "Gestartet" msgstr "Total records" -#: workflows/models.py:236 workflows/views.py:493 +#: workflows/models.py:309 workflows/views.py:601 #, fuzzy #| msgid "Eingereicht" msgid "Erfolgreich" msgstr "Submitted" -#: workflows/models.py:237 workflows/models.py:290 workflows/models.py:544 +#: workflows/models.py:310 workflows/models.py:363 workflows/models.py:617 #: workflows/templates/workflows/backup_recovery.html:102 #: workflows/templates/workflows/requests_dashboard.html:222 -#: workflows/templates/workflows/welcome_emails.html:108 workflows/views.py:319 -#: workflows/views.py:493 +#: workflows/templates/workflows/welcome_emails.html:108 workflows/views.py:427 +#: workflows/views.py:601 msgid "Fehlgeschlagen" msgstr "Failed" -#: workflows/models.py:287 workflows/views.py:316 +#: workflows/models.py:360 workflows/views.py:424 msgid "Eingereicht" msgstr "Submitted" -#: workflows/models.py:288 workflows/views.py:317 +#: workflows/models.py:361 workflows/views.py:425 msgid "In Bearbeitung" msgstr "Processing" -#: workflows/models.py:289 workflows/models.py:604 workflows/views.py:318 +#: workflows/models.py:362 workflows/models.py:677 workflows/views.py:426 msgid "Abgeschlossen" msgstr "Completed" -#: workflows/models.py:297 +#: workflows/models.py:370 msgid "Herr" msgstr "" -#: workflows/models.py:297 +#: workflows/models.py:370 msgid "Frau" msgstr "" -#: workflows/models.py:297 +#: workflows/models.py:370 msgid "Divers" msgstr "" -#: workflows/models.py:307 +#: workflows/models.py:380 msgid "befristet" msgstr "" -#: workflows/models.py:307 +#: workflows/models.py:380 msgid "unbefristet" msgstr "" -#: workflows/models.py:371 +#: workflows/models.py:444 msgid "Geräte" msgstr "" -#: workflows/models.py:372 +#: workflows/models.py:445 msgid "Software" msgstr "" -#: workflows/models.py:373 +#: workflows/models.py:446 #, fuzzy #| msgid "Vorgänge" msgid "Zugänge" msgstr "Requests" -#: workflows/models.py:374 +#: workflows/models.py:447 msgid "Workspace-Gruppen" msgstr "" -#: workflows/models.py:375 +#: workflows/models.py:448 msgid "Ressourcen" msgstr "" -#: workflows/models.py:376 +#: workflows/models.py:449 msgid "Telefonnummern" msgstr "" -#: workflows/models.py:402 +#: workflows/models.py:475 msgid "Automatisch" msgstr "" -#: workflows/models.py:403 workflows/views.py:103 +#: workflows/models.py:476 workflows/views.py:119 msgid "Stammdaten" msgstr "Master data" -#: workflows/models.py:404 workflows/views.py:104 +#: workflows/models.py:477 workflows/views.py:120 msgid "Vertrag" msgstr "Contract" -#: workflows/models.py:405 workflows/views.py:105 +#: workflows/models.py:478 workflows/views.py:121 msgid "IT-Setup" msgstr "IT setup" -#: workflows/models.py:406 workflows/views.py:106 +#: workflows/models.py:479 workflows/views.py:122 msgid "Abschluss" msgstr "Finish" -#: workflows/models.py:448 +#: workflows/models.py:521 #, fuzzy #| msgid "Onboarding" msgid "Onboarding: IT" msgstr "Onboarding" -#: workflows/models.py:449 +#: workflows/models.py:522 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Onboarding: Allgemeine Info" msgstr "Save offboarding request" -#: workflows/models.py:450 +#: workflows/models.py:523 #, fuzzy #| msgid "Onboarding starten" msgid "Onboarding: Visitenkarte" msgstr "Start onboarding" -#: workflows/models.py:451 +#: workflows/models.py:524 #, fuzzy #| msgid "Onboarding" msgid "Onboarding: HR Works" msgstr "Onboarding" -#: workflows/models.py:452 +#: workflows/models.py:525 #, fuzzy #| msgid "Onboarding starten" msgid "Onboarding: Schlüssel" msgstr "Start onboarding" -#: workflows/models.py:453 +#: workflows/models.py:526 msgid "Onboarding: Referenz Anfordernde Person" msgstr "" -#: workflows/models.py:454 +#: workflows/models.py:527 #, fuzzy #| msgid "Welcome E-Mails" msgid "Onboarding: Welcome E-Mail" msgstr "Welcome Emails" -#: workflows/models.py:455 +#: workflows/models.py:528 #, fuzzy #| msgid "Offboarding" msgid "Offboarding: IT" msgstr "Offboarding" -#: workflows/models.py:456 +#: workflows/models.py:529 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Offboarding: Allgemeine Info" msgstr "Save offboarding request" -#: workflows/models.py:457 +#: workflows/models.py:530 #, fuzzy #| msgid "Offboarding starten" msgid "Offboarding: HR Works Deaktivierung" msgstr "Start offboarding" -#: workflows/models.py:458 +#: workflows/models.py:531 msgid "Offboarding: Referenz Anfordernde Person" msgstr "" -#: workflows/models.py:494 +#: workflows/models.py:567 msgid "Immer" msgstr "" -#: workflows/models.py:495 workflows/models.py:573 +#: workflows/models.py:568 workflows/models.py:646 msgid "Enthält" msgstr "" -#: workflows/models.py:496 workflows/models.py:574 +#: workflows/models.py:569 workflows/models.py:647 msgid "Ist gleich" msgstr "" -#: workflows/models.py:497 +#: workflows/models.py:570 msgid "Ist aktiv/Ja" msgstr "" -#: workflows/models.py:498 +#: workflows/models.py:571 #, fuzzy #| msgid "inaktiv" msgid "Ist inaktiv/Nein" msgstr "inactive" -#: workflows/models.py:540 +#: workflows/models.py:613 #: workflows/templates/workflows/welcome_emails.html:100 msgid "Geplant" msgstr "Scheduled" -#: workflows/models.py:541 +#: workflows/models.py:614 #: workflows/templates/workflows/welcome_emails.html:102 msgid "Pausiert" msgstr "Paused" -#: workflows/models.py:542 +#: workflows/models.py:615 #: workflows/templates/workflows/welcome_emails.html:104 msgid "Abgebrochen" msgstr "Cancelled" -#: workflows/models.py:543 +#: workflows/models.py:616 #: workflows/templates/workflows/welcome_emails.html:106 msgid "Gesendet" msgstr "Sent" -#: workflows/models.py:566 workflows/tasks.py:600 +#: workflows/models.py:639 workflows/tasks.py:627 msgid "Geräte und Arbeitsplatz" msgstr "Devices and workplace" -#: workflows/models.py:567 workflows/tasks.py:601 +#: workflows/models.py:640 workflows/tasks.py:628 msgid "Konten und Berechtigungen" msgstr "Accounts and permissions" -#: workflows/models.py:568 workflows/tasks.py:602 +#: workflows/models.py:641 workflows/tasks.py:629 msgid "Software und Tools" msgstr "Software and tools" -#: workflows/models.py:569 workflows/tasks.py:603 +#: workflows/models.py:642 workflows/tasks.py:630 msgid "Prozesse und Hinweise" msgstr "Processes and notes" -#: workflows/models.py:572 +#: workflows/models.py:645 msgid "Immer anzeigen" msgstr "Always show" -#: workflows/models.py:575 +#: workflows/models.py:648 msgid "Ist Ja / aktiv" msgstr "Is yes / active" -#: workflows/models.py:576 +#: workflows/models.py:649 msgid "Ist Nein / inaktiv" msgstr "Is no / inactive" -#: workflows/models.py:603 +#: workflows/models.py:676 msgid "Entwurf" msgstr "Draft" -#: workflows/models.py:623 +#: workflows/models.py:696 #, fuzzy #| msgid "Nextcloud:" msgid "Nextcloud" msgstr "Nextcloud:" -#: workflows/models.py:624 +#: workflows/models.py:697 msgid "S3" msgstr "" -#: workflows/models.py:625 +#: workflows/models.py:698 msgid "NFS" msgstr "" @@ -1040,94 +1148,146 @@ msgstr "IT Staff" msgid "Mitarbeiter" msgstr "Staff" -#: workflows/tasks.py:616 +#: workflows/tasks.py:270 +#, fuzzy, python-format +#| msgid "Welcome E-Mails" +msgid "Welcome E-Mail gesendet: %(name)s" +msgstr "Welcome Emails" + +#: workflows/tasks.py:272 +#, fuzzy, python-format +#| msgid "Fehlgeschlagen" +msgid "Welcome E-Mail fehlgeschlagen: %(name)s" +msgstr "Failed" + +#: workflows/tasks.py:643 #, python-format msgid "%(item)s übergeben und Grundfunktionen erklärt" msgstr "%(item)s handed over and basic functions explained" -#: workflows/tasks.py:618 +#: workflows/tasks.py:645 #, python-format msgid "%(item)s gezeigt bzw. Nutzung erklärt" msgstr "%(item)s shown or usage explained" -#: workflows/tasks.py:620 +#: workflows/tasks.py:647 #, python-format msgid "Telefonnummer / Direktwahl erklärt: %(value)s" msgstr "Phone number / direct extension explained: %(value)s" -#: workflows/tasks.py:622 +#: workflows/tasks.py:649 msgid "Arbeitsplatz, Geräte und allgemeine Nutzung besprochen" msgstr "Workplace, devices, and general usage reviewed" -#: workflows/tasks.py:624 +#: workflows/tasks.py:651 #, python-format msgid "%(item)s Zugang erklärt" msgstr "%(item)s access explained" -#: workflows/tasks.py:625 +#: workflows/tasks.py:652 #, python-format msgid "%(item)s Gruppe / Berechtigung erläutert" msgstr "%(item)s group / permission explained" -#: workflows/tasks.py:627 +#: workflows/tasks.py:654 #, python-format msgid "Dienstliche E-Mail-Adresse erläutert: %(value)s" msgstr "Work email address explained: %(value)s" -#: workflows/tasks.py:629 +#: workflows/tasks.py:656 #, python-format msgid "Gruppenpostfach erklärt: %(item)s" msgstr "Group mailbox explained: %(item)s" -#: workflows/tasks.py:631 +#: workflows/tasks.py:658 msgid "Zugänge, Konten und Anmeldelogik besprochen" msgstr "Accesses, accounts, and login logic reviewed" -#: workflows/tasks.py:633 +#: workflows/tasks.py:660 #, python-format msgid "%(item)s Einführung durchgeführt" msgstr "%(item)s introduction completed" -#: workflows/tasks.py:634 +#: workflows/tasks.py:661 #, python-format msgid "%(item)s zusätzlich besprochen" msgstr "%(item)s discussed additionally" -#: workflows/tasks.py:636 +#: workflows/tasks.py:663 msgid "Benötigte Standardsoftware und tägliche Nutzung erklärt" msgstr "Required standard software and daily usage explained" -#: workflows/tasks.py:639 +#: workflows/tasks.py:666 msgid "Passwortregeln und sicherer Umgang besprochen" msgstr "Password rules and secure handling reviewed" -#: workflows/tasks.py:640 +#: workflows/tasks.py:667 msgid "Dateiablage, Nextcloud und Freigaben erklärt" msgstr "File storage, Nextcloud, and sharing explained" -#: workflows/tasks.py:641 +#: workflows/tasks.py:668 msgid "Kommunikationswege und Support-Prozess erklärt" msgstr "Communication channels and support process explained" -#: workflows/tasks.py:644 +#: workflows/tasks.py:671 #, python-format msgid "%(item)s als zusätzliche Ausstattung besprochen" msgstr "%(item)s discussed as additional equipment" -#: workflows/tasks.py:646 +#: workflows/tasks.py:673 #, python-format msgid "Zusätzlicher Zugang besprochen: %(item)s" msgstr "Additional access discussed: %(item)s" -#: workflows/tasks.py:648 +#: workflows/tasks.py:675 #, python-format msgid "Übergabe-/Nachfolgekontext besprochen: %(value)s" msgstr "Handover / successor context reviewed: %(value)s" +#: workflows/tasks.py:1363 +#, fuzzy, python-format +#| msgid "Einweisung wurde als abgeschlossen gespeichert." +msgid "Onboarding abgeschlossen: %(name)s" +msgstr "Introduction was saved as completed." + +#: workflows/tasks.py:1364 +msgid "Die Onboarding-Anfrage wurde erfolgreich verarbeitet." +msgstr "" + +#: workflows/tasks.py:1375 +#, fuzzy, python-format +#| msgid "Fehlgeschlagen" +msgid "Onboarding fehlgeschlagen: %(name)s" +msgstr "Failed" + +#: workflows/tasks.py:1464 +#, fuzzy, python-format +#| msgid "Offboarding-Anfrage speichern" +msgid "Offboarding abgeschlossen: %(name)s" +msgstr "Save offboarding request" + +#: workflows/tasks.py:1465 +#, fuzzy +#| msgid "Offboarding-Anfrage speichern" +msgid "Die Offboarding-Anfrage wurde erfolgreich verarbeitet." +msgstr "Save offboarding request" + +#: workflows/tasks.py:1476 +#, fuzzy, python-format +#| msgid "Offboarding-Anfrage speichern" +msgid "Offboarding fehlgeschlagen: %(name)s" +msgstr "Save offboarding request" + +#: workflows/tasks.py:1551 +#, fuzzy +#| msgid "Offboarding-Anfrage speichern" +msgid "Die geplante Welcome E-Mail wurde erfolgreich versendet." +msgstr "Save offboarding request" + #: workflows/templates/registration/login.html:4 #: workflows/templates/registration/login.html:19 #: workflows/templates/workflows/auth/login.html:4 -#: workflows/templates/workflows/auth/login.html:17 +#: workflows/templates/workflows/auth/login.html:27 msgid "Anmeldung" msgstr "Sign in" @@ -1136,19 +1296,20 @@ msgid "Bitte melden Sie sich mit Ihrem Benutzerkonto an." msgstr "Please sign in with your user account." #: workflows/templates/registration/login.html:30 -#: workflows/templates/workflows/auth/login.html:28 +#: workflows/templates/workflows/auth/login.html:43 #, fuzzy #| msgid "Fehlgeschlagen" msgid "Anmeldung fehlgeschlagen" msgstr "Failed" #: workflows/templates/registration/login.html:31 +#: workflows/templates/workflows/auth/login.html:44 msgid "" "Benutzername oder Passwort sind nicht korrekt. Bitte versuchen Sie es erneut." msgstr "" #: workflows/templates/registration/login.html:37 -#: workflows/templates/workflows/auth/login.html:43 +#: workflows/templates/workflows/auth/login.html:74 msgid "Anmelden" msgstr "Sign in" @@ -1257,7 +1418,7 @@ msgstr "Request link" #: workflows/templates/workflows/account_profile.html:4 #: workflows/templates/workflows/account_profile.html:20 -#: workflows/templates/workflows/includes/app_header.html:47 +#: workflows/templates/workflows/includes/app_header.html:99 msgid "Profil" msgstr "Profile" @@ -1266,27 +1427,46 @@ msgid "Konto" msgstr "Account" #: workflows/templates/workflows/account_profile.html:21 -msgid "Ihre aktuelle Workdock-Kontoübersicht und wichtige Sicherheitsaktionen." +#, fuzzy +#| msgid "" +#| "Ihre aktuelle Workdock-Kontoübersicht und wichtige Sicherheitsaktionen." +msgid "Ihre aktuelle Kontoübersicht und wichtige Sicherheitsaktionen." msgstr "Your current Workdock account overview and important security actions." +#: workflows/templates/workflows/account_profile.html:23 +#: workflows/templates/workflows/user_management.html:76 +msgid "Letzte Anmeldung" +msgstr "Last login" + #: workflows/templates/workflows/account_profile.html:54 msgid "Klicken Sie auf das Bild, um ein neues Profilbild auszuwählen." msgstr "Click the image to choose a new profile picture." -#: workflows/templates/workflows/account_profile.html:78 -#: workflows/templates/workflows/user_management.html:76 -msgid "Letzte Anmeldung" -msgstr "Last login" - -#: workflows/templates/workflows/account_profile.html:88 +#: workflows/templates/workflows/account_profile.html:67 +#: workflows/templates/workflows/account_profile.html:92 msgid "Kontodaten" msgstr "Account details" -#: workflows/templates/workflows/account_profile.html:89 +#: workflows/templates/workflows/account_profile.html:75 +#: workflows/templates/workflows/account_profile.html:256 +msgid "Sicherheit & Aktionen" +msgstr "Security & actions" + +#: workflows/templates/workflows/account_profile.html:83 +#: workflows/templates/workflows/account_profile.html:178 +#: workflows/templates/workflows/includes/app_header.html:24 +#: workflows/templates/workflows/includes/app_header.html:37 +#, fuzzy +#| msgid "Offboarding-Anfrage speichern" +msgid "Benachrichtigungen" +msgstr "Save offboarding request" + +#: workflows/templates/workflows/account_profile.html:93 msgid "Die wichtigsten Stammdaten Ihres aktuellen Kontos." msgstr "The most important master data of your current account." -#: workflows/templates/workflows/account_profile.html:97 +#: workflows/templates/workflows/account_profile.html:101 +#: workflows/templates/workflows/account_profile.html:187 #: workflows/templates/workflows/branding_settings.html:32 #: workflows/templates/workflows/company_config.html:25 #, fuzzy @@ -1294,14 +1474,14 @@ msgstr "The most important master data of your current account." msgid "Bearbeiten" msgstr "Processing" -#: workflows/templates/workflows/account_profile.html:104 +#: workflows/templates/workflows/account_profile.html:108 #: workflows/templates/workflows/onboarding_intro_session.html:27 #: workflows/templates/workflows/request_timeline.html:66 #: workflows/templates/workflows/user_management.html:71 msgid "Name" msgstr "Name" -#: workflows/templates/workflows/account_profile.html:108 +#: workflows/templates/workflows/account_profile.html:112 #: workflows/templates/workflows/request_timeline.html:74 #: workflows/templates/workflows/requests_dashboard.html:190 #: workflows/templates/workflows/user_management.html:73 @@ -1309,14 +1489,18 @@ msgstr "Name" msgid "E-Mail" msgstr "Email" -#: workflows/templates/workflows/account_profile.html:165 +#: workflows/templates/workflows/account_profile.html:169 +#: workflows/templates/workflows/account_profile.html:248 #: workflows/templates/workflows/branding_settings.html:177 #: workflows/templates/workflows/company_config.html:54 #: workflows/templates/workflows/user_management.html:115 msgid "Speichern" msgstr "Save" -#: workflows/templates/workflows/account_profile.html:166 +#: workflows/templates/workflows/account_profile.html:170 +#: workflows/templates/workflows/account_profile.html:249 +#: workflows/templates/workflows/account_profile.html:332 +#: workflows/templates/workflows/account_profile.html:386 #: workflows/templates/workflows/base_shell.html:79 #: workflows/templates/workflows/branding_settings.html:178 #: workflows/templates/workflows/company_config.html:55 @@ -1324,57 +1508,14 @@ msgstr "Save" msgid "Abbrechen" msgstr "Cancel" -#: workflows/templates/workflows/account_profile.html:173 -msgid "Sicherheit & Aktionen" -msgstr "Security & actions" - -#: workflows/templates/workflows/account_profile.html:174 -msgid "Direkte Aktionen für Ihr Workdock-Konto." -msgstr "Direct actions for your Workdock account." - #: workflows/templates/workflows/account_profile.html:179 -#: workflows/templates/workflows/account_profile.html:315 -#: workflows/templates/workflows/auth/password_change_form.html:4 -#: workflows/templates/workflows/auth/password_change_form.html:17 -#: workflows/templates/workflows/includes/app_header.html:48 -msgid "Passwort ändern" -msgstr "Change password" - -#: workflows/templates/workflows/account_profile.html:180 -msgid "Aktualisieren Sie Ihr Passwort direkt im Konto." -msgstr "Update your password directly in your account." - -#: workflows/templates/workflows/account_profile.html:184 -msgid "TOTP" -msgstr "" - -#: workflows/templates/workflows/account_profile.html:187 -msgid "Zweiter Faktor ist aktiv und wird bei der Anmeldung geprüft." -msgstr "" - -#: workflows/templates/workflows/account_profile.html:189 -msgid "Standardmäßig deaktiviert. Kann hier jederzeit aktiviert werden." -msgstr "" - -#: workflows/templates/workflows/account_profile.html:195 -msgid "Sitzung" -msgstr "Session" - -#: workflows/templates/workflows/account_profile.html:196 -msgid "Sie können sich jederzeit sicher vom aktuellen Gerät abmelden." -msgstr "" - -#: workflows/templates/workflows/account_profile.html:203 -msgid "Zwei-Faktor-Authentifizierung" -msgstr "" - -#: workflows/templates/workflows/account_profile.html:204 msgid "" -"Aktivieren Sie TOTP mit einer Authenticator-App. Standardmäßig bleibt es " -"ausgeschaltet." +"Legen Sie fest, welche Workflow-Ereignisse im Header als Benachrichtigung " +"erscheinen sollen." msgstr "" -#: workflows/templates/workflows/account_profile.html:208 +#: workflows/templates/workflows/account_profile.html:199 +#: workflows/templates/workflows/account_profile.html:262 #: workflows/templates/workflows/app_registry.html:35 #: workflows/templates/workflows/app_registry.html:84 #: workflows/templates/workflows/form_builder.html:91 @@ -1385,70 +1526,178 @@ msgstr "" msgid "Aktiv" msgstr "Active" -#: workflows/templates/workflows/account_profile.html:210 +#: workflows/templates/workflows/account_profile.html:199 +#: workflows/templates/workflows/account_profile.html:262 #, fuzzy #| msgid "Auf" msgid "Aus" msgstr "To" -#: workflows/templates/workflows/account_profile.html:219 +#: workflows/templates/workflows/account_profile.html:223 +msgid "Benachrichtigung nach erfolgreich abgeschlossenem Onboarding." +msgstr "" + +#: workflows/templates/workflows/account_profile.html:224 +#, fuzzy +#| msgid "Offboarding-Anfrage speichern" +msgid "Benachrichtigung wenn ein Onboarding fehlschlägt." +msgstr "Save offboarding request" + +#: workflows/templates/workflows/account_profile.html:225 +msgid "Benachrichtigung nach erfolgreich abgeschlossenem Offboarding." +msgstr "" + +#: workflows/templates/workflows/account_profile.html:226 +#, fuzzy +#| msgid "Offboarding-Anfrage speichern" +msgid "Benachrichtigung wenn ein Offboarding fehlschlägt." +msgstr "Save offboarding request" + +#: workflows/templates/workflows/account_profile.html:227 +msgid "Benachrichtigung bei erfolgreicher Backup-Erstellung oder Verifikation." +msgstr "" + +#: workflows/templates/workflows/account_profile.html:228 +#, fuzzy +#| msgid "Offboarding-Anfrage speichern" +msgid "Benachrichtigung wenn Backup-Aktionen fehlschlagen." +msgstr "Save offboarding request" + +#: workflows/templates/workflows/account_profile.html:229 +#, fuzzy +#| msgid "Passwort konnte nicht gespeichert werden" +msgid "" +"Benachrichtigung wenn eine geplante Welcome E-Mail erfolgreich gesendet " +"wurde." +msgstr "Password could not be saved" + +#: workflows/templates/workflows/account_profile.html:230 +#, fuzzy +#| msgid "Offboarding-Anfrage speichern" +msgid "Benachrichtigung wenn eine geplante Welcome E-Mail fehlschlägt." +msgstr "Save offboarding request" + +#: workflows/templates/workflows/account_profile.html:231 +msgid "Hinweise zu Trial-Ablauf, Ablaufdatum oder Deaktivierung." +msgstr "" + +#: workflows/templates/workflows/account_profile.html:232 +msgid "Hinweise aus Systemtests wie SMTP oder Nextcloud." +msgstr "" + +#: workflows/templates/workflows/account_profile.html:261 +msgid "TOTP" +msgstr "" + +#: workflows/templates/workflows/account_profile.html:265 +msgid "Anmeldung wird zusätzlich mit einem zweiten Faktor geschützt." +msgstr "" + +#: workflows/templates/workflows/account_profile.html:267 +msgid "Optional. Kann bei Bedarf direkt unten aktiviert werden." +msgstr "" + +#: workflows/templates/workflows/account_profile.html:272 +#: workflows/templates/workflows/account_profile.html:441 +msgid "Recovery-Codes" +msgstr "" + +#: workflows/templates/workflows/account_profile.html:282 +msgid "Einmal-Codes für Notfälle oder verlorene Authenticator-Geräte." +msgstr "" + +#: workflows/templates/workflows/account_profile.html:284 +msgid "Werden automatisch erzeugt, sobald TOTP aktiviert wird." +msgstr "" + +#: workflows/templates/workflows/account_profile.html:293 +msgid "Zwei-Faktor-Authentifizierung" +msgstr "" + +#: workflows/templates/workflows/account_profile.html:294 +msgid "" +"Aktivieren Sie TOTP mit einer Authenticator-App. Standardmäßig bleibt es " +"ausgeschaltet." +msgstr "" + +#: workflows/templates/workflows/account_profile.html:301 #, fuzzy #| msgid "Deaktivieren" msgid "TOTP ist aktiviert." msgstr "Disabled" -#: workflows/templates/workflows/account_profile.html:222 +#: workflows/templates/workflows/account_profile.html:302 msgid "Bestätigt am" msgstr "" -#: workflows/templates/workflows/account_profile.html:241 +#: workflows/templates/workflows/account_profile.html:310 #, fuzzy #| msgid "Aktivieren" msgid "TOTP deaktivieren" msgstr "Enable" -#: workflows/templates/workflows/account_profile.html:259 +#: workflows/templates/workflows/account_profile.html:331 +#, fuzzy +#| msgid "Deaktivieren" +msgid "Deaktivierung bestätigen" +msgstr "Disabled" + +#: workflows/templates/workflows/account_profile.html:340 +#: workflows/templates/workflows/account_profile.html:349 msgid "Recovery-Codes neu erzeugen" msgstr "" -#: workflows/templates/workflows/account_profile.html:268 +#: workflows/templates/workflows/account_profile.html:341 +msgid "" +"Neue Recovery-Codes sollten nur erzeugt werden, wenn die bisherigen Codes " +"nicht mehr sicher sind." +msgstr "" + +#: workflows/templates/workflows/account_profile.html:368 +msgid "Stattdessen Recovery-Code verwenden" +msgstr "" + +#: workflows/templates/workflows/account_profile.html:385 +#, fuzzy +#| msgid "Deaktivieren" +msgid "Erzeugung bestätigen" +msgstr "Disabled" + +#: workflows/templates/workflows/account_profile.html:397 #, fuzzy #| msgid "Onboarding starten" msgid "Manueller Schlüssel" msgstr "Start onboarding" -#: workflows/templates/workflows/account_profile.html:272 -#, fuzzy -#| msgid "Setup Mail" -msgid "Setup-Link" -msgstr "Setup Mail" - -#: workflows/templates/workflows/account_profile.html:276 -msgid "" -"Wenn Ihre App keinen QR-Code scannen kann, tragen Sie den Schlüssel oder den " -"otpauth-Link manuell ein." +#: workflows/templates/workflows/account_profile.html:398 +msgid "Nur bei Bedarf anzeigen" msgstr "" -#: workflows/templates/workflows/account_profile.html:292 +#: workflows/templates/workflows/account_profile.html:406 +msgid "Manuellen Schlüssel anzeigen oder ausblenden" +msgstr "" + +#: workflows/templates/workflows/account_profile.html:416 +msgid "" +"Scannen Sie den QR-Code mit Ihrer Authenticator-App. Den manuellen Schlüssel " +"können Sie bei Bedarf einblenden." +msgstr "" + +#: workflows/templates/workflows/account_profile.html:432 #, fuzzy #| msgid "Aktivieren" msgid "TOTP aktivieren" msgstr "Enable" -#: workflows/templates/workflows/account_profile.html:301 -msgid "Recovery-Codes" -msgstr "" - -#: workflows/templates/workflows/account_profile.html:302 +#: workflows/templates/workflows/account_profile.html:442 msgid "" "Diese Codes werden nur jetzt im Klartext angezeigt. Jeden Code können Sie " "genau einmal verwenden." msgstr "" -#: workflows/templates/workflows/account_profile.html:318 -#: workflows/templates/workflows/includes/app_header.html:51 -msgid "Abmelden" -msgstr "Log out" +#: workflows/templates/workflows/account_profile.html:445 +msgid "Herunterladen" +msgstr "" #: workflows/templates/workflows/app_registry.html:3 #, fuzzy @@ -1750,20 +1999,44 @@ msgstr "" msgid "Noch keine Audit-Einträge vorhanden." msgstr "No requests available yet." -#: workflows/templates/workflows/auth/login.html:29 -msgid "" -"Anmeldedaten oder TOTP-Code sind nicht korrekt. Bitte versuchen Sie es " -"erneut." +#: workflows/templates/workflows/auth/login.html:18 +msgid "Zwei-Faktor-Prüfung" msgstr "" -#: workflows/templates/workflows/auth/login.html:37 -msgid "Nur erforderlich, wenn TOTP für Ihr Konto aktiviert ist." +#: workflows/templates/workflows/auth/login.html:19 +msgid "Geben Sie Ihren TOTP-Code ein, um die Anmeldung abzuschließen." msgstr "" +#: workflows/templates/workflows/auth/login.html:40 +#, fuzzy +#| msgid "Link ungültig" +msgid "Code ungültig" +msgstr "Invalid link" + #: workflows/templates/workflows/auth/login.html:41 -msgid "Alternativ können Sie einen einmaligen Recovery-Code verwenden." +msgid "" +"Der eingegebene TOTP- oder Recovery-Code ist nicht korrekt. Bitte versuchen " +"Sie es erneut." msgstr "" +#: workflows/templates/workflows/auth/login.html:60 +msgid "Recovery-Code verwenden" +msgstr "" + +#: workflows/templates/workflows/auth/login.html:66 +msgid "Nutzen Sie stattdessen einen einmaligen Recovery-Code." +msgstr "" + +#: workflows/templates/workflows/auth/login.html:69 +msgid "Code prüfen" +msgstr "" + +#: workflows/templates/workflows/auth/login.html:70 +#, fuzzy +#| msgid "Zur Anmeldung" +msgid "Zurück zur Anmeldung" +msgstr "Back to sign in" + #: workflows/templates/workflows/auth/password_change_done.html:4 #: workflows/templates/workflows/auth/password_change_done.html:17 #: workflows/templates/workflows/user_management.html:174 @@ -1780,6 +2053,12 @@ msgstr "" msgid "Zum Profil" msgstr "" +#: workflows/templates/workflows/auth/password_change_form.html:4 +#: workflows/templates/workflows/auth/password_change_form.html:17 +#: workflows/templates/workflows/includes/app_header.html:100 +msgid "Passwort ändern" +msgstr "Change password" + #: workflows/templates/workflows/auth/password_change_form.html:18 #, fuzzy #| msgid "Bitte vergeben Sie jetzt ein neues Passwort für Ihr Konto." @@ -2159,10 +2438,6 @@ msgstr "" "Single documentation entry point for both operational knowledge and long-" "term engineering knowledge." -#: workflows/templates/workflows/handbook.html:21 -msgid "Operations" -msgstr "Operations" - #: workflows/templates/workflows/handbook.html:22 msgid "Project Wiki" msgstr "Project Wiki" @@ -2339,6 +2614,28 @@ msgstr "Back to home" msgid "Zum Dashboard" msgstr "Go to dashboard" +#: workflows/templates/workflows/includes/app_header.html:42 +#, fuzzy +#| msgid "Alle auswählen" +msgid "Alle als gelesen" +msgstr "Select all" + +#: workflows/templates/workflows/includes/app_header.html:63 +#, fuzzy +#| msgid "Gesendet" +msgid "Gelesen" +msgstr "Sent" + +#: workflows/templates/workflows/includes/app_header.html:71 +#, fuzzy +#| msgid "Noch keine Vorgänge vorhanden." +msgid "Keine Benachrichtigungen vorhanden." +msgstr "No backup bundles available yet." + +#: workflows/templates/workflows/includes/app_header.html:103 +msgid "Abmelden" +msgstr "Log out" + #: workflows/templates/workflows/integrations_setup.html:4 #: workflows/templates/workflows/integrations_setup.html:14 msgid "Integrationen Setup" @@ -2715,10 +3012,6 @@ msgstr "" msgid "Task ID" msgstr "" -#: workflows/templates/workflows/job_monitor.html:53 -msgid "Fehler" -msgstr "" - #: workflows/templates/workflows/job_monitor.html:67 #, fuzzy #| msgid "Noch keine Vorgänge vorhanden." @@ -2910,7 +3203,7 @@ msgid "Dienstliche E-Mail" msgstr "Work email" #: workflows/templates/workflows/onboarding_intro_session.html:31 -#: workflows/views.py:1275 +#: workflows/views.py:1444 msgid "Vertragsbeginn" msgstr "Contract start" @@ -3714,312 +4007,324 @@ msgstr "Resume" msgid "Keine geplanten Welcome E-Mails vorhanden." msgstr "No scheduled welcome emails available." -#: workflows/views.py:103 +#: workflows/views.py:119 msgid "Person, Rolle, Abteilung" msgstr "Person, role, department" -#: workflows/views.py:104 +#: workflows/views.py:120 msgid "Beschäftigung und Termine" msgstr "Employment and dates" -#: workflows/views.py:105 +#: workflows/views.py:121 msgid "Geräte, Software und Zugänge" msgstr "Devices, software, and access" -#: workflows/views.py:106 +#: workflows/views.py:122 msgid "Notizen und Freigabe" msgstr "Notes and approval" -#: workflows/views.py:158 +#: workflows/views.py:255 #, fuzzy #| msgid "Lokal gespeichert" msgid "Profilbild gespeichert." msgstr "Stored locally" -#: workflows/views.py:160 +#: workflows/views.py:257 #, fuzzy #| msgid "Passwort konnte nicht gespeichert werden" msgid "Profilbild konnte nicht gespeichert werden." msgstr "Password could not be saved" -#: workflows/views.py:166 +#: workflows/views.py:263 #, fuzzy #| msgid "Lokal gespeichert" msgid "Profildaten gespeichert." msgstr "Stored locally" -#: workflows/views.py:168 +#: workflows/views.py:265 #, fuzzy #| msgid "Passwort konnte nicht gespeichert werden" msgid "Profildaten konnten nicht gespeichert werden." msgstr "Password could not be saved" -#: workflows/views.py:177 +#: workflows/views.py:271 +#, fuzzy +#| msgid "Offboarding-Anfrage speichern" +msgid "Benachrichtigungseinstellungen gespeichert." +msgstr "Save offboarding request" + +#: workflows/views.py:273 +#, fuzzy +#| msgid "Passwort konnte nicht gespeichert werden" +msgid "Benachrichtigungseinstellungen konnten nicht gespeichert werden." +msgstr "Password could not be saved" + +#: workflows/views.py:282 #, fuzzy #| msgid "Deaktivieren" msgid "TOTP wurde aktiviert." msgstr "Disabled" -#: workflows/views.py:179 +#: workflows/views.py:284 #, fuzzy #| msgid "Passwort konnte nicht gespeichert werden" msgid "TOTP konnte nicht aktiviert werden." msgstr "Password could not be saved" -#: workflows/views.py:186 +#: workflows/views.py:291 msgid "TOTP wurde deaktiviert." msgstr "" -#: workflows/views.py:188 +#: workflows/views.py:293 #, fuzzy #| msgid "Passwort konnte nicht gespeichert werden" msgid "TOTP konnte nicht deaktiviert werden." msgstr "Password could not be saved" -#: workflows/views.py:197 +#: workflows/views.py:302 msgid "Recovery-Codes wurden neu erzeugt." msgstr "" -#: workflows/views.py:199 +#: workflows/views.py:304 #, fuzzy #| msgid "Passwort konnte nicht gespeichert werden" msgid "Recovery-Codes konnten nicht neu erzeugt werden." msgstr "Password could not be saved" -#: workflows/views.py:245 workflows/views.py:1361 workflows/views.py:1366 +#: workflows/views.py:353 workflows/views.py:1530 workflows/views.py:1535 msgid "Sie haben keine Berechtigung für diese Aktion." msgstr "You do not have permission for this action." -#: workflows/views.py:326 +#: workflows/views.py:434 #, fuzzy #| msgid "Vorgänge" msgid "Vorgänge gelöscht" msgstr "Requests" -#: workflows/views.py:327 +#: workflows/views.py:435 msgid "Vorgang gelöscht" msgstr "" -#: workflows/views.py:328 +#: workflows/views.py:436 msgid "Vorgang erneut angestoßen" msgstr "" -#: workflows/views.py:329 +#: workflows/views.py:437 #, fuzzy #| msgid "Einweisung" msgid "Einweisungs-PDF erzeugt" msgstr "Introduction" -#: workflows/views.py:330 +#: workflows/views.py:438 #, fuzzy #| msgid "Live-Protokoll erzeugen" msgid "Live-Protokoll erzeugt" msgstr "Generate live protocol" -#: workflows/views.py:331 +#: workflows/views.py:439 #, fuzzy #| msgid "Einweisung wurde zurückgesetzt." msgid "Einweisung zurückgesetzt" msgstr "Introduction was reset." -#: workflows/views.py:332 +#: workflows/views.py:440 #, fuzzy #| msgid "Einweisung wurde als Entwurf gespeichert." msgid "Einweisung als Entwurf gespeichert" msgstr "Introduction was saved as draft." -#: workflows/views.py:333 +#: workflows/views.py:441 #, fuzzy #| msgid "Einweisung wurde als abgeschlossen gespeichert." msgid "Einweisung abgeschlossen" msgstr "Introduction was saved as completed." -#: workflows/views.py:334 +#: workflows/views.py:442 msgid "Formularoption gelöscht" msgstr "" -#: workflows/views.py:335 +#: workflows/views.py:443 #, fuzzy #| msgid "Optionen speichern" msgid "Formularoptionen gespeichert" msgstr "Save options" -#: workflows/views.py:336 +#: workflows/views.py:444 #, fuzzy #| msgid "Feldtexte speichern" msgid "Feldtexte gespeichert" msgstr "Save field text" -#: workflows/views.py:337 +#: workflows/views.py:445 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Formularlayout gespeichert" msgstr "Save offboarding request" -#: workflows/views.py:338 +#: workflows/views.py:446 msgid "Einweisungs-Checkpunkt gelöscht" msgstr "" -#: workflows/views.py:339 +#: workflows/views.py:447 msgid "Einweisungs-Checkpunkt hinzugefügt" msgstr "" -#: workflows/views.py:340 +#: workflows/views.py:448 #, fuzzy #| msgid "Checkliste speichern" msgid "Einweisungs-Checkliste gespeichert" msgstr "Save checklist" -#: workflows/views.py:341 +#: workflows/views.py:449 #, fuzzy #| msgid "Welcome E-Mails" msgid "Welcome E-Mail sofort ausgelöst" msgstr "Welcome Emails" -#: workflows/views.py:342 +#: workflows/views.py:450 #, fuzzy #| msgid "Welcome-Einstellungen speichern" msgid "Welcome E-Mail Einstellungen gespeichert" msgstr "Save welcome settings" -#: workflows/views.py:343 +#: workflows/views.py:451 msgid "Welcome E-Mail Sammelaktion ausgeführt" msgstr "" -#: workflows/views.py:344 +#: workflows/views.py:452 #, fuzzy #| msgid "Welcome E-Mails" msgid "Welcome E-Mail pausiert" msgstr "Welcome Emails" -#: workflows/views.py:345 +#: workflows/views.py:453 #, fuzzy #| msgid "Welcome E-Mails" msgid "Welcome E-Mail fortgesetzt" msgstr "Welcome Emails" -#: workflows/views.py:346 +#: workflows/views.py:454 #, fuzzy #| msgid "Welcome E-Mails" msgid "Welcome E-Mail abgebrochen" msgstr "Welcome Emails" -#: workflows/views.py:347 +#: workflows/views.py:455 #, fuzzy #| msgid "SMTP-Test" msgid "SMTP-Test gesendet" msgstr "SMTP test" -#: workflows/views.py:348 +#: workflows/views.py:456 #, fuzzy #| msgid "Nextcloud-Test" msgid "Nextcloud-Testupload ausgeführt" msgstr "Nextcloud test" -#: workflows/views.py:349 +#: workflows/views.py:457 #, fuzzy #| msgid "Nextcloud schalten" msgid "Nextcloud-Modus umgeschaltet" msgstr "Toggle Nextcloud" -#: workflows/views.py:350 +#: workflows/views.py:458 msgid "E-Mail-Modus umgeschaltet" msgstr "" -#: workflows/views.py:351 +#: workflows/views.py:459 #, fuzzy #| msgid "Integrationen Setup" msgid "Integrationen gespeichert" msgstr "Integrations Setup" -#: workflows/views.py:352 +#: workflows/views.py:460 #, fuzzy #| msgid "Welcome-Einstellungen speichern" msgid "Nextcloud-Einstellungen gespeichert" msgstr "Save welcome settings" -#: workflows/views.py:353 +#: workflows/views.py:461 #, fuzzy #| msgid "Welcome-Einstellungen speichern" msgid "Mail-Einstellungen gespeichert" msgstr "Save welcome settings" -#: workflows/views.py:354 +#: workflows/views.py:462 #, fuzzy #| msgid "E-Mail Routing & Vorlagen speichern" msgid "E-Mail-Routing gespeichert" msgstr "Save email routing & templates" -#: workflows/views.py:355 +#: workflows/views.py:463 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Benachrichtigungsregeln gespeichert" msgstr "Save offboarding request" -#: workflows/views.py:356 +#: workflows/views.py:464 #, fuzzy #| msgid "Anfrage gespeichert" msgid "Benutzer erstellt" msgstr "Request saved" -#: workflows/views.py:357 +#: workflows/views.py:465 msgid "Benutzer aktualisiert" msgstr "" -#: workflows/views.py:358 +#: workflows/views.py:466 msgid "Passwort-Reset-Link versendet" msgstr "" -#: workflows/views.py:359 +#: workflows/views.py:467 #, fuzzy #| msgid "Benutzerübersicht" msgid "Benutzer gelöscht" msgstr "User overview" -#: workflows/views.py:360 +#: workflows/views.py:468 #, fuzzy #| msgid "Anfrage gespeichert" msgid "Backup erstellt" msgstr "Request saved" -#: workflows/views.py:361 +#: workflows/views.py:469 msgid "Backup verifiziert" msgstr "" -#: workflows/views.py:362 +#: workflows/views.py:470 #, fuzzy #| msgid "Anfrage gespeichert" msgid "Backup gelöscht" msgstr "Request saved" -#: workflows/views.py:363 +#: workflows/views.py:471 #, fuzzy #| msgid "Welcome-Einstellungen speichern" msgid "Backup-Einstellungen gespeichert" msgstr "Save welcome settings" -#: workflows/views.py:364 +#: workflows/views.py:472 #, fuzzy #| msgid "Anfrage gespeichert" msgid "App-Registry gespeichert" msgstr "Request saved" -#: workflows/views.py:536 +#: workflows/views.py:644 #, fuzzy #| msgid "Anfrage gespeichert" msgid "App-Registry gespeichert." msgstr "Request saved" -#: workflows/views.py:635 +#: workflows/views.py:743 msgid "Für diesen Benutzer ist keine E-Mail-Adresse hinterlegt." msgstr "" -#: workflows/views.py:644 +#: workflows/views.py:752 #, python-format msgid "Zugangseinladung für %(username)s" msgstr "" -#: workflows/views.py:646 +#: workflows/views.py:754 #, python-format msgid "" "Hallo %(name)s,\n" @@ -4032,12 +4337,12 @@ msgid "" "Ihrem Administrator." msgstr "" -#: workflows/views.py:657 +#: workflows/views.py:765 #, python-format msgid "Passwort zurücksetzen für %(username)s" msgstr "" -#: workflows/views.py:659 +#: workflows/views.py:767 #, python-format msgid "" "Hallo %(name)s,\n" @@ -4050,7 +4355,7 @@ msgid "" "ignorieren." msgstr "" -#: workflows/views.py:710 +#: workflows/views.py:818 #, fuzzy #| msgid "" #| "Benutzer konnte nicht erstellt werden. Bitte prüfen Sie die Eingaben." @@ -4058,69 +4363,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:738 +#: workflows/views.py:846 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Portal-Branding wurde gespeichert." msgstr "Save offboarding request" -#: workflows/views.py:755 +#: workflows/views.py:863 msgid "Identität" msgstr "" -#: workflows/views.py:756 +#: workflows/views.py:864 msgid "Titel, Firmenname und zentrale Spracheinstellungen." msgstr "" -#: workflows/views.py:760 +#: workflows/views.py:868 msgid "" "Wird für E-Mail-Vorschläge und Domain-bezogene Standardtexte verwendet, z. " -"B. tub.co." +"B. workdock.de." msgstr "" -#: workflows/views.py:765 +#: workflows/views.py:873 msgid "Farben & Erscheinungsbild" msgstr "" -#: workflows/views.py:766 +#: workflows/views.py:874 msgid "Zentrale visuelle Markenwerte und Browser-Icon." msgstr "" -#: workflows/views.py:770 +#: workflows/views.py:878 msgid "Erlaubte Formate: SVG, PNG, JPG, JPEG, WEBP. Maximal 5 MB." msgstr "" -#: workflows/views.py:771 +#: workflows/views.py:879 msgid "Erlaubte Formate: ICO, PNG, SVG, WEBP. Maximal 2 MB." msgstr "" -#: workflows/views.py:776 +#: workflows/views.py:884 #, fuzzy #| msgid "Produktion" msgid "Kommunikation" msgstr "Production" -#: workflows/views.py:777 +#: workflows/views.py:885 msgid "Absender, Support und PDF-Branding für ausgehende Kommunikation." msgstr "" -#: workflows/views.py:781 +#: workflows/views.py:889 msgid "Wird für ausgehende System-E-Mails als Anzeigename verwendet." msgstr "" -#: workflows/views.py:782 +#: workflows/views.py:890 msgid "Erlaubtes Format: PDF. Maximal 10 MB." msgstr "" -#: workflows/views.py:787 +#: workflows/views.py:895 msgid "Footer & Rechtliches" msgstr "" -#: workflows/views.py:788 +#: workflows/views.py:896 msgid "Gemeinsame Footer-Texte und rechtliche Hinweise für die Shell." msgstr "" -#: workflows/views.py:842 +#: workflows/views.py:950 #, fuzzy #| msgid "" #| "Benutzer konnte nicht erstellt werden. Bitte prüfen Sie die Eingaben." @@ -4129,53 +4434,53 @@ msgid "" "Eingaben." msgstr "User could not be created. Please check the input." -#: workflows/views.py:871 +#: workflows/views.py:979 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Firmenkonfiguration wurde gespeichert." msgstr "Save offboarding request" -#: workflows/views.py:888 +#: workflows/views.py:996 #, fuzzy #| msgid "Firmenname" msgid "Firmenprofil" msgstr "Company name" -#: workflows/views.py:889 +#: workflows/views.py:997 msgid "Rechtlicher Name und zentrale Stammdaten der Firma." msgstr "" -#: workflows/views.py:894 +#: workflows/views.py:1002 msgid "Adresse & Register" msgstr "" -#: workflows/views.py:895 +#: workflows/views.py:1003 msgid "Anschrift sowie optionale Register- und Steuerangaben." msgstr "" -#: workflows/views.py:900 +#: workflows/views.py:1008 msgid "Kontaktpunkte" msgstr "" -#: workflows/views.py:901 +#: workflows/views.py:1009 msgid "Zentrale Ansprechpartner für HR, IT und Operations." msgstr "" -#: workflows/views.py:906 +#: workflows/views.py:1014 msgid "Recht & Öffentlichkeit" msgstr "" -#: workflows/views.py:907 +#: workflows/views.py:1015 msgid "Öffentliche Links für Website, Impressum und Datenschutz." msgstr "" -#: workflows/views.py:909 +#: workflows/views.py:1017 msgid "" "Diese Links können später im Portal-Footer oder in öffentlichen Seiten " "verwendet werden." msgstr "" -#: workflows/views.py:949 +#: workflows/views.py:1057 #, fuzzy #| msgid "" #| "Benutzer konnte nicht erstellt werden. Bitte prüfen Sie die Eingaben." @@ -4184,21 +4489,54 @@ msgid "" "Eingaben." msgstr "Trial configuration could not be saved. Please check the input." -#: workflows/views.py:976 +#: workflows/views.py:1089 +#, fuzzy +#| msgid "Trial abgelaufen" +msgid "Trial ist abgelaufen" +msgstr "Trial expired" + +#: workflows/views.py:1090 +msgid "" +"Der Trial-Zeitraum ist überschritten. Nicht-Platform-Owner werden jetzt " +"blockiert." +msgstr "" + +#: workflows/views.py:1098 +msgid "Trial läuft bald ab" +msgstr "" + +#: workflows/views.py:1099 +#, python-format +msgid "Der Trial endet am %(date)s." +msgstr "" + +#: workflows/views.py:1107 +#, fuzzy +#| msgid "Trial-Modus" +msgid "Trial-Modus deaktiviert" +msgstr "Trial mode" + +#: workflows/views.py:1108 +#, fuzzy +#| msgid "Nextcloud schalten" +msgid "Der Trial-Modus wurde ausgeschaltet." +msgstr "Toggle Nextcloud" + +#: workflows/views.py:1113 msgid "Trial-Konfiguration wurde gespeichert." msgstr "Trial configuration was saved." -#: workflows/views.py:993 +#: workflows/views.py:1130 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:1006 +#: workflows/views.py:1143 #, 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:1028 +#: workflows/views.py:1165 #, fuzzy #| msgid "" #| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " @@ -4209,14 +4547,14 @@ msgid "" msgstr "" "The currently signed-in super admin cannot lock or downgrade themselves here." -#: workflows/views.py:1031 +#: workflows/views.py:1168 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:1034 +#: workflows/views.py:1171 #, fuzzy #| msgid "" #| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " @@ -4227,7 +4565,7 @@ msgid "" msgstr "" "The currently signed-in super admin cannot lock or downgrade themselves here." -#: workflows/views.py:1037 +#: workflows/views.py:1174 #, fuzzy #| msgid "" #| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " @@ -4238,18 +4576,18 @@ msgid "" msgstr "" "The currently signed-in super admin cannot lock or downgrade themselves here." -#: workflows/views.py:1054 +#: workflows/views.py:1191 #, python-format msgid "Benutzer wurde aktualisiert: %(username)s" msgstr "User updated: %(username)s" -#: workflows/views.py:1076 +#: workflows/views.py:1213 #, 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:1088 +#: workflows/views.py:1225 #, fuzzy #| msgid "" #| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " @@ -4259,7 +4597,7 @@ msgid "" msgstr "" "The currently signed-in super admin cannot lock or downgrade themselves here." -#: workflows/views.py:1091 +#: workflows/views.py:1228 #, fuzzy #| msgid "" #| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " @@ -4269,7 +4607,7 @@ msgid "" msgstr "" "The currently signed-in super admin cannot lock or downgrade themselves here." -#: workflows/views.py:1094 +#: workflows/views.py:1231 #, fuzzy #| msgid "" #| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " @@ -4278,7 +4616,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:1097 +#: workflows/views.py:1234 #, fuzzy #| msgid "" #| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " @@ -4287,124 +4625,206 @@ 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:1110 +#: workflows/views.py:1247 #, fuzzy, python-format #| msgid "Benutzer wurde erstellt: %(username)s" msgid "Benutzer wurde gelöscht: %(username)s" msgstr "User created: %(username)s" -#: workflows/views.py:1199 +#: workflows/views.py:1338 +#, fuzzy, python-format +#| msgid "Anfrage gespeichert" +msgid "Backup erstellt: %(name)s" +msgstr "Request saved" + +#: workflows/views.py:1339 +#, fuzzy +#| msgid "Offboarding-Anfrage speichern" +msgid "Das Backup-Bundle wurde erfolgreich erstellt." +msgstr "Save offboarding request" + +#: workflows/views.py:1344 #, python-format msgid "Backup wurde erstellt: %(name)s" msgstr "" -#: workflows/views.py:1201 +#: workflows/views.py:1354 #, python-format msgid "Backup konnte nicht erstellt werden: %(error)s" msgstr "" -#: workflows/views.py:1217 +#: workflows/views.py:1372 +#, fuzzy, python-format +#| msgid "Backup wird verifiziert" +msgid "Backup verifiziert: %(name)s" +msgstr "Backup is being verified" + +#: workflows/views.py:1373 +#, fuzzy +#| msgid "Backup wird verifiziert" +msgid "Das Backup wurde erfolgreich verifiziert." +msgstr "Backup is being verified" + +#: workflows/views.py:1378 #, python-format msgid "Backup wurde verifiziert: %(name)s" msgstr "" -#: workflows/views.py:1219 +#: workflows/views.py:1382 +#, fuzzy +#| msgid "Fehlgeschlagen" +msgid "Backup-Verifikation fehlgeschlagen" +msgstr "Failed" + +#: workflows/views.py:1388 #, python-format msgid "Backup-Verifikation fehlgeschlagen: %(error)s" msgstr "" -#: workflows/views.py:1235 +#: workflows/views.py:1404 #, python-format msgid "Backup wurde gelöscht: %(name)s" msgstr "" -#: workflows/views.py:1237 +#: workflows/views.py:1406 #, python-format msgid "Backup konnte nicht gelöscht werden: %(error)s" msgstr "" -#: workflows/views.py:1263 +#: workflows/views.py:1432 #, fuzzy #| msgid "Anfrage gespeichert" msgid "Anfrage erstellt" msgstr "Request saved" -#: workflows/views.py:1265 +#: workflows/views.py:1434 #, fuzzy, python-format #| msgid "Sitzungsstatus" msgid "Status: %(status)s" msgstr "Session status" -#: workflows/views.py:1277 +#: workflows/views.py:1446 #, fuzzy #| msgid "Geplant für" msgid "Geplanter Start" msgstr "Scheduled for" -#: workflows/views.py:1287 +#: workflows/views.py:1456 msgid "Geräteübergabe / Hardware-Abholung" msgstr "" -#: workflows/views.py:1289 +#: workflows/views.py:1458 msgid "Geplanter Hardware-Termin" msgstr "" -#: workflows/views.py:1298 +#: workflows/views.py:1467 #, fuzzy #| msgid "Noch nicht verfügbar" msgid "PDF verfügbar" msgstr "Not available yet" -#: workflows/views.py:1324 +#: workflows/views.py:1493 #, fuzzy #| msgid "Einweisung" msgid "Einweisungssitzung" msgstr "Introduction" -#: workflows/views.py:1336 -#, fuzzy -#| msgid "Welcome E-Mails" -msgid "Welcome E-Mail" -msgstr "Welcome Emails" - -#: workflows/views.py:1375 +#: workflows/views.py:1544 msgid "Keine Einträge ausgewählt." msgstr "No entries selected." -#: workflows/views.py:1418 +#: workflows/views.py:1587 #, python-format msgid "%(count)s Eintrag/Einträge gelöscht." msgstr "%(count)s entry/entries deleted." -#: workflows/views.py:1420 +#: workflows/views.py:1589 #, python-format msgid "%(count)s Auswahl(en) konnten nicht verarbeitet werden." msgstr "%(count)s selection(s) could not be processed." -#: workflows/views.py:1422 +#: workflows/views.py:1591 msgid "Keine passenden Einträge gefunden." msgstr "No matching entries found." -#: workflows/views.py:1650 +#: workflows/views.py:1819 msgid "Einweisungs- und Übergabeprotokoll wurde erzeugt." msgstr "Introduction and handover protocol was generated." -#: workflows/views.py:1667 +#: workflows/views.py:1836 msgid "Einweisungsprotokoll aus Live-Status wurde erzeugt." msgstr "Introduction protocol from live status was generated." -#: workflows/views.py:1696 +#: workflows/views.py:1865 msgid "Einweisung wurde zurückgesetzt." msgstr "Introduction was reset." -#: workflows/views.py:1710 +#: workflows/views.py:1879 msgid "Einweisung wurde als abgeschlossen gespeichert." msgstr "Introduction was saved as completed." -#: workflows/views.py:1723 +#: workflows/views.py:1892 msgid "Einweisung wurde als Entwurf gespeichert." msgstr "Introduction was saved as draft." +#: workflows/views.py:2677 +#, fuzzy +#| msgid "SMTP-Test starten" +msgid "SMTP-Test erfolgreich" +msgstr "Run SMTP test" + +#: workflows/views.py:2678 +#, fuzzy +#| msgid "Offboarding-Anfrage speichern" +msgid "Die SMTP-Testmail wurde erfolgreich gesendet." +msgstr "Save offboarding request" + +#: workflows/views.py:2687 +#, fuzzy +#| msgid "SMTP-Test" +msgid "SMTP-Test fehlgeschlagen" +msgstr "SMTP test" + +#: workflows/views.py:2693 +#, fuzzy, python-format +#| msgid "Passwort konnte nicht gespeichert werden" +msgid "SMTP-Testmail konnte nicht gesendet werden: %(error)s" +msgstr "Password could not be saved" + +#: workflows/views.py:2718 +#, fuzzy +#| msgid "Nextcloud-Test starten" +msgid "Nextcloud-Test erfolgreich" +msgstr "Run Nextcloud test" + +#: workflows/views.py:2719 +msgid "Der Testupload nach Nextcloud war erfolgreich." +msgstr "" + +#: workflows/views.py:2729 workflows/views.py:2739 +#, fuzzy +#| msgid "Nextcloud-Test starten" +msgid "Nextcloud-Test fehlgeschlagen" +msgstr "Run Nextcloud test" + +#: workflows/views.py:2730 +msgid "Der Testupload nach Nextcloud ist fehlgeschlagen." +msgstr "" + +#~ msgid "Direkte Aktionen für Ihr Workdock-Konto." +#~ msgstr "Direct actions for your Workdock account." + +#~ msgid "Aktualisieren Sie Ihr Passwort direkt im Konto." +#~ msgstr "Update your password directly in your account." + +#~ msgid "Sitzung" +#~ msgstr "Session" + +#, fuzzy +#~| msgid "Setup Mail" +#~ msgid "Setup-Link" +#~ msgstr "Setup Mail" + #~ msgid "Branding speichern" #~ msgstr "Save branding" diff --git a/backend/workflows/context_processors.py b/backend/workflows/context_processors.py index 91208d2..e31f0e4 100644 --- a/backend/workflows/context_processors.py +++ b/backend/workflows/context_processors.py @@ -1,4 +1,5 @@ from .branding import get_branding_context, get_trial_context +from .models import UserNotification from .roles import template_role_context @@ -6,4 +7,15 @@ def role_context(request): context = template_role_context(getattr(request, 'user', None)) context.update(get_branding_context()) context.update(get_trial_context()) + user = getattr(request, 'user', None) + if getattr(user, 'is_authenticated', False): + notifications = list(UserNotification.objects.filter(user=user).order_by('-created_at')[:8]) + context.update( + { + 'header_notifications': notifications, + 'header_unread_notification_count': UserNotification.objects.filter(user=user, read_at__isnull=True).count(), + } + ) + else: + context.update({'header_notifications': [], 'header_unread_notification_count': 0}) return context diff --git a/backend/workflows/forms.py b/backend/workflows/forms.py index a9f71a7..4bf0d1f 100644 --- a/backend/workflows/forms.py +++ b/backend/workflows/forms.py @@ -1,7 +1,7 @@ from django import forms from pathlib import Path from datetime import timedelta -from django.contrib.auth import get_user_model, password_validation +from django.contrib.auth import authenticate, get_user_model, password_validation from django.contrib.auth.forms import AuthenticationForm, PasswordChangeForm, PasswordResetForm, SetPasswordForm from django.core.exceptions import ValidationError from django.utils import timezone @@ -10,7 +10,7 @@ from django.utils.translation import get_language, gettext as _, gettext_lazy from .branding import get_company_email_domain from .form_builder import apply_form_field_config from .models import EmployeeProfile, FormOption, OffboardingRequest, OnboardingRequest, PortalBranding, 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 +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 @@ -102,9 +102,41 @@ HARDWARE_EXTRA_CHOICES = [('Smartphone', 'Smartphone'), ('Anderes', 'Anderes')] SOFTWARE_EXTRA_CHOICES = [('Adobe Acrobat Pro (Abonnement: Zusätzliche Kosten)', 'Adobe Acrobat Pro (Abonnement: Zusätzliche Kosten)'), ('Anderes', 'Anderes')] -class AppAuthenticationForm(AuthenticationForm): +class AppLoginForm(forms.Form): username = forms.CharField(label=gettext_lazy('Benutzername')) - password = forms.CharField(label=gettext_lazy('Passwort'), strip=False, widget=forms.PasswordInput(attrs={'autocomplete': 'current-password'})) + password = forms.CharField( + label=gettext_lazy('Passwort'), + strip=False, + widget=forms.PasswordInput(attrs={'autocomplete': 'current-password'}), + ) + + error_messages = { + 'invalid_login': gettext_lazy('Benutzername oder Passwort sind nicht korrekt.'), + 'inactive': gettext_lazy('Dieses Konto ist deaktiviert.'), + } + + def __init__(self, request=None, *args, **kwargs): + self.request = request + self.user_cache = None + super().__init__(*args, **kwargs) + + def clean(self): + cleaned_data = super().clean() + username = cleaned_data.get('username') + password = cleaned_data.get('password') + if username and password: + self.user_cache = authenticate(self.request, username=username, password=password) + if self.user_cache is None: + raise ValidationError(self.error_messages['invalid_login'], code='invalid_login') + if not self.user_cache.is_active: + raise ValidationError(self.error_messages['inactive'], code='inactive') + return cleaned_data + + def get_user(self): + return self.user_cache + + +class AppTOTPChallengeForm(forms.Form): otp_code = forms.CharField( label=gettext_lazy('TOTP-Code'), required=False, @@ -119,37 +151,30 @@ class AppAuthenticationForm(AuthenticationForm): ) error_messages = { - **AuthenticationForm.error_messages, 'invalid_otp': gettext_lazy('Der TOTP-Code ist ungültig.'), 'missing_otp': gettext_lazy('Bitte geben Sie Ihren TOTP-Code ein.'), } + def __init__(self, *args, profile=None, **kwargs): + self.profile = profile + super().__init__(*args, **kwargs) + def clean(self): cleaned_data = super().clean() - user = self.get_user() - if not user: + profile = self.profile + if not profile or not profile.totp_enabled: return cleaned_data - profile, _ = UserProfile.objects.get_or_create(user=user) - if profile.totp_enabled: - otp_code = normalize_totp_token(cleaned_data.get('otp_code')) - recovery_code = normalize_recovery_code(cleaned_data.get('recovery_code')) - if recovery_code: - if not profile.consume_recovery_code(recovery_code): - raise ValidationError( - self.error_messages['invalid_otp'], - code='invalid_otp', - ) - return cleaned_data - if not otp_code: - raise ValidationError( - self.error_messages['missing_otp'], - code='missing_otp', - ) - if not profile.totp_secret or not verify_totp_token(profile.totp_secret, otp_code, for_time=int(timezone.now().timestamp())): - raise ValidationError( - self.error_messages['invalid_otp'], - code='invalid_otp', - ) + + otp_code = normalize_totp_token(cleaned_data.get('otp_code')) + recovery_code = normalize_recovery_code(cleaned_data.get('recovery_code')) + if recovery_code: + if not profile.consume_recovery_code(recovery_code): + raise ValidationError(self.error_messages['invalid_otp'], code='invalid_otp') + return cleaned_data + if not otp_code: + raise ValidationError(self.error_messages['missing_otp'], code='missing_otp') + if not profile.totp_secret or not verify_totp_token(profile.totp_secret, otp_code, for_time=int(timezone.now().timestamp())): + raise ValidationError(self.error_messages['invalid_otp'], code='invalid_otp') return cleaned_data @@ -307,18 +332,6 @@ class AccountTOTPDisableForm(forms.Form): strip=False, widget=forms.PasswordInput(attrs={'autocomplete': 'current-password'}), ) - verification_code = forms.CharField( - label=gettext_lazy('TOTP-Code'), - max_length=12, - required=False, - widget=forms.TextInput(attrs={'autocomplete': 'one-time-code', 'inputmode': 'numeric'}), - ) - recovery_code = forms.CharField( - label=gettext_lazy('Recovery-Code'), - max_length=32, - required=False, - widget=forms.TextInput(attrs={'autocomplete': 'one-time-code'}), - ) def __init__(self, *args, user=None, profile=None, **kwargs): super().__init__(*args, **kwargs) @@ -333,26 +346,12 @@ class AccountTOTPDisableForm(forms.Form): def clean(self): cleaned_data = super().clean() - code = normalize_totp_token(cleaned_data.get('verification_code')) - recovery_code = normalize_recovery_code(cleaned_data.get('recovery_code')) - if not code and not recovery_code: - raise ValidationError(_('Bitte geben Sie einen gültigen TOTP-Code oder Recovery-Code ein.')) - secret = getattr(self.profile, 'totp_secret', '') or '' - if code: - if not secret or not verify_totp_token(secret, code, for_time=int(timezone.now().timestamp())): - raise ValidationError(_('Der TOTP-Code ist ungültig.')) - return cleaned_data - if not self.profile.consume_recovery_code(recovery_code): - raise ValidationError(_('Der Recovery-Code ist ungültig.')) + if not self.profile or not self.profile.totp_enabled: + raise ValidationError(_('TOTP ist für dieses Konto nicht aktiv.')) return cleaned_data class AccountTOTPRegenerateRecoveryCodesForm(forms.Form): - current_password = forms.CharField( - label=gettext_lazy('Aktuelles Passwort'), - strip=False, - widget=forms.PasswordInput(attrs={'autocomplete': 'current-password'}), - ) verification_code = forms.CharField( label=gettext_lazy('TOTP-Code'), max_length=12, @@ -371,12 +370,6 @@ class AccountTOTPRegenerateRecoveryCodesForm(forms.Form): self.user = user self.profile = profile - def clean_current_password(self): - password = self.cleaned_data.get('current_password') or '' - if not self.user or not self.user.check_password(password): - raise ValidationError(_('Das aktuelle Passwort ist nicht korrekt.')) - return password - def clean(self): cleaned_data = super().clean() code = normalize_totp_token(cleaned_data.get('verification_code')) @@ -393,6 +386,87 @@ class AccountTOTPRegenerateRecoveryCodesForm(forms.Form): return cleaned_data +class AccountNotificationPreferencesForm(forms.Form): + onboarding_success = forms.BooleanField(label=gettext_lazy('Onboarding erfolgreich'), required=False) + onboarding_failure = forms.BooleanField(label=gettext_lazy('Onboarding fehlgeschlagen'), required=False) + offboarding_success = forms.BooleanField(label=gettext_lazy('Offboarding erfolgreich'), required=False) + offboarding_failure = forms.BooleanField(label=gettext_lazy('Offboarding fehlgeschlagen'), required=False) + backup_success = forms.BooleanField(label=gettext_lazy('Backup erfolgreich'), required=False) + backup_failure = forms.BooleanField(label=gettext_lazy('Backup fehlgeschlagen'), required=False) + welcome_email_success = forms.BooleanField(label=gettext_lazy('Welcome E-Mail erfolgreich'), required=False) + welcome_email_failure = forms.BooleanField(label=gettext_lazy('Welcome E-Mail fehlgeschlagen'), required=False) + trial_alerts = forms.BooleanField(label=gettext_lazy('Trial-Hinweise'), required=False) + system_alerts = forms.BooleanField(label=gettext_lazy('System-Hinweise'), required=False) + + FIELD_TO_EVENT = { + 'onboarding_success': UserProfile.NOTIFICATION_ONBOARDING_SUCCESS, + 'onboarding_failure': UserProfile.NOTIFICATION_ONBOARDING_FAILURE, + 'offboarding_success': UserProfile.NOTIFICATION_OFFBOARDING_SUCCESS, + 'offboarding_failure': UserProfile.NOTIFICATION_OFFBOARDING_FAILURE, + 'backup_success': UserProfile.NOTIFICATION_BACKUP_SUCCESS, + 'backup_failure': UserProfile.NOTIFICATION_BACKUP_FAILURE, + 'welcome_email_success': UserProfile.NOTIFICATION_WELCOME_EMAIL_SUCCESS, + 'welcome_email_failure': UserProfile.NOTIFICATION_WELCOME_EMAIL_FAILURE, + 'trial_alerts': UserProfile.NOTIFICATION_TRIAL_ALERTS, + 'system_alerts': UserProfile.NOTIFICATION_SYSTEM_ALERTS, + } + + GROUPS = [ + ('workflow', gettext_lazy('Workflow'), ['onboarding_success', 'onboarding_failure', 'offboarding_success', 'offboarding_failure']), + ('welcome', gettext_lazy('Welcome E-Mail'), ['welcome_email_success', 'welcome_email_failure']), + ('operations', gettext_lazy('Operations'), ['backup_success', 'backup_failure', 'system_alerts']), + ('platform', gettext_lazy('Platform'), ['trial_alerts']), + ] + + def __init__(self, *args, profile=None, user=None, **kwargs): + self.profile = profile + self.user = user + initial = kwargs.setdefault('initial', {}) + if profile is not None and not args: + prefs = profile.get_notification_preferences() + for field_name, event_key in self.FIELD_TO_EVENT.items(): + initial.setdefault(field_name, prefs.get(event_key, True)) + super().__init__(*args, **kwargs) + self.visible_field_names = self._compute_visible_field_names() + for field_name in list(self.fields.keys()): + if field_name not in self.visible_field_names: + self.fields.pop(field_name) + + def _compute_visible_field_names(self) -> list[str]: + visible = [ + 'onboarding_success', + 'onboarding_failure', + 'offboarding_success', + 'offboarding_failure', + 'welcome_email_success', + 'welcome_email_failure', + ] + if user_has_capability(self.user, 'manage_backups'): + visible.extend(['backup_success', 'backup_failure']) + if user_has_capability(self.user, 'manage_integrations'): + visible.append('system_alerts') + if user_has_capability(self.user, 'manage_trial_lifecycle'): + visible.append('trial_alerts') + return visible + + def grouped_fields(self): + groups = [] + for key, label, field_names in self.GROUPS: + rows = [self[name] for name in field_names if name in self.fields] + if rows: + groups.append({'key': key, 'label': label, 'fields': rows}) + return groups + + def save(self): + prefs = self.profile.get_notification_preferences() + for field_name in self.visible_field_names: + event_key = self.FIELD_TO_EVENT[field_name] + prefs[event_key] = bool(self.cleaned_data.get(field_name)) + self.profile.notification_preferences = prefs + self.profile.save(update_fields=['notification_preferences', 'updated_at']) + return self.profile + + class UserManagementCreateForm(forms.Form): first_name = forms.CharField(label=_('Vorname'), max_length=150, required=False) last_name = forms.CharField(label=_('Nachname'), max_length=150, required=False) @@ -604,7 +678,7 @@ class OnboardingRequestForm(forms.ModelForm): department = forms.ChoiceField(label='Abteilung', choices=DEPARTMENT_CHOICES, required=True) work_email = forms.EmailField( label='Gewünschte dienstliche E-Mail-Adresse', - help_text='Bitte nutzen Sie das Format name@tub.co.', + help_text='', ) contract_start = forms.DateField(label='Vertragsbeginn', widget=forms.DateInput(attrs={'type': 'date'})) employment_type = forms.ChoiceField(label='Beschäftigungsverhältnis', choices=EMPLOYMENT_CHOICES, required=True) diff --git a/backend/workflows/middleware.py b/backend/workflows/middleware.py index e19f025..f378f73 100644 --- a/backend/workflows/middleware.py +++ b/backend/workflows/middleware.py @@ -39,7 +39,7 @@ class RequestIDMiddleware: class RateLimitMiddleware: - LOGIN_PATHS = ('/accounts/login/',) + LOGIN_PATHS = ('/accounts/login/', '/accounts/login/totp/') PASSWORD_RESET_PATHS = ('/accounts/password_reset/',) # Keep this list path-prefix based so new platform actions get protected # without having to wire every single view into a second permission layer. @@ -157,7 +157,13 @@ class AuthSessionHardeningMiddleware: login_url = reverse('login') return redirect(f'{login_url}?next={request.get_full_path()}') - if request.method == 'POST' and any(path.startswith(prefix) for prefix in self.SENSITIVE_POST_PREFIXES): + is_sensitive_post = request.method == 'POST' and any(path.startswith(prefix) for prefix in self.SENSITIVE_POST_PREFIXES) + if request.method == 'POST' and path == '/account/': + account_form = (request.POST.get('account_form') or '').strip() + if account_form in {'totp_disable', 'totp_regenerate_codes'}: + is_sensitive_post = True + + if is_sensitive_post: fresh_window = max(60, settings.SENSITIVE_ACTION_REAUTH_SECONDS) auth_fresh_ts = int(request.session.get('auth_fresh_ts') or last_activity_ts) if now_ts - auth_fresh_ts > fresh_window: diff --git a/backend/workflows/migrations/0051_usernotification.py b/backend/workflows/migrations/0051_usernotification.py new file mode 100644 index 0000000..de26bc8 --- /dev/null +++ b/backend/workflows/migrations/0051_usernotification.py @@ -0,0 +1,31 @@ +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('workflows', '0050_userprofile_totp_recovery_codes'), + ] + + operations = [ + migrations.CreateModel( + name='UserNotification', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=255)), + ('body', models.TextField(blank=True, default='')), + ('level', models.CharField(choices=[('info', 'Info'), ('success', 'Erfolg'), ('warning', 'Warnung'), ('error', 'Fehler')], default='info', max_length=20)), + ('link_url', models.CharField(blank=True, default='', max_length=500)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('read_at', models.DateTimeField(blank=True, null=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'User Notification', + 'verbose_name_plural': 'User Notifications', + 'ordering': ['-created_at', '-id'], + }, + ), + ] diff --git a/backend/workflows/migrations/0052_userprofile_notification_preferences.py b/backend/workflows/migrations/0052_userprofile_notification_preferences.py new file mode 100644 index 0000000..33eda56 --- /dev/null +++ b/backend/workflows/migrations/0052_userprofile_notification_preferences.py @@ -0,0 +1,16 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('workflows', '0051_usernotification'), + ] + + operations = [ + migrations.AddField( + model_name='userprofile', + name='notification_preferences', + field=models.JSONField(blank=True, default=dict), + ), + ] diff --git a/backend/workflows/models.py b/backend/workflows/models.py index 0b5f3eb..963315f 100644 --- a/backend/workflows/models.py +++ b/backend/workflows/models.py @@ -28,6 +28,29 @@ class EmployeeProfile(models.Model): class UserProfile(models.Model): + NOTIFICATION_ONBOARDING_SUCCESS = 'onboarding_success' + NOTIFICATION_ONBOARDING_FAILURE = 'onboarding_failure' + NOTIFICATION_OFFBOARDING_SUCCESS = 'offboarding_success' + NOTIFICATION_OFFBOARDING_FAILURE = 'offboarding_failure' + NOTIFICATION_BACKUP_SUCCESS = 'backup_success' + NOTIFICATION_BACKUP_FAILURE = 'backup_failure' + NOTIFICATION_WELCOME_EMAIL_SUCCESS = 'welcome_email_success' + NOTIFICATION_WELCOME_EMAIL_FAILURE = 'welcome_email_failure' + NOTIFICATION_TRIAL_ALERTS = 'trial_alerts' + NOTIFICATION_SYSTEM_ALERTS = 'system_alerts' + NOTIFICATION_PREFERENCE_DEFAULTS = { + NOTIFICATION_ONBOARDING_SUCCESS: True, + NOTIFICATION_ONBOARDING_FAILURE: True, + NOTIFICATION_OFFBOARDING_SUCCESS: True, + NOTIFICATION_OFFBOARDING_FAILURE: True, + NOTIFICATION_BACKUP_SUCCESS: True, + NOTIFICATION_BACKUP_FAILURE: True, + NOTIFICATION_WELCOME_EMAIL_SUCCESS: True, + NOTIFICATION_WELCOME_EMAIL_FAILURE: True, + NOTIFICATION_TRIAL_ALERTS: True, + NOTIFICATION_SYSTEM_ALERTS: True, + } + user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='profile') avatar_image = models.FileField( upload_to='profiles/', @@ -45,6 +68,7 @@ class UserProfile(models.Model): totp_enabled = models.BooleanField(default=False) totp_confirmed_at = models.DateTimeField(null=True, blank=True) totp_recovery_codes = models.JSONField(default=list, blank=True) + notification_preferences = models.JSONField(default=dict, blank=True) updated_at = models.DateTimeField(auto_now=True) class Meta: @@ -84,6 +108,55 @@ class UserProfile(models.Model): self.save(update_fields=['totp_recovery_codes', 'updated_at']) return matched + def get_notification_preferences(self) -> dict[str, bool]: + current = self.notification_preferences or {} + prefs = dict(self.NOTIFICATION_PREFERENCE_DEFAULTS) + for key in prefs: + if key in current: + prefs[key] = bool(current[key]) + return prefs + + def notification_enabled(self, event_key: str) -> bool: + return bool(self.get_notification_preferences().get(event_key, True)) + + +class UserNotification(models.Model): + LEVEL_INFO = 'info' + LEVEL_SUCCESS = 'success' + LEVEL_WARNING = 'warning' + LEVEL_ERROR = 'error' + LEVEL_CHOICES = [ + (LEVEL_INFO, _('Info')), + (LEVEL_SUCCESS, _('Erfolg')), + (LEVEL_WARNING, _('Warnung')), + (LEVEL_ERROR, _('Fehler')), + ] + + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='notifications') + title = models.CharField(max_length=255) + body = models.TextField(blank=True, default='') + level = models.CharField(max_length=20, choices=LEVEL_CHOICES, default=LEVEL_INFO) + link_url = models.CharField(max_length=500, blank=True, default='') + created_at = models.DateTimeField(auto_now_add=True) + read_at = models.DateTimeField(null=True, blank=True) + + class Meta: + ordering = ['-created_at', '-id'] + verbose_name = 'User Notification' + verbose_name_plural = 'User Notifications' + + def __str__(self) -> str: + return f'{self.user_id} | {self.level} | {self.title}' + + @property + def is_unread(self) -> bool: + return self.read_at is None + + def mark_read(self) -> None: + if self.read_at is None: + self.read_at = timezone.now() + self.save(update_fields=['read_at']) + class PortalBranding(models.Model): name = models.CharField(max_length=80, default='Default', unique=True) diff --git a/backend/workflows/notifications.py b/backend/workflows/notifications.py new file mode 100644 index 0000000..e7f35b0 --- /dev/null +++ b/backend/workflows/notifications.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +from django.contrib.auth import get_user_model + +from .models import UserNotification, UserProfile + + +def create_user_notification(*, user, title: str, body: str = '', level: str = UserNotification.LEVEL_INFO, link_url: str = '') -> UserNotification: + return UserNotification.objects.create( + user=user, + title=(title or '').strip(), + body=(body or '').strip(), + level=level, + link_url=(link_url or '').strip(), + ) + + +def notify_user(*, user, title: str, body: str = '', level: str = UserNotification.LEVEL_INFO, link_url: str = '', event_key: str = '') -> bool: + if not user or not getattr(user, 'is_authenticated', False): + return False + profile, _ = UserProfile.objects.get_or_create(user=user) + if event_key and not profile.notification_enabled(event_key): + return False + create_user_notification(user=user, title=title, body=body, level=level, link_url=link_url) + return True + + +def notify_user_by_email(*, email: str, title: str, body: str = '', level: str = UserNotification.LEVEL_INFO, link_url: str = '', event_key: str = '') -> bool: + normalized_email = (email or '').strip().lower() + if not normalized_email: + return False + user = get_user_model().objects.filter(email__iexact=normalized_email).first() + if not user: + return False + return notify_user(user=user, title=title, body=body, level=level, link_url=link_url, event_key=event_key) diff --git a/backend/workflows/static/workflows/css/account.css b/backend/workflows/static/workflows/css/account.css index bd235a8..648f1b0 100644 --- a/backend/workflows/static/workflows/css/account.css +++ b/backend/workflows/static/workflows/css/account.css @@ -19,7 +19,7 @@ body { } .account-shell-body { - padding: 28px; + padding: 24px; background: radial-gradient(90% 120% at 10% 0%, rgba(31, 79, 214, 0.06), rgba(31, 79, 214, 0)), linear-gradient(180deg, rgba(255,255,255,0.72), rgba(248,251,255,0.48)); @@ -29,7 +29,7 @@ body { width: min(1120px, 100%); margin: 0 auto; display: grid; - gap: 22px; + gap: 18px; } .account-hero { @@ -37,13 +37,14 @@ body { justify-content: space-between; gap: 18px; align-items: flex-start; - padding: 26px 28px; + padding: 22px 24px; border: 1px solid #d9e3f0; border-radius: 24px; background: radial-gradient(circle at top right, rgba(30, 64, 175, 0.1), transparent 24%), linear-gradient(135deg, rgba(255,255,255,0.96), rgba(244,248,255,0.9)); box-shadow: 0 14px 34px rgba(28, 45, 79, 0.08); + animation: accountFadeUp 320ms cubic-bezier(0.2, 0.8, 0.2, 1); } .account-kicker { @@ -72,6 +73,16 @@ body { line-height: 1.55; } +.account-hero-submeta { + margin-top: 14px !important; + color: #6a7a8f; + font-size: 13px; +} + +.account-hero-submeta strong { + color: #132238; +} + .account-hero-badges { display: flex; gap: 10px; @@ -111,12 +122,20 @@ body { border-radius: 22px; background: rgba(255, 255, 255, 0.94); box-shadow: 0 14px 32px rgba(28, 45, 79, 0.09); + transition: transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1), box-shadow 220ms cubic-bezier(0.2, 0.8, 0.2, 1), border-color 180ms cubic-bezier(0.2, 0.8, 0.2, 1); } .account-profile-card { - padding: 24px; + padding: 20px; position: sticky; top: 24px; + animation: accountFadeUp 360ms cubic-bezier(0.2, 0.8, 0.2, 1); +} + +.account-profile-card:hover, +.account-panel:hover { + transform: translateY(-1px); + box-shadow: 0 18px 34px rgba(28, 45, 79, 0.10); } .account-avatar-form { @@ -205,30 +224,36 @@ body { word-break: break-word; } -.account-profile-meta { +.account-nav { display: grid; - gap: 12px; + gap: 10px; margin-top: 22px; } -.account-profile-meta div { - padding: 12px 14px; - border-radius: 14px; - background: #f7faff; +.account-nav-item { + width: 100%; + padding: 14px 16px; + text-align: left; + border-radius: 16px; border: 1px solid #dce6f2; + background: #f7faff; + color: #17345e; + font: inherit; + font-weight: 700; + cursor: pointer; + transition: border-color 180ms cubic-bezier(0.2, 0.8, 0.2, 1), background-color 180ms cubic-bezier(0.2, 0.8, 0.2, 1), box-shadow 180ms cubic-bezier(0.2, 0.8, 0.2, 1); } -.account-profile-meta span { - display: block; - margin-bottom: 4px; - color: #6b7a90; - font-size: 12px; +.account-nav-item:hover { + border-color: #c5d5ea; + background: #f3f8ff; } -.account-profile-meta strong { - color: #132238; - font-size: 14px; - line-height: 1.4; +.account-nav-item.is-active { + border-color: rgba(0, 0, 120, 0.18); + background: linear-gradient(180deg, rgba(238,243,255,0.95), rgba(231,239,255,0.92)); + box-shadow: inset 0 1px 0 rgba(255,255,255,0.7); + transform: translateX(3px); } .account-main { @@ -236,8 +261,129 @@ body { gap: 22px; } +.account-notification-pref-grid { + display: grid; + gap: 12px; +} + +.account-notification-group + .account-notification-group { + margin-top: 18px; +} + +.account-notification-group h3 { + margin: 0 0 10px; + color: #17345e; + font-size: 13px; + font-weight: 800; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.account-notification-pref-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 14px 16px; + border: 1px solid #dce6f2; + border-radius: 16px; + background: #f7faff; +} + +.account-notification-pref-item span { + color: #17345e; + font-weight: 700; +} + +.account-notification-pref-item strong { + color: #617389; + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.account-notification-pref-grid-edit { + gap: 14px; +} + +.account-notification-pref-toggle { + display: flex; + align-items: center; + justify-content: space-between; + gap: 18px; + padding: 14px 16px; + border: 1px solid #dce6f2; + border-radius: 16px; + background: #f7faff; +} + +.account-notification-pref-copy { + display: grid; + gap: 4px; +} + +.account-notification-pref-copy strong { + color: #17345e; + font-size: 14px; +} + +.account-notification-pref-copy small { + color: #617389; + font-size: 12px; + line-height: 1.45; +} + +.account-toggle-control { + position: relative; + display: inline-flex; + align-items: center; + flex: 0 0 auto; +} + +.account-toggle-control input { + position: absolute; + inset: 0; + opacity: 0; + cursor: pointer; +} + +.account-toggle-slider { + position: relative; + width: 50px; + height: 30px; + border-radius: 999px; + background: #d7e1ee; + transition: background-color 180ms cubic-bezier(0.2, 0.8, 0.2, 1); +} + +.account-toggle-slider::after { + content: ""; + position: absolute; + top: 4px; + left: 4px; + width: 22px; + height: 22px; + border-radius: 50%; + background: #fff; + box-shadow: 0 4px 10px rgba(18, 34, 56, 0.12); + transition: transform 180ms cubic-bezier(0.2, 0.8, 0.2, 1); +} + +.account-toggle-control input:checked + .account-toggle-slider { + background: #0f8f57; +} + +.account-toggle-control input:checked + .account-toggle-slider::after { + transform: translateX(20px); +} + .account-panel { padding: 24px; + animation: accountFadeUp 380ms cubic-bezier(0.2, 0.8, 0.2, 1); +} + +.account-panel.is-entering { + animation: accountPanelSwap 260ms cubic-bezier(0.2, 0.8, 0.2, 1); } .account-panel-head { @@ -299,7 +445,7 @@ body { } .account-security-item { - padding: 16px 18px; + padding: 14px 16px; border-radius: 18px; border: 1px solid #dbe5f2; background: @@ -307,6 +453,13 @@ body { linear-gradient(180deg, rgba(255,255,255,0.96), rgba(246,250,255,0.9)); } +.account-security-item-active { + border-color: rgba(34, 139, 86, 0.26); + background: + radial-gradient(circle at top right, rgba(34, 139, 86, 0.10), transparent 26%), + linear-gradient(180deg, rgba(242,255,247,0.96), rgba(236,251,242,0.92)); +} + .account-security-item span { display: block; margin-bottom: 6px; @@ -333,7 +486,7 @@ body { .account-totp-card { margin-bottom: 18px; - padding: 18px; + padding: 16px; border-radius: 18px; border: 1px solid #dbe5f2; background: @@ -356,7 +509,55 @@ body { } .account-totp-form { - margin-top: 14px; + margin-top: 12px; +} + +.account-totp-action-row { + display: flex; + justify-content: space-between; + align-items: center; + gap: 16px; +} + +.account-totp-action-copy strong { + display: block; + color: #132238; + font-size: 15px; +} + +.account-totp-action-copy p { + margin: 6px 0 0; + color: #617389; + font-size: 13px; + line-height: 1.45; +} + +.account-totp-toggle-form { + margin-top: 12px; +} + +.account-totp-status-row { + display: flex; + justify-content: space-between; + gap: 18px; + align-items: center; + padding: 14px 16px; + border-radius: 18px; + border: 1px solid #dbe5f2; + background: rgba(255,255,255,0.88); +} + +.account-totp-status-copy strong { + display: block; + color: #132238; + font-size: 16px; +} + +.account-totp-status-copy p { + margin: 6px 0 0; + color: #55718f; + font-size: 13px; + line-height: 1.45; } .account-qr-card, @@ -456,6 +657,19 @@ body { grid-column: 1 / -1; } +.account-form-field.is-hidden { + display: none; +} + +.account-totp-form.is-hidden { + display: none; +} + +.account-form-grid.is-hidden, +.account-inline-actions.is-hidden { + display: none; +} + .account-form-field label { color: #132238; font-size: 13px; @@ -506,6 +720,32 @@ body { margin-top: 16px; } +.account-recovery-toggle { + width: auto; +} + +@keyframes accountFadeUp { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes accountPanelSwap { + from { + opacity: 0; + transform: translateY(10px) scale(0.995); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + @media (max-width: 980px) { .account-layout { grid-template-columns: 1fr; @@ -540,10 +780,6 @@ body { font-size: 26px; } - .account-hero-badges { - justify-content: flex-start; - } - .account-detail-grid, .account-security-overview, .account-form-grid, @@ -558,4 +794,39 @@ body { .account-panel-head { flex-direction: column; } + + .account-totp-status-card { + flex-direction: column; + } + + .account-totp-status-row { + flex-direction: column; + align-items: stretch; + } + + .account-totp-action-row { + flex-direction: column; + align-items: stretch; + } +} + +@media (prefers-reduced-motion: reduce) { + .account-hero, + .account-profile-card, + .account-panel, + .account-panel.is-entering { + animation: none; + } + + .account-profile-card, + .account-panel, + .account-nav-item { + transition: none; + } + + .account-profile-card:hover, + .account-panel:hover, + .account-nav-item.is-active { + transform: none; + } } diff --git a/backend/workflows/static/workflows/css/app_chrome.css b/backend/workflows/static/workflows/css/app_chrome.css index ad297ff..f1d184f 100644 --- a/backend/workflows/static/workflows/css/app_chrome.css +++ b/backend/workflows/static/workflows/css/app_chrome.css @@ -180,10 +180,227 @@ align-items: center; } +.app-notification-menu, .app-user-menu { position: relative; } +.app-notification-trigger { + list-style: none; + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + width: 44px; + height: 44px; + padding: 0; + border: 1px solid var(--app-line); + border-radius: 999px; + background: rgba(248, 251, 255, 0.92); + color: #1f3a5f; + cursor: pointer; + transition: + border-color var(--motion-fast) var(--motion-ease), + background-color var(--motion-fast) var(--motion-ease), + transform var(--motion-fast) var(--motion-ease), + box-shadow var(--motion-fast) var(--motion-ease); +} + +.app-notification-trigger::-webkit-details-marker { + display: none; +} + +.app-notification-trigger:hover { + transform: translateY(-1px); +} + +.app-notification-menu[open] .app-notification-trigger { + border-color: rgba(0, 0, 120, 0.22); + box-shadow: 0 0 0 4px rgba(0, 0, 120, 0.08); +} + +.app-notification-bell { + display: inline-flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + color: #28446e; +} + +.app-notification-bell svg { + width: 18px; + height: 18px; + display: block; +} + +.app-notification-count { + position: absolute; + top: -2px; + right: -2px; + min-width: 18px; + height: 18px; + padding: 0 5px; + border-radius: 999px; + background: #c0002b; + color: #fff; + font-size: 10px; + font-weight: 800; + line-height: 18px; + text-align: center; + box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.98); +} + +.app-notification-panel { + position: absolute; + top: calc(100% + 10px); + right: 0; + width: min(380px, calc(100vw - 32px)); + max-height: min(70vh, 520px); + padding: 10px; + border: 1px solid rgba(217, 227, 238, 0.96); + border-radius: 18px; + background: rgba(255, 255, 255, 0.98); + box-shadow: 0 24px 44px rgba(18, 34, 56, 0.16); + display: flex; + flex-direction: column; + gap: 10px; + z-index: 45; + overflow: hidden; +} + +.app-notification-panel-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 4px 6px 8px; + border-bottom: 1px solid rgba(217, 227, 238, 0.85); +} + +.app-notification-panel-head strong { + color: #132238; + font-size: 13px; + line-height: 1.2; +} + +.app-notification-panel-head form { + margin: 0; +} + +.app-notification-panel-head button { + border: 0; + background: transparent; + color: var(--app-brand-blue); + font-size: 12px; + font-weight: 700; + cursor: pointer; + padding: 0; +} + +.app-notification-list { + display: grid; + gap: 8px; + overflow: auto; + padding-right: 2px; +} + +.app-notification-item { + display: grid; + gap: 8px; + padding: 10px 12px; + border: 1px solid rgba(217, 227, 238, 0.92); + border-radius: 14px; + background: linear-gradient(180deg, rgba(249, 252, 255, 0.96), rgba(243, 248, 255, 0.92)); +} + +.app-notification-item.is-unread { + border-color: rgba(0, 0, 120, 0.22); + box-shadow: inset 3px 0 0 rgba(0, 0, 120, 0.9); +} + +.app-notification-success { + background: linear-gradient(180deg, #f4fcf7, #edf9f1); + border-color: #cce9d5; +} + +.app-notification-error { + background: linear-gradient(180deg, #fff7f7, #fff1f1); + border-color: #f0c8c8; +} + +.app-notification-warning { + background: linear-gradient(180deg, #fffaf0, #fff4dd); + border-color: #f3d9a7; +} + +.app-notification-copy { + display: grid; + gap: 4px; +} + +.app-notification-copy strong { + color: #132238; + font-size: 13px; + line-height: 1.35; +} + +.app-notification-copy p { + margin: 0; + color: #51657f; + font-size: 12px; + line-height: 1.45; +} + +.app-notification-copy span { + color: #7a8ca3; + font-size: 11px; + line-height: 1.3; +} + +.app-notification-actions { + display: flex; + align-items: center; + gap: 8px; +} + +.app-notification-actions a, +.app-notification-actions button { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 32px; + padding: 0 10px; + border-radius: 10px; + border: 1px solid rgba(217, 227, 238, 0.92); + background: rgba(255, 255, 255, 0.82); + color: #1f3a5f; + font: inherit; + font-size: 12px; + font-weight: 700; + line-height: 1; + text-decoration: none; + cursor: pointer; + transition: + background-color var(--motion-fast) var(--motion-ease), + border-color var(--motion-fast) var(--motion-ease), + color var(--motion-fast) var(--motion-ease); +} + +.app-notification-actions a:hover, +.app-notification-actions button:hover, +.app-notification-panel-head button:hover { + color: var(--app-brand-blue); +} + +.app-notification-empty { + padding: 14px 8px 8px; + color: #64748b; + font-size: 13px; + line-height: 1.5; + text-align: center; +} + .app-user-trigger { list-style: none; display: inline-flex; @@ -330,6 +547,10 @@ .app-user-panel a:focus-visible, .app-user-panel button:focus-visible, +.app-notification-trigger:focus-visible, +.app-notification-actions a:focus-visible, +.app-notification-actions button:focus-visible, +.app-notification-panel-head button:focus-visible, .app-user-trigger:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(0, 0, 120, 0.10); diff --git a/backend/workflows/static/workflows/css/login.css b/backend/workflows/static/workflows/css/login.css index 2b3664a..78a976a 100644 --- a/backend/workflows/static/workflows/css/login.css +++ b/backend/workflows/static/workflows/css/login.css @@ -47,6 +47,26 @@ body { line-height: 1.45; } +.login-step-caption { + display: grid; + gap: 2px; + margin: 0 0 14px; + padding: 12px 14px; + border: 1px solid #d9e3f0; + border-radius: 14px; + background: #f7faff; +} + +.login-step-caption strong { + color: #132238; + font-size: 14px; +} + +.login-step-caption span { + color: #607086; + font-size: 13px; +} + .account-card { width: min(560px, 100%); } @@ -138,6 +158,34 @@ body { width: 100%; } +.btn-inline-toggle { + width: auto; + min-width: 0; +} + +.login-recovery-toggle-row { + display: flex; + justify-content: flex-start; + margin: -2px 0 12px; +} + +.login-recovery-box.is-hidden { + display: none; +} + +.login-back-link { + display: inline-flex; + align-items: center; + margin-top: 14px; + color: #36506e; + font-size: 14px; + text-decoration: none; +} + +.login-back-link:hover { + text-decoration: underline; +} + .login-card .app-alert { margin: 0 0 12px; } diff --git a/backend/workflows/tasks.py b/backend/workflows/tasks.py index 950e4eb..1b9aa58 100644 --- a/backend/workflows/tasks.py +++ b/backend/workflows/tasks.py @@ -28,6 +28,7 @@ from .forms import ( SOFTWARE_EXTRA_CHOICES, WORKSPACE_GROUP_CHOICES, ) +from .notifications import notify_user_by_email # These templates are the product-level defaults for fresh deployments. # Runtime branding and company config can override the company-facing identity @@ -251,6 +252,32 @@ DEFAULT_NOTIFICATION_TEMPLATES = { } +def _notify_request_result(*, recipient_email: str, title: str, body: str, level: str, event_key: str) -> None: + notify_user_by_email( + email=recipient_email, + title=title, + body=body, + level=level, + link_url='/requests/', + event_key=event_key, + ) + + +def _notify_welcome_email_result(*, recipient_email: str, full_name: str, body: str, level: str, event_key: str) -> None: + notify_user_by_email( + email=recipient_email, + title=( + _('Welcome E-Mail gesendet: %(name)s') % {'name': full_name} + if event_key == 'welcome_email_success' + else _('Welcome E-Mail fehlgeschlagen: %(name)s') % {'name': full_name} + ), + body=body, + level=level, + link_url='/admin-tools/welcome-emails/', + event_key=event_key, + ) + + def _start_task_log(task_name: str, *, target_type: str = '', target_id: int | None = None, target_label: str = '') -> AsyncTaskLog: task_request = getattr(current_task, 'request', None) return AsyncTaskLog.objects.create( @@ -1331,11 +1358,25 @@ def process_onboarding_request(onboarding_request_id: int) -> None: request_obj.processing_status = 'completed' request_obj.last_error = '' request_obj.save(update_fields=['processing_status', 'last_error']) + _notify_request_result( + recipient_email=request_obj.onboarded_by_email, + title=_('Onboarding abgeschlossen: %(name)s') % {'name': request_obj.full_name}, + body=_('Die Onboarding-Anfrage wurde erfolgreich verarbeitet.'), + level='success', + event_key='onboarding_success', + ) _finish_task_log(task_log, status='succeeded') except Exception as exc: request_obj.processing_status = 'failed' request_obj.last_error = str(exc) request_obj.save(update_fields=['processing_status', 'last_error']) + _notify_request_result( + recipient_email=request_obj.onboarded_by_email, + title=_('Onboarding fehlgeschlagen: %(name)s') % {'name': request_obj.full_name}, + body=str(exc), + level='error', + event_key='onboarding_failure', + ) _finish_task_log(task_log, status='failed', error_message=str(exc)) raise @@ -1355,7 +1396,7 @@ def process_offboarding_request(offboarding_request_id: int) -> None: try: branding_copy = get_branding_email_copy() company_contact = get_company_contact_copy() - it_email, general_info_email, _, hr_works_email, _ = _resolve_workflow_emails() + it_email, general_info_email, business_card_email_unused, hr_works_email, key_email_unused = _resolve_workflow_emails() pdf_path = _generate_offboarding_pdf(request_obj) request_obj.generated_pdf_path = str(pdf_path) @@ -1418,11 +1459,25 @@ def process_offboarding_request(offboarding_request_id: int) -> None: request_obj.processing_status = 'completed' request_obj.last_error = '' request_obj.save(update_fields=['processing_status', 'last_error']) + _notify_request_result( + recipient_email=request_obj.requested_by_email, + title=_('Offboarding abgeschlossen: %(name)s') % {'name': request_obj.full_name}, + body=_('Die Offboarding-Anfrage wurde erfolgreich verarbeitet.'), + level='success', + event_key='offboarding_success', + ) _finish_task_log(task_log, status='succeeded') except Exception as exc: request_obj.processing_status = 'failed' request_obj.last_error = str(exc) request_obj.save(update_fields=['processing_status', 'last_error']) + _notify_request_result( + recipient_email=request_obj.requested_by_email, + title=_('Offboarding fehlgeschlagen: %(name)s') % {'name': request_obj.full_name}, + body=str(exc), + level='error', + event_key='offboarding_failure', + ) _finish_task_log(task_log, status='failed', error_message=str(exc)) raise @@ -1490,10 +1545,24 @@ def send_scheduled_welcome_email(scheduled_email_id: int, force_now: bool = Fals scheduled.status = 'sent' scheduled.sent_at = timezone.now() scheduled.last_error = '' + _notify_welcome_email_result( + recipient_email=request_obj.onboarded_by_email, + full_name=request_obj.full_name, + body=_('Die geplante Welcome E-Mail wurde erfolgreich versendet.'), + level='success', + event_key='welcome_email_success', + ) _finish_task_log(task_log, status='succeeded') except Exception as exc: scheduled.status = 'failed' scheduled.last_error = str(exc) + _notify_welcome_email_result( + recipient_email=request_obj.onboarded_by_email, + full_name=request_obj.full_name, + body=str(exc), + level='error', + event_key='welcome_email_failure', + ) _finish_task_log(task_log, status='failed', error_message=str(exc)) raise finally: diff --git a/backend/workflows/templates/workflows/account_profile.html b/backend/workflows/templates/workflows/account_profile.html index 349aa89..12919a8 100644 --- a/backend/workflows/templates/workflows/account_profile.html +++ b/backend/workflows/templates/workflows/account_profile.html @@ -18,7 +18,11 @@
{% trans "Ihre aktuelle Workdock-Kontoübersicht und wichtige Sicherheitsaktionen." %}
+{% trans "Ihre aktuelle Kontoübersicht und wichtige Sicherheitsaktionen." %}
+{{ account_user.email|default:account_user.username }}
-