snapshot: preserve configurable onboarding conditional logic
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
]
|
||||
@@ -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')),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -246,6 +246,70 @@
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{% if form_type == 'onboarding' %}
|
||||
<section class="options-panel">
|
||||
<div class="options-head">
|
||||
<h2>{% trans "Bedingte Logik" %}</h2>
|
||||
</div>
|
||||
<form method="post" action="/admin-tools/form-builder/?form_type={{ form_type }}&option_category={{ selected_option_category }}">
|
||||
{% csrf_token %}
|
||||
<div class="field-rule-groups">
|
||||
{% for item in conditional_rule_items %}
|
||||
<section class="field-rule-group">
|
||||
<div class="field-rule-group-head">
|
||||
<h3>{{ item.title }}</h3>
|
||||
<span class="column-count">{{ item.target_fields|join:", " }}</span>
|
||||
</div>
|
||||
<div class="field-rule-list">
|
||||
<div class="field-rule-row">
|
||||
<div class="field-rule-main">
|
||||
<strong>{{ item.title }}</strong>
|
||||
<div class="mini">{{ item.description }}</div>
|
||||
</div>
|
||||
<label class="field-rule-control">
|
||||
<span>{% trans "Aktiv" %}</span>
|
||||
<input type="checkbox" name="conditional_active_{{ item.target_key }}" {% if item.is_active %}checked{% endif %} />
|
||||
</label>
|
||||
</div>
|
||||
{% for clause in item.clauses %}
|
||||
<div class="field-rule-row">
|
||||
<div class="field-rule-main">
|
||||
<strong>{% blocktrans trimmed with number=forloop.counter %}Bedingung {{ number }}{% endblocktrans %}</strong>
|
||||
</div>
|
||||
<label class="field-rule-control">
|
||||
<span>{% trans "Feld" %}</span>
|
||||
<select name="conditional_field_{{ item.target_key }}_{{ forloop.counter0 }}">
|
||||
<option value="">{% trans "Keine" %}</option>
|
||||
{% for value, label in item.field_choices %}
|
||||
<option value="{{ value }}" {% if clause.field == value %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label class="field-rule-control">
|
||||
<span>{% trans "Operator" %}</span>
|
||||
<select name="conditional_operator_{{ item.target_key }}_{{ forloop.counter0 }}">
|
||||
{% for value, label in item.operator_choices %}
|
||||
<option value="{{ value }}" {% if clause.operator == value %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label class="field-rule-control">
|
||||
<span>{% trans "Wert" %}</span>
|
||||
<input type="text" name="conditional_value_{{ item.target_key }}_{{ forloop.counter0 }}" value="{{ clause.value }}" {% if clause.operator == 'checked' %}placeholder="{% trans 'wird ignoriert' %}"{% endif %} />
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="options-actions">
|
||||
<button class="btn btn-primary" type="submit" name="builder_action" value="save_conditional_rules">{% trans "Bedingte Logik speichern" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
|
||||
<form method="post" id="onboarding-form" enctype="multipart/form-data" data-email-domain="{{ portal_email_domain }}">
|
||||
{% csrf_token %}
|
||||
{{ onboarding_conditional_rules|json_script:"onboarding-conditional-rules" }}
|
||||
|
||||
{% for section in onboarding_sections %}
|
||||
<section class="page {% if forloop.first %}active{% endif %}" data-step="{{ forloop.counter }}">
|
||||
@@ -92,7 +93,11 @@
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
<div id="{{ block.id }}" class="field-group field-full {% if block.hidden_default %}hidden{% endif %}">
|
||||
<div
|
||||
id="{{ block.id }}"
|
||||
class="field-group field-full {% if block.hidden_default %}hidden{% endif %}"
|
||||
data-conditional-target="{{ block.id }}"
|
||||
>
|
||||
<div class="grid-2">
|
||||
{% for field in block.fields %}
|
||||
{% if field.is_hidden %}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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, {}),
|
||||
|
||||
Reference in New Issue
Block a user