snapshot: preserve company config foundation and staff dashboard access

This commit is contained in:
Md Bayazid Bostame
2026-03-26 13:58:45 +01:00
parent 9437aaa29a
commit 7bd03fc86e
15 changed files with 681 additions and 154 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@ from django.conf import settings
from django import forms from django import forms
from .emailing import send_system_email from .emailing import send_system_email
from .models import AdminAuditLog, EmployeeProfile, FormFieldConfig, FormOption, IntroChecklistItem, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, PortalAppConfig, PortalBranding, ScheduledWelcomeEmail, SystemEmailConfig, WorkflowConfig from .models import AdminAuditLog, EmployeeProfile, FormFieldConfig, FormOption, IntroChecklistItem, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, PortalAppConfig, PortalBranding, PortalCompanyConfig, ScheduledWelcomeEmail, SystemEmailConfig, WorkflowConfig
@admin.register(EmployeeProfile) @admin.register(EmployeeProfile)
@@ -25,6 +25,11 @@ class PortalBrandingAdmin(admin.ModelAdmin):
list_display = ('name', 'portal_title', 'company_name', 'company_domain', 'support_email', 'default_language', 'updated_at') list_display = ('name', 'portal_title', 'company_name', 'company_domain', 'support_email', 'default_language', 'updated_at')
@admin.register(PortalCompanyConfig)
class PortalCompanyConfigAdmin(admin.ModelAdmin):
list_display = ('name', 'legal_company_name', 'website_url', 'hr_contact_email', 'it_contact_email', 'updated_at')
@admin.register(PortalAppConfig) @admin.register(PortalAppConfig)
class PortalAppConfigAdmin(admin.ModelAdmin): class PortalAppConfigAdmin(admin.ModelAdmin):
list_display = ( list_display = (

View File

@@ -58,6 +58,15 @@ APP_DEFINITIONS: tuple[AppDefinition, ...] = (
accent='APP', accent='APP',
tags=(_('Suche'), _('Status'), _('PDF Zugriff')), tags=(_('Suche'), _('Status'), _('PDF Zugriff')),
), ),
AppDefinition(
key='company_config',
section=PortalAppConfig.SECTION_PLATFORM,
route_name='portal_company_config_page',
title=_('Company Config'),
description=_('Rechtliche Firmendaten, Kontaktpunkte und öffentliche Unternehmenslinks pflegen.'),
action_label=_('Öffnen'),
capability='manage_company_config',
),
AppDefinition( AppDefinition(
key='branding', key='branding',
section=PortalAppConfig.SECTION_PLATFORM, section=PortalAppConfig.SECTION_PLATFORM,
@@ -185,6 +194,12 @@ DEFAULT_ROLE_VISIBILITY = {
ROLE_IT_STAFF: False, ROLE_IT_STAFF: False,
ROLE_STAFF: False, ROLE_STAFF: False,
}, },
'company_config': {
ROLE_SUPER_ADMIN: False,
ROLE_ADMIN: False,
ROLE_IT_STAFF: False,
ROLE_STAFF: False,
},
'app_registry': { 'app_registry': {
ROLE_SUPER_ADMIN: False, ROLE_SUPER_ADMIN: False,
ROLE_ADMIN: False, ROLE_ADMIN: False,

View File

@@ -7,7 +7,7 @@ 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 django.utils.translation import get_language
from .models import PortalBranding from .models import PortalBranding, PortalCompanyConfig
def get_portal_branding() -> PortalBranding: def get_portal_branding() -> PortalBranding:
@@ -32,6 +32,26 @@ def get_portal_branding() -> PortalBranding:
return branding return branding
def get_portal_company_config() -> PortalCompanyConfig:
company_config, _ = PortalCompanyConfig.objects.get_or_create(
name='Default',
defaults={
'legal_company_name': 'TUBCO GmbH',
'country': 'Deutschland',
'website_url': '',
'imprint_url': '',
'privacy_url': '',
'hr_contact_email': '',
'it_contact_email': '',
'operations_contact_email': '',
'phone_number': '',
'vat_id': '',
'registration_number': '',
},
)
return company_config
def get_company_email_domain() -> str: def get_company_email_domain() -> str:
branding = get_portal_branding() branding = get_portal_branding()
domain = (branding.company_domain or '').strip().lower().lstrip('@') domain = (branding.company_domain or '').strip().lower().lstrip('@')
@@ -72,6 +92,7 @@ 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()
company_config = get_portal_company_config()
lang = (get_language() or branding.default_language or 'de').split('-')[0] lang = (get_language() or branding.default_language or 'de').split('-')[0]
footer_text = (branding.footer_text_en or '').strip() if lang == 'en' else '' footer_text = (branding.footer_text_en or '').strip() if lang == 'en' else ''
legal_notice = (branding.legal_notice_en or '').strip() if lang == 'en' else '' legal_notice = (branding.legal_notice_en or '').strip() if lang == 'en' else ''
@@ -97,6 +118,21 @@ def get_branding_context() -> dict[str, object]:
'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), 'portal_has_custom_favicon': bool(branding.favicon_image),
'portal_company_config': company_config,
'portal_company_legal_name': company_config.legal_company_name or branding.company_name,
'portal_company_street': company_config.street_address,
'portal_company_postal_code': company_config.postal_code,
'portal_company_city': company_config.city,
'portal_company_country': company_config.country,
'portal_company_website_url': company_config.website_url,
'portal_company_imprint_url': company_config.imprint_url,
'portal_company_privacy_url': company_config.privacy_url,
'portal_company_hr_contact_email': company_config.hr_contact_email,
'portal_company_it_contact_email': company_config.it_contact_email,
'portal_company_operations_contact_email': company_config.operations_contact_email,
'portal_company_phone_number': company_config.phone_number,
'portal_company_vat_id': company_config.vat_id,
'portal_company_registration_number': company_config.registration_number,
} }

View File

@@ -8,7 +8,7 @@ from django.utils.translation import get_language, gettext as _, gettext_lazy
from .branding import get_company_email_domain from .branding import get_company_email_domain
from .form_builder import apply_form_field_config from .form_builder import apply_form_field_config
from .models import EmployeeProfile, FormOption, OffboardingRequest, OnboardingRequest, PortalBranding, WorkflowConfig from .models import EmployeeProfile, FormOption, OffboardingRequest, OnboardingRequest, PortalBranding, PortalCompanyConfig, WorkflowConfig
from .roles import ROLE_ADMIN, ROLE_GROUP_NAMES, ROLE_IT_STAFF, ROLE_LABELS, ROLE_PLATFORM_OWNER, ROLE_STAFF, ROLE_SUPER_ADMIN, assign_user_role from .roles import ROLE_ADMIN, ROLE_GROUP_NAMES, ROLE_IT_STAFF, ROLE_LABELS, ROLE_PLATFORM_OWNER, ROLE_STAFF, ROLE_SUPER_ADMIN, assign_user_role
@@ -244,6 +244,43 @@ class PortalBrandingForm(forms.ModelForm):
return favicon return favicon
class PortalCompanyConfigForm(forms.ModelForm):
class Meta:
model = PortalCompanyConfig
fields = [
'legal_company_name',
'street_address',
'postal_code',
'city',
'country',
'website_url',
'imprint_url',
'privacy_url',
'hr_contact_email',
'it_contact_email',
'operations_contact_email',
'phone_number',
'vat_id',
'registration_number',
]
labels = {
'legal_company_name': gettext_lazy('Rechtlicher Firmenname'),
'street_address': gettext_lazy('Straße und Hausnummer'),
'postal_code': gettext_lazy('Postleitzahl'),
'city': gettext_lazy('Stadt'),
'country': gettext_lazy('Land'),
'website_url': gettext_lazy('Website'),
'imprint_url': gettext_lazy('Impressum-URL'),
'privacy_url': gettext_lazy('Datenschutz-URL'),
'hr_contact_email': gettext_lazy('HR-Kontakt'),
'it_contact_email': gettext_lazy('IT-Kontakt'),
'operations_contact_email': gettext_lazy('Operations-Kontakt'),
'phone_number': gettext_lazy('Zentrale Telefonnummer'),
'vat_id': gettext_lazy('USt-IdNr.'),
'registration_number': gettext_lazy('Register- oder Handelsnummer'),
}
class OnboardingRequestForm(forms.ModelForm): class OnboardingRequestForm(forms.ModelForm):
first_name = forms.CharField(label='Vorname', required=False) first_name = forms.CharField(label='Vorname', required=False)
last_name = forms.CharField(label='Nachname', required=False) last_name = forms.CharField(label='Nachname', required=False)

View File

@@ -0,0 +1,39 @@
# Generated by Django 5.1.5 on 2026-03-26 12:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('workflows', '0041_portalappconfig_visible_to_admin_and_more'),
]
operations = [
migrations.CreateModel(
name='PortalCompanyConfig',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(default='Default', max_length=80, unique=True)),
('legal_company_name', models.CharField(blank=True, default='', max_length=255)),
('street_address', models.CharField(blank=True, default='', max_length=255)),
('postal_code', models.CharField(blank=True, default='', max_length=50)),
('city', models.CharField(blank=True, default='', max_length=120)),
('country', models.CharField(blank=True, default='Deutschland', max_length=120)),
('website_url', models.URLField(blank=True, default='')),
('imprint_url', models.URLField(blank=True, default='')),
('privacy_url', models.URLField(blank=True, default='')),
('hr_contact_email', models.EmailField(blank=True, default='', max_length=254)),
('it_contact_email', models.EmailField(blank=True, default='', max_length=254)),
('operations_contact_email', models.EmailField(blank=True, default='', max_length=254)),
('phone_number', models.CharField(blank=True, default='', max_length=80)),
('vat_id', models.CharField(blank=True, default='', max_length=80)),
('registration_number', models.CharField(blank=True, default='', max_length=120)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'verbose_name': 'Portal Company Config',
'verbose_name_plural': 'Portal Company Config',
},
),
]

