snapshot: preserve extended branding layer and branding UI polish

This commit is contained in:
Md Bayazid Bostame
2026-03-26 12:29:26 +01:00
parent c195efe339
commit 007d4e329a
14 changed files with 525 additions and 141 deletions

View File

@@ -2,14 +2,14 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: tubco-portal\n" "Project-Id-Version: tubco-portal\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-26 10:55+0000\n" "POT-Creation-Date: 2026-03-26 11:02+0000\n"
"PO-Revision-Date: 2026-03-24 00:00+0000\n" "PO-Revision-Date: 2026-03-24 00:00+0000\n"
"Language: en\n" "Language: en\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
#: workflows/app_registry.py:32 workflows/models.py:261 workflows/models.py:342 #: workflows/app_registry.py:32 workflows/models.py:273 workflows/models.py:354
#: workflows/templates/workflows/onboarding_form.html:25 #: workflows/templates/workflows/onboarding_form.html:25
#: workflows/templates/workflows/requests_dashboard.html:68 #: workflows/templates/workflows/requests_dashboard.html:68
#: workflows/templates/workflows/requests_dashboard.html:131 #: workflows/templates/workflows/requests_dashboard.html:131
@@ -36,7 +36,7 @@ msgstr "Multi-step form"
msgid "E-Mail Routing" msgid "E-Mail Routing"
msgstr "Email routing" msgstr "Email routing"
#: workflows/app_registry.py:43 workflows/models.py:262 workflows/models.py:343 #: workflows/app_registry.py:43 workflows/models.py:274 workflows/models.py:355
#: workflows/templates/workflows/requests_dashboard.html:78 #: workflows/templates/workflows/requests_dashboard.html:78
#: workflows/templates/workflows/requests_dashboard.html:132 #: workflows/templates/workflows/requests_dashboard.html:132
msgid "Offboarding" msgid "Offboarding"
@@ -213,7 +213,7 @@ msgstr "Django Admin"
msgid "Vollständige Datenverwaltung." msgid "Vollständige Datenverwaltung."
msgstr "Full data management." msgstr "Full data management."
#: workflows/app_registry.py:165 workflows/models.py:68 #: workflows/app_registry.py:165 workflows/models.py:80
msgid "Apps" msgid "Apps"
msgstr "Apps" msgstr "Apps"
@@ -221,7 +221,7 @@ msgstr "Apps"
msgid "Wählen Sie den gewünschten Prozess." msgid "Wählen Sie den gewünschten Prozess."
msgstr "Choose the desired process." msgstr "Choose the desired process."
#: workflows/app_registry.py:171 workflows/models.py:69 #: workflows/app_registry.py:171 workflows/models.py:81
msgid "Platform Apps" msgid "Platform Apps"
msgstr "" msgstr ""
@@ -231,7 +231,7 @@ msgstr ""
msgid "Produktweite Konfiguration und Produktsteuerung." msgid "Produktweite Konfiguration und Produktsteuerung."
msgstr "Configuration, tests, and controls." msgstr "Configuration, tests, and controls."
#: workflows/app_registry.py:177 workflows/models.py:70 #: workflows/app_registry.py:177 workflows/models.py:82
msgid "Admin Apps" msgid "Admin Apps"
msgstr "Admin Apps" msgstr "Admin Apps"
@@ -338,307 +338,339 @@ msgstr "Invalid role."
msgid "Nur Platform Owner dürfen diese Rolle vergeben." msgid "Nur Platform Owner dürfen diese Rolle vergeben."
msgstr "" msgstr ""
#: workflows/forms.py:188 #: workflows/forms.py:195
msgid "Portal-Titel" msgid "Portal-Titel"
msgstr "Portal title" msgstr "Portal title"
#: workflows/forms.py:189 #: workflows/forms.py:196
msgid "Firmenname" msgid "Firmenname"
msgstr "Company name" msgstr "Company name"
#: workflows/forms.py:190 #: workflows/forms.py:197
#, fuzzy #, fuzzy
#| msgid "Firmenname" #| msgid "Firmenname"
msgid "Firmen-Domain" msgid "Firmen-Domain"
msgstr "Company name" msgstr "Company name"
#: workflows/forms.py:191 #: workflows/forms.py:198
msgid "Support-E-Mail" msgid "Support-E-Mail"
msgstr "Support email" msgstr "Support email"
#: workflows/forms.py:192 #: workflows/forms.py:199
msgid "Absender-Anzeigename"
msgstr ""
#: workflows/forms.py:200
msgid "Login-Untertitel"
msgstr ""
#: workflows/forms.py:201
msgid "Footer-Text DE"
msgstr ""
#: workflows/forms.py:202
msgid "Footer-Text EN"
msgstr ""
#: workflows/forms.py:203
msgid "Rechtlicher Hinweis DE"
msgstr ""
#: workflows/forms.py:204
msgid "Rechtlicher Hinweis EN"
msgstr ""
#: workflows/forms.py:205
msgid "Standardsprache" msgid "Standardsprache"
msgstr "Default language" msgstr "Default language"
#: workflows/forms.py:193 #: workflows/forms.py:206
msgid "Logo" msgid "Logo"
msgstr "Logo" msgstr "Logo"
#: workflows/forms.py:194 #: workflows/forms.py:207
msgid "PDF-Briefkopf" msgid "PDF-Briefkopf"
msgstr "PDF letterhead" msgstr "PDF letterhead"
#: workflows/forms.py:195 #: workflows/forms.py:208
msgid "Favicon"
msgstr ""
#: workflows/forms.py:209
msgid "Primärfarbe" msgid "Primärfarbe"
msgstr "Primary color" msgstr "Primary color"
#: workflows/forms.py:196 #: workflows/forms.py:210
msgid "Sekundärfarbe" msgid "Sekundärfarbe"
msgstr "Secondary color" msgstr "Secondary color"
#: workflows/forms.py:210 #: workflows/forms.py:227
msgid "Das Logo darf maximal 5 MB groß sein." msgid "Das Logo darf maximal 5 MB groß sein."
msgstr "" msgstr ""
#: workflows/forms.py:218 #: workflows/forms.py:235
msgid "Der PDF-Briefkopf darf maximal 10 MB groß sein." msgid "Der PDF-Briefkopf darf maximal 10 MB groß sein."
msgstr "" msgstr ""
#: workflows/forms.py:357 workflows/forms.py:542 #: workflows/forms.py:243
msgid "Das Favicon darf maximal 2 MB groß sein."
msgstr ""
#: workflows/forms.py:382 workflows/forms.py:567
#, python-format #, python-format
msgid "Bitte nutzen Sie das Format name@%(domain)s." msgid "Bitte nutzen Sie das Format name@%(domain)s."
msgstr "" msgstr ""
#: workflows/forms.py:379 workflows/forms.py:556 #: workflows/forms.py:404 workflows/forms.py:581
#, python-format #, python-format
msgid "Bitte verwenden Sie eine @%(domain)s E-Mail-Adresse." msgid "Bitte verwenden Sie eine @%(domain)s E-Mail-Adresse."
msgstr "" msgstr ""
#: workflows/forms.py:464 #: workflows/forms.py:489
#, python-format #, python-format
msgid "" msgid ""
"Das Übergabedatum muss mindestens %(days)s Tage in der Zukunft liegen " "Das Übergabedatum muss mindestens %(days)s Tage in der Zukunft liegen "
"(frühestens %(date)s)." "(frühestens %(date)s)."
msgstr "" msgstr ""
#: workflows/models.py:139 workflows/views.py:200 #: workflows/models.py:151 workflows/views.py:200
msgid "Eingereicht" msgid "Eingereicht"
msgstr "Submitted" msgstr "Submitted"
#: workflows/models.py:140 workflows/views.py:201 #: workflows/models.py:152 workflows/views.py:201
msgid "In Bearbeitung" msgid "In Bearbeitung"
msgstr "Processing" msgstr "Processing"
#: workflows/models.py:141 workflows/models.py:456 workflows/views.py:202 #: workflows/models.py:153 workflows/models.py:468 workflows/views.py:202
msgid "Abgeschlossen" msgid "Abgeschlossen"
msgstr "Completed" msgstr "Completed"
#: workflows/models.py:142 workflows/models.py:396 #: workflows/models.py:154 workflows/models.py:408
#: workflows/templates/workflows/backup_recovery.html:70 #: workflows/templates/workflows/backup_recovery.html:70
#: workflows/templates/workflows/requests_dashboard.html:222 #: workflows/templates/workflows/requests_dashboard.html:222
#: workflows/templates/workflows/welcome_emails.html:108 workflows/views.py:203 #: workflows/templates/workflows/welcome_emails.html:108 workflows/views.py:203
msgid "Fehlgeschlagen" msgid "Fehlgeschlagen"
msgstr "Failed" msgstr "Failed"
#: workflows/models.py:149 #: workflows/models.py:161
msgid "Herr" msgid "Herr"
msgstr "" msgstr ""
#: workflows/models.py:149 #: workflows/models.py:161
msgid "Frau" msgid "Frau"
msgstr "" msgstr ""
#: workflows/models.py:149 #: workflows/models.py:161
msgid "Divers" msgid "Divers"
msgstr "" msgstr ""
#: workflows/models.py:159 #: workflows/models.py:171
msgid "befristet" msgid "befristet"
msgstr "" msgstr ""
#: workflows/models.py:159 #: workflows/models.py:171
msgid "unbefristet" msgid "unbefristet"
msgstr "" msgstr ""
#: workflows/models.py:222 #: workflows/models.py:234
#: workflows/templates/workflows/onboarding_intro_session.html:28 #: workflows/templates/workflows/onboarding_intro_session.html:28
#: workflows/templates/workflows/requests_dashboard.html:145 #: workflows/templates/workflows/requests_dashboard.html:145
msgid "Abteilung" msgid "Abteilung"
msgstr "Department" msgstr "Department"
#: workflows/models.py:223 #: workflows/models.py:235
msgid "Geräte" msgid "Geräte"
msgstr "" msgstr ""
#: workflows/models.py:224 #: workflows/models.py:236
msgid "Software" msgid "Software"
msgstr "" msgstr ""
#: workflows/models.py:225 #: workflows/models.py:237
#, fuzzy #, fuzzy
#| msgid "Vorgänge" #| msgid "Vorgänge"
msgid "Zugänge" msgid "Zugänge"
msgstr "Requests" msgstr "Requests"
#: workflows/models.py:226 #: workflows/models.py:238
msgid "Workspace-Gruppen" msgid "Workspace-Gruppen"
msgstr "" msgstr ""
#: workflows/models.py:227 #: workflows/models.py:239
msgid "Ressourcen" msgid "Ressourcen"
msgstr "" msgstr ""
#: workflows/models.py:228 #: workflows/models.py:240
msgid "Telefonnummern" msgid "Telefonnummern"
msgstr "" msgstr ""
#: workflows/models.py:254 #: workflows/models.py:266
msgid "Automatisch" msgid "Automatisch"
msgstr "" msgstr ""
#: workflows/models.py:255 workflows/views.py:95 #: workflows/models.py:267 workflows/views.py:95
msgid "Stammdaten" msgid "Stammdaten"
msgstr "Master data" msgstr "Master data"
#: workflows/models.py:256 workflows/views.py:96 #: workflows/models.py:268 workflows/views.py:96
msgid "Vertrag" msgid "Vertrag"
msgstr "Contract" msgstr "Contract"
#: workflows/models.py:257 workflows/views.py:97 #: workflows/models.py:269 workflows/views.py:97
msgid "IT-Setup" msgid "IT-Setup"
msgstr "IT setup" msgstr "IT setup"
#: workflows/models.py:258 workflows/views.py:98 #: workflows/models.py:270 workflows/views.py:98
msgid "Abschluss" msgid "Abschluss"
msgstr "Finish" msgstr "Finish"
#: workflows/models.py:300 #: workflows/models.py:312
#, fuzzy #, fuzzy
#| msgid "Onboarding" #| msgid "Onboarding"
msgid "Onboarding: IT" msgid "Onboarding: IT"
msgstr "Onboarding" msgstr "Onboarding"
#: workflows/models.py:301 #: workflows/models.py:313
#, fuzzy #, fuzzy
#| msgid "Offboarding-Anfrage speichern" #| msgid "Offboarding-Anfrage speichern"
msgid "Onboarding: Allgemeine Info" msgid "Onboarding: Allgemeine Info"
msgstr "Save offboarding request" msgstr "Save offboarding request"
#: workflows/models.py:302 #: workflows/models.py:314
#, fuzzy #, fuzzy
#| msgid "Onboarding starten" #| msgid "Onboarding starten"
msgid "Onboarding: Visitenkarte" msgid "Onboarding: Visitenkarte"
msgstr "Start onboarding" msgstr "Start onboarding"
#: workflows/models.py:303 #: workflows/models.py:315
#, fuzzy #, fuzzy
#| msgid "Onboarding" #| msgid "Onboarding"
msgid "Onboarding: HR Works" msgid "Onboarding: HR Works"
msgstr "Onboarding" msgstr "Onboarding"
#: workflows/models.py:304 #: workflows/models.py:316
#, fuzzy #, fuzzy
#| msgid "Onboarding starten" #| msgid "Onboarding starten"
msgid "Onboarding: Schlüssel" msgid "Onboarding: Schlüssel"
msgstr "Start onboarding" msgstr "Start onboarding"
#: workflows/models.py:305 #: workflows/models.py:317
msgid "Onboarding: Referenz Anfordernde Person" msgid "Onboarding: Referenz Anfordernde Person"
msgstr "" msgstr ""
#: workflows/models.py:306 #: workflows/models.py:318
#, fuzzy #, fuzzy
#| msgid "Welcome E-Mails" #| msgid "Welcome E-Mails"
msgid "Onboarding: Welcome E-Mail" msgid "Onboarding: Welcome E-Mail"
msgstr "Welcome Emails" msgstr "Welcome Emails"
#: workflows/models.py:307 #: workflows/models.py:319
#, fuzzy #, fuzzy
#| msgid "Offboarding" #| msgid "Offboarding"
msgid "Offboarding: IT" msgid "Offboarding: IT"
msgstr "Offboarding" msgstr "Offboarding"
#: workflows/models.py:308 #: workflows/models.py:320
#, fuzzy #, fuzzy
#| msgid "Offboarding-Anfrage speichern" #| msgid "Offboarding-Anfrage speichern"
msgid "Offboarding: Allgemeine Info" msgid "Offboarding: Allgemeine Info"
msgstr "Save offboarding request" msgstr "Save offboarding request"
#: workflows/models.py:309 #: workflows/models.py:321
#, fuzzy #, fuzzy
#| msgid "Offboarding starten" #| msgid "Offboarding starten"
msgid "Offboarding: HR Works Deaktivierung" msgid "Offboarding: HR Works Deaktivierung"
msgstr "Start offboarding" msgstr "Start offboarding"
#: workflows/models.py:310 #: workflows/models.py:322
msgid "Offboarding: Referenz Anfordernde Person" msgid "Offboarding: Referenz Anfordernde Person"
msgstr "" msgstr ""
#: workflows/models.py:346 #: workflows/models.py:358
msgid "Immer" msgid "Immer"
msgstr "" msgstr ""
#: workflows/models.py:347 workflows/models.py:425 #: workflows/models.py:359 workflows/models.py:437
msgid "Enthält" msgid "Enthält"
msgstr "" msgstr ""
#: workflows/models.py:348 workflows/models.py:426 #: workflows/models.py:360 workflows/models.py:438
msgid "Ist gleich" msgid "Ist gleich"
msgstr "" msgstr ""
#: workflows/models.py:349 #: workflows/models.py:361
msgid "Ist aktiv/Ja" msgid "Ist aktiv/Ja"
msgstr "" msgstr ""
#: workflows/models.py:350 #: workflows/models.py:362
#, fuzzy #, fuzzy
#| msgid "inaktiv" #| msgid "inaktiv"
msgid "Ist inaktiv/Nein" msgid "Ist inaktiv/Nein"
msgstr "inactive" msgstr "inactive"
#: workflows/models.py:392 #: workflows/models.py:404
#: workflows/templates/workflows/welcome_emails.html:100 #: workflows/templates/workflows/welcome_emails.html:100
msgid "Geplant" msgid "Geplant"
msgstr "Scheduled" msgstr "Scheduled"
#: workflows/models.py:393 #: workflows/models.py:405
#: workflows/templates/workflows/welcome_emails.html:102 #: workflows/templates/workflows/welcome_emails.html:102
msgid "Pausiert" msgid "Pausiert"
msgstr "Paused" msgstr "Paused"
#: workflows/models.py:394 #: workflows/models.py:406
#: workflows/templates/workflows/welcome_emails.html:104 #: workflows/templates/workflows/welcome_emails.html:104
msgid "Abgebrochen" msgid "Abgebrochen"
msgstr "Cancelled" msgstr "Cancelled"
#: workflows/models.py:395 #: workflows/models.py:407
#: workflows/templates/workflows/welcome_emails.html:106 #: workflows/templates/workflows/welcome_emails.html:106
msgid "Gesendet" msgid "Gesendet"
msgstr "Sent" msgstr "Sent"
#: workflows/models.py:418 workflows/tasks.py:576 #: workflows/models.py:430 workflows/tasks.py:576
msgid "Geräte und Arbeitsplatz" msgid "Geräte und Arbeitsplatz"
msgstr "Devices and workplace" msgstr "Devices and workplace"
#: workflows/models.py:419 workflows/tasks.py:577 #: workflows/models.py:431 workflows/tasks.py:577
msgid "Konten und Berechtigungen" msgid "Konten und Berechtigungen"
msgstr "Accounts and permissions" msgstr "Accounts and permissions"
#: workflows/models.py:420 workflows/tasks.py:578 #: workflows/models.py:432 workflows/tasks.py:578
msgid "Software und Tools" msgid "Software und Tools"
msgstr "Software and tools" msgstr "Software and tools"
#: workflows/models.py:421 workflows/tasks.py:579 #: workflows/models.py:433 workflows/tasks.py:579
msgid "Prozesse und Hinweise" msgid "Prozesse und Hinweise"
msgstr "Processes and notes" msgstr "Processes and notes"
#: workflows/models.py:424 #: workflows/models.py:436
msgid "Immer anzeigen" msgid "Immer anzeigen"
msgstr "Always show" msgstr "Always show"
#: workflows/models.py:427 #: workflows/models.py:439
msgid "Ist Ja / aktiv" msgid "Ist Ja / aktiv"
msgstr "Is yes / active" msgstr "Is yes / active"
#: workflows/models.py:428 #: workflows/models.py:440
msgid "Ist Nein / inaktiv" msgid "Ist Nein / inaktiv"
msgstr "Is no / inactive" msgstr "Is no / inactive"
#: workflows/models.py:455 #: workflows/models.py:467
msgid "Entwurf" msgid "Entwurf"
msgstr "Draft" msgstr "Draft"
#: workflows/models.py:475 #: workflows/models.py:487
#, fuzzy #, fuzzy
#| msgid "Nextcloud:" #| msgid "Nextcloud:"
msgid "Nextcloud" msgid "Nextcloud"
msgstr "Nextcloud:" msgstr "Nextcloud:"
#: workflows/models.py:476 #: workflows/models.py:488
msgid "S3" msgid "S3"
msgstr "" msgstr ""
#: workflows/models.py:477 #: workflows/models.py:489
msgid "NFS" msgid "NFS"
msgstr "" msgstr ""
@@ -754,7 +786,6 @@ msgid "Anmeldung"
msgstr "Sign in" msgstr "Sign in"
#: workflows/templates/registration/login.html:20 #: workflows/templates/registration/login.html:20
#: workflows/templates/workflows/auth/login.html:18
msgid "Bitte melden Sie sich mit Ihrem Benutzerkonto an." msgid "Bitte melden Sie sich mit Ihrem Benutzerkonto an."
msgstr "Please sign in with your user account." msgstr "Please sign in with your user account."
@@ -1173,28 +1204,28 @@ msgstr "Delete"
msgid "Noch keine Backup-Bundles vorhanden." msgid "Noch keine Backup-Bundles vorhanden."
msgstr "No backup bundles available yet." msgstr "No backup bundles available yet."
#: workflows/templates/workflows/base_shell.html:24 #: workflows/templates/workflows/base_shell.html:31
msgid "Bitte bestätigen" msgid "Bitte bestätigen"
msgstr "" msgstr ""
#: workflows/templates/workflows/base_shell.html:28 #: workflows/templates/workflows/base_shell.html:35
#: workflows/templates/workflows/welcome_emails.html:134 #: workflows/templates/workflows/welcome_emails.html:134
msgid "Abbrechen" msgid "Abbrechen"
msgstr "Cancel" msgstr "Cancel"
#: workflows/templates/workflows/base_shell.html:29 #: workflows/templates/workflows/base_shell.html:36
msgid "Bestätigen" msgid "Bestätigen"
msgstr "" msgstr ""
#: workflows/templates/workflows/base_shell.html:36 #: workflows/templates/workflows/base_shell.html:43
msgid "Bitte warten" msgid "Bitte warten"
msgstr "Please wait" msgstr "Please wait"
#: workflows/templates/workflows/base_shell.html:37 #: workflows/templates/workflows/base_shell.html:44
msgid "Aktion läuft" msgid "Aktion läuft"
msgstr "Action in progress" msgstr "Action in progress"
#: workflows/templates/workflows/base_shell.html:38 #: workflows/templates/workflows/base_shell.html:45
msgid "Die Aktion wird im aktuellen Tab ausgeführt." msgid "Die Aktion wird im aktuellen Tab ausgeführt."
msgstr "The action is running in the current tab." msgstr "The action is running in the current tab."
@@ -1209,28 +1240,43 @@ msgid ""
"B. tub.co." "B. tub.co."
msgstr "" msgstr ""
#: workflows/templates/workflows/branding_settings.html:53 #: workflows/templates/workflows/branding_settings.html:41
msgid "Wird für ausgehende System-E-Mails als Anzeigename verwendet."
msgstr ""
#: workflows/templates/workflows/branding_settings.html:78
msgid "Erlaubte Formate: SVG, PNG, JPG, JPEG, WEBP. Maximal 5 MB." msgid "Erlaubte Formate: SVG, PNG, JPG, JPEG, WEBP. Maximal 5 MB."
msgstr "" msgstr ""
#: workflows/templates/workflows/branding_settings.html:56 #: workflows/templates/workflows/branding_settings.html:81
msgid "Aktuelles Logo:" msgid "Aktuelles Logo:"
msgstr "Current logo:" msgstr "Current logo:"
#: workflows/templates/workflows/branding_settings.html:56 #: workflows/templates/workflows/branding_settings.html:81
#: workflows/templates/workflows/branding_settings.html:65 #: workflows/templates/workflows/branding_settings.html:90
#: workflows/templates/workflows/branding_settings.html:99
msgid "öffnen" msgid "öffnen"
msgstr "open" msgstr "open"
#: workflows/templates/workflows/branding_settings.html:62 #: workflows/templates/workflows/branding_settings.html:87
msgid "Erlaubtes Format: PDF. Maximal 10 MB." msgid "Erlaubtes Format: PDF. Maximal 10 MB."
msgstr "" msgstr ""
#: workflows/templates/workflows/branding_settings.html:65 #: workflows/templates/workflows/branding_settings.html:90
msgid "Aktueller Briefkopf:" msgid "Aktueller Briefkopf:"
msgstr "Current letterhead:" msgstr "Current letterhead:"
#: workflows/templates/workflows/branding_settings.html:70 #: workflows/templates/workflows/branding_settings.html:96
msgid "Erlaubte Formate: ICO, PNG, SVG, WEBP. Maximal 2 MB."
msgstr ""
#: workflows/templates/workflows/branding_settings.html:99
#, fuzzy
#| msgid "Aktuelles Logo:"
msgid "Aktuelles Favicon:"
msgstr "Current logo:"
#: workflows/templates/workflows/branding_settings.html:104
msgid "" msgid ""
"TUBCO bleibt als Standard erhalten, bis hier Werte geändert oder Dateien " "TUBCO bleibt als Standard erhalten, bis hier Werte geändert oder Dateien "
"hochgeladen werden." "hochgeladen werden."
@@ -1238,7 +1284,7 @@ msgstr ""
"TUBCO remains the default until values are changed or files are uploaded " "TUBCO remains the default until values are changed or files are uploaded "
"here." "here."
#: workflows/templates/workflows/branding_settings.html:71 #: workflows/templates/workflows/branding_settings.html:105
msgid "Branding speichern" msgid "Branding speichern"
msgstr "Save branding" msgstr "Save branding"

