snapshot: preserve custom field parity across forms timeline and pdf

This commit is contained in:
Md Bayazid Bostame
2026-03-27 13:21:25 +01:00
parent 2e5e941d41
commit fdc27f2123
20 changed files with 2294 additions and 545 deletions

View File

@@ -51,6 +51,10 @@
<span class="builder-stat-label">{% trans "Aktuell ausgeblendet" %}</span>
<strong>{{ builder_summary.hidden_field_count }}</strong>
</article>
<article class="builder-stat-card">
<span class="builder-stat-label">{% trans "Eigene Felder" %}</span>
<strong>{{ builder_summary.custom_field_count }}</strong>
</article>
{% if form_type == 'onboarding' %}
<article class="builder-stat-card">
<span class="builder-stat-label">{% trans "Versteckte Abschnitte" %}</span>
@@ -129,6 +133,7 @@
<div class="field-name">{{ item.field_name }}</div>
</div>
<div class="badges">
{% if item.is_custom %}<span class="badge">{% trans "Eigen" %}</span>{% endif %}
{% 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 %}
@@ -451,22 +456,134 @@
</form>
</div>
</details>
<details class="options-panel nested-accordion js-single-accordion" data-accordion-group="builder-content-subpanels" {% if active_subpanel == 'custom-fields' %}open{% endif %}>
<summary class="nested-accordion-summary">
<div class="options-head">
<h2>{% trans "Eigene Felder" %}</h2>
<span class="builder-panel-toggle">{% trans "Öffnen" %}</span>
</div>
</summary>
<div class="nested-accordion-body">
<form class="add-option-form" method="post" action="/admin-tools/form-builder/?form_type={{ form_type }}&option_category={{ selected_option_category }}">
{% csrf_token %}
<input type="hidden" name="builder_action" value="add_custom_field" />
<input type="text" name="custom_label" placeholder="{% trans 'Label (DE)' %}" required />
<input type="text" name="custom_label_en" placeholder="{% trans 'Label (EN, optional)' %}" />
<select name="custom_section_key">
{% for group in custom_field_groups %}
<option value="{{ group.key }}">{{ group.title }}</option>
{% endfor %}
</select>
<select name="custom_field_type">
{% for value, label in custom_field_type_choices %}
<option value="{{ value }}">{{ label }}</option>
{% endfor %}
</select>
<input type="number" name="custom_sort_order" min="0" value="0" placeholder="{% trans 'Sortierung' %}" />
<label class="field-rule-control compact-inline">
<span>{% trans "Pflicht" %}</span>
<input type="checkbox" name="custom_is_required" />
</label>
<input type="text" name="custom_help_text" placeholder="{% trans 'Hilfetext (DE, optional)' %}" />
<input type="text" name="custom_help_text_en" placeholder="{% trans 'Hilfetext (EN, optional)' %}" />
<textarea name="custom_select_options" rows="3" placeholder="{% trans 'Optionen (eine pro Zeile, optional: wert|Label)' %}"></textarea>
<textarea name="custom_select_options_en" rows="3" placeholder="{% trans 'Optionen EN (eine pro Zeile, optional: value|Label)' %}"></textarea>
<button class="btn btn-primary" type="submit">{% trans "Eigenes Feld hinzufügen" %}</button>
</form>
<form method="post" action="/admin-tools/form-builder/?form_type={{ form_type }}&option_category={{ selected_option_category }}">
{% csrf_token %}
<div class="option-table-wrap">
<table class="option-table">
<thead>
<tr>
<th>{% trans "Schlüssel" %}</th>
<th>{% trans "Abschnitt" %}</th>
<th>{% trans "Typ" %}</th>
<th>{% trans "Sortierung" %}</th>
<th>{% trans "Label (DE)" %}</th>
<th>{% trans "Label (EN)" %}</th>
<th>{% trans "Pflicht" %}</th>
<th>{% trans "Aktiv" %}</th>
<th>{% trans "Löschen" %}</th>
</tr>
</thead>
<tbody>
{% for group in custom_field_groups %}
<tr class="option-table-group-row">
<th colspan="9">{{ group.title }}</th>
</tr>
{% for item in group.items %}
<tr>
<td>
<input type="hidden" name="custom_field_ids" value="{{ item.id }}" />
<strong>{{ item.field_key }}</strong>
</td>
<td>
<select name="custom_section_key_{{ item.id }}">
{% for group_choice in custom_field_groups %}
<option value="{{ group_choice.key }}" {% if group_choice.key == item.section_key %}selected{% endif %}>{{ group_choice.title }}</option>
{% endfor %}
</select>
</td>
<td>
<select name="custom_field_type_{{ item.id }}">
{% for value, label in custom_field_type_choices %}
<option value="{{ value }}" {% if value == item.field_type %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</td>
<td><input type="number" min="0" name="custom_sort_order_{{ item.id }}" value="{{ item.sort_order }}" /></td>
<td>
<input type="text" name="custom_label_{{ item.id }}" value="{{ item.label }}" required />
<input type="text" name="custom_help_text_{{ item.id }}" value="{{ item.help_text }}" placeholder="{% trans 'Hilfetext (DE)' %}" />
<textarea name="custom_select_options_{{ item.id }}" rows="2" placeholder="{% trans 'Optionen (DE)' %}">{{ item.select_options }}</textarea>
</td>
<td>
<input type="text" name="custom_label_en_{{ item.id }}" value="{{ item.label_en }}" />
<input type="text" name="custom_help_text_en_{{ item.id }}" value="{{ item.help_text_en }}" placeholder="{% trans 'Hilfetext (EN)' %}" />
<textarea name="custom_select_options_en_{{ item.id }}" rows="2" placeholder="{% trans 'Optionen (EN)' %}">{{ item.select_options_en }}</textarea>
</td>
<td><input type="checkbox" name="custom_is_required_{{ item.id }}" {% if item.is_required %}checked{% endif %} /></td>
<td><input type="checkbox" name="custom_is_active_{{ item.id }}" {% if item.is_active %}checked{% endif %} /></td>
<td>
<button class="btn btn-secondary" type="submit" name="delete_custom_field_id" value="{{ item.id }}" data-confirm="{% trans 'Eigenes Feld wirklich löschen?' %}">{% trans "Löschen" %}</button>
</td>
</tr>
{% empty %}
<tr><td colspan="9">{% trans "Keine eigenen Felder vorhanden." %}</td></tr>
{% endfor %}
{% endfor %}
</tbody>
</table>
</div>
<div class="options-actions">
<button class="btn btn-primary" type="submit" name="builder_action" value="save_custom_fields">{% trans "Eigene Felder 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;
});
{% endblock %}
{% block extra_scripts %}
<script src="{% static 'workflows/js/form_builder.js' %}"></script>
<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>
});
})();
</script>
{% endblock %}

