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

@@ -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 %}

View File

@@ -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">

View File

@@ -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 %}

View File

@@ -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>

View File

@@ -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 &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>