View File

@@ -22,7 +22,7 @@ class AdminAuditLogAdmin(admin.ModelAdmin):
@admin.register(PortalBranding) @admin.register(PortalBranding)
class PortalBrandingAdmin(admin.ModelAdmin): class PortalBrandingAdmin(admin.ModelAdmin):
list_display = ('name', 'portal_title', 'company_name', 'support_email', 'default_language', 'updated_at') list_display = ('name', 'portal_title', 'company_name', 'company_domain', 'support_email', 'default_language', 'updated_at')
@admin.register(PortalAppConfig) @admin.register(PortalAppConfig)

View File

@@ -1,9 +1,11 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path from pathlib import Path
from email.utils import formataddr
from django.conf import settings from django.conf import settings
from django.templatetags.static import static from django.templatetags.static import static
from django.utils.translation import get_language
from .models import PortalBranding from .models import PortalBranding
@@ -16,6 +18,12 @@ def get_portal_branding() -> PortalBranding:
'company_name': 'TUBCO', 'company_name': 'TUBCO',
'company_domain': 'tub.co', 'company_domain': 'tub.co',
'support_email': 'info@tub.co', 'support_email': 'info@tub.co',
'sender_display_name': 'TUBCO',
'login_subtitle': 'Bitte melden Sie sich mit Ihrem Benutzerkonto an.',
'footer_text': 'TUBCO Onboarding & Offboarding Portal',
'footer_text_en': 'TUBCO Onboarding & Offboarding Portal',
'legal_notice': '',
'legal_notice_en': '',
'default_language': 'de', 'default_language': 'de',
'primary_color': '#000078', 'primary_color': '#000078',
'secondary_color': '#c0002b', 'secondary_color': '#c0002b',
@@ -40,6 +48,16 @@ def get_portal_logo_url() -> str:
return static('workflows/img/tubco-logo.svg') return static('workflows/img/tubco-logo.svg')
def get_portal_favicon_url() -> str:
branding = get_portal_branding()
if branding.favicon_image:
try:
return branding.favicon_image.url
except ValueError:
pass
return static('workflows/img/tubco-logo.svg')
def get_portal_letterhead_path() -> Path: def get_portal_letterhead_path() -> Path:
branding = get_portal_branding() branding = get_portal_branding()
if branding.pdf_letterhead: if branding.pdf_letterhead:
@@ -54,18 +72,31 @@ def get_portal_letterhead_path() -> Path:
def get_branding_context() -> dict[str, object]: def get_branding_context() -> dict[str, object]:
branding = get_portal_branding() branding = get_portal_branding()
lang = (get_language() or branding.default_language or 'de').split('-')[0]
footer_text = (branding.footer_text_en or '').strip() if lang == 'en' else ''
legal_notice = (branding.legal_notice_en or '').strip() if lang == 'en' else ''
if not footer_text:
footer_text = (branding.footer_text or branding.portal_title).strip()
if not legal_notice:
legal_notice = (branding.legal_notice or '').strip()
return { return {
'portal_branding': branding, 'portal_branding': branding,
'portal_title': branding.portal_title, 'portal_title': branding.portal_title,
'portal_company_name': branding.company_name, 'portal_company_name': branding.company_name,
'portal_email_domain': get_company_email_domain(), 'portal_email_domain': get_company_email_domain(),
'portal_support_email': branding.support_email, 'portal_support_email': branding.support_email,
'portal_sender_display_name': branding.sender_display_name or branding.company_name,
'portal_login_subtitle': branding.login_subtitle,
'portal_footer_text': footer_text,
'portal_legal_notice': legal_notice,
'portal_default_language': branding.default_language, 'portal_default_language': branding.default_language,
'portal_primary_color': branding.primary_color, 'portal_primary_color': branding.primary_color,
'portal_secondary_color': branding.secondary_color, 'portal_secondary_color': branding.secondary_color,
'portal_logo_url': get_portal_logo_url(), 'portal_logo_url': get_portal_logo_url(),
'portal_favicon_url': get_portal_favicon_url(),
'portal_has_custom_logo': bool(branding.logo_image), 'portal_has_custom_logo': bool(branding.logo_image),
'portal_has_custom_letterhead': bool(branding.pdf_letterhead), 'portal_has_custom_letterhead': bool(branding.pdf_letterhead),
'portal_has_custom_favicon': bool(branding.favicon_image),
} }
@@ -78,9 +109,20 @@ def get_branding_email_copy() -> dict[str, str]:
'company_domain': get_company_email_domain(), 'company_domain': get_company_email_domain(),
'portal_title': portal_title, 'portal_title': portal_title,
'support_email': (branding.support_email or '').strip(), 'support_email': (branding.support_email or '').strip(),
'sender_display_name': (branding.sender_display_name or company_name).strip(),
} }
def get_branded_from_email(email_address: str | None) -> str | None:
address = (email_address or '').strip()
if not address:
return None
display_name = (get_branding_email_copy()['sender_display_name'] or '').strip()
if not display_name:
return address
return formataddr((display_name, address))
def get_default_notification_templates() -> dict[str, dict[str, str]]: def get_default_notification_templates() -> dict[str, dict[str, str]]:
from copy import deepcopy from copy import deepcopy

