Files
workdock-platform/backend/workflows/form_builder_views.py
2026-03-28 08:56:43 +01:00

894 lines
48 KiB
Python

from django.contrib import messages
from django.db import IntegrityError
from django.shortcuts import redirect, render
from django.utils.translation import get_language, gettext as _
from .forms import OffboardingRequestForm, OnboardingRequestForm
from .form_builder import (
DEFAULT_FIELD_ORDER,
FORM_PRESETS,
LOCKED_FIELD_RULES,
LOCKED_SECTION_RULES,
ONBOARDING_DEFAULT_PAGE,
apply_form_preset,
build_custom_field_key,
ensure_form_conditional_rule_configs,
ensure_form_field_configs,
ensure_form_section_configs,
get_custom_section_configs,
get_default_page_map,
get_section_definitions,
get_section_order,
)
from .models import (
FormConditionalRuleConfig,
FormCustomFieldConfig,
FormCustomSectionConfig,
FormFieldConfig,
FormOption,
)
from .roles import ROLE_PLATFORM_OWNER, get_user_role_key
import re
def form_builder_page_impl(
request,
*,
audit_fn,
translate_choice_list,
form_field_labels_fn,
field_rule_summary_fn,
conditional_rule_summary_fn,
onboarding_groups,
conditional_rule_operator_choices,
):
_audit = audit_fn
_translate_choice_list = translate_choice_list
_form_field_labels = form_field_labels_fn
_field_rule_summary = field_rule_summary_fn
_conditional_rule_summary = conditional_rule_summary_fn
ONBOARDING_GROUPS = onboarding_groups
CONDITIONAL_RULE_OPERATOR_CHOICES = conditional_rule_operator_choices
language_code = get_language()
form_type = request.GET.get('form_type', 'onboarding')
can_override_locked_builder_rules = get_user_role_key(request.user) == ROLE_PLATFORM_OWNER
anchor = (request.GET.get('anchor') or '').strip()
active_panel = (request.GET.get('panel') or '').strip()
active_subpanel = (request.GET.get('subpanel') or '').strip()
active_rules_panel = (request.GET.get('rules_panel') or '').strip()
active_module = (request.GET.get('module') or '').strip()
active_structure_section = (request.GET.get('structure_section') or '').strip()
active_field_rules_section = ((request.POST.get('field_rules_section') if request.method == 'POST' else '') or request.GET.get('field_rules_section') or '').strip()
active_field_texts_section = ((request.POST.get('field_texts_section') if request.method == 'POST' else '') or request.GET.get('field_texts_section') or '').strip()
active_custom_fields_section = ((request.POST.get('custom_fields_section') if request.method == 'POST' else '') or request.GET.get('custom_fields_section') or '').strip()
active_section_rules_section = ((request.POST.get('section_rules_section') if request.method == 'POST' else '') or request.GET.get('section_rules_section') or '').strip()
active_conditional_target = ((request.POST.get('conditional_target') if request.method == 'POST' else '') or request.GET.get('conditional_target') or '').strip()
if form_type not in DEFAULT_FIELD_ORDER:
form_type = 'onboarding'
option_category = request.GET.get('option_category', 'department')
option_categories = [c[0] for c in FormOption.CATEGORY_CHOICES]
if option_category not in option_categories:
option_category = option_categories[0]
valid_modules = {
'structure',
'section-rules',
'field-rules',
'conditional-rules',
'options',
'field-texts',
'custom-sections',
'custom-fields',
'preview',
}
if not active_module:
if active_panel == 'builder-structure':
active_module = 'structure'
elif active_panel == 'builder-rules':
active_module = active_rules_panel or 'section-rules'
elif active_panel == 'builder-content':
active_module = active_subpanel or 'options'
else:
active_module = 'structure'
if active_module not in valid_modules:
active_module = 'structure'
if form_type != 'onboarding' and active_module == 'custom-sections':
active_module = 'options'
if form_type != 'onboarding' and active_module == 'conditional-rules':
active_module = 'field-rules'
if request.method == 'POST':
delete_option_id = request.POST.get('delete_option_id', '').strip()
delete_custom_field_id = request.POST.get('delete_custom_field_id', '').strip()
delete_custom_section_id = request.POST.get('delete_custom_section_id', '').strip()
if delete_option_id:
option = FormOption.objects.filter(id=delete_option_id).first()
if not option:
messages.error(request, _('Option nicht gefunden.'))
else:
option_category = option.category
deleted_label = option.label
deleted_id = option.id
option.delete()
_audit(request, 'form_option_deleted', target_type='form_option', target_id=deleted_id, target_label=deleted_label)
messages.success(request, _('Option wurde gelöscht.'))
return redirect(f"/admin-tools/form-builder/?form_type={form_type}&option_category={option_category}&module=options")
if delete_custom_field_id:
custom_field = FormCustomFieldConfig.objects.filter(id=delete_custom_field_id, form_type=form_type).first()
if not custom_field:
messages.error(request, _('Benutzerdefiniertes Feld nicht gefunden.'))
else:
deleted_label = custom_field.label
deleted_id = custom_field.id
custom_field.delete()
_audit(request, 'form_custom_field_deleted', target_type='form_custom_field', target_id=deleted_id, target_label=deleted_label)
messages.success(request, _('Benutzerdefiniertes Feld wurde gelöscht.'))
return redirect(f"/admin-tools/form-builder/?form_type={form_type}&option_category={option_category}&module=custom-fields")
if delete_custom_section_id:
custom_section = FormCustomSectionConfig.objects.filter(id=delete_custom_section_id, form_type=form_type).first()
if not custom_section:
messages.error(request, _('Benutzerdefinierter Abschnitt nicht gefunden.'))
else:
deleted_label = custom_section.title
deleted_id = custom_section.id
section_key = custom_section.section_key
custom_fields = list(FormCustomFieldConfig.objects.filter(form_type=form_type, section_key=section_key))
deleted_field_count = len(custom_fields)
if custom_fields:
field_keys = [item.field_key for item in custom_fields]
FormConditionalRuleConfig.objects.filter(
form_type=form_type,
target_key__in=[f'custom__{field_key}' for field_key in field_keys],
).delete()
FormCustomFieldConfig.objects.filter(id__in=[item.id for item in custom_fields]).delete()
custom_section.delete()
_audit(
request,
'form_custom_section_deleted',
target_type='form_custom_section',
target_id=deleted_id,
target_label=deleted_label,
details={'section_key': section_key, 'deleted_field_count': deleted_field_count},
)
messages.success(request, _('Benutzerdefinierter Abschnitt wurde gelöscht.'))
return redirect(f"/admin-tools/form-builder/?form_type={form_type}&option_category={option_category}&module=custom-sections")
action = request.POST.get('builder_action', '')
if action == 'add_option':
category = request.POST.get('category', '').strip()
label = request.POST.get('label', '').strip()
label_en = request.POST.get('label_en', '').strip()
value = request.POST.get('value', '').strip()
if category not in option_categories:
messages.error(request, _('Ungültige Kategorie.'))
elif not label:
messages.error(request, _('Bitte einen Namen für die Option angeben.'))
else:
next_sort = (
FormOption.objects.filter(category=category).order_by('-sort_order').values_list('sort_order', flat=True).first()
)
FormOption.objects.create(
# Global form option catalog entry
category=category,
label=label,
label_en=label_en,
value=value or label,
sort_order=(next_sort + 1) if next_sort is not None else 0,
is_active=True,
)
_audit(
request,
'form_option_added',
target_type='form_option',
target_label=label,
details={'category': category, 'label_en': label_en, 'value': value or label},
)
messages.success(request, _('Option wurde hinzugefügt.'))
option_category = category
elif action == 'save_options':
option_ids = request.POST.getlist('option_ids')
for pos, raw_id in enumerate(option_ids):
option = FormOption.objects.filter(id=raw_id).first()
if not option:
continue
next_label = request.POST.get(f'label_{option.id}', '').strip() or option.label
option.label = next_label
option.label_en = request.POST.get(f'label_en_{option.id}', '').strip()
option.value = request.POST.get(f'value_{option.id}', '').strip() or next_label
option.is_active = request.POST.get(f'active_{option.id}') == 'on'
option.sort_order = pos
try:
option.save(update_fields=['label', 'label_en', 'value', 'is_active', 'sort_order'])
except IntegrityError:
messages.error(request, _('Doppelte Bezeichnung in Kategorie: %(label)s') % {'label': next_label})
return redirect(f"/admin-tools/form-builder/?form_type={form_type}&option_category={option.category}&module=options")
option_category = option.category
_audit(request, 'form_options_saved', target_type='form_option', target_label=option_category, details={'count': len(option_ids)})
messages.success(request, _('Optionen wurden gespeichert.'))
elif action == 'save_field_texts':
field_ids = request.POST.getlist('field_ids')
for raw_id in field_ids:
cfg = FormFieldConfig.objects.filter(id=raw_id, form_type=form_type).first()
if not cfg:
continue
cfg.label_override = (request.POST.get(f'label_override_{cfg.id}') or '').strip()
cfg.label_override_en = (request.POST.get(f'label_override_en_{cfg.id}') or '').strip()
cfg.help_text_override = (request.POST.get(f'help_text_override_{cfg.id}') or '').strip()
cfg.help_text_override_en = (request.POST.get(f'help_text_override_en_{cfg.id}') or '').strip()
cfg.save(update_fields=['label_override', 'label_override_en', 'help_text_override', 'help_text_override_en'])
_audit(request, 'form_field_texts_saved', target_type='form_config', target_label=form_type, details={'count': len(field_ids)})
messages.success(request, _('Feldtexte wurden gespeichert.'))
elif action == 'add_custom_section' and form_type == 'onboarding':
title = (request.POST.get('custom_section_title') or '').strip()
title_en = (request.POST.get('custom_section_title_en') or '').strip()
sort_order_raw = (request.POST.get('custom_section_sort_order') or '').strip()
if not title:
messages.error(request, _('Bitte einen Titel für den benutzerdefinierten Abschnitt angeben.'))
else:
section_key_base = build_custom_field_key(title)
section_key = section_key_base
suffix = 2
while FormCustomSectionConfig.objects.filter(form_type=form_type, section_key=section_key).exists():
section_key = f'{section_key_base}_{suffix}'
suffix += 1
try:
sort_order = int(sort_order_raw or 0)
except ValueError:
sort_order = 0
FormCustomSectionConfig.objects.create(
form_type=form_type,
section_key=section_key,
sort_order=max(0, sort_order),
title=title,
title_en=title_en,
is_active=True,
)
_audit(request, 'form_custom_section_added', target_type='form_custom_section', target_label=title, details={'form_type': form_type, 'section_key': section_key})
messages.success(request, _('Benutzerdefinierter Abschnitt wurde hinzugefügt.'))
elif action == 'save_custom_sections' and form_type == 'onboarding':
section_ids = request.POST.getlist('custom_section_ids')
updated = 0
for raw_id in section_ids:
cfg = FormCustomSectionConfig.objects.filter(id=raw_id, form_type=form_type).first()
if not cfg:
continue
try:
sort_order = int((request.POST.get(f'custom_section_sort_order_{cfg.id}') or '').strip() or cfg.sort_order)
except ValueError:
sort_order = cfg.sort_order
cfg.title = (request.POST.get(f'custom_section_title_{cfg.id}') or '').strip() or cfg.title
cfg.title_en = (request.POST.get(f'custom_section_title_en_{cfg.id}') or '').strip()
cfg.is_active = request.POST.get(f'custom_section_is_active_{cfg.id}') == 'on'
cfg.sort_order = max(0, sort_order)
cfg.save(update_fields=['title', 'title_en', 'is_active', 'sort_order'])
updated += 1
_audit(request, 'form_custom_sections_saved', target_type='form_custom_section', target_label=form_type, details={'count': updated})
messages.success(request, _('Benutzerdefinierte Abschnitte wurden gespeichert.'))
elif action == 'add_custom_field':
label = (request.POST.get('custom_label') or '').strip()
label_en = (request.POST.get('custom_label_en') or '').strip()
section_key = (request.POST.get('custom_section_key') or '').strip()
field_type = (request.POST.get('custom_field_type') or '').strip()
sort_order_raw = (request.POST.get('custom_sort_order') or '').strip()
help_text = (request.POST.get('custom_help_text') or '').strip()
help_text_en = (request.POST.get('custom_help_text_en') or '').strip()
select_options = (request.POST.get('custom_select_options') or '').strip()
select_options_en = (request.POST.get('custom_select_options_en') or '').strip()
section_choices = {key for key in get_section_order(form_type)}
field_type_choices = {key for key, _ in FormCustomFieldConfig.FIELD_TYPE_CHOICES}
if not label:
messages.error(request, _('Bitte eine Bezeichnung für das benutzerdefinierte Feld angeben.'))
elif section_key not in section_choices:
messages.error(request, _('Ungültiger Abschnitt für das benutzerdefinierte Feld.'))
elif field_type not in field_type_choices:
messages.error(request, _('Ungültiger Feldtyp.'))
elif field_type == FormCustomFieldConfig.FIELD_TYPE_SELECT and not select_options:
messages.error(request, _('Auswahlfelder benötigen mindestens eine Option.'))
else:
field_key_base = build_custom_field_key(label)
field_key = field_key_base
suffix = 2
while FormCustomFieldConfig.objects.filter(form_type=form_type, field_key=field_key).exists():
field_key = f'{field_key_base}_{suffix}'
suffix += 1
try:
sort_order = int(sort_order_raw or 0)
except ValueError:
sort_order = 0
FormCustomFieldConfig.objects.create(
form_type=form_type,
field_key=field_key,
section_key=section_key,
sort_order=max(0, sort_order),
field_type=field_type,
is_active=True,
is_required=request.POST.get('custom_is_required') == 'on',
label=label,
label_en=label_en,
help_text=help_text,
help_text_en=help_text_en,
select_options=select_options,
select_options_en=select_options_en,
)
_audit(request, 'form_custom_field_added', target_type='form_custom_field', target_label=label, details={'form_type': form_type, 'field_type': field_type, 'section_key': section_key})
messages.success(request, _('Benutzerdefiniertes Feld wurde hinzugefügt.'))
elif action == 'save_custom_fields':
custom_ids = request.POST.getlist('custom_field_ids')
updated = 0
section_choices = {key for key in get_section_order(form_type)}
field_type_choices = {key for key, _ in FormCustomFieldConfig.FIELD_TYPE_CHOICES}
for raw_id in custom_ids:
cfg = FormCustomFieldConfig.objects.filter(id=raw_id, form_type=form_type).first()
if not cfg:
continue
field_type = (request.POST.get(f'custom_field_type_{cfg.id}') or '').strip()
section_key = (request.POST.get(f'custom_section_key_{cfg.id}') or '').strip()
try:
sort_order = int((request.POST.get(f'custom_sort_order_{cfg.id}') or '').strip() or cfg.sort_order)
except ValueError:
sort_order = cfg.sort_order
cfg.label = (request.POST.get(f'custom_label_{cfg.id}') or '').strip() or cfg.label
cfg.label_en = (request.POST.get(f'custom_label_en_{cfg.id}') or '').strip()
cfg.help_text = (request.POST.get(f'custom_help_text_{cfg.id}') or '').strip()
cfg.help_text_en = (request.POST.get(f'custom_help_text_en_{cfg.id}') or '').strip()
cfg.is_required = request.POST.get(f'custom_is_required_{cfg.id}') == 'on'
cfg.is_active = request.POST.get(f'custom_is_active_{cfg.id}') == 'on'
if field_type in field_type_choices:
cfg.field_type = field_type
if section_key in section_choices:
cfg.section_key = section_key
cfg.sort_order = max(0, sort_order)
cfg.select_options = (request.POST.get(f'custom_select_options_{cfg.id}') or '').strip()
cfg.select_options_en = (request.POST.get(f'custom_select_options_en_{cfg.id}') or '').strip()
if cfg.field_type == FormCustomFieldConfig.FIELD_TYPE_SELECT and not cfg.select_options:
messages.error(request, _('Auswahlfeld "%(label)s" benötigt mindestens eine Option.') % {'label': cfg.label})
return redirect(f"/admin-tools/form-builder/?form_type={form_type}&option_category={option_category}&module=custom-fields")
cfg.save()
updated += 1
_audit(request, 'form_custom_fields_saved', target_type='form_custom_field', target_label=form_type, details={'count': updated})
messages.success(request, _('Benutzerdefinierte Felder wurden gespeichert.'))
elif action == 'save_field_rules':
field_ids = request.POST.getlist('field_rule_ids')
locked_fields = LOCKED_FIELD_RULES.get(form_type, set())
updated = 0
for raw_id in field_ids:
cfg = FormFieldConfig.objects.filter(id=raw_id, form_type=form_type).first()
if not cfg:
continue
if cfg.field_name in locked_fields and not can_override_locked_builder_rules:
cfg.is_visible = True
cfg.is_required = None
else:
cfg.is_visible = request.POST.get(f'is_visible_{cfg.id}') == 'on'
required_mode = (request.POST.get(f'is_required_{cfg.id}') or '').strip()
cfg.is_required = True if required_mode == 'required' else False if required_mode == 'optional' else None
cfg.save(update_fields=['is_visible', 'is_required'])
updated += 1
_audit(request, 'form_field_rules_saved', target_type='form_config', target_label=form_type, details={'count': updated})
messages.success(request, _('Feldregeln wurden gespeichert.'))
elif action == 'save_section_rules' and form_type in {'onboarding', 'offboarding'}:
section_configs = ensure_form_section_configs(form_type)
locked_sections = LOCKED_SECTION_RULES.get(form_type, set())
posted_order = request.POST.getlist('section_order')
next_sort_order = 0
updated = 0
for section_key in posted_order:
cfg = section_configs.get(section_key)
if cfg is not None:
if cfg.sort_order != next_sort_order:
cfg.sort_order = next_sort_order
cfg.save(update_fields=['sort_order'])
updated += 1
next_sort_order += 1
continue
if form_type == 'onboarding':
custom_cfg = FormCustomSectionConfig.objects.filter(form_type=form_type, section_key=section_key).first()
if custom_cfg and custom_cfg.sort_order != next_sort_order:
custom_cfg.sort_order = next_sort_order
custom_cfg.save(update_fields=['sort_order'])
updated += 1
next_sort_order += 1
for section_key, cfg in section_configs.items():
if section_key in locked_sections and not can_override_locked_builder_rules:
if not cfg.is_visible:
cfg.is_visible = True
cfg.save(update_fields=['is_visible'])
continue
cfg.is_visible = request.POST.get(f'section_visible_{section_key}') == 'on'
cfg.save(update_fields=['is_visible'])
updated += 1
if form_type == 'onboarding':
for cfg in FormCustomSectionConfig.objects.filter(form_type=form_type):
cfg.is_active = request.POST.get(f'section_visible_{cfg.section_key}') == 'on'
cfg.save(update_fields=['is_active'])
updated += 1
_audit(request, 'form_section_rules_saved', target_type='form_config', target_label=form_type, details={'count': updated})
messages.success(request, _('Abschnittsregeln wurden gespeichert.'))
elif action == 'save_conditional_rules' and form_type == 'onboarding':
rule_configs = ensure_form_conditional_rule_configs(form_type)
updated = 0
for target_key, cfg in rule_configs.items():
cfg.is_active = request.POST.get(f'conditional_active_{target_key}') == 'on'
clauses = []
clause_total = 2
for index in range(clause_total):
field_name = (request.POST.get(f'conditional_field_{target_key}_{index}') or '').strip()
operator = (request.POST.get(f'conditional_operator_{target_key}_{index}') or '').strip()
value = (request.POST.get(f'conditional_value_{target_key}_{index}') or '').strip()
if not field_name or not operator:
continue
parsed_value = True if operator == 'checked' else value
clauses.append({'field': field_name, 'operator': operator, 'value': parsed_value})
cfg.clauses = clauses
cfg.save(update_fields=['is_active', 'clauses'])
updated += 1
_audit(request, 'form_conditional_rules_saved', target_type='form_config', target_label=form_type, details={'count': updated})
messages.success(request, _('Bedingte Logik wurde gespeichert.'))
elif action == 'apply_preset':
preset_key = (request.POST.get('preset_key') or '').strip()
if apply_form_preset(form_type, preset_key):
active_module = 'preview'
_audit(request, 'form_preset_applied', target_type='form_config', target_label=form_type, details={'preset': preset_key})
messages.success(request, _('Preset wurde angewendet.'))
else:
messages.error(request, _('Preset konnte nicht angewendet werden.'))
if action in {'add_option', 'save_options'}:
active_module = 'options'
elif action == 'save_field_texts':
active_module = 'field-texts'
elif action in {'add_custom_field', 'save_custom_fields'}:
active_module = 'custom-fields'
elif action in {'add_custom_section', 'save_custom_sections'}:
active_module = 'custom-sections'
elif action in {'save_field_rules', 'save_section_rules', 'save_conditional_rules'}:
active_module = 'section-rules'
if action == 'save_section_rules':
active_module = 'section-rules'
elif action == 'save_field_rules':
active_module = 'field-rules'
elif action == 'save_conditional_rules':
active_module = 'conditional-rules'
redirect_target = f"/admin-tools/form-builder/?form_type={form_type}&option_category={option_category}"
if active_module:
redirect_target += f"&module={active_module}"
if active_structure_section:
redirect_target += f"&structure_section={active_structure_section}"
if active_section_rules_section and active_module == 'section-rules':
redirect_target += f"&section_rules_section={active_section_rules_section}"
if active_field_rules_section and active_module == 'field-rules':
redirect_target += f"&field_rules_section={active_field_rules_section}"
if active_conditional_target and active_module == 'conditional-rules':
redirect_target += f"&conditional_target={active_conditional_target}"
if active_field_texts_section and active_module == 'field-texts':
redirect_target += f"&field_texts_section={active_field_texts_section}"
if active_custom_fields_section and active_module == 'custom-fields':
redirect_target += f"&custom_fields_section={active_custom_fields_section}"
return redirect(redirect_target)
default_names = list(DEFAULT_FIELD_ORDER.get(form_type, []))
existing_names = list(
OnboardingRequestForm.base_fields.keys()
if form_type == 'onboarding'
else OffboardingRequestForm.base_fields.keys()
)
for name in existing_names:
if name not in default_names:
default_names.append(name)
ensure_form_field_configs(form_type, default_names)
section_configs = ensure_form_section_configs(form_type)
conditional_rule_configs = ensure_form_conditional_rule_configs(form_type) if form_type == 'onboarding' else {}
section_definitions = get_section_definitions(form_type, include_inactive_custom=True)
section_order = [item['key'] for item in section_definitions]
section_labels = {item['key']: item['title'] for item in section_definitions}
default_page_map = get_default_page_map(form_type)
configs = list(
FormFieldConfig.objects.filter(form_type=form_type).order_by('sort_order', 'field_name')
)
labels = _form_field_labels(form_type)
locked = LOCKED_FIELD_RULES.get(form_type, set())
locked_sections = LOCKED_SECTION_RULES.get(form_type, set())
custom_field_configs = list(FormCustomFieldConfig.objects.filter(form_type=form_type).order_by('section_key', 'sort_order', 'field_key'))
custom_section_configs = get_custom_section_configs(form_type, include_inactive=True)
if form_type == 'onboarding':
columns = [
{
'key': key,
'title': section_labels.get(key, key),
'items': [],
}
for key in section_order
]
column_by_key = {c['key']: c for c in columns}
fallback = 'abschluss'
for cfg in configs:
page_key = cfg.page_key or ONBOARDING_DEFAULT_PAGE.get(cfg.field_name, fallback)
if page_key not in column_by_key:
page_key = fallback
column_by_key[page_key]['items'].append(
{
'field_name': cfg.field_name,
'label': cfg.translated_label_override(language_code) or labels.get(cfg.field_name, cfg.field_name),
'label_de': cfg.label_override or labels.get(cfg.field_name, cfg.field_name),
'label_en': cfg.label_override_en,
'is_visible': cfg.is_visible,
'is_required': cfg.is_required,
'locked': cfg.field_name in locked,
'page_key': page_key,
'is_custom': False,
'sort_order': cfg.sort_order,
}
)
for cfg in custom_field_configs:
page_key = cfg.section_key or fallback
if page_key not in column_by_key:
page_key = fallback
column_by_key[page_key]['items'].append(
{
'field_name': f'custom__{cfg.field_key}',
'label': cfg.translated_label(language_code),
'label_de': cfg.label,
'label_en': cfg.label_en,
'is_visible': cfg.is_active,
'is_required': cfg.is_required,
'locked': False,
'page_key': page_key,
'is_custom': True,
'sort_order': cfg.sort_order,
}
)
for column in columns:
column['items'].sort(key=lambda item: (item.get('sort_order', 9999), item['field_name']))
else:
columns = [
{
'key': key,
'title': section_labels.get(key, key),
'items': [],
}
for key in section_order
]
column_by_key = {c['key']: c for c in columns}
fallback = section_order[-1] if section_order else 'all'
for cfg in configs:
page_key = cfg.page_key or default_page_map.get(cfg.field_name, fallback)
if page_key not in column_by_key:
page_key = fallback
column_by_key[page_key]['items'].append(
{
'field_name': cfg.field_name,
'label': cfg.translated_label_override(language_code) or labels.get(cfg.field_name, cfg.field_name),
'label_de': cfg.label_override or labels.get(cfg.field_name, cfg.field_name),
'label_en': cfg.label_override_en,
'is_visible': cfg.is_visible,
'is_required': cfg.is_required,
'locked': cfg.field_name in locked,
'page_key': page_key,
'is_custom': False,
'sort_order': cfg.sort_order,
}
)
for cfg in custom_field_configs:
page_key = cfg.section_key or fallback
if page_key not in column_by_key:
page_key = fallback
column_by_key[page_key]['items'].append(
{
'field_name': f'custom__{cfg.field_key}',
'label': cfg.translated_label(language_code),
'label_de': cfg.label,
'label_en': cfg.label_en,
'is_visible': cfg.is_active,
'is_required': cfg.is_required,
'locked': False,
'page_key': page_key,
'is_custom': True,
'sort_order': cfg.sort_order,
}
)
for column in columns:
column['items'].sort(key=lambda item: (item.get('sort_order', 9999), item['field_name']))
section_rule_items = []
if section_order:
if active_structure_section not in section_order:
active_structure_section = section_order[0]
fallback_section = section_order[-1] if section_order else ''
custom_section_map = {cfg.section_key: cfg for cfg in custom_section_configs}
for key in section_order:
cfg = section_configs.get(key)
custom_cfg = custom_section_map.get(key)
is_custom = custom_cfg is not None
raw_title = str(section_labels.get(key, key))
display_title = re.sub(r'^\d+\.\s*', '', raw_title) if not is_custom else raw_title
section_rule_items.append(
{
'key': key,
'title': raw_title,
'display_title': display_title,
'is_visible': bool(custom_cfg.is_active) if is_custom else (True if not cfg else cfg.is_visible),
'locked': False if is_custom else (key in locked_sections and not can_override_locked_builder_rules),
'is_custom': is_custom,
'sort_order': custom_cfg.sort_order if is_custom else (cfg.sort_order if cfg else 0),
'field_count': len([c for c in configs if (c.page_key or default_page_map.get(c.field_name, fallback_section)) == key]) + len([c for c in custom_field_configs if c.section_key == key]),
}
)
section_rule_keys = [item['key'] for item in section_rule_items]
if section_rule_keys and active_section_rules_section not in section_rule_keys:
active_section_rules_section = section_rule_keys[0]
field_rule_items = []
for cfg in configs:
page_key = cfg.page_key or default_page_map.get(cfg.field_name, section_order[-1] if section_order else '')
field_rule_items.append(
{
'id': cfg.id,
'field_name': cfg.field_name,
'label': cfg.translated_label_override(language_code) or labels.get(cfg.field_name, cfg.field_name),
'page_key': page_key,
'page_label': section_labels.get(page_key, page_key) if section_order else '',
'is_visible': cfg.is_visible,
'is_required': cfg.is_required,
'locked': cfg.field_name in locked and not can_override_locked_builder_rules,
'summary': _field_rule_summary(
is_visible=cfg.is_visible,
is_required=cfg.is_required,
locked=cfg.field_name in locked and not can_override_locked_builder_rules,
),
}
)
custom_field_groups = []
if section_order:
grouped_custom = {key: [] for key in section_order}
for cfg in custom_field_configs:
grouped_custom.setdefault(cfg.section_key, []).append(cfg)
for key in section_order:
custom_field_groups.append(
{
'key': key,
'title': section_labels.get(key, key),
'items': grouped_custom.get(key, []),
}
)
custom_section_field_counts: dict[str, int] = {}
for cfg in custom_field_configs:
custom_section_field_counts[cfg.section_key] = custom_section_field_counts.get(cfg.section_key, 0) + 1
for cfg in custom_section_configs:
cfg.custom_field_count = custom_section_field_counts.get(cfg.section_key, 0)
field_rule_groups = []
if section_order:
grouped_rules = {key: [] for key in section_order}
for item in field_rule_items:
grouped_rules.setdefault(item['page_key'], []).append(item)
for key in section_order:
field_rule_groups.append(
{
'key': key,
'title': section_labels.get(key, key),
'items': grouped_rules.get(key, []),
}
)
field_rule_group_keys = [group['key'] for group in field_rule_groups]
if field_rule_group_keys and active_field_rules_section not in field_rule_group_keys:
active_field_rules_section = field_rule_group_keys[0]
field_text_groups = []
if section_order:
grouped_texts = {key: [] for key in section_order}
for cfg in configs:
page_key = cfg.page_key or default_page_map.get(cfg.field_name, section_order[-1] if section_order else '')
grouped_texts.setdefault(page_key, []).append(cfg)
for key in section_order:
field_text_groups.append(
{
'key': key,
'title': section_labels.get(key, key),
'items': grouped_texts.get(key, []),
}
)
field_text_group_keys = [group['key'] for group in field_text_groups]
if field_text_group_keys and active_field_texts_section not in field_text_group_keys:
active_field_texts_section = field_text_group_keys[0]
custom_field_group_keys = [group['key'] for group in custom_field_groups]
if custom_field_group_keys and active_custom_fields_section not in custom_field_group_keys:
active_custom_fields_section = custom_field_group_keys[0]
conditional_rule_items = []
if form_type == 'onboarding':
conditional_field_choices = []
for field_name in [
'order_business_cards',
'employment_type',
'group_mailboxes_required_choice',
'additional_hardware_needed_choice',
'additional_software_needed_choice',
'additional_access_needed_choice',
'successor_required_choice',
'inherit_phone_number_choice',
]:
conditional_field_choices.append((field_name, labels.get(field_name, field_name)))
for cfg in custom_field_configs:
conditional_field_choices.append((f'custom__{cfg.field_key}', cfg.translated_label(language_code)))
conditional_field_label_map = {value: label for value, label in conditional_field_choices}
conditional_target_titles = {
'business-card-box': _('Visitenkarten-Details'),
'employment-end-box': _('Vertragsende'),
'group-mailboxes-box': _('Gruppenpostfächer'),
'extra-hardware-box': _('Zusätzliche Hardware'),
'extra-software-box': _('Zusätzliche Software'),
'extra-access-box': _('Zusätzliche Zugänge'),
'successor-box': _('Nachfolge'),
}
conditional_target_descriptions = {
'business-card-box': _('Steuert die Detailfelder für Visitenkarten.'),
'employment-end-box': _('Steuert das Enddatum bei befristeter Beschäftigung.'),
'group-mailboxes-box': _('Steuert das Freitextfeld für Gruppenpostfächer.'),
'extra-hardware-box': _('Steuert zusätzliche Hardware-Felder.'),
'extra-software-box': _('Steuert zusätzliche Software-Felder.'),
'extra-access-box': _('Steuert zusätzliche Zugangsangaben.'),
'successor-box': _('Steuert Nachfolge- und Übernahmefelder.'),
}
for target_key, cfg in conditional_rule_configs.items():
clauses = list(cfg.clauses or [])
while len(clauses) < 2:
clauses.append({'field': '', 'operator': 'equals', 'value': ''})
if target_key.startswith('custom__'):
custom_field_key = target_key.replace('custom__', '', 1)
custom_field = next((item for item in custom_field_configs if item.field_key == custom_field_key), None)
target_title = custom_field.translated_label(language_code) if custom_field else target_key
target_description = _('Steuert die Sichtbarkeit dieses benutzerdefinierten Feldes.')
target_fields = [target_title]
else:
target_title = conditional_target_titles.get(target_key, target_key)
target_description = conditional_target_descriptions.get(target_key, '')
target_fields = [labels.get(name, name) for name in ONBOARDING_GROUPS.get(target_key, [])]
conditional_rule_items.append(
{
'target_key': target_key,
'title': target_title,
'description': target_description,
'is_active': cfg.is_active,
'clauses': clauses[:2],
'summary': _conditional_rule_summary(clauses[:2], conditional_field_label_map),
'field_choices': conditional_field_choices,
'operator_choices': CONDITIONAL_RULE_OPERATOR_CHOICES,
'target_fields': target_fields,
}
)
conditional_rule_keys = [item['target_key'] for item in conditional_rule_items]
if conditional_rule_keys and active_conditional_target not in conditional_rule_keys:
active_conditional_target = conditional_rule_keys[0]
preview_sections = []
if section_order:
field_rule_group_map = {group['key']: group['items'] for group in field_rule_groups}
for key in section_order:
section_cfg = section_configs.get(key)
section_locked = key in locked_sections
custom_section = next((cfg for cfg in custom_section_configs if cfg.section_key == key), None)
section_visible = bool(custom_section.is_active) if custom_section else (True if section_locked or not section_cfg else section_cfg.is_visible)
visible_items = [
item for item in field_rule_group_map.get(key, [])
if item['locked'] or item['is_visible']
]
visible_items.extend(
[
{
'label': cfg.translated_label(language_code),
'locked': False,
}
for cfg in custom_field_configs
if cfg.section_key == key and cfg.is_active
]
)
if section_visible:
preview_sections.append(
{
'key': key,
'title': section_labels.get(key, key),
'items': visible_items,
}
)
locked_field_count = len([item for item in field_rule_items if item['locked']])
hidden_field_count = len([item for item in field_rule_items if not item['is_visible']])
configurable_field_count = len(field_rule_items) - locked_field_count
hidden_section_count = len([item for item in section_rule_items if not item['is_visible']]) if section_rule_items else 0
builder_summary = {
'locked_field_count': locked_field_count,
'configurable_field_count': configurable_field_count,
'hidden_field_count': hidden_field_count,
'hidden_section_count': hidden_section_count,
'custom_field_count': len([cfg for cfg in custom_field_configs if cfg.is_active]),
'custom_section_count': len([cfg for cfg in custom_section_configs if cfg.is_active]),
}
module_labels = {
'structure': _('Struktur & Reihenfolge'),
'section-rules': _('Abschnitte'),
'field-rules': _('Feldregeln'),
'conditional-rules': _('Bedingte Logik'),
'options': _('Optionen'),
'field-texts': _('Feldtexte'),
'custom-sections': _('Eigene Abschnitte'),
'custom-fields': _('Eigene Felder'),
'preview': _('Vorschau'),
}
option_category_labels = dict(_translate_choice_list(FormOption.CATEGORY_CHOICES))
form_type_labels = {
'onboarding': _('Onboarding'),
'offboarding': _('Offboarding'),
}
active_focus_label = ''
if active_module == 'structure' and active_structure_section:
active_focus_label = section_labels.get(active_structure_section, active_structure_section)
elif active_module == 'section-rules' and section_rule_items:
active_focus_label = _('Alle Abschnitte')
elif active_module == 'field-rules' and active_field_rules_section:
active_focus_label = section_labels.get(active_field_rules_section, active_field_rules_section)
elif active_module == 'conditional-rules' and active_conditional_target:
active_focus_label = next((item['title'] for item in conditional_rule_items if item['target_key'] == active_conditional_target), active_conditional_target)
elif active_module == 'options':
active_focus_label = option_category_labels.get(option_category, option_category)
elif active_module == 'field-texts' and active_field_texts_section:
active_focus_label = section_labels.get(active_field_texts_section, active_field_texts_section)
elif active_module == 'custom-sections':
active_focus_label = _('Onboarding')
elif active_module == 'custom-fields' and active_custom_fields_section:
active_focus_label = section_labels.get(active_custom_fields_section, active_custom_fields_section)
elif active_module == 'preview':
active_focus_label = _('Live-Vorschau')
return render(
request,
'workflows/form_builder.html',
{
'form_type': form_type,
'columns': columns,
'form_types': [('onboarding', _('Onboarding')), ('offboarding', _('Offboarding'))],
'option_categories': _translate_choice_list(FormOption.CATEGORY_CHOICES),
'selected_option_category': option_category,
'option_items': FormOption.objects.filter(category=option_category).order_by('sort_order', 'label'),
'field_text_items': configs,
'field_rule_items': field_rule_items,
'field_rule_groups': field_rule_groups,
'field_text_groups': field_text_groups,
'preview_sections': preview_sections,
'section_rule_items': section_rule_items,
'builder_summary': builder_summary,
'conditional_rule_items': conditional_rule_items,
'custom_field_groups': custom_field_groups,
'custom_field_type_choices': _translate_choice_list(FormCustomFieldConfig.FIELD_TYPE_CHOICES),
'custom_section_items': custom_section_configs,
'active_panel': active_panel,
'active_subpanel': active_subpanel,
'active_rules_panel': active_rules_panel,
'active_module': active_module,
'active_form_type_label': form_type_labels.get(form_type, form_type),
'active_module_label': module_labels.get(active_module, active_module),
'active_focus_label': active_focus_label,
'active_structure_section': active_structure_section,
'active_field_rules_section': active_field_rules_section,
'active_field_texts_section': active_field_texts_section,
'active_custom_fields_section': active_custom_fields_section,
'active_section_rules_section': active_section_rules_section,
'active_conditional_target': active_conditional_target,
'available_presets': FORM_PRESETS.get(form_type, {}),
'can_override_locked_builder_rules': can_override_locked_builder_rules,
},
)