View File

@@ -37,6 +37,57 @@
<main class="main">
{% include 'workflows/includes/messages.html' %}
{% if ops_summary.show %}
<section class="ops-overview-card">
<div class="ops-overview-head">
<div>
<h2>{% trans "Operations Overview" %}</h2>
<p>{% trans "Letzte Laufzeit- und Backup-Signale auf einen Blick." %}</p>
</div>
</div>
<div class="ops-overview-grid">
{% if ops_summary.can_view_jobs %}
<article class="ops-stat-card">
<span class="ops-stat-label">{% trans "Fehlgeschlagene Jobs (24h)" %}</span>
<strong class="{% if ops_summary.failed_count_24h %}is-error{% endif %}">{{ ops_summary.failed_count_24h }}</strong>
</article>
<article class="ops-stat-card">
<span class="ops-stat-label">{% trans "Erfolgreiche Jobs (24h)" %}</span>
<strong>{{ ops_summary.success_count_24h }}</strong>
</article>
<article class="ops-stat-card">
<span class="ops-stat-label">{% trans "Offene Starts (24h)" %}</span>
<strong>{{ ops_summary.started_count_24h }}</strong>
</article>
{% endif %}
{% if ops_summary.can_manage_backups and ops_summary.backup_health %}
<article class="ops-stat-card">
<span class="ops-stat-label">{% trans "Backup-Status" %}</span>
<strong>{{ ops_summary.backup_health.label }}</strong>
<span class="mini">{{ ops_summary.backup_health.summary }}</span>
</article>
{% endif %}
</div>
{% if ops_summary.can_view_jobs and ops_summary.recent_failed_logs %}
<div class="ops-failure-list">
<div class="ops-failure-head">
<h3>{% trans "Letzte Fehler" %}</h3>
<a class="btn btn-secondary" href="/admin-tools/jobs/">{% trans "Job Monitor öffnen" %}</a>
</div>
<div class="ops-failure-items">
{% for log in ops_summary.recent_failed_logs %}
<article class="ops-failure-item">
<strong>{{ log.task_name }}</strong>
<span>{{ log.target_label|default:log.target_type }}</span>
<code>{{ log.error_message|truncatechars:120 }}</code>
</article>
{% endfor %}
</div>
</div>
{% endif %}
</section>
{% endif %}
{% for section in portal_app_sections %}
{% if not forloop.first %}
<div class="section-divider" aria-hidden="true"></div>

View File