View File

@@ -1,6 +1,7 @@
from django.conf import settings from django.conf import settings
from django.core.mail import EmailMessage, get_connection from django.core.mail import EmailMessage, get_connection
from .branding import get_branded_from_email
from .models import SystemEmailConfig, WorkflowConfig from .models import SystemEmailConfig, WorkflowConfig
@@ -66,7 +67,7 @@ def send_system_email(
msg = EmailMessage( msg = EmailMessage(
subject=subject, subject=subject,
body=body, body=body,
from_email=(from_email or smtp['from_email']), from_email=get_branded_from_email(from_email or smtp['from_email']) or (from_email or smtp['from_email']),
to=to, to=to,
connection=connection, connection=connection,
) )

View File

@@ -178,9 +178,16 @@ class PortalBrandingForm(forms.ModelForm):
'company_name', 'company_name',
'company_domain', 'company_domain',
'support_email', 'support_email',
'sender_display_name',
'login_subtitle',
'footer_text',
'footer_text_en',
'legal_notice',
'legal_notice_en',
'default_language', 'default_language',
'logo_image', 'logo_image',
'pdf_letterhead', 'pdf_letterhead',
'favicon_image',
'primary_color', 'primary_color',
'secondary_color', 'secondary_color',
] ]
@@ -189,9 +196,16 @@ class PortalBrandingForm(forms.ModelForm):
'company_name': gettext_lazy('Firmenname'), 'company_name': gettext_lazy('Firmenname'),
'company_domain': gettext_lazy('Firmen-Domain'), 'company_domain': gettext_lazy('Firmen-Domain'),
'support_email': gettext_lazy('Support-E-Mail'), 'support_email': gettext_lazy('Support-E-Mail'),
'sender_display_name': gettext_lazy('Absender-Anzeigename'),
'login_subtitle': gettext_lazy('Login-Untertitel'),
'footer_text': gettext_lazy('Footer-Text DE'),
'footer_text_en': gettext_lazy('Footer-Text EN'),
'legal_notice': gettext_lazy('Rechtlicher Hinweis DE'),
'legal_notice_en': gettext_lazy('Rechtlicher Hinweis EN'),
'default_language': gettext_lazy('Standardsprache'), 'default_language': gettext_lazy('Standardsprache'),
'logo_image': gettext_lazy('Logo'), 'logo_image': gettext_lazy('Logo'),
'pdf_letterhead': gettext_lazy('PDF-Briefkopf'), 'pdf_letterhead': gettext_lazy('PDF-Briefkopf'),
'favicon_image': gettext_lazy('Favicon'),
'primary_color': gettext_lazy('Primärfarbe'), 'primary_color': gettext_lazy('Primärfarbe'),
'secondary_color': gettext_lazy('Sekundärfarbe'), 'secondary_color': gettext_lazy('Sekundärfarbe'),
} }
@@ -200,6 +214,9 @@ class PortalBrandingForm(forms.ModelForm):
'secondary_color': forms.TextInput(attrs={'type': 'color'}), 'secondary_color': forms.TextInput(attrs={'type': 'color'}),
'logo_image': forms.ClearableFileInput(attrs={'accept': '.svg,.png,.jpg,.jpeg,.webp'}), 'logo_image': forms.ClearableFileInput(attrs={'accept': '.svg,.png,.jpg,.jpeg,.webp'}),
'pdf_letterhead': forms.ClearableFileInput(attrs={'accept': '.pdf'}), 'pdf_letterhead': forms.ClearableFileInput(attrs={'accept': '.pdf'}),
'favicon_image': forms.ClearableFileInput(attrs={'accept': '.ico,.png,.svg,.webp'}),
'legal_notice': forms.Textarea(attrs={'rows': 3}),
'legal_notice_en': forms.Textarea(attrs={'rows': 3}),
} }
def clean_logo_image(self): def clean_logo_image(self):
@@ -218,6 +235,14 @@ class PortalBrandingForm(forms.ModelForm):
raise forms.ValidationError(_('Der PDF-Briefkopf darf maximal 10 MB groß sein.')) raise forms.ValidationError(_('Der PDF-Briefkopf darf maximal 10 MB groß sein.'))
return letterhead return letterhead
def clean_favicon_image(self):
favicon = self.cleaned_data.get('favicon_image')
if not favicon:
return favicon
if getattr(favicon, 'size', 0) > 2 * 1024 * 1024:
raise forms.ValidationError(_('Das Favicon darf maximal 2 MB groß sein.'))
return favicon
class OnboardingRequestForm(forms.ModelForm): class OnboardingRequestForm(forms.ModelForm):
first_name = forms.CharField(label='Vorname', required=False) first_name = forms.CharField(label='Vorname', required=False)

