snapshot: refine builder field rules and conditional summaries

This commit is contained in:
Md Bayazid Bostame
2026-03-27 23:03:17 +01:00
parent e84ddd558b
commit 8e950de994
5 changed files with 1405 additions and 553 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -116,6 +116,34 @@ body {
gap: 12px;
}
.builder-sidebar-context {
gap: 12px;
}
.builder-context-stack {
display: grid;
gap: 10px;
}
.builder-context-row {
display: grid;
gap: 3px;
padding-top: 2px;
}
.builder-context-label {
color: #607086;
font-size: 11px;
font-weight: 800;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.builder-context-row strong {
color: #142033;
font-size: 14px;
}
.builder-side-stat {
display: grid;
gap: 2px;
@@ -147,7 +175,7 @@ body {
align-items: flex-end;
justify-content: space-between;
gap: 20px;
padding: 8px 0 6px;
padding: 4px 0 6px;
}
.builder-hero-copy {
@@ -155,10 +183,10 @@ body {
}
.builder-hero-sub {
margin: 10px 0 0;
max-width: 640px;
margin: 8px 0 0;
max-width: 620px;
color: #5c6d87;
font-size: 15px;
font-size: 14px;
line-height: 1.6;
}
@@ -191,6 +219,88 @@ body {
justify-content: flex-end;
}
.builder-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
margin: 10px 0 14px;
padding: 12px 14px;
border: 1px solid #d7e0ec;
border-radius: 16px;
background: linear-gradient(180deg, #fbfdff, #f7fbff);
box-shadow: 0 10px 22px rgba(15, 23, 42, 0.04);
}
.builder-toolbar-main {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.builder-toolbar-chip {
display: inline-grid;
gap: 1px;
min-height: 40px;
padding: 7px 12px;
border: 1px solid #dbe5f1;
border-radius: 14px;
background: #fff;
}
.builder-toolbar-chip-label {
color: #607086;
font-size: 10px;
font-weight: 800;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.builder-toolbar-chip strong {
font-size: 13px;
color: #142033;
}
.builder-toolbar-note {
color: #5f7089;
font-size: 12px;
line-height: 1.5;
max-width: 360px;
text-align: right;
}
.builder-lang-switch {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px;
border: 1px solid #c6d1e1;
border-radius: 999px;
background: #f8fbff;
}
.builder-lang-btn {
min-width: 38px;
min-height: 34px;
border: 0;
border-radius: 999px;
background: transparent;
color: #304159;
font-size: 12px;
font-weight: 800;
cursor: pointer;
transition: background-color 0.16s ease, color 0.16s ease, transform 0.16s ease;
}
.builder-lang-btn:hover {
transform: translateY(-1px);
}
.builder-lang-btn.active {
background: linear-gradient(135deg, #0f3b7a 0%, #1759b8 100%);
color: #fff;
}
.tab {
border: 1px solid #c6d1e1;
border-radius: 999px;
@@ -575,9 +685,9 @@ body {
.field-rule-row {
display: grid;
grid-template-columns: minmax(240px, 1.5fr) minmax(120px, 0.55fr) minmax(170px, 0.7fr) auto;
gap: 14px;
gap: 12px;
align-items: center;
padding: 14px;
padding: 12px;
border: 1px solid #e7edf6;
border-radius: 14px;
background: rgba(255, 255, 255, 0.96);
@@ -598,8 +708,15 @@ body {
font-size: 14px;
}
.field-rule-summary {
margin-top: 4px;
color: #526379;
font-size: 12px;
line-height: 1.5;
}
.field-rule-meta {
margin-top: 8px;
margin-top: 6px;
display: flex;
align-items: center;
justify-content: space-between;
@@ -619,7 +736,7 @@ body {
display: flex;
align-items: end;
justify-content: flex-end;
gap: 12px;
gap: 10px;
flex-wrap: wrap;
}
@@ -673,10 +790,19 @@ body {
.conditional-rule-head-main {
min-width: 0;
display: grid;
gap: 8px;
}
.conditional-rule-title-row {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.conditional-rule-head h3 {
margin: 2px 0 2px;
margin: 0;
font-size: 15px;
color: #142033;
}
@@ -710,6 +836,11 @@ body {
height: 15px;
}
.conditional-rule-target-inline {
display: grid;
gap: 6px;
}
.conditional-meta-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -734,6 +865,27 @@ body {
background: linear-gradient(180deg, #f6faff, #ffffff);
}
.conditional-summary-text {
color: #21354f;
font-size: 13px;
line-height: 1.55;
}
.conditional-rule-state {
display: grid;
align-content: start;
gap: 4px;
padding: 10px 12px;
border: 1px solid #e7edf6;
border-radius: 12px;
background: #f9fbff;
}
.conditional-rule-state strong {
color: #142033;
font-size: 14px;
}
.conditional-summary-prefix {
color: #294567;
font-size: 11px;
@@ -759,12 +911,7 @@ body {
.conditional-target-chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.conditional-clause-list {
display: grid;
gap: 10px;
gap: 6px;
}
.conditional-sentence-builder {
@@ -788,8 +935,7 @@ body {
font-size: 11px;
font-weight: 800;
line-height: 1.45;
text-transform: uppercase;
letter-spacing: 0.04em;
letter-spacing: 0.01em;
}
.conditional-sentence-row select,
@@ -1807,6 +1953,16 @@ body {
justify-content: flex-start;
}
.builder-toolbar {
flex-direction: column;
align-items: flex-start;
}
.builder-toolbar-note {
max-width: none;
text-align: left;
}
.builder-panel-meta {
justify-content: flex-start;
}
@@ -1819,6 +1975,10 @@ body {
grid-template-columns: 1fr;
}
.conditional-rule-title-row {
align-items: flex-start;
}
.builder-entity-card-head {
flex-direction: column;
align-items: flex-start;

View File

@@ -1,5 +1,6 @@
{% extends 'workflows/base_shell.html' %}
{% load static i18n %}
{% get_current_language as CURRENT_LANGUAGE %}
{% block title %}{% trans "Form Builder" %}{% endblock %}
@@ -33,6 +34,26 @@
</a>
</nav>
<div class="builder-sidebar-card builder-sidebar-context">
<span class="builder-sidebar-eyebrow">{% trans "Aktive Ansicht" %}</span>
<div class="builder-context-stack">
<div class="builder-context-row">
<span class="builder-context-label">{% trans "Workflow" %}</span>
<strong>{{ active_form_type_label }}</strong>
</div>
<div class="builder-context-row">
<span class="builder-context-label">{% trans "Modul" %}</span>
<strong>{{ active_module_label }}</strong>
</div>
{% if active_focus_label %}
<div class="builder-context-row">
<span class="builder-context-label">{% trans "Fokus" %}</span>
<strong>{{ active_focus_label }}</strong>
</div>
{% endif %}
</div>
</div>
<div class="builder-sidebar-card builder-sidebar-stats">
<div class="builder-side-stat">
<strong>{{ builder_summary.configurable_field_count }}</strong>
@@ -57,6 +78,12 @@
<p class="builder-hero-sub">{% trans "Steuern Sie Struktur, Regeln und Inhalte Ihrer Standard-Workflows an einem Ort." %}</p>
</div>
<div class="builder-hero-actions">
<form method="post" action="{% url 'set_language' %}" class="builder-lang-switch">
{% csrf_token %}
<input type="hidden" name="next" value="{{ request.get_full_path }}" />
<button class="builder-lang-btn {% if CURRENT_LANGUAGE == 'de' %}active{% endif %}" type="submit" name="language" value="de">DE</button>
<button class="builder-lang-btn {% if CURRENT_LANGUAGE == 'en' %}active{% endif %}" type="submit" name="language" value="en">EN</button>
</form>
{% for key, label in form_types %}
<a
class="tab {% if form_type == key %}active{% endif %}"
@@ -69,6 +96,28 @@
</div>
</section>
<section class="builder-toolbar" aria-label="{% trans 'Arbeitskontext' %}">
<div class="builder-toolbar-main">
<span class="builder-toolbar-chip">
<span class="builder-toolbar-chip-label">{% trans "Workflow" %}</span>
<strong>{{ active_form_type_label }}</strong>
</span>
<span class="builder-toolbar-chip">
<span class="builder-toolbar-chip-label">{% trans "Modul" %}</span>
<strong>{{ active_module_label }}</strong>
</span>
{% if active_focus_label %}
<span class="builder-toolbar-chip">
<span class="builder-toolbar-chip-label">{% trans "Aktiv" %}</span>
<strong>{{ active_focus_label }}</strong>
</span>
{% endif %}
</div>
<div class="builder-toolbar-note">
{% trans "Arbeiten Sie jeweils nur in einem Bereich und speichern Sie Änderungen abschnittsweise." %}
</div>
</section>
{% include 'workflows/includes/messages.html' %}
<div id="status-message" class="status" aria-live="polite"></div>
@@ -281,6 +330,7 @@
<div class="field-rule-main">
<input type="hidden" name="field_rule_ids" value="{{ item.id }}" />
<strong>{{ item.label }}</strong>
<div class="field-rule-summary">{{ item.summary }}</div>
<div class="field-rule-meta">
<span class="entity-meta">{{ item.field_name }}</span>
<div class="field-rule-status field-rule-status-inline">
@@ -357,11 +407,20 @@
<section class="conditional-rule-card">
<div class="conditional-rule-head">
<div class="conditional-rule-head-main">
<span class="conditional-rule-eyebrow">{% trans "Sichtbarkeit" %}</span>
<h3>{{ item.title }}</h3>
{% if item.description %}
<p class="mini">{{ item.description }}</p>
{% endif %}
<div class="conditional-rule-title-row">
<span class="conditional-rule-eyebrow">{% trans "Sichtbarkeit" %}</span>
<h3>{{ item.title }}</h3>
</div>
<div class="conditional-rule-target-inline">
<span class="conditional-target-label">{% trans "Steuert" %}</span>
<div class="conditional-target-chips">
{% for field_name in item.target_fields %}
<span class="preview-chip">{{ field_name }}</span>
{% empty %}
<span class="mini">{% trans "Keine Ziel-Felder." %}</span>
{% endfor %}
</div>
</div>
</div>
<label class="conditional-toggle">
<span>{% trans "Aktiv" %}</span>
@@ -371,33 +430,14 @@
<div class="conditional-meta-grid">
<div class="conditional-rule-summary">
<span class="conditional-summary-prefix">{% trans "Sichtbar, wenn" %}</span>
<div class="conditional-summary-chips">
{% with first_clause=item.clauses.0 second_clause=item.clauses.1 %}
{% if first_clause.field %}
<span class="preview-chip">{{ first_clause.field }}</span>
<span class="preview-chip">{{ first_clause.operator }}</span>
{% if first_clause.value %}<span class="preview-chip">{{ first_clause.value }}</span>{% endif %}
{% else %}
<span class="preview-chip">{% trans "Keine Bedingung" %}</span>
{% endif %}
{% if second_clause.field %}
<span class="preview-chip">{% trans "und" %}</span>
<span class="preview-chip">{{ second_clause.field }}</span>
<span class="preview-chip">{{ second_clause.operator }}</span>
{% if second_clause.value %}<span class="preview-chip">{{ second_clause.value }}</span>{% endif %}
{% endif %}
{% endwith %}
</div>
<div class="conditional-summary-text">{{ item.summary }}</div>
</div>
<div class="conditional-targets">
<span class="conditional-target-label">{% trans "Steuert" %}</span>
<div class="conditional-target-chips">
{% for field_name in item.target_fields %}
<span class="preview-chip">{{ field_name }}</span>
{% empty %}
<span class="mini">{% trans "Keine Ziel-Felder." %}</span>
{% endfor %}
</div>
<div class="conditional-rule-state">
<span class="conditional-summary-prefix">{% trans "Status" %}</span>
<strong>{% if item.is_active %}{% trans "Aktiv" %}{% else %}{% trans "Inaktiv" %}{% endif %}</strong>
{% if item.description %}
<span class="mini">{{ item.description }}</span>
{% endif %}
</div>
</div>
<div class="conditional-sentence-builder">

View File

@@ -133,6 +133,46 @@ CONDITIONAL_RULE_OPERATOR_CHOICES = [
]
def _field_rule_summary(*, is_visible: bool, is_required, locked: bool) -> str:
if locked:
return str(_('Fixes Kernfeld, immer sichtbar.'))
if not is_visible:
return str(_('Ausgeblendet, erscheint nicht im Formular.'))
if is_required is True:
return str(_('Sichtbar und als Pflichtfeld markiert.'))
if is_required is False:
return str(_('Sichtbar und optional.'))
return str(_('Sichtbar mit Standardverhalten.'))
def _conditional_clause_sentence(clause: dict, field_label_map: dict[str, str]) -> str:
field_name = (clause.get('field') or '').strip()
operator = (clause.get('operator') or '').strip()
value = clause.get('value')
if not field_name or not operator:
return ''
field_label = field_label_map.get(field_name, field_name)
if operator == 'checked':
return _('%(field)s ist aktiviert') % {'field': field_label}
if operator == 'equals':
if value not in (None, ''):
return _('%(field)s ist gleich %(value)s') % {'field': field_label, 'value': value}
return _('%(field)s ist gleich') % {'field': field_label}
if operator == 'not_equals':
if value not in (None, ''):
return _('%(field)s ist nicht gleich %(value)s') % {'field': field_label, 'value': value}
return _('%(field)s ist nicht gleich') % {'field': field_label}
return _('%(field)s erfüllt die Bedingung') % {'field': field_label}
def _conditional_rule_summary(clauses: list[dict], field_label_map: dict[str, str]) -> str:
active_clauses = [clause for clause in clauses if clause.get('field') and clause.get('operator')]
if not active_clauses:
return str(_('Immer sichtbar.'))
parts = [str(_conditional_clause_sentence(clause, field_label_map)) for clause in active_clauses]
return str(_('Sichtbar, wenn %(conditions)s.') % {'conditions': ' und '.join(parts)})
def _normalized_conditional_rule_payload(form_type: str) -> dict[str, dict]:
configs = ensure_form_conditional_rule_configs(form_type)
payload = {}
@@ -2239,30 +2279,30 @@ def form_builder_page(request):
if delete_option_id:
option = FormOption.objects.filter(id=delete_option_id).first()
if not option:
messages.error(request, 'Option nicht gefunden.')
messages.error(request, _('Option nicht gefunden.'))
else:
option_category = option.category
deleted_label = option.label
deleted_id = option.id
option.delete()
_audit(request, 'form_option_deleted', target_type='form_option', target_id=deleted_id, target_label=deleted_label)
messages.success(request, 'Option wurde gelöscht.')
messages.success(request, _('Option wurde gelöscht.'))
return redirect(f"/admin-tools/form-builder/?form_type={form_type}&option_category={option_category}&module=options")
if delete_custom_field_id:
custom_field = FormCustomFieldConfig.objects.filter(id=delete_custom_field_id, form_type=form_type).first()
if not custom_field:
messages.error(request, 'Benutzerdefiniertes Feld nicht gefunden.')
messages.error(request, _('Benutzerdefiniertes Feld nicht gefunden.'))
else:
deleted_label = custom_field.label
deleted_id = custom_field.id
custom_field.delete()
_audit(request, 'form_custom_field_deleted', target_type='form_custom_field', target_id=deleted_id, target_label=deleted_label)
messages.success(request, 'Benutzerdefiniertes Feld wurde gelöscht.')
messages.success(request, _('Benutzerdefiniertes Feld wurde gelöscht.'))
return redirect(f"/admin-tools/form-builder/?form_type={form_type}&option_category={option_category}&module=custom-fields")
if delete_custom_section_id:
custom_section = FormCustomSectionConfig.objects.filter(id=delete_custom_section_id, form_type=form_type).first()
if not custom_section:
messages.error(request, 'Benutzerdefinierter Abschnitt nicht gefunden.')
messages.error(request, _('Benutzerdefinierter Abschnitt nicht gefunden.'))
else:
deleted_label = custom_section.title
deleted_id = custom_section.id
@@ -2285,7 +2325,7 @@ def form_builder_page(request):
target_label=deleted_label,
details={'section_key': section_key, 'deleted_field_count': deleted_field_count},
)
messages.success(request, 'Benutzerdefinierter Abschnitt wurde gelöscht.')
messages.success(request, _('Benutzerdefinierter Abschnitt wurde gelöscht.'))
return redirect(f"/admin-tools/form-builder/?form_type={form_type}&option_category={option_category}&module=custom-sections")
action = request.POST.get('builder_action', '')
@@ -2295,9 +2335,9 @@ def form_builder_page(request):
label_en = request.POST.get('label_en', '').strip()
value = request.POST.get('value', '').strip()
if category not in option_categories:
messages.error(request, 'Ungültige Kategorie.')
messages.error(request, _('Ungültige Kategorie.'))
elif not label:
messages.error(request, 'Bitte einen Namen für die Option angeben.')
messages.error(request, _('Bitte einen Namen für die Option angeben.'))
else:
next_sort = (
FormOption.objects.filter(category=category).order_by('-sort_order').values_list('sort_order', flat=True).first()
@@ -2318,7 +2358,7 @@ def form_builder_page(request):
target_label=label,
details={'category': category, 'label_en': label_en, 'value': value or label},
)
messages.success(request, 'Option wurde hinzugefügt.')
messages.success(request, _('Option wurde hinzugefügt.'))
option_category = category
elif action == 'save_options':
@@ -2336,11 +2376,11 @@ def form_builder_page(request):
try:
option.save(update_fields=['label', 'label_en', 'value', 'is_active', 'sort_order'])
except IntegrityError:
messages.error(request, f'Doppelte Bezeichnung in Kategorie: {next_label}')
messages.error(request, _('Doppelte Bezeichnung in Kategorie: %(label)s') % {'label': next_label})
return redirect(f"/admin-tools/form-builder/?form_type={form_type}&option_category={option.category}&module=options")
option_category = option.category
_audit(request, 'form_options_saved', target_type='form_option', target_label=option_category, details={'count': len(option_ids)})
messages.success(request, 'Optionen wurden gespeichert.')
messages.success(request, _('Optionen wurden gespeichert.'))
elif action == 'save_field_texts':
field_ids = request.POST.getlist('field_ids')
@@ -2354,14 +2394,14 @@ def form_builder_page(request):
cfg.help_text_override_en = (request.POST.get(f'help_text_override_en_{cfg.id}') or '').strip()
cfg.save(update_fields=['label_override', 'label_override_en', 'help_text_override', 'help_text_override_en'])
_audit(request, 'form_field_texts_saved', target_type='form_config', target_label=form_type, details={'count': len(field_ids)})
messages.success(request, 'Feldtexte wurden gespeichert.')
messages.success(request, _('Feldtexte wurden gespeichert.'))
elif action == 'add_custom_section' and form_type == 'onboarding':
title = (request.POST.get('custom_section_title') or '').strip()
title_en = (request.POST.get('custom_section_title_en') or '').strip()
sort_order_raw = (request.POST.get('custom_section_sort_order') or '').strip()
if not title:
messages.error(request, 'Bitte einen Titel für den benutzerdefinierten Abschnitt angeben.')
messages.error(request, _('Bitte einen Titel für den benutzerdefinierten Abschnitt angeben.'))
else:
section_key_base = build_custom_field_key(title)
section_key = section_key_base
@@ -2382,7 +2422,7 @@ def form_builder_page(request):
is_active=True,
)
_audit(request, 'form_custom_section_added', target_type='form_custom_section', target_label=title, details={'form_type': form_type, 'section_key': section_key})
messages.success(request, 'Benutzerdefinierter Abschnitt wurde hinzugefügt.')
messages.success(request, _('Benutzerdefinierter Abschnitt wurde hinzugefügt.'))
elif action == 'save_custom_sections' and form_type == 'onboarding':
section_ids = request.POST.getlist('custom_section_ids')
@@ -2402,7 +2442,7 @@ def form_builder_page(request):
cfg.save(update_fields=['title', 'title_en', 'is_active', 'sort_order'])
updated += 1
_audit(request, 'form_custom_sections_saved', target_type='form_custom_section', target_label=form_type, details={'count': updated})
messages.success(request, 'Benutzerdefinierte Abschnitte wurden gespeichert.')
messages.success(request, _('Benutzerdefinierte Abschnitte wurden gespeichert.'))
elif action == 'add_custom_field':
label = (request.POST.get('custom_label') or '').strip()
@@ -2417,13 +2457,13 @@ def form_builder_page(request):
section_choices = {key for key in get_section_order(form_type)}
field_type_choices = {key for key, _ in FormCustomFieldConfig.FIELD_TYPE_CHOICES}
if not label:
messages.error(request, 'Bitte eine Bezeichnung für das benutzerdefinierte Feld angeben.')
messages.error(request, _('Bitte eine Bezeichnung für das benutzerdefinierte Feld angeben.'))
elif section_key not in section_choices:
messages.error(request, 'Ungültiger Abschnitt für das benutzerdefinierte Feld.')
messages.error(request, _('Ungültiger Abschnitt für das benutzerdefinierte Feld.'))
elif field_type not in field_type_choices:
messages.error(request, 'Ungültiger Feldtyp.')
messages.error(request, _('Ungültiger Feldtyp.'))
elif field_type == FormCustomFieldConfig.FIELD_TYPE_SELECT and not select_options:
messages.error(request, 'Auswahlfelder benötigen mindestens eine Option.')
messages.error(request, _('Auswahlfelder benötigen mindestens eine Option.'))
else:
field_key_base = build_custom_field_key(label)
field_key = field_key_base
@@ -2451,7 +2491,7 @@ def form_builder_page(request):
select_options_en=select_options_en,
)
_audit(request, 'form_custom_field_added', target_type='form_custom_field', target_label=label, details={'form_type': form_type, 'field_type': field_type, 'section_key': section_key})
messages.success(request, 'Benutzerdefiniertes Feld wurde hinzugefügt.')
messages.success(request, _('Benutzerdefiniertes Feld wurde hinzugefügt.'))
elif action == 'save_custom_fields':
custom_ids = request.POST.getlist('custom_field_ids')
@@ -2482,12 +2522,12 @@ def form_builder_page(request):
cfg.select_options = (request.POST.get(f'custom_select_options_{cfg.id}') or '').strip()
cfg.select_options_en = (request.POST.get(f'custom_select_options_en_{cfg.id}') or '').strip()
if cfg.field_type == FormCustomFieldConfig.FIELD_TYPE_SELECT and not cfg.select_options:
messages.error(request, f'Auswahlfeld "{cfg.label}" benötigt mindestens eine Option.')
messages.error(request, _('Auswahlfeld "%(label)s" benötigt mindestens eine Option.') % {'label': cfg.label})
return redirect(f"/admin-tools/form-builder/?form_type={form_type}&option_category={option_category}&module=custom-fields")
cfg.save()
updated += 1
_audit(request, 'form_custom_fields_saved', target_type='form_custom_field', target_label=form_type, details={'count': updated})
messages.success(request, 'Benutzerdefinierte Felder wurden gespeichert.')
messages.success(request, _('Benutzerdefinierte Felder wurden gespeichert.'))
elif action == 'save_field_rules':
field_ids = request.POST.getlist('field_rule_ids')
@@ -2507,7 +2547,7 @@ def form_builder_page(request):
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.')
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)
@@ -2546,7 +2586,7 @@ def form_builder_page(request):
cfg.save(update_fields=['is_active'])
updated += 1
_audit(request, 'form_section_rules_saved', target_type='form_config', target_label=form_type, details={'count': updated})
messages.success(request, 'Abschnittsregeln wurden gespeichert.')
messages.success(request, _('Abschnittsregeln wurden gespeichert.'))
elif action == 'save_conditional_rules' and form_type == 'onboarding':
rule_configs = ensure_form_conditional_rule_configs(form_type)
@@ -2567,16 +2607,16 @@ def form_builder_page(request):
cfg.save(update_fields=['is_active', 'clauses'])
updated += 1
_audit(request, 'form_conditional_rules_saved', target_type='form_config', target_label=form_type, details={'count': updated})
messages.success(request, 'Bedingte Logik wurde gespeichert.')
messages.success(request, _('Bedingte Logik wurde gespeichert.'))
elif action == 'apply_preset':
preset_key = (request.POST.get('preset_key') or '').strip()
if apply_form_preset(form_type, preset_key):
active_module = 'preview'
_audit(request, 'form_preset_applied', target_type='form_config', target_label=form_type, details={'preset': preset_key})
messages.success(request, 'Preset wurde angewendet.')
messages.success(request, _('Preset wurde angewendet.'))
else:
messages.error(request, 'Preset konnte nicht angewendet werden.')
messages.error(request, _('Preset konnte nicht angewendet werden.'))
if action in {'add_option', 'save_options'}:
active_module = 'options'
@@ -2778,6 +2818,11 @@ def form_builder_page(request):
'is_visible': cfg.is_visible,
'is_required': cfg.is_required,
'locked': cfg.field_name in locked and not can_override_locked_builder_rules,
'summary': _field_rule_summary(
is_visible=cfg.is_visible,
is_required=cfg.is_required,
locked=cfg.field_name in locked and not can_override_locked_builder_rules,
),
}
)
@@ -2854,6 +2899,7 @@ def form_builder_page(request):
conditional_field_choices.append((field_name, labels.get(field_name, field_name)))
for cfg in custom_field_configs:
conditional_field_choices.append((f'custom__{cfg.field_key}', cfg.translated_label(language_code)))
conditional_field_label_map = {value: label for value, label in conditional_field_choices}
conditional_target_titles = {
'business-card-box': _('Visitenkarten-Details'),
'employment-end-box': _('Vertragsende'),
@@ -2893,6 +2939,7 @@ def form_builder_page(request):
'description': target_description,
'is_active': cfg.is_active,
'clauses': clauses[:2],
'summary': _conditional_rule_summary(clauses[:2], conditional_field_label_map),
'field_choices': conditional_field_choices,
'operator_choices': CONDITIONAL_RULE_OPERATOR_CHOICES,
'target_fields': target_fields,
@@ -2945,6 +2992,41 @@ def form_builder_page(request):
'custom_field_count': len([cfg for cfg in custom_field_configs if cfg.is_active]),
'custom_section_count': len([cfg for cfg in custom_section_configs if cfg.is_active]),
}
module_labels = {
'structure': _('Struktur & Reihenfolge'),
'section-rules': _('Abschnitte'),
'field-rules': _('Feldregeln'),
'conditional-rules': _('Bedingte Logik'),
'options': _('Optionen'),
'field-texts': _('Feldtexte'),
'custom-sections': _('Eigene Abschnitte'),
'custom-fields': _('Eigene Felder'),
'preview': _('Vorschau'),
}
option_category_labels = dict(_translate_choice_list(FormOption.CATEGORY_CHOICES))
form_type_labels = {
'onboarding': _('Onboarding'),
'offboarding': _('Offboarding'),
}
active_focus_label = ''
if active_module == 'structure' and active_structure_section:
active_focus_label = section_labels.get(active_structure_section, active_structure_section)
elif active_module == 'section-rules' and section_rule_items:
active_focus_label = _('Alle Abschnitte')
elif active_module == 'field-rules' and active_field_rules_section:
active_focus_label = section_labels.get(active_field_rules_section, active_field_rules_section)
elif active_module == 'conditional-rules' and active_conditional_target:
active_focus_label = next((item['title'] for item in conditional_rule_items if item['target_key'] == active_conditional_target), active_conditional_target)
elif active_module == 'options':
active_focus_label = option_category_labels.get(option_category, option_category)
elif active_module == 'field-texts' and active_field_texts_section:
active_focus_label = section_labels.get(active_field_texts_section, active_field_texts_section)
elif active_module == 'custom-sections':
active_focus_label = _('Onboarding')
elif active_module == 'custom-fields' and active_custom_fields_section:
active_focus_label = section_labels.get(active_custom_fields_section, active_custom_fields_section)
elif active_module == 'preview':
active_focus_label = _('Live-Vorschau')
return render(
request,
@@ -2952,7 +3034,7 @@ def form_builder_page(request):
{
'form_type': form_type,
'columns': columns,
'form_types': [('onboarding', 'Onboarding'), ('offboarding', 'Offboarding')],
'form_types': [('onboarding', _('Onboarding')), ('offboarding', _('Offboarding'))],
'option_categories': _translate_choice_list(FormOption.CATEGORY_CHOICES),
'selected_option_category': option_category,
'option_items': FormOption.objects.filter(category=option_category).order_by('sort_order', 'label'),
@@ -2971,6 +3053,9 @@ def form_builder_page(request):
'active_subpanel': active_subpanel,
'active_rules_panel': active_rules_panel,
'active_module': active_module,
'active_form_type_label': form_type_labels.get(form_type, form_type),
'active_module_label': module_labels.get(active_module, active_module),
'active_focus_label': active_focus_label,
'active_structure_section': active_structure_section,
'active_field_rules_section': active_field_rules_section,
'active_field_texts_section': active_field_texts_section,