View File

@@ -72,6 +72,32 @@ class PortalBranding(models.Model):
return self.portal_title or self.company_name or self.name return self.portal_title or self.company_name or self.name
class PortalCompanyConfig(models.Model):
name = models.CharField(max_length=80, default='Default', unique=True)
legal_company_name = models.CharField(max_length=255, blank=True, default='')
street_address = models.CharField(max_length=255, blank=True, default='')
postal_code = models.CharField(max_length=50, blank=True, default='')
city = models.CharField(max_length=120, blank=True, default='')
country = models.CharField(max_length=120, blank=True, default='Deutschland')
website_url = models.URLField(blank=True, default='')
imprint_url = models.URLField(blank=True, default='')
privacy_url = models.URLField(blank=True, default='')
hr_contact_email = models.EmailField(blank=True, default='')
it_contact_email = models.EmailField(blank=True, default='')
operations_contact_email = models.EmailField(blank=True, default='')
phone_number = models.CharField(max_length=80, blank=True, default='')
vat_id = models.CharField(max_length=80, blank=True, default='')
registration_number = models.CharField(max_length=120, blank=True, default='')
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = 'Portal Company Config'
verbose_name_plural = 'Portal Company Config'
def __str__(self) -> str:
return self.legal_company_name or self.name
class PortalAppConfig(models.Model): class PortalAppConfig(models.Model):
SECTION_APP = 'app' SECTION_APP = 'app'
SECTION_PLATFORM = 'platform' SECTION_PLATFORM = 'platform'

