snapshot: preserve app registry and branding domain foundation
This commit is contained in:
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@ from django.conf import settings
|
||||
from django import forms
|
||||
|
||||
from .emailing import send_system_email
|
||||
from .models import AdminAuditLog, EmployeeProfile, FormFieldConfig, FormOption, IntroChecklistItem, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, PortalBranding, ScheduledWelcomeEmail, SystemEmailConfig, WorkflowConfig
|
||||
from .models import AdminAuditLog, EmployeeProfile, FormFieldConfig, FormOption, IntroChecklistItem, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, PortalAppConfig, PortalBranding, ScheduledWelcomeEmail, SystemEmailConfig, WorkflowConfig
|
||||
|
||||
|
||||
@admin.register(EmployeeProfile)
|
||||
@@ -25,6 +25,15 @@ class PortalBrandingAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'portal_title', 'company_name', 'support_email', 'default_language', 'updated_at')
|
||||
|
||||
|
||||
@admin.register(PortalAppConfig)
|
||||
class PortalAppConfigAdmin(admin.ModelAdmin):
|
||||
list_display = ('key', 'section', 'sort_order', 'is_enabled', 'updated_at')
|
||||
list_filter = ('section', 'is_enabled')
|
||||
search_fields = ('key', 'title_override', 'title_override_en')
|
||||
ordering = ('section', 'sort_order', 'key')
|
||||
list_editable = ('section', 'sort_order', 'is_enabled')
|
||||
|
||||
|
||||
@admin.register(OnboardingRequest)
|
||||
class OnboardingRequestAdmin(admin.ModelAdmin):
|
||||
list_display = ('id', 'full_name', 'work_email', 'department', 'contract_start', 'created_at')
|
||||
|
||||
255
backend/workflows/app_registry.py
Normal file
255
backend/workflows/app_registry.py
Normal file
@@ -0,0 +1,255 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .models import PortalAppConfig
|
||||
from .roles import user_has_capability
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AppDefinition:
|
||||
key: str
|
||||
section: str
|
||||
route_name: str
|
||||
title: object
|
||||
description: object
|
||||
action_label: object
|
||||
capability: str | None = None
|
||||
accent: str = ''
|
||||
accent_label: str = 'APP'
|
||||
style_variant: str = ''
|
||||
tags: tuple[object, ...] = ()
|
||||
|
||||
|
||||
APP_DEFINITIONS: tuple[AppDefinition, ...] = (
|
||||
AppDefinition(
|
||||
key='onboarding',
|
||||
section=PortalAppConfig.SECTION_APP,
|
||||
route_name='onboarding_create',
|
||||
title=_('Onboarding'),
|
||||
description=_('Neue Mitarbeitende erfassen, PDF mit Briefkopf erstellen, Benachrichtigungen senden und in Nextcloud ablegen.'),
|
||||
action_label=_('Onboarding starten'),
|
||||
accent='ON',
|
||||
tags=(_('Mehrschritt-Formular'), 'PDF', _('E-Mail Routing')),
|
||||
style_variant='primary',
|
||||
),
|
||||
AppDefinition(
|
||||
key='offboarding',
|
||||
section=PortalAppConfig.SECTION_APP,
|
||||
route_name='offboarding_create',
|
||||
title=_('Offboarding'),
|
||||
description=_('Mitarbeitende suchen, Daten vorbefüllen, Offboarding-Dokumente erzeugen und Rückgabe-Prozess starten.'),
|
||||
action_label=_('Offboarding starten'),
|
||||
accent='OFF',
|
||||
tags=(_('Profile-Suche'), _('Hardware-Liste'), _('IT-Rückgabe')),
|
||||
style_variant='red',
|
||||
),
|
||||
AppDefinition(
|
||||
key='requests_dashboard',
|
||||
section=PortalAppConfig.SECTION_APP,
|
||||
route_name='requests_dashboard',
|
||||
title=_('Anfragen Dashboard'),
|
||||
description=_('Status, Suchfunktion, PDF-Links und Verlauf aller Onboarding-/Offboarding-Anfragen.'),
|
||||
action_label=_('Dashboard öffnen'),
|
||||
capability='access_requests_dashboard',
|
||||
accent='APP',
|
||||
tags=(_('Suche'), _('Status'), _('PDF Zugriff')),
|
||||
),
|
||||
AppDefinition(
|
||||
key='branding',
|
||||
section=PortalAppConfig.SECTION_PLATFORM,
|
||||
route_name='portal_branding_page',
|
||||
title=_('Branding'),
|
||||
description=_('Logo, Portalname, Farben und PDF-Briefkopf verwalten.'),
|
||||
action_label=_('Öffnen'),
|
||||
capability='manage_product_branding',
|
||||
),
|
||||
AppDefinition(
|
||||
key='app_registry',
|
||||
section=PortalAppConfig.SECTION_PLATFORM,
|
||||
route_name='portal_app_registry_page',
|
||||
title=_('App Registry'),
|
||||
description=_('Apps zentral aktivieren, sortieren und für Kundenauftritte vorbereiten.'),
|
||||
action_label=_('Öffnen'),
|
||||
capability='manage_app_registry',
|
||||
),
|
||||
AppDefinition(
|
||||
key='integrations',
|
||||
section=PortalAppConfig.SECTION_ADMIN,
|
||||
route_name='integrations_setup_page',
|
||||
title=_('Integrationen'),
|
||||
description=_('Nextcloud- und E-Mail-Setup.'),
|
||||
action_label=_('Öffnen'),
|
||||
capability='manage_integrations',
|
||||
),
|
||||
AppDefinition(
|
||||
key='users',
|
||||
section=PortalAppConfig.SECTION_ADMIN,
|
||||
route_name='user_management_page',
|
||||
title=_('Benutzer & Rollen'),
|
||||
description=_('Benutzer anlegen, Rollen zuweisen und Zugriffe steuern.'),
|
||||
action_label=_('Öffnen'),
|
||||
capability='manage_users',
|
||||
),
|
||||
AppDefinition(
|
||||
key='audit_log',
|
||||
section=PortalAppConfig.SECTION_ADMIN,
|
||||
route_name='audit_log_page',
|
||||
title=_('Audit Log'),
|
||||
description=_('Wichtige Admin-Aktionen nachvollziehen und prüfen.'),
|
||||
action_label=_('Öffnen'),
|
||||
capability='view_audit_log',
|
||||
),
|
||||
AppDefinition(
|
||||
key='backups',
|
||||
section=PortalAppConfig.SECTION_ADMIN,
|
||||
route_name='backup_recovery_page',
|
||||
title=_('Backup & Recovery'),
|
||||
description=_('Backups erstellen und sicher verifizieren.'),
|
||||
action_label=_('Öffnen'),
|
||||
capability='manage_backups',
|
||||
),
|
||||
AppDefinition(
|
||||
key='welcome_emails',
|
||||
section=PortalAppConfig.SECTION_ADMIN,
|
||||
route_name='welcome_emails_page',
|
||||
title=_('Welcome E-Mails'),
|
||||
description=_('Geplante Welcome Mails verwalten.'),
|
||||
action_label=_('Öffnen'),
|
||||
capability='manage_welcome_emails',
|
||||
),
|
||||
AppDefinition(
|
||||
key='form_builder',
|
||||
section=PortalAppConfig.SECTION_ADMIN,
|
||||
route_name='form_builder_page',
|
||||
title=_('Form Builder'),
|
||||
description=_('Felder, Schritte und Optionen verwalten.'),
|
||||
action_label=_('Öffnen'),
|
||||
capability='manage_builders',
|
||||
),
|
||||
AppDefinition(
|
||||
key='intro_builder',
|
||||
section=PortalAppConfig.SECTION_ADMIN,
|
||||
route_name='intro_builder_page',
|
||||
title=_('Einweisungs-Builder'),
|
||||
description=_('Checklistenpunkte für das Einweisungsprotokoll konfigurieren.'),
|
||||
action_label=_('Öffnen'),
|
||||
capability='manage_builders',
|
||||
),
|
||||
AppDefinition(
|
||||
key='handbook',
|
||||
section=PortalAppConfig.SECTION_ADMIN,
|
||||
route_name='handbook_page',
|
||||
title=_('Handbook'),
|
||||
description=_('Project wiki and developer documentation in one place.'),
|
||||
action_label=_('Öffnen'),
|
||||
capability='view_docs',
|
||||
),
|
||||
AppDefinition(
|
||||
key='django_admin',
|
||||
section=PortalAppConfig.SECTION_ADMIN,
|
||||
route_name='admin:index',
|
||||
title=_('Django Admin'),
|
||||
description=_('Vollständige Datenverwaltung.'),
|
||||
action_label=_('Öffnen'),
|
||||
capability='access_django_admin_link',
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
SECTION_META = {
|
||||
PortalAppConfig.SECTION_APP: {
|
||||
'title': _('Apps'),
|
||||
'subtitle': _('Wählen Sie den gewünschten Prozess.'),
|
||||
'css_class': 'section-head-primary',
|
||||
'grid_class': 'apps-grid',
|
||||
},
|
||||
PortalAppConfig.SECTION_PLATFORM: {
|
||||
'title': _('Platform Apps'),
|
||||
'subtitle': _('Produktweite Konfiguration und Produktsteuerung.'),
|
||||
'css_class': 'section-head-platform',
|
||||
'grid_class': 'admin-grid',
|
||||
},
|
||||
PortalAppConfig.SECTION_ADMIN: {
|
||||
'title': _('Admin Apps'),
|
||||
'subtitle': _('Konfiguration, Tests und Steuerung.'),
|
||||
'css_class': 'section-head-admin',
|
||||
'grid_class': 'admin-grid',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def ensure_portal_app_configs() -> None:
|
||||
for index, definition in enumerate(APP_DEFINITIONS):
|
||||
PortalAppConfig.objects.get_or_create(
|
||||
key=definition.key,
|
||||
defaults={
|
||||
'section': definition.section,
|
||||
'sort_order': index,
|
||||
'is_enabled': True,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def get_portal_app_registry_rows() -> list[dict[str, object]]:
|
||||
ensure_portal_app_configs()
|
||||
config_map = {config.key: config for config in PortalAppConfig.objects.all()}
|
||||
rows: list[dict[str, object]] = []
|
||||
for index, definition in enumerate(APP_DEFINITIONS):
|
||||
config = config_map[definition.key]
|
||||
rows.append(
|
||||
{
|
||||
'definition': definition,
|
||||
'config': config,
|
||||
'default_section': definition.section,
|
||||
'default_sort_order': index,
|
||||
}
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def build_portal_app_sections(user) -> list[dict[str, object]]:
|
||||
ensure_portal_app_configs()
|
||||
config_map = {config.key: config for config in PortalAppConfig.objects.all()}
|
||||
grouped: dict[str, list[dict[str, object]]] = {key: [] for key in SECTION_META}
|
||||
|
||||
for definition in APP_DEFINITIONS:
|
||||
config = config_map.get(definition.key)
|
||||
if not config or not config.is_enabled:
|
||||
continue
|
||||
if definition.capability and not user_has_capability(user, definition.capability):
|
||||
continue
|
||||
grouped[config.section].append(
|
||||
{
|
||||
'key': definition.key,
|
||||
'href': reverse(definition.route_name),
|
||||
'title': config.translated_title_override() or str(definition.title),
|
||||
'description': config.translated_description_override() or str(definition.description),
|
||||
'action_label': config.translated_action_label_override() or str(definition.action_label),
|
||||
'accent': definition.accent,
|
||||
'accent_label': definition.accent_label,
|
||||
'style_variant': definition.style_variant,
|
||||
'tags': [str(tag) for tag in definition.tags],
|
||||
'sort_order': config.sort_order,
|
||||
}
|
||||
)
|
||||
|
||||
sections: list[dict[str, object]] = []
|
||||
for section_key, meta in SECTION_META.items():
|
||||
apps = sorted(grouped.get(section_key, []), key=lambda item: (item['sort_order'], item['title']))
|
||||
if not apps:
|
||||
continue
|
||||
sections.append(
|
||||
{
|
||||
'key': section_key,
|
||||
'title': str(meta['title']),
|
||||
'subtitle': str(meta['subtitle']),
|
||||
'css_class': meta['css_class'],
|
||||
'grid_class': meta['grid_class'],
|
||||
'apps': apps,
|
||||
}
|
||||
)
|
||||
return sections
|
||||
@@ -14,6 +14,7 @@ def get_portal_branding() -> PortalBranding:
|
||||
defaults={
|
||||
'portal_title': 'TUBCO Onboarding & Offboarding Portal',
|
||||
'company_name': 'TUBCO',
|
||||
'company_domain': 'tub.co',
|
||||
'support_email': 'info@tub.co',
|
||||
'default_language': 'de',
|
||||
'primary_color': '#000078',
|
||||
@@ -23,6 +24,12 @@ def get_portal_branding() -> PortalBranding:
|
||||
return branding
|
||||
|
||||
|
||||
def get_company_email_domain() -> str:
|
||||
branding = get_portal_branding()
|
||||
domain = (branding.company_domain or '').strip().lower().lstrip('@')
|
||||
return domain or 'tub.co'
|
||||
|
||||
|
||||
def get_portal_logo_url() -> str:
|
||||
branding = get_portal_branding()
|
||||
if branding.logo_image:
|
||||
@@ -51,6 +58,7 @@ def get_branding_context() -> dict[str, object]:
|
||||
'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_default_language': branding.default_language,
|
||||
'portal_primary_color': branding.primary_color,
|
||||
@@ -67,6 +75,7 @@ def get_branding_email_copy() -> dict[str, str]:
|
||||
portal_title = (branding.portal_title or f'{company_name} Portal').strip()
|
||||
return {
|
||||
'company_name': company_name,
|
||||
'company_domain': get_company_email_domain(),
|
||||
'portal_title': portal_title,
|
||||
'support_email': (branding.support_email or '').strip(),
|
||||
}
|
||||
@@ -78,7 +87,9 @@ def get_default_notification_templates() -> dict[str, dict[str, str]]:
|
||||
from .tasks import DEFAULT_NOTIFICATION_TEMPLATES
|
||||
|
||||
templates = deepcopy(DEFAULT_NOTIFICATION_TEMPLATES)
|
||||
company_name = get_branding_email_copy()['company_name']
|
||||
branding_copy = get_branding_email_copy()
|
||||
company_name = branding_copy['company_name']
|
||||
support_email = branding_copy['support_email'] or f"it@{branding_copy['company_domain']}"
|
||||
welcome = templates.get('onboarding_welcome')
|
||||
if welcome:
|
||||
welcome['subject'] = f'Willkommen bei {company_name}, {{ VORNAME }}'
|
||||
@@ -89,7 +100,7 @@ def get_default_notification_templates() -> dict[str, dict[str, str]]:
|
||||
'Wir freuen uns sehr, dass du ab dem {{ CONTRACT_START }} unser Team in der Abteilung {{ DEPARTMENT }} verstärkst.\n\n'
|
||||
'Deine dienstliche E-Mail-Adresse lautet: {{ EMAIL }}.\n'
|
||||
'Im Anhang findest du deine Onboarding-Unterlagen als PDF.\n\n'
|
||||
'Wenn du Fragen hast, melde dich gerne jederzeit.\n\n'
|
||||
f'Wenn du Fragen hast, melde dich gerne jederzeit unter {support_email}.\n\n'
|
||||
'Viele Grüße\n'
|
||||
f'{company_name} IT'
|
||||
)
|
||||
@@ -99,7 +110,7 @@ def get_default_notification_templates() -> dict[str, dict[str, str]]:
|
||||
'We are very happy that you will join our {{ DEPARTMENT }} team starting on {{ CONTRACT_START }}.\n\n'
|
||||
'Your work email address is: {{ EMAIL }}.\n'
|
||||
'You will find your onboarding documents attached as a PDF.\n\n'
|
||||
'If you have any questions, feel free to contact us anytime.\n\n'
|
||||
f'If you have any questions, feel free to contact {support_email}.\n\n'
|
||||
'Best regards,\n'
|
||||
f'{company_name} IT'
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ from django.contrib.auth.forms import AuthenticationForm, PasswordResetForm, Set
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import get_language, gettext as _, gettext_lazy
|
||||
|
||||
from .branding import get_company_email_domain
|
||||
from .form_builder import apply_form_field_config
|
||||
from .models import EmployeeProfile, FormOption, OffboardingRequest, OnboardingRequest, PortalBranding, 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
|
||||
@@ -175,6 +176,7 @@ class PortalBrandingForm(forms.ModelForm):
|
||||
fields = [
|
||||
'portal_title',
|
||||
'company_name',
|
||||
'company_domain',
|
||||
'support_email',
|
||||
'default_language',
|
||||
'logo_image',
|
||||
@@ -185,6 +187,7 @@ class PortalBrandingForm(forms.ModelForm):
|
||||
labels = {
|
||||
'portal_title': gettext_lazy('Portal-Titel'),
|
||||
'company_name': gettext_lazy('Firmenname'),
|
||||
'company_domain': gettext_lazy('Firmen-Domain'),
|
||||
'support_email': gettext_lazy('Support-E-Mail'),
|
||||
'default_language': gettext_lazy('Standardsprache'),
|
||||
'logo_image': gettext_lazy('Logo'),
|
||||
@@ -343,6 +346,7 @@ class OnboardingRequestForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.requester_email = (kwargs.pop('requester_email', '') or '').strip().lower()
|
||||
super().__init__(*args, **kwargs)
|
||||
self.email_domain = get_company_email_domain()
|
||||
|
||||
config = WorkflowConfig.objects.order_by('id').first()
|
||||
self.handover_lead_days = max(0, int(getattr(config, 'device_handover_lead_days', 5) or 5))
|
||||
@@ -350,6 +354,7 @@ class OnboardingRequestForm(forms.ModelForm):
|
||||
self.fields['handover_date'].widget.attrs['min'] = minimum_handover_date.isoformat()
|
||||
|
||||
self.fields['full_name'].label = 'Name'
|
||||
self.fields['work_email'].help_text = _('Bitte nutzen Sie das Format name@%(domain)s.') % {'domain': self.email_domain}
|
||||
full_name_initial = (self.initial.get('full_name') or '').strip()
|
||||
if full_name_initial and not self.initial.get('first_name') and not self.initial.get('last_name'):
|
||||
name_parts = full_name_initial.split()
|
||||
@@ -369,8 +374,9 @@ class OnboardingRequestForm(forms.ModelForm):
|
||||
value = (self.cleaned_data.get('work_email') or '').strip().lower()
|
||||
if not value:
|
||||
return value
|
||||
if not value.endswith('@tub.co'):
|
||||
raise forms.ValidationError('Bitte verwenden Sie eine @tub.co E-Mail-Adresse.')
|
||||
expected_suffix = f'@{self.email_domain}'
|
||||
if self.email_domain and not value.endswith(expected_suffix):
|
||||
raise forms.ValidationError(_('Bitte verwenden Sie eine @%(domain)s E-Mail-Adresse.') % {'domain': self.email_domain})
|
||||
return value
|
||||
|
||||
def clean_signature_image(self):
|
||||
@@ -531,11 +537,21 @@ class OffboardingRequestForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
prefill_profile = kwargs.pop('prefill_profile', None)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.email_domain = get_company_email_domain()
|
||||
self.fields['full_name'].label = 'Vorname und Nachname'
|
||||
self.fields['work_email'].help_text = ''
|
||||
self.fields['work_email'].help_text = _('Bitte nutzen Sie das Format name@%(domain)s.') % {'domain': self.email_domain}
|
||||
if prefill_profile:
|
||||
self.fields['full_name'].initial = prefill_profile.full_name
|
||||
self.fields['work_email'].initial = prefill_profile.work_email
|
||||
self.fields['department'].initial = prefill_profile.department
|
||||
self.fields['job_title'].initial = prefill_profile.job_title
|
||||
apply_form_field_config('offboarding', self)
|
||||
|
||||
def clean_work_email(self):
|
||||
value = (self.cleaned_data.get('work_email') or '').strip().lower()
|
||||
if not value:
|
||||
return value
|
||||
expected_suffix = f'@{self.email_domain}'
|
||||
if self.email_domain and not value.endswith(expected_suffix):
|
||||
raise forms.ValidationError(_('Bitte verwenden Sie eine @%(domain)s E-Mail-Adresse.') % {'domain': self.email_domain})
|
||||
return value
|
||||
|
||||
35
backend/workflows/migrations/0038_portalappconfig.py
Normal file
35
backend/workflows/migrations/0038_portalappconfig.py
Normal file
@@ -0,0 +1,35 @@
|
||||
# Generated by Django 5.1.5 on 2026-03-26 10:48
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('workflows', '0037_alter_portalbranding_logo_image_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PortalAppConfig',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('key', models.CharField(max_length=80, unique=True)),
|
||||
('section', models.CharField(choices=[('app', 'Apps'), ('platform', 'Platform Apps'), ('admin', 'Admin Apps')], default='app', max_length=20)),
|
||||
('sort_order', models.PositiveIntegerField(default=0)),
|
||||
('is_enabled', models.BooleanField(default=True)),
|
||||
('title_override', models.CharField(blank=True, max_length=255)),
|
||||
('title_override_en', models.CharField(blank=True, max_length=255)),
|
||||
('description_override', models.TextField(blank=True)),
|
||||
('description_override_en', models.TextField(blank=True)),
|
||||
('action_label_override', models.CharField(blank=True, max_length=255)),
|
||||
('action_label_override_en', models.CharField(blank=True, max_length=255)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Portal App',
|
||||
'verbose_name_plural': 'Portal Apps',
|
||||
'ordering': ['section', 'sort_order', 'key'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.5 on 2026-03-26 10:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('workflows', '0038_portalappconfig'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='portalbranding',
|
||||
name='company_domain',
|
||||
field=models.CharField(blank=True, default='tub.co', max_length=120),
|
||||
),
|
||||
]
|
||||
@@ -29,6 +29,7 @@ class PortalBranding(models.Model):
|
||||
name = models.CharField(max_length=80, default='Default', unique=True)
|
||||
portal_title = models.CharField(max_length=255, default='TUBCO Onboarding & Offboarding Portal')
|
||||
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')
|
||||
default_language = models.CharField(
|
||||
max_length=10,
|
||||
@@ -59,6 +60,54 @@ class PortalBranding(models.Model):
|
||||
return self.portal_title or self.company_name or self.name
|
||||
|
||||
|
||||
class PortalAppConfig(models.Model):
|
||||
SECTION_APP = 'app'
|
||||
SECTION_PLATFORM = 'platform'
|
||||
SECTION_ADMIN = 'admin'
|
||||
SECTION_CHOICES = [
|
||||
(SECTION_APP, _('Apps')),
|
||||
(SECTION_PLATFORM, _('Platform Apps')),
|
||||
(SECTION_ADMIN, _('Admin Apps')),
|
||||
]
|
||||
|
||||
key = models.CharField(max_length=80, unique=True)
|
||||
section = models.CharField(max_length=20, choices=SECTION_CHOICES, default=SECTION_APP)
|
||||
sort_order = models.PositiveIntegerField(default=0)
|
||||
is_enabled = models.BooleanField(default=True)
|
||||
title_override = models.CharField(max_length=255, blank=True)
|
||||
title_override_en = models.CharField(max_length=255, blank=True)
|
||||
description_override = models.TextField(blank=True)
|
||||
description_override_en = models.TextField(blank=True)
|
||||
action_label_override = models.CharField(max_length=255, blank=True)
|
||||
action_label_override_en = models.CharField(max_length=255, blank=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['section', 'sort_order', 'key']
|
||||
verbose_name = 'Portal App'
|
||||
verbose_name_plural = 'Portal Apps'
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.key
|
||||
|
||||
def _translated_value(self, field_name: str, language_code: str | None = None) -> str:
|
||||
lang = (language_code or get_language() or 'de').split('-')[0]
|
||||
if lang == 'en':
|
||||
english_value = (getattr(self, f'{field_name}_en', '') or '').strip()
|
||||
if english_value:
|
||||
return english_value
|
||||
return (getattr(self, field_name, '') or '').strip()
|
||||
|
||||
def translated_title_override(self, language_code: str | None = None) -> str:
|
||||
return self._translated_value('title_override', language_code)
|
||||
|
||||
def translated_description_override(self, language_code: str | None = None) -> str:
|
||||
return self._translated_value('description_override', language_code)
|
||||
|
||||
def translated_action_label_override(self, language_code: str | None = None) -> str:
|
||||
return self._translated_value('action_label_override', language_code)
|
||||
|
||||
|
||||
class AdminAuditLog(models.Model):
|
||||
actor = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
|
||||
@@ -29,6 +29,7 @@ ROLE_LABELS = {
|
||||
CAPABILITIES = {
|
||||
'manage_users': {ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN},
|
||||
'manage_product_branding': {ROLE_PLATFORM_OWNER},
|
||||
'manage_app_registry': {ROLE_PLATFORM_OWNER},
|
||||
'access_requests_dashboard': {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},
|
||||
@@ -123,6 +124,7 @@ def template_role_context(user) -> dict[str, object]:
|
||||
'role_key': role_key,
|
||||
'role_label': str(ROLE_LABELS[role_key]),
|
||||
'can_manage_product_branding': user_has_capability(user, 'manage_product_branding'),
|
||||
'can_manage_app_registry': user_has_capability(user, 'manage_app_registry'),
|
||||
'can_manage_users': user_has_capability(user, 'manage_users'),
|
||||
'can_access_requests_dashboard': user_has_capability(user, 'access_requests_dashboard'),
|
||||
'can_run_intro_session': user_has_capability(user, 'run_intro_session'),
|
||||
|
||||
@@ -35,6 +35,12 @@ textarea { min-height: 120px; font-family: ui-monospace, SFMono-Regular, Menlo,
|
||||
table { width: 100%; border-collapse: collapse; font-size: 14px; }
|
||||
th, td { border: 1px solid #dce5f1; padding: 8px; text-align: left; vertical-align: top; }
|
||||
th { background: #f6f9ff; color: #334155; }
|
||||
.app-registry-wrap textarea { min-height: 86px; }
|
||||
.app-registry-table input[type="text"],
|
||||
.app-registry-table input[type="number"],
|
||||
.app-registry-table select,
|
||||
.app-registry-table textarea { min-width: 160px; }
|
||||
.app-registry-table td { background: rgba(255,255,255,0.9); }
|
||||
.template-block { border: 1px solid #d8e3f0; border-radius: 10px; background: #fff; padding: 10px; margin-top: 10px; }
|
||||
.template-title, .rule-title { margin: 0 0 8px; color: #24344e; font-weight: 700; font-size: 14px; }
|
||||
.rule-card { margin-top: 12px; border: 1px solid #d8e3f0; border-radius: 12px; padding: 10px; background: #fff; }
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
|
||||
const fullName = byName('full_name');
|
||||
const workEmail = byName('work_email');
|
||||
const form = fullName ? fullName.closest('form') : null;
|
||||
const emailDomain = (((form && form.dataset.emailDomain) || 'tub.co') + '').replace(/^@+/, '').trim();
|
||||
if (!fullName || !workEmail) return;
|
||||
|
||||
let lastSuggested = '';
|
||||
@@ -31,7 +33,7 @@
|
||||
const lastName = extractLastName(fullName.value);
|
||||
const slug = slugifyForEmail(lastName);
|
||||
if (!slug) return;
|
||||
const suggestion = slug + '@tub.co';
|
||||
const suggestion = slug + '@' + emailDomain;
|
||||
const current = (workEmail.value || '').trim();
|
||||
if (!userEditedEmail || current === '' || current === lastSuggested) {
|
||||
workEmail.value = suggestion;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
const btnNext = document.getElementById('btn-next');
|
||||
const btnSubmit = document.getElementById('btn-submit');
|
||||
const form = document.getElementById('onboarding-form');
|
||||
const emailDomain = ((form && form.dataset.emailDomain) || 'tub.co').replace(/^@+/, '').trim();
|
||||
let current = 0;
|
||||
form.setAttribute('novalidate', 'novalidate');
|
||||
|
||||
@@ -82,7 +83,7 @@
|
||||
function suggestEmail() {
|
||||
const slug = slugifyForEmail(lastName.value);
|
||||
if (!slug) return;
|
||||
const suggestion = slug + '@tub.co';
|
||||
const suggestion = slug + '@' + emailDomain;
|
||||
if (!userEditedEmail || workEmail.value === '' || workEmail.value === lastSuggested) {
|
||||
workEmail.value = suggestion;
|
||||
lastSuggested = suggestion;
|
||||
|
||||
@@ -101,7 +101,7 @@ DEFAULT_NOTIFICATION_TEMPLATES = {
|
||||
'Vertragsbeginn: {{ CONTRACT_START }}\n'
|
||||
'E-Mail-Adresse: {{ EMAIL }}\n\n'
|
||||
'{% if PDF_LINK %}In 2 Minuten findest du alle Infos über den Mitarbeiter als PDF unter diesem Link: {{ PDF_LINK }}\n\n{% endif %}'
|
||||
'Falls du noch irgendwelche anderen Informationen benötigen solltest, kannst du dich bei der it@tub.co melden!\n\n'
|
||||
'Falls du noch irgendwelche anderen Informationen benötigen solltest, kannst du dich bei {{ SUPPORT_EMAIL }} melden!\n\n'
|
||||
'Vielen Dank und schöne Grüße,\n'
|
||||
'Die IT.'
|
||||
),
|
||||
@@ -114,7 +114,7 @@ DEFAULT_NOTIFICATION_TEMPLATES = {
|
||||
'Contract start: {{ CONTRACT_START }}\n'
|
||||
'Email address: {{ EMAIL }}\n\n'
|
||||
'{% if PDF_LINK %}You will find the employee PDF here in about 2 minutes: {{ PDF_LINK }}\n\n{% endif %}'
|
||||
'If you need any other information, please contact it@tub.co.\n\n'
|
||||
'If you need any other information, please contact {{ SUPPORT_EMAIL }}.\n\n'
|
||||
'Thank you and best regards,\n'
|
||||
'IT'
|
||||
),
|
||||
@@ -1176,6 +1176,7 @@ def process_onboarding_request(onboarding_request_id: int) -> None:
|
||||
request_obj.last_error = ''
|
||||
request_obj.save(update_fields=['processing_status', 'last_error'])
|
||||
try:
|
||||
branding_copy = get_branding_email_copy()
|
||||
it_email, general_info_email, business_card_email, hr_works_email, key_email = _resolve_workflow_emails()
|
||||
salutation = (request_obj.get_gender_display() or '').strip()
|
||||
display_name = f"{salutation} {request_obj.full_name}".strip()
|
||||
@@ -1204,6 +1205,7 @@ def process_onboarding_request(onboarding_request_id: int) -> None:
|
||||
'CONTRACT_START': request_obj.contract_start,
|
||||
'EMAIL': request_obj.work_email,
|
||||
'REQUESTED_BY': request_obj.onboarded_by_email or '-',
|
||||
'SUPPORT_EMAIL': branding_copy['support_email'] or f"it@{branding_copy['company_domain']}",
|
||||
'BUSINESS_CARD_NAME': request_obj.business_card_name or display_name,
|
||||
'BUSINESS_CARD_TITLE': request_obj.business_card_title or '-',
|
||||
'BUSINESS_CARD_EMAIL': request_obj.business_card_email or request_obj.work_email,
|
||||
@@ -1285,6 +1287,7 @@ def process_offboarding_request(offboarding_request_id: int) -> None:
|
||||
request_obj.last_error = ''
|
||||
request_obj.save(update_fields=['processing_status', 'last_error'])
|
||||
try:
|
||||
branding_copy = get_branding_email_copy()
|
||||
it_email, general_info_email, _, hr_works_email, _ = _resolve_workflow_emails()
|
||||
|
||||
pdf_path = _generate_offboarding_pdf(request_obj)
|
||||
@@ -1297,6 +1300,7 @@ def process_offboarding_request(offboarding_request_id: int) -> None:
|
||||
'LAST_WORKING_DAY': request_obj.last_working_day,
|
||||
'REQUESTED_BY': request_obj.requested_by_email,
|
||||
'EMAIL': request_obj.work_email,
|
||||
'SUPPORT_EMAIL': branding_copy['support_email'] or f"it@{branding_copy['company_domain']}",
|
||||
}
|
||||
|
||||
_send_templated_email(
|
||||
|
||||
83
backend/workflows/templates/workflows/app_registry.html
Normal file
83
backend/workflows/templates/workflows/app_registry.html
Normal file
@@ -0,0 +1,83 @@
|
||||
{% extends 'workflows/base_shell.html' %}
|
||||
{% load static i18n %}
|
||||
|
||||
{% block title %}{% trans "App Registry" %}{% 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 "App Registry" %}</h1>
|
||||
<p class="sub">{% trans "Apps zentral steuern, für Kunden vorbereiten und ohne Template-Eingriffe auf der Landing Page ausspielen." %}</p>
|
||||
|
||||
{% include 'workflows/includes/messages.html' %}
|
||||
|
||||
<section class="card">
|
||||
<div class="toolbar">
|
||||
<div class="hint">{% trans "Sicherheit bleibt codebasiert: Sichtbarkeit und Reihenfolge sind hier steuerbar, Berechtigungen weiterhin über Rollen und Capabilities." %}</div>
|
||||
<span class="badge scheduled">{% trans "Produktkern" %}</span>
|
||||
</div>
|
||||
<form method="post" action="{% url 'save_portal_app_registry' %}" class="stack-form">
|
||||
{% csrf_token %}
|
||||
<div class="table-wrap app-registry-wrap">
|
||||
<table class="app-registry-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Key" %}</th>
|
||||
<th>{% trans "Aktiv" %}</th>
|
||||
<th>{% trans "Bereich" %}</th>
|
||||
<th>{% trans "Reihenfolge" %}</th>
|
||||
<th>{% trans "Titel DE" %}</th>
|
||||
<th>{% trans "Titel EN" %}</th>
|
||||
<th>{% trans "Aktion DE" %}</th>
|
||||
<th>{% trans "Aktion EN" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in rows %}
|
||||
<tr>
|
||||
<td>
|
||||
<div><strong>{{ row.config.key }}</strong></div>
|
||||
<div class="mini">{{ row.definition.title }}</div>
|
||||
</td>
|
||||
<td class="select-col">
|
||||
<input type="checkbox" name="is_enabled__{{ row.config.key }}" {% if row.config.is_enabled %}checked{% endif %} />
|
||||
</td>
|
||||
<td>
|
||||
<select name="section__{{ row.config.key }}">
|
||||
{% for value, label in section_choices %}
|
||||
<option value="{{ value }}"{% if row.config.section == value %} selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" name="sort_order__{{ row.config.key }}" value="{{ row.config.sort_order }}" min="0" step="1" />
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" name="title_override__{{ row.config.key }}" value="{{ row.config.title_override }}" placeholder="{{ row.definition.title }}" />
|
||||
<textarea name="description_override__{{ row.config.key }}" rows="3" placeholder="{{ row.definition.description }}">{{ row.config.description_override }}</textarea>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" name="title_override_en__{{ row.config.key }}" value="{{ row.config.title_override_en }}" placeholder="{{ row.definition.title }}" />
|
||||
<textarea name="description_override_en__{{ row.config.key }}" rows="3" placeholder="{{ row.definition.description }}">{{ row.config.description_override_en }}</textarea>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" name="action_label_override__{{ row.config.key }}" value="{{ row.config.action_label_override }}" placeholder="{{ row.definition.action_label }}" />
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" name="action_label_override_en__{{ row.config.key }}" value="{{ row.config.action_label_override_en }}" placeholder="{{ row.definition.action_label }}" />
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="toolbar" style="margin-top:1rem;">
|
||||
<div class="hint">{% trans "Empfehlung: Produktweite Apps sparsam halten, kundenbezogene Prozesse unter Apps oder Admin Apps einordnen." %}</div>
|
||||
<button class="btn btn-primary" type="submit">{% trans "App Registry speichern" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -26,6 +26,11 @@
|
||||
<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 }}
|
||||
|
||||
@@ -102,12 +102,13 @@ docker compose exec -T web python manage.py check</code></pre>
|
||||
|
||||
<h3>Role and Permission Model</h3>
|
||||
<ul>
|
||||
<li>Stable Django group names: <code>Super Admin</code>, <code>Admin</code>, <code>IT Staff</code>, <code>Staff</code>.</li>
|
||||
<li>Stable Django group names: <code>Platform Owner</code>, <code>Super Admin</code>, <code>Admin</code>, <code>IT Staff</code>, <code>Staff</code>.</li>
|
||||
<li>Groups are created automatically through a <code>post_migrate</code> hook in <code>workflows.signals</code>.</li>
|
||||
<li>Capability checks are centralized in <code>workflows.roles.CAPABILITIES</code>.</li>
|
||||
<li>Use <code>_require_capability(...)</code> in views instead of flat <code>is_staff</code> checks.</li>
|
||||
<li>Templates receive permission flags from <code>workflows.context_processors.role_context</code>.</li>
|
||||
<li>Super-admin-only user management lives at <code>/admin-tools/users/</code> and is the preferred path for normal role assignment, account activation, invitation mail dispatch, password-reset mail dispatch, and controlled user deletion.</li>
|
||||
<li><code>Platform Owner</code> is the product-level role. Company roles remain <code>Super Admin</code>, <code>Admin</code>, <code>IT Staff</code>, and <code>Staff</code>.</li>
|
||||
<li>User management lives at <code>/admin-tools/users/</code> and is the preferred path for normal role assignment, account activation, invitation mail dispatch, password-reset mail dispatch, and controlled user deletion.</li>
|
||||
<li>Backward-compatibility rule: authenticated legacy users with <code>is_staff=True</code> but no explicit role group currently fall back to the <code>Admin</code> capability set.</li>
|
||||
<li><code>superuser</code> accounts resolve to <code>Super Admin</code>.</li>
|
||||
<li>When adding a new operational page or action, define the capability in <code>roles.py</code>, gate the view, and hide the UI affordance when the capability is absent.</li>
|
||||
@@ -180,6 +181,15 @@ docker compose exec -T web django-admin compilemessages</code></pre>
|
||||
<li>User invitation emails and welcome-template fallbacks also use the configured branding defaults.</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="app-registry">10b) App Registry</h2>
|
||||
<ul>
|
||||
<li>Registry definitions live in <code>workflows/app_registry.py</code>.</li>
|
||||
<li>DB overrides live in <code>PortalAppConfig</code>.</li>
|
||||
<li>The landing page now renders from registry data instead of hardcoded cards.</li>
|
||||
<li>Security remains code-based: app visibility/order is configurable, but access still depends on role capabilities in <code>roles.py</code>.</li>
|
||||
<li>Management UI: <code>/admin-tools/apps/</code> for <code>Platform Owner</code>.</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="builders">11) Builder Architecture</h2>
|
||||
<h3>Form Builder</h3>
|
||||
<ul>
|
||||
|
||||
@@ -51,147 +51,44 @@
|
||||
<main class="main">
|
||||
{% include 'workflows/includes/messages.html' %}
|
||||
|
||||
<div class="section-head section-head-primary">
|
||||
<h2>{% trans "Apps" %}</h2>
|
||||
<p>{% trans "Wählen Sie den gewünschten Prozess." %}</p>
|
||||
</div>
|
||||
<div class="apps-grid">
|
||||
<section class="app-card primary">
|
||||
<div>
|
||||
<div class="top-line"><div class="accent">ON</div></div>
|
||||
<h3 class="app-title">{% trans "Onboarding" %}</h3>
|
||||
<p class="app-text">{% trans "Neue Mitarbeitende erfassen, PDF mit Briefkopf erstellen, Benachrichtigungen senden und in Nextcloud ablegen." %}</p>
|
||||
<div class="tag-row">
|
||||
<span class="tag">{% trans "Mehrschritt-Formular" %}</span>
|
||||
<span class="tag">PDF</span>
|
||||
<span class="tag">{% trans "E-Mail Routing" %}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<a class="btn btn-primary" href="/onboarding/new/">{% trans "Onboarding starten" %}</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="app-card red">
|
||||
<div>
|
||||
<div class="top-line"><div class="accent red">OFF</div></div>
|
||||
<h3 class="app-title">{% trans "Offboarding" %}</h3>
|
||||
<p class="app-text">{% trans "Mitarbeitende suchen, Daten vorbefüllen, Offboarding-Dokumente erzeugen und Rückgabe-Prozess starten." %}</p>
|
||||
<div class="tag-row">
|
||||
<span class="tag">{% trans "Profile-Suche" %}</span>
|
||||
<span class="tag">{% trans "Hardware-Liste" %}</span>
|
||||
<span class="tag">{% trans "IT-Rückgabe" %}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<a class="btn btn-primary" href="/offboarding/new/">{% trans "Offboarding starten" %}</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% if can_access_requests_dashboard %}
|
||||
<section class="app-card">
|
||||
<div>
|
||||
<div class="top-line"><div class="accent">APP</div></div>
|
||||
<h3 class="app-title">{% trans "Anfragen Dashboard" %}</h3>
|
||||
<p class="app-text">{% trans "Status, Suchfunktion, PDF-Links und Verlauf aller Onboarding-/Offboarding-Anfragen." %}</p>
|
||||
<div class="tag-row">
|
||||
<span class="tag">{% trans "Suche" %}</span>
|
||||
<span class="tag">{% trans "Status" %}</span>
|
||||
<span class="tag">{% trans "PDF Zugriff" %}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<a class="btn btn-secondary" href="/requests/">{% trans "Dashboard öffnen" %}</a>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if can_manage_product_branding %}
|
||||
{% for section in portal_app_sections %}
|
||||
{% if not forloop.first %}
|
||||
<div class="section-divider" aria-hidden="true"></div>
|
||||
<div class="section-head section-head-platform">
|
||||
<h2>{% trans "Platform Apps" %}</h2>
|
||||
<p>{% trans "Produktweite Konfiguration und Produktsteuerung." %}</p>
|
||||
</div>
|
||||
<div class="admin-grid">
|
||||
<section class="admin-card">
|
||||
<h3>{% trans "Branding" %}</h3>
|
||||
<p>{% trans "Logo, Portalname, Farben und PDF-Briefkopf verwalten." %}</p>
|
||||
<a class="btn btn-secondary" href="/admin-tools/branding/">{% trans "Öffnen" %}</a>
|
||||
</section>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if can_manage_users or can_manage_integrations or can_view_audit_log or can_manage_backups or can_manage_welcome_emails or can_manage_builders or can_view_docs or can_access_django_admin_link %}
|
||||
<div class="section-divider" aria-hidden="true"></div>
|
||||
<div class="section-head section-head-admin">
|
||||
<h2>{% trans "Admin Apps" %}</h2>
|
||||
<p>{% trans "Konfiguration, Tests und Steuerung." %}</p>
|
||||
<div class="section-head {{ section.css_class }}">
|
||||
<h2>{{ section.title }}</h2>
|
||||
<p>{{ section.subtitle }}</p>
|
||||
</div>
|
||||
<div class="admin-grid">
|
||||
{% if can_manage_integrations %}
|
||||
<div class="{{ section.grid_class }}">
|
||||
{% for app in section.apps %}
|
||||
{% if section.key == 'app' %}
|
||||
<section class="app-card{% if app.style_variant %} {{ app.style_variant }}{% endif %}">
|
||||
<div>
|
||||
<div class="top-line"><div class="accent{% if app.style_variant == 'red' %} red{% endif %}">{{ app.accent }}</div></div>
|
||||
<h3 class="app-title">{{ app.title }}</h3>
|
||||
<p class="app-text">{{ app.description }}</p>
|
||||
{% if app.tags %}
|
||||
<div class="tag-row">
|
||||
{% for tag in app.tags %}
|
||||
<span class="tag">{{ tag }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<a class="btn {% if app.style_variant == 'primary' or app.style_variant == 'red' %}btn-primary{% else %}btn-secondary{% endif %}" href="{{ app.href }}">{{ app.action_label }}</a>
|
||||
</div>
|
||||
</section>
|
||||
{% else %}
|
||||
<section class="admin-card">
|
||||
<h3>{% trans "Integrationen" %}</h3>
|
||||
<p>{% trans "Nextcloud- und E-Mail-Setup." %}</p>
|
||||
<a class="btn btn-secondary" href="/admin-tools/integrations/?kind=nextcloud">{% trans "Öffnen" %}</a>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% if can_manage_users %}
|
||||
<section class="admin-card">
|
||||
<h3>{% trans "Benutzer & Rollen" %}</h3>
|
||||
<p>{% trans "Benutzer anlegen, Rollen zuweisen und Zugriffe steuern." %}</p>
|
||||
<a class="btn btn-secondary" href="/admin-tools/users/">{% trans "Öffnen" %}</a>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% if can_view_audit_log %}
|
||||
<section class="admin-card">
|
||||
<h3>{% trans "Audit Log" %}</h3>
|
||||
<p>{% trans "Wichtige Admin-Aktionen nachvollziehen und prüfen." %}</p>
|
||||
<a class="btn btn-secondary" href="/admin-tools/audit-log/">{% trans "Öffnen" %}</a>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% if can_manage_backups %}
|
||||
<section class="admin-card">
|
||||
<h3>{% trans "Backup & Recovery" %}</h3>
|
||||
<p>{% trans "Backups erstellen und sicher verifizieren." %}</p>
|
||||
<a class="btn btn-secondary" href="/admin-tools/backups/">{% trans "Öffnen" %}</a>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% if can_manage_welcome_emails %}
|
||||
<section class="admin-card">
|
||||
<h3>{% trans "Welcome E-Mails" %}</h3>
|
||||
<p>{% trans "Geplante Welcome Mails verwalten." %}</p>
|
||||
<a class="btn btn-secondary" href="/admin-tools/welcome-emails/">{% trans "Öffnen" %}</a>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% if can_manage_builders %}
|
||||
<section class="admin-card">
|
||||
<h3>{% trans "Form Builder" %}</h3>
|
||||
<p>{% trans "Felder, Schritte und Optionen verwalten." %}</p>
|
||||
<a class="btn btn-secondary" href="/admin-tools/form-builder/">{% trans "Öffnen" %}</a>
|
||||
</section>
|
||||
<section class="admin-card">
|
||||
<h3>{% trans "Einweisungs-Builder" %}</h3>
|
||||
<p>{% trans "Checklistenpunkte für das Einweisungsprotokoll konfigurieren." %}</p>
|
||||
<a class="btn btn-secondary" href="/admin-tools/intro-builder/">{% trans "Öffnen" %}</a>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% if can_view_docs %}
|
||||
<section class="admin-card">
|
||||
<h3>{% trans "Handbook" %}</h3>
|
||||
<p>{% trans "Project wiki and developer documentation in one place." %}</p>
|
||||
<a class="btn btn-secondary" href="/admin-tools/handbook/">{% trans "Öffnen" %}</a>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% if can_access_django_admin_link %}
|
||||
<section class="admin-card">
|
||||
<h3>{% trans "Django Admin" %}</h3>
|
||||
<p>{% trans "Vollständige Datenverwaltung." %}</p>
|
||||
<a class="btn btn-secondary" href="/admin/">{% trans "Öffnen" %}</a>
|
||||
<h3>{{ app.title }}</h3>
|
||||
<p>{{ app.description }}</p>
|
||||
<a class="btn btn-secondary" href="{{ app.href }}">{{ app.action_label }}</a>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
<div class="footer-note">
|
||||
{% trans "Tipp: Die letzten Vorgänge sehen Sie jederzeit im Anfragen Dashboard." %}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<form method="get" action="/offboarding/new/">
|
||||
<div class="field">
|
||||
<label for="q">{% trans "Mitarbeitende suchen (Name oder E-Mail)" %}</label>
|
||||
<input id="q" name="q" value="{{ search_query }}" placeholder="{% trans "z. B. max.mustermann@tub.co" %}" />
|
||||
<input id="q" name="q" value="{{ search_query }}" placeholder="{% blocktrans trimmed with domain=portal_email_domain %}z. B. max.mustermann@{{ domain }}{% endblocktrans %}" />
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">{% trans "Suchen" %}</button>
|
||||
</form>
|
||||
@@ -47,7 +47,7 @@
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<form method="post">
|
||||
<form method="post" data-email-domain="{{ portal_email_domain }}">
|
||||
{% csrf_token %}
|
||||
<div class="grid">
|
||||
{% for field in form.visible_fields %}
|
||||
@@ -71,4 +71,3 @@
|
||||
{% block extra_scripts %}
|
||||
<script src="{% static 'workflows/js/offboarding_form.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
<div class="error-banner">{% trans "Bitte prüfen Sie die markierten Felder. Ungültige Eingaben wurden erkannt." %}</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" id="onboarding-form" enctype="multipart/form-data">
|
||||
<form method="post" id="onboarding-form" enctype="multipart/form-data" data-email-domain="{{ portal_email_domain }}">
|
||||
{% csrf_token %}
|
||||
|
||||
{% for section in onboarding_sections %}
|
||||
@@ -165,4 +165,3 @@
|
||||
{% block extra_scripts %}
|
||||
<script src="{% static 'workflows/js/onboarding_form.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -179,6 +179,7 @@
|
||||
<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>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>
|
||||
<li><strong>Audit Log:</strong> staff-only trace of important admin changes such as builder edits, settings updates, PDF generation, welcome-email operations, and request deletions. Supports filtering by action, user, and date range.</li>
|
||||
|
||||
@@ -32,6 +32,8 @@ urlpatterns = [
|
||||
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/save/', views.save_portal_branding, name='save_portal_branding'),
|
||||
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/users/', views.user_management_page, name='user_management_page'),
|
||||
path('admin-tools/users/create/', views.create_user_from_admin, name='create_user_from_admin'),
|
||||
path('admin-tools/users/<int:user_id>/update/', views.update_user_from_admin, name='update_user_from_admin'),
|
||||
|
||||
@@ -24,8 +24,9 @@ from django.utils.translation import gettext as _, gettext_lazy
|
||||
from django.utils.translation import get_language, override
|
||||
from django.urls import reverse
|
||||
|
||||
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 .branding import get_branding_email_copy, 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 .form_builder import (
|
||||
DEFAULT_FIELD_ORDER,
|
||||
@@ -35,7 +36,7 @@ from .form_builder import (
|
||||
ONBOARDING_PAGE_ORDER,
|
||||
ensure_form_field_configs,
|
||||
)
|
||||
from .models import AdminAuditLog, EmployeeProfile, FormFieldConfig, FormOption, IntroChecklistItem, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, PortalBranding, ScheduledWelcomeEmail, SystemEmailConfig, WorkflowConfig
|
||||
from .models import AdminAuditLog, EmployeeProfile, FormFieldConfig, FormOption, IntroChecklistItem, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, PortalAppConfig, PortalBranding, ScheduledWelcomeEmail, SystemEmailConfig, WorkflowConfig
|
||||
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 .services import get_email_test_redirect, is_email_test_mode, is_nextcloud_enabled, upload_to_nextcloud
|
||||
@@ -244,6 +245,7 @@ def _audit_action_label(action: str) -> str:
|
||||
'backup_verified': _('Backup verifiziert'),
|
||||
'backup_deleted': _('Backup gelöscht'),
|
||||
'backup_settings_saved': _('Backup-Einstellungen gespeichert'),
|
||||
'portal_app_registry_saved': _('App-Registry gespeichert'),
|
||||
}
|
||||
return labels.get(action, action.replace('_', ' ').strip().capitalize())
|
||||
|
||||
@@ -334,10 +336,57 @@ def home(request):
|
||||
'email_test_mode': is_email_test_mode(),
|
||||
'workflow_config': config,
|
||||
'role_label': get_user_role_label(request.user),
|
||||
'portal_app_sections': build_portal_app_sections(request.user),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@_require_capability('manage_app_registry')
|
||||
def portal_app_registry_page(request):
|
||||
return render(
|
||||
request,
|
||||
'workflows/app_registry.html',
|
||||
{
|
||||
'rows': get_portal_app_registry_rows(),
|
||||
'section_choices': _translate_choice_list(PortalAppConfig.SECTION_CHOICES),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@_require_capability('manage_app_registry')
|
||||
@require_POST
|
||||
def save_portal_app_registry(request):
|
||||
rows = get_portal_app_registry_rows()
|
||||
for row in rows:
|
||||
config = row['config']
|
||||
key = config.key
|
||||
config.section = (request.POST.get(f'section__{key}') or config.section).strip()
|
||||
if config.section not in dict(PortalAppConfig.SECTION_CHOICES):
|
||||
config.section = row['default_section']
|
||||
config.is_enabled = request.POST.get(f'is_enabled__{key}') == 'on'
|
||||
try:
|
||||
config.sort_order = int((request.POST.get(f'sort_order__{key}') or '').strip() or row['default_sort_order'])
|
||||
except ValueError:
|
||||
config.sort_order = row['default_sort_order']
|
||||
config.title_override = (request.POST.get(f'title_override__{key}') or '').strip()
|
||||
config.title_override_en = (request.POST.get(f'title_override_en__{key}') or '').strip()
|
||||
config.description_override = (request.POST.get(f'description_override__{key}') or '').strip()
|
||||
config.description_override_en = (request.POST.get(f'description_override_en__{key}') or '').strip()
|
||||
config.action_label_override = (request.POST.get(f'action_label_override__{key}') or '').strip()
|
||||
config.action_label_override_en = (request.POST.get(f'action_label_override_en__{key}') or '').strip()
|
||||
config.save()
|
||||
|
||||
_audit(
|
||||
request,
|
||||
'portal_app_registry_saved',
|
||||
target_type='portal_app_registry',
|
||||
target_label='Portal App Registry',
|
||||
details={'updated_apps': len(rows)},
|
||||
)
|
||||
messages.success(request, _('App-Registry gespeichert.'))
|
||||
return redirect('portal_app_registry_page')
|
||||
|
||||
|
||||
def _user_management_rows():
|
||||
user_model = get_user_model()
|
||||
role_order = {
|
||||
@@ -1170,6 +1219,7 @@ def onboarding_create(request):
|
||||
'legal_text': legal_text,
|
||||
'saved': request.GET.get('saved') == '1',
|
||||
'saved_request_id': request.GET.get('id', ''),
|
||||
'portal_email_domain': get_company_email_domain(),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1331,7 +1381,8 @@ def offboarding_create(request):
|
||||
if selected_profile:
|
||||
obj.employee_profile = selected_profile
|
||||
requester_email = (request.user.email or '').strip().lower()
|
||||
if requester_email and requester_email.endswith('@tub.co'):
|
||||
company_suffix = f"@{get_company_email_domain()}"
|
||||
if requester_email and requester_email.endswith(company_suffix):
|
||||
obj.requested_by_email = requester_email
|
||||
else:
|
||||
obj.requested_by_email = settings.DEFAULT_FROM_EMAIL
|
||||
@@ -1353,6 +1404,7 @@ def offboarding_create(request):
|
||||
'search_query': search_query,
|
||||
'saved': request.GET.get('saved') == '1',
|
||||
'saved_request_id': request.GET.get('id', ''),
|
||||
'portal_email_domain': get_company_email_domain(),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user