From e0231a6cca7d9fa15cb2d37918f8f9b9c05d31b1 Mon Sep 17 00:00:00 2001 From: Md Bayazid Bostame Date: Thu, 26 Mar 2026 00:06:43 +0100 Subject: [PATCH] snapshot: preserve integrations controls and status UX cleanup --- backend/locale/en/LC_MESSAGES/django.mo | Bin 20874 -> 23156 bytes backend/locale/en/LC_MESSAGES/django.po | 991 ++++++++++++++---- backend/workflows/forms.py | 23 +- ...orkflowconfig_device_handover_lead_days.py | 18 + backend/workflows/models.py | 101 +- .../static/workflows/css/admin_tools.css | 10 + .../workflows/static/workflows/css/home.css | 125 +++ backend/workflows/tasks.py | 126 +-- .../templates/workflows/audit_log.html | 20 +- .../workflows/templates/workflows/home.html | 73 +- .../workflows/integrations_setup.html | 66 +- .../templates/workflows/request_timeline.html | 134 +++ .../workflows/requests_dashboard.html | 10 +- backend/workflows/urls.py | 2 + backend/workflows/views.py | 284 ++++- 15 files changed, 1591 insertions(+), 392 deletions(-) create mode 100644 backend/workflows/migrations/0034_workflowconfig_device_handover_lead_days.py create mode 100644 backend/workflows/templates/workflows/request_timeline.html diff --git a/backend/locale/en/LC_MESSAGES/django.mo b/backend/locale/en/LC_MESSAGES/django.mo index 48580ec9861b48de70bf02c367670a3da49f70bb..5f4707a7ed088dcdf2be0b0227ae65c01d420818 100644 GIT binary patch delta 8649 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 delta 6384 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 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 = [] diff --git a/backend/workflows/templates/workflows/audit_log.html b/backend/workflows/templates/workflows/audit_log.html index b47d3c7..c1903ab 100644 --- a/backend/workflows/templates/workflows/audit_log.html +++ b/backend/workflows/templates/workflows/audit_log.html @@ -67,14 +67,28 @@ {% if row.target_label %} {{ row.target_label }} - {% if row.target_id %}
#{{ row.target_id }}
{% endif %} {% elif row.target_id %} - #{{ row.target_id }} + ID {{ row.target_id }} {% else %} - {% endif %} - {{ row.details|default:"{}" }} + + {% if row.details.request_labels %} +
{% trans "Betroffene Vorgänge" %}
+
    + {% for label in row.details.request_labels %} +
  • {{ label }}
  • + {% endfor %} +
+ {% endif %} + {% if row.details.deleted_count %}
{% trans "Gelöscht" %}: {{ row.details.deleted_count }}
{% endif %} + {% if row.details.invalid_count %}
{% trans "Ungültig" %}: {{ row.details.invalid_count }}
{% endif %} + {% if row.details.result %}
{% trans "Ergebnis" %}: {{ row.details.result }}
{% endif %} + {% if not row.details.request_labels and not row.details.deleted_count and not row.details.invalid_count and not row.details.result %} + {{ row.details|default:"{}" }} + {% endif %} + {% empty %} diff --git a/backend/workflows/templates/workflows/home.html b/backend/workflows/templates/workflows/home.html index d0131d1..f3abc38 100644 --- a/backend/workflows/templates/workflows/home.html +++ b/backend/workflows/templates/workflows/home.html @@ -35,14 +35,14 @@

{% trans "TUBCO Onboarding & Offboarding Portal" %}

{% trans "Zentrale Arbeitsfläche für Anfragen, PDF-Generierung, E-Mail-Workflows und Ablage in Nextcloud." %}

-{% trans "Rolle:" %} {% if request.user.is_staff %}{% trans "Admin" %}{% else %}{% trans "Mitarbeiter" %}{% endif %} + {% trans "Rolle:" %} {% if request.user.is_staff %}{% trans "Admin" %}{% else %}{% trans "Mitarbeiter" %}{% endif %} {% trans "Nextcloud:" %} {% if nextcloud_enabled %}{% trans "aktiv" %}{% else %}{% trans "inaktiv" %}{% endif %} -{% trans "PDF + E-Mail Workflow bereit" %} +{% trans "PDF + E-Mail Workflow bereit" %}
@@ -116,6 +116,21 @@
+

