import json import re from django.contrib import messages from django.db import IntegrityError from django.http import JsonResponse 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 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"§ion_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, }, ) def form_builder_save_order_impl(request, *, audit_fn): try: payload = json.loads(request.body.decode('utf-8')) except (json.JSONDecodeError, UnicodeDecodeError): return JsonResponse({'ok': False, 'error': _('Ungültige JSON-Daten.')}, status=400) form_type = payload.get('form_type') if form_type not in DEFAULT_FIELD_ORDER: return JsonResponse({'ok': False, 'error': _('Ungültiger Formulartyp.')}, status=400) default_page_map = get_default_page_map(form_type) columns = payload.get('columns') if not isinstance(columns, dict): return JsonResponse({'ok': False, 'error': _('Spalten-Daten fehlen.')}, status=400) configs = list(FormFieldConfig.objects.filter(form_type=form_type).order_by('sort_order', 'field_name')) custom_configs = list(FormCustomFieldConfig.objects.filter(form_type=form_type).order_by('sort_order', 'field_key')) allowed_names = {cfg.field_name for cfg in configs} | {f'custom__{cfg.field_key}' for cfg in custom_configs} seen = set() allowed_columns = get_section_order(form_type) fallback_section = allowed_columns[-1] if allowed_columns else '' name_to_cfg = {cfg.field_name: cfg for cfg in configs} custom_name_to_cfg = {f'custom__{cfg.field_key}': cfg for cfg in custom_configs} sort_order = 0 for column_key in allowed_columns: names = columns.get(column_key, []) if not isinstance(names, list): return JsonResponse({'ok': False, 'error': _('Ungültige Spalte: %(column)s') % {'column': column_key}}, status=400) for name in names: if not isinstance(name, str): continue if name not in allowed_names or name in seen: continue seen.add(name) if name in name_to_cfg: cfg = name_to_cfg[name] cfg.sort_order = sort_order cfg.page_key = column_key else: cfg = custom_name_to_cfg[name] cfg.sort_order = sort_order cfg.section_key = column_key sort_order += 1 missing = [cfg.field_name for cfg in configs if cfg.field_name not in seen] for name in missing: cfg = name_to_cfg[name] cfg.sort_order = sort_order sort_order += 1 cfg.page_key = cfg.page_key or default_page_map.get(name, fallback_section) missing_custom = [name for name in custom_name_to_cfg.keys() if name not in seen] for name in missing_custom: cfg = custom_name_to_cfg[name] cfg.sort_order = sort_order sort_order += 1 cfg.section_key = cfg.section_key or fallback_section FormFieldConfig.objects.bulk_update(configs, ['sort_order', 'page_key']) if custom_configs: FormCustomFieldConfig.objects.bulk_update(custom_configs, ['sort_order', 'section_key']) saved_count = len(configs) + len(custom_configs) audit_fn(request, 'form_layout_saved', target_type='form_config', target_label=form_type, details={'saved_count': saved_count}) return JsonResponse({'ok': True, 'saved_count': saved_count})