snapshot: preserve extended branding layer and branding UI polish
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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)
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<section class="login-shell-body">
|
||||
<div class="login-card">
|
||||
<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/">
|
||||
{% csrf_token %}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<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/app_chrome.css' %}" />
|
||||
{% block extra_css %}{% endblock %}
|
||||
@@ -17,6 +18,12 @@
|
||||
{% block shell_header %}{% endblock %}
|
||||
{% block shell_body %}{% endblock %}
|
||||
</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-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">
|
||||
|
||||
@@ -17,54 +17,150 @@
|
||||
<section class="card">
|
||||
<form method="post" action="{% url 'save_portal_branding' %}" enctype="multipart/form-data" class="stack-form">
|
||||
{% csrf_token %}
|
||||
<div class="grid two">
|
||||
<div class="field">
|
||||
<label for="{{ form.portal_title.id_for_label }}">{{ form.portal_title.label }}</label>
|
||||
{{ form.portal_title }}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="{{ form.company_name.id_for_label }}">{{ form.company_name.label }}</label>
|
||||
{{ form.company_name }}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="{{ form.company_domain.id_for_label }}">{{ form.company_domain.label }}</label>
|
||||
{{ form.company_domain }}
|
||||
<div class="hint">{% trans "Wird für E-Mail-Vorschläge und Domain-bezogene Standardtexte verwendet, z. B. tub.co." %}</div>
|
||||
</div>
|
||||
<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.default_language.id_for_label }}">{{ form.default_language.label }}</label>
|
||||
{{ form.default_language }}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="{{ form.primary_color.id_for_label }}">{{ form.primary_color.label }}</label>
|
||||
{{ form.primary_color }}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="{{ form.secondary_color.id_for_label }}">{{ form.secondary_color.label }}</label>
|
||||
{{ form.secondary_color }}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="{{ form.logo_image.id_for_label }}">{{ form.logo_image.label }}</label>
|
||||
{{ 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.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 class="branding-sections">
|
||||
<section class="branding-block">
|
||||
<div class="branding-block-head">
|
||||
<h2>{% trans "Identität" %}</h2>
|
||||
<p>{% trans "Titel, Firmenname und zentrale Spracheinstellungen." %}</p>
|
||||
</div>
|
||||
<div class="grid two">
|
||||
<div class="field">
|
||||
<label for="{{ form.portal_title.id_for_label }}">{{ form.portal_title.label }}</label>
|
||||
{{ form.portal_title }}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="{{ form.company_name.id_for_label }}">{{ form.company_name.label }}</label>
|
||||
{{ form.company_name }}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="{{ form.company_domain.id_for_label }}">{{ form.company_domain.label }}</label>
|
||||
{{ form.company_domain }}
|
||||
<div class="hint">{% trans "Wird für E-Mail-Vorschläge und Domain-bezogene Standardtexte verwendet, z. B. tub.co." %}</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="{{ form.default_language.id_for_label }}">{{ form.default_language.label }}</label>
|
||||
{{ form.default_language }}
|
||||
</div>
|
||||
<div class="field field-full">
|
||||
<label for="{{ form.login_subtitle.id_for_label }}">{{ form.login_subtitle.label }}</label>
|
||||
{{ form.login_subtitle }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="branding-block">
|
||||
<div class="branding-block-head">
|
||||
<h2>{% trans "Farben & Erscheinungsbild" %}</h2>
|
||||
<p>{% trans "Zentrale visuelle Markenwerte und Browser-Icon." %}</p>
|
||||
</div>
|
||||
<div class="grid two">
|
||||
<div class="field">
|
||||
<label for="{{ form.primary_color.id_for_label }}">{{ form.primary_color.label }}</label>
|
||||
{{ form.primary_color }}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="{{ form.secondary_color.id_for_label }}">{{ form.secondary_color.label }}</label>
|
||||
{{ form.secondary_color }}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="{{ form.logo_image.id_for_label }}">{{ form.logo_image.label }}</label>
|
||||
{{ 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 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>
|
||||
@@ -73,3 +169,59 @@
|
||||
</form>
|
||||
</section>
|
||||
{% 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 %}
|
||||
|
||||
@@ -176,8 +176,10 @@ docker compose exec -T web django-admin compilemessages</code></pre>
|
||||
<ul>
|
||||
<li>Portal-level branding is stored in the singleton model <code>PortalBranding</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>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>
|
||||
</ul>
|
||||
|
||||
|
||||
@@ -178,7 +178,7 @@
|
||||
<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>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>Benutzer & 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>
|
||||
|
||||
Reference in New Issue
Block a user