{% trans "Integrationen" %}

+

{% trans "Nextcloud- und E-Mail-Setup." %}

+{% trans "Öffnen" %} +
+
+

{% trans "Audit Log" %}

+

{% trans "Wichtige Admin-Aktionen nachvollziehen und prüfen." %}

+{% trans "Öffnen" %} +
+
+

{% trans "Welcome E-Mails" %}

+

{% trans "Geplante Welcome Mails verwalten." %}

+{% trans "Öffnen" %} +
+

{% trans "Form Builder" %}

{% trans "Felder, Schritte und Optionen verwalten." %}

{% trans "Öffnen" %} @@ -131,64 +146,10 @@ {% trans "Öffnen" %}
-

{% trans "Audit Log" %}

-

{% trans "Wichtige Admin-Aktionen nachvollziehen und prüfen." %}

-{% trans "Öffnen" %} -
-
-

{% trans "Integrationen" %}

-

{% trans "Nextcloud- und E-Mail-Setup." %}

-{% trans "Öffnen" %} -
-
-

{% trans "Welcome E-Mails" %}

-

{% trans "Geplante Welcome Mails verwalten." %}

-{% trans "Öffnen" %} -
-

{% trans "Django Admin" %}

{% trans "Vollständige Datenverwaltung." %}

{% trans "Öffnen" %}
-
-

SMTP Einstellungen

-

Server und Absender in der Backend-UI.

- Öffnen -
-
-

Nextcloud schalten

-

Aktiv/Inaktiv direkt umschalten.

-
- {% csrf_token %} - -
-
-
-

E-Mail Modus

-

Zwischen Testmodus und Produktion wechseln.

-
- {% csrf_token %} - -
-
-
-

Verbindungstests

-

Testupload und Testmail auslösen.

-
-
- {% csrf_token %} - -
-
- {% csrf_token %} - -
-
-
{% endif %} diff --git a/backend/workflows/templates/workflows/integrations_setup.html b/backend/workflows/templates/workflows/integrations_setup.html index 1d44d55..e5419a4 100644 --- a/backend/workflows/templates/workflows/integrations_setup.html +++ b/backend/workflows/templates/workflows/integrations_setup.html @@ -18,6 +18,7 @@ {% trans "Setup Nextcloud" %} {% trans "Setup Mail" %} {% trans "E-Mail Routing & Vorlagen" %} + {% trans "Workflow-Regeln" %} {% if messages %} @@ -29,6 +30,7 @@ {% if kind == 'nextcloud' %}
{% csrf_token %} +
@@ -53,14 +55,32 @@
+
+
+ + {% trans "Status:" %} + {% if nextcloud_enabled %}{% trans "aktiv" %}{% else %}{% trans "inaktiv" %}{% endif %} + + +
+
{% trans "Schaltet den produktiven Nextcloud-Upload sofort für alle nachfolgenden Vorgänge ein oder aus." %}
{% trans "Leeres Passwortfeld lässt das bestehende Passwort unverändert." %}
+
+ {% csrf_token %} + +
{% endif %} {% if kind == 'mail' %}
{% csrf_token %} +
@@ -86,6 +106,10 @@
+
+ + +
@@ -93,9 +117,26 @@
+
+
+ + {% trans "Status:" %} + {% if email_test_mode %}{% trans "Testmodus" %}{% else %}{% trans "Produktion" %}{% endif %} + + +
+
{% trans "Im Testmodus werden Systemmails umgeleitet. In Produktion werden sie an die echten Empfänger gesendet." %}
{% trans "Leeres Passwortfeld lässt das bestehende Passwort unverändert." %}
+
+ {% csrf_token %} + +
{% endif %} {% if kind == 'emails' %} @@ -302,5 +343,28 @@
{% endif %} -{% endblock %} + {% if kind == 'rules' %} +
+ {% csrf_token %} +
+
+ + +
+
+
+ +
+
{% trans "Steuert den Mindestvorlauf für das gewünschte Übergabedatum der Geräte im Onboarding-Formular." %}
+
+ {% endif %} + +{% endblock %} diff --git a/backend/workflows/templates/workflows/request_timeline.html b/backend/workflows/templates/workflows/request_timeline.html new file mode 100644 index 0000000..a0c0dc0 --- /dev/null +++ b/backend/workflows/templates/workflows/request_timeline.html @@ -0,0 +1,134 @@ +{% extends 'workflows/base_shell.html' %} +{% load static i18n %} + +{% block title %}{% trans "Request Timeline" %}{% endblock %} + +{% block extra_css %} + + +{% endblock %} + +{% block shell_body %} +{% include 'workflows/includes/app_header.html' with header_show_home=1 header_show_dashboard=1 header_inside_shell=1 %} + +
+
+

