snapshot: preserve dashboard redesign and live protocol workflow state

This commit is contained in:
Md Bayazid Bostame
2026-03-19 16:10:30 +01:00
parent 3bf43921ff
commit 1cb92682cf
14 changed files with 1948 additions and 121 deletions

View File

@@ -12,7 +12,7 @@ from jinja2 import Template
from pypdf import PageObject, PdfReader, PdfWriter
from xhtml2pdf import pisa
from .models import EmployeeProfile, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingRequest, ScheduledWelcomeEmail, WorkflowConfig
from .models import EmployeeProfile, IntroChecklistItem, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, ScheduledWelcomeEmail, WorkflowConfig
from .emailing import send_system_email
from .services import upload_to_nextcloud
from .services import get_email_test_redirect, is_email_test_mode
@@ -269,6 +269,111 @@ def _resolve_workflow_emails() -> tuple[str, str, str, str, str]:
return it_email, general_info_email, business_card_email, hr_works_email, key_email
def _matches_intro_condition(request_obj: OnboardingRequest, item: IntroChecklistItem) -> bool:
operator = (item.condition_operator or 'always').strip()
field_name = (item.condition_field or '').strip()
expected = (item.condition_value or '').strip()
if operator == 'always' or not field_name:
return True
raw_value = getattr(request_obj, field_name, '')
if raw_value is None:
raw_value = ''
if operator == 'is_true':
return bool(raw_value)
if operator == 'is_false':
return not bool(raw_value)
text_value = str(raw_value).strip()
if operator == 'equals':
return text_value.lower() == expected.lower()
if operator == 'contains':
return expected.lower() in text_value.lower()
return True
def _build_intro_sections_from_admin(request_obj: OnboardingRequest) -> dict[str, list[str]]:
items = list(IntroChecklistItem.objects.filter(is_active=True).order_by('section', 'sort_order', 'label'))
if not items:
return {}
section_map = {key: [] for key, _label in IntroChecklistItem.SECTION_CHOICES}
for item in items:
if item.section not in section_map:
continue
if _matches_intro_condition(request_obj, item):
section_map[item.section].append(item.label)
return {key: values for key, values in section_map.items() if values}
def build_intro_sections_for_request(request_obj: OnboardingRequest) -> list[dict]:
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:
workplace_items.append(f'{item} übergeben und Grundfunktionen erklärt')
for item in resources:
workplace_items.append(f'{item} gezeigt bzw. Nutzung erklärt')
if request_obj.phone_number:
workplace_items.append(f'Telefonnummer / Direktwahl erklärt: {request_obj.phone_number}')
if not workplace_items:
workplace_items.append('Arbeitsplatz, Geräte und allgemeine Nutzung besprochen')
account_items = [f'{item} Zugang erklärt' for item in accesses]
account_items.extend([f'{item} Gruppe / Berechtigung erläutert' for item in groups])
if request_obj.work_email:
account_items.insert(0, f'Dienstliche E-Mail-Adresse erläutert: {request_obj.work_email}')
if group_mailboxes:
account_items.extend([f'Gruppenpostfach erklärt: {item}' for item in group_mailboxes])
if not account_items:
account_items.append('Zugänge, Konten und Anmeldelogik besprochen')
software_items = [f'{item} Einführung durchgeführt' for item in software]
software_items.extend([f'{item} zusätzlich besprochen' for item in extra_software])
if not software_items:
software_items.append('Benötigte Standardsoftware und tägliche Nutzung erklärt')
process_items = [
'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} als zusätzliche Ausstattung besprochen' for item in extra_hardware])
if request_obj.additional_access_text:
process_items.extend([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'Übergabe-/Nachfolgekontext besprochen: {request_obj.successor_name}')
custom_intro_items = _build_intro_sections_from_admin(request_obj)
intro_sections_raw = [
('workplace', 'Geräte und Arbeitsplatz', workplace_items),
('accounts', 'Konten und Berechtigungen', account_items),
('software', 'Software und Tools', software_items),
('process', 'Prozesse und Hinweise', process_items),
]
sections = []
for key, title, default_items in intro_sections_raw:
merged_items = list(default_items)
merged_items.extend(custom_intro_items.get(key, []))
section_items = []
for idx, label in enumerate(merged_items, start=1):
section_items.append({'id': f'{key}_{idx}', 'label': label})
if section_items:
sections.append({'key': key, 'title': title, 'items': section_items})
return sections
def _send_workflow_email(
subject: str,
body: str,
@@ -609,6 +714,107 @@ def _generate_onboarding_pdf(request_obj: OnboardingRequest) -> Path:
return output_pdf
def _generate_onboarding_intro_pdf(request_obj: OnboardingRequest) -> Path:
safe_name = _safe_filename_fragment(request_obj.full_name, fallback=f'onboarding_intro_{request_obj.id}')
output_pdf = settings.PDF_OUTPUT_DIR / f'onboarding_intro_{safe_name}.pdf'
temp_pdf = settings.PDF_OUTPUT_DIR / f'_temp_onboarding_intro_{safe_name}.pdf'
template_path = settings.PDF_TEMPLATES_DIR / 'onboarding_intro_template.html'
letterhead_path = settings.PDF_TEMPLATES_DIR / 'templates.pdf'
salutation = (request_obj.get_gender_display() or '').strip()
display_name = f"{salutation} {request_obj.full_name}".strip() if salutation else request_obj.full_name
intro_sections = [
{
'title': section['title'],
'rows': _chunk_list([item['label'] for item in section['items']], chunk_size=2),
}
for section in build_intro_sections_for_request(request_obj)
]
requester_email = request_obj.onboarded_by_email or '-'
requester_name = request_obj.onboarded_by_name or _resolve_user_display_name(request_obj.onboarded_by_email) or '-'
context = {
'DISPLAY_NAME': display_name,
'ABTEILUNG': request_obj.department or '-',
'BERUFSBEZEICHNUNG': request_obj.job_title or '-',
'VERTRAGSBEGINN': request_obj.contract_start,
'EMAIL': request_obj.work_email or '-',
'REQUESTED_BY_NAME': requester_name,
'REQUESTED_BY_EMAIL': requester_email,
'INTRO_SECTIONS': intro_sections,
}
html = _render_html(template_path, context)
_generate_content_pdf(html, temp_pdf)
_overlay_with_letterhead(temp_pdf, letterhead_path, output_pdf)
if temp_pdf.exists():
temp_pdf.unlink(missing_ok=True)
return output_pdf
def _generate_onboarding_intro_session_pdf(session: OnboardingIntroductionSession, admin_signature_name: str = '-') -> Path:
request_obj = session.onboarding_request
safe_name = _safe_filename_fragment(request_obj.full_name, fallback=f'onboarding_intro_session_{request_obj.id}')
version = timezone.now().strftime('%Y%m%d%H%M%S')
output_pdf = settings.PDF_OUTPUT_DIR / f'onboarding_intro_session_{safe_name}_{version}.pdf'
temp_pdf = settings.PDF_OUTPUT_DIR / f'_temp_onboarding_intro_session_{safe_name}_{version}.pdf'
template_path = settings.PDF_TEMPLATES_DIR / 'onboarding_intro_session_pdf.html'
letterhead_path = settings.PDF_TEMPLATES_DIR / 'templates.pdf'
salutation = (request_obj.get_gender_display() or '').strip()
display_name = f"{salutation} {request_obj.full_name}".strip() if salutation else request_obj.full_name
raw_sections = build_intro_sections_for_request(request_obj)
checked_map = session.checklist_state or {}
exported_sections = []
checked_count = 0
total_count = 0
for section in raw_sections:
checked_items = []
for item in section['items']:
checked = bool(checked_map.get(item['id']))
total_count += 1
if checked:
checked_count += 1
checked_items.append({'label': item['label']})
if checked_items:
exported_sections.append({
'title': section['title'],
'rows': [checked_items[i:i + 2] for i in range(0, len(checked_items), 2)],
})
requester_email = request_obj.onboarded_by_email or '-'
requester_name = request_obj.onboarded_by_name or _resolve_user_display_name(request_obj.onboarded_by_email) or '-'
context = {
'DISPLAY_NAME': display_name,
'ABTEILUNG': request_obj.department or '-',
'BERUFSBEZEICHNUNG': request_obj.job_title or '-',
'VERTRAGSBEGINN': request_obj.contract_start,
'EMAIL': request_obj.work_email or '-',
'REQUESTED_BY_NAME': requester_name,
'REQUESTED_BY_EMAIL': requester_email,
'SESSION_STATUS': session.get_status_display(),
'SESSION_COMPLETED_BY': session.completed_by_name or '-',
'SESSION_COMPLETED_AT': session.completed_at or '-',
'SESSION_UPDATED_AT': session.updated_at,
'SESSION_NOTES': session.notes or '-',
'INTRO_SECTIONS': exported_sections,
}
html = _render_html(template_path, context)
_generate_content_pdf(html, temp_pdf)
_overlay_with_letterhead(temp_pdf, letterhead_path, output_pdf)
if temp_pdf.exists():
temp_pdf.unlink(missing_ok=True)
return output_pdf
def _generate_offboarding_pdf(request_obj: OffboardingRequest) -> Path:
safe_name = _safe_filename_fragment(request_obj.full_name, fallback=f'offboarding_{request_obj.id}')
output_pdf = settings.PDF_OUTPUT_DIR / f'offboarding_letter_{safe_name}.pdf'