View File

@@ -0,0 +1,49 @@
# Generated by Django 5.1.5 on 2026-03-26 11:02
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('workflows', '0039_portalbranding_company_domain'),
]
operations = [
migrations.AddField(
model_name='portalbranding',
name='favicon_image',
field=models.FileField(blank=True, null=True, upload_to='branding/', validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['ico', 'png', 'svg', 'webp'])]),
),
migrations.AddField(
model_name='portalbranding',
name='footer_text',
field=models.CharField(blank=True, default='TUBCO Onboarding & Offboarding Portal', max_length=255),
),
migrations.AddField(
model_name='portalbranding',
name='footer_text_en',
field=models.CharField(blank=True, default='TUBCO Onboarding & Offboarding Portal', max_length=255),
),
migrations.AddField(
model_name='portalbranding',
name='legal_notice',
field=models.TextField(blank=True, default=''),
),
migrations.AddField(
model_name='portalbranding',
name='legal_notice_en',
field=models.TextField(blank=True, default=''),
),
migrations.AddField(
model_name='portalbranding',
name='login_subtitle',
field=models.CharField(blank=True, default='Bitte melden Sie sich mit Ihrem Benutzerkonto an.', max_length=255),
),
migrations.AddField(
model_name='portalbranding',
name='sender_display_name',
field=models.CharField(blank=True, default='TUBCO', max_length=255),
),
]