View File

@@ -29,8 +29,10 @@ ROLE_LABELS = {
CAPABILITIES = { CAPABILITIES = {
'manage_users': {ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN}, 'manage_users': {ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN},
'manage_product_branding': {ROLE_PLATFORM_OWNER}, 'manage_product_branding': {ROLE_PLATFORM_OWNER},
'manage_company_config': {ROLE_PLATFORM_OWNER},
'manage_app_registry': {ROLE_PLATFORM_OWNER}, 'manage_app_registry': {ROLE_PLATFORM_OWNER},
'access_requests_dashboard': {ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF}, 'access_requests_dashboard': {ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF, ROLE_STAFF},
'view_request_timeline': {ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF},
'run_intro_session': {ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF}, 'run_intro_session': {ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF},
'generate_intro_pdfs': {ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF}, 'generate_intro_pdfs': {ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF},
'retry_requests': {ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF}, 'retry_requests': {ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF},
@@ -124,9 +126,11 @@ def template_role_context(user) -> dict[str, object]:
'role_key': role_key, 'role_key': role_key,
'role_label': str(ROLE_LABELS[role_key]), 'role_label': str(ROLE_LABELS[role_key]),
'can_manage_product_branding': user_has_capability(user, 'manage_product_branding'), 'can_manage_product_branding': user_has_capability(user, 'manage_product_branding'),
'can_manage_company_config': user_has_capability(user, 'manage_company_config'),
'can_manage_app_registry': user_has_capability(user, 'manage_app_registry'), 'can_manage_app_registry': user_has_capability(user, 'manage_app_registry'),
'can_manage_users': user_has_capability(user, 'manage_users'), 'can_manage_users': user_has_capability(user, 'manage_users'),
'can_access_requests_dashboard': user_has_capability(user, 'access_requests_dashboard'), 'can_access_requests_dashboard': user_has_capability(user, 'access_requests_dashboard'),
'can_view_request_timeline': user_has_capability(user, 'view_request_timeline'),
'can_run_intro_session': user_has_capability(user, 'run_intro_session'), 'can_run_intro_session': user_has_capability(user, 'run_intro_session'),
'can_generate_intro_pdfs': user_has_capability(user, 'generate_intro_pdfs'), 'can_generate_intro_pdfs': user_has_capability(user, 'generate_intro_pdfs'),
'can_retry_requests': user_has_capability(user, 'retry_requests'), 'can_retry_requests': user_has_capability(user, 'retry_requests'),

View File

@@ -141,6 +141,29 @@
line-height: 1.5; line-height: 1.5;
} }
.app-site-footer-links {
margin-top: 8px;
display: flex;
gap: 12px;
justify-content: center;
flex-wrap: wrap;
}
.app-site-footer-links a {
color: #1f3a5f;
font-size: 12px;
font-weight: 700;
text-decoration: none;
transition:
color var(--motion-fast) var(--motion-ease),
transform var(--motion-fast) var(--motion-ease);
}
.app-site-footer-links a:hover {
color: var(--app-brand-blue);
transform: translateY(-1px);
}
@media (max-width: 900px) { @media (max-width: 900px) {
.app-header, .app-header,
.app-header-in-shell { .app-header-in-shell {

View File

@@ -197,7 +197,7 @@
</section> </section>
{% endblock %} {% endblock %}
{% block extra_js %} {% block extra_scripts %}
<script> <script>
(function () { (function () {
const searchInput = document.getElementById('app-registry-search'); const searchInput = document.getElementById('app-registry-search');

View File

@@ -22,6 +22,13 @@
<div class="app-site-footer"> <div class="app-site-footer">
{% if portal_footer_text %}<div class="app-site-footer-main">{{ portal_footer_text }}</div>{% endif %} {% 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 %} {% if portal_legal_notice %}<div class="app-site-footer-legal">{{ portal_legal_notice }}</div>{% endif %}
{% if portal_company_imprint_url or portal_company_privacy_url or portal_company_website_url %}
<div class="app-site-footer-links">
{% if portal_company_website_url %}<a href="{{ portal_company_website_url }}" target="_blank" rel="noopener">{% trans "Website" %}</a>{% endif %}
{% if portal_company_imprint_url %}<a href="{{ portal_company_imprint_url }}" target="_blank" rel="noopener">{% trans "Impressum" %}</a>{% endif %}
{% if portal_company_privacy_url %}<a href="{{ portal_company_privacy_url }}" target="_blank" rel="noopener">{% trans "Datenschutz" %}</a>{% endif %}
</div>
{% endif %}
</div> </div>
{% endif %} {% 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">

View File

@@ -0,0 +1,120 @@
{% extends 'workflows/base_shell.html' %}
{% load static i18n %}
{% block title %}{% trans "Company Config" %}{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'workflows/css/admin_tools.css' %}" />
{% endblock %}
{% block shell_body %}
{% include 'workflows/includes/app_header.html' with header_show_home=1 header_show_lang=1 header_inside_shell=1 %}
<h1>{% trans "Company Config" %}</h1>
<p class="sub">{% trans "Strukturierte Firmendaten, Kontaktpunkte und öffentliche Unternehmenslinks zentral pflegen." %}</p>
{% include 'workflows/includes/messages.html' %}
<section class="branding-sections">
<form method="post" action="{% url 'save_portal_company_config' %}" class="stack-form">
{% csrf_token %}
<section class="branding-block">
<div class="branding-block-head">
<h2>{% trans "Firmenprofil" %}</h2>
<p>{% trans "Rechtlicher Name und zentrale Stammdaten der Firma." %}</p>
</div>
<div class="grid">
<div class="field">
<label for="{{ form.legal_company_name.id_for_label }}">{{ form.legal_company_name.label }}</label>
{{ form.legal_company_name }}
</div>
<div class="field">
<label for="{{ form.phone_number.id_for_label }}">{{ form.phone_number.label }}</label>
{{ form.phone_number }}
</div>
<div class="field">
<label for="{{ form.website_url.id_for_label }}">{{ form.website_url.label }}</label>
{{ form.website_url }}
</div>
<div class="field">
<label for="{{ form.country.id_for_label }}">{{ form.country.label }}</label>
{{ form.country }}
</div>
</div>
</section>
<section class="branding-block">
<div class="branding-block-head">
<h2>{% trans "Adresse & Register" %}</h2>
<p>{% trans "Anschrift sowie optionale Register- und Steuerangaben." %}</p>
</div>
<div class="grid">
<div class="field field-full">
<label for="{{ form.street_address.id_for_label }}">{{ form.street_address.label }}</label>
{{ form.street_address }}
</div>
<div class="field">
<label for="{{ form.postal_code.id_for_label }}">{{ form.postal_code.label }}</label>
{{ form.postal_code }}
</div>
<div class="field">
<label for="{{ form.city.id_for_label }}">{{ form.city.label }}</label>
{{ form.city }}
</div>
<div class="field">
<label for="{{ form.registration_number.id_for_label }}">{{ form.registration_number.label }}</label>
{{ form.registration_number }}
</div>
<div class="field">
<label for="{{ form.vat_id.id_for_label }}">{{ form.vat_id.label }}</label>
{{ form.vat_id }}
</div>
</div>
</section>
<section class="branding-block">
<div class="branding-block-head">
<h2>{% trans "Kontaktpunkte" %}</h2>
<p>{% trans "Zentrale Ansprechpartner für HR, IT und Operations." %}</p>
</div>
<div class="grid">
<div class="field">
<label for="{{ form.hr_contact_email.id_for_label }}">{{ form.hr_contact_email.label }}</label>
{{ form.hr_contact_email }}
</div>
<div class="field">
<label for="{{ form.it_contact_email.id_for_label }}">{{ form.it_contact_email.label }}</label>
{{ form.it_contact_email }}
</div>
<div class="field">
<label for="{{ form.operations_contact_email.id_for_label }}">{{ form.operations_contact_email.label }}</label>
{{ form.operations_contact_email }}
</div>
</div>
</section>
<section class="branding-block">
<div class="branding-block-head">
<h2>{% trans "Recht & Öffentlichkeit" %}</h2>
<p>{% trans "Öffentliche Links für Website, Impressum und Datenschutz." %}</p>
</div>
<div class="grid">
<div class="field">
<label for="{{ form.imprint_url.id_for_label }}">{{ form.imprint_url.label }}</label>
{{ form.imprint_url }}
</div>
<div class="field">
<label for="{{ form.privacy_url.id_for_label }}">{{ form.privacy_url.label }}</label>
{{ form.privacy_url }}
</div>
</div>
<div class="hint">{% trans "Diese Links können später im Portal-Footer oder in öffentlichen Seiten verwendet werden." %}</div>
</section>
<div class="toolbar" style="margin-top:1rem;">
<div class="hint">{% trans "Diese Ebene ist bewusst von Branding getrennt: Hier geht es um strukturierte Firmendaten, nicht um visuelle Gestaltung." %}</div>
<button class="btn btn-primary" type="submit">{% trans "Firmenkonfiguration speichern" %}</button>
</div>
</form>
</section>
{% endblock %}

View File

@@ -190,7 +190,7 @@
<th>{% trans "E-Mail" %}</th> <th>{% trans "E-Mail" %}</th>
<th>{% trans "Dokument" %}</th> <th>{% trans "Dokument" %}</th>
{% if can_run_intro_session or can_generate_intro_pdfs %}<th>{% trans "Einweisung" %}</th>{% endif %} {% if can_run_intro_session or can_generate_intro_pdfs %}<th>{% trans "Einweisung" %}</th>{% endif %}
{% if can_retry_requests or can_delete_requests or can_access_requests_dashboard %}<th>{% trans "Aktion" %}</th>{% endif %} {% if can_view_request_timeline or can_retry_requests or can_delete_requests %}<th>{% trans "Aktion" %}</th>{% endif %}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -271,9 +271,11 @@
{% endif %} {% endif %}
</td> </td>
{% endif %} {% endif %}
{% if can_retry_requests or can_delete_requests or can_access_requests_dashboard %} {% if can_view_request_timeline or can_retry_requests or can_delete_requests %}
<td class="actions-cell"> <td class="actions-cell">
{% if can_view_request_timeline %}
<a class="btn btn-secondary" href="/requests/timeline/{{ row.kind_slug }}/{{ row.id }}/">{% trans "Timeline" %}</a> <a class="btn btn-secondary" href="/requests/timeline/{{ row.kind_slug }}/{{ row.id }}/">{% trans "Timeline" %}</a>
{% endif %}
{% if can_retry_requests and row.status_key == 'failed' %} {% if can_retry_requests and row.status_key == 'failed' %}
<form method="post" action="/requests/retry/{{ row.kind_slug }}/{{ row.id }}/" class="inline-delete" data-confirm="{% trans 'Eintrag erneut verarbeiten?' %}"> <form method="post" action="/requests/retry/{{ row.kind_slug }}/{{ row.id }}/" class="inline-delete" data-confirm="{% trans 'Eintrag erneut verarbeiten?' %}">
{% csrf_token %} {% csrf_token %}

View File

@@ -32,6 +32,8 @@ urlpatterns = [
path('admin-tools/handbook/', views.handbook_page, name='handbook_page'), path('admin-tools/handbook/', views.handbook_page, name='handbook_page'),
path('admin-tools/branding/', views.portal_branding_page, name='portal_branding_page'), path('admin-tools/branding/', views.portal_branding_page, name='portal_branding_page'),
path('admin-tools/branding/save/', views.save_portal_branding, name='save_portal_branding'), path('admin-tools/branding/save/', views.save_portal_branding, name='save_portal_branding'),
path('admin-tools/company/', views.portal_company_config_page, name='portal_company_config_page'),
path('admin-tools/company/save/', views.save_portal_company_config, name='save_portal_company_config'),
path('admin-tools/apps/', views.portal_app_registry_page, name='portal_app_registry_page'), path('admin-tools/apps/', views.portal_app_registry_page, name='portal_app_registry_page'),
path('admin-tools/apps/save/', views.save_portal_app_registry, name='save_portal_app_registry'), path('admin-tools/apps/save/', views.save_portal_app_registry, name='save_portal_app_registry'),
path('admin-tools/users/', views.user_management_page, name='user_management_page'), path('admin-tools/users/', views.user_management_page, name='user_management_page'),

View File

@@ -27,7 +27,7 @@ from django.urls import reverse
from .app_registry import build_portal_app_sections, get_portal_app_registry_rows from .app_registry import build_portal_app_sections, get_portal_app_registry_rows
from .backup_ops import create_backup_bundle, delete_backup_bundle, list_backup_bundles, verify_backup_bundle from .backup_ops import create_backup_bundle, delete_backup_bundle, list_backup_bundles, verify_backup_bundle
from .branding import get_branding_email_copy, get_company_email_domain, get_default_notification_templates from .branding import get_branding_email_copy, get_company_email_domain, get_default_notification_templates
from .forms import OffboardingRequestForm, OnboardingRequestForm, PortalBrandingForm, UserManagementCreateForm from .forms import OffboardingRequestForm, OnboardingRequestForm, PortalBrandingForm, PortalCompanyConfigForm, UserManagementCreateForm
from .form_builder import ( from .form_builder import (
DEFAULT_FIELD_ORDER, DEFAULT_FIELD_ORDER,
LOCKED_FIELD_RULES, LOCKED_FIELD_RULES,
@@ -36,7 +36,7 @@ from .form_builder import (
ONBOARDING_PAGE_ORDER, ONBOARDING_PAGE_ORDER,
ensure_form_field_configs, ensure_form_field_configs,
) )
from .models import AdminAuditLog, EmployeeProfile, FormFieldConfig, FormOption, IntroChecklistItem, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, PortalAppConfig, PortalBranding, ScheduledWelcomeEmail, SystemEmailConfig, WorkflowConfig from .models import AdminAuditLog, EmployeeProfile, FormFieldConfig, FormOption, IntroChecklistItem, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, PortalAppConfig, PortalBranding, PortalCompanyConfig, ScheduledWelcomeEmail, SystemEmailConfig, WorkflowConfig
from .emailing import send_system_email from .emailing import send_system_email
from .roles import ROLE_GROUP_NAMES, ROLE_LABELS, ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN, assign_user_role, get_user_role_key, get_user_role_label, user_has_capability from .roles import ROLE_GROUP_NAMES, ROLE_LABELS, ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN, assign_user_role, get_user_role_key, get_user_role_label, user_has_capability
from .services import get_email_test_redirect, is_email_test_mode, is_nextcloud_enabled, upload_to_nextcloud from .services import get_email_test_redirect, is_email_test_mode, is_nextcloud_enabled, upload_to_nextcloud
@@ -587,6 +587,64 @@ def save_portal_branding(request):
) )
@_require_capability('manage_company_config')
def portal_company_config_page(request):
company_config, created = PortalCompanyConfig.objects.get_or_create(name='Default')
form = PortalCompanyConfigForm(instance=company_config)
return render(
request,
'workflows/company_config.html',
{
'form': form,
'company_config': company_config,
},
)
@_require_capability('manage_company_config')
@require_POST
def save_portal_company_config(request):
company_config, created = PortalCompanyConfig.objects.get_or_create(name='Default')
form = PortalCompanyConfigForm(request.POST, instance=company_config)
if not form.is_valid():
messages.error(request, _('Firmenkonfiguration konnte nicht gespeichert werden. Bitte prüfen Sie die Eingaben.'))
return render(
request,
'workflows/company_config.html',
{
'form': form,
'company_config': company_config,
},
status=400,
)
company_config = form.save()
_audit(
request,
'portal_company_config_saved',
target_type='portal_company_config',
target_id=company_config.id,
target_label=company_config.legal_company_name or 'Default',
details={
'website_url': company_config.website_url,
'imprint_url': company_config.imprint_url,
'privacy_url': company_config.privacy_url,
'hr_contact_email': company_config.hr_contact_email,
'it_contact_email': company_config.it_contact_email,
'operations_contact_email': company_config.operations_contact_email,
},
)
messages.success(request, _('Firmenkonfiguration wurde gespeichert.'))
return render(
request,
'workflows/company_config.html',
{
'form': PortalCompanyConfigForm(instance=company_config),
'company_config': company_config,
},
)
@_require_capability('manage_users') @_require_capability('manage_users')
@require_POST @require_POST
def create_user_from_admin(request): def create_user_from_admin(request):
@@ -838,7 +896,7 @@ def delete_backup_from_admin(request, backup_name: str):
return redirect('backup_recovery_page') return redirect('backup_recovery_page')
@_require_capability('access_requests_dashboard') @_require_capability('view_request_timeline')
def request_timeline_page(request, kind: str, request_id: int): def request_timeline_page(request, kind: str, request_id: int):
if kind == 'onboarding': if kind == 'onboarding':
obj = get_object_or_404(OnboardingRequest, id=request_id) obj = get_object_or_404(OnboardingRequest, id=request_id)