snapshot: preserve integrations controls and status UX cleanup
This commit is contained in:
@@ -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