snapshot: preserve integrations controls and status UX cleanup
This commit is contained in:
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,11 @@
|
||||
from django import forms
|
||||
from pathlib import Path
|
||||
from django.utils.translation import get_language
|
||||
from datetime import timedelta
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import get_language, gettext as _
|
||||
|
||||
from .form_builder import apply_form_field_config
|
||||
from .models import EmployeeProfile, FormOption, OffboardingRequest, OnboardingRequest
|
||||
from .models import EmployeeProfile, FormOption, OffboardingRequest, OnboardingRequest, WorkflowConfig
|
||||
|
||||
|
||||
YES_NO_CHOICES = [('', '--'), ('ja', 'Ja'), ('nein', 'Nein')]
|
||||
@@ -222,6 +224,11 @@ class OnboardingRequestForm(forms.ModelForm):
|
||||
self.requester_email = (kwargs.pop('requester_email', '') or '').strip().lower()
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
config = WorkflowConfig.objects.order_by('id').first()
|
||||
self.handover_lead_days = max(0, int(getattr(config, 'device_handover_lead_days', 5) or 5))
|
||||
minimum_handover_date = timezone.localdate() + timedelta(days=self.handover_lead_days)
|
||||
self.fields['handover_date'].widget.attrs['min'] = minimum_handover_date.isoformat()
|
||||
|
||||
self.fields['full_name'].label = 'Name'
|
||||
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'):
|
||||
@@ -322,6 +329,18 @@ class OnboardingRequestForm(forms.ModelForm):
|
||||
if has('successor_name') and cleaned.get('successor_required_choice') == 'ja' and not cleaned.get('successor_name'):
|
||||
self.add_error('successor_name', 'Bitte Name der Vorgängerperson eintragen.')
|
||||
|
||||
handover_date = cleaned.get('handover_date')
|
||||
if has('handover_date') and handover_date:
|
||||
minimum_date = timezone.localdate() + timedelta(days=getattr(self, 'handover_lead_days', 5))
|
||||
if handover_date < minimum_date:
|
||||
self.add_error(
|
||||
'handover_date',
|
||||
_('Das Übergabedatum muss mindestens %(days)s Tage in der Zukunft liegen (frühestens %(date)s).') % {
|
||||
'days': getattr(self, 'handover_lead_days', 5),
|
||||
'date': minimum_date.strftime('%Y-%m-%d'),
|
||||
},
|
||||
)
|
||||
|
||||
return cleaned
|
||||
|
||||
def save(self, commit=True):
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.5 on 2026-03-25 22:21
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('workflows', '0033_offboardingrequest_last_error_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='workflowconfig',
|
||||
name='device_handover_lead_days',
|
||||
field=models.PositiveIntegerField(default=5, verbose_name='Vorlauf Geräteübergabe (Tage)'),
|
||||
),
|
||||
]
|
||||
@@ -62,7 +62,7 @@ class OnboardingRequest(models.Model):
|
||||
gender = models.CharField(
|
||||
max_length=20,
|
||||
blank=True,
|
||||
choices=[('herr', 'Herr'), ('frau', 'Frau'), ('divers', 'Divers')],
|
||||
choices=[('herr', _('Herr')), ('frau', _('Frau')), ('divers', _('Divers'))],
|
||||
verbose_name='Anrede',
|
||||
)
|
||||
job_title = models.CharField(max_length=255, blank=True, verbose_name='Berufsbezeichnung')
|
||||
@@ -72,7 +72,7 @@ class OnboardingRequest(models.Model):
|
||||
employment_type = models.CharField(
|
||||
max_length=20,
|
||||
blank=True,
|
||||
choices=[('befristet', 'befristet'), ('unbefristet', 'unbefristet')],
|
||||
choices=[('befristet', _('befristet')), ('unbefristet', _('unbefristet'))],
|
||||
verbose_name='Beschäftigungsverhältnis',
|
||||
)
|
||||
employment_end_date = models.DateField(null=True, blank=True, verbose_name='Enddatum (nur bei befristet)')
|
||||
@@ -135,13 +135,13 @@ class OnboardingRequest(models.Model):
|
||||
|
||||
class FormOption(models.Model):
|
||||
CATEGORY_CHOICES = [
|
||||
('department', 'Abteilung'),
|
||||
('device', 'Geräte'),
|
||||
('software', 'Software'),
|
||||
('access', 'Zugänge'),
|
||||
('workspace_group', 'Workspace-Gruppen'),
|
||||
('resource', 'Ressourcen'),
|
||||
('phone', 'Telefonnummern'),
|
||||
('department', _('Abteilung')),
|
||||
('device', _('Geräte')),
|
||||
('software', _('Software')),
|
||||
('access', _('Zugänge')),
|
||||
('workspace_group', _('Workspace-Gruppen')),
|
||||
('resource', _('Ressourcen')),
|
||||
('phone', _('Telefonnummern')),
|
||||
]
|
||||
|
||||
category = models.CharField(max_length=40, choices=CATEGORY_CHOICES)
|
||||
@@ -167,15 +167,15 @@ class FormOption(models.Model):
|
||||
|
||||
class FormFieldConfig(models.Model):
|
||||
PAGE_CHOICES = [
|
||||
('', 'Automatisch'),
|
||||
('stammdaten', 'Stammdaten'),
|
||||
('vertrag', 'Vertrag'),
|
||||
('itsetup', 'IT-Setup'),
|
||||
('abschluss', 'Abschluss'),
|
||||
('', _('Automatisch')),
|
||||
('stammdaten', _('Stammdaten')),
|
||||
('vertrag', _('Vertrag')),
|
||||
('itsetup', _('IT-Setup')),
|
||||
('abschluss', _('Abschluss')),
|
||||
]
|
||||
FORM_CHOICES = [
|
||||
('onboarding', 'Onboarding'),
|
||||
('offboarding', 'Offboarding'),
|
||||
('onboarding', _('Onboarding')),
|
||||
('offboarding', _('Offboarding')),
|
||||
]
|
||||
|
||||
form_type = models.CharField(max_length=20, choices=FORM_CHOICES)
|
||||
@@ -213,17 +213,17 @@ class FormFieldConfig(models.Model):
|
||||
|
||||
class NotificationTemplate(models.Model):
|
||||
TEMPLATE_CHOICES = [
|
||||
('onboarding_it', 'Onboarding: IT'),
|
||||
('onboarding_general_info', 'Onboarding: Allgemeine Info'),
|
||||
('onboarding_business_card', 'Onboarding: Visitenkarte'),
|
||||
('onboarding_hr_works', 'Onboarding: HR Works'),
|
||||
('onboarding_key', 'Onboarding: Schlüssel'),
|
||||
('onboarding_reference', 'Onboarding: Referenz Anfordernde Person'),
|
||||
('onboarding_welcome', 'Onboarding: Welcome E-Mail'),
|
||||
('offboarding_it', 'Offboarding: IT'),
|
||||
('offboarding_general_info', 'Offboarding: Allgemeine Info'),
|
||||
('offboarding_hr_works_disable', 'Offboarding: HR Works Deaktivierung'),
|
||||
('offboarding_reference', 'Offboarding: Referenz Anfordernde Person'),
|
||||
('onboarding_it', _('Onboarding: IT')),
|
||||
('onboarding_general_info', _('Onboarding: Allgemeine Info')),
|
||||
('onboarding_business_card', _('Onboarding: Visitenkarte')),
|
||||
('onboarding_hr_works', _('Onboarding: HR Works')),
|
||||
('onboarding_key', _('Onboarding: Schlüssel')),
|
||||
('onboarding_reference', _('Onboarding: Referenz Anfordernde Person')),
|
||||
('onboarding_welcome', _('Onboarding: Welcome E-Mail')),
|
||||
('offboarding_it', _('Offboarding: IT')),
|
||||
('offboarding_general_info', _('Offboarding: Allgemeine Info')),
|
||||
('offboarding_hr_works_disable', _('Offboarding: HR Works Deaktivierung')),
|
||||
('offboarding_reference', _('Offboarding: Referenz Anfordernde Person')),
|
||||
]
|
||||
|
||||
key = models.CharField(max_length=60, choices=TEMPLATE_CHOICES, unique=True)
|
||||
@@ -255,15 +255,15 @@ class NotificationTemplate(models.Model):
|
||||
|
||||
class NotificationRule(models.Model):
|
||||
EVENT_CHOICES = [
|
||||
('onboarding', 'Onboarding'),
|
||||
('offboarding', 'Offboarding'),
|
||||
('onboarding', _('Onboarding')),
|
||||
('offboarding', _('Offboarding')),
|
||||
]
|
||||
OPERATOR_CHOICES = [
|
||||
('always', 'Immer'),
|
||||
('contains', 'Enthält'),
|
||||
('equals', 'Ist gleich'),
|
||||
('is_true', 'Ist aktiv/Ja'),
|
||||
('is_false', 'Ist inaktiv/Nein'),
|
||||
('always', _('Immer')),
|
||||
('contains', _('Enthält')),
|
||||
('equals', _('Ist gleich')),
|
||||
('is_true', _('Ist aktiv/Ja')),
|
||||
('is_false', _('Ist inaktiv/Nein')),
|
||||
]
|
||||
|
||||
name = models.CharField(max_length=120)
|
||||
@@ -305,11 +305,11 @@ class NotificationRule(models.Model):
|
||||
|
||||
class ScheduledWelcomeEmail(models.Model):
|
||||
STATUS_CHOICES = [
|
||||
('scheduled', 'Geplant'),
|
||||
('paused', 'Pausiert'),
|
||||
('cancelled', 'Abgebrochen'),
|
||||
('sent', 'Gesendet'),
|
||||
('failed', 'Fehlgeschlagen'),
|
||||
('scheduled', _('Geplant')),
|
||||
('paused', _('Pausiert')),
|
||||
('cancelled', _('Abgebrochen')),
|
||||
('sent', _('Gesendet')),
|
||||
('failed', _('Fehlgeschlagen')),
|
||||
]
|
||||
|
||||
onboarding_request = models.OneToOneField(OnboardingRequest, on_delete=models.CASCADE)
|
||||
@@ -331,17 +331,17 @@ class ScheduledWelcomeEmail(models.Model):
|
||||
|
||||
class IntroChecklistItem(models.Model):
|
||||
SECTION_CHOICES = [
|
||||
('workplace', 'Geräte und Arbeitsplatz'),
|
||||
('accounts', 'Konten und Berechtigungen'),
|
||||
('software', 'Software und Tools'),
|
||||
('process', 'Prozesse und Hinweise'),
|
||||
('workplace', _('Geräte und Arbeitsplatz')),
|
||||
('accounts', _('Konten und Berechtigungen')),
|
||||
('software', _('Software und Tools')),
|
||||
('process', _('Prozesse und Hinweise')),
|
||||
]
|
||||
OPERATOR_CHOICES = [
|
||||
('always', 'Immer anzeigen'),
|
||||
('contains', 'Enthält'),
|
||||
('equals', 'Ist gleich'),
|
||||
('is_true', 'Ist Ja / aktiv'),
|
||||
('is_false', 'Ist Nein / inaktiv'),
|
||||
('always', _('Immer anzeigen')),
|
||||
('contains', _('Enthält')),
|
||||
('equals', _('Ist gleich')),
|
||||
('is_true', _('Ist Ja / aktiv')),
|
||||
('is_false', _('Ist Nein / inaktiv')),
|
||||
]
|
||||
|
||||
section = models.CharField(max_length=30, choices=SECTION_CHOICES)
|
||||
@@ -368,8 +368,8 @@ class IntroChecklistItem(models.Model):
|
||||
|
||||
class OnboardingIntroductionSession(models.Model):
|
||||
STATUS_CHOICES = [
|
||||
('draft', 'Entwurf'),
|
||||
('completed', 'Abgeschlossen'),
|
||||
('draft', _('Entwurf')),
|
||||
('completed', _('Abgeschlossen')),
|
||||
]
|
||||
|
||||
onboarding_request = models.OneToOneField(OnboardingRequest, on_delete=models.CASCADE)
|
||||
@@ -412,6 +412,7 @@ class WorkflowConfig(models.Model):
|
||||
nextcloud_password_override = models.CharField(max_length=255, blank=True, verbose_name='Nextcloud Passwort (Override)')
|
||||
nextcloud_directory_override = models.CharField(max_length=255, blank=True, verbose_name='Nextcloud Verzeichnis (Override)')
|
||||
sync_interval_seconds = models.PositiveIntegerField(default=60, verbose_name='Sync-Intervall (Sekunden)')
|
||||
device_handover_lead_days = models.PositiveIntegerField(default=5, verbose_name='Vorlauf Geräteübergabe (Tage)')
|
||||
welcome_email_delay_days = models.PositiveIntegerField(default=5, verbose_name='Welcome E-Mail Verzögerung (Tage)')
|
||||
welcome_sender_email = models.EmailField(blank=True, verbose_name='Welcome E-Mail Absender')
|
||||
welcome_include_pdf = models.BooleanField(default=True, verbose_name='Welcome E-Mail mit PDF-Anhang')
|
||||
|
||||
@@ -11,6 +11,16 @@ label { display: block; margin-bottom: 4px; font-size: 12px; color: #334155; fon
|
||||
input, select, textarea { width: 100%; box-sizing: border-box; border: 1px solid #cbd5e1; border-radius: 8px; padding: 8px 9px; background: #fff; }
|
||||
textarea { min-height: 120px; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; }
|
||||
.actions { margin-top: 10px; display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
.toggle-row { margin-top: 10px; display: inline-flex; align-items: center; gap: 10px; padding: 8px 10px 8px 12px; border: 1px solid #d8e3f0; border-radius: 999px; background: linear-gradient(180deg, rgba(255,255,255,0.98), rgba(246,249,255,0.94)); box-shadow: inset 0 1px 0 rgba(255,255,255,0.9), 0 6px 14px rgba(16, 32, 57, 0.05); }
|
||||
.toggle-copy { display: inline-flex; align-items: center; gap: 4px; white-space: nowrap; font-size: 12px; font-weight: 700; color: #30445f; }
|
||||
.toggle-label, .toggle-value { white-space: nowrap; }
|
||||
.switch-toggle { display: inline-flex; align-items: center; justify-content: center; border: 0; background: rgba(255,255,255,0.72); padding: 3px; border-radius: 999px; cursor: pointer; box-shadow: inset 0 1px 0 rgba(255,255,255,0.95); }
|
||||
.switch-track { position: relative; width: 52px; height: 28px; border-radius: 999px; border: 1px solid rgba(16, 32, 57, 0.12); background: linear-gradient(180deg, #dfe6ef, #cfd7e2); transition: background 0.18s ease, border-color 0.18s ease; box-shadow: inset 0 1px 2px rgba(16, 32, 57, 0.08), inset 0 -1px 0 rgba(255,255,255,0.4); }
|
||||
.switch-thumb { position: absolute; top: 2px; left: 2px; width: 22px; height: 22px; border-radius: 50%; background: linear-gradient(180deg, #ffffff, #f1f5fb); box-shadow: 0 2px 6px rgba(16, 32, 57, 0.22), inset 0 1px 0 rgba(255,255,255,0.95); transition: transform 0.18s ease; }
|
||||
.switch-toggle.on .switch-track { background: linear-gradient(180deg, #2caf57, #248b44); border-color: #1f7a3b; }
|
||||
.switch-toggle.on .switch-thumb { transform: translateX(24px); }
|
||||
.switch-toggle.off .switch-track { background: linear-gradient(180deg, #d55252, #b83939); border-color: #9d3030; }
|
||||
.switch-toggle:focus-visible .switch-track, .switch-toggle:hover .switch-track { box-shadow: 0 0 0 3px rgba(0, 0, 120, 0.12); }
|
||||
.hint { margin-top: 6px; color: #64748b; font-size: 12px; }
|
||||
.toolbar { display: flex; justify-content: space-between; align-items: center; gap: 10px; margin-bottom: 10px; flex-wrap: wrap; }
|
||||
.switch { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
|
||||
|
||||
@@ -150,6 +150,12 @@
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.status-switch-form {
|
||||
margin: 0;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
@@ -160,6 +166,125 @@
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
min-height: 38px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.status-pill-neutral {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255,255,255,0.98), rgba(246,249,255,0.94));
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255,255,255,0.9),
|
||||
0 6px 14px rgba(16, 32, 57, 0.05);
|
||||
}
|
||||
|
||||
.status-pill-switch {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 8px 8px 12px;
|
||||
min-height: 38px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255,255,255,0.98), rgba(246,249,255,0.94));
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255,255,255,0.9),
|
||||
0 6px 14px rgba(16, 32, 57, 0.05);
|
||||
}
|
||||
|
||||
.status-switch-copy {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.status-switch-label {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: normal;
|
||||
text-transform: none;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.status-switch-value {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: normal;
|
||||
}
|
||||
|
||||
.status-switch-label,
|
||||
.status-switch-value,
|
||||
.status-pill-switch .status-switch-copy {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.switch-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 0;
|
||||
background: rgba(255,255,255,0.72);
|
||||
padding: 3px;
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
box-shadow: inset 0 1px 0 rgba(255,255,255,0.95);
|
||||
}
|
||||
|
||||
.switch-track {
|
||||
position: relative;
|
||||
width: 52px;
|
||||
height: 28px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(16, 32, 57, 0.12);
|
||||
background: linear-gradient(180deg, #dfe6ef, #cfd7e2);
|
||||
transition: background 0.18s ease, border-color 0.18s ease;
|
||||
box-shadow:
|
||||
inset 0 1px 2px rgba(16, 32, 57, 0.08),
|
||||
inset 0 -1px 0 rgba(255,255,255,0.4);
|
||||
}
|
||||
|
||||
.switch-thumb {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(180deg, #ffffff, #f1f5fb);
|
||||
box-shadow:
|
||||
0 2px 6px rgba(16, 32, 57, 0.22),
|
||||
inset 0 1px 0 rgba(255,255,255,0.95);
|
||||
transition: transform 0.18s ease;
|
||||
}
|
||||
|
||||
.switch-toggle.on .switch-track {
|
||||
background: linear-gradient(180deg, #2caf57, #248b44);
|
||||
border-color: #1f7a3b;
|
||||
}
|
||||
|
||||
.switch-toggle.on .switch-thumb {
|
||||
transform: translateX(24px);
|
||||
}
|
||||
|
||||
.switch-toggle.off .switch-track {
|
||||
background: linear-gradient(180deg, #d55252, #b83939);
|
||||
border-color: #9d3030;
|
||||
}
|
||||
|
||||
.switch-toggle:focus-visible .switch-track,
|
||||
.switch-toggle:hover .switch-track {
|
||||
box-shadow: 0 0 0 3px rgba(0, 0, 120, 0.12);
|
||||
}
|
||||
|
||||
.status-pill-switch.ok .status-switch-value {
|
||||
color: var(--ok-ink);
|
||||
}
|
||||
|
||||
.status-pill-switch.warn .status-switch-value {
|
||||
color: var(--warn-ink);
|
||||
}
|
||||
|
||||
.status-pill.ok {
|
||||
|
||||
@@ -8,7 +8,7 @@ from celery import shared_task
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext as _, get_language
|
||||
from django.utils.translation import gettext as _, get_language, override
|
||||
from jinja2 import Template
|
||||
from pypdf import PageObject, PdfReader, PdfWriter
|
||||
from xhtml2pdf import pisa
|
||||
@@ -570,88 +570,64 @@ def _build_intro_sections_from_admin(request_obj: OnboardingRequest, language_co
|
||||
|
||||
def build_intro_sections_for_request(request_obj: OnboardingRequest, language_code: str | None = None) -> list[dict]:
|
||||
lang = _normalized_lang(language_code or get_language())
|
||||
section_titles = {
|
||||
'de': {
|
||||
'workplace': 'Geräte und Arbeitsplatz',
|
||||
'accounts': 'Konten und Berechtigungen',
|
||||
'software': 'Software und Tools',
|
||||
'process': 'Prozesse und Hinweise',
|
||||
},
|
||||
'en': {
|
||||
'workplace': 'Devices and workplace',
|
||||
'accounts': 'Accounts and permissions',
|
||||
'software': 'Software and tools',
|
||||
'process': 'Processes and notes',
|
||||
},
|
||||
}
|
||||
devices = _split_multiline(request_obj.needed_devices)
|
||||
software = _split_multiline(request_obj.needed_software)
|
||||
accesses = _split_multiline(request_obj.needed_accesses)
|
||||
groups = _split_multiline(request_obj.needed_workspace_groups)
|
||||
resources = _split_multiline(request_obj.needed_resources)
|
||||
extra_hardware = _split_multiline(request_obj.additional_hardware)
|
||||
extra_software = _split_multiline(request_obj.additional_software)
|
||||
group_mailboxes = _split_multiline(request_obj.group_mailboxes)
|
||||
with override(lang):
|
||||
section_titles = {
|
||||
'workplace': _('Geräte und Arbeitsplatz'),
|
||||
'accounts': _('Konten und Berechtigungen'),
|
||||
'software': _('Software und Tools'),
|
||||
'process': _('Prozesse und Hinweise'),
|
||||
}
|
||||
devices = _split_multiline(request_obj.needed_devices)
|
||||
software = _split_multiline(request_obj.needed_software)
|
||||
accesses = _split_multiline(request_obj.needed_accesses)
|
||||
groups = _split_multiline(request_obj.needed_workspace_groups)
|
||||
resources = _split_multiline(request_obj.needed_resources)
|
||||
extra_hardware = _split_multiline(request_obj.additional_hardware)
|
||||
extra_software = _split_multiline(request_obj.additional_software)
|
||||
group_mailboxes = _split_multiline(request_obj.group_mailboxes)
|
||||
|
||||
workplace_items = []
|
||||
for item in devices:
|
||||
if lang == 'en':
|
||||
workplace_items.append(f'{item} handed over and basic functions explained')
|
||||
else:
|
||||
workplace_items.append(f'{item} übergeben und Grundfunktionen erklärt')
|
||||
for item in resources:
|
||||
if lang == 'en':
|
||||
workplace_items.append(f'{item} shown or usage explained')
|
||||
else:
|
||||
workplace_items.append(f'{item} gezeigt bzw. Nutzung erklärt')
|
||||
if request_obj.phone_number:
|
||||
if lang == 'en':
|
||||
workplace_items.append(f'Phone number / direct extension explained: {request_obj.phone_number}')
|
||||
else:
|
||||
workplace_items.append(f'Telefonnummer / Direktwahl erklärt: {request_obj.phone_number}')
|
||||
if not workplace_items:
|
||||
workplace_items.append('Workplace, devices, and general usage reviewed' if lang == 'en' else 'Arbeitsplatz, Geräte und allgemeine Nutzung besprochen')
|
||||
workplace_items = []
|
||||
for item in devices:
|
||||
workplace_items.append(_('%(item)s übergeben und Grundfunktionen erklärt') % {'item': item})
|
||||
for item in resources:
|
||||
workplace_items.append(_('%(item)s gezeigt bzw. Nutzung erklärt') % {'item': item})
|
||||
if request_obj.phone_number:
|
||||
workplace_items.append(_('Telefonnummer / Direktwahl erklärt: %(value)s') % {'value': request_obj.phone_number})
|
||||
if not workplace_items:
|
||||
workplace_items.append(_('Arbeitsplatz, Geräte und allgemeine Nutzung besprochen'))
|
||||
|
||||
account_items = [f'{item} access explained' if lang == 'en' else f'{item} Zugang erklärt' for item in accesses]
|
||||
account_items.extend([f'{item} group / permission explained' if lang == 'en' else f'{item} Gruppe / Berechtigung erläutert' for item in groups])
|
||||
if request_obj.work_email:
|
||||
account_items.insert(0, f'Work email address explained: {request_obj.work_email}' if lang == 'en' else f'Dienstliche E-Mail-Adresse erläutert: {request_obj.work_email}')
|
||||
if group_mailboxes:
|
||||
account_items.extend([f'Group mailbox explained: {item}' if lang == 'en' else f'Gruppenpostfach erklärt: {item}' for item in group_mailboxes])
|
||||
if not account_items:
|
||||
account_items.append('Accesses, accounts, and login logic reviewed' if lang == 'en' else 'Zugänge, Konten und Anmeldelogik besprochen')
|
||||
account_items = [_('%(item)s Zugang erklärt') % {'item': item} for item in accesses]
|
||||
account_items.extend([_('%(item)s Gruppe / Berechtigung erläutert') % {'item': item} for item in groups])
|
||||
if request_obj.work_email:
|
||||
account_items.insert(0, _('Dienstliche E-Mail-Adresse erläutert: %(value)s') % {'value': request_obj.work_email})
|
||||
if group_mailboxes:
|
||||
account_items.extend([_('Gruppenpostfach erklärt: %(item)s') % {'item': item} for item in group_mailboxes])
|
||||
if not account_items:
|
||||
account_items.append(_('Zugänge, Konten und Anmeldelogik besprochen'))
|
||||
|
||||
software_items = [f'{item} introduction completed' if lang == 'en' else f'{item} Einführung durchgeführt' for item in software]
|
||||
software_items.extend([f'{item} discussed additionally' if lang == 'en' else f'{item} zusätzlich besprochen' for item in extra_software])
|
||||
if not software_items:
|
||||
software_items.append('Required standard software and daily usage explained' if lang == 'en' else 'Benötigte Standardsoftware und tägliche Nutzung erklärt')
|
||||
software_items = [_('%(item)s Einführung durchgeführt') % {'item': item} for item in software]
|
||||
software_items.extend([_('%(item)s zusätzlich besprochen') % {'item': item} for item in extra_software])
|
||||
if not software_items:
|
||||
software_items.append(_('Benötigte Standardsoftware und tägliche Nutzung erklärt'))
|
||||
|
||||
process_items = (
|
||||
[
|
||||
'Password rules and secure handling reviewed',
|
||||
'File storage, Nextcloud, and sharing explained',
|
||||
'Communication channels and support process explained',
|
||||
process_items = [
|
||||
_('Passwortregeln und sicherer Umgang besprochen'),
|
||||
_('Dateiablage, Nextcloud und Freigaben erklärt'),
|
||||
_('Kommunikationswege und Support-Prozess erklärt'),
|
||||
]
|
||||
if lang == 'en'
|
||||
else [
|
||||
'Passwortregeln und sicherer Umgang besprochen',
|
||||
'Dateiablage, Nextcloud und Freigaben erklärt',
|
||||
'Kommunikationswege und Support-Prozess erklärt',
|
||||
]
|
||||
)
|
||||
if extra_hardware:
|
||||
process_items.extend([f'{item} discussed as additional equipment' if lang == 'en' else f'{item} als zusätzliche Ausstattung besprochen' for item in extra_hardware])
|
||||
if request_obj.additional_access_text:
|
||||
process_items.extend([f'Additional access discussed: {item}' if lang == 'en' else f'Zusätzlicher Zugang besprochen: {item}' for item in _split_multiline(request_obj.additional_access_text)])
|
||||
if request_obj.successor_name:
|
||||
process_items.append(f'Handover / successor context reviewed: {request_obj.successor_name}' if lang == 'en' else f'Übergabe-/Nachfolgekontext besprochen: {request_obj.successor_name}')
|
||||
if extra_hardware:
|
||||
process_items.extend([_('%(item)s als zusätzliche Ausstattung besprochen') % {'item': item} for item in extra_hardware])
|
||||
if request_obj.additional_access_text:
|
||||
process_items.extend([_('Zusätzlicher Zugang besprochen: %(item)s') % {'item': item} for item in _split_multiline(request_obj.additional_access_text)])
|
||||
if request_obj.successor_name:
|
||||
process_items.append(_('Übergabe-/Nachfolgekontext besprochen: %(value)s') % {'value': request_obj.successor_name})
|
||||
|
||||
custom_intro_items = _build_intro_sections_from_admin(request_obj, lang)
|
||||
intro_sections_raw = [
|
||||
('workplace', section_titles.get(lang, section_titles['de'])['workplace'], workplace_items),
|
||||
('accounts', section_titles.get(lang, section_titles['de'])['accounts'], account_items),
|
||||
('software', section_titles.get(lang, section_titles['de'])['software'], software_items),
|
||||
('process', section_titles.get(lang, section_titles['de'])['process'], process_items),
|
||||
('workplace', section_titles['workplace'], workplace_items),
|
||||
('accounts', section_titles['accounts'], account_items),
|
||||
('software', section_titles['software'], software_items),
|
||||
('process', section_titles['process'], process_items),
|
||||
]
|
||||
|
||||
sections = []
|
||||
|
||||
@@ -67,14 +67,28 @@
|
||||
<td>
|
||||
{% if row.target_label %}
|
||||
{{ row.target_label }}
|
||||
{% if row.target_id %}<div class="hint">#{{ row.target_id }}</div>{% endif %}
|
||||
{% elif row.target_id %}
|
||||
#{{ row.target_id }}
|
||||
<span class="hint">ID {{ row.target_id }}</span>
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td><code>{{ row.details|default:"{}" }}</code></td>
|
||||
<td>
|
||||
{% if row.details.request_labels %}
|
||||
<div class="hint">{% trans "Betroffene Vorgänge" %}</div>
|
||||
<ul class="hint" style="margin:6px 0 10px 18px; padding:0;">
|
||||
{% for label in row.details.request_labels %}
|
||||
<li>{{ label }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if row.details.deleted_count %}<div class="hint">{% trans "Gelöscht" %}: {{ row.details.deleted_count }}</div>{% endif %}
|
||||
{% if row.details.invalid_count %}<div class="hint">{% trans "Ungültig" %}: {{ row.details.invalid_count }}</div>{% endif %}
|
||||
{% if row.details.result %}<div class="hint">{% trans "Ergebnis" %}: {{ row.details.result }}</div>{% endif %}
|
||||
{% if not row.details.request_labels and not row.details.deleted_count and not row.details.invalid_count and not row.details.result %}
|
||||
<code>{{ row.details|default:"{}" }}</code>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
|
||||
@@ -35,14 +35,14 @@
|
||||
<h1>{% trans "TUBCO Onboarding & Offboarding Portal" %}</h1>
|
||||
<p>{% trans "Zentrale Arbeitsfläche für Anfragen, PDF-Generierung, E-Mail-Workflows und Ablage in Nextcloud." %}</p>
|
||||
<div class="status-row">
|
||||
<span class="status-pill">{% trans "Rolle:" %} {% if request.user.is_staff %}{% trans "Admin" %}{% else %}{% trans "Mitarbeiter" %}{% endif %}</span>
|
||||
<span class="status-pill status-pill-neutral">{% trans "Rolle:" %} {% if request.user.is_staff %}{% trans "Admin" %}{% else %}{% trans "Mitarbeiter" %}{% endif %}</span>
|
||||
<span class="status-pill {% if nextcloud_enabled %}ok{% else %}warn{% endif %}">
|
||||
{% trans "Nextcloud:" %} {% if nextcloud_enabled %}{% trans "aktiv" %}{% else %}{% trans "inaktiv" %}{% endif %}
|
||||
</span>
|
||||
<span class="status-pill {% if email_test_mode %}warn{% else %}ok{% endif %}">
|
||||
{% trans "E-Mail:" %} {% if email_test_mode %}{% trans "Testmodus" %}{% else %}{% trans "Produktion" %}{% endif %}
|
||||
</span>
|
||||
<span class="status-pill">{% trans "PDF + E-Mail Workflow bereit" %}</span>
|
||||
<span class="status-pill status-pill-neutral">{% trans "PDF + E-Mail Workflow bereit" %}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -116,6 +116,21 @@
|
||||
</div>
|
||||
<div class="admin-grid">
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
@@ -131,64 +146,10 @@
|
||||
<a class="btn btn-secondary" href="/admin-tools/handbook/">{% trans "Öffnen" %}</a>
|
||||
</section>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
</section>
|
||||
<section class="admin-card">
|
||||
<h3>SMTP Einstellungen</h3>
|
||||
<p>Server und Absender in der Backend-UI.</p>
|
||||
<a class="btn btn-secondary" href="/admin/workflows/systememailconfig/">Öffnen</a>
|
||||
</section>
|
||||
<section class="admin-card">
|
||||
<h3>Nextcloud schalten</h3>
|
||||
<p>Aktiv/Inaktiv direkt umschalten.</p>
|
||||
<form method="post" action="/admin-tools/nextcloud/toggle/" style="display:inline;">
|
||||
{% csrf_token %}
|
||||
<button class="btn btn-secondary" type="submit">
|
||||
{% if nextcloud_enabled %}Deaktivieren{% else %}Aktivieren{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
<section class="admin-card">
|
||||
<h3>E-Mail Modus</h3>
|
||||
<p>Zwischen Testmodus und Produktion wechseln.</p>
|
||||
<form method="post" action="/admin-tools/email-mode/toggle/" style="display:inline;">
|
||||
{% csrf_token %}
|
||||
<button class="btn btn-secondary" type="submit">
|
||||
Auf {% if email_test_mode %}Produktion{% else %}Testmodus{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
<section class="admin-card">
|
||||
<h3>Verbindungstests</h3>
|
||||
<p>Testupload und Testmail auslösen.</p>
|
||||
<div class="card-actions">
|
||||
<form method="post" action="/test/nextcloud/" style="display:inline;">
|
||||
{% csrf_token %}
|
||||
<button class="btn btn-secondary" type="submit">Nextcloud-Test</button>
|
||||
</form>
|
||||
<form method="post" action="/test/email/" style="display:inline;">
|
||||
{% csrf_token %}
|
||||
<button class="btn btn-secondary" type="submit">SMTP-Test</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
<a class="tab {% if kind == 'nextcloud' %}active{% endif %}" href="/admin-tools/integrations/?kind=nextcloud">{% trans "Setup Nextcloud" %}</a>
|
||||
<a class="tab {% if kind == 'mail' %}active{% endif %}" href="/admin-tools/integrations/?kind=mail">{% trans "Setup Mail" %}</a>
|
||||
<a class="tab {% if kind == 'emails' %}active{% endif %}" href="/admin-tools/integrations/?kind=emails">{% trans "E-Mail Routing & Vorlagen" %}</a>
|
||||
<a class="tab {% if kind == 'rules' %}active{% endif %}" href="/admin-tools/integrations/?kind=rules">{% trans "Workflow-Regeln" %}</a>
|
||||
</div>
|
||||
|
||||
{% if messages %}
|
||||
@@ -29,6 +30,7 @@
|
||||
{% if kind == 'nextcloud' %}
|
||||
<form class="card" method="post" action="/admin-tools/integrations/save-nextcloud/">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="next" value="/admin-tools/integrations/?kind=nextcloud" />
|
||||
<div class="grid">
|
||||
<div>
|
||||
<label for="nc_base">NEXTCLOUD_BASE_URL</label>
|
||||
@@ -53,14 +55,32 @@
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn btn-primary" type="submit">{% trans "Nextcloud speichern" %}</button>
|
||||
<button class="btn btn-secondary" type="submit" formaction="/test/nextcloud/">{% trans "Nextcloud-Test starten" %}</button>
|
||||
</div>
|
||||
<div class="toggle-row">
|
||||
<span class="toggle-copy">
|
||||
<span class="toggle-label">{% trans "Status:" %}</span>
|
||||
<span class="toggle-value">{% if nextcloud_enabled %}{% trans "aktiv" %}{% else %}{% trans "inaktiv" %}{% endif %}</span>
|
||||
</span>
|
||||
<button class="switch-toggle {% if nextcloud_enabled %}on{% else %}off{% endif %}" type="submit" form="toggle-nextcloud-form" aria-label="{% trans 'Nextcloud schalten' %}">
|
||||
<span class="switch-track">
|
||||
<span class="switch-thumb"></span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="hint">{% trans "Schaltet den produktiven Nextcloud-Upload sofort für alle nachfolgenden Vorgänge ein oder aus." %}</div>
|
||||
<div class="hint">{% trans "Leeres Passwortfeld lässt das bestehende Passwort unverändert." %}</div>
|
||||
</form>
|
||||
<form id="toggle-nextcloud-form" method="post" action="/admin-tools/nextcloud/toggle/">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="next" value="/admin-tools/integrations/?kind=nextcloud" />
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
{% if kind == 'mail' %}
|
||||
<form class="card" method="post" action="/admin-tools/integrations/save-mail/">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="next" value="/admin-tools/integrations/?kind=mail" />
|
||||
<div class="grid">
|
||||
<div>
|
||||
<label for="imap_server">IMAP_SERVER</label>
|
||||
@@ -86,6 +106,10 @@
|
||||
<label for="email_password">PASSWORD</label>
|
||||
<input id="email_password" name="email_password" type="password" placeholder="Leer lassen = unverändert" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="from_email">{% trans "Absenderadresse" %}</label>
|
||||
<input id="from_email" name="from_email" value="{% if system_email_config %}{{ system_email_config.from_email }}{% endif %}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="check-row">
|
||||
<label><input type="checkbox" name="smtp_use_ssl" {% if workflow_config.smtp_use_ssl %}checked{% endif %} /> {% trans "SMTP SSL" %}</label>
|
||||
@@ -93,9 +117,26 @@
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn btn-primary" type="submit">{% trans "Mail speichern" %}</button>
|
||||
<button class="btn btn-secondary" type="submit" formaction="/test/email/">{% trans "SMTP-Test starten" %}</button>
|
||||
</div>
|
||||
<div class="toggle-row">
|
||||
<span class="toggle-copy">
|
||||
<span class="toggle-label">{% trans "Status:" %}</span>
|
||||
<span class="toggle-value">{% if email_test_mode %}{% trans "Testmodus" %}{% else %}{% trans "Produktion" %}{% endif %}</span>
|
||||
</span>
|
||||
<button class="switch-toggle {% if email_test_mode %}off{% else %}on{% endif %}" type="submit" form="toggle-email-mode-form" aria-label="{% trans 'E-Mail Modus schalten' %}">
|
||||
<span class="switch-track">
|
||||
<span class="switch-thumb"></span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="hint">{% trans "Im Testmodus werden Systemmails umgeleitet. In Produktion werden sie an die echten Empfänger gesendet." %}</div>
|
||||
<div class="hint">{% trans "Leeres Passwortfeld lässt das bestehende Passwort unverändert." %}</div>
|
||||
</form>
|
||||
<form id="toggle-email-mode-form" method="post" action="/admin-tools/email-mode/toggle/">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="next" value="/admin-tools/integrations/?kind=mail" />
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
{% if kind == 'emails' %}
|
||||
@@ -302,5 +343,28 @@
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% if kind == 'rules' %}
|
||||
<form class="card" method="post" action="/admin-tools/integrations/save-workflow-rules/">
|
||||
{% csrf_token %}
|
||||
<div class="grid">
|
||||
<div>
|
||||
<label for="device_handover_lead_days">{% trans "Vorlauf Hardware-Übergabe (Tage)" %}</label>
|
||||
<input
|
||||
id="device_handover_lead_days"
|
||||
name="device_handover_lead_days"
|
||||
type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
value="{{ workflow_config.device_handover_lead_days }}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn btn-primary" type="submit">{% trans "Workflow-Regeln speichern" %}</button>
|
||||
</div>
|
||||
<div class="hint">{% trans "Steuert den Mindestvorlauf für das gewünschte Übergabedatum der Geräte im Onboarding-Formular." %}</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
134
backend/workflows/templates/workflows/request_timeline.html
Normal file
134
backend/workflows/templates/workflows/request_timeline.html
Normal file
@@ -0,0 +1,134 @@
|
||||
{% extends 'workflows/base_shell.html' %}
|
||||
{% load static i18n %}
|
||||
|
||||
{% block title %}{% trans "Request Timeline" %}{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<link rel="stylesheet" href="{% static 'workflows/css/admin_tools.css' %}" />
|
||||
<style>
|
||||
.timeline-summary-grid { display:grid; grid-template-columns:repeat(5, minmax(0,1fr)); gap:16px; margin-bottom:20px; }
|
||||
.timeline-stat { border:1px solid #d9e3f8; border-radius:20px; padding:16px 18px; background:linear-gradient(180deg,#ffffff 0%,#f7faff 100%); box-shadow:0 18px 40px rgba(23,39,90,.08); }
|
||||
.timeline-stat label { display:block; margin-bottom:6px; font-size:12px; font-weight:700; letter-spacing:.08em; text-transform:uppercase; color:#7081a5; }
|
||||
.timeline-stat strong { display:block; font-size:18px; line-height:1.35; color:#20345f; }
|
||||
.timeline-list { position:relative; display:grid; gap:18px; padding-left:28px; }
|
||||
.timeline-list::before { content:""; position:absolute; top:6px; bottom:6px; left:11px; width:2px; background:linear-gradient(180deg,#cad8f8 0%,#e8eefc 100%); }
|
||||
.timeline-item { position:relative; border:1px solid #d9e3f8; border-radius:20px; padding:18px 20px 18px 22px; background:#fff; box-shadow:0 18px 40px rgba(23,39,90,.08); }
|
||||
.timeline-item::before { content:""; position:absolute; top:22px; left:-24px; width:16px; height:16px; border-radius:999px; background:#1e2bb8; box-shadow:0 0 0 4px #eef3ff; }
|
||||
.timeline-item[data-kind="document"]::before { background:#1f7a45; }
|
||||
.timeline-item[data-kind="audit"]::before { background:#6a5acd; }
|
||||
.timeline-item[data-kind="session"]::before { background:#b86b12; }
|
||||
.timeline-item[data-kind="email"]::before { background:#c2354e; }
|
||||
.timeline-item[data-kind="milestone"]::before { background:#0d7c88; }
|
||||
.timeline-head { display:flex; justify-content:space-between; gap:16px; align-items:flex-start; margin-bottom:8px; }
|
||||
.timeline-title-wrap { display:grid; gap:8px; }
|
||||
.timeline-kind { display:inline-flex; align-items:center; width:max-content; padding:6px 10px; border-radius:999px; font-size:12px; font-weight:700; letter-spacing:.06em; text-transform:uppercase; background:#eef3ff; color:#27407a; }
|
||||
.timeline-item[data-kind="document"] .timeline-kind { background:#edf8f0; color:#20623c; }
|
||||
.timeline-item[data-kind="audit"] .timeline-kind { background:#f2efff; color:#5b49aa; }
|
||||
.timeline-item[data-kind="session"] .timeline-kind { background:#fff2df; color:#8a560a; }
|
||||
.timeline-item[data-kind="email"] .timeline-kind { background:#ffe8ee; color:#9f2749; }
|
||||
.timeline-item[data-kind="milestone"] .timeline-kind { background:#e8fbfd; color:#0e6a72; }
|
||||
.timeline-stamp { font-size:13px; color:#6b7a9b; white-space:nowrap; }
|
||||
.timeline-title { margin:0; font-size:20px; color:#20345f; }
|
||||
.timeline-summary { margin:0; font-size:15px; color:#22324d; line-height:1.55; }
|
||||
.timeline-meta { display:flex; gap:8px; flex-wrap:wrap; margin-top:12px; }
|
||||
.timeline-chip { display:inline-flex; align-items:center; padding:6px 10px; border-radius:999px; background:#f7faff; border:1px solid #d8e1f5; color:#4d6087; font-size:12px; }
|
||||
.timeline-actions { margin-top:14px; }
|
||||
.timeline-details { margin-top:14px; padding-top:14px; border-top:1px dashed #d7e0f5; display:grid; gap:8px; }
|
||||
.timeline-detail-row { display:grid; grid-template-columns:160px 1fr; gap:12px; font-size:13px; }
|
||||
.timeline-detail-row strong { color:#566886; }
|
||||
.timeline-detail-list { margin:0; padding-left:18px; color:#4f617f; }
|
||||
@media (max-width: 1160px) { .timeline-summary-grid { grid-template-columns:repeat(3, minmax(0,1fr)); } }
|
||||
@media (max-width: 820px) { .timeline-summary-grid { grid-template-columns:repeat(2, minmax(0,1fr)); } }
|
||||
@media (max-width: 700px) { .timeline-summary-grid { grid-template-columns:1fr; } .timeline-head { flex-direction:column; } .timeline-stamp { white-space:normal; } .timeline-detail-row { grid-template-columns:1fr; } }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block shell_body %}
|
||||
{% include 'workflows/includes/app_header.html' with header_show_home=1 header_show_dashboard=1 header_inside_shell=1 %}
|
||||
|
||||
<div class="toolbar">
|
||||
<div>
|
||||
<h1>{% trans "Request Timeline" %}</h1>
|
||||
<p class="sub">{{ request_label }}</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<a class="btn btn-secondary" href="/requests/">{% trans "Zum Dashboard" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="timeline-summary-grid">
|
||||
<div class="timeline-stat">
|
||||
<label>{% trans "Typ" %}</label>
|
||||
<strong>{{ request_kind|capfirst }}</strong>
|
||||
</div>
|
||||
<div class="timeline-stat">
|
||||
<label>{% trans "Name" %}</label>
|
||||
<strong>{{ request_obj.full_name }}</strong>
|
||||
</div>
|
||||
<div class="timeline-stat">
|
||||
<label>{% trans "Status" %}</label>
|
||||
<strong>{{ request_obj.get_processing_status_display }}</strong>
|
||||
</div>
|
||||
<div class="timeline-stat">
|
||||
<label>{% trans "E-Mail" %}</label>
|
||||
<strong>{{ request_obj.work_email }}</strong>
|
||||
</div>
|
||||
<div class="timeline-stat">
|
||||
<label>{% trans "Hardware-Übergabetermin" %}</label>
|
||||
<strong>{% if handover_date %}{{ handover_date|date:"Y-m-d" }}{% else %}-{% endif %}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="timeline-list">
|
||||
{% for row in timeline_rows %}
|
||||
<article class="timeline-item" data-kind="{{ row.kind }}">
|
||||
<div class="timeline-head">
|
||||
<div class="timeline-title-wrap">
|
||||
<span class="timeline-kind">{{ row.kind }}</span>
|
||||
<h2 class="timeline-title">{{ row.title }}</h2>
|
||||
</div>
|
||||
<div class="timeline-stamp">{{ row.created_at|date:"Y-m-d H:i:s" }}</div>
|
||||
</div>
|
||||
|
||||
<p class="timeline-summary">{{ row.summary }}</p>
|
||||
|
||||
{% if row.meta %}
|
||||
<div class="timeline-meta">
|
||||
<span class="timeline-chip">{{ row.meta }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if row.url %}
|
||||
<div class="timeline-actions">
|
||||
<a class="btn btn-secondary" href="{{ row.url }}" target="_blank" rel="noopener">{% trans "PDF öffnen" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if row.details %}
|
||||
<div class="timeline-details">
|
||||
{% for key, value in row.details.items %}
|
||||
<div class="timeline-detail-row">
|
||||
<strong>{{ key }}</strong>
|
||||
<div>
|
||||
{% if key == 'request_labels' and value %}
|
||||
<ul class="timeline-detail-list">
|
||||
{% for item in value %}
|
||||
<li>{{ item }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
{{ value }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</article>
|
||||
{% empty %}
|
||||
<div class="empty-state">{% trans "Noch keine Timeline-Einträge vorhanden." %}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -134,7 +134,6 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
<div class="search-help">{% trans "Datensätze können direkt in der Tabelle gefiltert, geöffnet, geprüft oder gelöscht werden." %}</div>
|
||||
</div>
|
||||
{% if request.user.is_staff %}
|
||||
<div class="control-stack">
|
||||
@@ -157,7 +156,6 @@
|
||||
<th>{% trans "Typ" %}</th>
|
||||
<th>{% trans "Person" %}</th>
|
||||
<th>{% trans "E-Mail" %}</th>
|
||||
<th>{% trans "Erstellt" %}</th>
|
||||
<th>{% trans "Dokument" %}</th>
|
||||
{% if request.user.is_staff %}<th>{% trans "Einweisung" %}</th>{% endif %}
|
||||
{% if request.user.is_staff %}<th>{% trans "Aktion" %}</th>{% endif %}
|
||||
@@ -182,16 +180,17 @@
|
||||
<td>
|
||||
<a class="mail-link" href="mailto:{{ row.work_email }}">{{ row.work_email }}</a>
|
||||
</td>
|
||||
<td>{{ row.created_at|date:"Y-m-d H:i" }}</td>
|
||||
<td>
|
||||
{% if row.pdf_url %}
|
||||
<a class="doc-link" href="{{ row.pdf_url }}" target="_blank" rel="noopener">{% trans "PDF öffnen" %}</a>
|
||||
{% else %}
|
||||
<span class="person-meta">{% trans "Noch nicht verfügbar" %}</span>
|
||||
{% endif %}
|
||||
<div class="person-meta" style="margin-top:8px;">{{ row.status }}</div>
|
||||
{% if row.status_key == 'failed' and row.last_error %}
|
||||
{% if row.status_key == 'failed' %}
|
||||
<div class="person-meta" style="margin-top:8px; color:#8e1e1e;">{% trans "Fehlgeschlagen" %}</div>
|
||||
{% if row.last_error %}
|
||||
<div class="person-meta" style="margin-top:6px; color:#8e1e1e;">{{ row.last_error|truncatechars:140 }}</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if request.user.is_staff %}
|
||||
@@ -236,6 +235,7 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="actions-cell">
|
||||
<a class="btn btn-secondary" href="/requests/timeline/{{ row.kind_slug }}/{{ row.id }}/">{% trans "Timeline" %}</a>
|
||||
{% if row.status_key == 'failed' %}
|
||||
<form method="post" action="/requests/retry/{{ row.kind_slug }}/{{ row.id }}/" class="inline-delete" onsubmit="return confirm('Eintrag erneut verarbeiten?');">
|
||||
{% csrf_token %}
|
||||
|
||||
@@ -20,6 +20,7 @@ urlpatterns = [
|
||||
path('admin-tools/integrations/save-mail/', views.save_mail_settings, name='save_mail_settings'),
|
||||
path('admin-tools/integrations/save-emails/', views.save_email_routing_settings, name='save_email_routing_settings'),
|
||||
path('admin-tools/integrations/save-rules/', views.save_notification_rules, name='save_notification_rules'),
|
||||
path('admin-tools/integrations/save-workflow-rules/', views.save_workflow_rules, name='save_workflow_rules'),
|
||||
path('admin-tools/welcome-emails/', views.welcome_emails_page, name='welcome_emails_page'),
|
||||
path('admin-tools/welcome-emails/settings/', views.save_welcome_email_settings, name='save_welcome_email_settings'),
|
||||
path('admin-tools/welcome-emails/bulk-action/', views.bulk_welcome_email_action, name='bulk_welcome_email_action'),
|
||||
@@ -40,4 +41,5 @@ urlpatterns = [
|
||||
path('requests/onboarding/<int:request_id>/intro-pdf/generate/', views.generate_onboarding_intro_pdf, name='generate_onboarding_intro_pdf'),
|
||||
path('requests/delete/<str:kind>/<int:request_id>/', views.delete_request_from_dashboard, name='delete_request_from_dashboard'),
|
||||
path('requests/retry/<str:kind>/<int:request_id>/', views.retry_request_from_dashboard, name='retry_request_from_dashboard'),
|
||||
path('requests/timeline/<str:kind>/<int:request_id>/', views.request_timeline_page, name='request_timeline_page'),
|
||||
]
|
||||
|
||||
@@ -16,7 +16,7 @@ from django.views.decorators.http import require_POST
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext as _, gettext_lazy
|
||||
from django.utils.translation import get_language
|
||||
from django.utils.translation import get_language, override
|
||||
|
||||
from .forms import OffboardingRequestForm, OnboardingRequestForm
|
||||
from .form_builder import (
|
||||
@@ -27,7 +27,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, ScheduledWelcomeEmail, WorkflowConfig
|
||||
from .models import AdminAuditLog, EmployeeProfile, FormFieldConfig, FormOption, IntroChecklistItem, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, ScheduledWelcomeEmail, SystemEmailConfig, WorkflowConfig
|
||||
from .emailing import send_system_email
|
||||
from .services import get_email_test_redirect, is_email_test_mode, is_nextcloud_enabled, upload_to_nextcloud
|
||||
from .tasks import (
|
||||
@@ -40,6 +40,16 @@ from .tasks import (
|
||||
send_scheduled_welcome_email,
|
||||
)
|
||||
|
||||
|
||||
def _redirect_back(request, fallback: str):
|
||||
target = (request.POST.get('next') or request.GET.get('next') or '').strip()
|
||||
if target.startswith('/'):
|
||||
return redirect(target)
|
||||
referer = (request.META.get('HTTP_REFERER') or '').strip()
|
||||
if referer.startswith('http://127.0.0.1') or referer.startswith('http://localhost') or referer.startswith('/'):
|
||||
return redirect(referer)
|
||||
return redirect(fallback)
|
||||
|
||||
ONBOARDING_GROUPS = {
|
||||
'business-card-box': ['business_card_name', 'business_card_title', 'business_card_email', 'business_card_phone'],
|
||||
'employment-end-box': ['employment_end_date'],
|
||||
@@ -147,16 +157,70 @@ def _form_field_labels(form_type: str) -> dict[str, str]:
|
||||
return {}
|
||||
|
||||
|
||||
def _request_status_label(status_key: str) -> str:
|
||||
labels = {
|
||||
'submitted': _('Eingereicht'),
|
||||
'processing': _('In Bearbeitung'),
|
||||
'completed': _('Abgeschlossen'),
|
||||
'failed': _('Fehlgeschlagen'),
|
||||
}
|
||||
def _request_target_label(obj, kind: str | None = None) -> str:
|
||||
request_kind = (kind or '').strip()
|
||||
if not request_kind:
|
||||
request_kind = 'onboarding' if isinstance(obj, OnboardingRequest) else 'offboarding'
|
||||
name = (getattr(obj, 'full_name', '') or '').strip() or f'#{getattr(obj, "id", "?")}'
|
||||
email = (getattr(obj, 'work_email', '') or '').strip()
|
||||
created_at = getattr(obj, 'created_at', None)
|
||||
date_label = created_at.strftime('%Y-%m-%d') if created_at else ''
|
||||
parts = [request_kind.capitalize(), name]
|
||||
if email:
|
||||
parts.append(f'<{email}>')
|
||||
if date_label:
|
||||
parts.append(date_label)
|
||||
return ' | '.join(parts)
|
||||
|
||||
|
||||
def _request_status_label(status_key: str, language_code: str | None = None) -> str:
|
||||
lang = ((language_code or 'de').split('-')[0] or 'de').lower()
|
||||
with override(lang):
|
||||
labels = {
|
||||
'submitted': _('Eingereicht'),
|
||||
'processing': _('In Bearbeitung'),
|
||||
'completed': _('Abgeschlossen'),
|
||||
'failed': _('Fehlgeschlagen'),
|
||||
}
|
||||
return labels.get(status_key, status_key)
|
||||
|
||||
|
||||
def _audit_action_label(action: str) -> str:
|
||||
labels = {
|
||||
'requests_deleted': _('Vorgänge gelöscht'),
|
||||
'request_deleted': _('Vorgang gelöscht'),
|
||||
'request_retried': _('Vorgang erneut angestoßen'),
|
||||
'intro_pdf_generated': _('Einweisungs-PDF erzeugt'),
|
||||
'intro_live_pdf_generated': _('Live-Protokoll erzeugt'),
|
||||
'intro_session_reset': _('Einweisung zurückgesetzt'),
|
||||
'intro_session_saved': _('Einweisung als Entwurf gespeichert'),
|
||||
'intro_session_completed': _('Einweisung abgeschlossen'),
|
||||
'form_option_deleted': _('Formularoption gelöscht'),
|
||||
'form_options_saved': _('Formularoptionen gespeichert'),
|
||||
'form_field_texts_saved': _('Feldtexte gespeichert'),
|
||||
'form_layout_saved': _('Formularlayout gespeichert'),
|
||||
'intro_checklist_item_deleted': _('Einweisungs-Checkpunkt gelöscht'),
|
||||
'intro_checklist_item_added': _('Einweisungs-Checkpunkt hinzugefügt'),
|
||||
'intro_checklist_saved': _('Einweisungs-Checkliste gespeichert'),
|
||||
'welcome_email_triggered_now': _('Welcome E-Mail sofort ausgelöst'),
|
||||
'welcome_email_settings_saved': _('Welcome E-Mail Einstellungen gespeichert'),
|
||||
'welcome_email_bulk_action': _('Welcome E-Mail Sammelaktion ausgeführt'),
|
||||
'welcome_email_paused': _('Welcome E-Mail pausiert'),
|
||||
'welcome_email_resumed': _('Welcome E-Mail fortgesetzt'),
|
||||
'welcome_email_cancelled': _('Welcome E-Mail abgebrochen'),
|
||||
'smtp_test_sent': _('SMTP-Test gesendet'),
|
||||
'nextcloud_test_upload': _('Nextcloud-Testupload ausgeführt'),
|
||||
'nextcloud_mode_toggled': _('Nextcloud-Modus umgeschaltet'),
|
||||
'email_mode_toggled': _('E-Mail-Modus umgeschaltet'),
|
||||
'integrations_saved': _('Integrationen gespeichert'),
|
||||
'nextcloud_settings_saved': _('Nextcloud-Einstellungen gespeichert'),
|
||||
'mail_settings_saved': _('Mail-Einstellungen gespeichert'),
|
||||
'email_routing_saved': _('E-Mail-Routing gespeichert'),
|
||||
'notification_rules_saved': _('Benachrichtigungsregeln gespeichert'),
|
||||
}
|
||||
return labels.get(action, action.replace('_', ' ').strip().capitalize())
|
||||
|
||||
|
||||
def _translate_choice_list(choices):
|
||||
return [(value, str(label)) for value, label in choices]
|
||||
|
||||
@@ -311,6 +375,124 @@ def audit_log_page(request):
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@user_passes_test(_is_staff)
|
||||
def request_timeline_page(request, kind: str, request_id: int):
|
||||
if kind == 'onboarding':
|
||||
obj = get_object_or_404(OnboardingRequest, id=request_id)
|
||||
elif kind == 'offboarding':
|
||||
obj = get_object_or_404(OffboardingRequest, id=request_id)
|
||||
else:
|
||||
messages.error(request, f'Unbekannter Typ: {kind}')
|
||||
return redirect('requests_dashboard')
|
||||
|
||||
request_label = _request_target_label(obj, kind)
|
||||
audit_rows = list(
|
||||
AdminAuditLog.objects.select_related('actor')
|
||||
.filter(target_type__in=[kind, 'request'])
|
||||
.filter(Q(target_id=request_id) | Q(target_label__icontains=(obj.full_name or '').strip()))
|
||||
.order_by('-created_at', '-id')[:200]
|
||||
)
|
||||
|
||||
timeline_rows = [
|
||||
{
|
||||
'created_at': obj.created_at,
|
||||
'kind': 'system',
|
||||
'title': _('Anfrage erstellt'),
|
||||
'summary': request_label,
|
||||
'meta': _('Status: %(status)s') % {'status': obj.get_processing_status_display()},
|
||||
}
|
||||
]
|
||||
|
||||
contract_start = getattr(obj, 'contract_start', None)
|
||||
if contract_start:
|
||||
timeline_rows.append(
|
||||
{
|
||||
'created_at': timezone.make_aware(timezone.datetime.combine(contract_start, timezone.datetime.min.time())),
|
||||
'kind': 'milestone',
|
||||
'title': _('Vertragsbeginn'),
|
||||
'summary': str(contract_start),
|
||||
'meta': _('Geplanter Start'),
|
||||
}
|
||||
)
|
||||
|
||||
handover_date = getattr(obj, 'handover_date', None)
|
||||
if handover_date:
|
||||
timeline_rows.append(
|
||||
{
|
||||
'created_at': timezone.make_aware(timezone.datetime.combine(handover_date, timezone.datetime.min.time())),
|
||||
'kind': 'milestone',
|
||||
'title': _('Geräteübergabe / Hardware-Abholung'),
|
||||
'summary': str(handover_date),
|
||||
'meta': _('Geplanter Hardware-Termin'),
|
||||
}
|
||||
)
|
||||
|
||||
if getattr(obj, 'generated_pdf_path', ''):
|
||||
timeline_rows.append(
|
||||
{
|
||||
'created_at': obj.created_at,
|
||||
'kind': 'document',
|
||||
'title': _('PDF verfügbar'),
|
||||
'summary': Path(obj.generated_pdf_path).name,
|
||||
'meta': '',
|
||||
'url': f"/media/pdfs/{Path(obj.generated_pdf_path).name}",
|
||||
}
|
||||
)
|
||||
|
||||
for row in audit_rows:
|
||||
timeline_rows.append(
|
||||
{
|
||||
'created_at': row.created_at,
|
||||
'kind': 'audit',
|
||||
'title': _audit_action_label(row.action),
|
||||
'summary': row.target_label or row.target_type or '-',
|
||||
'meta': row.actor_display or '-',
|
||||
'details': row.details,
|
||||
}
|
||||
)
|
||||
|
||||
if kind == 'onboarding':
|
||||
intro_session = OnboardingIntroductionSession.objects.filter(onboarding_request=obj).first()
|
||||
if intro_session:
|
||||
timeline_rows.append(
|
||||
{
|
||||
'created_at': intro_session.updated_at,
|
||||
'kind': 'session',
|
||||
'title': _('Einweisungssitzung'),
|
||||
'summary': intro_session.get_status_display(),
|
||||
'meta': intro_session.completed_by_name or '-',
|
||||
'url': (f"/media/pdfs/{Path(intro_session.exported_pdf_path).name}" if intro_session.exported_pdf_path else ''),
|
||||
}
|
||||
)
|
||||
welcome_email = ScheduledWelcomeEmail.objects.filter(onboarding_request=obj).first()
|
||||
if welcome_email:
|
||||
timeline_rows.append(
|
||||
{
|
||||
'created_at': welcome_email.updated_at,
|
||||
'kind': 'email',
|
||||
'title': _('Welcome E-Mail'),
|
||||
'summary': welcome_email.get_status_display(),
|
||||
'meta': welcome_email.recipient_email,
|
||||
}
|
||||
)
|
||||
|
||||
timeline_rows.sort(key=lambda item: item['created_at'])
|
||||
|
||||
return render(
|
||||
request,
|
||||
'workflows/request_timeline.html',
|
||||
{
|
||||
'request_kind': kind,
|
||||
'request_obj': obj,
|
||||
'request_label': request_label,
|
||||
'timeline_rows': timeline_rows,
|
||||
'contract_start': getattr(obj, 'contract_start', None),
|
||||
'handover_date': getattr(obj, 'handover_date', None),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def requests_dashboard(request):
|
||||
if request.method == 'POST':
|
||||
@@ -329,6 +511,7 @@ def requests_dashboard(request):
|
||||
|
||||
deleted_count = 0
|
||||
invalid_count = 0
|
||||
deleted_labels = []
|
||||
for token in selected:
|
||||
try:
|
||||
kind, raw_id = token.split(':', 1)
|
||||
@@ -349,6 +532,7 @@ def requests_dashboard(request):
|
||||
obj = model.objects.filter(id=request_id).first()
|
||||
if not obj:
|
||||
continue
|
||||
deleted_labels.append(_request_target_label(obj, kind))
|
||||
obj.delete()
|
||||
deleted_count += 1
|
||||
|
||||
@@ -358,7 +542,12 @@ def requests_dashboard(request):
|
||||
'requests_deleted',
|
||||
target_type='request',
|
||||
target_label='Dashboard bulk/single delete',
|
||||
details={'deleted_count': deleted_count, 'invalid_count': invalid_count, 'selected': selected},
|
||||
details={
|
||||
'deleted_count': deleted_count,
|
||||
'invalid_count': invalid_count,
|
||||
'selected': selected,
|
||||
'request_labels': deleted_labels,
|
||||
},
|
||||
)
|
||||
messages.success(request, _('%(count)s Eintrag/Einträge gelöscht.') % {'count': deleted_count})
|
||||
if invalid_count:
|
||||
@@ -376,6 +565,12 @@ def requests_dashboard(request):
|
||||
|
||||
onboarding_items = onboarding_qs[:50]
|
||||
offboarding_items = offboarding_qs[:50]
|
||||
language_code = (
|
||||
request.COOKIES.get(settings.LANGUAGE_COOKIE_NAME)
|
||||
or getattr(request, 'LANGUAGE_CODE', '')
|
||||
or get_language()
|
||||
or 'de'
|
||||
).split('-')[0].lower()
|
||||
|
||||
rows = []
|
||||
for obj in onboarding_items:
|
||||
@@ -393,7 +588,7 @@ def requests_dashboard(request):
|
||||
'pdf_url': f"/media/pdfs/{Path(obj.generated_pdf_path).name}" if obj.generated_pdf_path else None,
|
||||
'intro_pdf_url': f"/media/pdfs/{Path(obj.intro_pdf_path).name}" if obj.intro_pdf_path else None,
|
||||
'intro_session': intro_session,
|
||||
'status': _request_status_label(obj.processing_status),
|
||||
'status': _request_status_label(obj.processing_status, language_code),
|
||||
'status_key': obj.processing_status,
|
||||
'last_error': obj.last_error,
|
||||
}
|
||||
@@ -410,7 +605,7 @@ def requests_dashboard(request):
|
||||
'pdf_url': f"/media/pdfs/{Path(obj.generated_pdf_path).name}" if obj.generated_pdf_path else None,
|
||||
'intro_pdf_url': None,
|
||||
'intro_session': None,
|
||||
'status': _request_status_label(obj.processing_status),
|
||||
'status': _request_status_label(obj.processing_status, language_code),
|
||||
'status_key': obj.processing_status,
|
||||
'last_error': obj.last_error,
|
||||
}
|
||||
@@ -997,14 +1192,21 @@ def intro_builder_page(request):
|
||||
def integrations_setup_page(request):
|
||||
config, _ = WorkflowConfig.objects.get_or_create(name='Default')
|
||||
kind = (request.GET.get('kind') or 'nextcloud').strip().lower()
|
||||
if kind not in {'nextcloud', 'mail', 'emails'}:
|
||||
if kind not in {'nextcloud', 'mail', 'emails', 'rules'}:
|
||||
kind = 'nextcloud'
|
||||
templates = list(NotificationTemplate.objects.all().order_by('key'))
|
||||
system_email_config = (
|
||||
SystemEmailConfig.objects.filter(is_active=True).order_by('-updated_at').first()
|
||||
or SystemEmailConfig.objects.filter(name='Default SMTP').first()
|
||||
)
|
||||
return render(
|
||||
request,
|
||||
'workflows/integrations_setup.html',
|
||||
{
|
||||
'workflow_config': config,
|
||||
'system_email_config': system_email_config,
|
||||
'nextcloud_enabled': is_nextcloud_enabled(),
|
||||
'email_test_mode': is_email_test_mode(),
|
||||
'kind': kind,
|
||||
'templates': templates,
|
||||
'notification_rules': NotificationRule.objects.all().order_by('event_type', 'sort_order', 'id'),
|
||||
@@ -1388,7 +1590,7 @@ def send_test_email(request):
|
||||
)
|
||||
_audit(request, 'smtp_test_sent', target_type='system_email', target_label=settings.TEST_NOTIFICATION_EMAIL, details={'email_test_mode': is_email_test_mode()})
|
||||
messages.success(request, f'SMTP-Testmail wurde gesendet ({mode}).')
|
||||
return redirect('home')
|
||||
return _redirect_back(request, 'home')
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -1421,7 +1623,7 @@ def nextcloud_test_upload(request):
|
||||
if temp_path and temp_path.exists():
|
||||
temp_path.unlink(missing_ok=True)
|
||||
|
||||
return redirect('home')
|
||||
return _redirect_back(request, 'home')
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -1436,7 +1638,7 @@ def toggle_nextcloud_enabled(request):
|
||||
|
||||
state = 'aktiviert' if config.nextcloud_enabled_override else 'deaktiviert'
|
||||
messages.success(request, f'Nextcloud Upload wurde {state}.')
|
||||
return redirect('home')
|
||||
return _redirect_back(request, 'home')
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -1451,7 +1653,7 @@ def toggle_email_mode(request):
|
||||
|
||||
state = 'Testmodus (Umleitung)' if config.email_test_mode_override else 'Produktionsmodus'
|
||||
messages.success(request, f'E-Mail-Modus wurde auf {state} gesetzt.')
|
||||
return redirect('home')
|
||||
return _redirect_back(request, 'home')
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -1519,6 +1721,35 @@ def save_nextcloud_settings(request):
|
||||
return redirect('/admin-tools/integrations/?kind=nextcloud')
|
||||
|
||||
|
||||
@login_required
|
||||
@user_passes_test(_is_staff)
|
||||
@require_POST
|
||||
def save_workflow_rules(request):
|
||||
config, _ = WorkflowConfig.objects.get_or_create(name='Default')
|
||||
try:
|
||||
handover_lead_days = int(
|
||||
request.POST.get(
|
||||
'device_handover_lead_days',
|
||||
config.device_handover_lead_days or 5,
|
||||
)
|
||||
)
|
||||
except ValueError:
|
||||
messages.error(request, 'Ungültige Zahl beim Hardware-Vorlauf.')
|
||||
return redirect('/admin-tools/integrations/?kind=rules')
|
||||
|
||||
config.device_handover_lead_days = max(0, handover_lead_days)
|
||||
config.save(update_fields=['device_handover_lead_days'])
|
||||
_audit(
|
||||
request,
|
||||
'workflow_rules_saved',
|
||||
target_type='workflow_config',
|
||||
target_label='workflow_rules',
|
||||
details={'device_handover_lead_days': config.device_handover_lead_days},
|
||||
)
|
||||
messages.success(request, 'Workflow-Regeln wurden gespeichert.')
|
||||
return redirect('/admin-tools/integrations/?kind=rules')
|
||||
|
||||
|
||||
@login_required
|
||||
@user_passes_test(_is_staff)
|
||||
@require_POST
|
||||
@@ -1543,6 +1774,18 @@ def save_mail_settings(request):
|
||||
config.email_password = email_password
|
||||
|
||||
config.save()
|
||||
smtp_cfg, _ = SystemEmailConfig.objects.get_or_create(name='Default SMTP')
|
||||
SystemEmailConfig.objects.exclude(id=smtp_cfg.id).update(is_active=False)
|
||||
smtp_cfg.is_active = True
|
||||
smtp_cfg.host = config.smtp_server
|
||||
smtp_cfg.port = config.smtp_port
|
||||
smtp_cfg.username = config.email_account
|
||||
if email_password:
|
||||
smtp_cfg.password = email_password
|
||||
smtp_cfg.use_ssl = config.smtp_use_ssl
|
||||
smtp_cfg.use_tls = config.smtp_use_tls
|
||||
smtp_cfg.from_email = request.POST.get('from_email', '').strip()
|
||||
smtp_cfg.save()
|
||||
_audit(request, 'mail_settings_saved', target_type='workflow_config', target_label='mail')
|
||||
messages.success(request, 'Mail-Einstellungen wurden gespeichert.')
|
||||
return redirect('/admin-tools/integrations/?kind=mail')
|
||||
@@ -1692,8 +1935,9 @@ def delete_request_from_dashboard(request, kind: str, request_id: int):
|
||||
messages.error(request, f'Unbekannter Typ: {kind}')
|
||||
return redirect('requests_dashboard')
|
||||
|
||||
target_label = _request_target_label(obj, kind)
|
||||
obj.delete()
|
||||
_audit(request, 'request_deleted', target_type=kind, target_id=request_id, target_label=str(obj))
|
||||
_audit(request, 'request_deleted', target_type=kind, target_id=request_id, target_label=target_label)
|
||||
messages.success(request, f'{kind.capitalize()}-Anfrage #{request_id} wurde gelöscht.')
|
||||
return redirect('requests_dashboard')
|
||||
|
||||
@@ -1708,14 +1952,14 @@ def retry_request_from_dashboard(request, kind: str, request_id: int):
|
||||
obj.last_error = ''
|
||||
obj.save(update_fields=['processing_status', 'last_error'])
|
||||
process_onboarding_request.delay(obj.id)
|
||||
_audit(request, 'request_retried', target_type='onboarding', target_id=obj.id, target_label=obj.full_name)
|
||||
_audit(request, 'request_retried', target_type='onboarding', target_id=obj.id, target_label=_request_target_label(obj, 'onboarding'))
|
||||
elif kind == 'offboarding':
|
||||
obj = get_object_or_404(OffboardingRequest, id=request_id)
|
||||
obj.processing_status = 'submitted'
|
||||
obj.last_error = ''
|
||||
obj.save(update_fields=['processing_status', 'last_error'])
|
||||
process_offboarding_request.delay(obj.id)
|
||||
_audit(request, 'request_retried', target_type='offboarding', target_id=obj.id, target_label=obj.full_name)
|
||||
_audit(request, 'request_retried', target_type='offboarding', target_id=obj.id, target_label=_request_target_label(obj, 'offboarding'))
|
||||
else:
|
||||
messages.error(request, f'Unbekannter Typ: {kind}')
|
||||
return redirect('requests_dashboard')
|
||||
|
||||
Reference in New Issue
Block a user