snapshot: preserve dynamic form builder parity and presets

This commit is contained in:
Md Bayazid Bostame
2026-03-27 12:30:10 +01:00
parent aa54f41731
commit e929e7509b
12 changed files with 2097 additions and 505 deletions

View File

@@ -10,153 +10,399 @@
{% block shell_body %}
{% include 'workflows/includes/app_header.html' with header_show_home=1 header_inside_shell=1 %}
<header class="header">
<h1>{% trans "Form Builder" %}</h1>
<p>{% trans "Felder per Drag-and-Drop sortieren und pro Schritt gruppieren." %}</p>
</header>
<section class="builder-hero">
<div class="builder-hero-copy">
<span class="builder-eyebrow">{% trans "Deployment Configuration" %}</span>
<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' %}
<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 class="columns {% if form_type == 'offboarding' %}single{% endif %}" id="builder-columns" data-form-type="{{ form_type }}">
{% for column in columns %}
<section class="column" data-column-key="{{ column.key }}">
<h2>{{ column.title }}</h2>
<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>
<nav class="builder-quicknav" aria-label="{% trans 'Bereiche' %}">
<a href="#builder-structure">{% trans "Reihenfolge" %}</a>
<a href="#builder-rules">{% trans "Regeln" %}</a>
<a href="#builder-content">{% trans "Optionen & Texte" %}</a>
</nav>
<section class="builder-overview">
<article class="builder-stat-card">
<span class="builder-stat-label">{% trans "Fixe Kernfelder" %}</span>
<strong>{{ builder_summary.locked_field_count }}</strong>
</article>
<article class="builder-stat-card">
<span class="builder-stat-label">{% trans "Konfigurierbar" %}</span>
<strong>{{ builder_summary.configurable_field_count }}</strong>
</article>
<article class="builder-stat-card">
<span class="builder-stat-label">{% trans "Aktuell ausgeblendet" %}</span>
<strong>{{ builder_summary.hidden_field_count }}</strong>
</article>
{% if form_type == 'onboarding' %}
<article class="builder-stat-card">
<span class="builder-stat-label">{% trans "Versteckte Abschnitte" %}</span>
<strong>{{ builder_summary.hidden_section_count }}</strong>
</article>
{% endif %}
</section>
<section class="builder-preset-bar">
<form class="builder-preset-form" method="post" action="/admin-tools/form-builder/?form_type={{ form_type }}&option_category={{ selected_option_category }}">
{% csrf_token %}
<input type="hidden" name="builder_action" value="apply_preset" />
<label class="builder-preset-label" for="preset_key">{% trans "Vorlage anwenden" %}</label>
<select id="preset_key" name="preset_key">
{% for preset_key, preset in available_presets.items %}
<option value="{{ preset_key }}">{{ preset.label }}</option>
{% endfor %}
</select>
<button class="btn btn-secondary" type="submit">{% trans "Anwenden" %}</button>
</form>
</section>
<details class="builder-panel builder-accordion js-single-accordion" data-accordion-group="builder-panels" id="builder-preview" {% if active_panel == 'builder-preview' %}open{% endif %}>
<summary class="builder-panel-summary">
<div class="builder-panel-head">
<div>
<h2>{% trans "Live-Vorschau" %}</h2>
</div>
<span class="builder-panel-toggle">{% trans "Öffnen" %}</span>
</div>
</summary>
<div class="builder-panel-body">
<div class="preview-shell">
{% for section in preview_sections %}
<section class="preview-section">
<div class="preview-section-head">
<h3>{{ section.title }}</h3>
<span class="column-count">{% blocktrans trimmed with count=section.items|length %}{{ count }} Feld/Felder{% endblocktrans %}</span>
</div>
<div class="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 class="preview-chip-list">
{% for item in section.items %}
<span class="preview-chip{% if item.locked %} is-locked{% endif %}">{{ item.label }}</span>
{% empty %}
<span class="mini">{% trans "Keine sichtbaren Felder." %}</span>
{% endfor %}
</div>
</article>
</section>
{% endfor %}
</div>
</section>
{% endfor %}
</div>
</div>
</details>
<section class="options-panel">
<div class="options-head">
<h2>{% trans "Optionen verwalten" %}</h2>
<form class="category-switch" method="get" action="/admin-tools/form-builder/">
<input type="hidden" name="form_type" value="{{ form_type }}" />
<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>
<details class="builder-panel builder-accordion js-single-accordion" data-accordion-group="builder-panels" id="builder-structure" {% if active_panel == 'builder-structure' %}open{% endif %}>
<summary class="builder-panel-summary">
<div class="builder-panel-head">
<div>
<h2>{% trans "Struktur & Reihenfolge" %}</h2>
</div>
<span class="builder-panel-toggle">{% trans "Geöffnet" %}</span>
</div>
</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 %}
</select>
</form>
</div>
</section>
{% endfor %}
</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>
<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 item in field_text_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>
</details>
<details class="builder-panel builder-accordion js-single-accordion" data-accordion-group="builder-panels" id="builder-rules" {% if active_panel == 'builder-rules' %}open{% endif %}>
<summary class="builder-panel-summary">
<div class="builder-panel-head">
<div>
<h2>{% trans "Sichtbarkeit & Regeln" %}</h2>
</div>
<span class="builder-panel-toggle">{% trans "Öffnen" %}</span>
</div>
</summary>
<div class="builder-panel-body">
<div class="builder-rule-layout">
<section class="options-panel">
<div class="options-head">
<h2>{% trans "Abschnitte steuern" %}</h2>
</div>
<form method="post" action="/admin-tools/form-builder/?form_type={{ form_type }}&option_category={{ selected_option_category }}">
{% csrf_token %}
<div class="section-rule-grid">
{% for section in section_rule_items %}
<label class="section-rule-card{% if section.locked %} is-locked{% endif %}">
<div class="section-rule-copy">
<strong>{{ section.title }}</strong>
<span>{% blocktrans trimmed with count=section.field_count %}{{ count }} Feld/Felder in diesem Abschnitt.{% endblocktrans %}</span>
</div>
<div class="section-rule-toggle">
<input
type="checkbox"
name="section_visible_{{ section.key }}"
{% if section.is_visible %}checked{% endif %}
{% if section.locked %}disabled{% endif %}
/>
<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 %}
</tbody>
</table>
</div>
<div class="options-actions">
<button class="btn btn-primary" type="submit" name="builder_action" value="save_section_rules">{% trans "Abschnittsregeln speichern" %}</button>
</div>
</form>
</section>
<section class="options-panel">
<div class="options-head">
<h2>{% trans "Feldregeln verwalten" %}</h2>
</div>
<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 class="options-actions">
<button class="btn btn-primary" type="submit" name="builder_action" value="save_field_texts">{% trans "Feldtexte speichern" %}</button>
</div>
</form>
</section>
</summary>
<div class="builder-panel-body">
<div class="builder-stack-layout">
<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 %}

View File

@@ -49,19 +49,29 @@
<div class="card">
<form method="post" data-email-domain="{{ portal_email_domain }}">
{% csrf_token %}
<div class="grid">
{% for field in form.visible_fields %}
{% if field.name != 'search_query' %}
<div class="offboarding-sections">
{% for section in offboarding_sections %}
<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 %}">
{{ field.label_tag }}
{{ field }}
{% if field.help_text %}<div class="hint">{{ field.help_text }}</div>{% endif %}
{{ field.errors }}
</div>
{% endif %}
{% endfor %}
</div>
</section>
{% endfor %}
</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>
</div>
</div>