snapshot: preserve state before onboarding field parity and refactor slice

This commit is contained in:
Md Bayazid Bostame
2026-03-28 00:08:27 +01:00
parent 5cb7ef78f8
commit b2686522c7
5 changed files with 556 additions and 179 deletions

View File

@@ -114,7 +114,6 @@
.muted-cell { .muted-cell {
color: #64748b; color: #64748b;
} }
</style> </style>
</head> </head>
<body> <body>
@@ -122,41 +121,250 @@
<h1 class="title">{{ T.onboarding_title }}</h1> <h1 class="title">{{ T.onboarding_title }}</h1>
</div> </div>
{% for section in PDF_SECTIONS %} <div class="section">{% if PDF_LANG == 'en' %}Master data{% else %}Stammdaten{% endif %}</div>
{% if section.has_content %} <div class="opt-card">
<div class="section">{{ section.title }}</div> <div class="opt-title">{{ T.onboarding_staff_data }}</div>
<table>
<tr>
<th>{{ T.name }}</th>
<td class="mono" colspan="3">{{ DISPLAY_NAME }}</td>
</tr>
<tr>
<th>{{ T.department }}</th>
<td>{{ ABTEILUNG }}</td>
<th>{{ T.job_title }}</th>
<td>{{ BERUFSBEZEICHNUNG }}</td>
</tr>
<tr>
<th>{{ T.work_email }}</th>
<td>{{ EMAIL }}</td>
<th>{{ T.employment_type }}</th>
<td>{{ BESCHAEFTIGUNG }}</td>
</tr>
<tr>
<th>{{ T.contract_start }}</th>
<td>{{ VERTRAGSBEGINN }}</td>
<th>{{ T.contract_end }}</th>
<td>{{ VERTRAGSENDE }}</td>
</tr>
<tr>
<th>{{ T.handover_date }}</th>
<td colspan="3">{{ UEBERGABEDATUM }}</td>
</tr>
</table>
</div>
{% if section.scalar_rows %} {% if HAS_DEVICES or HAS_GROUPS or HAS_SOFTWARE or HAS_ACCESSES or HAS_RESOURCES or HAS_GROUP_MAILBOXES or HAS_ADDITIONAL_HARDWARE or HAS_ADDITIONAL_SOFTWARE or HAS_ADDITIONAL_ACCESS %}
<table> <div class="section">{% if PDF_LANG == 'en' %}IT setup{% else %}IT-Setup{% endif %}</div>
{% for row in section.scalar_rows %}
<tr>
<th>{{ row[0].label }}</th>
<td{% if not row[1] %} colspan="3"{% endif %}{% if row[0].name in ['full_name', 'work_email'] %} class="mono"{% endif %}>{{ row[0].display_value }}</td>
{% if row[1] %}
<th>{{ row[1].label }}</th>
<td>{{ row[1].display_value }}</td>
{% endif %}
</tr>
{% endfor %}
</table>
{% endif %}
{% for field in section.list_fields %} {% if HAS_DEVICES %}
<div class="opt-card"> <div class="opt-card">
<div class="opt-title">{{ field.label }}</div> <div class="opt-title">{{ T.devices }}</div>
<table class="opt-grid"> <table class="opt-grid">
{% for row in field.display_value|batch(3, '') %} {% for row in ARBEITSGERÄTE_LIST %}
<tr> <tr>
{% for cell in row %} {% for cell in row %}<td>• {{ cell }}</td>{% endfor %}
<td>{% if cell %}• {{ cell }}{% endif %}</td> {% if row|length < 3 %}
{% endfor %} {% for _ in range(3 - row|length) %}<td></td>{% endfor %}
</tr> {% endif %}
{% endfor %} </tr>
</table>
</div>
{% endfor %} {% endfor %}
{% endif %} </table>
{% endfor %} </div>
{% endif %}
{% if HAS_GROUPS %}
<div class="opt-card">
<div class="opt-title">{{ T.workspace_groups }}</div>
<table class="opt-grid">
{% for row in ZUGAENGE_LIST %}
<tr>
{% for cell in row %}<td>• {{ cell }}</td>{% endfor %}
{% if row|length < 3 %}
{% for _ in range(3 - row|length) %}<td></td>{% endfor %}
{% endif %}
</tr>
{% endfor %}
</table>
</div>
{% endif %}
{% if HAS_SOFTWARE %}
<div class="opt-card">
<div class="opt-title">{{ T.software }}</div>
<table class="opt-grid">
{% for row in SOFTWARE_LIST %}
<tr>
{% for cell in row %}<td>• {{ cell }}</td>{% endfor %}
{% if row|length < 3 %}
{% for _ in range(3 - row|length) %}<td></td>{% endfor %}
{% endif %}
</tr>
{% endfor %}
</table>
</div>
{% endif %}
{% if HAS_ACCESSES %}
<div class="opt-card">
<div class="opt-title">{{ T.accesses }}</div>
<table class="opt-grid">
{% for row in ACCOUNT_LIST %}
<tr>
{% for cell in row %}<td>• {{ cell }}</td>{% endfor %}
{% if row|length < 3 %}
{% for _ in range(3 - row|length) %}<td></td>{% endfor %}
{% endif %}
</tr>
{% endfor %}
</table>
</div>
{% endif %}
{% if HAS_RESOURCES %}
<div class="opt-card">
<div class="opt-title">{{ T.resources }}</div>
<table class="opt-grid">
{% for row in STANDARD_RESSOURCEN %}
<tr>
{% for cell in row %}<td>• {{ cell }}</td>{% endfor %}
{% if row|length < 3 %}
{% for _ in range(3 - row|length) %}<td></td>{% endfor %}
{% endif %}
</tr>
{% endfor %}
</table>
</div>
{% endif %}
{% if GROUP_MAILBOXES_REQUIRED and HAS_GROUP_MAILBOXES %}
<div class="opt-card">
<div class="opt-title">{{ T.group_mailboxes_required }}</div>
<table class="opt-grid">
{% for row in GROUP_MAILBOXES_LIST %}
<tr>
{% for cell in row %}<td>• {{ cell }}</td>{% endfor %}
{% if row|length < 3 %}
{% for _ in range(3 - row|length) %}<td></td>{% endfor %}
{% endif %}
</tr>
{% endfor %}
</table>
</div>
{% endif %}
{% if ADDITIONAL_HARDWARE_NEEDED and HAS_ADDITIONAL_HARDWARE %}
<div class="opt-card">
<div class="opt-title">{{ T.additional_hardware_needed }}</div>
<table class="opt-grid">
{% for row in ADDITIONAL_HARDWARE_LIST %}
<tr>
{% for cell in row %}<td>• {{ cell }}</td>{% endfor %}
{% if row|length < 3 %}
{% for _ in range(3 - row|length) %}<td></td>{% endfor %}
{% endif %}
</tr>
{% endfor %}
</table>
</div>
{% endif %}
{% if ADDITIONAL_SOFTWARE_NEEDED and HAS_ADDITIONAL_SOFTWARE %}
<div class="opt-card">
<div class="opt-title">{{ T.additional_software_needed }}</div>
<table class="opt-grid">
{% for row in ADDITIONAL_SOFTWARE_LIST %}
<tr>
{% for cell in row %}<td>• {{ cell }}</td>{% endfor %}
{% if row|length < 3 %}
{% for _ in range(3 - row|length) %}<td></td>{% endfor %}
{% endif %}
</tr>
{% endfor %}
</table>
</div>
{% endif %}
{% if ADDITIONAL_ACCESS_NEEDED and HAS_ADDITIONAL_ACCESS %}
<div class="opt-card">
<div class="opt-title">{{ T.additional_access_needed }}</div>
<table class="opt-grid">
{% for row in ADDITIONAL_ACCESS_LIST %}
<tr>
{% for cell in row %}<td>• {{ cell }}</td>{% endfor %}
{% if row|length < 3 %}
{% for _ in range(3 - row|length) %}<td></td>{% endfor %}
{% endif %}
</tr>
{% endfor %}
</table>
</div>
{% endif %}
{% endif %}
{% if (VISITENKARTE_BESTELLT and HAS_VISITENKARTE_DATEN) or HAS_ADDITIONAL_HARDWARE_OTHER or HAS_SUCCESSOR_INFO or HAS_ADDITIONAL_NOTES %}
<div class="section">{{ T.additional_details }}</div>
{% endif %}
{% if VISITENKARTE_BESTELLT and HAS_VISITENKARTE_DATEN %}
<div class="opt-card">
<div class="opt-title">{{ T.business_cards }}</div>
<table>
<tr>
<th>{{ T.name }}</th>
<td>{{ VISITENKARTE_NAME }}</td>
<th>{{ T.job_title }}</th>
<td>{{ VISITENKARTE_TITEL }}</td>
</tr>
<tr>
<th>{{ T.email }}</th>
<td>{{ VISITENKARTE_EMAIL }}</td>
<th>{{ T.phone }}</th>
<td>{{ VISITENKARTE_TELEFON }}</td>
</tr>
</table>
</div>
{% endif %}
{% if HAS_ADDITIONAL_HARDWARE_OTHER %}
<div class="opt-card">
<div class="opt-title">{{ T.additional_hardware_other }}</div>
<table>
<tr>
<td>{{ ADDITIONAL_HARDWARE_OTHER }}</td>
</tr>
</table>
</div>
{% endif %}
{% if HAS_SUCCESSOR_INFO %}
<div class="opt-card">
<div class="opt-title">{{ T.successor_phone }}</div>
<table>
<tr>
<th>{{ T.successor_of }}</th>
<td>{{ SUCCESSOR_NAME }}</td>
<th>{{ T.inherit_phone_number }}</th>
<td>{{ INHERIT_PHONE_NUMBER }}</td>
</tr>
<tr>
<th>{{ T.direct_extension }}</th>
<td colspan="3">{{ PHONE_NUMBER }}</td>
</tr>
</table>
</div>
{% endif %}
{% if HAS_ADDITIONAL_NOTES %}
<div class="opt-card">
<div class="opt-title">{{ T.notes }}</div>
<table>
<tr>
<td>{{ ADDITIONAL_NOTES }}</td>
</tr>
</table>
</div>
{% endif %}
<div class="section">{{ T.confirmation }}</div> <div class="section">{{ T.confirmation }}</div>
<table> <table>