View File

@@ -31,6 +31,12 @@ class PortalBranding(models.Model):
company_name = models.CharField(max_length=255, default='TUBCO') company_name = models.CharField(max_length=255, default='TUBCO')
company_domain = models.CharField(max_length=120, blank=True, default='tub.co') company_domain = models.CharField(max_length=120, blank=True, default='tub.co')
support_email = models.EmailField(blank=True, default='info@tub.co') support_email = models.EmailField(blank=True, default='info@tub.co')
sender_display_name = models.CharField(max_length=255, blank=True, default='TUBCO')
login_subtitle = models.CharField(max_length=255, blank=True, default='Bitte melden Sie sich mit Ihrem Benutzerkonto an.')
footer_text = models.CharField(max_length=255, blank=True, default='TUBCO Onboarding & Offboarding Portal')
footer_text_en = models.CharField(max_length=255, blank=True, default='TUBCO Onboarding & Offboarding Portal')
legal_notice = models.TextField(blank=True, default='')
legal_notice_en = models.TextField(blank=True, default='')
default_language = models.CharField( default_language = models.CharField(
max_length=10, max_length=10,
choices=[('de', 'Deutsch'), ('en', 'English')], choices=[('de', 'Deutsch'), ('en', 'English')],
@@ -48,6 +54,12 @@ class PortalBranding(models.Model):
null=True, null=True,
validators=[FileExtensionValidator(allowed_extensions=['pdf'])], validators=[FileExtensionValidator(allowed_extensions=['pdf'])],
) )
favicon_image = models.FileField(
upload_to='branding/',
blank=True,
null=True,
validators=[FileExtensionValidator(allowed_extensions=['ico', 'png', 'svg', 'webp'])],
)
primary_color = models.CharField(max_length=20, blank=True, default='#000078') primary_color = models.CharField(max_length=20, blank=True, default='#000078')
secondary_color = models.CharField(max_length=20, blank=True, default='#c0002b') secondary_color = models.CharField(max_length=20, blank=True, default='#c0002b')
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)

View File

