diff --git a/backend/workflows/form_builder.py b/backend/workflows/form_builder.py index a366a38..fa2094a 100644 --- a/backend/workflows/form_builder.py +++ b/backend/workflows/form_builder.py @@ -1,7 +1,7 @@ from collections import OrderedDict from django.utils.translation import get_language -from .models import FormFieldConfig, FormSectionConfig +from .models import FormConditionalRuleConfig, FormFieldConfig, FormSectionConfig DEFAULT_FIELD_ORDER = { @@ -132,6 +132,38 @@ OFFBOARDING_DEFAULT_PAGE = { 'notes': 'abschluss', } +DEFAULT_CONDITIONAL_RULES = { + 'onboarding': { + 'business-card-box': { + 'clauses': [{'field': 'order_business_cards', 'operator': 'checked', 'value': True}], + }, + 'employment-end-box': { + 'clauses': [{'field': 'employment_type', 'operator': 'equals', 'value': 'befristet'}], + }, + 'group-mailboxes-box': { + 'clauses': [{'field': 'group_mailboxes_required_choice', 'operator': 'equals', 'value': 'ja'}], + }, + 'extra-hardware-box': { + 'clauses': [{'field': 'additional_hardware_needed_choice', 'operator': 'equals', 'value': 'ja'}], + }, + 'extra-software-box': { + 'clauses': [{'field': 'additional_software_needed_choice', 'operator': 'equals', 'value': 'ja'}], + }, + 'extra-access-box': { + 'clauses': [{'field': 'additional_access_needed_choice', 'operator': 'equals', 'value': 'ja'}], + }, + 'successor-box': { + 'clauses': [{'field': 'successor_required_choice', 'operator': 'equals', 'value': 'ja'}], + }, + 'phone-box': { + 'clauses': [ + {'field': 'successor_required_choice', 'operator': 'equals', 'value': 'ja'}, + {'field': 'inherit_phone_number_choice', 'operator': 'not_equals', 'value': 'ja'}, + ], + }, + }, +} + def get_section_order(form_type: str) -> list[str]: if form_type == 'onboarding': @@ -157,6 +189,10 @@ def get_default_page_map(form_type: str) -> dict[str, str]: return {} +def get_default_conditional_rules(form_type: str) -> dict[str, dict]: + return DEFAULT_CONDITIONAL_RULES.get(form_type, {}) + + def _default_sort(form_type: str, field_name: str) -> int: ordered = DEFAULT_FIELD_ORDER.get(form_type, []) if field_name in ordered: @@ -215,6 +251,35 @@ def ensure_form_section_configs(form_type: str) -> dict[str, FormSectionConfig]: return existing +def ensure_form_conditional_rule_configs(form_type: str) -> dict[str, FormConditionalRuleConfig]: + defaults = get_default_conditional_rules(form_type) + if not defaults: + return {} + existing = { + cfg.target_key: cfg + for cfg in FormConditionalRuleConfig.objects.filter(form_type=form_type) + } + missing = [key for key in defaults.keys() if key not in existing] + if missing: + FormConditionalRuleConfig.objects.bulk_create( + [ + FormConditionalRuleConfig( + form_type=form_type, + target_key=key, + clauses=defaults[key].get('clauses', []), + is_active=True, + ) + for key in missing + ], + ignore_conflicts=True, + ) + existing = { + cfg.target_key: cfg + for cfg in FormConditionalRuleConfig.objects.filter(form_type=form_type) + } + return existing + + def apply_form_field_config(form_type: str, form) -> None: field_names = list(form.fields.keys()) configs = _ensure_configs(form_type, field_names) diff --git a/backend/workflows/migrations/0054_formconditionalruleconfig.py b/backend/workflows/migrations/0054_formconditionalruleconfig.py new file mode 100644 index 0000000..f1e705a --- /dev/null +++ b/backend/workflows/migrations/0054_formconditionalruleconfig.py @@ -0,0 +1,67 @@ +from django.db import migrations, models + + +DEFAULT_RULES = { + 'business-card-box': [ + {'field': 'order_business_cards', 'operator': 'checked', 'value': True}, + ], + 'employment-end-box': [ + {'field': 'employment_type', 'operator': 'equals', 'value': 'befristet'}, + ], + 'group-mailboxes-box': [ + {'field': 'group_mailboxes_required_choice', 'operator': 'equals', 'value': 'ja'}, + ], + 'extra-hardware-box': [ + {'field': 'additional_hardware_needed_choice', 'operator': 'equals', 'value': 'ja'}, + ], + 'extra-software-box': [ + {'field': 'additional_software_needed_choice', 'operator': 'equals', 'value': 'ja'}, + ], + 'extra-access-box': [ + {'field': 'additional_access_needed_choice', 'operator': 'equals', 'value': 'ja'}, + ], + 'successor-box': [ + {'field': 'successor_required_choice', 'operator': 'equals', 'value': 'ja'}, + ], + 'phone-box': [ + {'field': 'successor_required_choice', 'operator': 'equals', 'value': 'ja'}, + {'field': 'inherit_phone_number_choice', 'operator': 'not_equals', 'value': 'ja'}, + ], +} + + +def seed_conditional_rules(apps, schema_editor): + FormConditionalRuleConfig = apps.get_model('workflows', 'FormConditionalRuleConfig') + for target_key, clauses in DEFAULT_RULES.items(): + FormConditionalRuleConfig.objects.get_or_create( + form_type='onboarding', + target_key=target_key, + defaults={'clauses': clauses, 'is_active': True}, + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ('workflows', '0053_formsectionconfig'), + ] + + operations = [ + migrations.CreateModel( + name='FormConditionalRuleConfig', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('form_type', models.CharField(choices=[('onboarding', 'Onboarding')], max_length=20)), + ('target_key', models.CharField(max_length=80)), + ('clauses', models.JSONField(blank=True, default=list)), + ('is_active', models.BooleanField(default=True)), + ], + options={ + 'verbose_name': 'Formular-Bedingungsregel', + 'verbose_name_plural': 'Formular-Bedingungsregeln', + 'ordering': ['form_type', 'target_key'], + 'unique_together': {('form_type', 'target_key')}, + }, + ), + migrations.RunPython(seed_conditional_rules, migrations.RunPython.noop), + ] diff --git a/backend/workflows/models.py b/backend/workflows/models.py index f524efa..57ceb76 100644 --- a/backend/workflows/models.py +++ b/backend/workflows/models.py @@ -546,6 +546,26 @@ class FormSectionConfig(models.Model): return f'{self.form_type}: {self.section_key}' +class FormConditionalRuleConfig(models.Model): + FORM_CHOICES = [ + ('onboarding', _('Onboarding')), + ] + + form_type = models.CharField(max_length=20, choices=FORM_CHOICES) + target_key = models.CharField(max_length=80) + clauses = models.JSONField(default=list, blank=True) + is_active = models.BooleanField(default=True) + + class Meta: + ordering = ['form_type', 'target_key'] + unique_together = ('form_type', 'target_key') + verbose_name = 'Formular-Bedingungsregel' + verbose_name_plural = 'Formular-Bedingungsregeln' + + def __str__(self) -> str: + return f'{self.form_type}: {self.target_key}' + + class NotificationTemplate(models.Model): TEMPLATE_CHOICES = [ ('onboarding_it', _('Onboarding: IT')), diff --git a/backend/workflows/static/workflows/js/onboarding_form.js b/backend/workflows/static/workflows/js/onboarding_form.js index b33ac38..534355a 100644 --- a/backend/workflows/static/workflows/js/onboarding_form.js +++ b/backend/workflows/static/workflows/js/onboarding_form.js @@ -6,6 +6,8 @@ const btnSubmit = document.getElementById('btn-submit'); const form = document.getElementById('onboarding-form'); const emailDomain = ((form && form.dataset.emailDomain) || 'workdock.de').replace(/^@+/, '').trim(); + const conditionalRulesNode = document.getElementById('onboarding-conditional-rules'); + const conditionalRules = conditionalRulesNode ? JSON.parse(conditionalRulesNode.textContent || '{}') : {}; let current = 0; form.setAttribute('novalidate', 'novalidate'); @@ -14,34 +16,39 @@ const el = document.getElementById(id); if (!el) return; el.classList.toggle('hidden', !state); + el.setAttribute('aria-hidden', state ? 'false' : 'true'); + } + + function fieldState(name) { + const field = byName(name); + if (!field) return { exists: false, value: '', checked: false }; + return { + exists: true, + value: (field.value || '').trim(), + checked: !!field.checked, + }; + } + + function evaluateClause(clause) { + const state = fieldState(clause.field); + if (!state.exists) return false; + if (clause.operator === 'checked') return state.checked === !!clause.value; + if (clause.operator === 'equals') return state.value === String(clause.value); + if (clause.operator === 'not_equals') return state.value !== String(clause.value); + return false; + } + + function evaluateRule(rule) { + const all = Array.isArray(rule.all) ? rule.all : []; + return all.every(evaluateClause); } function syncConditionals() { - const orderCards = byName('order_business_cards'); - toggle('business-card-box', orderCards && orderCards.checked); - - const employmentType = byName('employment_type'); - toggle('employment-end-box', employmentType && employmentType.value === 'befristet'); - - const groupMailbox = byName('group_mailboxes_required_choice'); - toggle('group-mailboxes-box', groupMailbox && groupMailbox.value === 'ja'); - - const extraHardware = byName('additional_hardware_needed_choice'); - toggle('extra-hardware-box', extraHardware && extraHardware.value === 'ja'); - - const extraSoftware = byName('additional_software_needed_choice'); - toggle('extra-software-box', extraSoftware && extraSoftware.value === 'ja'); - - const extraAccess = byName('additional_access_needed_choice'); - toggle('extra-access-box', extraAccess && extraAccess.value === 'ja'); - - const successor = byName('successor_required_choice'); - const showSuccessor = successor && successor.value === 'ja'; - toggle('successor-box', showSuccessor); - - const inheritPhone = byName('inherit_phone_number_choice'); - const hidePhone = showSuccessor && inheritPhone && inheritPhone.value === 'ja'; - toggle('phone-box', !hidePhone); + Object.entries(conditionalRules).forEach(function (entry) { + const targetId = entry[0]; + const rule = entry[1] || {}; + toggle(targetId, evaluateRule(rule)); + }); // Hidden conditional groups must not block submit with invisible required fields. document.querySelectorAll('.field-group').forEach(function (group) { @@ -60,6 +67,22 @@ }); } + function setupConditionalBindings() { + const watched = new Set(); + Object.values(conditionalRules).forEach(function (rule) { + const all = Array.isArray(rule.all) ? rule.all : []; + all.forEach(function (clause) { + if (clause.field) watched.add(clause.field); + }); + }); + watched.forEach(function (name) { + const field = byName(name); + if (!field) return; + field.addEventListener('change', syncConditionals); + field.addEventListener('input', syncConditionals); + }); + } + function slugifyForEmail(value) { const map = { 'ä': 'ae', 'ö': 'oe', 'ü': 'ue', 'ß': 'ss' }; const lower = (value || '').toLowerCase(); @@ -224,7 +247,6 @@ current = Math.max(0, step); } - document.addEventListener('change', syncConditionals); navItems.forEach((n, idx) => { n.addEventListener('click', function () { current = idx; updateStep(); }); n.addEventListener('keydown', function (e) { @@ -245,6 +267,7 @@ }); syncConditionals(); + setupConditionalBindings(); setupWorkEmailAutofill(); setupBusinessCardAutofill(); setupChecklistToggles(); diff --git a/backend/workflows/templates/workflows/form_builder.html b/backend/workflows/templates/workflows/form_builder.html index 2e1cef1..f14536a 100644 --- a/backend/workflows/templates/workflows/form_builder.html +++ b/backend/workflows/templates/workflows/form_builder.html @@ -246,6 +246,70 @@ + + {% if form_type == 'onboarding' %} +
+
+

{% trans "Bedingte Logik" %}

+
+
+ {% csrf_token %} +
+ {% for item in conditional_rule_items %} +
+
+

{{ item.title }}

+ {{ item.target_fields|join:", " }} +
+
+
+
+ {{ item.title }} +
{{ item.description }}
+
+ +
+ {% for clause in item.clauses %} +
+
+ {% blocktrans trimmed with number=forloop.counter %}Bedingung {{ number }}{% endblocktrans %} +
+ + + +
+ {% endfor %} +
+
+ {% endfor %} +
+
+ +
+
+
+ {% endif %} diff --git a/backend/workflows/templates/workflows/onboarding_form.html b/backend/workflows/templates/workflows/onboarding_form.html index 9a2953a..121cc05 100644 --- a/backend/workflows/templates/workflows/onboarding_form.html +++ b/backend/workflows/templates/workflows/onboarding_form.html @@ -44,6 +44,7 @@
{% csrf_token %} + {{ onboarding_conditional_rules|json_script:"onboarding-conditional-rules" }} {% for section in onboarding_sections %}
@@ -92,7 +93,11 @@ {% endif %} {% endwith %} {% else %} -
+
{% for field in block.fields %} {% if field.is_hidden %} diff --git a/backend/workflows/tests/test_form_builder_admin.py b/backend/workflows/tests/test_form_builder_admin.py index ab75efd..69ee28e 100644 --- a/backend/workflows/tests/test_form_builder_admin.py +++ b/backend/workflows/tests/test_form_builder_admin.py @@ -3,7 +3,7 @@ import json from django.contrib.auth import get_user_model from django.test import TestCase -from workflows.models import FormFieldConfig, FormOption, FormSectionConfig +from workflows.models import FormConditionalRuleConfig, FormFieldConfig, FormOption, FormSectionConfig class FormBuilderAdminTests(TestCase): @@ -172,3 +172,33 @@ class FormBuilderAdminTests(TestCase): self.assertEqual(notes.is_visible, True) self.assertEqual(notes.is_required, True) self.assertEqual(department.is_required, True) + + def test_staff_can_save_onboarding_conditional_rules(self): + self.client.force_login(self.staff) + self.client.get('/admin-tools/form-builder/?form_type=onboarding', HTTP_HOST='localhost') + + response = self.client.post( + '/admin-tools/form-builder/?form_type=onboarding&option_category=device', + data={ + 'builder_action': 'save_conditional_rules', + 'conditional_active_employment-end-box': 'on', + 'conditional_field_employment-end-box_0': 'employment_type', + 'conditional_operator_employment-end-box_0': 'equals', + 'conditional_value_employment-end-box_0': 'befristet', + 'conditional_active_phone-box': 'on', + 'conditional_field_phone-box_0': 'successor_required_choice', + 'conditional_operator_phone-box_0': 'equals', + 'conditional_value_phone-box_0': 'ja', + 'conditional_field_phone-box_1': 'inherit_phone_number_choice', + 'conditional_operator_phone-box_1': 'not_equals', + 'conditional_value_phone-box_1': 'ja', + }, + HTTP_HOST='localhost', + ) + + self.assertEqual(response.status_code, 302) + rule = FormConditionalRuleConfig.objects.get(form_type='onboarding', target_key='phone-box') + self.assertEqual(rule.is_active, True) + self.assertEqual(len(rule.clauses), 2) + self.assertEqual(rule.clauses[0]['field'], 'successor_required_choice') + self.assertEqual(rule.clauses[1]['operator'], 'not_equals') diff --git a/backend/workflows/tests/test_onboarding_flow.py b/backend/workflows/tests/test_onboarding_flow.py index a91a714..c43bad6 100644 --- a/backend/workflows/tests/test_onboarding_flow.py +++ b/backend/workflows/tests/test_onboarding_flow.py @@ -4,7 +4,7 @@ from django.contrib.auth import get_user_model from django.test import TestCase from workflows.branding import get_company_email_domain -from workflows.models import FormFieldConfig, FormSectionConfig, OnboardingRequest +from workflows.models import FormConditionalRuleConfig, FormFieldConfig, FormSectionConfig, OnboardingRequest class OnboardingFlowTests(TestCase): @@ -143,3 +143,31 @@ class OnboardingFlowTests(TestCase): self.assertEqual(submit_response.status_code, 302) self.assertTrue(OnboardingRequest.objects.filter(work_email=f'nora.section@{self.company_domain}').exists()) mock_delay.assert_called_once() + + def test_onboarding_page_renders_conditional_rules_payload(self): + response = self.client.get('/onboarding/new/', HTTP_HOST='localhost') + html = response.content.decode('utf-8') + + self.assertEqual(response.status_code, 200) + self.assertIn('id="onboarding-conditional-rules"', html) + self.assertIn('business-card-box', html) + self.assertIn('employment-end-box', html) + self.assertIn('data-conditional-target="business-card-box"', html) + self.assertIn('data-conditional-target="phone-box"', html) + + def test_onboarding_page_uses_stored_conditional_rule_config(self): + FormConditionalRuleConfig.objects.update_or_create( + form_type='onboarding', + target_key='employment-end-box', + defaults={ + 'is_active': True, + 'clauses': [{'field': 'employment_type', 'operator': 'equals', 'value': 'unbefristet'}], + }, + ) + + response = self.client.get('/onboarding/new/', HTTP_HOST='localhost') + html = response.content.decode('utf-8') + + self.assertEqual(response.status_code, 200) + self.assertIn('employment-end-box', html) + self.assertIn('"value": "unbefristet"', html) diff --git a/backend/workflows/views.py b/backend/workflows/views.py index 0b0233c..e44bbc7 100644 --- a/backend/workflows/views.py +++ b/backend/workflows/views.py @@ -37,6 +37,7 @@ from .branding import get_branding_email_copy, get_company_email_domain, get_def from .forms import AccountAvatarForm, AccountDetailsForm, AccountNotificationPreferencesForm, AccountTOTPDisableForm, AccountTOTPEnableForm, AccountTOTPRegenerateRecoveryCodesForm, AppLoginForm, AppTOTPChallengeForm, OffboardingRequestForm, OnboardingRequestForm, PortalBrandingForm, PortalCompanyConfigForm, PortalTrialConfigForm, UserManagementCreateForm from .form_builder import ( DEFAULT_FIELD_ORDER, + DEFAULT_CONDITIONAL_RULES, FORM_PRESETS, LOCKED_FIELD_RULES, LOCKED_SECTION_RULES, @@ -46,13 +47,14 @@ from .form_builder import ( ONBOARDING_PAGE_LABELS, ONBOARDING_PAGE_ORDER, ensure_form_field_configs, + ensure_form_conditional_rule_configs, ensure_form_section_configs, get_default_page_map, get_section_labels, get_section_order, apply_form_preset, ) -from .models import AdminAuditLog, AsyncTaskLog, EmployeeProfile, FormFieldConfig, FormOption, FormSectionConfig, IntroChecklistItem, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, PortalAppConfig, PortalBranding, PortalCompanyConfig, PortalTrialConfig, ScheduledWelcomeEmail, SystemEmailConfig, UserNotification, UserProfile, WorkflowConfig +from .models import AdminAuditLog, AsyncTaskLog, EmployeeProfile, FormConditionalRuleConfig, FormFieldConfig, FormOption, FormSectionConfig, IntroChecklistItem, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, PortalAppConfig, PortalBranding, PortalCompanyConfig, PortalTrialConfig, ScheduledWelcomeEmail, SystemEmailConfig, UserNotification, UserProfile, WorkflowConfig from .emailing import send_system_email from .notifications import notify_user from .roles import ROLE_GROUP_NAMES, ROLE_LABELS, ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN, assign_user_role, get_user_role_key, get_user_role_label, user_has_capability @@ -103,15 +105,7 @@ ONBOARDING_GROUPS = { 'phone-box': ['phone_number_choice'], } -ONBOARDING_HIDDEN_BY_DEFAULT = { - 'business-card-box', - 'employment-end-box', - 'group-mailboxes-box', - 'extra-hardware-box', - 'extra-software-box', - 'extra-access-box', - 'successor-box', -} +ONBOARDING_HIDDEN_BY_DEFAULT = set(DEFAULT_CONDITIONAL_RULES.get('onboarding', {}).keys()) ONBOARDING_INLINE_CHECKS = {'order_business_cards', 'agreement_confirm'} ONBOARDING_CHECKBOX_LISTS = { @@ -131,6 +125,24 @@ ONBOARDING_SECTION_META = { 'abschluss': {'title': gettext_lazy('Abschluss'), 'subtitle': gettext_lazy('Notizen und Freigabe')}, } +CONDITIONAL_RULE_OPERATOR_CHOICES = [ + ('checked', _('ist aktiviert')), + ('equals', _('ist gleich')), + ('not_equals', _('ist nicht gleich')), +] + + +def _normalized_conditional_rule_payload(form_type: str) -> dict[str, dict]: + configs = ensure_form_conditional_rule_configs(form_type) + payload = {} + for target_key, cfg in configs.items(): + if not cfg.is_active: + continue + clauses = [clause for clause in (cfg.clauses or []) if clause.get('field') and clause.get('operator')] + if clauses: + payload[target_key] = {'all': clauses} + return payload + def healthz(request): db_ok = True @@ -1825,6 +1837,7 @@ def onboarding_create(request): if key in LOCKED_SECTION_RULES.get('onboarding', set()) or section_configs.get(key, None) is None or section_configs[key].is_visible } onboarding_sections = _build_onboarding_sections(onboarding_blocks, field_pages, visible_section_keys=visible_section_keys) + onboarding_conditional_rules = _normalized_conditional_rule_payload('onboarding') return render( request, @@ -1835,6 +1848,7 @@ def onboarding_create(request): 'onboarding_sections': onboarding_sections, 'onboarding_inline_checks': ONBOARDING_INLINE_CHECKS, 'onboarding_checkbox_lists': ONBOARDING_CHECKBOX_LISTS, + 'onboarding_conditional_rules': onboarding_conditional_rules, 'legal_text': legal_text, 'saved': request.GET.get('saved') == '1', 'saved_request_id': request.GET.get('id', ''), @@ -2179,6 +2193,27 @@ def form_builder_page(request): _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): @@ -2194,7 +2229,7 @@ def form_builder_page(request): active_subpanel = 'options' elif action == 'save_field_texts': active_subpanel = 'field-texts' - elif action in {'save_field_rules', 'save_section_rules'}: + elif action in {'save_field_rules', 'save_section_rules', 'save_conditional_rules'}: active_panel = 'builder-rules' redirect_target = f"/admin-tools/form-builder/?form_type={form_type}&option_category={option_category}" if active_panel: @@ -2217,6 +2252,7 @@ def form_builder_page(request): 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_order = get_section_order(form_type) section_labels = get_section_labels(form_type) default_page_map = get_default_page_map(form_type) @@ -2343,6 +2379,57 @@ def form_builder_page(request): } ) + 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))) + 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'), + 'phone-box': _('Direktwahl'), + } + 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.'), + 'phone-box': _('Steuert die manuelle Direktwahl.'), + } + for target_key, cfg in conditional_rule_configs.items(): + clauses = list(cfg.clauses or []) + while len(clauses) < 2: + clauses.append({'field': '', 'operator': 'equals', 'value': ''}) + conditional_rule_items.append( + { + 'target_key': target_key, + 'title': conditional_target_titles.get(target_key, target_key), + 'description': conditional_target_descriptions.get(target_key, ''), + 'is_active': cfg.is_active, + 'clauses': clauses[:2], + 'field_choices': conditional_field_choices, + 'operator_choices': CONDITIONAL_RULE_OPERATOR_CHOICES, + 'target_fields': [labels.get(name, name) for name in ONBOARDING_GROUPS.get(target_key, [])], + } + ) + preview_sections = [] if section_order: field_rule_group_map = {group['key']: group['items'] for group in field_rule_groups} @@ -2391,6 +2478,7 @@ def form_builder_page(request): 'preview_sections': preview_sections, 'section_rule_items': section_rule_items, 'builder_summary': builder_summary, + 'conditional_rule_items': conditional_rule_items, 'active_panel': active_panel, 'active_subpanel': active_subpanel, 'available_presets': FORM_PRESETS.get(form_type, {}),