View File

@@ -218,3 +218,43 @@ th { background: #f6f9ff; color: #334155; }
.app-registry-group-head h2 { margin: 0; color: #17345e; font-size: 18px; } .app-registry-group-head h2 { margin: 0; color: #17345e; font-size: 18px; }
.app-registry-group-body { display: grid; gap: 14px; } .app-registry-group-body { display: grid; gap: 14px; }
.app-registry-group[hidden] { display: none !important; } .app-registry-group[hidden] { display: none !important; }
.intro-builder-shell { display: grid; grid-template-columns: 260px minmax(0, 1fr); gap: 18px; align-items: start; }
.intro-builder-sidebar { position: sticky; top: 18px; display: grid; gap: 14px; }
.intro-side-card { padding: 16px; }
.intro-eyebrow { margin-bottom: 10px; }
.intro-side-stats { display: grid; gap: 12px; }
.intro-side-stat { display: grid; gap: 2px; }
.intro-side-stat strong { font-size: 22px; line-height: 1; color: #163566; }
.intro-side-stat span { color: #60738d; font-size: 12px; font-weight: 700; }
.intro-section-nav { display: grid; gap: 8px; }
.intro-section-link { justify-content: flex-start; }
.intro-builder-main { min-width: 0; display: grid; gap: 14px; }
.intro-builder-form-card, .intro-builder-list-card { padding: 16px; }
.intro-surface-head { margin-bottom: 14px; }
.intro-surface-head h2 { margin: 0; color: #17345e; font-size: 20px; }
.intro-surface-head p { margin: 4px 0 0; color: #60738d; font-size: 13px; }
.intro-builder-actions { display: flex; gap: 10px; align-items: center; justify-content: space-between; flex-wrap: wrap; margin-top: 12px; }
.intro-builder-actions-sticky { position: sticky; bottom: 14px; z-index: 4; padding-top: 12px; border-top: 1px solid #dbe5f2; background: linear-gradient(180deg, rgba(255,255,255,0.92), rgba(255,255,255,0.98)); }
.intro-group-stack { display: grid; gap: 16px; }
.intro-group-card { border: 1px solid #d7e3f0; border-radius: 18px; background: linear-gradient(180deg, rgba(255,255,255,0.98), rgba(244,248,255,0.95)); padding: 14px; box-shadow: inset 0 1px 0 rgba(255,255,255,0.94); }
.intro-group-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; margin-bottom: 12px; }
.intro-group-head h3 { margin: 0; color: #17345e; font-size: 18px; }
.intro-item-list { display: grid; gap: 12px; }
.intro-item-card { border: 1px solid #dce6f2; border-radius: 16px; background: rgba(255,255,255,0.88); padding: 14px; box-shadow: inset 0 1px 0 rgba(255,255,255,0.92); }
.intro-item-head { display: flex; justify-content: space-between; gap: 12px; align-items: flex-start; margin-bottom: 12px; }
.intro-item-head strong { color: #17345e; font-size: 15px; line-height: 1.35; }
.intro-item-actions { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.intro-inline-check { display: inline-flex; align-items: center; gap: 6px; margin: 0; font-size: 13px; font-weight: 700; color: #334155; }
.intro-inline-check input[type="checkbox"] { width: auto; }
.intro-item-grid { display: grid; grid-template-columns: repeat(2, minmax(220px, 1fr)); gap: 12px; }
.intro-item-field { margin: 0; }
.intro-item-field > span { display: block; margin-bottom: 6px; font-size: 12px; color: #334155; font-weight: 700; }
.intro-item-field-wide { grid-column: 1 / -1; }
@media (max-width: 760px) {
.intro-builder-shell { grid-template-columns: 1fr; }
.intro-builder-sidebar { position: static; }
.intro-item-head { flex-direction: column; }
.intro-item-grid { grid-template-columns: 1fr; }
.intro-builder-actions { align-items: stretch; flex-direction: column; }
}

View File

@@ -963,73 +963,112 @@ def _generate_onboarding_pdf(request_obj: OnboardingRequest) -> Path:
phone_number = (request_obj.phone_number or '').strip() phone_number = (request_obj.phone_number or '').strip()
display_name = f"{gender} {first_name} {last_name}".strip() if gender and gender != '-' else f"{first_name} {last_name}".strip() display_name = f"{gender} {first_name} {last_name}".strip() if gender and gender != '-' else f"{first_name} {last_name}".strip()
pdf_sections = build_pdf_sections('onboarding', request_obj, lang)
pdf_section_map = {section['key']: section for section in pdf_sections if section.get('has_content')}
pdf_field_map = {}
for section in pdf_sections:
for field in section.get('render_fields', []):
pdf_field_map[field['name']] = field
def _field_text(name: str, fallback):
field = pdf_field_map.get(name)
if not field:
return fallback
value = field.get('display_value')
if isinstance(value, list):
return ' | '.join(value) if value else fallback
return value or fallback
def _field_list(name: str) -> list[str]:
field = pdf_field_map.get(name)
if not field:
return []
value = field.get('display_value')
if isinstance(value, list):
return value
text = str(value or '').strip()
return [text] if text else []
devices_visible = _field_list('needed_devices_multi')
groups_visible = _field_list('needed_workspace_groups_multi')
software_visible = _field_list('needed_software_multi')
accesses_visible = _field_list('needed_accesses_multi')
resources_visible = _field_list('needed_resources_multi')
group_mailboxes_visible = _field_list('group_mailboxes')
additional_hardware_visible = _field_list('additional_hardware_multi')
additional_software_visible = _field_list('additional_software_multi')
additional_access_visible = _field_list('additional_access_text')
job_title_visible = 'job_title' in pdf_field_map
itsetup_visible = 'itsetup' in pdf_section_map
context = { context = {
'T': t, 'T': t,
'PDF_LANG': lang, 'PDF_LANG': lang,
'PDF_SECTIONS': build_pdf_sections('onboarding', request_obj, lang), 'PDF_SECTIONS': pdf_sections,
'VORNAME': first_name, 'VORNAME': first_name,
'NACHNAME': last_name, 'NACHNAME': last_name,
'DISPLAY_NAME': display_name or request_obj.full_name, 'DISPLAY_NAME': display_name or request_obj.full_name,
'ANREDE': gender, 'ANREDE': _field_text('gender', gender),
'BERUFSBEZEICHNUNG': request_obj.job_title or t['not_available'], 'JOB_TITLE_VISIBLE': job_title_visible,
'ABTEILUNG': request_obj.department or t['not_available'], 'BERUFSBEZEICHNUNG': _field_text('job_title', t['not_available']),
'EMAIL': request_obj.work_email or t['not_available'], 'ABTEILUNG': _field_text('department', t['not_available']),
'VERTRAGSBEGINN': request_obj.contract_start, 'EMAIL': _field_text('work_email', t['not_available']),
'BESCHAEFTIGUNG': employment_type, 'VERTRAGSBEGINN': _field_text('contract_start', request_obj.contract_start),
'VERTRAGSENDE': employment_end, 'BESCHAEFTIGUNG': _field_text('employment_type', employment_type),
'UEBERGABEDATUM': request_obj.handover_date or t['not_available_short'], 'VERTRAGSENDE': _field_text('employment_end_date', employment_end),
'ARBEITSGERAETE_TEXT': ' | '.join(devices) if devices else t['not_available'], 'UEBERGABEDATUM': _field_text('handover_date', request_obj.handover_date or t['not_available_short']),
'WORKSPACE_GROUPS_TEXT': ' | '.join(groups) if groups else t['not_available'], 'ARBEITSGERAETE_TEXT': ' | '.join(devices_visible) if devices_visible else t['not_available'],
'SOFTWARE_TEXT': ' | '.join(software) if software else t['not_available'], 'WORKSPACE_GROUPS_TEXT': ' | '.join(groups_visible) if groups_visible else t['not_available'],
'ZUGAENGE_TEXT': ' | '.join(accesses) if accesses else t['not_available'], 'SOFTWARE_TEXT': ' | '.join(software_visible) if software_visible else t['not_available'],
'RESSOURCEN_TEXT': ' | '.join(resources) if resources else t['not_available'], 'ZUGAENGE_TEXT': ' | '.join(accesses_visible) if accesses_visible else t['not_available'],
'RESSOURCEN_TEXT': ' | '.join(resources_visible) if resources_visible else t['not_available'],
'VISITENKARTE_BESTELLT': order_business_cards, 'VISITENKARTE_BESTELLT': order_business_cards,
'HAS_VISITENKARTE_DATEN': order_business_cards and any( 'HAS_VISITENKARTE_DATEN': order_business_cards and ('business_card_name' in pdf_field_map or 'business_card_title' in pdf_field_map or 'business_card_email' in pdf_field_map or 'business_card_phone' in pdf_field_map) and any(
[ [
(request_obj.business_card_name or '').strip(), _field_text('business_card_name', '').strip(),
(request_obj.business_card_title or '').strip(), _field_text('business_card_title', '').strip(),
(request_obj.business_card_email or '').strip(), _field_text('business_card_email', '').strip(),
(request_obj.business_card_phone or '').strip(), _field_text('business_card_phone', '').strip(),
] ]
), ),
'VISITENKARTE_NAME': request_obj.business_card_name or t['not_available_short'], 'VISITENKARTE_NAME': _field_text('business_card_name', t['not_available_short']),
'VISITENKARTE_TITEL': request_obj.business_card_title or t['not_available_short'], 'VISITENKARTE_TITEL': _field_text('business_card_title', t['not_available_short']),
'VISITENKARTE_EMAIL': request_obj.business_card_email or t['not_available_short'], 'VISITENKARTE_EMAIL': _field_text('business_card_email', t['not_available_short']),
'VISITENKARTE_TELEFON': request_obj.business_card_phone or t['not_available_short'], 'VISITENKARTE_TELEFON': _field_text('business_card_phone', t['not_available_short']),
'GROUP_MAILBOXES': group_mailboxes or t['not_available'], 'GROUP_MAILBOXES': _field_text('group_mailboxes', group_mailboxes or t['not_available']),
'ADDITIONAL_HARDWARE_OTHER': additional_hardware_other or t['not_available'], 'ADDITIONAL_HARDWARE_OTHER': _field_text('additional_hardware_other', additional_hardware_other or t['not_available']),
'ADDITIONAL_HARDWARE': additional_hardware or t['not_available'], 'ADDITIONAL_HARDWARE': _field_text('additional_hardware_other', additional_hardware or t['not_available']),
'ADDITIONAL_SOFTWARE': additional_software or t['not_available'], 'ADDITIONAL_SOFTWARE': _field_text('additional_software', additional_software or t['not_available']),
'ADDITIONAL_ACCESS_TEXT': additional_access_text or t['not_available'], 'ADDITIONAL_ACCESS_TEXT': _field_text('additional_access_text', additional_access_text or t['not_available']),
'SUCCESSOR_NAME': successor_name or t['not_available'], 'SUCCESSOR_NAME': _field_text('successor_name', successor_name or t['not_available']),
'PHONE_NUMBER': phone_number or t['not_available_short'], 'PHONE_NUMBER': _field_text('phone_number_choice', phone_number or t['not_available_short']),
'INHERIT_PHONE_NUMBER': t['yes'] if request_obj.inherit_phone_number else t['no'], 'INHERIT_PHONE_NUMBER': _field_text('inherit_phone_number_choice', t['yes'] if request_obj.inherit_phone_number else t['no']),
'ADDITIONAL_NOTES': additional_notes or t['not_available'], 'ADDITIONAL_NOTES': _field_text('additional_notes', additional_notes or t['not_available']),
'GROUP_MAILBOXES_REQUIRED': bool(request_obj.group_mailboxes_required), 'GROUP_MAILBOXES_REQUIRED': 'group_mailboxes_required_choice' in pdf_field_map and bool(group_mailboxes_visible),
'ADDITIONAL_HARDWARE_NEEDED': bool(request_obj.additional_hardware_needed), 'ADDITIONAL_HARDWARE_NEEDED': 'additional_hardware_needed_choice' in pdf_field_map and bool(additional_hardware_visible),
'ADDITIONAL_SOFTWARE_NEEDED': bool(request_obj.additional_software_needed), 'ADDITIONAL_SOFTWARE_NEEDED': 'additional_software_needed_choice' in pdf_field_map and bool(additional_software_visible),
'ADDITIONAL_ACCESS_NEEDED': bool(request_obj.additional_access_needed), 'ADDITIONAL_ACCESS_NEEDED': 'additional_access_needed_choice' in pdf_field_map and bool(additional_access_visible),
'HAS_DEVICES': bool(devices), 'HAS_DEVICES': itsetup_visible and bool(devices_visible),
'HAS_GROUPS': bool(groups), 'HAS_GROUPS': itsetup_visible and bool(groups_visible),
'HAS_SOFTWARE': bool(software), 'HAS_SOFTWARE': itsetup_visible and bool(software_visible),
'HAS_ACCESSES': bool(accesses), 'HAS_ACCESSES': itsetup_visible and bool(accesses_visible),
'HAS_RESOURCES': bool(resources), 'HAS_RESOURCES': itsetup_visible and bool(resources_visible),
'HAS_GROUP_MAILBOXES': bool(group_mailboxes_list), 'HAS_GROUP_MAILBOXES': bool(group_mailboxes_visible),
'HAS_ADDITIONAL_HARDWARE': bool(additional_hardware_list), 'HAS_ADDITIONAL_HARDWARE': bool(additional_hardware_visible),
'HAS_ADDITIONAL_SOFTWARE': bool(additional_software_list), 'HAS_ADDITIONAL_SOFTWARE': bool(additional_software_visible),
'HAS_ADDITIONAL_ACCESS': bool(additional_access_list), 'HAS_ADDITIONAL_ACCESS': bool(additional_access_visible),
'HAS_ADDITIONAL_HARDWARE_OTHER': bool(additional_hardware_other), 'HAS_ADDITIONAL_HARDWARE_OTHER': bool(_field_text('additional_hardware_other', '').strip()),
'HAS_SUCCESSOR_INFO': bool(successor_name) or bool(request_obj.inherit_phone_number) or bool(phone_number), 'HAS_SUCCESSOR_INFO': bool(_field_text('successor_name', '').strip()) or 'inherit_phone_number_choice' in pdf_field_map or bool(_field_text('phone_number_choice', '').strip()),
'HAS_ADDITIONAL_NOTES': bool(additional_notes), 'HAS_ADDITIONAL_NOTES': bool(_field_text('additional_notes', '').strip()),
'GROUP_MAILBOXES_LIST': _chunk_list(group_mailboxes_list), 'GROUP_MAILBOXES_LIST': _chunk_list(group_mailboxes_visible),
'ADDITIONAL_HARDWARE_LIST': _chunk_list(additional_hardware_list), 'ADDITIONAL_HARDWARE_LIST': _chunk_list(additional_hardware_visible),
'ADDITIONAL_SOFTWARE_LIST': _chunk_list(additional_software_list), 'ADDITIONAL_SOFTWARE_LIST': _chunk_list(additional_software_visible),
'ADDITIONAL_ACCESS_LIST': _chunk_list(additional_access_list), 'ADDITIONAL_ACCESS_LIST': _chunk_list(additional_access_visible),
'ZUGAENGE_LIST': _chunk_list(groups), 'ZUGAENGE_LIST': _chunk_list(groups_visible),
'ARBEITSGERÄTE_LIST': _chunk_list(devices), 'ARBEITSGERÄTE_LIST': _chunk_list(devices_visible),
'SOFTWARE_LIST': _chunk_list(software), 'SOFTWARE_LIST': _chunk_list(software_visible),
'ACCOUNT_LIST': _chunk_list(accesses), 'ACCOUNT_LIST': _chunk_list(accesses_visible),
'STANDARD_RESSOURCEN': _chunk_list(resources), 'STANDARD_RESSOURCEN': _chunk_list(resources_visible),
'UNTERSCHRIFT': signature_src, 'UNTERSCHRIFT': signature_src,
'UNTERSCHRIFT_HINWEIS': signature_note, 'UNTERSCHRIFT_HINWEIS': signature_note,
'REQUESTED_BY_NAME': requester_name, 'REQUESTED_BY_NAME': requester_name,

View File

@@ -25,97 +25,167 @@
{% include 'workflows/includes/messages.html' %} {% include 'workflows/includes/messages.html' %}
<form class="card" method="post" action="/admin-tools/intro-builder/"> <section class="intro-builder-shell">
{% csrf_token %} <aside class="intro-builder-sidebar">
<input type="hidden" name="builder_action" value="add_item" /> <section class="card intro-side-card">
<div class="grid"> <span class="page-eyebrow intro-eyebrow">{% trans "Übersicht" %}</span>
<div class="field"> <div class="intro-side-stats">
<label for="section">{% trans "Abschnitt" %}</label> <div class="intro-side-stat">
<select id="section" name="section"> <strong>{{ intro_summary.total_items }}</strong>
{% for value, label in section_choices %} <span>{% trans "Punkte gesamt" %}</span>
<option value="{{ value }}">{{ label }}</option> </div>
{% endfor %} <div class="intro-side-stat">
</select> <strong>{{ intro_summary.active_items }}</strong>
</div> <span>{% trans "aktiv" %}</span>
<div class="field"> </div>
<label for="label">{% trans "Checklistenpunkt (DE)" %}</label> <div class="intro-side-stat">
<input id="label" name="label" placeholder="{% trans 'z. B. Nextcloud Ordnerstruktur erklärt' %}" required /> <strong>{{ intro_summary.conditional_items }}</strong>
</div> <span>{% trans "mit Bedingung" %}</span>
<div class="field"> </div>
<label for="label_en">{% trans "Checklist item (EN)" %}</label> <div class="intro-side-stat">
<input id="label_en" name="label_en" placeholder="{% trans 'e.g. Nextcloud folder structure explained' %}" /> <strong>{{ intro_summary.section_count }}</strong>
</div> <span>{% trans "genutzte Abschnitte" %}</span>
<div class="actions"> </div>
<button class="btn btn-primary" type="submit">{% trans "Punkt hinzufügen" %}</button> </div>
</div> </section>
</div>
<div class="hint">{% trans "Bedingungen und Sortierung können anschließend in der Tabelle bearbeitet werden." %}</div>
</form>
<form class="card" method="post" action="/admin-tools/intro-builder/"> <nav class="app-module-nav intro-section-nav" aria-label="{% trans 'Abschnitte' %}">
{% csrf_token %} {% for group in grouped_items %}
<input type="hidden" name="builder_action" value="save_items" /> {% if group.count %}
<div class="table-wrap app-table-shell"> <a class="app-module-link intro-section-link" href="#intro-section-{{ group.key }}">{{ group.label }}</a>
<table class="app-table table-controls"> {% endif %}
<thead> {% endfor %}
<tr> </nav>
<th>{% trans "Sortierung" %}</th> </aside>
<th>{% trans "Abschnitt" %}</th>
<th>{% trans "Checklistenpunkt (DE)" %}</th> <div class="intro-builder-main">
<th>{% trans "Checklistenpunkt (EN)" %}</th> <form class="card intro-builder-form-card" method="post" action="/admin-tools/intro-builder/">
<th>{% trans "Feld-Bedingung" %}</th> {% csrf_token %}
<th>{% trans "Operator" %}</th> <input type="hidden" name="builder_action" value="add_item" />
<th>{% trans "Wert" %}</th> <div class="surface-head intro-surface-head">
<th>{% trans "Aktiv" %}</th> <div>
<th>{% trans "Löschen" %}</th> <h2>{% trans "Punkt hinzufügen" %}</h2>
</tr> <p>{% trans "Neue Checklistenpunkte direkt in den passenden Abschnitt einfügen." %}</p>
</thead> </div>
<tbody> </div>
{% for item in items %} <div class="grid">
<tr> <div class="field">
<td> <label for="section">{% trans "Abschnitt" %}</label>
<input type="hidden" name="item_ids" value="{{ item.id }}" /> <select id="section" name="section">
{{ forloop.counter }} {% for value, label in section_choices %}
</td> <option value="{{ value }}">{{ label }}</option>
<td> {% endfor %}
<select name="section_{{ item.id }}"> </select>
{% for value, label in section_choices %} </div>
<option value="{{ value }}" {% if item.section == value %}selected{% endif %}>{{ label }}</option> <div class="field">
{% endfor %} <label for="label">{% trans "Checklistenpunkt (DE)" %}</label>
</select> <input id="label" name="label" placeholder="{% trans 'z. B. Nextcloud Ordnerstruktur erklärt' %}" required />
</td> </div>
<td><input type="text" name="label_{{ item.id }}" value="{{ item.label }}" required /></td> <div class="field">
<td><input type="text" name="label_en_{{ item.id }}" value="{{ item.label_en }}" /></td> <label for="label_en">{% trans "Checklist item (EN)" %}</label>
<td> <input id="label_en" name="label_en" placeholder="{% trans 'e.g. Nextcloud folder structure explained' %}" />
<select name="field_{{ item.id }}"> </div>
{% for value, label in condition_field_choices %} </div>
<option value="{{ value }}" {% if item.condition_field == value %}selected{% endif %}>{{ label }}</option> <div class="intro-builder-actions">
{% endfor %} <button class="btn btn-primary" type="submit">{% trans "Punkt hinzufügen" %}</button>
</select> <span class="hint">{% trans "Bedingungen und Sortierung können anschließend im Editor gepflegt werden." %}</span>
</td> </div>
<td> </form>
<select name="operator_{{ item.id }}">
{% for value, label in operator_choices %} <form class="card intro-builder-list-card" method="post" action="/admin-tools/intro-builder/">
<option value="{{ value }}" {% if item.condition_operator == value %}selected{% endif %}>{{ label }}</option> {% csrf_token %}
{% endfor %} <input type="hidden" name="builder_action" value="save_items" />
</select> <div class="surface-head intro-surface-head">
</td> <div>
<td><input type="text" name="value_{{ item.id }}" value="{{ item.condition_value }}" placeholder="{% trans 'z. B. HR Works' %}" /></td> <h2>{% trans "Checkliste bearbeiten" %}</h2>
<td><input type="checkbox" name="active_{{ item.id }}" {% if item.is_active %}checked{% endif %} /></td> <p>{% trans "Punkte pro Abschnitt pflegen, Bedingungen setzen und aktive Einträge steuern." %}</p>
<td class="actions"> </div>
<button class="btn btn-secondary" type="submit" name="delete_item_id" value="{{ item.id }}" data-confirm="{% trans 'Checklistenpunkt wirklich löschen?' %}">{% trans "Löschen" %}</button> </div>
</td>
</tr> <div class="intro-group-stack">
{% empty %} {% for group in grouped_items %}
<tr><td colspan="9">{% trans "Noch keine benutzerdefinierten Checklistenpunkte angelegt. Solange die Liste leer ist, nutzt das System die integrierten Standardpunkte." %}</td></tr> <section class="intro-group-card" id="intro-section-{{ group.key }}">
<div class="intro-group-head">
<div>
<h3>{{ group.label }}</h3>
<p class="mini">{% blocktrans trimmed with count=group.count active=group.active_count %}{{ count }} Punkte, {{ active }} aktiv{% endblocktrans %}</p>
</div>
</div>
{% if group.items %}
<div class="intro-item-list">
{% for item in group.items %}
<article class="intro-item-card">
<input type="hidden" name="item_ids" value="{{ item.id }}" />
<div class="intro-item-head">
<strong>{{ item.label }}</strong>
<div class="intro-item-actions">
<label class="intro-inline-check">
<input type="checkbox" name="active_{{ item.id }}" {% if item.is_active %}checked{% endif %} />
<span>{% trans "Aktiv" %}</span>
</label>
<button class="btn btn-secondary" type="submit" name="delete_item_id" value="{{ item.id }}" data-confirm="{% trans 'Checklistenpunkt wirklich löschen?' %}">{% trans "Löschen" %}</button>
</div>
</div>
<div class="intro-item-grid">
<label class="field intro-item-field">
<span>{% trans "Sortierung" %}</span>
<div class="branding-inline-value">{{ item.sort_order|add:1 }}</div>
</label>
<label class="field intro-item-field">
<span>{% trans "Abschnitt" %}</span>
<select name="section_{{ item.id }}">
{% for value, label in section_choices %}
<option value="{{ value }}" {% if item.section == value %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</label>
<label class="field intro-item-field intro-item-field-wide">
<span>{% trans "Checklistenpunkt (DE)" %}</span>
<input type="text" name="label_{{ item.id }}" value="{{ item.label }}" required />
</label>
<label class="field intro-item-field intro-item-field-wide">
<span>{% trans "Checklistenpunkt (EN)" %}</span>
<input type="text" name="label_en_{{ item.id }}" value="{{ item.label_en }}" />
</label>
<label class="field intro-item-field">
<span>{% trans "Feld-Bedingung" %}</span>
<select name="field_{{ item.id }}">
{% for value, label in condition_field_choices %}
<option value="{{ value }}" {% if item.condition_field == value %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</label>
<label class="field intro-item-field">
<span>{% trans "Operator" %}</span>
<select name="operator_{{ item.id }}">
{% for value, label in operator_choices %}
<option value="{{ value }}" {% if item.condition_operator == value %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</label>
<label class="field intro-item-field intro-item-field-wide">
<span>{% trans "Wert" %}</span>
<input type="text" name="value_{{ item.id }}" value="{{ item.condition_value }}" placeholder="{% trans 'z. B. HR Works' %}" />
</label>
</div>
</article>
{% endfor %}
</div>
{% else %}
<div class="builder-empty-state">{% trans "Noch keine Punkte in diesem Abschnitt." %}</div>
{% endif %}
</section>
{% endfor %} {% endfor %}
</tbody> </div>
</table>
<div class="intro-builder-actions intro-builder-actions-sticky">
<button class="btn btn-primary" type="submit">{% trans "Checkliste speichern" %}</button>
<span class="hint">{% trans "Die Reihenfolge folgt aktuell der gespeicherten Listenreihenfolge innerhalb der Abschnitte." %}</span>
</div>
</form>
</div> </div>
<div class="hint">{% trans "Reihenfolge folgt derzeit der Tabellenreihenfolge beim Speichern." %}</div> </section>
<div class="u-mt-12">
<button class="btn btn-primary" type="submit">{% trans "Checkliste speichern" %}</button>
</div>
</form>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -3167,11 +3167,31 @@ def intro_builder_page(request):
] ]
items = list(IntroChecklistItem.objects.all().order_by('section', 'sort_order', 'label')) items = list(IntroChecklistItem.objects.all().order_by('section', 'sort_order', 'label'))
section_label_map = dict(_translate_choice_list(IntroChecklistItem.SECTION_CHOICES))
grouped_items = []
for value, _label in IntroChecklistItem.SECTION_CHOICES:
section_items = [item for item in items if item.section == value]
grouped_items.append(
{
'key': value,
'label': section_label_map.get(value, value),
'items': section_items,
'count': len(section_items),
'active_count': len([item for item in section_items if item.is_active]),
}
)
return render( return render(
request, request,
'workflows/intro_builder.html', 'workflows/intro_builder.html',
{ {
'items': items, 'items': items,
'grouped_items': grouped_items,
'intro_summary': {
'total_items': len(items),
'active_items': len([item for item in items if item.is_active]),
'conditional_items': len([item for item in items if item.condition_operator != 'always']),
'section_count': len([group for group in grouped_items if group['count']]),
},
'section_choices': _translate_choice_list(IntroChecklistItem.SECTION_CHOICES), 'section_choices': _translate_choice_list(IntroChecklistItem.SECTION_CHOICES),
'operator_choices': _translate_choice_list(IntroChecklistItem.OPERATOR_CHOICES), 'operator_choices': _translate_choice_list(IntroChecklistItem.OPERATOR_CHOICES),
'condition_field_choices': condition_field_choices, 'condition_field_choices': condition_field_choices,