{% trans "Request Timeline" %}

+

{{ request_label }}

+
+ +
+ +
+
+
+ + {{ request_kind|capfirst }} +
+
+ + {{ request_obj.full_name }} +
+
+ + {{ request_obj.get_processing_status_display }} +
+
+ + {{ request_obj.work_email }} +
+
+ + {% if handover_date %}{{ handover_date|date:"Y-m-d" }}{% else %}-{% endif %} +
+
+ +
+ {% for row in timeline_rows %} +
+
+
+ {{ row.kind }} +

{{ row.title }}

+
+
{{ row.created_at|date:"Y-m-d H:i:s" }}
+
+ +

{{ row.summary }}

+ + {% if row.meta %} +
+ {{ row.meta }} +
+ {% endif %} + + {% if row.url %} + + {% endif %} + + {% if row.details %} +
+ {% for key, value in row.details.items %} +
+ {{ key }} +
+ {% if key == 'request_labels' and value %} +
    + {% for item in value %} +
  • {{ item }}
  • + {% endfor %} +
+ {% else %} + {{ value }} + {% endif %} +
+
+ {% endfor %} +
+ {% endif %} +
+ {% empty %} +
{% trans "Noch keine Timeline-Einträge vorhanden." %}
+ {% endfor %} +
+
+{% endblock %} diff --git a/backend/workflows/templates/workflows/requests_dashboard.html b/backend/workflows/templates/workflows/requests_dashboard.html index 9561476..ed0841f 100644 --- a/backend/workflows/templates/workflows/requests_dashboard.html +++ b/backend/workflows/templates/workflows/requests_dashboard.html @@ -134,7 +134,6 @@ {% endif %} -
{% trans "Datensätze können direkt in der Tabelle gefiltert, geöffnet, geprüft oder gelöscht werden." %}
{% if request.user.is_staff %}
@@ -157,7 +156,6 @@ {% trans "Typ" %} {% trans "Person" %} {% trans "E-Mail" %} -{% trans "Erstellt" %} {% trans "Dokument" %} {% if request.user.is_staff %}{% trans "Einweisung" %}{% endif %} {% if request.user.is_staff %}{% trans "Aktion" %}{% endif %} @@ -182,16 +180,17 @@ {{ row.work_email }} - {{ row.created_at|date:"Y-m-d H:i" }} {% if row.pdf_url %} {% trans "PDF öffnen" %} {% else %} {% trans "Noch nicht verfügbar" %} {% endif %} -
{{ row.status }}
- {% if row.status_key == 'failed' and row.last_error %} + {% if row.status_key == 'failed' %} +
{% trans "Fehlgeschlagen" %}
+ {% if row.last_error %}
{{ row.last_error|truncatechars:140 }}
+ {% endif %} {% endif %} {% if request.user.is_staff %} @@ -236,6 +235,7 @@ {% endif %} + {% trans "Timeline" %} {% if row.status_key == 'failed' %}
{% csrf_token %} diff --git a/backend/workflows/urls.py b/backend/workflows/urls.py index 2d5e292..c907e26 100644 --- a/backend/workflows/urls.py +++ b/backend/workflows/urls.py @@ -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//intro-pdf/generate/', views.generate_onboarding_intro_pdf, name='generate_onboarding_intro_pdf'), path('requests/delete///', views.delete_request_from_dashboard, name='delete_request_from_dashboard'), path('requests/retry///', views.retry_request_from_dashboard, name='retry_request_from_dashboard'), + path('requests/timeline///', views.request_timeline_page, name='request_timeline_page'), ] diff --git a/backend/workflows/views.py b/backend/workflows/views.py index e8263d1..d8a524e 100644 --- a/backend/workflows/views.py +++ b/backend/workflows/views.py @@ -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')