@@ -14,6 +14,45 @@
{% include 'workflows/includes/messages.html' %}
<section class="card">
<div class="grid">
<div class="field">
<label>{% trans "Fehlgeschlagene Jobs (24h)" %}</label>
<div class="branding-inline-value">{{ job_summary.failed_count_24h }}</div>
</div>
<div class="field">
<label>{% trans "Erfolgreiche Jobs (24h)" %}</label>
<div class="branding-inline-value">{{ job_summary.success_count_24h }}</div>
</div>
<div class="field">
<label>{% trans "Offene Starts (24h)" %}</label>
<div class="branding-inline-value">{{ job_summary.started_count_24h }}</div>
</div>
</div>
{% if job_summary.recent_failed %}
<div class="table-wrap" style="margin-top:12px;">
<table>
<thead>
<tr>
<th>{% trans "Zuletzt fehlgeschlagen" %}</th>
<th>{% trans "Ziel" %}</th>
<th>{% trans "Fehler" %}</th>
</tr>
</thead>
<tbody>
{% for log in job_summary.recent_failed %}
<tr>
<td>{{ log.task_name }}</td>
<td>{{ log.target_label|default:log.target_type }}</td>
<td><code>{{ log.error_message|truncatechars:140 }}</code></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</section>
<section class="card">
<form method="get" class="app-registry-filters">
<div class="field">

View File

@@ -60,12 +60,20 @@
</div>
<div class="grid">
{% for field in section.fields %}
{% if field.field.widget.input_type == 'checkbox' %}
<div class="field inline-check field-full">
{{ field }} {{ field.label_tag }}
{% if field.help_text %}<div class="hint">{{ field.help_text }}</div>{% endif %}
{{ field.errors }}
</div>
{% else %}
<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>

View File

@@ -59,7 +59,7 @@
{% with field=block.field %}
{% if field.is_hidden %}
{{ field }}
{% elif field.name in onboarding_inline_checks %}
{% elif field.name in onboarding_inline_checks or field.field.widget.input_type == 'checkbox' %}
<div class="field inline-check field-full {% if section.key == 'abschluss' %}finish-check{% endif %}">
{{ field }} {{ field.label_tag }}
{% if field.help_text %}<div class="hint">{{ field.help_text }}</div>{% endif %}
@@ -102,7 +102,7 @@
{% for field in block.fields %}
{% if field.is_hidden %}
{{ field }}
{% elif field.name in onboarding_inline_checks %}
{% elif field.name in onboarding_inline_checks or field.field.widget.input_type == 'checkbox' %}
<div class="field inline-check field-full {% if section.key == 'abschluss' %}finish-check{% endif %}">
{{ field }} {{ field.label_tag }}
{% if field.help_text %}<div class="hint">{{ field.help_text }}</div>{% endif %}

View File

@@ -37,9 +37,15 @@
.timeline-detail-row { display:grid; grid-template-columns:160px 1fr; gap:12px; font-size:13px; }
.timeline-detail-row strong { color:#566886; }
.timeline-detail-list { margin:0; padding-left:18px; color:#4f617f; }
.timeline-custom-fields { margin: 0 0 20px; padding: 18px 20px; border: 1px solid #d9e3f8; border-radius: 20px; background: linear-gradient(180deg,#ffffff 0%,#f7faff 100%); box-shadow: 0 18px 40px rgba(23,39,90,.08); }
.timeline-custom-fields h2 { margin: 0 0 14px; font-size: 18px; color: #20345f; }
.timeline-custom-grid { display:grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 12px 16px; }
.timeline-custom-item { padding: 12px 14px; border: 1px solid #d8e1f5; border-radius: 16px; background: #fff; }
.timeline-custom-item strong { display:block; margin-bottom: 4px; color:#566886; font-size:12px; letter-spacing:.05em; text-transform:uppercase; }
.timeline-custom-item span { color:#22324d; font-size:14px; line-height:1.45; }
@media (max-width: 1160px) { .timeline-summary-grid { grid-template-columns:repeat(3, minmax(0,1fr)); } }
@media (max-width: 820px) { .timeline-summary-grid { grid-template-columns:repeat(2, minmax(0,1fr)); } }
@media (max-width: 700px) { .timeline-summary-grid { grid-template-columns:1fr; } .timeline-head { flex-direction:column; } .timeline-stamp { white-space:normal; } .timeline-detail-row { grid-template-columns:1fr; } }
@media (max-width: 700px) { .timeline-summary-grid { grid-template-columns:1fr; } .timeline-custom-grid { grid-template-columns:1fr; } .timeline-head { flex-direction:column; } .timeline-stamp { white-space:normal; } .timeline-detail-row { grid-template-columns:1fr; } }
</style>
{% endblock %}
@@ -80,6 +86,20 @@
</div>
</div>
{% if custom_field_details %}
<section class="timeline-custom-fields">
<h2>{% trans "Benutzerdefinierte Felder" %}</h2>
<div class="timeline-custom-grid">
{% for item in custom_field_details %}
<div class="timeline-custom-item">
<strong>{{ item.label }}</strong>
<span>{{ item.value }}</span>
</div>
{% endfor %}
</div>
</section>
{% endif %}
<div class="timeline-list">
{% for row in timeline_rows %}
<article class="timeline-item" data-kind="{{ row.kind }}">