@@ -7,6 +7,30 @@ h1 { margin: 12px 0 6px; color: #000078; }
.app-messages { margin-bottom: 12px; } .app-messages { margin-bottom: 12px; }
.card { border: 1px solid #d8e3f0; border-radius: 12px; background: #fbfdff; padding: 12px; margin-bottom: 14px; transition: border-color 180ms cubic-bezier(0.2, 0.8, 0.2, 1), box-shadow 220ms cubic-bezier(0.2, 0.8, 0.2, 1), transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1); } .card { border: 1px solid #d8e3f0; border-radius: 12px; background: #fbfdff; padding: 12px; margin-bottom: 14px; transition: border-color 180ms cubic-bezier(0.2, 0.8, 0.2, 1), box-shadow 220ms cubic-bezier(0.2, 0.8, 0.2, 1), transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1); }
.grid { display: grid; grid-template-columns: repeat(2, minmax(240px, 1fr)); gap: 10px; } .grid { display: grid; grid-template-columns: repeat(2, minmax(240px, 1fr)); gap: 10px; }
.branding-sections { display: grid; gap: 14px; }
.branding-block { border: 1px solid #dce5f1; border-radius: 16px; background: linear-gradient(180deg, rgba(255,255,255,0.98), rgba(246,250,255,0.94)); padding: 14px; box-shadow: inset 0 1px 0 rgba(255,255,255,0.92); }
.branding-block-head { margin-bottom: 12px; }
.branding-block-head h2 { margin: 0; color: #17345e; font-size: 18px; }
.branding-block-head p { margin: 4px 0 0; color: #60738d; font-size: 13px; }
.lang-pairs { align-items: start; }
.lang-block { border: 1px solid #d9e4f1; border-radius: 14px; background: rgba(255,255,255,0.82); padding: 12px; }
.lang-block h3 { margin: 0 0 10px; color: #223b63; font-size: 15px; }
.branding-preview { max-width: 460px; margin-left: auto; border: 1px solid #dce5f1; border-radius: 18px; background:
radial-gradient(circle at top right, rgba(59,112,234,0.10), transparent 30%),
linear-gradient(180deg, #f9fbff, #eef4ff);
padding: 10px; }
.branding-preview-shell { border: 1px solid rgba(210, 221, 236, 0.95); border-radius: 18px; overflow: hidden; background: linear-gradient(180deg, rgba(255,255,255,0.99), rgba(247,250,255,0.96)); box-shadow: 0 8px 22px rgba(16, 32, 57, 0.05); }
.branding-preview-header { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-bottom: 1px solid rgba(217, 227, 238, 0.9); }
.branding-preview-logo { width: 64px; max-width: 100%; height: auto; display: block; object-fit: contain; filter: saturate(1.02); }
.branding-preview-copy { display: grid; gap: 2px; min-width: 0; }
.branding-preview-copy strong { color: #18335b; font-size: 13px; line-height: 1.2; }
.branding-preview-copy span { color: #61738d; font-size: 12px; line-height: 1.3; }
.branding-preview-band { display: flex; gap: 8px; padding: 10px 12px; }
.branding-preview-chip { display: inline-flex; align-items: center; justify-content: center; min-width: 104px; padding: 5px 10px; border-radius: 999px; color: #fff; font-size: 10px; font-weight: 800; letter-spacing: 0.04em; text-transform: uppercase; background: #000078; box-shadow: inset 0 1px 0 rgba(255,255,255,0.16); }
.branding-preview-chip-secondary { background: #c0002b; }
.branding-preview-footer { padding: 0 12px 12px; }
.branding-preview-footer-main { color: #20385f; font-size: 11px; font-weight: 700; line-height: 1.35; }
.branding-preview-footer-legal { margin-top: 4px; color: #6c7f99; font-size: 10px; line-height: 1.4; }
.backup-grid { grid-template-columns: minmax(280px, 720px); } .backup-grid { grid-template-columns: minmax(280px, 720px); }
label { display: block; margin-bottom: 4px; font-size: 12px; color: #334155; font-weight: 700; } label { display: block; margin-bottom: 4px; font-size: 12px; color: #334155; font-weight: 700; }
input, select, textarea { width: 100%; box-sizing: border-box; border: 1px solid #cbd5e1; border-radius: 8px; padding: 8px 9px; background: #fff; transition: border-color 180ms cubic-bezier(0.2, 0.8, 0.2, 1), box-shadow 180ms cubic-bezier(0.2, 0.8, 0.2, 1), background-color 180ms cubic-bezier(0.2, 0.8, 0.2, 1); } input, select, textarea { width: 100%; box-sizing: border-box; border: 1px solid #cbd5e1; border-radius: 8px; padding: 8px 9px; background: #fff; transition: border-color 180ms cubic-bezier(0.2, 0.8, 0.2, 1), box-shadow 180ms cubic-bezier(0.2, 0.8, 0.2, 1), background-color 180ms cubic-bezier(0.2, 0.8, 0.2, 1); }
@@ -56,8 +80,13 @@ th { background: #f6f9ff; color: #334155; }
.bulk-note { color: #64748b; font-size: 12px; } .bulk-note { color: #64748b; font-size: 12px; }
.field label { display: block; font-weight: 600; margin-bottom: 6px; } .field label { display: block; font-weight: 600; margin-bottom: 6px; }
.field input, .field select { min-height: 40px; } .field input, .field select { min-height: 40px; }
.field-full { grid-column: 1 / -1; }
.mini { color: #64748b; font-size: 12px; } .mini { color: #64748b; font-size: 12px; }
.table-controls input[type="text"], .table-controls select { width: 100%; min-height: 36px; padding: 7px 9px; border: 1px solid #cfd9e8; border-radius: 8px; box-sizing: border-box; } .table-controls input[type="text"], .table-controls select { width: 100%; min-height: 36px; padding: 7px 9px; border: 1px solid #cfd9e8; border-radius: 8px; box-sizing: border-box; }
.table-controls input[type="checkbox"] { transform: scale(1.1); width: auto; } .table-controls input[type="checkbox"] { transform: scale(1.1); width: auto; }
.actions { white-space: nowrap; } .actions { white-space: nowrap; }
@media (max-width: 760px) { .grid { grid-template-columns: 1fr; } } @media (max-width: 760px) {
.grid { grid-template-columns: 1fr; }
.branding-preview-header { flex-direction: column; align-items: flex-start; }
.branding-preview-band { flex-wrap: wrap; }
}

View File

@@ -122,6 +122,25 @@
margin-right: auto !important; margin-right: auto !important;
} }
.app-site-footer {
width: min(var(--app-shell-width), 100%);
margin: 14px auto 0;
padding: 0 10px 18px;
color: #5f728d;
text-align: center;
}
.app-site-footer-main {
font-size: 13px;
font-weight: 700;
}
.app-site-footer-legal {
margin-top: 4px;
font-size: 12px;
line-height: 1.5;
}
@media (max-width: 900px) { @media (max-width: 900px) {
.app-header, .app-header,
.app-header-in-shell { .app-header-in-shell {

View File

@@ -15,7 +15,7 @@
<section class="login-shell-body"> <section class="login-shell-body">
<div class="login-card"> <div class="login-card">
<h1>{% trans "Anmeldung" %}</h1> <h1>{% trans "Anmeldung" %}</h1>
<p>{% trans "Bitte melden Sie sich mit Ihrem Benutzerkonto an." %}</p> <p>{{ portal_login_subtitle }}</p>
<form method="post" action="/accounts/login/"> <form method="post" action="/accounts/login/">
{% csrf_token %} {% csrf_token %}

View File

@@ -6,6 +6,7 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{% block title %}{% endblock %}</title> <title>{% block title %}{% endblock %}</title>
<link rel="icon" href="{{ portal_favicon_url }}" />
<link rel="stylesheet" href="{% static 'workflows/css/buttons.css' %}" /> <link rel="stylesheet" href="{% static 'workflows/css/buttons.css' %}" />
<link rel="stylesheet" href="{% static 'workflows/css/app_chrome.css' %}" /> <link rel="stylesheet" href="{% static 'workflows/css/app_chrome.css' %}" />
{% block extra_css %}{% endblock %} {% block extra_css %}{% endblock %}
@@ -17,6 +18,12 @@
{% block shell_header %}{% endblock %} {% block shell_header %}{% endblock %}
{% block shell_body %}{% endblock %} {% block shell_body %}{% endblock %}
</div> </div>
{% if portal_footer_text or portal_legal_notice %}
<div class="app-site-footer">
{% if portal_footer_text %}<div class="app-site-footer-main">{{ portal_footer_text }}</div>{% endif %}
{% if portal_legal_notice %}<div class="app-site-footer-legal">{{ portal_legal_notice }}</div>{% endif %}
</div>
{% endif %}
<div class="confirm-modal" id="app-confirm-modal" hidden aria-hidden="true"> <div class="confirm-modal" id="app-confirm-modal" hidden aria-hidden="true">
<div class="confirm-backdrop" data-confirm-close="1"></div> <div class="confirm-backdrop" data-confirm-close="1"></div>
<div class="confirm-dialog" role="dialog" aria-modal="true" aria-labelledby="app-confirm-title" aria-describedby="app-confirm-message"> <div class="confirm-dialog" role="dialog" aria-modal="true" aria-labelledby="app-confirm-title" aria-describedby="app-confirm-message">

View File

@@ -17,54 +17,150 @@
<section class="card"> <section class="card">
<form method="post" action="{% url 'save_portal_branding' %}" enctype="multipart/form-data" class="stack-form"> <form method="post" action="{% url 'save_portal_branding' %}" enctype="multipart/form-data" class="stack-form">
{% csrf_token %} {% csrf_token %}
<div class="grid two"> <div class="branding-sections">
<div class="field"> <section class="branding-block">
<label for="{{ form.portal_title.id_for_label }}">{{ form.portal_title.label }}</label> <div class="branding-block-head">
{{ form.portal_title }} <h2>{% trans "Identität" %}</h2>
</div> <p>{% trans "Titel, Firmenname und zentrale Spracheinstellungen." %}</p>
<div class="field"> </div>
<label for="{{ form.company_name.id_for_label }}">{{ form.company_name.label }}</label> <div class="grid two">
{{ form.company_name }} <div class="field">
</div> <label for="{{ form.portal_title.id_for_label }}">{{ form.portal_title.label }}</label>
<div class="field"> {{ form.portal_title }}
<label for="{{ form.company_domain.id_for_label }}">{{ form.company_domain.label }}</label> </div>
{{ form.company_domain }} <div class="field">
<div class="hint">{% trans "Wird für E-Mail-Vorschläge und Domain-bezogene Standardtexte verwendet, z. B. tub.co." %}</div> <label for="{{ form.company_name.id_for_label }}">{{ form.company_name.label }}</label>
</div> {{ form.company_name }}
<div class="field"> </div>
<label for="{{ form.support_email.id_for_label }}">{{ form.support_email.label }}</label> <div class="field">
{{ form.support_email }} <label for="{{ form.company_domain.id_for_label }}">{{ form.company_domain.label }}</label>
</div> {{ form.company_domain }}
<div class="field"> <div class="hint">{% trans "Wird für E-Mail-Vorschläge und Domain-bezogene Standardtexte verwendet, z. B. tub.co." %}</div>
<label for="{{ form.default_language.id_for_label }}">{{ form.default_language.label }}</label> </div>
{{ form.default_language }} <div class="field">
</div> <label for="{{ form.default_language.id_for_label }}">{{ form.default_language.label }}</label>
<div class="field"> {{ form.default_language }}
<label for="{{ form.primary_color.id_for_label }}">{{ form.primary_color.label }}</label> </div>
{{ form.primary_color }} <div class="field field-full">
</div> <label for="{{ form.login_subtitle.id_for_label }}">{{ form.login_subtitle.label }}</label>
<div class="field"> {{ form.login_subtitle }}
<label for="{{ form.secondary_color.id_for_label }}">{{ form.secondary_color.label }}</label> </div>
{{ form.secondary_color }} </div>
</div> </section>
<div class="field">
<label for="{{ form.logo_image.id_for_label }}">{{ form.logo_image.label }}</label> <section class="branding-block">
{{ form.logo_image }} <div class="branding-block-head">
<div class="hint">{% trans "Erlaubte Formate: SVG, PNG, JPG, JPEG, WEBP. Maximal 5 MB." %}</div> <h2>{% trans "Farben & Erscheinungsbild" %}</h2>
{% for error in form.logo_image.errors %}<div class="hint">{{ error }}</div>{% endfor %} <p>{% trans "Zentrale visuelle Markenwerte und Browser-Icon." %}</p>
{% if branding.logo_image %} </div>
<div class="hint">{% trans "Aktuelles Logo:" %} <a href="{{ branding.logo_image.url }}" target="_blank" rel="noopener">{% trans "öffnen" %}</a></div> <div class="grid two">
{% endif %} <div class="field">
</div> <label for="{{ form.primary_color.id_for_label }}">{{ form.primary_color.label }}</label>
<div class="field"> {{ form.primary_color }}
<label for="{{ form.pdf_letterhead.id_for_label }}">{{ form.pdf_letterhead.label }}</label> </div>
{{ form.pdf_letterhead }} <div class="field">
<div class="hint">{% trans "Erlaubtes Format: PDF. Maximal 10 MB." %}</div> <label for="{{ form.secondary_color.id_for_label }}">{{ form.secondary_color.label }}</label>
{% for error in form.pdf_letterhead.errors %}<div class="hint">{{ error }}</div>{% endfor %} {{ form.secondary_color }}
{% if branding.pdf_letterhead %} </div>
<div class="hint">{% trans "Aktueller Briefkopf:" %} <a href="{{ branding.pdf_letterhead.url }}" target="_blank" rel="noopener">{% trans "öffnen" %}</a></div> <div class="field">
{% endif %} <label for="{{ form.logo_image.id_for_label }}">{{ form.logo_image.label }}</label>
</div> {{ form.logo_image }}
<div class="hint">{% trans "Erlaubte Formate: SVG, PNG, JPG, JPEG, WEBP. Maximal 5 MB." %}</div>
{% for error in form.logo_image.errors %}<div class="hint">{{ error }}</div>{% endfor %}
{% if branding.logo_image %}
<div class="hint">{% trans "Aktuelles Logo:" %} <a href="{{ branding.logo_image.url }}" target="_blank" rel="noopener">{% trans "öffnen" %}</a></div>
{% endif %}
</div>
<div class="field">
<label for="{{ form.favicon_image.id_for_label }}">{{ form.favicon_image.label }}</label>
{{ form.favicon_image }}
<div class="hint">{% trans "Erlaubte Formate: ICO, PNG, SVG, WEBP. Maximal 2 MB." %}</div>
{% for error in form.favicon_image.errors %}<div class="hint">{{ error }}</div>{% endfor %}
{% if branding.favicon_image %}
<div class="hint">{% trans "Aktuelles Favicon:" %} <a href="{{ branding.favicon_image.url }}" target="_blank" rel="noopener">{% trans "öffnen" %}</a></div>
{% endif %}
</div>
<div class="field field-full">
<div class="branding-preview" id="branding-preview" data-default-logo="{{ portal_logo_url }}">
<div class="branding-preview-shell">
<div class="branding-preview-header">
<img class="branding-preview-logo" id="branding-preview-logo" src="{{ portal_logo_url }}" alt="{{ portal_company_name }} Logo" />
<div class="branding-preview-copy">
<strong id="branding-preview-company">{{ branding.company_name }}</strong>
<span id="branding-preview-title">{{ branding.portal_title }}</span>
</div>
</div>
<div class="branding-preview-band">
<span class="branding-preview-chip" id="branding-preview-primary">{% trans "Primärfarbe" %}</span>
<span class="branding-preview-chip branding-preview-chip-secondary" id="branding-preview-secondary">{% trans "Sekundärfarbe" %}</span>
</div>
<div class="branding-preview-footer">
<div class="branding-preview-footer-main" id="branding-preview-footer">{{ branding.footer_text|default:branding.portal_title }}</div>
<div class="branding-preview-footer-legal" id="branding-preview-legal">{{ branding.legal_notice }}</div>
</div>
</div>
</div>
</div>
</div>
</section>
<section class="branding-block">
<div class="branding-block-head">
<h2>{% trans "Kommunikation" %}</h2>
<p>{% trans "Absender, Support und PDF-Branding für ausgehende Kommunikation." %}</p>
</div>
<div class="grid two">
<div class="field">
<label for="{{ form.support_email.id_for_label }}">{{ form.support_email.label }}</label>
{{ form.support_email }}
</div>
<div class="field">
<label for="{{ form.sender_display_name.id_for_label }}">{{ form.sender_display_name.label }}</label>
{{ form.sender_display_name }}
<div class="hint">{% trans "Wird für ausgehende System-E-Mails als Anzeigename verwendet." %}</div>
</div>
<div class="field field-full">
<label for="{{ form.pdf_letterhead.id_for_label }}">{{ form.pdf_letterhead.label }}</label>
{{ form.pdf_letterhead }}
<div class="hint">{% trans "Erlaubtes Format: PDF. Maximal 10 MB." %}</div>
{% for error in form.pdf_letterhead.errors %}<div class="hint">{{ error }}</div>{% endfor %}
{% if branding.pdf_letterhead %}
<div class="hint">{% trans "Aktueller Briefkopf:" %} <a href="{{ branding.pdf_letterhead.url }}" target="_blank" rel="noopener">{% trans "öffnen" %}</a></div>
{% endif %}
</div>
</div>
</section>
<section class="branding-block">
<div class="branding-block-head">
<h2>{% trans "Footer & Rechtliches" %}</h2>
<p>{% trans "Gemeinsame Footer-Texte und rechtliche Hinweise für die Shell." %}</p>
</div>
<div class="grid two lang-pairs">
<div class="lang-block">
<h3>{% trans "Deutsch" %}</h3>
<div class="field">
<label for="{{ form.footer_text.id_for_label }}">{{ form.footer_text.label }}</label>
{{ form.footer_text }}
</div>
<div class="field">
<label for="{{ form.legal_notice.id_for_label }}">{{ form.legal_notice.label }}</label>
{{ form.legal_notice }}
</div>
</div>
<div class="lang-block">
<h3>{% trans "English" %}</h3>
<div class="field">
<label for="{{ form.footer_text_en.id_for_label }}">{{ form.footer_text_en.label }}</label>
{{ form.footer_text_en }}
</div>
<div class="field">
<label for="{{ form.legal_notice_en.id_for_label }}">{{ form.legal_notice_en.label }}</label>
{{ form.legal_notice_en }}
</div>
</div>
</div>
</section>
</div> </div>
<div class="toolbar" style="margin-top:1.25rem;"> <div class="toolbar" style="margin-top:1.25rem;">
<div class="hint">{% trans "TUBCO bleibt als Standard erhalten, bis hier Werte geändert oder Dateien hochgeladen werden." %}</div> <div class="hint">{% trans "TUBCO bleibt als Standard erhalten, bis hier Werte geändert oder Dateien hochgeladen werden." %}</div>
@@ -73,3 +169,59 @@
</form> </form>
</section> </section>
{% endblock %} {% endblock %}
{% block extra_scripts %}
<script>
(() => {
const byId = (id) => document.getElementById(id);
const title = byId('{{ form.portal_title.id_for_label }}');
const company = byId('{{ form.company_name.id_for_label }}');
const footer = byId('{{ form.footer_text.id_for_label }}');
const legal = byId('{{ form.legal_notice.id_for_label }}');
const primary = byId('{{ form.primary_color.id_for_label }}');
const secondary = byId('{{ form.secondary_color.id_for_label }}');
const logo = byId('{{ form.logo_image.id_for_label }}');
const previewLogo = byId('branding-preview-logo');
const previewTitle = byId('branding-preview-title');
const previewCompany = byId('branding-preview-company');
const previewFooter = byId('branding-preview-footer');
const previewLegal = byId('branding-preview-legal');
const previewPrimary = byId('branding-preview-primary');
const previewSecondary = byId('branding-preview-secondary');
const preview = byId('branding-preview');
if (!preview) return;
const defaultLogo = preview.dataset.defaultLogo || '';
function syncPreview() {
if (previewTitle && title) previewTitle.textContent = title.value || '{{ branding.portal_title|escapejs }}';
if (previewCompany && company) previewCompany.textContent = company.value || '{{ branding.company_name|escapejs }}';
if (previewFooter && footer) previewFooter.textContent = footer.value || '{{ branding.footer_text|default:branding.portal_title|escapejs }}';
if (previewLegal && legal) previewLegal.textContent = legal.value || '{{ branding.legal_notice|escapejs }}';
if (previewPrimary && primary) previewPrimary.style.background = primary.value || '#000078';
if (previewSecondary && secondary) previewSecondary.style.background = secondary.value || '#c0002b';
}
[title, company, footer, legal, primary, secondary].forEach((input) => {
if (input) input.addEventListener('input', syncPreview);
});
if (logo && previewLogo) {
logo.addEventListener('change', () => {
const file = logo.files && logo.files[0];
if (!file) {
previewLogo.src = defaultLogo;
return;
}
const reader = new FileReader();
reader.onload = (event) => {
previewLogo.src = event.target.result;
};
reader.readAsDataURL(file);
});
}
syncPreview();
})();
</script>
{% endblock %}

View File

@@ -176,8 +176,10 @@ docker compose exec -T web django-admin compilemessages</code></pre>
<ul> <ul>
<li>Portal-level branding is stored in the singleton model <code>PortalBranding</code>.</li> <li>Portal-level branding is stored in the singleton model <code>PortalBranding</code>.</li>
<li>Configured from Admin Apps → <code>Branding</code>.</li> <li>Configured from Admin Apps → <code>Branding</code>.</li>
<li>Current scope: portal title, company name, support email, default language, logo, PDF letterhead, and primary/secondary colors.</li> <li>Current scope: portal title, company name, company domain, support email, sender display name, login subtitle, footer/legal text, logo, favicon, PDF letterhead, and primary/secondary colors.</li>
<li>Shared header/logo rendering now uses the branding context processor instead of hardcoded TUBCO asset references.</li> <li>Shared header/logo rendering now uses the branding context processor instead of hardcoded TUBCO asset references.</li>
<li>The company domain now drives onboarding/offboarding email autofill and domain validation, so new customer deployments no longer require <code>@tub.co</code> code changes.</li>
<li>Outgoing system mail sender names are now branded through the same layer.</li>
<li>User invitation emails and welcome-template fallbacks also use the configured branding defaults.</li> <li>User invitation emails and welcome-template fallbacks also use the configured branding defaults.</li>
</ul> </ul>

View File

@@ -178,7 +178,7 @@
<li><strong>Form Builder:</strong> manage field visibility/order/options.</li> <li><strong>Form Builder:</strong> manage field visibility/order/options.</li>
<li><strong>Einweisungs-Builder:</strong> manage custom checklist items for the intro PDF and live introduction checklist, including section, visibility, and conditional display logic.</li> <li><strong>Einweisungs-Builder:</strong> manage custom checklist items for the intro PDF and live introduction checklist, including section, visibility, and conditional display logic.</li>
<li><strong>Integrations:</strong> Nextcloud, SMTP, default routing addresses, notification rules, workflow rules, and remote backup target settings.</li> <li><strong>Integrations:</strong> Nextcloud, SMTP, default routing addresses, notification rules, workflow rules, and remote backup target settings.</li>
<li><strong>Branding:</strong> portal title, company name, logo, support email, default language, PDF letterhead, and basic brand colors.</li> <li><strong>Branding:</strong> portal title, company name, company domain, support email, sender display name, logo, favicon, default language, PDF letterhead, footer/legal text, and basic brand colors.</li>
<li><strong>App Registry:</strong> platform-level registry for enabling, ordering, and relabeling landing-page apps without editing the home template.</li> <li><strong>App Registry:</strong> platform-level registry for enabling, ordering, and relabeling landing-page apps without editing the home template.</li>
<li><strong>Benutzer &amp; Rollen:</strong> super-admin-only page for creating users, assigning roles, activating/deactivating access, sending access or password-reset links by email, and deleting accounts when appropriate.</li> <li><strong>Benutzer &amp; Rollen:</strong> super-admin-only page for creating users, assigning roles, activating/deactivating access, sending access or password-reset links by email, and deleting accounts when appropriate.</li>
<li><strong>Welcome Emails:</strong> scheduled jobs, pause/resume/cancel/trigger now.</li> <li><strong>Welcome Emails:</strong> scheduled jobs, pause/resume/cancel/trigger now.</li>