diff --git a/backend/locale/en/LC_MESSAGES/django.po b/backend/locale/en/LC_MESSAGES/django.po index 8bc10ab..dc566a3 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-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" "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: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/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: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:132 msgid "Offboarding" @@ -213,7 +213,7 @@ msgstr "Django Admin" msgid "Vollständige Datenverwaltung." msgstr "Full data management." -#: workflows/app_registry.py:165 workflows/models.py:68 +#: workflows/app_registry.py:165 workflows/models.py:80 msgid "Apps" msgstr "Apps" @@ -221,7 +221,7 @@ msgstr "Apps" msgid "Wählen Sie den gewünschten Prozess." 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" msgstr "" @@ -231,7 +231,7 @@ msgstr "" msgid "Produktweite Konfiguration und Produktsteuerung." 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" msgstr "Admin Apps" @@ -338,307 +338,339 @@ msgstr "Invalid role." msgid "Nur Platform Owner dürfen diese Rolle vergeben." msgstr "" -#: workflows/forms.py:188 +#: workflows/forms.py:195 msgid "Portal-Titel" msgstr "Portal title" -#: workflows/forms.py:189 +#: workflows/forms.py:196 msgid "Firmenname" msgstr "Company name" -#: workflows/forms.py:190 +#: workflows/forms.py:197 #, fuzzy #| msgid "Firmenname" msgid "Firmen-Domain" msgstr "Company name" -#: workflows/forms.py:191 +#: workflows/forms.py:198 msgid "Support-E-Mail" 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" msgstr "Default language" -#: workflows/forms.py:193 +#: workflows/forms.py:206 msgid "Logo" msgstr "Logo" -#: workflows/forms.py:194 +#: workflows/forms.py:207 msgid "PDF-Briefkopf" msgstr "PDF letterhead" -#: workflows/forms.py:195 +#: workflows/forms.py:208 +msgid "Favicon" +msgstr "" + +#: workflows/forms.py:209 msgid "Primärfarbe" msgstr "Primary color" -#: workflows/forms.py:196 +#: workflows/forms.py:210 msgid "Sekundärfarbe" msgstr "Secondary color" -#: workflows/forms.py:210 +#: workflows/forms.py:227 msgid "Das Logo darf maximal 5 MB groß sein." msgstr "" -#: workflows/forms.py:218 +#: workflows/forms.py:235 msgid "Der PDF-Briefkopf darf maximal 10 MB groß sein." 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 msgid "Bitte nutzen Sie das Format name@%(domain)s." msgstr "" -#: workflows/forms.py:379 workflows/forms.py:556 +#: workflows/forms.py:404 workflows/forms.py:581 #, python-format msgid "Bitte verwenden Sie eine @%(domain)s E-Mail-Adresse." msgstr "" -#: workflows/forms.py:464 +#: workflows/forms.py:489 #, python-format msgid "" "Das Übergabedatum muss mindestens %(days)s Tage in der Zukunft liegen " "(frühestens %(date)s)." msgstr "" -#: workflows/models.py:139 workflows/views.py:200 +#: workflows/models.py:151 workflows/views.py:200 msgid "Eingereicht" msgstr "Submitted" -#: workflows/models.py:140 workflows/views.py:201 +#: workflows/models.py:152 workflows/views.py:201 msgid "In Bearbeitung" 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" 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/requests_dashboard.html:222 #: workflows/templates/workflows/welcome_emails.html:108 workflows/views.py:203 msgid "Fehlgeschlagen" msgstr "Failed" -#: workflows/models.py:149 +#: workflows/models.py:161 msgid "Herr" msgstr "" -#: workflows/models.py:149 +#: workflows/models.py:161 msgid "Frau" msgstr "" -#: workflows/models.py:149 +#: workflows/models.py:161 msgid "Divers" msgstr "" -#: workflows/models.py:159 +#: workflows/models.py:171 msgid "befristet" msgstr "" -#: workflows/models.py:159 +#: workflows/models.py:171 msgid "unbefristet" msgstr "" -#: workflows/models.py:222 +#: workflows/models.py:234 #: workflows/templates/workflows/onboarding_intro_session.html:28 #: workflows/templates/workflows/requests_dashboard.html:145 msgid "Abteilung" msgstr "Department" -#: workflows/models.py:223 +#: workflows/models.py:235 msgid "Geräte" msgstr "" -#: workflows/models.py:224 +#: workflows/models.py:236 msgid "Software" msgstr "" -#: workflows/models.py:225 +#: workflows/models.py:237 #, fuzzy #| msgid "Vorgänge" msgid "Zugänge" msgstr "Requests" -#: workflows/models.py:226 +#: workflows/models.py:238 msgid "Workspace-Gruppen" msgstr "" -#: workflows/models.py:227 +#: workflows/models.py:239 msgid "Ressourcen" msgstr "" -#: workflows/models.py:228 +#: workflows/models.py:240 msgid "Telefonnummern" msgstr "" -#: workflows/models.py:254 +#: workflows/models.py:266 msgid "Automatisch" msgstr "" -#: workflows/models.py:255 workflows/views.py:95 +#: workflows/models.py:267 workflows/views.py:95 msgid "Stammdaten" msgstr "Master data" -#: workflows/models.py:256 workflows/views.py:96 +#: workflows/models.py:268 workflows/views.py:96 msgid "Vertrag" msgstr "Contract" -#: workflows/models.py:257 workflows/views.py:97 +#: workflows/models.py:269 workflows/views.py:97 msgid "IT-Setup" msgstr "IT setup" -#: workflows/models.py:258 workflows/views.py:98 +#: workflows/models.py:270 workflows/views.py:98 msgid "Abschluss" msgstr "Finish" -#: workflows/models.py:300 +#: workflows/models.py:312 #, fuzzy #| msgid "Onboarding" msgid "Onboarding: IT" msgstr "Onboarding" -#: workflows/models.py:301 +#: workflows/models.py:313 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Onboarding: Allgemeine Info" msgstr "Save offboarding request" -#: workflows/models.py:302 +#: workflows/models.py:314 #, fuzzy #| msgid "Onboarding starten" msgid "Onboarding: Visitenkarte" msgstr "Start onboarding" -#: workflows/models.py:303 +#: workflows/models.py:315 #, fuzzy #| msgid "Onboarding" msgid "Onboarding: HR Works" msgstr "Onboarding" -#: workflows/models.py:304 +#: workflows/models.py:316 #, fuzzy #| msgid "Onboarding starten" msgid "Onboarding: Schlüssel" msgstr "Start onboarding" -#: workflows/models.py:305 +#: workflows/models.py:317 msgid "Onboarding: Referenz Anfordernde Person" msgstr "" -#: workflows/models.py:306 +#: workflows/models.py:318 #, fuzzy #| msgid "Welcome E-Mails" msgid "Onboarding: Welcome E-Mail" msgstr "Welcome Emails" -#: workflows/models.py:307 +#: workflows/models.py:319 #, fuzzy #| msgid "Offboarding" msgid "Offboarding: IT" msgstr "Offboarding" -#: workflows/models.py:308 +#: workflows/models.py:320 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Offboarding: Allgemeine Info" msgstr "Save offboarding request" -#: workflows/models.py:309 +#: workflows/models.py:321 #, fuzzy #| msgid "Offboarding starten" msgid "Offboarding: HR Works Deaktivierung" msgstr "Start offboarding" -#: workflows/models.py:310 +#: workflows/models.py:322 msgid "Offboarding: Referenz Anfordernde Person" msgstr "" -#: workflows/models.py:346 +#: workflows/models.py:358 msgid "Immer" msgstr "" -#: workflows/models.py:347 workflows/models.py:425 +#: workflows/models.py:359 workflows/models.py:437 msgid "Enthält" msgstr "" -#: workflows/models.py:348 workflows/models.py:426 +#: workflows/models.py:360 workflows/models.py:438 msgid "Ist gleich" msgstr "" -#: workflows/models.py:349 +#: workflows/models.py:361 msgid "Ist aktiv/Ja" msgstr "" -#: workflows/models.py:350 +#: workflows/models.py:362 #, fuzzy #| msgid "inaktiv" msgid "Ist inaktiv/Nein" msgstr "inactive" -#: workflows/models.py:392 +#: workflows/models.py:404 #: workflows/templates/workflows/welcome_emails.html:100 msgid "Geplant" msgstr "Scheduled" -#: workflows/models.py:393 +#: workflows/models.py:405 #: workflows/templates/workflows/welcome_emails.html:102 msgid "Pausiert" msgstr "Paused" -#: workflows/models.py:394 +#: workflows/models.py:406 #: workflows/templates/workflows/welcome_emails.html:104 msgid "Abgebrochen" msgstr "Cancelled" -#: workflows/models.py:395 +#: workflows/models.py:407 #: workflows/templates/workflows/welcome_emails.html:106 msgid "Gesendet" msgstr "Sent" -#: workflows/models.py:418 workflows/tasks.py:576 +#: workflows/models.py:430 workflows/tasks.py:576 msgid "Geräte und Arbeitsplatz" msgstr "Devices and workplace" -#: workflows/models.py:419 workflows/tasks.py:577 +#: workflows/models.py:431 workflows/tasks.py:577 msgid "Konten und Berechtigungen" msgstr "Accounts and permissions" -#: workflows/models.py:420 workflows/tasks.py:578 +#: workflows/models.py:432 workflows/tasks.py:578 msgid "Software und 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" msgstr "Processes and notes" -#: workflows/models.py:424 +#: workflows/models.py:436 msgid "Immer anzeigen" msgstr "Always show" -#: workflows/models.py:427 +#: workflows/models.py:439 msgid "Ist Ja / aktiv" msgstr "Is yes / active" -#: workflows/models.py:428 +#: workflows/models.py:440 msgid "Ist Nein / inaktiv" msgstr "Is no / inactive" -#: workflows/models.py:455 +#: workflows/models.py:467 msgid "Entwurf" msgstr "Draft" -#: workflows/models.py:475 +#: workflows/models.py:487 #, fuzzy #| msgid "Nextcloud:" msgid "Nextcloud" msgstr "Nextcloud:" -#: workflows/models.py:476 +#: workflows/models.py:488 msgid "S3" msgstr "" -#: workflows/models.py:477 +#: workflows/models.py:489 msgid "NFS" msgstr "" @@ -754,7 +786,6 @@ msgid "Anmeldung" msgstr "Sign in" #: workflows/templates/registration/login.html:20 -#: workflows/templates/workflows/auth/login.html:18 msgid "Bitte melden Sie sich mit Ihrem Benutzerkonto an." msgstr "Please sign in with your user account." @@ -1173,28 +1204,28 @@ msgstr "Delete" msgid "Noch keine Backup-Bundles vorhanden." msgstr "No backup bundles available yet." -#: workflows/templates/workflows/base_shell.html:24 +#: workflows/templates/workflows/base_shell.html:31 msgid "Bitte bestätigen" msgstr "" -#: workflows/templates/workflows/base_shell.html:28 +#: workflows/templates/workflows/base_shell.html:35 #: workflows/templates/workflows/welcome_emails.html:134 msgid "Abbrechen" msgstr "Cancel" -#: workflows/templates/workflows/base_shell.html:29 +#: workflows/templates/workflows/base_shell.html:36 msgid "Bestätigen" msgstr "" -#: workflows/templates/workflows/base_shell.html:36 +#: workflows/templates/workflows/base_shell.html:43 msgid "Bitte warten" msgstr "Please wait" -#: workflows/templates/workflows/base_shell.html:37 +#: workflows/templates/workflows/base_shell.html:44 msgid "Aktion läuft" 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." msgstr "The action is running in the current tab." @@ -1209,28 +1240,43 @@ msgid "" "B. tub.co." 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." msgstr "" -#: workflows/templates/workflows/branding_settings.html:56 +#: workflows/templates/workflows/branding_settings.html:81 msgid "Aktuelles Logo:" msgstr "Current logo:" -#: workflows/templates/workflows/branding_settings.html:56 -#: workflows/templates/workflows/branding_settings.html:65 +#: workflows/templates/workflows/branding_settings.html:81 +#: workflows/templates/workflows/branding_settings.html:90 +#: workflows/templates/workflows/branding_settings.html:99 msgid "öffnen" msgstr "open" -#: workflows/templates/workflows/branding_settings.html:62 +#: workflows/templates/workflows/branding_settings.html:87 msgid "Erlaubtes Format: PDF. Maximal 10 MB." msgstr "" -#: workflows/templates/workflows/branding_settings.html:65 +#: workflows/templates/workflows/branding_settings.html:90 msgid "Aktueller Briefkopf:" 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 "" "TUBCO bleibt als Standard erhalten, bis hier Werte geändert oder Dateien " "hochgeladen werden." @@ -1238,7 +1284,7 @@ msgstr "" "TUBCO remains the default until values are changed or files are uploaded " "here." -#: workflows/templates/workflows/branding_settings.html:71 +#: workflows/templates/workflows/branding_settings.html:105 msgid "Branding speichern" msgstr "Save branding" diff --git a/backend/workflows/admin.py b/backend/workflows/admin.py index 31a851f..1064d62 100644 --- a/backend/workflows/admin.py +++ b/backend/workflows/admin.py @@ -22,7 +22,7 @@ class AdminAuditLogAdmin(admin.ModelAdmin): @admin.register(PortalBranding) 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) diff --git a/backend/workflows/branding.py b/backend/workflows/branding.py index 46d4ae6..94759b0 100644 --- a/backend/workflows/branding.py +++ b/backend/workflows/branding.py @@ -1,9 +1,11 @@ from __future__ import annotations from pathlib import Path +from email.utils import formataddr from django.conf import settings from django.templatetags.static import static +from django.utils.translation import get_language from .models import PortalBranding @@ -16,6 +18,12 @@ def get_portal_branding() -> PortalBranding: 'company_name': 'TUBCO', 'company_domain': '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', 'primary_color': '#000078', 'secondary_color': '#c0002b', @@ -40,6 +48,16 @@ def get_portal_logo_url() -> str: 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: branding = get_portal_branding() if branding.pdf_letterhead: @@ -54,18 +72,31 @@ def get_portal_letterhead_path() -> Path: def get_branding_context() -> dict[str, object]: 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 { 'portal_branding': branding, 'portal_title': branding.portal_title, 'portal_company_name': branding.company_name, 'portal_email_domain': get_company_email_domain(), '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_primary_color': branding.primary_color, 'portal_secondary_color': branding.secondary_color, '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_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(), 'portal_title': portal_title, '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]]: from copy import deepcopy diff --git a/backend/workflows/emailing.py b/backend/workflows/emailing.py index 84620f9..68bba55 100644 --- a/backend/workflows/emailing.py +++ b/backend/workflows/emailing.py @@ -1,6 +1,7 @@ from django.conf import settings from django.core.mail import EmailMessage, get_connection +from .branding import get_branded_from_email from .models import SystemEmailConfig, WorkflowConfig @@ -66,7 +67,7 @@ def send_system_email( msg = EmailMessage( subject=subject, 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, connection=connection, ) diff --git a/backend/workflows/forms.py b/backend/workflows/forms.py index acd6060..5aaabc4 100644 --- a/backend/workflows/forms.py +++ b/backend/workflows/forms.py @@ -178,9 +178,16 @@ class PortalBrandingForm(forms.ModelForm): 'company_name', 'company_domain', 'support_email', + 'sender_display_name', + 'login_subtitle', + 'footer_text', + 'footer_text_en', + 'legal_notice', + 'legal_notice_en', 'default_language', 'logo_image', 'pdf_letterhead', + 'favicon_image', 'primary_color', 'secondary_color', ] @@ -189,9 +196,16 @@ class PortalBrandingForm(forms.ModelForm): 'company_name': gettext_lazy('Firmenname'), 'company_domain': gettext_lazy('Firmen-Domain'), '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'), 'logo_image': gettext_lazy('Logo'), 'pdf_letterhead': gettext_lazy('PDF-Briefkopf'), + 'favicon_image': gettext_lazy('Favicon'), 'primary_color': gettext_lazy('Primärfarbe'), 'secondary_color': gettext_lazy('Sekundärfarbe'), } @@ -200,6 +214,9 @@ class PortalBrandingForm(forms.ModelForm): 'secondary_color': forms.TextInput(attrs={'type': 'color'}), 'logo_image': forms.ClearableFileInput(attrs={'accept': '.svg,.png,.jpg,.jpeg,.webp'}), 'pdf_letterhead': forms.ClearableFileInput(attrs={'accept': '.pdf'}), + '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): @@ -218,6 +235,14 @@ class PortalBrandingForm(forms.ModelForm): raise forms.ValidationError(_('Der PDF-Briefkopf darf maximal 10 MB groß sein.')) 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): first_name = forms.CharField(label='Vorname', required=False) diff --git a/backend/workflows/migrations/0040_portalbranding_favicon_image_and_more.py b/backend/workflows/migrations/0040_portalbranding_favicon_image_and_more.py new file mode 100644 index 0000000..4408dd2 --- /dev/null +++ b/backend/workflows/migrations/0040_portalbranding_favicon_image_and_more.py @@ -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), + ), + ] diff --git a/backend/workflows/models.py b/backend/workflows/models.py index 7108549..54e3091 100644 --- a/backend/workflows/models.py +++ b/backend/workflows/models.py @@ -31,6 +31,12 @@ class PortalBranding(models.Model): company_name = models.CharField(max_length=255, default='TUBCO') company_domain = models.CharField(max_length=120, blank=True, default='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( max_length=10, choices=[('de', 'Deutsch'), ('en', 'English')], @@ -48,6 +54,12 @@ class PortalBranding(models.Model): null=True, 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') secondary_color = models.CharField(max_length=20, blank=True, default='#c0002b') updated_at = models.DateTimeField(auto_now=True) diff --git a/backend/workflows/static/workflows/css/admin_tools.css b/backend/workflows/static/workflows/css/admin_tools.css index ada691d..35878d6 100644 --- a/backend/workflows/static/workflows/css/admin_tools.css +++ b/backend/workflows/static/workflows/css/admin_tools.css @@ -7,6 +7,30 @@ h1 { margin: 12px 0 6px; color: #000078; } .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); } .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); } 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); } @@ -56,8 +80,13 @@ th { background: #f6f9ff; color: #334155; } .bulk-note { color: #64748b; font-size: 12px; } .field label { display: block; font-weight: 600; margin-bottom: 6px; } .field input, .field select { min-height: 40px; } +.field-full { grid-column: 1 / -1; } .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="checkbox"] { transform: scale(1.1); width: auto; } .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; } +} diff --git a/backend/workflows/static/workflows/css/app_chrome.css b/backend/workflows/static/workflows/css/app_chrome.css index 3129592..8ba66d8 100644 --- a/backend/workflows/static/workflows/css/app_chrome.css +++ b/backend/workflows/static/workflows/css/app_chrome.css @@ -122,6 +122,25 @@ 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) { .app-header, .app-header-in-shell { diff --git a/backend/workflows/templates/workflows/auth/login.html b/backend/workflows/templates/workflows/auth/login.html index cbca437..ab0ff1d 100644 --- a/backend/workflows/templates/workflows/auth/login.html +++ b/backend/workflows/templates/workflows/auth/login.html @@ -15,7 +15,7 @@

{% trans "Anmeldung" %}

-

{% trans "Bitte melden Sie sich mit Ihrem Benutzerkonto an." %}

+

{{ portal_login_subtitle }}

{% csrf_token %} diff --git a/backend/workflows/templates/workflows/base_shell.html b/backend/workflows/templates/workflows/base_shell.html index a5d167b..0057e9f 100644 --- a/backend/workflows/templates/workflows/base_shell.html +++ b/backend/workflows/templates/workflows/base_shell.html @@ -6,6 +6,7 @@ {% block title %}{% endblock %} + {% block extra_css %}{% endblock %} @@ -17,6 +18,12 @@ {% block shell_header %}{% endblock %} {% block shell_body %}{% endblock %}
+ {% if portal_footer_text or portal_legal_notice %} + + {% endif %}