snapshot: preserve dynamic form builder parity and presets
This commit is contained in:
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from django.utils.translation import get_language
|
from django.utils.translation import get_language
|
||||||
|
|
||||||
from .models import FormFieldConfig
|
from .models import FormFieldConfig, FormSectionConfig
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_FIELD_ORDER = {
|
DEFAULT_FIELD_ORDER = {
|
||||||
@@ -58,18 +58,29 @@ DEFAULT_FIELD_ORDER = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ONBOARDING_PAGE_ORDER = ['stammdaten', 'vertrag', 'itsetup', 'abschluss']
|
ONBOARDING_PAGE_ORDER = ['stammdaten', 'vertrag', 'itsetup', 'abschluss']
|
||||||
|
OFFBOARDING_PAGE_ORDER = ['mitarbeitende', 'austritt', 'abschluss']
|
||||||
ONBOARDING_PAGE_LABELS = {
|
ONBOARDING_PAGE_LABELS = {
|
||||||
'stammdaten': '1. Stammdaten',
|
'stammdaten': '1. Stammdaten',
|
||||||
'vertrag': '2. Vertrag',
|
'vertrag': '2. Vertrag',
|
||||||
'itsetup': '3. IT-Setup',
|
'itsetup': '3. IT-Setup',
|
||||||
'abschluss': '4. Abschluss',
|
'abschluss': '4. Abschluss',
|
||||||
}
|
}
|
||||||
|
OFFBOARDING_PAGE_LABELS = {
|
||||||
|
'mitarbeitende': '1. Mitarbeitende',
|
||||||
|
'austritt': '2. Austritt',
|
||||||
|
'abschluss': '3. Abschluss',
|
||||||
|
}
|
||||||
|
|
||||||
LOCKED_FIELD_RULES = {
|
LOCKED_FIELD_RULES = {
|
||||||
'onboarding': {'full_name', 'work_email', 'contract_start', 'agreement_confirm'},
|
'onboarding': {'full_name', 'work_email', 'contract_start', 'agreement_confirm'},
|
||||||
'offboarding': {'full_name', 'work_email', 'last_working_day'},
|
'offboarding': {'full_name', 'work_email', 'last_working_day'},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LOCKED_SECTION_RULES = {
|
||||||
|
'onboarding': {'stammdaten', 'vertrag', 'abschluss'},
|
||||||
|
'offboarding': {'mitarbeitende', 'austritt'},
|
||||||
|
}
|
||||||
|
|
||||||
ONBOARDING_DEFAULT_PAGE = {
|
ONBOARDING_DEFAULT_PAGE = {
|
||||||
'first_name': 'stammdaten',
|
'first_name': 'stammdaten',
|
||||||
'last_name': 'stammdaten',
|
'last_name': 'stammdaten',
|
||||||
@@ -112,6 +123,38 @@ ONBOARDING_DEFAULT_PAGE = {
|
|||||||
'onboarded_by_email': 'abschluss',
|
'onboarded_by_email': 'abschluss',
|
||||||
'agreement_confirm': 'abschluss',
|
'agreement_confirm': 'abschluss',
|
||||||
}
|
}
|
||||||
|
OFFBOARDING_DEFAULT_PAGE = {
|
||||||
|
'full_name': 'mitarbeitende',
|
||||||
|
'work_email': 'mitarbeitende',
|
||||||
|
'department': 'mitarbeitende',
|
||||||
|
'job_title': 'mitarbeitende',
|
||||||
|
'last_working_day': 'austritt',
|
||||||
|
'notes': 'abschluss',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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 []
|
||||||
|
|
||||||
|
|
||||||
|
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 {}
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_page_map(form_type: str) -> dict[str, str]:
|
||||||
|
if form_type == 'onboarding':
|
||||||
|
return ONBOARDING_DEFAULT_PAGE
|
||||||
|
if form_type == 'offboarding':
|
||||||
|
return OFFBOARDING_DEFAULT_PAGE
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
def _default_sort(form_type: str, field_name: str) -> int:
|
def _default_sort(form_type: str, field_name: str) -> int:
|
||||||
@@ -134,7 +177,7 @@ def _ensure_configs(form_type: str, field_names: list[str]) -> dict[str, FormFie
|
|||||||
form_type=form_type,
|
form_type=form_type,
|
||||||
field_name=name,
|
field_name=name,
|
||||||
sort_order=_default_sort(form_type, name),
|
sort_order=_default_sort(form_type, name),
|
||||||
page_key=ONBOARDING_DEFAULT_PAGE.get(name, '') if form_type == 'onboarding' else '',
|
page_key=get_default_page_map(form_type).get(name, ''),
|
||||||
)
|
)
|
||||||
for name in missing_names
|
for name in missing_names
|
||||||
],
|
],
|
||||||
@@ -151,10 +194,34 @@ def ensure_form_field_configs(form_type: str, field_names: list[str]) -> dict[st
|
|||||||
return _ensure_configs(form_type, field_names)
|
return _ensure_configs(form_type, field_names)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_form_section_configs(form_type: str) -> dict[str, FormSectionConfig]:
|
||||||
|
section_order = get_section_order(form_type)
|
||||||
|
if not section_order:
|
||||||
|
return {}
|
||||||
|
existing = {
|
||||||
|
cfg.section_key: cfg
|
||||||
|
for cfg in FormSectionConfig.objects.filter(form_type=form_type)
|
||||||
|
}
|
||||||
|
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],
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
existing = {
|
||||||
|
cfg.section_key: cfg
|
||||||
|
for cfg in FormSectionConfig.objects.filter(form_type=form_type)
|
||||||
|
}
|
||||||
|
return existing
|
||||||
|
|
||||||
|
|
||||||
def apply_form_field_config(form_type: str, form) -> None:
|
def apply_form_field_config(form_type: str, form) -> None:
|
||||||
field_names = list(form.fields.keys())
|
field_names = list(form.fields.keys())
|
||||||
configs = _ensure_configs(form_type, field_names)
|
configs = _ensure_configs(form_type, field_names)
|
||||||
|
section_configs = ensure_form_section_configs(form_type)
|
||||||
locked = LOCKED_FIELD_RULES.get(form_type, set())
|
locked = LOCKED_FIELD_RULES.get(form_type, set())
|
||||||
|
locked_sections = LOCKED_SECTION_RULES.get(form_type, set())
|
||||||
|
default_page_map = get_default_page_map(form_type)
|
||||||
language_code = get_language()
|
language_code = get_language()
|
||||||
|
|
||||||
for field_name, field in list(form.fields.items()):
|
for field_name, field in list(form.fields.items()):
|
||||||
@@ -173,7 +240,14 @@ def apply_form_field_config(form_type: str, form) -> None:
|
|||||||
if field_name not in locked and cfg.is_required is not None:
|
if field_name not in locked and cfg.is_required is not None:
|
||||||
field.required = cfg.is_required
|
field.required = cfg.is_required
|
||||||
|
|
||||||
if field_name not in locked and not cfg.is_visible:
|
section_key = cfg.page_key or default_page_map.get(field_name, '')
|
||||||
|
section_hidden = (
|
||||||
|
form_type in {'onboarding', 'offboarding'}
|
||||||
|
and section_key not in locked_sections
|
||||||
|
and section_key in section_configs
|
||||||
|
and not section_configs[section_key].is_visible
|
||||||
|
)
|
||||||
|
if field_name not in locked and (not cfg.is_visible or section_hidden):
|
||||||
form.fields.pop(field_name, None)
|
form.fields.pop(field_name, None)
|
||||||
|
|
||||||
ordered_items = sorted(
|
ordered_items = sorted(
|
||||||
@@ -184,9 +258,138 @@ def apply_form_field_config(form_type: str, form) -> None:
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
form.fields = OrderedDict(ordered_items)
|
form.fields = OrderedDict(ordered_items)
|
||||||
if form_type == 'onboarding':
|
if form_type in {'onboarding', 'offboarding'}:
|
||||||
form._field_page_keys = {
|
form._field_page_keys = {
|
||||||
name: (configs[name].page_key or ONBOARDING_DEFAULT_PAGE.get(name, 'abschluss'))
|
name: (configs[name].page_key or default_page_map.get(name, ''))
|
||||||
for name in form.fields.keys()
|
for name in form.fields.keys()
|
||||||
if name in configs
|
if name in configs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
FORM_PRESETS = {
|
||||||
|
'onboarding': {
|
||||||
|
'standard': {
|
||||||
|
'label': 'Standard',
|
||||||
|
'sections': {
|
||||||
|
'stammdaten': True,
|
||||||
|
'vertrag': True,
|
||||||
|
'itsetup': True,
|
||||||
|
'abschluss': True,
|
||||||
|
},
|
||||||
|
'fields': {},
|
||||||
|
},
|
||||||
|
'lean': {
|
||||||
|
'label': 'Lean',
|
||||||
|
'sections': {
|
||||||
|
'stammdaten': True,
|
||||||
|
'vertrag': True,
|
||||||
|
'itsetup': False,
|
||||||
|
'abschluss': True,
|
||||||
|
},
|
||||||
|
'fields': {
|
||||||
|
'gender': {'is_visible': False},
|
||||||
|
'order_business_cards': {'is_visible': False},
|
||||||
|
'business_card_name': {'is_visible': False},
|
||||||
|
'business_card_title': {'is_visible': False},
|
||||||
|
'business_card_email': {'is_visible': False},
|
||||||
|
'business_card_phone': {'is_visible': False},
|
||||||
|
'employment_end_date': {'is_visible': False},
|
||||||
|
'group_mailboxes_required_choice': {'is_visible': False},
|
||||||
|
'group_mailboxes': {'is_visible': False},
|
||||||
|
'additional_notes': {'is_required': False},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'it_heavy': {
|
||||||
|
'label': 'IT-heavy',
|
||||||
|
'sections': {
|
||||||
|
'stammdaten': True,
|
||||||
|
'vertrag': True,
|
||||||
|
'itsetup': True,
|
||||||
|
'abschluss': True,
|
||||||
|
},
|
||||||
|
'fields': {
|
||||||
|
'needed_devices_multi': {'is_required': True},
|
||||||
|
'needed_software_multi': {'is_required': True},
|
||||||
|
'needed_accesses_multi': {'is_required': True},
|
||||||
|
'needed_workspace_groups_multi': {'is_required': True},
|
||||||
|
'needed_resources_multi': {'is_required': True},
|
||||||
|
'additional_hardware_needed_choice': {'is_visible': True},
|
||||||
|
'additional_software_needed_choice': {'is_visible': True},
|
||||||
|
'additional_access_needed_choice': {'is_visible': True},
|
||||||
|
'successor_required_choice': {'is_visible': True},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'offboarding': {
|
||||||
|
'standard': {
|
||||||
|
'label': 'Standard',
|
||||||
|
'sections': {
|
||||||
|
'mitarbeitende': True,
|
||||||
|
'austritt': True,
|
||||||
|
'abschluss': True,
|
||||||
|
},
|
||||||
|
'fields': {},
|
||||||
|
},
|
||||||
|
'lean': {
|
||||||
|
'label': 'Lean',
|
||||||
|
'sections': {
|
||||||
|
'mitarbeitende': True,
|
||||||
|
'austritt': True,
|
||||||
|
'abschluss': False,
|
||||||
|
},
|
||||||
|
'fields': {
|
||||||
|
'department': {'is_visible': False},
|
||||||
|
'job_title': {'is_visible': False},
|
||||||
|
'notes': {'is_visible': False},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'hr_heavy': {
|
||||||
|
'label': 'HR-heavy',
|
||||||
|
'sections': {
|
||||||
|
'mitarbeitende': True,
|
||||||
|
'austritt': True,
|
||||||
|
'abschluss': True,
|
||||||
|
},
|
||||||
|
'fields': {
|
||||||
|
'department': {'is_visible': True, 'is_required': True},
|
||||||
|
'job_title': {'is_visible': True, 'is_required': True},
|
||||||
|
'notes': {'is_visible': True, 'is_required': True},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def apply_form_preset(form_type: str, preset_key: str) -> bool:
|
||||||
|
preset = FORM_PRESETS.get(form_type, {}).get(preset_key)
|
||||||
|
if not preset:
|
||||||
|
return False
|
||||||
|
|
||||||
|
locked_fields = LOCKED_FIELD_RULES.get(form_type, set())
|
||||||
|
locked_sections = LOCKED_SECTION_RULES.get(form_type, set())
|
||||||
|
default_names = list(DEFAULT_FIELD_ORDER.get(form_type, []))
|
||||||
|
ensure_form_field_configs(form_type, default_names)
|
||||||
|
section_configs = ensure_form_section_configs(form_type)
|
||||||
|
|
||||||
|
for section_key, is_visible in preset.get('sections', {}).items():
|
||||||
|
cfg = section_configs.get(section_key)
|
||||||
|
if not cfg or section_key in locked_sections:
|
||||||
|
continue
|
||||||
|
cfg.is_visible = bool(is_visible)
|
||||||
|
cfg.save(update_fields=['is_visible'])
|
||||||
|
|
||||||
|
for cfg in FormFieldConfig.objects.filter(form_type=form_type):
|
||||||
|
if cfg.field_name in locked_fields:
|
||||||
|
cfg.is_visible = True
|
||||||
|
cfg.is_required = None
|
||||||
|
else:
|
||||||
|
cfg.is_visible = True
|
||||||
|
cfg.is_required = None
|
||||||
|
override = preset.get('fields', {}).get(cfg.field_name, {})
|
||||||
|
if 'is_visible' in override:
|
||||||
|
cfg.is_visible = bool(override['is_visible'])
|
||||||
|
if 'is_required' in override:
|
||||||
|
cfg.is_required = override['is_required']
|
||||||
|
cfg.save(update_fields=['is_visible', 'is_required'])
|
||||||
|
|
||||||
|
return True
|
||||||
|
|||||||
37
backend/workflows/migrations/0053_formsectionconfig.py
Normal file
37
backend/workflows/migrations/0053_formsectionconfig.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def seed_onboarding_sections(apps, schema_editor):
|
||||||
|
FormSectionConfig = apps.get_model('workflows', 'FormSectionConfig')
|
||||||
|
for section_key in ['stammdaten', 'vertrag', 'itsetup', 'abschluss']:
|
||||||
|
FormSectionConfig.objects.get_or_create(
|
||||||
|
form_type='onboarding',
|
||||||
|
section_key=section_key,
|
||||||
|
defaults={'is_visible': True},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('workflows', '0052_userprofile_notification_preferences'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='FormSectionConfig',
|
||||||
|
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.CharField(choices=[('stammdaten', 'Stammdaten'), ('vertrag', 'Vertrag'), ('itsetup', 'IT-Setup'), ('abschluss', 'Abschluss')], max_length=20)),
|
||||||
|
('is_visible', models.BooleanField(default=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Formularabschnitt-Konfiguration',
|
||||||
|
'verbose_name_plural': 'Formularabschnitt-Konfigurationen',
|
||||||
|
'ordering': ['form_type', 'section_key'],
|
||||||
|
'unique_together': {('form_type', 'section_key')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.RunPython(seed_onboarding_sections, migrations.RunPython.noop),
|
||||||
|
]
|
||||||
@@ -516,6 +516,31 @@ class FormFieldConfig(models.Model):
|
|||||||
return self.help_text_override.strip()
|
return self.help_text_override.strip()
|
||||||
|
|
||||||
|
|
||||||
|
class FormSectionConfig(models.Model):
|
||||||
|
FORM_CHOICES = [
|
||||||
|
('onboarding', _('Onboarding')),
|
||||||
|
]
|
||||||
|
SECTION_CHOICES = [
|
||||||
|
('stammdaten', _('Stammdaten')),
|
||||||
|
('vertrag', _('Vertrag')),
|
||||||
|
('itsetup', _('IT-Setup')),
|
||||||
|
('abschluss', _('Abschluss')),
|
||||||
|
]
|
||||||
|
|
||||||
|
form_type = models.CharField(max_length=20, choices=FORM_CHOICES)
|
||||||
|
section_key = models.CharField(max_length=20, choices=SECTION_CHOICES)
|
||||||
|
is_visible = models.BooleanField(default=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['form_type', 'section_key']
|
||||||
|
unique_together = ('form_type', 'section_key')
|
||||||
|
verbose_name = 'Formularabschnitt-Konfiguration'
|
||||||
|
verbose_name_plural = 'Formularabschnitt-Konfigurationen'
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f'{self.form_type}: {self.section_key}'
|
||||||
|
|
||||||
|
|
||||||
class NotificationTemplate(models.Model):
|
class NotificationTemplate(models.Model):
|
||||||
TEMPLATE_CHOICES = [
|
TEMPLATE_CHOICES = [
|
||||||
('onboarding_it', _('Onboarding: IT')),
|
('onboarding_it', _('Onboarding: IT')),
|
||||||
|
|||||||
@@ -1,72 +1,98 @@
|
|||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: Arial, sans-serif;
|
font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
|
||||||
background: #f4f7fb;
|
background:
|
||||||
color: #1f2937;
|
radial-gradient(circle at top right, rgba(0, 120, 255, 0.08), transparent 28%),
|
||||||
|
linear-gradient(180deg, #eef4fb 0%, #f7f9fc 100%);
|
||||||
|
color: #142033;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shell {
|
.shell {
|
||||||
width: min(1280px, 94%);
|
width: min(1320px, 94%);
|
||||||
margin: 20px auto 28px;
|
margin: 20px auto 32px;
|
||||||
background: #ffffff;
|
background: rgba(255, 255, 255, 0.92);
|
||||||
border: 1px solid #d8e2f0;
|
border: 1px solid rgba(191, 204, 222, 0.8);
|
||||||
border-radius: 14px;
|
border-radius: 20px;
|
||||||
padding: 18px;
|
padding: 20px;
|
||||||
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08);
|
box-shadow: 0 22px 60px rgba(15, 23, 42, 0.08);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.topbar {
|
.builder-hero,
|
||||||
|
.builder-panel,
|
||||||
|
.builder-stat-card,
|
||||||
|
.section-rule-card,
|
||||||
|
.field-card,
|
||||||
|
.options-panel {
|
||||||
|
animation: builderFadeIn 0.32s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-hero {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: flex-end;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 12px;
|
gap: 20px;
|
||||||
margin-bottom: 10px;
|
padding: 8px 0 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-logo {
|
.builder-hero-copy {
|
||||||
width: 190px;
|
max-width: 760px;
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
display: block;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header h1 {
|
.builder-eyebrow {
|
||||||
margin: 0;
|
display: inline-flex;
|
||||||
font-size: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header p {
|
|
||||||
margin: 6px 0 0;
|
|
||||||
color: #64748b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar {
|
|
||||||
margin-top: 14px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 6px 11px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #e9f2ff;
|
||||||
|
color: #174ea6;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-hero h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: clamp(30px, 4vw, 40px);
|
||||||
|
line-height: 1.02;
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-hero-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab {
|
.tab {
|
||||||
border: 1px solid #cbd5e1;
|
border: 1px solid #c6d1e1;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
padding: 8px 14px;
|
padding: 9px 15px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: #1f2937;
|
color: #1c2a41;
|
||||||
background: #f8fafc;
|
background: #f8fbff;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
|
transition: transform 0.18s ease, border-color 0.18s ease, background 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
border-color: #9db4d2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab.active {
|
.tab.active {
|
||||||
background: #000078;
|
background: linear-gradient(135deg, #0f3b7a 0%, #1759b8 100%);
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
border-color: #000078;
|
border-color: #1759b8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status {
|
.status {
|
||||||
min-height: 22px;
|
min-height: 22px;
|
||||||
margin: 10px 0 8px;
|
margin: 14px 0 10px;
|
||||||
color: #334155;
|
color: #334155;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
@@ -83,11 +109,401 @@ body {
|
|||||||
color: #166534;
|
color: #166534;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.builder-overview {
|
||||||
|
margin-top: 12px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-stat-card,
|
||||||
|
.builder-panel,
|
||||||
|
.options-panel {
|
||||||
|
border: 1px solid rgba(201, 212, 226, 0.95);
|
||||||
|
border-radius: 18px;
|
||||||
|
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(247, 250, 255, 0.98));
|
||||||
|
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;
|
||||||
|
color: #142033;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini {
|
||||||
|
color: #61718a;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-quicknav {
|
||||||
|
margin-top: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-quicknav a {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 34px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border: 1px solid #d1dbea;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #f7fbff;
|
||||||
|
color: #304159;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: transform 0.18s ease, border-color 0.18s ease, background-color 0.18s ease, box-shadow 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-quicknav a:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
border-color: #adc2dd;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 10px 18px rgba(15, 23, 42, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-preset-bar {
|
||||||
|
margin-top: 12px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-preset-form {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-preset-label {
|
||||||
|
color: #61718a;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-preset-form select {
|
||||||
|
min-height: 36px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border: 1px solid #cfdbeb;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: linear-gradient(180deg, #ffffff, #f6faff);
|
||||||
|
color: #24405f;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-panel {
|
||||||
|
margin-top: 14px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-panel-head,
|
||||||
|
.options-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 14px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options-head-inline {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-accordion {
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nested-accordion {
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nested-accordion-summary {
|
||||||
|
list-style: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 14px 16px;
|
||||||
|
transition: background-color 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nested-accordion-summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nested-accordion-summary .options-head {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nested-accordion-summary:hover {
|
||||||
|
background: rgba(242, 247, 255, 0.72);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nested-accordion[open] .nested-accordion-summary {
|
||||||
|
border-bottom: 1px solid rgba(201, 212, 226, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nested-accordion:not([open]) .builder-panel-toggle::after {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nested-accordion[open] .builder-panel-toggle {
|
||||||
|
color: #194ea7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nested-accordion-body {
|
||||||
|
padding: 14px 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nested-accordion[open] .nested-accordion-body {
|
||||||
|
animation: builderReveal 0.24s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-panel-summary {
|
||||||
|
list-style: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 14px 16px;
|
||||||
|
transition: background-color 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-panel-summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-panel-summary .builder-panel-head {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-panel-summary:hover {
|
||||||
|
background: rgba(242, 247, 255, 0.72);
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-panel-toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: #5f7089;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-panel-toggle::after {
|
||||||
|
content: "▾";
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1;
|
||||||
|
transition: transform 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-accordion:not([open]) .builder-panel-toggle::after {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-accordion[open] .builder-panel-toggle {
|
||||||
|
color: #194ea7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-accordion[open] .builder-panel-toggle::before {
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-accordion[open] .builder-panel-summary {
|
||||||
|
border-bottom: 1px solid rgba(201, 212, 226, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-panel-body {
|
||||||
|
padding: 14px 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-accordion[open] .builder-panel-body {
|
||||||
|
animation: builderReveal 0.24s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-rule-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(300px, 0.85fr) minmax(0, 1.15fr);
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-stack-layout {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-shell {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-section {
|
||||||
|
border: 1px solid #d7e0ec;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: linear-gradient(180deg, #f8fbff 0%, #ffffff 100%);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-section-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border-bottom: 1px solid #dfe7f1;
|
||||||
|
background: #f2f7ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-section-head h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 15px;
|
||||||
|
color: #142033;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-chip-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 32px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border: 1px solid #d8e1ec;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #304159;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-chip.is-locked {
|
||||||
|
background: #eef2ff;
|
||||||
|
border-color: #c7d2fe;
|
||||||
|
color: #3730a3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-rule-groups {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-rule-group {
|
||||||
|
border: 1px solid #d7e0ec;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: linear-gradient(180deg, #f8fbff 0%, #ffffff 100%);
|
||||||
|
overflow: hidden;
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-rule-group:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
border-color: #bfd0e4;
|
||||||
|
box-shadow: 0 14px 26px rgba(15, 23, 42, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-rule-group-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border-bottom: 1px solid #dfe7f1;
|
||||||
|
background: #f2f7ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-rule-group-head h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 15px;
|
||||||
|
color: #142033;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-rule-list {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-rule-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(220px, 1.4fr) 120px 160px 120px;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border-top: 1px solid #edf2f7;
|
||||||
|
transition: background-color 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-rule-row:first-child {
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-rule-row:hover {
|
||||||
|
background: rgba(246, 250, 255, 0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-rule-main strong {
|
||||||
|
display: block;
|
||||||
|
color: #162133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-rule-control {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
color: #5f7089;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-rule-control input[type='checkbox'] {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-rule-status {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
.columns {
|
.columns {
|
||||||
margin-top: 8px;
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, minmax(220px, 1fr));
|
grid-template-columns: repeat(4, minmax(220px, 1fr));
|
||||||
gap: 10px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.columns.single {
|
.columns.single {
|
||||||
@@ -95,28 +511,47 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.column {
|
.column {
|
||||||
border: 1px solid #d4dce7;
|
border: 1px solid #d7e0ec;
|
||||||
border-radius: 12px;
|
border-radius: 16px;
|
||||||
background: #f9fbff;
|
background: linear-gradient(180deg, #f7faff 0%, #fdfefe 100%);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 460px;
|
min-height: 460px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.column h2 {
|
.column:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
border-color: #bfd0e4;
|
||||||
|
box-shadow: 0 16px 28px rgba(15, 23, 42, 0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 13px 14px;
|
||||||
|
border-bottom: 1px solid #deE7f1;
|
||||||
|
background: linear-gradient(180deg, #eef5ff, #f7fbff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-head h3 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 10px 12px;
|
|
||||||
border-bottom: 1px solid #dbe3ee;
|
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: #0f172a;
|
}
|
||||||
background: #edf3fb;
|
|
||||||
border-radius: 12px 12px 0 0;
|
.column-count {
|
||||||
|
color: #5c6d87;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropzone {
|
.dropzone {
|
||||||
padding: 10px;
|
padding: 12px;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 10px;
|
||||||
align-content: start;
|
align-content: start;
|
||||||
min-height: 140px;
|
min-height: 140px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -127,14 +562,22 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.field-card {
|
.field-card {
|
||||||
background: #ffffff;
|
background: rgba(255, 255, 255, 0.96);
|
||||||
border: 1px solid #d3dbe8;
|
border: 1px solid #d7dfeb;
|
||||||
border-radius: 10px;
|
border-radius: 14px;
|
||||||
padding: 9px 10px;
|
padding: 11px 12px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 8px;
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
cursor: move;
|
cursor: move;
|
||||||
|
transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-card:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
border-color: #b9cadf;
|
||||||
|
box-shadow: 0 12px 20px rgba(15, 23, 42, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
.field-card.dragging {
|
.field-card.dragging {
|
||||||
@@ -145,13 +588,20 @@ body {
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field-name {
|
.field-name {
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
margin-top: 2px;
|
margin-top: 3px;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-main {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badges {
|
.badges {
|
||||||
@@ -159,13 +609,15 @@ body {
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge {
|
.badge {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
border: 1px solid #d1d5db;
|
border: 1px solid #d1d5db;
|
||||||
padding: 2px 7px;
|
padding: 3px 8px;
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
color: #334155;
|
color: #334155;
|
||||||
}
|
}
|
||||||
@@ -189,24 +641,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.options-panel {
|
.options-panel {
|
||||||
margin-top: 16px;
|
padding: 14px;
|
||||||
border: 1px solid #d4dce7;
|
|
||||||
border-radius: 12px;
|
|
||||||
background: #ffffff;
|
|
||||||
padding: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.options-head {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.options-head h2 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-switch {
|
.category-switch {
|
||||||
@@ -215,23 +650,22 @@ body {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-switch select {
|
.category-switch select,
|
||||||
|
.option-table select,
|
||||||
|
.add-option-form input,
|
||||||
|
.option-table input[type='text'] {
|
||||||
border: 1px solid #cbd5e1;
|
border: 1px solid #cbd5e1;
|
||||||
border-radius: 8px;
|
border-radius: 10px;
|
||||||
padding: 6px 8px;
|
padding: 8px 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-option-form {
|
.add-option-form {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(180px, 1fr) minmax(180px, 1fr) minmax(180px, 1fr) auto;
|
grid-template-columns: minmax(180px, 1fr) minmax(180px, 1fr) minmax(180px, 1fr) auto;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 12px;
|
||||||
}
|
|
||||||
|
|
||||||
.add-option-form input {
|
|
||||||
border: 1px solid #cbd5e1;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 8px 9px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.option-table-wrap {
|
.option-table-wrap {
|
||||||
@@ -246,21 +680,22 @@ body {
|
|||||||
.option-table th,
|
.option-table th,
|
||||||
.option-table td {
|
.option-table td {
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
padding: 7px 8px;
|
padding: 8px 9px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
.option-table th {
|
.option-table th {
|
||||||
background: #f8fafc;
|
background: #f8fbff;
|
||||||
|
color: #3d4c63;
|
||||||
}
|
}
|
||||||
|
|
||||||
.option-table input[type='text'] {
|
.option-table-group-row th {
|
||||||
width: 100%;
|
background: #eef5ff;
|
||||||
border: 1px solid #cbd5e1;
|
color: #17335e;
|
||||||
border-radius: 7px;
|
font-size: 12px;
|
||||||
padding: 6px 8px;
|
text-transform: uppercase;
|
||||||
box-sizing: border-box;
|
letter-spacing: 0.04em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.option-row {
|
.option-row {
|
||||||
@@ -279,7 +714,7 @@ body {
|
|||||||
width: 28px;
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
border: 1px solid #cbd5e1;
|
border: 1px solid #cbd5e1;
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
color: #475569;
|
color: #475569;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -288,28 +723,146 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.options-actions {
|
.options-actions {
|
||||||
margin-top: 10px;
|
margin-top: 12px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1120px) {
|
.section-rule-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-rule-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 13px 14px;
|
||||||
|
border: 1px solid #d6e0ec;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: linear-gradient(180deg, #f9fbff, #ffffff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-rule-card.is-locked {
|
||||||
|
background: linear-gradient(180deg, #f4f7fb, #fafcff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-rule-copy {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-rule-copy strong {
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-rule-copy span,
|
||||||
|
.mini {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-rule-toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes builderFadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes builderReveal {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-4px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.builder-hero,
|
||||||
|
.builder-panel,
|
||||||
|
.builder-stat-card,
|
||||||
|
.section-rule-card,
|
||||||
|
.field-card,
|
||||||
|
.options-panel {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-card,
|
||||||
|
.tab,
|
||||||
|
.builder-stat-card,
|
||||||
|
.builder-quicknav a,
|
||||||
|
.builder-panel-summary,
|
||||||
|
.field-rule-group,
|
||||||
|
.field-rule-row,
|
||||||
|
.column {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-accordion[open] .builder-panel-body {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1220px) {
|
||||||
|
.builder-overview {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-rule-layout,
|
||||||
.columns {
|
.columns {
|
||||||
grid-template-columns: repeat(2, minmax(220px, 1fr));
|
grid-template-columns: repeat(2, minmax(220px, 1fr));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.builder-hero,
|
||||||
|
.builder-panel-head,
|
||||||
|
.options-head {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-hero-actions {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-rule-layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 760px) {
|
@media (max-width: 760px) {
|
||||||
|
.builder-overview,
|
||||||
.columns {
|
.columns {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.field-rule-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-rule-status {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
.add-option-form {
|
.add-option-form {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.options-head {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ body {
|
|||||||
.card { background: linear-gradient(180deg, #ffffff, #fbfcff); border: 1px solid #d9dcf3; border-radius: 14px; padding: 18px; margin-bottom: 14px; box-shadow: 0 10px 24px rgba(0, 0, 120, 0.08); }
|
.card { background: linear-gradient(180deg, #ffffff, #fbfcff); border: 1px solid #d9dcf3; border-radius: 14px; padding: 18px; margin-bottom: 14px; box-shadow: 0 10px 24px rgba(0, 0, 120, 0.08); }
|
||||||
.wrap-body .card:last-child { margin-bottom: 0; }
|
.wrap-body .card:last-child { margin-bottom: 0; }
|
||||||
h1 { margin-top: 0; color: #000078; }
|
h1 { margin-top: 0; color: #000078; }
|
||||||
|
h2 { margin: 0; color: #17335e; font-size: 18px; }
|
||||||
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||||||
.field { margin-bottom: 12px; }
|
.field { margin-bottom: 12px; }
|
||||||
.field-full { grid-column: 1 / -1; }
|
.field-full { grid-column: 1 / -1; }
|
||||||
@@ -21,6 +22,11 @@ label { display: block; font-weight: 600; margin-bottom: 6px; }
|
|||||||
input, textarea { width: 100%; min-height: 44px; padding: 9px 11px; box-sizing: border-box; border: 1px solid #d4dbf7; border-radius: 10px; background: #fff; }
|
input, textarea { width: 100%; min-height: 44px; padding: 9px 11px; box-sizing: border-box; border: 1px solid #d4dbf7; border-radius: 10px; background: #fff; }
|
||||||
textarea { min-height: 120px; resize: vertical; }
|
textarea { min-height: 120px; resize: vertical; }
|
||||||
.hint { color: #64748b; font-size: 12px; margin-top: 4px; }
|
.hint { color: #64748b; font-size: 12px; margin-top: 4px; }
|
||||||
|
.offboarding-sections { display: grid; gap: 14px; margin-bottom: 14px; }
|
||||||
|
.offboarding-section-card { border: 1px solid #dbe5f1; border-radius: 14px; background: linear-gradient(180deg, #f9fbff, #ffffff); overflow: hidden; box-shadow: 0 8px 20px rgba(15, 23, 42, 0.05); }
|
||||||
|
.offboarding-section-head { padding: 14px 16px; border-bottom: 1px solid #e2e8f4; background: #f2f7ff; }
|
||||||
|
.offboarding-section-head p { margin: 4px 0 0; color: #5f7089; font-size: 13px; }
|
||||||
|
.offboarding-section-card .grid { padding: 16px; }
|
||||||
.results a { display: inline-block; margin: 4px 8px 4px 0; padding: 6px 8px; border: 1px solid #d4dbf7; border-radius: 6px; text-decoration: none; color: #000078; background: #f7f8ff; }
|
.results a { display: inline-block; margin: 4px 8px 4px 0; padding: 6px 8px; border: 1px solid #d4dbf7; border-radius: 6px; text-decoration: none; color: #000078; background: #f7f8ff; }
|
||||||
.errorlist { color: #b91c1c; margin: 4px 0; }
|
.errorlist { color: #b91c1c; margin: 4px 0; }
|
||||||
.popup-backdrop { position: fixed; inset: 0; background: rgba(15, 23, 42, 0.38); display: none; align-items: center; justify-content: center; z-index: 1000; }
|
.popup-backdrop { position: fixed; inset: 0; background: rgba(15, 23, 42, 0.38); display: none; align-items: center; justify-content: center; z-index: 1000; }
|
||||||
|
|||||||
@@ -10,153 +10,399 @@
|
|||||||
{% block shell_body %}
|
{% block shell_body %}
|
||||||
{% include 'workflows/includes/app_header.html' with header_show_home=1 header_inside_shell=1 %}
|
{% include 'workflows/includes/app_header.html' with header_show_home=1 header_inside_shell=1 %}
|
||||||
|
|
||||||
<header class="header">
|
<section class="builder-hero">
|
||||||
<h1>{% trans "Form Builder" %}</h1>
|
<div class="builder-hero-copy">
|
||||||
<p>{% trans "Felder per Drag-and-Drop sortieren und pro Schritt gruppieren." %}</p>
|
<span class="builder-eyebrow">{% trans "Deployment Configuration" %}</span>
|
||||||
</header>
|
<h1>{% trans "Form Builder" %}</h1>
|
||||||
|
</div>
|
||||||
|
<div class="builder-hero-actions">
|
||||||
|
{% for key, label in form_types %}
|
||||||
|
<a
|
||||||
|
class="tab {% if form_type == key %}active{% endif %}"
|
||||||
|
href="/admin-tools/form-builder/?form_type={{ key }}"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
<button id="save-order" class="btn btn-primary" type="button">{% trans "Reihenfolge speichern" %}</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
{% include 'workflows/includes/messages.html' %}
|
{% include 'workflows/includes/messages.html' %}
|
||||||
|
|
||||||
<div class="toolbar">
|
|
||||||
{% for key, label in form_types %}
|
|
||||||
<a
|
|
||||||
class="tab {% if form_type == key %}active{% endif %}"
|
|
||||||
href="/admin-tools/form-builder/?form_type={{ key }}"
|
|
||||||
>
|
|
||||||
{{ label }}
|
|
||||||
</a>
|
|
||||||
{% endfor %}
|
|
||||||
<button id="save-order" class="btn btn-primary" type="button">{% trans "Reihenfolge speichern" %}</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="status-message" class="status" aria-live="polite"></div>
|
<div id="status-message" class="status" aria-live="polite"></div>
|
||||||
|
|
||||||
<div class="columns {% if form_type == 'offboarding' %}single{% endif %}" id="builder-columns" data-form-type="{{ form_type }}">
|
<nav class="builder-quicknav" aria-label="{% trans 'Bereiche' %}">
|
||||||
{% for column in columns %}
|
<a href="#builder-structure">{% trans "Reihenfolge" %}</a>
|
||||||
<section class="column" data-column-key="{{ column.key }}">
|
<a href="#builder-rules">{% trans "Regeln" %}</a>
|
||||||
<h2>{{ column.title }}</h2>
|
<a href="#builder-content">{% trans "Optionen & Texte" %}</a>
|
||||||
<div class="dropzone" data-column-key="{{ column.key }}">
|
</nav>
|
||||||
{% for item in column.items %}
|
|
||||||
<article class="field-card" draggable="true" data-field-name="{{ item.field_name }}">
|
<section class="builder-overview">
|
||||||
<div class="field-main">
|
<article class="builder-stat-card">
|
||||||
<div class="field-label">{{ item.label }}</div>
|
<span class="builder-stat-label">{% trans "Fixe Kernfelder" %}</span>
|
||||||
<div class="field-name">{{ item.field_name }}</div>
|
<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>
|
||||||
|
{% 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>
|
||||||
<div class="badges">
|
<div class="preview-chip-list">
|
||||||
{% if item.locked %}<span class="badge locked">{% trans "Fix" %}</span>{% endif %}
|
{% for item in section.items %}
|
||||||
{% if not item.is_visible %}<span class="badge hidden">{% trans "Ausgeblendet" %}</span>{% endif %}
|
<span class="preview-chip{% if item.locked %} is-locked{% endif %}">{{ item.label }}</span>
|
||||||
{% if item.is_required %}<span class="badge required">{% trans "Pflicht" %}</span>{% endif %}
|
{% empty %}
|
||||||
|
<span class="mini">{% trans "Keine sichtbaren Felder." %}</span>
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</section>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
{% endfor %}
|
</details>
|
||||||
</div>
|
|
||||||
|
|
||||||
<section class="options-panel">
|
<details class="builder-panel builder-accordion js-single-accordion" data-accordion-group="builder-panels" id="builder-structure" {% if active_panel == 'builder-structure' %}open{% endif %}>
|
||||||
<div class="options-head">
|
<summary class="builder-panel-summary">
|
||||||
<h2>{% trans "Optionen verwalten" %}</h2>
|
<div class="builder-panel-head">
|
||||||
<form class="category-switch" method="get" action="/admin-tools/form-builder/">
|
<div>
|
||||||
<input type="hidden" name="form_type" value="{{ form_type }}" />
|
<h2>{% trans "Struktur & Reihenfolge" %}</h2>
|
||||||
<label for="option_category">{% trans "Kategorie" %}</label>
|
</div>
|
||||||
<select id="option_category" name="option_category" onchange="this.form.submit()">
|
<span class="builder-panel-toggle">{% trans "Geöffnet" %}</span>
|
||||||
{% for value, label in option_categories %}
|
</div>
|
||||||
<option value="{{ value }}" {% if value == selected_option_category %}selected{% endif %}>{{ label }}</option>
|
</summary>
|
||||||
|
<div class="builder-panel-body">
|
||||||
|
|
||||||
|
<div class="columns {% if columns|length == 1 %}single{% endif %}" id="builder-columns" data-form-type="{{ form_type }}">
|
||||||
|
{% for column in columns %}
|
||||||
|
<section class="column" data-column-key="{{ column.key }}">
|
||||||
|
<div class="column-head">
|
||||||
|
<h3>{{ column.title }}</h3>
|
||||||
|
<span class="column-count">{% blocktrans trimmed with count=column.items|length %}{{ count }} Feld/Felder{% endblocktrans %}</span>
|
||||||
|
</div>
|
||||||
|
<div class="dropzone" data-column-key="{{ column.key }}">
|
||||||
|
{% for item in column.items %}
|
||||||
|
<article class="field-card" draggable="true" data-field-name="{{ item.field_name }}">
|
||||||
|
<div class="field-main">
|
||||||
|
<div class="field-label">{{ item.label }}</div>
|
||||||
|
<div class="field-name">{{ item.field_name }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="badges">
|
||||||
|
{% if item.locked %}<span class="badge locked">{% trans "Fix" %}</span>{% endif %}
|
||||||
|
{% if not item.is_visible %}<span class="badge hidden">{% trans "Ausgeblendet" %}</span>{% endif %}
|
||||||
|
{% if item.is_required %}<span class="badge required">{% trans "Pflicht" %}</span>{% endif %}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</div>
|
||||||
</form>
|
</section>
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form class="add-option-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_option" />
|
|
||||||
<input type="hidden" name="category" value="{{ selected_option_category }}" />
|
|
||||||
<input type="text" name="label" placeholder="{% trans 'Label (DE)' %}" required />
|
|
||||||
<input type="text" name="label_en" placeholder="{% trans 'Label (EN, optional)' %}" />
|
|
||||||
<input type="text" name="value" placeholder="{% trans 'Technischer Wert (optional)' %}" />
|
|
||||||
<button class="btn btn-primary" type="submit">{% trans "Option hinzufügen" %}</button>
|
|
||||||
</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 "Sortierung" %}</th>
|
|
||||||
<th>{% trans "Label (DE)" %}</th>
|
|
||||||
<th>{% trans "Label (EN)" %}</th>
|
|
||||||
<th>Value</th>
|
|
||||||
<th>{% trans "Aktiv" %}</th>
|
|
||||||
<th>{% trans "Löschen" %}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="option-table-body">
|
|
||||||
{% for item in option_items %}
|
|
||||||
<tr class="option-row" draggable="true" data-option-row="1">
|
|
||||||
<td>
|
|
||||||
<input type="hidden" name="option_ids" value="{{ item.id }}" />
|
|
||||||
<span class="drag-handle" title="{% trans 'Ziehen zum Sortieren' %}">⋮⋮</span>
|
|
||||||
</td>
|
|
||||||
<td><input type="text" name="label_{{ item.id }}" value="{{ item.label }}" required /></td>
|
|
||||||
<td><input type="text" name="label_en_{{ item.id }}" value="{{ item.label_en }}" /></td>
|
|
||||||
<td><input type="text" name="value_{{ item.id }}" value="{{ item.value }}" /></td>
|
|
||||||
<td><input type="checkbox" name="active_{{ item.id }}" {% if item.is_active %}checked{% endif %} /></td>
|
|
||||||
<td>
|
|
||||||
<button class="btn btn-secondary" type="submit" name="delete_option_id" value="{{ item.id }}" data-confirm="{% trans 'Option wirklich löschen?' %}">{% trans "Löschen" %}</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% empty %}
|
|
||||||
<tr><td colspan="6">{% trans "Keine Optionen in dieser Kategorie." %}</td></tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="options-actions">
|
|
||||||
<button class="btn btn-primary" type="submit" name="builder_action" value="save_options">{% trans "Optionen speichern" %}</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="options-panel">
|
|
||||||
<div class="options-head">
|
|
||||||
<h2>{% trans "Feldtexte verwalten" %}</h2>
|
|
||||||
</div>
|
</div>
|
||||||
<form method="post" action="/admin-tools/form-builder/?form_type={{ form_type }}&option_category={{ selected_option_category }}">
|
</details>
|
||||||
{% csrf_token %}
|
|
||||||
<div class="option-table-wrap">
|
<details class="builder-panel builder-accordion js-single-accordion" data-accordion-group="builder-panels" id="builder-rules" {% if active_panel == 'builder-rules' %}open{% endif %}>
|
||||||
<table class="option-table">
|
<summary class="builder-panel-summary">
|
||||||
<thead>
|
<div class="builder-panel-head">
|
||||||
<tr>
|
<div>
|
||||||
<th>{% trans "Feld" %}</th>
|
<h2>{% trans "Sichtbarkeit & Regeln" %}</h2>
|
||||||
<th>{% trans "Label (DE)" %}</th>
|
</div>
|
||||||
<th>{% trans "Label (EN)" %}</th>
|
<span class="builder-panel-toggle">{% trans "Öffnen" %}</span>
|
||||||
<th>{% trans "Hilfetext (DE)" %}</th>
|
</div>
|
||||||
<th>{% trans "Hilfetext (EN)" %}</th>
|
</summary>
|
||||||
</tr>
|
<div class="builder-panel-body">
|
||||||
</thead>
|
|
||||||
<tbody>
|
<div class="builder-rule-layout">
|
||||||
{% for item in field_text_items %}
|
<section class="options-panel">
|
||||||
<tr>
|
<div class="options-head">
|
||||||
<td>
|
<h2>{% trans "Abschnitte steuern" %}</h2>
|
||||||
<input type="hidden" name="field_ids" value="{{ item.id }}" />
|
</div>
|
||||||
<strong>{{ item.field_name }}</strong>
|
<form method="post" action="/admin-tools/form-builder/?form_type={{ form_type }}&option_category={{ selected_option_category }}">
|
||||||
</td>
|
{% csrf_token %}
|
||||||
<td><input type="text" name="label_override_{{ item.id }}" value="{{ item.label_override }}" placeholder="{% trans 'Fallback: Standardlabel' %}" /></td>
|
<div class="section-rule-grid">
|
||||||
<td><input type="text" name="label_override_en_{{ item.id }}" value="{{ item.label_override_en }}" placeholder="{% trans 'English label' %}" /></td>
|
{% for section in section_rule_items %}
|
||||||
<td><input type="text" name="help_text_override_{{ item.id }}" value="{{ item.help_text_override }}" placeholder="{% trans 'Optionaler Hilfetext' %}" /></td>
|
<label class="section-rule-card{% if section.locked %} is-locked{% endif %}">
|
||||||
<td><input type="text" name="help_text_override_en_{{ item.id }}" value="{{ item.help_text_override_en }}" placeholder="{% trans 'Optional English help text' %}" /></td>
|
<div class="section-rule-copy">
|
||||||
</tr>
|
<strong>{{ section.title }}</strong>
|
||||||
{% empty %}
|
<span>{% blocktrans trimmed with count=section.field_count %}{{ count }} Feld/Felder in diesem Abschnitt.{% endblocktrans %}</span>
|
||||||
<tr><td colspan="5">{% trans "Keine Feldkonfigurationen verfügbar." %}</td></tr>
|
</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 %}
|
||||||
|
/>
|
||||||
|
<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>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</div>
|
||||||
</table>
|
<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>
|
||||||
|
<form method="post" action="/admin-tools/form-builder/?form_type={{ form_type }}&option_category={{ selected_option_category }}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="field-rule-groups">
|
||||||
|
{% for group in field_rule_groups %}
|
||||||
|
<section class="field-rule-group">
|
||||||
|
<div class="field-rule-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="field-rule-list">
|
||||||
|
{% for item in group.items %}
|
||||||
|
<div class="field-rule-row">
|
||||||
|
<div class="field-rule-main">
|
||||||
|
<input type="hidden" name="field_rule_ids" value="{{ item.id }}" />
|
||||||
|
<strong>{{ item.label }}</strong>
|
||||||
|
<div class="mini">{{ item.field_name }}</div>
|
||||||
|
</div>
|
||||||
|
<label class="field-rule-control">
|
||||||
|
<span>{% trans "Sichtbar" %}</span>
|
||||||
|
<input type="checkbox" name="is_visible_{{ item.id }}" {% if item.is_visible %}checked{% endif %} {% if item.locked %}disabled{% endif %}/>
|
||||||
|
</label>
|
||||||
|
<label class="field-rule-control">
|
||||||
|
<span>{% trans "Pflicht" %}</span>
|
||||||
|
<select name="is_required_{{ item.id }}" {% if item.locked %}disabled{% endif %}>
|
||||||
|
<option value="" {% if item.is_required is None %}selected{% endif %}>{% trans "Standard" %}</option>
|
||||||
|
<option value="required" {% if item.is_required is True %}selected{% endif %}>{% trans "Pflicht" %}</option>
|
||||||
|
<option value="optional" {% if item.is_required is False %}selected{% endif %}>{% trans "Optional" %}</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<div class="field-rule-status">
|
||||||
|
{% if item.locked %}
|
||||||
|
<span class="badge locked">{% trans "Fix" %}</span>
|
||||||
|
{% elif not item.is_visible %}
|
||||||
|
<span class="badge hidden">{% trans "Ausgeblendet" %}</span>
|
||||||
|
{% elif item.is_required %}
|
||||||
|
<span class="badge required">{% trans "Pflicht" %}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge">{% trans "Flexibel" %}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<div class="mini">{% trans "Keine Feldregeln verfügbar." %}</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div class="options-actions">
|
||||||
|
<button class="btn btn-primary" type="submit" name="builder_action" value="save_field_rules">{% trans "Feldregeln speichern" %}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details class="builder-panel builder-accordion js-single-accordion" data-accordion-group="builder-panels" id="builder-content" {% if active_panel == 'builder-content' %}open{% endif %}>
|
||||||
|
<summary class="builder-panel-summary">
|
||||||
|
<div class="builder-panel-head">
|
||||||
|
<div>
|
||||||
|
<h2>{% trans "Optionen & Texte" %}</h2>
|
||||||
|
</div>
|
||||||
|
<span class="builder-panel-toggle">{% trans "Öffnen" %}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="options-actions">
|
</summary>
|
||||||
<button class="btn btn-primary" type="submit" name="builder_action" value="save_field_texts">{% trans "Feldtexte speichern" %}</button>
|
<div class="builder-panel-body">
|
||||||
</div>
|
|
||||||
</form>
|
<div class="builder-stack-layout">
|
||||||
</section>
|
<details class="options-panel nested-accordion js-single-accordion" data-accordion-group="builder-content-subpanels" {% if active_subpanel == 'options' %}open{% endif %}>
|
||||||
|
<summary class="nested-accordion-summary">
|
||||||
|
<div class="options-head">
|
||||||
|
<h2>{% trans "Optionen verwalten" %}</h2>
|
||||||
|
<span class="builder-panel-toggle">{% trans "Öffnen" %}</span>
|
||||||
|
</div>
|
||||||
|
</summary>
|
||||||
|
<div class="nested-accordion-body">
|
||||||
|
<div class="options-head options-head-inline">
|
||||||
|
<form class="category-switch" method="get" action="/admin-tools/form-builder/">
|
||||||
|
<input type="hidden" name="form_type" value="{{ form_type }}" />
|
||||||
|
<input type="hidden" name="anchor" value="builder-content" />
|
||||||
|
<input type="hidden" name="panel" value="builder-content" />
|
||||||
|
<input type="hidden" name="subpanel" value="options" />
|
||||||
|
<label for="option_category">{% trans "Kategorie" %}</label>
|
||||||
|
<select id="option_category" name="option_category" onchange="this.form.submit()">
|
||||||
|
{% for value, label in option_categories %}
|
||||||
|
<option value="{{ value }}" {% if value == selected_option_category %}selected{% endif %}>{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form class="add-option-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_option" />
|
||||||
|
<input type="hidden" name="category" value="{{ selected_option_category }}" />
|
||||||
|
<input type="text" name="label" placeholder="{% trans 'Label (DE)' %}" required />
|
||||||
|
<input type="text" name="label_en" placeholder="{% trans 'Label (EN, optional)' %}" />
|
||||||
|
<input type="text" name="value" placeholder="{% trans 'Technischer Wert (optional)' %}" />
|
||||||
|
<button class="btn btn-primary" type="submit">{% trans "Option hinzufügen" %}</button>
|
||||||
|
</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 "Sortierung" %}</th>
|
||||||
|
<th>{% trans "Label (DE)" %}</th>
|
||||||
|
<th>{% trans "Label (EN)" %}</th>
|
||||||
|
<th>Value</th>
|
||||||
|
<th>{% trans "Aktiv" %}</th>
|
||||||
|
<th>{% trans "Löschen" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="option-table-body">
|
||||||
|
{% for item in option_items %}
|
||||||
|
<tr class="option-row" draggable="true" data-option-row="1">
|
||||||
|
<td>
|
||||||
|
<input type="hidden" name="option_ids" value="{{ item.id }}" />
|
||||||
|
<span class="drag-handle" title="{% trans 'Ziehen zum Sortieren' %}">⋮⋮</span>
|
||||||
|
</td>
|
||||||
|
<td><input type="text" name="label_{{ item.id }}" value="{{ item.label }}" required /></td>
|
||||||
|
<td><input type="text" name="label_en_{{ item.id }}" value="{{ item.label_en }}" /></td>
|
||||||
|
<td><input type="text" name="value_{{ item.id }}" value="{{ item.value }}" /></td>
|
||||||
|
<td><input type="checkbox" name="active_{{ item.id }}" {% if item.is_active %}checked{% endif %} /></td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-secondary" type="submit" name="delete_option_id" value="{{ item.id }}" data-confirm="{% trans 'Option wirklich löschen?' %}">{% trans "Löschen" %}</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr><td colspan="6">{% trans "Keine Optionen in dieser Kategorie." %}</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="options-actions">
|
||||||
|
<button class="btn btn-primary" type="submit" name="builder_action" value="save_options">{% trans "Optionen speichern" %}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details class="options-panel nested-accordion js-single-accordion" data-accordion-group="builder-content-subpanels" {% if active_subpanel == 'field-texts' %}open{% endif %}>
|
||||||
|
<summary class="nested-accordion-summary">
|
||||||
|
<div class="options-head">
|
||||||
|
<h2>{% trans "Feldtexte 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="option-table-wrap">
|
||||||
|
<table class="option-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Feld" %}</th>
|
||||||
|
<th>{% trans "Label (DE)" %}</th>
|
||||||
|
<th>{% trans "Label (EN)" %}</th>
|
||||||
|
<th>{% trans "Hilfetext (DE)" %}</th>
|
||||||
|
<th>{% trans "Hilfetext (EN)" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for group in field_text_groups %}
|
||||||
|
<tr class="option-table-group-row">
|
||||||
|
<th colspan="5">{{ group.title }}</th>
|
||||||
|
</tr>
|
||||||
|
{% for item in group.items %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<input type="hidden" name="field_ids" value="{{ item.id }}" />
|
||||||
|
<strong>{{ item.field_name }}</strong>
|
||||||
|
</td>
|
||||||
|
<td><input type="text" name="label_override_{{ item.id }}" value="{{ item.label_override }}" placeholder="{% trans 'Fallback: Standardlabel' %}" /></td>
|
||||||
|
<td><input type="text" name="label_override_en_{{ item.id }}" value="{{ item.label_override_en }}" placeholder="{% trans 'English label' %}" /></td>
|
||||||
|
<td><input type="text" name="help_text_override_{{ item.id }}" value="{{ item.help_text_override }}" placeholder="{% trans 'Optionaler Hilfetext' %}" /></td>
|
||||||
|
<td><input type="text" name="help_text_override_en_{{ item.id }}" value="{{ item.help_text_override_en }}" placeholder="{% trans 'Optional English help text' %}" /></td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr><td colspan="5">{% trans "Keine Feldkonfigurationen verfügbar." %}</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="options-actions">
|
||||||
|
<button class="btn btn-primary" type="submit" name="builder_action" value="save_field_texts">{% trans "Feldtexte speichern" %}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(() => {
|
||||||
|
const accordions = document.querySelectorAll('.js-single-accordion[data-accordion-group]');
|
||||||
|
accordions.forEach((accordion) => {
|
||||||
|
accordion.addEventListener('toggle', () => {
|
||||||
|
if (!accordion.open) return;
|
||||||
|
const group = accordion.dataset.accordionGroup;
|
||||||
|
document.querySelectorAll(`.js-single-accordion[data-accordion-group="${group}"]`).forEach((peer) => {
|
||||||
|
if (peer !== accordion) peer.open = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -49,19 +49,29 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<form method="post" data-email-domain="{{ portal_email_domain }}">
|
<form method="post" data-email-domain="{{ portal_email_domain }}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="grid">
|
<div class="offboarding-sections">
|
||||||
{% for field in form.visible_fields %}
|
{% for section in offboarding_sections %}
|
||||||
{% if field.name != 'search_query' %}
|
<section class="offboarding-section-card">
|
||||||
|
<div class="offboarding-section-head">
|
||||||
|
<div>
|
||||||
|
<h2>{{ section.title }}</h2>
|
||||||
|
<p>{{ section.subtitle }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid">
|
||||||
|
{% for field in section.fields %}
|
||||||
<div class="field {% if field.name == 'notes' %}field-full{% endif %}">
|
<div class="field {% if field.name == 'notes' %}field-full{% endif %}">
|
||||||
{{ field.label_tag }}
|
{{ field.label_tag }}
|
||||||
{{ field }}
|
{{ field }}
|
||||||
{% if field.help_text %}<div class="hint">{{ field.help_text }}</div>{% endif %}
|
{% if field.help_text %}<div class="hint">{{ field.help_text }}</div>{% endif %}
|
||||||
{{ field.errors }}
|
{{ field.errors }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary" type="submit">{% trans "Offboarding-Anfrage speichern" %}</button>
|
<button class="btn btn-primary" type="submit">{% trans "Offboarding-Anfrage speichern" %}</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import json
|
|||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from workflows.models import FormFieldConfig, FormOption
|
from workflows.models import FormFieldConfig, FormOption, FormSectionConfig
|
||||||
|
|
||||||
|
|
||||||
class FormBuilderAdminTests(TestCase):
|
class FormBuilderAdminTests(TestCase):
|
||||||
@@ -94,3 +94,81 @@ class FormBuilderAdminTests(TestCase):
|
|||||||
|
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertTrue(FormOption.objects.filter(category='device', label='Tablet').exists())
|
self.assertTrue(FormOption.objects.filter(category='device', label='Tablet').exists())
|
||||||
|
|
||||||
|
def test_staff_can_save_field_rules(self):
|
||||||
|
self.client.force_login(self.staff)
|
||||||
|
self.client.get('/admin-tools/form-builder/?form_type=onboarding', HTTP_HOST='localhost')
|
||||||
|
department = FormFieldConfig.objects.get(form_type='onboarding', field_name='department')
|
||||||
|
contract_start = FormFieldConfig.objects.get(form_type='onboarding', field_name='contract_start')
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
'/admin-tools/form-builder/?form_type=onboarding&option_category=device',
|
||||||
|
data={
|
||||||
|
'builder_action': 'save_field_rules',
|
||||||
|
'field_rule_ids': [str(department.id), str(contract_start.id)],
|
||||||
|
f'is_required_{department.id}': 'required',
|
||||||
|
f'is_visible_{contract_start.id}': 'on',
|
||||||
|
},
|
||||||
|
HTTP_HOST='localhost',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
department.refresh_from_db()
|
||||||
|
contract_start.refresh_from_db()
|
||||||
|
self.assertEqual(department.is_required, True)
|
||||||
|
self.assertEqual(contract_start.is_required, None)
|
||||||
|
|
||||||
|
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')
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
'/admin-tools/form-builder/?form_type=onboarding&option_category=device',
|
||||||
|
data={
|
||||||
|
'builder_action': 'save_section_rules',
|
||||||
|
},
|
||||||
|
HTTP_HOST='localhost',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
itsetup = FormSectionConfig.objects.get(form_type='onboarding', section_key='itsetup')
|
||||||
|
stammdaten = FormSectionConfig.objects.get(form_type='onboarding', section_key='stammdaten')
|
||||||
|
self.assertEqual(itsetup.is_visible, False)
|
||||||
|
self.assertEqual(stammdaten.is_visible, True)
|
||||||
|
|
||||||
|
def test_apply_onboarding_lean_preset_updates_section_and_field_rules(self):
|
||||||
|
self.client.force_login(self.staff)
|
||||||
|
response = self.client.post(
|
||||||
|
'/admin-tools/form-builder/?form_type=onboarding&option_category=device',
|
||||||
|
data={
|
||||||
|
'builder_action': 'apply_preset',
|
||||||
|
'preset_key': 'lean',
|
||||||
|
},
|
||||||
|
HTTP_HOST='localhost',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
itsetup = FormSectionConfig.objects.get(form_type='onboarding', section_key='itsetup')
|
||||||
|
gender = FormFieldConfig.objects.get(form_type='onboarding', field_name='gender')
|
||||||
|
contract_start = FormFieldConfig.objects.get(form_type='onboarding', field_name='contract_start')
|
||||||
|
self.assertEqual(itsetup.is_visible, False)
|
||||||
|
self.assertEqual(gender.is_visible, False)
|
||||||
|
self.assertEqual(contract_start.is_required, None)
|
||||||
|
|
||||||
|
def test_apply_offboarding_hr_heavy_preset_updates_fields(self):
|
||||||
|
self.client.force_login(self.staff)
|
||||||
|
response = self.client.post(
|
||||||
|
'/admin-tools/form-builder/?form_type=offboarding&option_category=device',
|
||||||
|
data={
|
||||||
|
'builder_action': 'apply_preset',
|
||||||
|
'preset_key': 'hr_heavy',
|
||||||
|
},
|
||||||
|
HTTP_HOST='localhost',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
notes = FormFieldConfig.objects.get(form_type='offboarding', field_name='notes')
|
||||||
|
department = FormFieldConfig.objects.get(form_type='offboarding', field_name='department')
|
||||||
|
self.assertEqual(notes.is_visible, True)
|
||||||
|
self.assertEqual(notes.is_required, True)
|
||||||
|
self.assertEqual(department.is_required, True)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from django.contrib.auth import get_user_model
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from workflows.branding import get_company_email_domain
|
from workflows.branding import get_company_email_domain
|
||||||
from workflows.models import FormFieldConfig, OnboardingRequest
|
from workflows.models import FormFieldConfig, FormSectionConfig, OnboardingRequest
|
||||||
|
|
||||||
|
|
||||||
class OnboardingFlowTests(TestCase):
|
class OnboardingFlowTests(TestCase):
|
||||||
@@ -112,3 +112,34 @@ class OnboardingFlowTests(TestCase):
|
|||||||
self.assertContains(response, 'Dieses Feld ist zwingend erforderlich.')
|
self.assertContains(response, 'Dieses Feld ist zwingend erforderlich.')
|
||||||
self.assertFalse(OnboardingRequest.objects.filter(work_email=f'lina.leer@{self.company_domain}').exists())
|
self.assertFalse(OnboardingRequest.objects.filter(work_email=f'lina.leer@{self.company_domain}').exists())
|
||||||
mock_delay.assert_not_called()
|
mock_delay.assert_not_called()
|
||||||
|
|
||||||
|
@patch('workflows.views.process_onboarding_request.delay')
|
||||||
|
def test_hidden_itsetup_section_is_removed_from_form_and_submission(self, mock_delay):
|
||||||
|
FormSectionConfig.objects.update_or_create(
|
||||||
|
form_type='onboarding',
|
||||||
|
section_key='itsetup',
|
||||||
|
defaults={'is_visible': False},
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.get('/onboarding/new/', HTTP_HOST='localhost')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertNotContains(response, '3. IT-Setup')
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
'first_name': 'Nora',
|
||||||
|
'last_name': 'Neutral',
|
||||||
|
'gender': 'frau',
|
||||||
|
'job_title': 'Consultant',
|
||||||
|
'department': 'IT-Service',
|
||||||
|
'work_email': f'nora.section@{self.company_domain}',
|
||||||
|
'contract_start': '2026-11-01',
|
||||||
|
'employment_type': 'unbefristet',
|
||||||
|
'group_mailboxes_required_choice': 'nein',
|
||||||
|
'agreement_confirm': 'on',
|
||||||
|
}
|
||||||
|
|
||||||
|
submit_response = self.client.post('/onboarding/new/', payload, HTTP_HOST='localhost')
|
||||||
|
|
||||||
|
self.assertEqual(submit_response.status_code, 302)
|
||||||
|
self.assertTrue(OnboardingRequest.objects.filter(work_email=f'nora.section@{self.company_domain}').exists())
|
||||||
|
mock_delay.assert_called_once()
|
||||||
|
|||||||
@@ -37,13 +37,22 @@ from .branding import get_branding_email_copy, get_company_email_domain, get_def
|
|||||||
from .forms import AccountAvatarForm, AccountDetailsForm, AccountNotificationPreferencesForm, AccountTOTPDisableForm, AccountTOTPEnableForm, AccountTOTPRegenerateRecoveryCodesForm, AppLoginForm, AppTOTPChallengeForm, OffboardingRequestForm, OnboardingRequestForm, PortalBrandingForm, PortalCompanyConfigForm, PortalTrialConfigForm, UserManagementCreateForm
|
from .forms import AccountAvatarForm, AccountDetailsForm, AccountNotificationPreferencesForm, AccountTOTPDisableForm, AccountTOTPEnableForm, AccountTOTPRegenerateRecoveryCodesForm, AppLoginForm, AppTOTPChallengeForm, OffboardingRequestForm, OnboardingRequestForm, PortalBrandingForm, PortalCompanyConfigForm, PortalTrialConfigForm, UserManagementCreateForm
|
||||||
from .form_builder import (
|
from .form_builder import (
|
||||||
DEFAULT_FIELD_ORDER,
|
DEFAULT_FIELD_ORDER,
|
||||||
|
FORM_PRESETS,
|
||||||
LOCKED_FIELD_RULES,
|
LOCKED_FIELD_RULES,
|
||||||
|
LOCKED_SECTION_RULES,
|
||||||
|
OFFBOARDING_PAGE_LABELS,
|
||||||
|
OFFBOARDING_PAGE_ORDER,
|
||||||
ONBOARDING_DEFAULT_PAGE,
|
ONBOARDING_DEFAULT_PAGE,
|
||||||
ONBOARDING_PAGE_LABELS,
|
ONBOARDING_PAGE_LABELS,
|
||||||
ONBOARDING_PAGE_ORDER,
|
ONBOARDING_PAGE_ORDER,
|
||||||
ensure_form_field_configs,
|
ensure_form_field_configs,
|
||||||
|
ensure_form_section_configs,
|
||||||
|
get_default_page_map,
|
||||||
|
get_section_labels,
|
||||||
|
get_section_order,
|
||||||
|
apply_form_preset,
|
||||||
)
|
)
|
||||||
from .models import AdminAuditLog, AsyncTaskLog, EmployeeProfile, FormFieldConfig, FormOption, IntroChecklistItem, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, PortalAppConfig, PortalBranding, PortalCompanyConfig, PortalTrialConfig, ScheduledWelcomeEmail, SystemEmailConfig, UserNotification, UserProfile, WorkflowConfig
|
from .models import AdminAuditLog, AsyncTaskLog, EmployeeProfile, FormFieldConfig, FormOption, FormSectionConfig, IntroChecklistItem, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, PortalAppConfig, PortalBranding, PortalCompanyConfig, PortalTrialConfig, ScheduledWelcomeEmail, SystemEmailConfig, UserNotification, UserProfile, WorkflowConfig
|
||||||
from .emailing import send_system_email
|
from .emailing import send_system_email
|
||||||
from .notifications import notify_user
|
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
|
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
|
||||||
@@ -531,13 +540,14 @@ def _section_for_block(block: dict, field_pages: dict[str, str]) -> str:
|
|||||||
return field_pages.get(fields[0].name, 'abschluss')
|
return field_pages.get(fields[0].name, 'abschluss')
|
||||||
|
|
||||||
|
|
||||||
def _build_onboarding_sections(blocks: list[dict], field_pages: dict[str, str]) -> list[dict]:
|
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}
|
grouped = {key: [] for key in ONBOARDING_SECTION_ORDER}
|
||||||
for block in blocks:
|
for block in blocks:
|
||||||
section_key = _section_for_block(block, field_pages)
|
section_key = _section_for_block(block, field_pages)
|
||||||
if section_key not in grouped:
|
if section_key not in grouped:
|
||||||
section_key = 'abschluss'
|
section_key = 'abschluss'
|
||||||
grouped[section_key].append(block)
|
grouped[section_key].append(block)
|
||||||
|
visible_keys = visible_section_keys or set(ONBOARDING_SECTION_ORDER)
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
'key': key,
|
'key': key,
|
||||||
@@ -546,6 +556,35 @@ def _build_onboarding_sections(blocks: list[dict], field_pages: dict[str, str])
|
|||||||
'blocks': grouped[key],
|
'blocks': grouped[key],
|
||||||
}
|
}
|
||||||
for key in ONBOARDING_SECTION_ORDER
|
for key in ONBOARDING_SECTION_ORDER
|
||||||
|
if key in visible_keys
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
OFFBOARDING_SECTION_META = {
|
||||||
|
'mitarbeitende': {'title': gettext_lazy('Mitarbeitende'), 'subtitle': gettext_lazy('Person, Rolle und Bereich')},
|
||||||
|
'austritt': {'title': gettext_lazy('Austritt'), 'subtitle': gettext_lazy('Letzter Arbeitstag')},
|
||||||
|
'abschluss': {'title': gettext_lazy('Abschluss'), 'subtitle': gettext_lazy('Hinweise und Abschlussnotizen')},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _build_offboarding_sections(form, visible_section_keys: set[str] | None = None) -> list[dict]:
|
||||||
|
field_pages = getattr(form, '_field_page_keys', {})
|
||||||
|
grouped = {key: [] for key in OFFBOARDING_PAGE_ORDER}
|
||||||
|
for field_name in form.fields.keys():
|
||||||
|
section_key = field_pages.get(field_name, 'abschluss')
|
||||||
|
if section_key not in grouped:
|
||||||
|
section_key = 'abschluss'
|
||||||
|
grouped[section_key].append(form[field_name])
|
||||||
|
visible_keys = visible_section_keys or set(OFFBOARDING_PAGE_ORDER)
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'key': key,
|
||||||
|
'title': OFFBOARDING_SECTION_META[key]['title'],
|
||||||
|
'subtitle': OFFBOARDING_SECTION_META[key]['subtitle'],
|
||||||
|
'fields': grouped[key],
|
||||||
|
}
|
||||||
|
for key in OFFBOARDING_PAGE_ORDER
|
||||||
|
if key in visible_keys and grouped[key]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -1780,7 +1819,12 @@ def onboarding_create(request):
|
|||||||
|
|
||||||
onboarding_blocks = _build_onboarding_layout(form)
|
onboarding_blocks = _build_onboarding_layout(form)
|
||||||
field_pages = getattr(form, '_field_page_keys', {})
|
field_pages = getattr(form, '_field_page_keys', {})
|
||||||
onboarding_sections = _build_onboarding_sections(onboarding_blocks, field_pages)
|
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
|
||||||
|
}
|
||||||
|
onboarding_sections = _build_onboarding_sections(onboarding_blocks, field_pages, visible_section_keys=visible_section_keys)
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
@@ -1969,6 +2013,14 @@ def offboarding_create(request):
|
|||||||
else:
|
else:
|
||||||
form = OffboardingRequestForm(prefill_profile=selected_profile, initial={'search_query': search_query})
|
form = OffboardingRequestForm(prefill_profile=selected_profile, initial={'search_query': search_query})
|
||||||
|
|
||||||
|
field_pages = getattr(form, '_field_page_keys', {})
|
||||||
|
section_configs = ensure_form_section_configs('offboarding')
|
||||||
|
visible_section_keys = {
|
||||||
|
key for key in OFFBOARDING_PAGE_ORDER
|
||||||
|
if key in LOCKED_SECTION_RULES.get('offboarding', set()) or section_configs.get(key, None) is None or section_configs[key].is_visible
|
||||||
|
}
|
||||||
|
offboarding_sections = _build_offboarding_sections(form, visible_section_keys=visible_section_keys)
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
'workflows/offboarding_form.html',
|
'workflows/offboarding_form.html',
|
||||||
@@ -1980,6 +2032,7 @@ def offboarding_create(request):
|
|||||||
'saved': request.GET.get('saved') == '1',
|
'saved': request.GET.get('saved') == '1',
|
||||||
'saved_request_id': request.GET.get('id', ''),
|
'saved_request_id': request.GET.get('id', ''),
|
||||||
'portal_email_domain': get_company_email_domain(),
|
'portal_email_domain': get_company_email_domain(),
|
||||||
|
'offboarding_sections': offboarding_sections,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1997,6 +2050,9 @@ def offboarding_success(request, request_id: int):
|
|||||||
def form_builder_page(request):
|
def form_builder_page(request):
|
||||||
language_code = get_language()
|
language_code = get_language()
|
||||||
form_type = request.GET.get('form_type', 'onboarding')
|
form_type = request.GET.get('form_type', 'onboarding')
|
||||||
|
anchor = (request.GET.get('anchor') or '').strip()
|
||||||
|
active_panel = (request.GET.get('panel') or '').strip()
|
||||||
|
active_subpanel = (request.GET.get('subpanel') or '').strip()
|
||||||
if form_type not in DEFAULT_FIELD_ORDER:
|
if form_type not in DEFAULT_FIELD_ORDER:
|
||||||
form_type = 'onboarding'
|
form_type = 'onboarding'
|
||||||
option_category = request.GET.get('option_category', 'department')
|
option_category = request.GET.get('option_category', 'department')
|
||||||
@@ -2017,7 +2073,7 @@ def form_builder_page(request):
|
|||||||
option.delete()
|
option.delete()
|
||||||
_audit(request, 'form_option_deleted', target_type='form_option', target_id=deleted_id, target_label=deleted_label)
|
_audit(request, 'form_option_deleted', target_type='form_option', target_id=deleted_id, target_label=deleted_label)
|
||||||
messages.success(request, 'Option wurde gelöscht.')
|
messages.success(request, 'Option wurde gelöscht.')
|
||||||
return redirect(f"/admin-tools/form-builder/?form_type={form_type}&option_category={option_category}")
|
return redirect(f"/admin-tools/form-builder/?form_type={form_type}&option_category={option_category}&panel=builder-content&subpanel=options#builder-content")
|
||||||
|
|
||||||
action = request.POST.get('builder_action', '')
|
action = request.POST.get('builder_action', '')
|
||||||
if action == 'add_option':
|
if action == 'add_option':
|
||||||
@@ -2068,7 +2124,7 @@ def form_builder_page(request):
|
|||||||
option.save(update_fields=['label', 'label_en', 'value', 'is_active', 'sort_order'])
|
option.save(update_fields=['label', 'label_en', 'value', 'is_active', 'sort_order'])
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
messages.error(request, f'Doppelte Bezeichnung in Kategorie: {next_label}')
|
messages.error(request, f'Doppelte Bezeichnung in Kategorie: {next_label}')
|
||||||
return redirect(f"/admin-tools/form-builder/?form_type={form_type}&option_category={option.category}")
|
return redirect(f"/admin-tools/form-builder/?form_type={form_type}&option_category={option.category}&panel=builder-content&subpanel=options#builder-content")
|
||||||
option_category = option.category
|
option_category = option.category
|
||||||
_audit(request, 'form_options_saved', target_type='form_option', target_label=option_category, details={'count': len(option_ids)})
|
_audit(request, 'form_options_saved', target_type='form_option', target_label=option_category, details={'count': len(option_ids)})
|
||||||
messages.success(request, 'Optionen wurden gespeichert.')
|
messages.success(request, 'Optionen wurden gespeichert.')
|
||||||
@@ -2087,7 +2143,67 @@ def form_builder_page(request):
|
|||||||
_audit(request, 'form_field_texts_saved', target_type='form_config', target_label=form_type, details={'count': len(field_ids)})
|
_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.')
|
messages.success(request, 'Feldtexte wurden gespeichert.')
|
||||||
|
|
||||||
return redirect(f"/admin-tools/form-builder/?form_type={form_type}&option_category={option_category}")
|
elif action == 'save_field_rules':
|
||||||
|
field_ids = request.POST.getlist('field_rule_ids')
|
||||||
|
locked_fields = LOCKED_FIELD_RULES.get(form_type, set())
|
||||||
|
updated = 0
|
||||||
|
for raw_id in field_ids:
|
||||||
|
cfg = FormFieldConfig.objects.filter(id=raw_id, form_type=form_type).first()
|
||||||
|
if not cfg:
|
||||||
|
continue
|
||||||
|
if cfg.field_name in locked_fields:
|
||||||
|
cfg.is_visible = True
|
||||||
|
cfg.is_required = None
|
||||||
|
else:
|
||||||
|
cfg.is_visible = request.POST.get(f'is_visible_{cfg.id}') == 'on'
|
||||||
|
required_mode = (request.POST.get(f'is_required_{cfg.id}') or '').strip()
|
||||||
|
cfg.is_required = True if required_mode == 'required' else False if required_mode == 'optional' else None
|
||||||
|
cfg.save(update_fields=['is_visible', 'is_required'])
|
||||||
|
updated += 1
|
||||||
|
_audit(request, 'form_field_rules_saved', target_type='form_config', target_label=form_type, details={'count': updated})
|
||||||
|
messages.success(request, 'Feldregeln wurden gespeichert.')
|
||||||
|
|
||||||
|
elif action == 'save_section_rules' and form_type in {'onboarding', 'offboarding'}:
|
||||||
|
section_configs = ensure_form_section_configs(form_type)
|
||||||
|
locked_sections = LOCKED_SECTION_RULES.get(form_type, set())
|
||||||
|
updated = 0
|
||||||
|
for section_key, cfg in section_configs.items():
|
||||||
|
if section_key in locked_sections:
|
||||||
|
if not cfg.is_visible:
|
||||||
|
cfg.is_visible = True
|
||||||
|
cfg.save(update_fields=['is_visible'])
|
||||||
|
continue
|
||||||
|
cfg.is_visible = request.POST.get(f'section_visible_{section_key}') == 'on'
|
||||||
|
cfg.save(update_fields=['is_visible'])
|
||||||
|
updated += 1
|
||||||
|
_audit(request, 'form_section_rules_saved', target_type='form_config', target_label=form_type, details={'count': updated})
|
||||||
|
messages.success(request, 'Abschnittsregeln wurden gespeichert.')
|
||||||
|
|
||||||
|
elif action == 'apply_preset':
|
||||||
|
preset_key = (request.POST.get('preset_key') or '').strip()
|
||||||
|
if apply_form_preset(form_type, preset_key):
|
||||||
|
active_panel = 'builder-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'}:
|
||||||
|
active_panel = 'builder-content'
|
||||||
|
if action in {'add_option', 'save_options'}:
|
||||||
|
active_subpanel = 'options'
|
||||||
|
elif action == 'save_field_texts':
|
||||||
|
active_subpanel = 'field-texts'
|
||||||
|
elif action in {'save_field_rules', 'save_section_rules'}:
|
||||||
|
active_panel = 'builder-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 anchor == 'builder-content' or active_panel == 'builder-content':
|
||||||
|
redirect_target += "#builder-content"
|
||||||
|
return redirect(redirect_target)
|
||||||
|
|
||||||
default_names = list(DEFAULT_FIELD_ORDER.get(form_type, []))
|
default_names = list(DEFAULT_FIELD_ORDER.get(form_type, []))
|
||||||
existing_names = list(
|
existing_names = list(
|
||||||
@@ -2100,12 +2216,17 @@ def form_builder_page(request):
|
|||||||
default_names.append(name)
|
default_names.append(name)
|
||||||
|
|
||||||
ensure_form_field_configs(form_type, default_names)
|
ensure_form_field_configs(form_type, default_names)
|
||||||
|
section_configs = ensure_form_section_configs(form_type)
|
||||||
|
section_order = get_section_order(form_type)
|
||||||
|
section_labels = get_section_labels(form_type)
|
||||||
|
default_page_map = get_default_page_map(form_type)
|
||||||
|
|
||||||
configs = list(
|
configs = list(
|
||||||
FormFieldConfig.objects.filter(form_type=form_type).order_by('sort_order', 'field_name')
|
FormFieldConfig.objects.filter(form_type=form_type).order_by('sort_order', 'field_name')
|
||||||
)
|
)
|
||||||
labels = _form_field_labels(form_type)
|
labels = _form_field_labels(form_type)
|
||||||
locked = LOCKED_FIELD_RULES.get(form_type, set())
|
locked = LOCKED_FIELD_RULES.get(form_type, set())
|
||||||
|
locked_sections = LOCKED_SECTION_RULES.get(form_type, set())
|
||||||
|
|
||||||
if form_type == 'onboarding':
|
if form_type == 'onboarding':
|
||||||
columns = [
|
columns = [
|
||||||
@@ -2131,27 +2252,127 @@ def form_builder_page(request):
|
|||||||
'is_visible': cfg.is_visible,
|
'is_visible': cfg.is_visible,
|
||||||
'is_required': cfg.is_required,
|
'is_required': cfg.is_required,
|
||||||
'locked': cfg.field_name in locked,
|
'locked': cfg.field_name in locked,
|
||||||
|
'page_key': page_key,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
columns = [
|
columns = [
|
||||||
{
|
{
|
||||||
'key': 'all',
|
'key': key,
|
||||||
'title': 'Offboarding Felder',
|
'title': section_labels.get(key, key),
|
||||||
'items': [
|
'items': [],
|
||||||
{
|
|
||||||
'field_name': cfg.field_name,
|
|
||||||
'label': cfg.translated_label_override(language_code) or labels.get(cfg.field_name, cfg.field_name),
|
|
||||||
'label_de': cfg.label_override or labels.get(cfg.field_name, cfg.field_name),
|
|
||||||
'label_en': cfg.label_override_en,
|
|
||||||
'is_visible': cfg.is_visible,
|
|
||||||
'is_required': cfg.is_required,
|
|
||||||
'locked': cfg.field_name in locked,
|
|
||||||
}
|
|
||||||
for cfg in configs
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
|
for key in section_order
|
||||||
]
|
]
|
||||||
|
column_by_key = {c['key']: c for c in columns}
|
||||||
|
fallback = section_order[-1] if section_order else 'all'
|
||||||
|
for cfg in configs:
|
||||||
|
page_key = cfg.page_key or default_page_map.get(cfg.field_name, fallback)
|
||||||
|
if page_key not in column_by_key:
|
||||||
|
page_key = fallback
|
||||||
|
column_by_key[page_key]['items'].append(
|
||||||
|
{
|
||||||
|
'field_name': cfg.field_name,
|
||||||
|
'label': cfg.translated_label_override(language_code) or labels.get(cfg.field_name, cfg.field_name),
|
||||||
|
'label_de': cfg.label_override or labels.get(cfg.field_name, cfg.field_name),
|
||||||
|
'label_en': cfg.label_override_en,
|
||||||
|
'is_visible': cfg.is_visible,
|
||||||
|
'is_required': cfg.is_required,
|
||||||
|
'locked': cfg.field_name in locked,
|
||||||
|
'page_key': page_key,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
section_rule_items = []
|
||||||
|
if section_order:
|
||||||
|
fallback_section = section_order[-1] if section_order else ''
|
||||||
|
for key in section_order:
|
||||||
|
cfg = section_configs.get(key)
|
||||||
|
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]),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
field_rule_items = []
|
||||||
|
for cfg in configs:
|
||||||
|
page_key = cfg.page_key or default_page_map.get(cfg.field_name, section_order[-1] if section_order else '')
|
||||||
|
field_rule_items.append(
|
||||||
|
{
|
||||||
|
'id': cfg.id,
|
||||||
|
'field_name': cfg.field_name,
|
||||||
|
'label': cfg.translated_label_override(language_code) or labels.get(cfg.field_name, cfg.field_name),
|
||||||
|
'page_key': page_key,
|
||||||
|
'page_label': section_labels.get(page_key, page_key) if section_order else '',
|
||||||
|
'is_visible': cfg.is_visible,
|
||||||
|
'is_required': cfg.is_required,
|
||||||
|
'locked': cfg.field_name in locked,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
field_rule_groups = []
|
||||||
|
if section_order:
|
||||||
|
grouped_rules = {key: [] for key in section_order}
|
||||||
|
for item in field_rule_items:
|
||||||
|
grouped_rules.setdefault(item['page_key'], []).append(item)
|
||||||
|
for key in section_order:
|
||||||
|
field_rule_groups.append(
|
||||||
|
{
|
||||||
|
'key': key,
|
||||||
|
'title': section_labels.get(key, key),
|
||||||
|
'items': grouped_rules.get(key, []),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
field_text_groups = []
|
||||||
|
if section_order:
|
||||||
|
grouped_texts = {key: [] for key in section_order}
|
||||||
|
for cfg in configs:
|
||||||
|
page_key = cfg.page_key or default_page_map.get(cfg.field_name, section_order[-1] if section_order else '')
|
||||||
|
grouped_texts.setdefault(page_key, []).append(cfg)
|
||||||
|
for key in section_order:
|
||||||
|
field_text_groups.append(
|
||||||
|
{
|
||||||
|
'key': key,
|
||||||
|
'title': section_labels.get(key, key),
|
||||||
|
'items': grouped_texts.get(key, []),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
preview_sections = []
|
||||||
|
if section_order:
|
||||||
|
field_rule_group_map = {group['key']: group['items'] for group in field_rule_groups}
|
||||||
|
for key in section_order:
|
||||||
|
section_cfg = section_configs.get(key)
|
||||||
|
section_locked = key in locked_sections
|
||||||
|
section_visible = 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']
|
||||||
|
]
|
||||||
|
if section_visible:
|
||||||
|
preview_sections.append(
|
||||||
|
{
|
||||||
|
'key': key,
|
||||||
|
'title': section_labels.get(key, key),
|
||||||
|
'items': visible_items,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
locked_field_count = len([item for item in field_rule_items if item['locked']])
|
||||||
|
hidden_field_count = len([item for item in field_rule_items if not item['is_visible']])
|
||||||
|
configurable_field_count = len(field_rule_items) - locked_field_count
|
||||||
|
hidden_section_count = len([item for item in section_rule_items if not item['is_visible']]) if section_rule_items else 0
|
||||||
|
builder_summary = {
|
||||||
|
'locked_field_count': locked_field_count,
|
||||||
|
'configurable_field_count': configurable_field_count,
|
||||||
|
'hidden_field_count': hidden_field_count,
|
||||||
|
'hidden_section_count': hidden_section_count,
|
||||||
|
}
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
@@ -2164,6 +2385,15 @@ def form_builder_page(request):
|
|||||||
'selected_option_category': option_category,
|
'selected_option_category': option_category,
|
||||||
'option_items': FormOption.objects.filter(category=option_category).order_by('sort_order', 'label'),
|
'option_items': FormOption.objects.filter(category=option_category).order_by('sort_order', 'label'),
|
||||||
'field_text_items': configs,
|
'field_text_items': configs,
|
||||||
|
'field_rule_items': field_rule_items,
|
||||||
|
'field_rule_groups': field_rule_groups,
|
||||||
|
'field_text_groups': field_text_groups,
|
||||||
|
'preview_sections': preview_sections,
|
||||||
|
'section_rule_items': section_rule_items,
|
||||||
|
'builder_summary': builder_summary,
|
||||||
|
'active_panel': active_panel,
|
||||||
|
'active_subpanel': active_subpanel,
|
||||||
|
'available_presets': FORM_PRESETS.get(form_type, {}),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user