snapshot: preserve dynamic builder and section ordering work
This commit is contained in:
@@ -3,7 +3,7 @@ from django import forms
|
||||
from django.utils.text import slugify
|
||||
from django.utils.translation import get_language
|
||||
|
||||
from .models import FormConditionalRuleConfig, FormCustomFieldConfig, FormFieldConfig, FormSectionConfig
|
||||
from .models import FormConditionalRuleConfig, FormCustomFieldConfig, FormCustomSectionConfig, FormFieldConfig, FormSectionConfig
|
||||
|
||||
|
||||
DEFAULT_FIELD_ORDER = {
|
||||
@@ -72,6 +72,10 @@ OFFBOARDING_PAGE_LABELS = {
|
||||
'austritt': '2. Austritt',
|
||||
'abschluss': '3. Abschluss',
|
||||
}
|
||||
CORE_SECTION_LABELS = {
|
||||
'onboarding': ONBOARDING_PAGE_LABELS,
|
||||
'offboarding': OFFBOARDING_PAGE_LABELS,
|
||||
}
|
||||
|
||||
LOCKED_FIELD_RULES = {
|
||||
'onboarding': {'full_name', 'work_email', 'contract_start', 'agreement_confirm'},
|
||||
@@ -157,12 +161,6 @@ DEFAULT_CONDITIONAL_RULES = {
|
||||
'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'},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -170,19 +168,11 @@ CUSTOM_FIELD_PREFIX = 'custom__'
|
||||
|
||||
|
||||
def get_section_order(form_type: str) -> list[str]:
|
||||
if form_type == 'onboarding':
|
||||
return ONBOARDING_PAGE_ORDER
|
||||
if form_type == 'offboarding':
|
||||
return OFFBOARDING_PAGE_ORDER
|
||||
return []
|
||||
return [item['key'] for item in get_section_definitions(form_type)]
|
||||
|
||||
|
||||
def get_section_labels(form_type: str) -> dict[str, str]:
|
||||
if form_type == 'onboarding':
|
||||
return ONBOARDING_PAGE_LABELS
|
||||
if form_type == 'offboarding':
|
||||
return OFFBOARDING_PAGE_LABELS
|
||||
return {}
|
||||
return {item['key']: item['title'] for item in get_section_definitions(form_type)}
|
||||
|
||||
|
||||
def get_default_page_map(form_type: str) -> dict[str, str]:
|
||||
@@ -193,6 +183,42 @@ def get_default_page_map(form_type: str) -> dict[str, str]:
|
||||
return {}
|
||||
|
||||
|
||||
def get_custom_section_configs(form_type: str, include_inactive: bool = False) -> list[FormCustomSectionConfig]:
|
||||
qs = FormCustomSectionConfig.objects.filter(form_type=form_type)
|
||||
if not include_inactive:
|
||||
qs = qs.filter(is_active=True)
|
||||
return list(qs.order_by('sort_order', 'section_key'))
|
||||
|
||||
|
||||
def get_section_definitions(form_type: str, include_inactive_custom: bool = False) -> list[dict[str, object]]:
|
||||
definitions: list[dict[str, object]] = []
|
||||
section_configs = ensure_form_section_configs(form_type)
|
||||
for cfg in sorted(section_configs.values(), key=lambda item: (item.sort_order, item.section_key)):
|
||||
label_map = CORE_SECTION_LABELS.get(form_type, {})
|
||||
definitions.append(
|
||||
{
|
||||
'key': cfg.section_key,
|
||||
'title': label_map.get(cfg.section_key, cfg.section_key),
|
||||
'locked': cfg.section_key in LOCKED_SECTION_RULES.get(form_type, set()),
|
||||
'is_custom': False,
|
||||
'sort_order': cfg.sort_order,
|
||||
}
|
||||
)
|
||||
for cfg in get_custom_section_configs(form_type, include_inactive=include_inactive_custom):
|
||||
definitions.append(
|
||||
{
|
||||
'key': cfg.section_key,
|
||||
'title': cfg.translated_title(get_language()),
|
||||
'locked': False,
|
||||
'is_custom': True,
|
||||
'is_active': cfg.is_active,
|
||||
'sort_order': cfg.sort_order,
|
||||
}
|
||||
)
|
||||
definitions.sort(key=lambda item: (item.get('sort_order', 9999), item['key']))
|
||||
return definitions
|
||||
|
||||
|
||||
def get_default_conditional_rules(form_type: str) -> dict[str, dict]:
|
||||
return DEFAULT_CONDITIONAL_RULES.get(form_type, {})
|
||||
|
||||
@@ -323,7 +349,7 @@ def ensure_form_field_configs(form_type: str, field_names: list[str]) -> dict[st
|
||||
|
||||
|
||||
def ensure_form_section_configs(form_type: str) -> dict[str, FormSectionConfig]:
|
||||
section_order = get_section_order(form_type)
|
||||
section_order = list(CORE_SECTION_LABELS.get(form_type, {}).keys())
|
||||
if not section_order:
|
||||
return {}
|
||||
existing = {
|
||||
@@ -333,7 +359,15 @@ def ensure_form_section_configs(form_type: str) -> dict[str, FormSectionConfig]:
|
||||
missing = [key for key in section_order if key not in existing]
|
||||
if missing:
|
||||
FormSectionConfig.objects.bulk_create(
|
||||
[FormSectionConfig(form_type=form_type, section_key=key, is_visible=True) for key in missing],
|
||||
[
|
||||
FormSectionConfig(
|
||||
form_type=form_type,
|
||||
section_key=key,
|
||||
sort_order=section_order.index(key),
|
||||
is_visible=True,
|
||||
)
|
||||
for key in missing
|
||||
],
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
existing = {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# Generated by Django 5.1.5 on 2026-03-27 12:26
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('workflows', '0055_offboardingrequest_custom_field_values_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='formcustomfieldconfig',
|
||||
name='section_key',
|
||||
field=models.CharField(max_length=80),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='formfieldconfig',
|
||||
name='page_key',
|
||||
field=models.CharField(blank=True, default='', max_length=80),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='formsectionconfig',
|
||||
name='section_key',
|
||||
field=models.CharField(max_length=80),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FormCustomSectionConfig',
|
||||
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)),
|
||||
('section_key', models.SlugField(max_length=80)),
|
||||
('sort_order', models.PositiveIntegerField(default=0)),
|
||||
('title', models.CharField(max_length=255)),
|
||||
('title_en', models.CharField(blank=True, max_length=255)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Benutzerdefinierter Formularabschnitt',
|
||||
'verbose_name_plural': 'Benutzerdefinierte Formularabschnitte',
|
||||
'ordering': ['form_type', 'sort_order', 'section_key'],
|
||||
'unique_together': {('form_type', 'section_key')},
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,20 @@
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def remove_phone_box_rule(apps, schema_editor):
|
||||
FormConditionalRuleConfig = apps.get_model('workflows', 'FormConditionalRuleConfig')
|
||||
FormConditionalRuleConfig.objects.filter(
|
||||
form_type='onboarding',
|
||||
target_key='phone-box',
|
||||
).update(is_active=False, clauses=[])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('workflows', '0056_alter_formcustomfieldconfig_section_key_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(remove_phone_box_rule, migrations.RunPython.noop),
|
||||
]
|
||||
@@ -0,0 +1,34 @@
|
||||
# Generated by Django 5.1.5 on 2026-03-27 15:45
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def seed_section_sort_order(apps, schema_editor):
|
||||
FormSectionConfig = apps.get_model('workflows', 'FormSectionConfig')
|
||||
defaults = {
|
||||
'onboarding': ['stammdaten', 'vertrag', 'itsetup', 'abschluss'],
|
||||
'offboarding': ['mitarbeitende', 'austritt', 'abschluss'],
|
||||
}
|
||||
for form_type, section_keys in defaults.items():
|
||||
for index, section_key in enumerate(section_keys):
|
||||
FormSectionConfig.objects.filter(form_type=form_type, section_key=section_key).update(sort_order=index)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('workflows', '0057_remove_phone_box_conditional_rule'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='formsectionconfig',
|
||||
options={'ordering': ['form_type', 'sort_order', 'section_key'], 'verbose_name': 'Formularabschnitt-Konfiguration', 'verbose_name_plural': 'Formularabschnitt-Konfigurationen'},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='formsectionconfig',
|
||||
name='sort_order',
|
||||
field=models.PositiveIntegerField(default=0),
|
||||
),
|
||||
migrations.RunPython(seed_section_sort_order, migrations.RunPython.noop),
|
||||
]
|
||||
@@ -491,7 +491,7 @@ class FormFieldConfig(models.Model):
|
||||
sort_order = models.PositiveIntegerField(default=0)
|
||||
is_visible = models.BooleanField(default=True)
|
||||
is_required = models.BooleanField(null=True, blank=True, default=None)
|
||||
page_key = models.CharField(max_length=20, blank=True, default='', choices=PAGE_CHOICES)
|
||||
page_key = models.CharField(max_length=80, blank=True, default='')
|
||||
label_override = models.CharField(max_length=255, blank=True)
|
||||
label_override_en = models.CharField(max_length=255, blank=True)
|
||||
help_text_override = models.TextField(blank=True)
|
||||
@@ -524,21 +524,13 @@ class FormSectionConfig(models.Model):
|
||||
('onboarding', _('Onboarding')),
|
||||
('offboarding', _('Offboarding')),
|
||||
]
|
||||
SECTION_CHOICES = [
|
||||
('stammdaten', _('Stammdaten')),
|
||||
('vertrag', _('Vertrag')),
|
||||
('itsetup', _('IT-Setup')),
|
||||
('abschluss', _('Abschluss')),
|
||||
('mitarbeitende', _('Mitarbeitende')),
|
||||
('austritt', _('Austritt')),
|
||||
]
|
||||
|
||||
form_type = models.CharField(max_length=20, choices=FORM_CHOICES)
|
||||
section_key = models.CharField(max_length=20, choices=SECTION_CHOICES)
|
||||
section_key = models.CharField(max_length=80)
|
||||
sort_order = models.PositiveIntegerField(default=0)
|
||||
is_visible = models.BooleanField(default=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['form_type', 'section_key']
|
||||
ordering = ['form_type', 'sort_order', 'section_key']
|
||||
unique_together = ('form_type', 'section_key')
|
||||
verbose_name = 'Formularabschnitt-Konfiguration'
|
||||
verbose_name_plural = 'Formularabschnitt-Konfigurationen'
|
||||
@@ -567,6 +559,34 @@ class FormConditionalRuleConfig(models.Model):
|
||||
return f'{self.form_type}: {self.target_key}'
|
||||
|
||||
|
||||
class FormCustomSectionConfig(models.Model):
|
||||
FORM_CHOICES = [
|
||||
('onboarding', _('Onboarding')),
|
||||
]
|
||||
|
||||
form_type = models.CharField(max_length=20, choices=FORM_CHOICES)
|
||||
section_key = models.SlugField(max_length=80)
|
||||
sort_order = models.PositiveIntegerField(default=0)
|
||||
title = models.CharField(max_length=255)
|
||||
title_en = models.CharField(max_length=255, blank=True)
|
||||
is_active = models.BooleanField(default=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['form_type', 'sort_order', 'section_key']
|
||||
unique_together = ('form_type', 'section_key')
|
||||
verbose_name = 'Benutzerdefinierter Formularabschnitt'
|
||||
verbose_name_plural = 'Benutzerdefinierte Formularabschnitte'
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f'{self.form_type}: {self.title}'
|
||||
|
||||
def translated_title(self, language_code: str | None = None) -> str:
|
||||
lang = (language_code or get_language() or 'de').split('-')[0]
|
||||
if lang == 'en' and self.title_en.strip():
|
||||
return self.title_en.strip()
|
||||
return self.title.strip()
|
||||
|
||||
|
||||
class FormCustomFieldConfig(models.Model):
|
||||
FIELD_TYPE_TEXT = 'text'
|
||||
FIELD_TYPE_TEXTAREA = 'textarea'
|
||||
@@ -582,18 +602,9 @@ class FormCustomFieldConfig(models.Model):
|
||||
('onboarding', _('Onboarding')),
|
||||
('offboarding', _('Offboarding')),
|
||||
]
|
||||
SECTION_CHOICES = [
|
||||
('stammdaten', _('Stammdaten')),
|
||||
('vertrag', _('Vertrag')),
|
||||
('itsetup', _('IT-Setup')),
|
||||
('abschluss', _('Abschluss')),
|
||||
('mitarbeitende', _('Mitarbeitende')),
|
||||
('austritt', _('Austritt')),
|
||||
]
|
||||
|
||||
form_type = models.CharField(max_length=20, choices=FORM_CHOICES)
|
||||
field_key = models.SlugField(max_length=80)
|
||||
section_key = models.CharField(max_length=20, choices=SECTION_CHOICES)
|
||||
section_key = models.CharField(max_length=80)
|
||||
sort_order = models.PositiveIntegerField(default=0)
|
||||
field_type = models.CharField(max_length=20, choices=FIELD_TYPE_CHOICES, default=FIELD_TYPE_TEXT)
|
||||
is_active = models.BooleanField(default=True)
|
||||
|
||||
@@ -109,14 +109,31 @@ body {
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.builder-overview {
|
||||
.builder-summary-strip {
|
||||
margin-top: 12px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.builder-summary-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-height: 34px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid #d5dfec;
|
||||
border-radius: 999px;
|
||||
background: #f8fbff;
|
||||
color: #304159;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.builder-summary-pill strong {
|
||||
color: #101c30;
|
||||
}
|
||||
|
||||
.builder-stat-card,
|
||||
.builder-panel,
|
||||
.options-panel {
|
||||
border: 1px solid rgba(201, 212, 226, 0.95);
|
||||
@@ -125,34 +142,6 @@ body {
|
||||
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.05);
|
||||
}
|
||||
|
||||
.builder-stat-card {
|
||||
padding: 14px 16px;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.builder-stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: rgba(146, 170, 199, 0.95);
|
||||
box-shadow: 0 16px 28px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.builder-stat-label {
|
||||
display: block;
|
||||
color: #65758f;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.03em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.builder-stat-card strong {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
font-size: 28px;
|
||||
line-height: 1;
|
||||
color: #101c30;
|
||||
}
|
||||
|
||||
.builder-panel-head h2,
|
||||
.options-head h2 {
|
||||
margin: 0;
|
||||
@@ -371,6 +360,18 @@ body {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.preview-shell-compact .preview-section {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.preview-shell-compact .preview-section-head {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.preview-shell-compact .preview-chip-list {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.preview-section {
|
||||
border: 1px solid #d7e0ec;
|
||||
border-radius: 14px;
|
||||
@@ -500,6 +501,169 @@ body {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.conditional-rule-grid {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.conditional-rule-card {
|
||||
border: 1px solid #d7e0ec;
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(180deg, #fbfdff 0%, #ffffff 100%);
|
||||
padding: 14px;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.conditional-rule-card:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: #bfd0e4;
|
||||
box-shadow: 0 14px 26px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.conditional-rule-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #e6edf6;
|
||||
}
|
||||
|
||||
.conditional-rule-head-main {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.conditional-rule-head h3 {
|
||||
margin: 2px 0 4px;
|
||||
font-size: 16px;
|
||||
color: #142033;
|
||||
}
|
||||
|
||||
.conditional-rule-eyebrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 24px;
|
||||
padding: 0 9px;
|
||||
border-radius: 999px;
|
||||
background: #eef4ff;
|
||||
color: #214d99;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.conditional-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #5f7089;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.conditional-toggle input[type='checkbox'] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.conditional-targets {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.conditional-target-label {
|
||||
color: #5f7089;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.conditional-target-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.conditional-clause-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.conditional-clause-row {
|
||||
display: grid;
|
||||
grid-template-columns: 56px minmax(220px, 1.35fr) minmax(180px, 0.8fr) minmax(180px, 0.85fr);
|
||||
gap: 10px;
|
||||
align-items: end;
|
||||
padding: 12px;
|
||||
border: 1px solid #e5ebf3;
|
||||
border-radius: 14px;
|
||||
background: #f8fbff;
|
||||
}
|
||||
|
||||
.conditional-clause-index {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 38px;
|
||||
color: #33506f;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.conditional-clause-control {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.conditional-clause-control span {
|
||||
color: #5f7089;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.conditional-clause-control select,
|
||||
.conditional-clause-control input[type='text'] {
|
||||
width: 100%;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.conditional-extra-clause {
|
||||
border: 1px dashed #d7e0ec;
|
||||
border-radius: 14px;
|
||||
background: #fbfdff;
|
||||
}
|
||||
|
||||
.conditional-extra-clause summary {
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
padding: 10px 12px;
|
||||
color: #35506f;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.03em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.conditional-extra-clause summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.conditional-extra-clause[open] summary {
|
||||
border-bottom: 1px solid #e6edf6;
|
||||
}
|
||||
|
||||
.conditional-extra-clause .conditional-clause-row {
|
||||
border: 0;
|
||||
border-radius: 0 0 14px 14px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.columns {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(220px, 1fr));
|
||||
@@ -728,35 +892,212 @@ body {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.section-rule-grid {
|
||||
.builder-entity-form {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 10px;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
padding: 14px;
|
||||
border: 1px solid #d7e0ec;
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(180deg, #fbfdff 0%, #ffffff 100%);
|
||||
}
|
||||
|
||||
.section-rule-card {
|
||||
.builder-entity-head h3,
|
||||
.builder-group-head h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
color: #142033;
|
||||
}
|
||||
|
||||
.builder-entity-head .mini {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.builder-entity-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.builder-entity-control {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.builder-entity-control span {
|
||||
color: #5f7089;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.builder-entity-control-narrow {
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
.builder-entity-control-full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.builder-card-list,
|
||||
.builder-group-stack {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.builder-group-card {
|
||||
border: 1px solid #d7e0ec;
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(180deg, #f8fbff 0%, #ffffff 100%);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.builder-group-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 12px 14px;
|
||||
border-bottom: 1px solid #dfe7f1;
|
||||
background: #f2f7ff;
|
||||
}
|
||||
|
||||
.builder-entity-card {
|
||||
padding: 14px;
|
||||
border: 1px solid #e5ebf3;
|
||||
border-radius: 16px;
|
||||
background: #ffffff;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.builder-entity-card-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.builder-entity-card-head strong {
|
||||
color: #142033;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.entity-meta {
|
||||
margin-top: 4px;
|
||||
color: #64748b;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: 12px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.builder-switch,
|
||||
.builder-switch-inline {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #5f7089;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.builder-switch-stack {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.builder-switch input[type='checkbox'],
|
||||
.builder-switch-inline input[type='checkbox'] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.builder-entity-card-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.builder-empty-state {
|
||||
padding: 14px;
|
||||
border: 1px dashed #d7e0ec;
|
||||
border-radius: 14px;
|
||||
background: #fbfdff;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.section-rule-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.section-rule-grid.drag-over {
|
||||
outline: 1px dashed #9db4d2;
|
||||
outline-offset: 6px;
|
||||
}
|
||||
|
||||
.section-rule-card {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 13px 14px;
|
||||
border: 1px solid #d6e0ec;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(180deg, #f9fbff, #ffffff);
|
||||
cursor: move;
|
||||
transition: transform 0.16s ease, box-shadow 0.16s ease, border-color 0.16s ease;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.section-rule-card:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 10px 20px rgba(15, 23, 42, 0.06);
|
||||
border-color: #b8cae0;
|
||||
}
|
||||
|
||||
.section-rule-card.is-locked {
|
||||
background: linear-gradient(180deg, #f4f7fb, #fafcff);
|
||||
}
|
||||
|
||||
.section-rule-card.dragging {
|
||||
opacity: 0.58;
|
||||
}
|
||||
|
||||
.section-rule-card.manual-dragging {
|
||||
opacity: 0.72;
|
||||
border-color: #8fb1d8;
|
||||
box-shadow: 0 16px 30px rgba(15, 23, 42, 0.10);
|
||||
}
|
||||
|
||||
.section-rule-drag {
|
||||
color: #8aa0be;
|
||||
font-size: 18px;
|
||||
letter-spacing: -2px;
|
||||
user-select: none;
|
||||
align-self: stretch;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding-right: 2px;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
body.builder-dragging,
|
||||
body.builder-dragging * {
|
||||
cursor: grabbing !important;
|
||||
user-select: none !important;
|
||||
}
|
||||
|
||||
.section-rule-copy {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.section-rule-copy strong {
|
||||
color: #0f172a;
|
||||
font-size: 14px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.section-rule-copy span,
|
||||
@@ -772,6 +1113,11 @@ body {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.section-rule-checkbox {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@keyframes builderFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
@@ -821,10 +1167,6 @@ body {
|
||||
}
|
||||
|
||||
@media (max-width: 1220px) {
|
||||
.builder-overview {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.builder-rule-layout,
|
||||
.columns {
|
||||
grid-template-columns: repeat(2, minmax(220px, 1fr));
|
||||
@@ -834,7 +1176,8 @@ body {
|
||||
@media (max-width: 900px) {
|
||||
.builder-hero,
|
||||
.builder-panel-head,
|
||||
.options-head {
|
||||
.options-head,
|
||||
.conditional-rule-head {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
@@ -846,15 +1189,20 @@ body {
|
||||
.builder-rule-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.builder-entity-card-head {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.builder-overview,
|
||||
.columns {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.field-rule-row {
|
||||
.field-rule-row,
|
||||
.conditional-clause-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@@ -862,7 +1210,26 @@ body {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.conditional-clause-index {
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.add-option-form {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.builder-entity-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.builder-entity-control-narrow,
|
||||
.builder-entity-control-full {
|
||||
max-width: none;
|
||||
grid-column: auto;
|
||||
}
|
||||
|
||||
.builder-group-head {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,4 +145,95 @@
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const sectionRuleGrid = document.getElementById('section-rule-grid');
|
||||
if (sectionRuleGrid) {
|
||||
let draggingSectionCard = null;
|
||||
let manualDraggingSectionCard = null;
|
||||
|
||||
function getSectionInsertBeforeNode(mouseY) {
|
||||
const cards = Array.from(sectionRuleGrid.querySelectorAll('.section-rule-card:not(.dragging)'));
|
||||
return cards.find((card) => {
|
||||
const box = card.getBoundingClientRect();
|
||||
return mouseY < box.top + box.height / 2;
|
||||
});
|
||||
}
|
||||
|
||||
function getManualSectionInsertBeforeNode(mouseY) {
|
||||
const cards = Array.from(sectionRuleGrid.querySelectorAll('.section-rule-card:not(.manual-dragging)'));
|
||||
return cards.find((card) => {
|
||||
const box = card.getBoundingClientRect();
|
||||
return mouseY < box.top + box.height / 2;
|
||||
});
|
||||
}
|
||||
|
||||
function onManualSectionMove(event) {
|
||||
if (!manualDraggingSectionCard) return;
|
||||
event.preventDefault();
|
||||
sectionRuleGrid.classList.add('drag-over');
|
||||
const beforeNode = getManualSectionInsertBeforeNode(event.clientY);
|
||||
if (beforeNode) {
|
||||
sectionRuleGrid.insertBefore(manualDraggingSectionCard, beforeNode);
|
||||
} else {
|
||||
sectionRuleGrid.appendChild(manualDraggingSectionCard);
|
||||
}
|
||||
}
|
||||
|
||||
function endManualSectionDrag() {
|
||||
if (!manualDraggingSectionCard) return;
|
||||
manualDraggingSectionCard.classList.remove('manual-dragging');
|
||||
manualDraggingSectionCard = null;
|
||||
sectionRuleGrid.classList.remove('drag-over');
|
||||
document.body.classList.remove('builder-dragging');
|
||||
document.removeEventListener('mousemove', onManualSectionMove);
|
||||
document.removeEventListener('mouseup', endManualSectionDrag);
|
||||
}
|
||||
|
||||
sectionRuleGrid.querySelectorAll('.section-rule-card').forEach((card) => {
|
||||
card.addEventListener('dragstart', (event) => {
|
||||
draggingSectionCard = card;
|
||||
card.classList.add('dragging');
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
event.dataTransfer.setData('text/plain', card.dataset.sectionKey || '');
|
||||
});
|
||||
card.addEventListener('dragend', () => {
|
||||
card.classList.remove('dragging');
|
||||
sectionRuleGrid.classList.remove('drag-over');
|
||||
draggingSectionCard = null;
|
||||
});
|
||||
const handle = card.querySelector('.section-rule-drag');
|
||||
if (handle) {
|
||||
handle.addEventListener('mousedown', (event) => {
|
||||
event.preventDefault();
|
||||
manualDraggingSectionCard = card;
|
||||
card.classList.add('manual-dragging');
|
||||
sectionRuleGrid.classList.add('drag-over');
|
||||
document.body.classList.add('builder-dragging');
|
||||
document.addEventListener('mousemove', onManualSectionMove);
|
||||
document.addEventListener('mouseup', endManualSectionDrag);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
sectionRuleGrid.addEventListener('dragover', (event) => {
|
||||
event.preventDefault();
|
||||
sectionRuleGrid.classList.add('drag-over');
|
||||
if (!draggingSectionCard) return;
|
||||
const beforeNode = getSectionInsertBeforeNode(event.clientY);
|
||||
if (beforeNode) {
|
||||
sectionRuleGrid.insertBefore(draggingSectionCard, beforeNode);
|
||||
} else {
|
||||
sectionRuleGrid.appendChild(draggingSectionCard);
|
||||
}
|
||||
});
|
||||
|
||||
sectionRuleGrid.addEventListener('dragleave', () => {
|
||||
sectionRuleGrid.classList.remove('drag-over');
|
||||
});
|
||||
|
||||
sectionRuleGrid.addEventListener('drop', (event) => {
|
||||
event.preventDefault();
|
||||
sectionRuleGrid.classList.remove('drag-over');
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -32,81 +32,6 @@
|
||||
|
||||
<div id="status-message" class="status" aria-live="polite"></div>
|
||||
|
||||
<nav class="builder-quicknav" aria-label="{% trans 'Bereiche' %}">
|
||||
<a href="#builder-structure">{% trans "Reihenfolge" %}</a>
|
||||
<a href="#builder-rules">{% trans "Regeln" %}</a>
|
||||
<a href="#builder-content">{% trans "Optionen & Texte" %}</a>
|
||||
</nav>
|
||||
|
||||
<section class="builder-overview">
|
||||
<article class="builder-stat-card">
|
||||
<span class="builder-stat-label">{% trans "Fixe Kernfelder" %}</span>
|
||||
<strong>{{ builder_summary.locked_field_count }}</strong>
|
||||
</article>
|
||||
<article class="builder-stat-card">
|
||||
<span class="builder-stat-label">{% trans "Konfigurierbar" %}</span>
|
||||
<strong>{{ builder_summary.configurable_field_count }}</strong>
|
||||
</article>
|
||||
<article class="builder-stat-card">
|
||||
<span class="builder-stat-label">{% trans "Aktuell ausgeblendet" %}</span>
|
||||
<strong>{{ builder_summary.hidden_field_count }}</strong>
|
||||
</article>
|
||||
<article class="builder-stat-card">
|
||||
<span class="builder-stat-label">{% trans "Eigene Felder" %}</span>
|
||||
<strong>{{ builder_summary.custom_field_count }}</strong>
|
||||
</article>
|
||||
{% if form_type == 'onboarding' %}
|
||||
<article class="builder-stat-card">
|
||||
<span class="builder-stat-label">{% trans "Versteckte Abschnitte" %}</span>
|
||||
<strong>{{ builder_summary.hidden_section_count }}</strong>
|
||||
</article>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<section class="builder-preset-bar">
|
||||
<form class="builder-preset-form" method="post" action="/admin-tools/form-builder/?form_type={{ form_type }}&option_category={{ selected_option_category }}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="builder_action" value="apply_preset" />
|
||||
<label class="builder-preset-label" for="preset_key">{% trans "Vorlage anwenden" %}</label>
|
||||
<select id="preset_key" name="preset_key">
|
||||
{% for preset_key, preset in available_presets.items %}
|
||||
<option value="{{ preset_key }}">{{ preset.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button class="btn btn-secondary" type="submit">{% trans "Anwenden" %}</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<details class="builder-panel builder-accordion js-single-accordion" data-accordion-group="builder-panels" id="builder-preview" {% if active_panel == 'builder-preview' %}open{% endif %}>
|
||||
<summary class="builder-panel-summary">
|
||||
<div class="builder-panel-head">
|
||||
<div>
|
||||
<h2>{% trans "Live-Vorschau" %}</h2>
|
||||
</div>
|
||||
<span class="builder-panel-toggle">{% trans "Öffnen" %}</span>
|
||||
</div>
|
||||
</summary>
|
||||
<div class="builder-panel-body">
|
||||
<div class="preview-shell">
|
||||
{% for section in preview_sections %}
|
||||
<section class="preview-section">
|
||||
<div class="preview-section-head">
|
||||
<h3>{{ section.title }}</h3>
|
||||
<span class="column-count">{% blocktrans trimmed with count=section.items|length %}{{ count }} Feld/Felder{% endblocktrans %}</span>
|
||||
</div>
|
||||
<div class="preview-chip-list">
|
||||
{% for item in section.items %}
|
||||
<span class="preview-chip{% if item.locked %} is-locked{% endif %}">{{ item.label }}</span>
|
||||
{% empty %}
|
||||
<span class="mini">{% trans "Keine sichtbaren Felder." %}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="builder-panel builder-accordion js-single-accordion" data-accordion-group="builder-panels" id="builder-structure" {% if active_panel == 'builder-structure' %}open{% endif %}>
|
||||
<summary class="builder-panel-summary">
|
||||
<div class="builder-panel-head">
|
||||
@@ -158,46 +83,63 @@
|
||||
</summary>
|
||||
<div class="builder-panel-body">
|
||||
|
||||
<div class="builder-rule-layout">
|
||||
<section class="options-panel">
|
||||
<div class="options-head">
|
||||
<h2>{% trans "Abschnitte steuern" %}</h2>
|
||||
</div>
|
||||
<div class="builder-stack-layout">
|
||||
<details class="options-panel nested-accordion js-single-accordion" data-accordion-group="builder-rules-subpanels" {% if active_rules_panel == 'section-rules' %}open{% endif %}>
|
||||
<summary class="nested-accordion-summary">
|
||||
<div class="options-head">
|
||||
<h2>{% trans "Abschnitte steuern" %}</h2>
|
||||
<span class="builder-panel-toggle">{% trans "Öffnen" %}</span>
|
||||
</div>
|
||||
</summary>
|
||||
<div class="nested-accordion-body">
|
||||
<form method="post" action="/admin-tools/form-builder/?form_type={{ form_type }}&option_category={{ selected_option_category }}">
|
||||
{% csrf_token %}
|
||||
<div class="section-rule-grid">
|
||||
<div class="section-rule-grid" id="section-rule-grid">
|
||||
{% for section in section_rule_items %}
|
||||
<label class="section-rule-card{% if section.locked %} is-locked{% endif %}">
|
||||
<article
|
||||
class="section-rule-card{% if section.locked %} is-locked{% endif %}"
|
||||
draggable="true"
|
||||
data-section-key="{{ section.key }}"
|
||||
>
|
||||
<input type="hidden" name="section_order" value="{{ section.key }}" />
|
||||
<div class="section-rule-drag" aria-hidden="true" title="{% trans 'Zum Verschieben ziehen' %}">⋮⋮</div>
|
||||
<div class="section-rule-copy">
|
||||
<strong>{{ section.title }}</strong>
|
||||
<span>{% blocktrans trimmed with count=section.field_count %}{{ count }} Feld/Felder in diesem Abschnitt.{% endblocktrans %}</span>
|
||||
</div>
|
||||
<div class="section-rule-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="section_visible_{{ section.key }}"
|
||||
{% if section.is_visible %}checked{% endif %}
|
||||
{% if section.locked %}disabled{% endif %}
|
||||
/>
|
||||
<label class="section-rule-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="section_visible_{{ section.key }}"
|
||||
{% if section.is_visible %}checked{% endif %}
|
||||
{% if section.locked %}disabled{% endif %}
|
||||
/>
|
||||
</label>
|
||||
<span class="badge {% if section.is_visible %}required{% else %}hidden{% endif %}">
|
||||
{% if section.locked %}{% trans "Fix" %}
|
||||
{% elif section.is_visible %}{% trans "Sichtbar" %}
|
||||
{% else %}{% trans "Ausgeblendet" %}{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="options-actions">
|
||||
<button class="btn btn-primary" type="submit" name="builder_action" value="save_section_rules">{% trans "Abschnittsregeln speichern" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="options-panel">
|
||||
<div class="options-head">
|
||||
<h2>{% trans "Feldregeln verwalten" %}</h2>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="options-panel nested-accordion js-single-accordion" data-accordion-group="builder-rules-subpanels" {% if active_rules_panel == 'field-rules' %}open{% endif %}>
|
||||
<summary class="nested-accordion-summary">
|
||||
<div class="options-head">
|
||||
<h2>{% trans "Feldregeln verwalten" %}</h2>
|
||||
<span class="builder-panel-toggle">{% trans "Öffnen" %}</span>
|
||||
</div>
|
||||
</summary>
|
||||
<div class="nested-accordion-body">
|
||||
<form method="post" action="/admin-tools/form-builder/?form_type={{ form_type }}&option_category={{ selected_option_category }}">
|
||||
{% csrf_token %}
|
||||
<div class="field-rule-groups">
|
||||
@@ -250,61 +192,104 @@
|
||||
<button class="btn btn-primary" type="submit" name="builder_action" value="save_field_rules">{% trans "Feldregeln speichern" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
{% if form_type == 'onboarding' %}
|
||||
<section class="options-panel">
|
||||
<div class="options-head">
|
||||
<h2>{% trans "Bedingte Logik" %}</h2>
|
||||
</div>
|
||||
<details class="options-panel nested-accordion js-single-accordion" data-accordion-group="builder-rules-subpanels" {% if active_rules_panel == 'conditional-rules' %}open{% endif %}>
|
||||
<summary class="nested-accordion-summary">
|
||||
<div class="options-head">
|
||||
<h2>{% trans "Bedingte Logik" %}</h2>
|
||||
<span class="builder-panel-toggle">{% trans "Öffnen" %}</span>
|
||||
</div>
|
||||
</summary>
|
||||
<div class="nested-accordion-body">
|
||||
<form method="post" action="/admin-tools/form-builder/?form_type={{ form_type }}&option_category={{ selected_option_category }}">
|
||||
{% csrf_token %}
|
||||
<div class="field-rule-groups">
|
||||
<div class="conditional-rule-grid">
|
||||
{% 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>
|
||||
<section class="conditional-rule-card">
|
||||
<div class="conditional-rule-head">
|
||||
<div class="conditional-rule-head-main">
|
||||
<span class="conditional-rule-eyebrow">{% trans "Sichtbarkeit" %}</span>
|
||||
<h3>{{ item.title }}</h3>
|
||||
{% if item.description %}
|
||||
<p class="mini">{{ item.description }}</p>
|
||||
{% endif %}
|
||||
</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>
|
||||
<label class="conditional-toggle">
|
||||
<span>{% trans "Aktiv" %}</span>
|
||||
<input type="checkbox" name="conditional_active_{{ item.target_key }}" {% if item.is_active %}checked{% endif %} />
|
||||
</label>
|
||||
</div>
|
||||
<div class="conditional-targets">
|
||||
<span class="conditional-target-label">{% trans "Steuert" %}</span>
|
||||
<div class="conditional-target-chips">
|
||||
{% for field_name in item.target_fields %}
|
||||
<span class="preview-chip">{{ field_name }}</span>
|
||||
{% empty %}
|
||||
<span class="mini">{% trans "Keine Ziel-Felder." %}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="conditional-clause-list">
|
||||
{% with first_clause=item.clauses.0 second_clause=item.clauses.1 %}
|
||||
<div class="conditional-clause-row">
|
||||
<div class="conditional-clause-index">
|
||||
{% trans "Wenn" %}
|
||||
</div>
|
||||
<label class="field-rule-control">
|
||||
<label class="conditional-clause-control conditional-clause-field">
|
||||
<span>{% trans "Feld" %}</span>
|
||||
<select name="conditional_field_{{ item.target_key }}_{{ forloop.counter0 }}">
|
||||
<select name="conditional_field_{{ item.target_key }}_0">
|
||||
<option value="">{% trans "Keine" %}</option>
|
||||
{% for value, label in item.field_choices %}
|
||||
<option value="{{ value }}" {% if clause.field == value %}selected{% endif %}>{{ label }}</option>
|
||||
<option value="{{ value }}" {% if first_clause.field == value %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label class="field-rule-control">
|
||||
<label class="conditional-clause-control conditional-clause-operator">
|
||||
<span>{% trans "Operator" %}</span>
|
||||
<select name="conditional_operator_{{ item.target_key }}_{{ forloop.counter0 }}">
|
||||
<select name="conditional_operator_{{ item.target_key }}_0">
|
||||
{% for value, label in item.operator_choices %}
|
||||
<option value="{{ value }}" {% if clause.operator == value %}selected{% endif %}>{{ label }}</option>
|
||||
<option value="{{ value }}" {% if first_clause.operator == value %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label class="field-rule-control">
|
||||
<label class="conditional-clause-control conditional-clause-value">
|
||||
<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 %} />
|
||||
<input type="text" name="conditional_value_{{ item.target_key }}_0" value="{{ first_clause.value }}" {% if first_clause.operator == 'checked' %}placeholder="{% trans 'wird ignoriert' %}"{% endif %} />
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<details class="conditional-extra-clause" {% if second_clause.field %}open{% endif %}>
|
||||
<summary>{% trans "Zusätzliche Bedingung" %}</summary>
|
||||
<div class="conditional-clause-row conditional-clause-row-secondary">
|
||||
<div class="conditional-clause-index">
|
||||
{% trans "Und" %}
|
||||
</div>
|
||||
<label class="conditional-clause-control conditional-clause-field">
|
||||
<span>{% trans "Feld" %}</span>
|
||||
<select name="conditional_field_{{ item.target_key }}_1">
|
||||
<option value="">{% trans "Keine" %}</option>
|
||||
{% for value, label in item.field_choices %}
|
||||
<option value="{{ value }}" {% if second_clause.field == value %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label class="conditional-clause-control conditional-clause-operator">
|
||||
<span>{% trans "Operator" %}</span>
|
||||
<select name="conditional_operator_{{ item.target_key }}_1">
|
||||
{% for value, label in item.operator_choices %}
|
||||
<option value="{{ value }}" {% if second_clause.operator == value %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label class="conditional-clause-control conditional-clause-value">
|
||||
<span>{% trans "Wert" %}</span>
|
||||
<input type="text" name="conditional_value_{{ item.target_key }}_1" value="{{ second_clause.value }}" {% if second_clause.operator == 'checked' %}placeholder="{% trans 'wird ignoriert' %}"{% endif %} />
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
{% endwith %}
|
||||
</div>
|
||||
</section>
|
||||
{% endfor %}
|
||||
@@ -313,7 +298,8 @@
|
||||
<button class="btn btn-primary" type="submit" name="builder_action" value="save_conditional_rules">{% trans "Bedingte Logik speichern" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
</details>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -457,6 +443,86 @@
|
||||
</div>
|
||||
</details>
|
||||
|
||||
{% if form_type == 'onboarding' %}
|
||||
<details class="options-panel nested-accordion js-single-accordion" data-accordion-group="builder-content-subpanels" {% if active_subpanel == 'custom-sections' %}open{% endif %}>
|
||||
<summary class="nested-accordion-summary">
|
||||
<div class="options-head">
|
||||
<h2>{% trans "Eigene Abschnitte" %}</h2>
|
||||
<span class="builder-panel-toggle">{% trans "Öffnen" %}</span>
|
||||
</div>
|
||||
</summary>
|
||||
<div class="nested-accordion-body">
|
||||
<form class="builder-entity-form" method="post" action="/admin-tools/form-builder/?form_type={{ form_type }}&option_category={{ selected_option_category }}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="builder_action" value="add_custom_section" />
|
||||
<div class="builder-entity-head">
|
||||
<div>
|
||||
<h3>{% trans "Abschnitt hinzufügen" %}</h3>
|
||||
<p class="mini">{% trans "Erstellen Sie zusätzliche Bereiche für deployment-spezifische Informationen." %}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="builder-entity-grid">
|
||||
<label class="builder-entity-control">
|
||||
<span>{% trans "Titel (DE)" %}</span>
|
||||
<input type="text" name="custom_section_title" required />
|
||||
</label>
|
||||
<label class="builder-entity-control">
|
||||
<span>{% trans "Titel (EN)" %}</span>
|
||||
<input type="text" name="custom_section_title_en" />
|
||||
</label>
|
||||
<label class="builder-entity-control builder-entity-control-narrow">
|
||||
<span>{% trans "Sortierung" %}</span>
|
||||
<input type="number" name="custom_section_sort_order" min="0" value="0" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="options-actions">
|
||||
<button class="btn btn-primary" type="submit">{% trans "Abschnitt hinzufügen" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form method="post" action="/admin-tools/form-builder/?form_type={{ form_type }}&option_category={{ selected_option_category }}">
|
||||
{% csrf_token %}
|
||||
<div class="builder-card-list">
|
||||
{% for item in custom_section_items %}
|
||||
<article class="builder-entity-card">
|
||||
<input type="hidden" name="custom_section_ids" value="{{ item.id }}" />
|
||||
<div class="builder-entity-card-head">
|
||||
<div>
|
||||
<strong>{{ item.title }}</strong>
|
||||
<div class="entity-meta">{{ item.section_key }}</div>
|
||||
</div>
|
||||
<label class="builder-switch">
|
||||
<input type="checkbox" name="custom_section_is_active_{{ item.id }}" {% if item.is_active %}checked{% endif %} />
|
||||
<span>{% trans "Aktiv" %}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="builder-entity-grid">
|
||||
<label class="builder-entity-control">
|
||||
<span>{% trans "Titel (DE)" %}</span>
|
||||
<input type="text" name="custom_section_title_{{ item.id }}" value="{{ item.title }}" required />
|
||||
</label>
|
||||
<label class="builder-entity-control">
|
||||
<span>{% trans "Titel (EN)" %}</span>
|
||||
<input type="text" name="custom_section_title_en_{{ item.id }}" value="{{ item.title_en }}" />
|
||||
</label>
|
||||
<label class="builder-entity-control builder-entity-control-narrow">
|
||||
<span>{% trans "Sortierung" %}</span>
|
||||
<input type="number" min="0" name="custom_section_sort_order_{{ item.id }}" value="{{ item.sort_order }}" />
|
||||
</label>
|
||||
</div>
|
||||
</article>
|
||||
{% empty %}
|
||||
<div class="builder-empty-state">{% trans "Keine eigenen Abschnitte vorhanden." %}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="options-actions">
|
||||
<button class="btn btn-primary" type="submit" name="builder_action" value="save_custom_sections">{% trans "Abschnitte speichern" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
{% endif %}
|
||||
|
||||
<details class="options-panel nested-accordion js-single-accordion" data-accordion-group="builder-content-subpanels" {% if active_subpanel == 'custom-fields' %}open{% endif %}>
|
||||
<summary class="nested-accordion-summary">
|
||||
<div class="options-head">
|
||||
@@ -465,98 +531,155 @@
|
||||
</div>
|
||||
</summary>
|
||||
<div class="nested-accordion-body">
|
||||
<form class="add-option-form" method="post" action="/admin-tools/form-builder/?form_type={{ form_type }}&option_category={{ selected_option_category }}">
|
||||
<form class="builder-entity-form" method="post" action="/admin-tools/form-builder/?form_type={{ form_type }}&option_category={{ selected_option_category }}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="builder_action" value="add_custom_field" />
|
||||
<input type="text" name="custom_label" placeholder="{% trans 'Label (DE)' %}" required />
|
||||
<input type="text" name="custom_label_en" placeholder="{% trans 'Label (EN, optional)' %}" />
|
||||
<select name="custom_section_key">
|
||||
{% for group in custom_field_groups %}
|
||||
<option value="{{ group.key }}">{{ group.title }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<select name="custom_field_type">
|
||||
{% for value, label in custom_field_type_choices %}
|
||||
<option value="{{ value }}">{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<input type="number" name="custom_sort_order" min="0" value="0" placeholder="{% trans 'Sortierung' %}" />
|
||||
<label class="field-rule-control compact-inline">
|
||||
<span>{% trans "Pflicht" %}</span>
|
||||
<input type="checkbox" name="custom_is_required" />
|
||||
</label>
|
||||
<input type="text" name="custom_help_text" placeholder="{% trans 'Hilfetext (DE, optional)' %}" />
|
||||
<input type="text" name="custom_help_text_en" placeholder="{% trans 'Hilfetext (EN, optional)' %}" />
|
||||
<textarea name="custom_select_options" rows="3" placeholder="{% trans 'Optionen (eine pro Zeile, optional: wert|Label)' %}"></textarea>
|
||||
<textarea name="custom_select_options_en" rows="3" placeholder="{% trans 'Optionen EN (eine pro Zeile, optional: value|Label)' %}"></textarea>
|
||||
<button class="btn btn-primary" type="submit">{% trans "Eigenes Feld hinzufügen" %}</button>
|
||||
<div class="builder-entity-head">
|
||||
<div>
|
||||
<h3>{% trans "Feld hinzufügen" %}</h3>
|
||||
<p class="mini">{% trans "Erstellen Sie zusätzliche Eingaben innerhalb eines bestehenden oder eigenen Abschnitts." %}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="builder-entity-grid">
|
||||
<label class="builder-entity-control">
|
||||
<span>{% trans "Label (DE)" %}</span>
|
||||
<input type="text" name="custom_label" required />
|
||||
</label>
|
||||
<label class="builder-entity-control">
|
||||
<span>{% trans "Label (EN)" %}</span>
|
||||
<input type="text" name="custom_label_en" />
|
||||
</label>
|
||||
<label class="builder-entity-control">
|
||||
<span>{% trans "Abschnitt" %}</span>
|
||||
<select name="custom_section_key">
|
||||
{% for group in custom_field_groups %}
|
||||
<option value="{{ group.key }}">{{ group.title }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label class="builder-entity-control">
|
||||
<span>{% trans "Typ" %}</span>
|
||||
<select name="custom_field_type">
|
||||
{% for value, label in custom_field_type_choices %}
|
||||
<option value="{{ value }}">{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label class="builder-entity-control builder-entity-control-narrow">
|
||||
<span>{% trans "Sortierung" %}</span>
|
||||
<input type="number" name="custom_sort_order" min="0" value="0" />
|
||||
</label>
|
||||
<label class="builder-switch builder-switch-inline">
|
||||
<input type="checkbox" name="custom_is_required" />
|
||||
<span>{% trans "Pflichtfeld" %}</span>
|
||||
</label>
|
||||
<label class="builder-entity-control">
|
||||
<span>{% trans "Hilfetext (DE)" %}</span>
|
||||
<input type="text" name="custom_help_text" />
|
||||
</label>
|
||||
<label class="builder-entity-control">
|
||||
<span>{% trans "Hilfetext (EN)" %}</span>
|
||||
<input type="text" name="custom_help_text_en" />
|
||||
</label>
|
||||
<label class="builder-entity-control builder-entity-control-full">
|
||||
<span>{% trans "Optionen (DE)" %}</span>
|
||||
<textarea name="custom_select_options" rows="3" placeholder="{% trans 'Eine Option pro Zeile, optional: wert|Label' %}"></textarea>
|
||||
</label>
|
||||
<label class="builder-entity-control builder-entity-control-full">
|
||||
<span>{% trans "Optionen (EN)" %}</span>
|
||||
<textarea name="custom_select_options_en" rows="3" placeholder="{% trans 'Eine Option pro Zeile, optional: value|Label' %}"></textarea>
|
||||
</label>
|
||||
</div>
|
||||
<div class="options-actions">
|
||||
<button class="btn btn-primary" type="submit">{% trans "Eigenes Feld hinzufügen" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form method="post" action="/admin-tools/form-builder/?form_type={{ form_type }}&option_category={{ selected_option_category }}">
|
||||
{% csrf_token %}
|
||||
<div class="option-table-wrap">
|
||||
<table class="option-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Schlüssel" %}</th>
|
||||
<th>{% trans "Abschnitt" %}</th>
|
||||
<th>{% trans "Typ" %}</th>
|
||||
<th>{% trans "Sortierung" %}</th>
|
||||
<th>{% trans "Label (DE)" %}</th>
|
||||
<th>{% trans "Label (EN)" %}</th>
|
||||
<th>{% trans "Pflicht" %}</th>
|
||||
<th>{% trans "Aktiv" %}</th>
|
||||
<th>{% trans "Löschen" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for group in custom_field_groups %}
|
||||
<tr class="option-table-group-row">
|
||||
<th colspan="9">{{ group.title }}</th>
|
||||
</tr>
|
||||
<div class="builder-group-stack">
|
||||
{% for group in custom_field_groups %}
|
||||
<section class="builder-group-card">
|
||||
<div class="builder-group-head">
|
||||
<h3>{{ group.title }}</h3>
|
||||
<span class="column-count">{% blocktrans trimmed with count=group.items|length %}{{ count }} Feld/Felder{% endblocktrans %}</span>
|
||||
</div>
|
||||
<div class="builder-card-list">
|
||||
{% for item in group.items %}
|
||||
<tr>
|
||||
<td>
|
||||
<input type="hidden" name="custom_field_ids" value="{{ item.id }}" />
|
||||
<strong>{{ item.field_key }}</strong>
|
||||
</td>
|
||||
<td>
|
||||
<select name="custom_section_key_{{ item.id }}">
|
||||
{% for group_choice in custom_field_groups %}
|
||||
<option value="{{ group_choice.key }}" {% if group_choice.key == item.section_key %}selected{% endif %}>{{ group_choice.title }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<select name="custom_field_type_{{ item.id }}">
|
||||
{% for value, label in custom_field_type_choices %}
|
||||
<option value="{{ value }}" {% if value == item.field_type %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
<td><input type="number" min="0" name="custom_sort_order_{{ item.id }}" value="{{ item.sort_order }}" /></td>
|
||||
<td>
|
||||
<input type="text" name="custom_label_{{ item.id }}" value="{{ item.label }}" required />
|
||||
<input type="text" name="custom_help_text_{{ item.id }}" value="{{ item.help_text }}" placeholder="{% trans 'Hilfetext (DE)' %}" />
|
||||
<textarea name="custom_select_options_{{ item.id }}" rows="2" placeholder="{% trans 'Optionen (DE)' %}">{{ item.select_options }}</textarea>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" name="custom_label_en_{{ item.id }}" value="{{ item.label_en }}" />
|
||||
<input type="text" name="custom_help_text_en_{{ item.id }}" value="{{ item.help_text_en }}" placeholder="{% trans 'Hilfetext (EN)' %}" />
|
||||
<textarea name="custom_select_options_en_{{ item.id }}" rows="2" placeholder="{% trans 'Optionen (EN)' %}">{{ item.select_options_en }}</textarea>
|
||||
</td>
|
||||
<td><input type="checkbox" name="custom_is_required_{{ item.id }}" {% if item.is_required %}checked{% endif %} /></td>
|
||||
<td><input type="checkbox" name="custom_is_active_{{ item.id }}" {% if item.is_active %}checked{% endif %} /></td>
|
||||
<td>
|
||||
<article class="builder-entity-card">
|
||||
<input type="hidden" name="custom_field_ids" value="{{ item.id }}" />
|
||||
<div class="builder-entity-card-head">
|
||||
<div>
|
||||
<strong>{{ item.label }}</strong>
|
||||
<div class="entity-meta">{{ item.field_key }}</div>
|
||||
</div>
|
||||
<div class="builder-switch-stack">
|
||||
<label class="builder-switch">
|
||||
<input type="checkbox" name="custom_is_required_{{ item.id }}" {% if item.is_required %}checked{% endif %} />
|
||||
<span>{% trans "Pflicht" %}</span>
|
||||
</label>
|
||||
<label class="builder-switch">
|
||||
<input type="checkbox" name="custom_is_active_{{ item.id }}" {% if item.is_active %}checked{% endif %} />
|
||||
<span>{% trans "Aktiv" %}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="builder-entity-grid">
|
||||
<label class="builder-entity-control">
|
||||
<span>{% trans "Abschnitt" %}</span>
|
||||
<select name="custom_section_key_{{ item.id }}">
|
||||
{% for group_choice in custom_field_groups %}
|
||||
<option value="{{ group_choice.key }}" {% if group_choice.key == item.section_key %}selected{% endif %}>{{ group_choice.title }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label class="builder-entity-control">
|
||||
<span>{% trans "Typ" %}</span>
|
||||
<select name="custom_field_type_{{ item.id }}">
|
||||
{% for value, label in custom_field_type_choices %}
|
||||
<option value="{{ value }}" {% if value == item.field_type %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label class="builder-entity-control builder-entity-control-narrow">
|
||||
<span>{% trans "Sortierung" %}</span>
|
||||
<input type="number" min="0" name="custom_sort_order_{{ item.id }}" value="{{ item.sort_order }}" />
|
||||
</label>
|
||||
<label class="builder-entity-control">
|
||||
<span>{% trans "Label (DE)" %}</span>
|
||||
<input type="text" name="custom_label_{{ item.id }}" value="{{ item.label }}" required />
|
||||
</label>
|
||||
<label class="builder-entity-control">
|
||||
<span>{% trans "Label (EN)" %}</span>
|
||||
<input type="text" name="custom_label_en_{{ item.id }}" value="{{ item.label_en }}" />
|
||||
</label>
|
||||
<label class="builder-entity-control">
|
||||
<span>{% trans "Hilfetext (DE)" %}</span>
|
||||
<input type="text" name="custom_help_text_{{ item.id }}" value="{{ item.help_text }}" />
|
||||
</label>
|
||||
<label class="builder-entity-control">
|
||||
<span>{% trans "Hilfetext (EN)" %}</span>
|
||||
<input type="text" name="custom_help_text_en_{{ item.id }}" value="{{ item.help_text_en }}" />
|
||||
</label>
|
||||
<label class="builder-entity-control builder-entity-control-full">
|
||||
<span>{% trans "Optionen (DE)" %}</span>
|
||||
<textarea name="custom_select_options_{{ item.id }}" rows="2">{{ item.select_options }}</textarea>
|
||||
</label>
|
||||
<label class="builder-entity-control builder-entity-control-full">
|
||||
<span>{% trans "Optionen (EN)" %}</span>
|
||||
<textarea name="custom_select_options_en_{{ item.id }}" rows="2">{{ item.select_options_en }}</textarea>
|
||||
</label>
|
||||
</div>
|
||||
<div class="builder-entity-card-actions">
|
||||
<button class="btn btn-secondary" type="submit" name="delete_custom_field_id" value="{{ item.id }}" data-confirm="{% trans 'Eigenes Feld wirklich löschen?' %}">{% trans "Löschen" %}</button>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</article>
|
||||
{% empty %}
|
||||
<tr><td colspan="9">{% trans "Keine eigenen Felder vorhanden." %}</td></tr>
|
||||
<div class="builder-empty-state">{% trans "Keine eigenen Felder vorhanden." %}</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="options-actions">
|
||||
<button class="btn btn-primary" type="submit" name="builder_action" value="save_custom_fields">{% trans "Eigene Felder speichern" %}</button>
|
||||
@@ -564,6 +687,34 @@
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="options-panel nested-accordion js-single-accordion" data-accordion-group="builder-content-subpanels" {% if active_subpanel == 'preview' %}open{% endif %}>
|
||||
<summary class="nested-accordion-summary">
|
||||
<div class="options-head">
|
||||
<h2>{% trans "Live-Vorschau" %}</h2>
|
||||
<span class="builder-panel-toggle">{% trans "Öffnen" %}</span>
|
||||
</div>
|
||||
</summary>
|
||||
<div class="nested-accordion-body">
|
||||
<div class="preview-shell preview-shell-compact">
|
||||
{% for section in preview_sections %}
|
||||
<section class="preview-section">
|
||||
<div class="preview-section-head">
|
||||
<h3>{{ section.title }}</h3>
|
||||
<span class="column-count">{% blocktrans trimmed with count=section.items|length %}{{ count }} Feld/Felder{% endblocktrans %}</span>
|
||||
</div>
|
||||
<div class="preview-chip-list">
|
||||
{% for item in section.items %}
|
||||
<span class="preview-chip{% if item.locked %} is-locked{% endif %}">{{ item.label }}</span>
|
||||
{% empty %}
|
||||
<span class="mini">{% trans "Keine sichtbaren Felder." %}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
@@ -59,13 +59,7 @@
|
||||
{% with field=block.field %}
|
||||
{% if field.is_hidden %}
|
||||
{{ field }}
|
||||
{% elif field.name in onboarding_inline_checks or field.field.widget.input_type == 'checkbox' %}
|
||||
<div class="field inline-check field-full {% if section.key == 'abschluss' %}finish-check{% endif %}">
|
||||
{{ field }} {{ field.label_tag }}
|
||||
{% if field.help_text %}<div class="hint">{{ field.help_text }}</div>{% endif %}
|
||||
{{ field.errors }}
|
||||
</div>
|
||||
{% elif section.key == 'itsetup' and field.name in onboarding_checkbox_lists %}
|
||||
{% elif field.name in onboarding_checkbox_lists %}
|
||||
<div class="itsetup-checklist-panel field-full">
|
||||
<div class="itsetup-checklist-head">
|
||||
<h3>{{ field.label }}</h3>
|
||||
@@ -83,8 +77,14 @@
|
||||
{{ field.errors }}
|
||||
</div>
|
||||
</div>
|
||||
{% elif field.name in onboarding_inline_checks or field.field.widget.input_type == 'checkbox' %}
|
||||
<div class="field inline-check field-full {% if section.key == 'abschluss' %}finish-check{% endif %}">
|
||||
{{ field }} {{ field.label_tag }}
|
||||
{% if field.help_text %}<div class="hint">{{ field.help_text }}</div>{% endif %}
|
||||
{{ field.errors }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="field {% if section.key == 'abschluss' %}finish-field{% endif %} {% if field.name in onboarding_checkbox_lists or field.name == 'gender' or field.name == 'additional_hardware_needed_choice' or field.name == 'additional_software_needed_choice' or field.name == 'additional_access_needed_choice' or field.name == 'successor_required_choice' %}field-full{% endif %} {% if field.name in onboarding_checkbox_lists %}checkbox-list{% endif %} {% if section.key == 'itsetup' and field.name in onboarding_checkbox_lists %}itsetup-checklist-block{% endif %}">
|
||||
<div class="field {% if section.key == 'abschluss' %}finish-field{% endif %} {% if field.name in onboarding_checkbox_lists or field.name == 'gender' or field.name == 'additional_hardware_needed_choice' or field.name == 'additional_software_needed_choice' or field.name == 'additional_access_needed_choice' or field.name == 'successor_required_choice' %}field-full{% endif %} {% if field.name in onboarding_checkbox_lists %}checkbox-list itsetup-checklist-block{% endif %}">
|
||||
{{ field.label_tag }}
|
||||
{{ field }}
|
||||
{% if field.help_text %}<div class="hint">{{ field.help_text }}</div>{% endif %}
|
||||
@@ -102,13 +102,7 @@
|
||||
{% for field in block.fields %}
|
||||
{% if field.is_hidden %}
|
||||
{{ field }}
|
||||
{% elif field.name in onboarding_inline_checks or field.field.widget.input_type == 'checkbox' %}
|
||||
<div class="field inline-check field-full {% if section.key == 'abschluss' %}finish-check{% endif %}">
|
||||
{{ field }} {{ field.label_tag }}
|
||||
{% if field.help_text %}<div class="hint">{{ field.help_text }}</div>{% endif %}
|
||||
{{ field.errors }}
|
||||
</div>
|
||||
{% elif section.key == 'itsetup' and field.name in onboarding_checkbox_lists %}
|
||||
{% elif field.name in onboarding_checkbox_lists %}
|
||||
<div class="itsetup-checklist-panel field-full">
|
||||
<div class="itsetup-checklist-head">
|
||||
<h3>{{ field.label }}</h3>
|
||||
@@ -126,8 +120,14 @@
|
||||
{{ field.errors }}
|
||||
</div>
|
||||
</div>
|
||||
{% elif field.name in onboarding_inline_checks or field.field.widget.input_type == 'checkbox' %}
|
||||
<div class="field inline-check field-full {% if section.key == 'abschluss' %}finish-check{% endif %}">
|
||||
{{ field }} {{ field.label_tag }}
|
||||
{% if field.help_text %}<div class="hint">{{ field.help_text }}</div>{% endif %}
|
||||
{{ field.errors }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="field {% if section.key == 'abschluss' %}finish-field{% endif %} {% if field.name in onboarding_checkbox_lists or field.name == 'gender' or field.name == 'additional_hardware_needed_choice' or field.name == 'additional_software_needed_choice' or field.name == 'additional_access_needed_choice' or field.name == 'successor_required_choice' %}field-full{% endif %} {% if field.name in onboarding_checkbox_lists %}checkbox-list{% endif %} {% if section.key == 'itsetup' and field.name in onboarding_checkbox_lists %}itsetup-checklist-block{% endif %}">
|
||||
<div class="field {% if section.key == 'abschluss' %}finish-field{% endif %} {% if field.name in onboarding_checkbox_lists or field.name == 'gender' or field.name == 'additional_hardware_needed_choice' or field.name == 'additional_software_needed_choice' or field.name == 'additional_access_needed_choice' or field.name == 'successor_required_choice' %}field-full{% endif %} {% if field.name in onboarding_checkbox_lists %}checkbox-list itsetup-checklist-block{% endif %}">
|
||||
{{ field.label_tag }}
|
||||
{{ field }}
|
||||
{% if field.help_text %}<div class="hint">{{ field.help_text }}</div>{% endif %}
|
||||
|
||||
@@ -3,7 +3,8 @@ import json
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import TestCase
|
||||
|
||||
from workflows.models import FormConditionalRuleConfig, FormCustomFieldConfig, FormFieldConfig, FormOption, FormSectionConfig
|
||||
from workflows.models import FormConditionalRuleConfig, FormCustomFieldConfig, FormCustomSectionConfig, FormFieldConfig, FormOption, FormSectionConfig
|
||||
from workflows.roles import ROLE_PLATFORM_OWNER, assign_user_role
|
||||
|
||||
|
||||
class FormBuilderAdminTests(TestCase):
|
||||
@@ -20,6 +21,14 @@ class FormBuilderAdminTests(TestCase):
|
||||
password='secret123',
|
||||
email='builder_user@tub.co',
|
||||
)
|
||||
self.platform_owner = user_model.objects.create_user(
|
||||
username='builder_owner',
|
||||
password='secret123',
|
||||
email='builder_owner@tub.co',
|
||||
is_staff=True,
|
||||
is_superuser=True,
|
||||
)
|
||||
assign_user_role(self.platform_owner, ROLE_PLATFORM_OWNER)
|
||||
|
||||
def test_staff_can_open_form_builder(self):
|
||||
self.client.force_login(self.staff)
|
||||
@@ -118,6 +127,25 @@ class FormBuilderAdminTests(TestCase):
|
||||
self.assertEqual(department.is_required, True)
|
||||
self.assertEqual(contract_start.is_required, None)
|
||||
|
||||
def test_platform_owner_can_modify_locked_field_rules(self):
|
||||
self.client.force_login(self.platform_owner)
|
||||
self.client.get('/admin-tools/form-builder/?form_type=onboarding', HTTP_HOST='localhost')
|
||||
full_name = FormFieldConfig.objects.get(form_type='onboarding', field_name='full_name')
|
||||
|
||||
response = self.client.post(
|
||||
'/admin-tools/form-builder/?form_type=onboarding&option_category=device',
|
||||
data={
|
||||
'builder_action': 'save_field_rules',
|
||||
'field_rule_ids': [str(full_name.id)],
|
||||
f'is_required_{full_name.id}': 'optional',
|
||||
},
|
||||
HTTP_HOST='localhost',
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
full_name.refresh_from_db()
|
||||
self.assertEqual(full_name.is_required, False)
|
||||
|
||||
def test_staff_can_save_section_rules_with_locked_sections_preserved(self):
|
||||
self.client.force_login(self.staff)
|
||||
self.client.get('/admin-tools/form-builder/?form_type=onboarding', HTTP_HOST='localhost')
|
||||
@@ -126,6 +154,7 @@ class FormBuilderAdminTests(TestCase):
|
||||
'/admin-tools/form-builder/?form_type=onboarding&option_category=device',
|
||||
data={
|
||||
'builder_action': 'save_section_rules',
|
||||
'section_order': ['itsetup', 'stammdaten', 'vertrag', 'abschluss'],
|
||||
},
|
||||
HTTP_HOST='localhost',
|
||||
)
|
||||
@@ -135,6 +164,31 @@ class FormBuilderAdminTests(TestCase):
|
||||
stammdaten = FormSectionConfig.objects.get(form_type='onboarding', section_key='stammdaten')
|
||||
self.assertEqual(itsetup.is_visible, False)
|
||||
self.assertEqual(stammdaten.is_visible, True)
|
||||
self.assertEqual(itsetup.sort_order, 0)
|
||||
self.assertEqual(stammdaten.sort_order, 1)
|
||||
|
||||
def test_platform_owner_can_modify_locked_section_rules(self):
|
||||
self.client.force_login(self.platform_owner)
|
||||
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_section_rules',
|
||||
'section_order': ['vertrag', 'stammdaten', 'itsetup', 'abschluss'],
|
||||
'section_visible_vertrag': 'on',
|
||||
'section_visible_itsetup': 'on',
|
||||
'section_visible_abschluss': 'on',
|
||||
},
|
||||
HTTP_HOST='localhost',
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
stammdaten = FormSectionConfig.objects.get(form_type='onboarding', section_key='stammdaten')
|
||||
vertrag = FormSectionConfig.objects.get(form_type='onboarding', section_key='vertrag')
|
||||
self.assertEqual(vertrag.sort_order, 0)
|
||||
self.assertEqual(stammdaten.sort_order, 1)
|
||||
self.assertEqual(stammdaten.is_visible, False)
|
||||
|
||||
def test_apply_onboarding_lean_preset_updates_section_and_field_rules(self):
|
||||
self.client.force_login(self.staff)
|
||||
@@ -185,19 +239,19 @@ class FormBuilderAdminTests(TestCase):
|
||||
'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',
|
||||
'conditional_active_successor-box': 'on',
|
||||
'conditional_field_successor-box_0': 'successor_required_choice',
|
||||
'conditional_operator_successor-box_0': 'equals',
|
||||
'conditional_value_successor-box_0': 'ja',
|
||||
'conditional_field_successor-box_1': 'inherit_phone_number_choice',
|
||||
'conditional_operator_successor-box_1': 'not_equals',
|
||||
'conditional_value_successor-box_1': 'ja',
|
||||
},
|
||||
HTTP_HOST='localhost',
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
rule = FormConditionalRuleConfig.objects.get(form_type='onboarding', target_key='phone-box')
|
||||
rule = FormConditionalRuleConfig.objects.get(form_type='onboarding', target_key='successor-box')
|
||||
self.assertEqual(rule.is_active, True)
|
||||
self.assertEqual(len(rule.clauses), 2)
|
||||
self.assertEqual(rule.clauses[0]['field'], 'successor_required_choice')
|
||||
@@ -258,3 +312,62 @@ class FormBuilderAdminTests(TestCase):
|
||||
custom_field.refresh_from_db()
|
||||
self.assertEqual(custom_field.section_key, 'itsetup')
|
||||
self.assertEqual(custom_field.sort_order, 2)
|
||||
|
||||
def test_staff_can_add_custom_section(self):
|
||||
self.client.force_login(self.staff)
|
||||
response = self.client.post(
|
||||
'/admin-tools/form-builder/?form_type=onboarding&option_category=device',
|
||||
data={
|
||||
'builder_action': 'add_custom_section',
|
||||
'custom_section_title': 'Benefits',
|
||||
'custom_section_title_en': 'Benefits',
|
||||
'custom_section_sort_order': '5',
|
||||
},
|
||||
HTTP_HOST='localhost',
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
section = FormCustomSectionConfig.objects.get(form_type='onboarding', section_key='benefits')
|
||||
self.assertEqual(section.title, 'Benefits')
|
||||
self.assertEqual(section.sort_order, 5)
|
||||
|
||||
def test_save_order_accepts_custom_section_column(self):
|
||||
self.client.force_login(self.staff)
|
||||
FormCustomSectionConfig.objects.create(
|
||||
form_type='onboarding',
|
||||
section_key='benefits',
|
||||
sort_order=10,
|
||||
title='Benefits',
|
||||
is_active=True,
|
||||
)
|
||||
custom_field = FormCustomFieldConfig.objects.create(
|
||||
form_type='onboarding',
|
||||
field_key='meal_allowance',
|
||||
section_key='stammdaten',
|
||||
sort_order=99,
|
||||
field_type='text',
|
||||
label='Essenszuschuss',
|
||||
)
|
||||
self.client.get('/admin-tools/form-builder/?form_type=onboarding', HTTP_HOST='localhost')
|
||||
|
||||
payload = {
|
||||
'form_type': 'onboarding',
|
||||
'columns': {
|
||||
'stammdaten': ['department'],
|
||||
'vertrag': ['contract_start'],
|
||||
'itsetup': [],
|
||||
'abschluss': [],
|
||||
'benefits': ['custom__meal_allowance'],
|
||||
},
|
||||
}
|
||||
|
||||
response = self.client.post(
|
||||
'/admin-tools/form-builder/save-order/',
|
||||
data=json.dumps(payload),
|
||||
content_type='application/json',
|
||||
HTTP_HOST='localhost',
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
custom_field.refresh_from_db()
|
||||
self.assertEqual(custom_field.section_key, 'benefits')
|
||||
|
||||
@@ -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 FormConditionalRuleConfig, FormCustomFieldConfig, FormFieldConfig, FormSectionConfig, OnboardingRequest
|
||||
from workflows.models import FormConditionalRuleConfig, FormCustomFieldConfig, FormCustomSectionConfig, FormFieldConfig, FormSectionConfig, OnboardingRequest
|
||||
|
||||
|
||||
class OnboardingFlowTests(TestCase):
|
||||
@@ -153,7 +153,7 @@ class OnboardingFlowTests(TestCase):
|
||||
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)
|
||||
self.assertNotIn('data-conditional-target="phone-box"', html)
|
||||
|
||||
def test_onboarding_page_uses_stored_conditional_rule_config(self):
|
||||
FormConditionalRuleConfig.objects.update_or_create(
|
||||
@@ -193,6 +193,39 @@ class OnboardingFlowTests(TestCase):
|
||||
|
||||
self.assertLess(html.index('Bürostandort'), html.index('Anrede'))
|
||||
|
||||
def test_phone_direct_dial_field_is_visible_without_successor(self):
|
||||
response = self.client.get('/onboarding/new/', HTTP_HOST='localhost')
|
||||
html = response.content.decode('utf-8')
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn('Telefon-Direktwahl', html)
|
||||
self.assertNotIn('data-conditional-target="phone-box"', html)
|
||||
|
||||
def test_onboarding_custom_section_is_rendered_in_navigation(self):
|
||||
FormCustomSectionConfig.objects.create(
|
||||
form_type='onboarding',
|
||||
section_key='benefits',
|
||||
sort_order=10,
|
||||
title='Benefits',
|
||||
is_active=True,
|
||||
)
|
||||
FormCustomFieldConfig.objects.create(
|
||||
form_type='onboarding',
|
||||
field_key='meal_allowance',
|
||||
section_key='benefits',
|
||||
sort_order=0,
|
||||
field_type='text',
|
||||
is_active=True,
|
||||
label='Essenszuschuss',
|
||||
)
|
||||
|
||||
response = self.client.get('/onboarding/new/', HTTP_HOST='localhost')
|
||||
html = response.content.decode('utf-8')
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn('Benefits', html)
|
||||
self.assertIn('Essenszuschuss', html)
|
||||
|
||||
@patch('workflows.views.process_onboarding_request.delay')
|
||||
def test_onboarding_custom_field_is_rendered_and_saved(self, mock_delay):
|
||||
FormCustomFieldConfig.objects.create(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from workflows.models import FormCustomFieldConfig, FormFieldConfig, FormSectionConfig, OffboardingRequest, OnboardingRequest
|
||||
from workflows.models import FormCustomFieldConfig, FormCustomSectionConfig, FormFieldConfig, FormSectionConfig, OffboardingRequest, OnboardingRequest
|
||||
from workflows.pdf_sections import build_pdf_sections
|
||||
|
||||
|
||||
@@ -123,3 +123,38 @@ class PDFSectionBuilderTests(TestCase):
|
||||
|
||||
self.assertEqual(custom_field['label'], 'Bürostandort')
|
||||
self.assertEqual(custom_field['display_value'], 'Berlin Mitte')
|
||||
|
||||
def test_custom_section_title_is_used_in_pdf_sections(self):
|
||||
FormCustomSectionConfig.objects.create(
|
||||
form_type='onboarding',
|
||||
section_key='benefits',
|
||||
sort_order=10,
|
||||
title='Benefits',
|
||||
is_active=True,
|
||||
)
|
||||
FormCustomFieldConfig.objects.create(
|
||||
form_type='onboarding',
|
||||
field_key='meal_allowance',
|
||||
section_key='benefits',
|
||||
sort_order=0,
|
||||
field_type='text',
|
||||
is_active=True,
|
||||
label='Essenszuschuss',
|
||||
)
|
||||
request_obj = OnboardingRequest.objects.create(
|
||||
full_name='Max Mustermann',
|
||||
gender='herr',
|
||||
job_title='Consultant',
|
||||
department='IT-Service',
|
||||
work_email='max.mustermann@workdock.de',
|
||||
contract_start='2026-11-01',
|
||||
employment_type='unbefristet',
|
||||
agreement='accepted',
|
||||
custom_field_values={'meal_allowance': 'Ja'},
|
||||
)
|
||||
|
||||
sections = build_pdf_sections('onboarding', request_obj, 'de')
|
||||
|
||||
custom_section = next(section for section in sections if section['key'] == 'benefits')
|
||||
self.assertEqual(custom_section['title'], 'Benefits')
|
||||
self.assertIn('custom__meal_allowance', [field['name'] for field in custom_section['fields']])
|
||||
|
||||
@@ -45,20 +45,20 @@ from .form_builder import (
|
||||
OFFBOARDING_PAGE_LABELS,
|
||||
OFFBOARDING_PAGE_ORDER,
|
||||
ONBOARDING_DEFAULT_PAGE,
|
||||
ONBOARDING_PAGE_LABELS,
|
||||
ONBOARDING_PAGE_ORDER,
|
||||
build_custom_field_key,
|
||||
custom_field_target_key,
|
||||
ensure_form_field_configs,
|
||||
ensure_form_conditional_rule_configs,
|
||||
ensure_form_section_configs,
|
||||
get_custom_field_configs,
|
||||
get_custom_section_configs,
|
||||
get_default_page_map,
|
||||
get_section_definitions,
|
||||
get_section_labels,
|
||||
get_section_order,
|
||||
apply_form_preset,
|
||||
)
|
||||
from .models import AdminAuditLog, AsyncTaskLog, EmployeeProfile, FormConditionalRuleConfig, FormCustomFieldConfig, 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, FormCustomFieldConfig, FormCustomSectionConfig, 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
|
||||
@@ -106,7 +106,6 @@ ONBOARDING_GROUPS = {
|
||||
'extra-software-box': ['additional_software_multi', 'additional_software'],
|
||||
'extra-access-box': ['additional_access_text'],
|
||||
'successor-box': ['successor_name', 'inherit_phone_number_choice'],
|
||||
'phone-box': ['phone_number_choice'],
|
||||
}
|
||||
|
||||
ONBOARDING_INLINE_CHECKS = {'order_business_cards', 'agreement_confirm'}
|
||||
@@ -119,7 +118,6 @@ ONBOARDING_CHECKBOX_LISTS = {
|
||||
'needed_workspace_groups_multi',
|
||||
'needed_resources_multi',
|
||||
}
|
||||
ONBOARDING_SECTION_ORDER = ['stammdaten', 'vertrag', 'itsetup', 'abschluss']
|
||||
ONBOARDING_SECTION_META = {
|
||||
'stammdaten': {'title': gettext_lazy('Stammdaten'), 'subtitle': gettext_lazy('Person, Rolle, Abteilung')},
|
||||
'vertrag': {'title': gettext_lazy('Vertrag'), 'subtitle': gettext_lazy('Beschäftigung und Termine')},
|
||||
@@ -602,21 +600,24 @@ def _section_for_block(block: dict, field_pages: dict[str, str]) -> str:
|
||||
|
||||
|
||||
def _build_onboarding_sections(blocks: list[dict], field_pages: dict[str, str], visible_section_keys: set[str] | None = None) -> list[dict]:
|
||||
grouped = {key: [] for key in ONBOARDING_SECTION_ORDER}
|
||||
section_defs = get_section_definitions('onboarding')
|
||||
section_order = [item['key'] for item in section_defs]
|
||||
section_titles = {item['key']: item['title'] for item in section_defs}
|
||||
grouped = {key: [] for key in section_order}
|
||||
for block in blocks:
|
||||
section_key = _section_for_block(block, field_pages)
|
||||
if section_key not in grouped:
|
||||
section_key = 'abschluss'
|
||||
grouped[section_key].append(block)
|
||||
visible_keys = visible_section_keys or set(ONBOARDING_SECTION_ORDER)
|
||||
visible_keys = visible_section_keys or set(section_order)
|
||||
return [
|
||||
{
|
||||
'key': key,
|
||||
'title': ONBOARDING_SECTION_META[key]['title'],
|
||||
'subtitle': ONBOARDING_SECTION_META[key]['subtitle'],
|
||||
'title': section_titles.get(key, ONBOARDING_SECTION_META.get(key, {}).get('title', key)),
|
||||
'subtitle': ONBOARDING_SECTION_META.get(key, {}).get('subtitle', ''),
|
||||
'blocks': grouped[key],
|
||||
}
|
||||
for key in ONBOARDING_SECTION_ORDER
|
||||
for key in section_order
|
||||
if key in visible_keys
|
||||
]
|
||||
|
||||
@@ -1930,10 +1931,14 @@ def onboarding_create(request):
|
||||
onboarding_blocks = _build_onboarding_layout(form)
|
||||
field_pages = getattr(form, '_field_page_keys', {})
|
||||
section_configs = ensure_form_section_configs('onboarding')
|
||||
visible_section_keys = {
|
||||
key for key in ONBOARDING_SECTION_ORDER
|
||||
if key in LOCKED_SECTION_RULES.get('onboarding', set()) or section_configs.get(key, None) is None or section_configs[key].is_visible
|
||||
}
|
||||
visible_section_keys = set()
|
||||
for section in get_section_definitions('onboarding'):
|
||||
key = section['key']
|
||||
if section.get('is_custom'):
|
||||
if section.get('is_active', True):
|
||||
visible_section_keys.add(key)
|
||||
elif key in LOCKED_SECTION_RULES.get('onboarding', set()) or section_configs.get(key, None) is None or section_configs[key].is_visible:
|
||||
visible_section_keys.add(key)
|
||||
onboarding_sections = _build_onboarding_sections(onboarding_blocks, field_pages, visible_section_keys=visible_section_keys)
|
||||
onboarding_conditional_rules = _normalized_conditional_rule_payload('onboarding')
|
||||
|
||||
@@ -2162,9 +2167,11 @@ def offboarding_success(request, request_id: int):
|
||||
def form_builder_page(request):
|
||||
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()
|
||||
if form_type not in DEFAULT_FIELD_ORDER:
|
||||
form_type = 'onboarding'
|
||||
option_category = request.GET.get('option_category', 'department')
|
||||
@@ -2267,6 +2274,54 @@ def form_builder_page(request):
|
||||
_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()
|
||||
@@ -2360,7 +2415,7 @@ def form_builder_page(request):
|
||||
cfg = FormFieldConfig.objects.filter(id=raw_id, form_type=form_type).first()
|
||||
if not cfg:
|
||||
continue
|
||||
if cfg.field_name in locked_fields:
|
||||
if cfg.field_name in locked_fields and not can_override_locked_builder_rules:
|
||||
cfg.is_visible = True
|
||||
cfg.is_required = None
|
||||
else:
|
||||
@@ -2375,9 +2430,27 @@ def form_builder_page(request):
|
||||
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:
|
||||
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'])
|
||||
@@ -2385,6 +2458,11 @@ def form_builder_page(request):
|
||||
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.')
|
||||
|
||||
@@ -2412,13 +2490,14 @@ def form_builder_page(request):
|
||||
elif action == 'apply_preset':
|
||||
preset_key = (request.POST.get('preset_key') or '').strip()
|
||||
if apply_form_preset(form_type, preset_key):
|
||||
active_panel = 'builder-preview'
|
||||
active_panel = 'builder-content'
|
||||
active_subpanel = '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', 'save_field_texts', 'add_custom_field', 'save_custom_fields'}:
|
||||
if action in {'add_option', 'save_options', 'save_field_texts', 'add_custom_field', 'save_custom_fields', 'add_custom_section', 'save_custom_sections'}:
|
||||
active_panel = 'builder-content'
|
||||
if action in {'add_option', 'save_options'}:
|
||||
active_subpanel = 'options'
|
||||
@@ -2426,13 +2505,23 @@ def form_builder_page(request):
|
||||
active_subpanel = 'field-texts'
|
||||
elif action in {'add_custom_field', 'save_custom_fields'}:
|
||||
active_subpanel = 'custom-fields'
|
||||
elif action in {'add_custom_section', 'save_custom_sections'}:
|
||||
active_subpanel = 'custom-sections'
|
||||
elif action in {'save_field_rules', 'save_section_rules', 'save_conditional_rules'}:
|
||||
active_panel = 'builder-rules'
|
||||
if action == 'save_section_rules':
|
||||
active_rules_panel = 'section-rules'
|
||||
elif action == 'save_field_rules':
|
||||
active_rules_panel = 'field-rules'
|
||||
elif action == 'save_conditional_rules':
|
||||
active_rules_panel = 'conditional-rules'
|
||||
redirect_target = f"/admin-tools/form-builder/?form_type={form_type}&option_category={option_category}"
|
||||
if active_panel:
|
||||
redirect_target += f"&panel={active_panel}"
|
||||
if active_subpanel:
|
||||
redirect_target += f"&subpanel={active_subpanel}"
|
||||
if active_rules_panel:
|
||||
redirect_target += f"&rules_panel={active_rules_panel}"
|
||||
if anchor == 'builder-content' or active_panel == 'builder-content':
|
||||
redirect_target += "#builder-content"
|
||||
return redirect(redirect_target)
|
||||
@@ -2450,8 +2539,9 @@ 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)
|
||||
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(
|
||||
@@ -2461,15 +2551,16 @@ def form_builder_page(request):
|
||||
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': ONBOARDING_PAGE_LABELS.get(key, key),
|
||||
'title': section_labels.get(key, key),
|
||||
'items': [],
|
||||
}
|
||||
for key in ONBOARDING_PAGE_ORDER
|
||||
for key in section_order
|
||||
]
|
||||
column_by_key = {c['key']: c for c in columns}
|
||||
fallback = 'abschluss'
|
||||
@@ -2564,15 +2655,20 @@ def form_builder_page(request):
|
||||
section_rule_items = []
|
||||
if section_order:
|
||||
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
|
||||
section_rule_items.append(
|
||||
{
|
||||
'key': key,
|
||||
'title': section_labels.get(key, key),
|
||||
'is_visible': True if not cfg else cfg.is_visible,
|
||||
'locked': key in locked_sections,
|
||||
'field_count': len([c for c in configs if (c.page_key or default_page_map.get(c.field_name, fallback_section)) == key]),
|
||||
'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]),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -2588,7 +2684,7 @@ def form_builder_page(request):
|
||||
'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,
|
||||
'locked': cfg.field_name in locked and not can_override_locked_builder_rules,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -2659,7 +2755,6 @@ def form_builder_page(request):
|
||||
'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.'),
|
||||
@@ -2669,7 +2764,6 @@ def form_builder_page(request):
|
||||
'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 [])
|
||||
@@ -2704,7 +2798,8 @@ def form_builder_page(request):
|
||||
for key in section_order:
|
||||
section_cfg = section_configs.get(key)
|
||||
section_locked = key in locked_sections
|
||||
section_visible = True if section_locked or not section_cfg else section_cfg.is_visible
|
||||
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']
|
||||
@@ -2738,6 +2833,7 @@ def form_builder_page(request):
|
||||
'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]),
|
||||
}
|
||||
|
||||
return render(
|
||||
@@ -2760,9 +2856,12 @@ def form_builder_page(request):
|
||||
'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,
|
||||
'available_presets': FORM_PRESETS.get(form_type, {}),
|
||||
'can_override_locked_builder_rules': can_override_locked_builder_rules,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -3213,10 +3312,8 @@ def form_builder_save_order(request):
|
||||
allowed_names = {cfg.field_name for cfg in configs} | {f'custom__{cfg.field_key}' for cfg in custom_configs}
|
||||
seen = set()
|
||||
|
||||
if form_type == 'onboarding':
|
||||
allowed_columns = ONBOARDING_PAGE_ORDER
|
||||
else:
|
||||
allowed_columns = OFFBOARDING_PAGE_ORDER
|
||||
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}
|
||||
@@ -3248,20 +3345,14 @@ def form_builder_save_order(request):
|
||||
cfg = name_to_cfg[name]
|
||||
cfg.sort_order = sort_order
|
||||
sort_order += 1
|
||||
if form_type == 'onboarding':
|
||||
cfg.page_key = cfg.page_key or ONBOARDING_DEFAULT_PAGE.get(name, 'abschluss')
|
||||
else:
|
||||
cfg.page_key = cfg.page_key or default_page_map.get(name, OFFBOARDING_PAGE_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
|
||||
if form_type == 'onboarding':
|
||||
cfg.section_key = cfg.section_key or 'abschluss'
|
||||
else:
|
||||
cfg.section_key = cfg.section_key or OFFBOARDING_PAGE_ORDER[-1]
|
||||
cfg.section_key = cfg.section_key or fallback_section
|
||||
|
||||
FormFieldConfig.objects.bulk_update(configs, ['sort_order', 'page_key'])
|
||||
if custom_configs:
|
||||
|
||||
Reference in New Issue
Block a user