snapshot: preserve builder deletion and onboarding ux improvements
This commit is contained in:
@@ -977,6 +977,14 @@ body {
|
|||||||
gap: 14px;
|
gap: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.builder-card-head-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
.builder-entity-card-head strong {
|
.builder-entity-card-head strong {
|
||||||
color: #142033;
|
color: #142033;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
@@ -1003,6 +1011,7 @@ body {
|
|||||||
.builder-switch-stack {
|
.builder-switch-stack {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
justify-items: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.builder-switch input[type='checkbox'],
|
.builder-switch input[type='checkbox'],
|
||||||
@@ -1031,61 +1040,62 @@ body {
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-rule-grid.drag-over {
|
|
||||||
outline: 1px dashed #9db4d2;
|
|
||||||
outline-offset: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-rule-card {
|
.section-rule-card {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 13px 14px;
|
padding: 14px 16px;
|
||||||
border: 1px solid #d6e0ec;
|
border: 1px solid #d6e0ec;
|
||||||
border-radius: 14px;
|
border-radius: 16px;
|
||||||
background: linear-gradient(180deg, #f9fbff, #ffffff);
|
background: linear-gradient(180deg, #fbfdff, #ffffff);
|
||||||
cursor: move;
|
|
||||||
transition: transform 0.16s ease, box-shadow 0.16s ease, border-color 0.16s ease;
|
transition: transform 0.16s ease, box-shadow 0.16s ease, border-color 0.16s ease;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-rule-card:hover {
|
.section-rule-card:hover {
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
box-shadow: 0 10px 20px rgba(15, 23, 42, 0.06);
|
box-shadow: 0 14px 24px rgba(15, 23, 42, 0.07);
|
||||||
border-color: #b8cae0;
|
border-color: #b2c6df;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-rule-card.is-locked {
|
.section-rule-card.is-locked {
|
||||||
background: linear-gradient(180deg, #f4f7fb, #fafcff);
|
background: linear-gradient(180deg, #f5f8fc, #fbfdff);
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-rule-card.dragging {
|
.section-rule-actions {
|
||||||
opacity: 0.58;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-rule-card.manual-dragging {
|
|
||||||
opacity: 0.72;
|
|
||||||
border-color: #8fb1d8;
|
|
||||||
box-shadow: 0 16px 30px rgba(15, 23, 42, 0.10);
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-rule-drag {
|
|
||||||
color: #8aa0be;
|
|
||||||
font-size: 18px;
|
|
||||||
letter-spacing: -2px;
|
|
||||||
user-select: none;
|
|
||||||
align-self: stretch;
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
padding-right: 2px;
|
padding-right: 2px;
|
||||||
cursor: grab;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body.builder-dragging,
|
.section-move-btn {
|
||||||
body.builder-dragging * {
|
width: 34px;
|
||||||
cursor: grabbing !important;
|
height: 34px;
|
||||||
user-select: none !important;
|
border: 1px solid #cdd9e8;
|
||||||
|
border-radius: 11px;
|
||||||
|
background: linear-gradient(180deg, #ffffff, #f5f9ff);
|
||||||
|
color: #274264;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 6px 12px rgba(15, 23, 42, 0.04);
|
||||||
|
transition: transform 0.16s ease, border-color 0.16s ease, background-color 0.16s ease, box-shadow 0.16s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-move-btn:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
border-color: #9db4d2;
|
||||||
|
background: linear-gradient(180deg, #ffffff, #eef5ff);
|
||||||
|
box-shadow: 0 10px 16px rgba(15, 23, 42, 0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-move-btn:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-rule-copy {
|
.section-rule-copy {
|
||||||
@@ -1096,7 +1106,8 @@ body.builder-dragging * {
|
|||||||
|
|
||||||
.section-rule-copy strong {
|
.section-rule-copy strong {
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
font-size: 14px;
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -192,6 +192,10 @@ h1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.section-head {
|
.section-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
border-bottom: 1px dashed #dde4f1;
|
border-bottom: 1px dashed #dde4f1;
|
||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
@@ -210,6 +214,9 @@ h1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.section-itsetup .section-head {
|
.section-itsetup .section-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
margin: -14px -14px 16px;
|
margin: -14px -14px 16px;
|
||||||
padding: 12px 14px;
|
padding: 12px 14px;
|
||||||
border-bottom: 1px solid #d5e2f9;
|
border-bottom: 1px solid #d5e2f9;
|
||||||
@@ -348,6 +355,11 @@ h1 {
|
|||||||
transform: translateY(1px);
|
transform: translateY(1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section-toggle-btn {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.itsetup-checklist-body {
|
.itsetup-checklist-body {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
|
|||||||
@@ -148,92 +148,36 @@
|
|||||||
|
|
||||||
const sectionRuleGrid = document.getElementById('section-rule-grid');
|
const sectionRuleGrid = document.getElementById('section-rule-grid');
|
||||||
if (sectionRuleGrid) {
|
if (sectionRuleGrid) {
|
||||||
let draggingSectionCard = null;
|
function updateSectionMoveButtons() {
|
||||||
let manualDraggingSectionCard = null;
|
const cards = Array.from(sectionRuleGrid.querySelectorAll('.section-rule-card'));
|
||||||
|
cards.forEach((card, index) => {
|
||||||
function getSectionInsertBeforeNode(mouseY) {
|
const upBtn = card.querySelector('[data-move-section="up"]');
|
||||||
const cards = Array.from(sectionRuleGrid.querySelectorAll('.section-rule-card:not(.dragging)'));
|
const downBtn = card.querySelector('[data-move-section="down"]');
|
||||||
return cards.find((card) => {
|
if (upBtn) upBtn.disabled = index === 0;
|
||||||
const box = card.getBoundingClientRect();
|
if (downBtn) downBtn.disabled = index === cards.length - 1;
|
||||||
return mouseY < box.top + box.height / 2;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getManualSectionInsertBeforeNode(mouseY) {
|
sectionRuleGrid.addEventListener('click', (event) => {
|
||||||
const cards = Array.from(sectionRuleGrid.querySelectorAll('.section-rule-card:not(.manual-dragging)'));
|
const button = event.target.closest('[data-move-section]');
|
||||||
return cards.find((card) => {
|
if (!button) return;
|
||||||
const box = card.getBoundingClientRect();
|
const card = button.closest('.section-rule-card');
|
||||||
return mouseY < box.top + box.height / 2;
|
if (!card) return;
|
||||||
});
|
const direction = button.dataset.moveSection;
|
||||||
}
|
if (direction === 'up') {
|
||||||
|
const previousCard = card.previousElementSibling;
|
||||||
function onManualSectionMove(event) {
|
if (previousCard) {
|
||||||
if (!manualDraggingSectionCard) return;
|
sectionRuleGrid.insertBefore(card, previousCard);
|
||||||
event.preventDefault();
|
}
|
||||||
sectionRuleGrid.classList.add('drag-over');
|
} else if (direction === 'down') {
|
||||||
const beforeNode = getManualSectionInsertBeforeNode(event.clientY);
|
const nextCard = card.nextElementSibling;
|
||||||
if (beforeNode) {
|
if (nextCard) {
|
||||||
sectionRuleGrid.insertBefore(manualDraggingSectionCard, beforeNode);
|
sectionRuleGrid.insertBefore(nextCard, card);
|
||||||
} else {
|
}
|
||||||
sectionRuleGrid.appendChild(manualDraggingSectionCard);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function endManualSectionDrag() {
|
|
||||||
if (!manualDraggingSectionCard) return;
|
|
||||||
manualDraggingSectionCard.classList.remove('manual-dragging');
|
|
||||||
manualDraggingSectionCard = null;
|
|
||||||
sectionRuleGrid.classList.remove('drag-over');
|
|
||||||
document.body.classList.remove('builder-dragging');
|
|
||||||
document.removeEventListener('mousemove', onManualSectionMove);
|
|
||||||
document.removeEventListener('mouseup', endManualSectionDrag);
|
|
||||||
}
|
|
||||||
|
|
||||||
sectionRuleGrid.querySelectorAll('.section-rule-card').forEach((card) => {
|
|
||||||
card.addEventListener('dragstart', (event) => {
|
|
||||||
draggingSectionCard = card;
|
|
||||||
card.classList.add('dragging');
|
|
||||||
event.dataTransfer.effectAllowed = 'move';
|
|
||||||
event.dataTransfer.setData('text/plain', card.dataset.sectionKey || '');
|
|
||||||
});
|
|
||||||
card.addEventListener('dragend', () => {
|
|
||||||
card.classList.remove('dragging');
|
|
||||||
sectionRuleGrid.classList.remove('drag-over');
|
|
||||||
draggingSectionCard = null;
|
|
||||||
});
|
|
||||||
const handle = card.querySelector('.section-rule-drag');
|
|
||||||
if (handle) {
|
|
||||||
handle.addEventListener('mousedown', (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
manualDraggingSectionCard = card;
|
|
||||||
card.classList.add('manual-dragging');
|
|
||||||
sectionRuleGrid.classList.add('drag-over');
|
|
||||||
document.body.classList.add('builder-dragging');
|
|
||||||
document.addEventListener('mousemove', onManualSectionMove);
|
|
||||||
document.addEventListener('mouseup', endManualSectionDrag);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
updateSectionMoveButtons();
|
||||||
});
|
});
|
||||||
|
|
||||||
sectionRuleGrid.addEventListener('dragover', (event) => {
|
updateSectionMoveButtons();
|
||||||
event.preventDefault();
|
|
||||||
sectionRuleGrid.classList.add('drag-over');
|
|
||||||
if (!draggingSectionCard) return;
|
|
||||||
const beforeNode = getSectionInsertBeforeNode(event.clientY);
|
|
||||||
if (beforeNode) {
|
|
||||||
sectionRuleGrid.insertBefore(draggingSectionCard, beforeNode);
|
|
||||||
} else {
|
|
||||||
sectionRuleGrid.appendChild(draggingSectionCard);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
sectionRuleGrid.addEventListener('dragleave', () => {
|
|
||||||
sectionRuleGrid.classList.remove('drag-over');
|
|
||||||
});
|
|
||||||
|
|
||||||
sectionRuleGrid.addEventListener('drop', (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
sectionRuleGrid.classList.remove('drag-over');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -222,6 +222,45 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setupSectionCheckboxToggles() {
|
||||||
|
document.querySelectorAll('[data-section-checkbox-toggle]').forEach(function (button) {
|
||||||
|
const sectionCard = button.closest('.section-card');
|
||||||
|
if (!sectionCard) return;
|
||||||
|
|
||||||
|
const getCheckboxes = function () {
|
||||||
|
return Array.from(sectionCard.querySelectorAll('.custom-section-checkbox input[type="checkbox"]'));
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshButtonLabel = function () {
|
||||||
|
const checkboxes = getCheckboxes();
|
||||||
|
if (!checkboxes.length) {
|
||||||
|
button.classList.add('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
button.classList.remove('hidden');
|
||||||
|
const allChecked = checkboxes.every(function (box) { return box.checked; });
|
||||||
|
button.textContent = allChecked ? (button.dataset.labelClear || 'Auswahl aufheben') : (button.dataset.labelSelect || 'Alle auswählen');
|
||||||
|
};
|
||||||
|
|
||||||
|
button.addEventListener('click', function () {
|
||||||
|
const checkboxes = getCheckboxes();
|
||||||
|
if (!checkboxes.length) return;
|
||||||
|
const shouldCheck = checkboxes.some(function (box) { return !box.checked; });
|
||||||
|
checkboxes.forEach(function (box) {
|
||||||
|
box.checked = shouldCheck;
|
||||||
|
box.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
});
|
||||||
|
refreshButtonLabel();
|
||||||
|
});
|
||||||
|
|
||||||
|
getCheckboxes().forEach(function (box) {
|
||||||
|
box.addEventListener('change', refreshButtonLabel);
|
||||||
|
});
|
||||||
|
|
||||||
|
refreshButtonLabel();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function setupChecklistColumns() {
|
function setupChecklistColumns() {
|
||||||
document.querySelectorAll('.itsetup-checklist-body > [id^="id_"]').forEach(function (container) {
|
document.querySelectorAll('.itsetup-checklist-body > [id^="id_"]').forEach(function (container) {
|
||||||
const itemCount = container.querySelectorAll(':scope > div').length;
|
const itemCount = container.querySelectorAll(':scope > div').length;
|
||||||
@@ -271,6 +310,7 @@
|
|||||||
setupWorkEmailAutofill();
|
setupWorkEmailAutofill();
|
||||||
setupBusinessCardAutofill();
|
setupBusinessCardAutofill();
|
||||||
setupChecklistToggles();
|
setupChecklistToggles();
|
||||||
|
setupSectionCheckboxToggles();
|
||||||
setupChecklistColumns();
|
setupChecklistColumns();
|
||||||
jumpToFirstErrorPage();
|
jumpToFirstErrorPage();
|
||||||
updateStep();
|
updateStep();
|
||||||
|
|||||||
@@ -98,11 +98,17 @@
|
|||||||
{% for section in section_rule_items %}
|
{% for section in section_rule_items %}
|
||||||
<article
|
<article
|
||||||
class="section-rule-card{% if section.locked %} is-locked{% endif %}"
|
class="section-rule-card{% if section.locked %} is-locked{% endif %}"
|
||||||
draggable="true"
|
|
||||||
data-section-key="{{ section.key }}"
|
data-section-key="{{ section.key }}"
|
||||||
>
|
>
|
||||||
<input type="hidden" name="section_order" value="{{ section.key }}" />
|
<input type="hidden" name="section_order" value="{{ section.key }}" />
|
||||||
<div class="section-rule-drag" aria-hidden="true" title="{% trans 'Zum Verschieben ziehen' %}">⋮⋮</div>
|
<div class="section-rule-actions">
|
||||||
|
<button class="section-move-btn" type="button" data-move-section="up" aria-label="{% trans 'Nach oben' %}" title="{% trans 'Nach oben' %}">
|
||||||
|
<span aria-hidden="true">↑</span>
|
||||||
|
</button>
|
||||||
|
<button class="section-move-btn" type="button" data-move-section="down" aria-label="{% trans 'Nach unten' %}" title="{% trans 'Nach unten' %}">
|
||||||
|
<span aria-hidden="true">↓</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div class="section-rule-copy">
|
<div class="section-rule-copy">
|
||||||
<strong>{{ section.title }}</strong>
|
<strong>{{ section.title }}</strong>
|
||||||
<span>{% blocktrans trimmed with count=section.field_count %}{{ count }} Feld/Felder in diesem Abschnitt.{% endblocktrans %}</span>
|
<span>{% blocktrans trimmed with count=section.field_count %}{{ count }} Feld/Felder in diesem Abschnitt.{% endblocktrans %}</span>
|
||||||
@@ -491,10 +497,13 @@
|
|||||||
<strong>{{ item.title }}</strong>
|
<strong>{{ item.title }}</strong>
|
||||||
<div class="entity-meta">{{ item.section_key }}</div>
|
<div class="entity-meta">{{ item.section_key }}</div>
|
||||||
</div>
|
</div>
|
||||||
<label class="builder-switch">
|
<div class="builder-card-head-actions">
|
||||||
<input type="checkbox" name="custom_section_is_active_{{ item.id }}" {% if item.is_active %}checked{% endif %} />
|
<label class="builder-switch">
|
||||||
<span>{% trans "Aktiv" %}</span>
|
<input type="checkbox" name="custom_section_is_active_{{ item.id }}" {% if item.is_active %}checked{% endif %} />
|
||||||
</label>
|
<span>{% trans "Aktiv" %}</span>
|
||||||
|
</label>
|
||||||
|
<button class="btn btn-secondary" type="submit" name="delete_custom_section_id" value="{{ item.id }}" data-confirm="{% trans 'Eigenen Abschnitt wirklich löschen? Zugehörige eigene Felder werden ebenfalls entfernt.' %}">{% trans "Löschen" %}</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="builder-entity-grid">
|
<div class="builder-entity-grid">
|
||||||
<label class="builder-entity-control">
|
<label class="builder-entity-control">
|
||||||
@@ -613,7 +622,7 @@
|
|||||||
<strong>{{ item.label }}</strong>
|
<strong>{{ item.label }}</strong>
|
||||||
<div class="entity-meta">{{ item.field_key }}</div>
|
<div class="entity-meta">{{ item.field_key }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="builder-switch-stack">
|
<div class="builder-switch-stack">
|
||||||
<label class="builder-switch">
|
<label class="builder-switch">
|
||||||
<input type="checkbox" name="custom_is_required_{{ item.id }}" {% if item.is_required %}checked{% endif %} />
|
<input type="checkbox" name="custom_is_required_{{ item.id }}" {% if item.is_required %}checked{% endif %} />
|
||||||
<span>{% trans "Pflicht" %}</span>
|
<span>{% trans "Pflicht" %}</span>
|
||||||
@@ -622,8 +631,9 @@
|
|||||||
<input type="checkbox" name="custom_is_active_{{ item.id }}" {% if item.is_active %}checked{% endif %} />
|
<input type="checkbox" name="custom_is_active_{{ item.id }}" {% if item.is_active %}checked{% endif %} />
|
||||||
<span>{% trans "Aktiv" %}</span>
|
<span>{% trans "Aktiv" %}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
<div class="builder-entity-grid">
|
<div class="builder-entity-grid">
|
||||||
<label class="builder-entity-control">
|
<label class="builder-entity-control">
|
||||||
<span>{% trans "Abschnitt" %}</span>
|
<span>{% trans "Abschnitt" %}</span>
|
||||||
@@ -670,9 +680,6 @@
|
|||||||
<textarea name="custom_select_options_en_{{ item.id }}" rows="2">{{ item.select_options_en }}</textarea>
|
<textarea name="custom_select_options_en_{{ item.id }}" rows="2">{{ item.select_options_en }}</textarea>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="builder-entity-card-actions">
|
|
||||||
<button class="btn btn-secondary" type="submit" name="delete_custom_field_id" value="{{ item.id }}" data-confirm="{% trans 'Eigenes Feld wirklich löschen?' %}">{% trans "Löschen" %}</button>
|
|
||||||
</div>
|
|
||||||
</article>
|
</article>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<div class="builder-empty-state">{% trans "Keine eigenen Felder vorhanden." %}</div>
|
<div class="builder-empty-state">{% trans "Keine eigenen Felder vorhanden." %}</div>
|
||||||
|
|||||||
@@ -50,8 +50,19 @@
|
|||||||
<section class="page {% if forloop.first %}active{% endif %}" data-step="{{ forloop.counter }}">
|
<section class="page {% if forloop.first %}active{% endif %}" data-step="{{ forloop.counter }}">
|
||||||
<div class="section-card section-{{ section.key }}">
|
<div class="section-card section-{{ section.key }}">
|
||||||
<div class="section-head">
|
<div class="section-head">
|
||||||
<h2>{{ section.title }}</h2>
|
<div>
|
||||||
<p>{{ section.subtitle }}</p>
|
<h2>{{ section.title }}</h2>
|
||||||
|
<p>{{ section.subtitle }}</p>
|
||||||
|
</div>
|
||||||
|
{% if section.has_custom_checkbox_fields %}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="checklist-toggle-btn section-toggle-btn"
|
||||||
|
data-section-checkbox-toggle
|
||||||
|
data-label-select="{% trans 'Alle auswählen' %}"
|
||||||
|
data-label-clear="{% trans 'Auswahl aufheben' %}"
|
||||||
|
>{% trans "Alle auswählen" %}</button>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="grid-2">
|
<div class="grid-2">
|
||||||
{% for block in section.blocks %}
|
{% for block in section.blocks %}
|
||||||
@@ -78,7 +89,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% elif field.name in onboarding_inline_checks or field.field.widget.input_type == 'checkbox' %}
|
{% 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 %}">
|
<div class="field inline-check field-full {% if section.key == 'abschluss' %}finish-check{% endif %} {% if field.name|slice:':8' == 'custom__' and field.field.widget.input_type == 'checkbox' %}custom-section-checkbox{% endif %}">
|
||||||
{{ field }} {{ field.label_tag }}
|
{{ field }} {{ field.label_tag }}
|
||||||
{% 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 }}
|
||||||
@@ -121,7 +132,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% elif field.name in onboarding_inline_checks or field.field.widget.input_type == 'checkbox' %}
|
{% 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 %}">
|
<div class="field inline-check field-full {% if section.key == 'abschluss' %}finish-check{% endif %} {% if field.name|slice:':8' == 'custom__' and field.field.widget.input_type == 'checkbox' %}custom-section-checkbox{% endif %}">
|
||||||
{{ field }} {{ field.label_tag }}
|
{{ field }} {{ field.label_tag }}
|
||||||
{% 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 }}
|
||||||
|
|||||||
@@ -331,6 +331,60 @@ class FormBuilderAdminTests(TestCase):
|
|||||||
self.assertEqual(section.title, 'Benefits')
|
self.assertEqual(section.title, 'Benefits')
|
||||||
self.assertEqual(section.sort_order, 5)
|
self.assertEqual(section.sort_order, 5)
|
||||||
|
|
||||||
|
def test_staff_can_delete_custom_field(self):
|
||||||
|
self.client.force_login(self.staff)
|
||||||
|
field = FormCustomFieldConfig.objects.create(
|
||||||
|
form_type='onboarding',
|
||||||
|
field_key='laptop_tag',
|
||||||
|
section_key='itsetup',
|
||||||
|
sort_order=0,
|
||||||
|
field_type='text',
|
||||||
|
label='Laptop-Tag',
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
'/admin-tools/form-builder/?form_type=onboarding&option_category=device',
|
||||||
|
data={'delete_custom_field_id': str(field.id)},
|
||||||
|
HTTP_HOST='localhost',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertFalse(FormCustomFieldConfig.objects.filter(id=field.id).exists())
|
||||||
|
|
||||||
|
def test_staff_can_delete_custom_section_and_its_fields(self):
|
||||||
|
self.client.force_login(self.staff)
|
||||||
|
section = FormCustomSectionConfig.objects.create(
|
||||||
|
form_type='onboarding',
|
||||||
|
section_key='benefits',
|
||||||
|
sort_order=0,
|
||||||
|
title='Benefits',
|
||||||
|
)
|
||||||
|
field = FormCustomFieldConfig.objects.create(
|
||||||
|
form_type='onboarding',
|
||||||
|
field_key='meal_allowance',
|
||||||
|
section_key='benefits',
|
||||||
|
sort_order=0,
|
||||||
|
field_type='checkbox',
|
||||||
|
label='Essenszuschuss',
|
||||||
|
)
|
||||||
|
FormConditionalRuleConfig.objects.create(
|
||||||
|
form_type='onboarding',
|
||||||
|
target_key='custom__meal_allowance',
|
||||||
|
clauses=[{'field': 'employment_type', 'operator': 'equals', 'value': 'unbefristet'}],
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
'/admin-tools/form-builder/?form_type=onboarding&option_category=device',
|
||||||
|
data={'delete_custom_section_id': str(section.id)},
|
||||||
|
HTTP_HOST='localhost',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertFalse(FormCustomSectionConfig.objects.filter(id=section.id).exists())
|
||||||
|
self.assertFalse(FormCustomFieldConfig.objects.filter(id=field.id).exists())
|
||||||
|
self.assertFalse(FormConditionalRuleConfig.objects.filter(target_key='custom__meal_allowance').exists())
|
||||||
|
|
||||||
def test_save_order_accepts_custom_section_column(self):
|
def test_save_order_accepts_custom_section_column(self):
|
||||||
self.client.force_login(self.staff)
|
self.client.force_login(self.staff)
|
||||||
FormCustomSectionConfig.objects.create(
|
FormCustomSectionConfig.objects.create(
|
||||||
|
|||||||
@@ -226,6 +226,41 @@ class OnboardingFlowTests(TestCase):
|
|||||||
self.assertIn('Benefits', html)
|
self.assertIn('Benefits', html)
|
||||||
self.assertIn('Essenszuschuss', html)
|
self.assertIn('Essenszuschuss', html)
|
||||||
|
|
||||||
|
def test_onboarding_custom_section_with_checkbox_fields_shows_section_select_all(self):
|
||||||
|
FormCustomSectionConfig.objects.create(
|
||||||
|
form_type='onboarding',
|
||||||
|
section_key='benefits',
|
||||||
|
sort_order=10,
|
||||||
|
title='Benefits',
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
FormCustomFieldConfig.objects.create(
|
||||||
|
form_type='onboarding',
|
||||||
|
field_key='meal_allowance',
|
||||||
|
section_key='benefits',
|
||||||
|
sort_order=0,
|
||||||
|
field_type='checkbox',
|
||||||
|
is_active=True,
|
||||||
|
label='Essenszuschuss',
|
||||||
|
)
|
||||||
|
FormCustomFieldConfig.objects.create(
|
||||||
|
form_type='onboarding',
|
||||||
|
field_key='parking_spot',
|
||||||
|
section_key='benefits',
|
||||||
|
sort_order=1,
|
||||||
|
field_type='checkbox',
|
||||||
|
is_active=True,
|
||||||
|
label='Parkplatz',
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.get('/onboarding/new/', HTTP_HOST='localhost')
|
||||||
|
html = response.content.decode('utf-8')
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertIn('data-section-checkbox-toggle', html)
|
||||||
|
self.assertIn('Essenszuschuss', html)
|
||||||
|
self.assertIn('Parkplatz', html)
|
||||||
|
|
||||||
@patch('workflows.views.process_onboarding_request.delay')
|
@patch('workflows.views.process_onboarding_request.delay')
|
||||||
def test_onboarding_custom_field_is_rendered_and_saved(self, mock_delay):
|
def test_onboarding_custom_field_is_rendered_and_saved(self, mock_delay):
|
||||||
FormCustomFieldConfig.objects.create(
|
FormCustomFieldConfig.objects.create(
|
||||||
|
|||||||
@@ -610,16 +610,33 @@ def _build_onboarding_sections(blocks: list[dict], field_pages: dict[str, str],
|
|||||||
section_key = 'abschluss'
|
section_key = 'abschluss'
|
||||||
grouped[section_key].append(block)
|
grouped[section_key].append(block)
|
||||||
visible_keys = visible_section_keys or set(section_order)
|
visible_keys = visible_section_keys or set(section_order)
|
||||||
return [
|
sections = []
|
||||||
{
|
custom_section_keys = {item['key'] for item in section_defs if item.get('is_custom')}
|
||||||
'key': key,
|
for key in section_order:
|
||||||
'title': section_titles.get(key, ONBOARDING_SECTION_META.get(key, {}).get('title', key)),
|
if key not in visible_keys:
|
||||||
'subtitle': ONBOARDING_SECTION_META.get(key, {}).get('subtitle', ''),
|
continue
|
||||||
'blocks': grouped[key],
|
blocks_for_section = grouped[key]
|
||||||
}
|
has_custom_checkbox_fields = False
|
||||||
for key in section_order
|
for block in blocks_for_section:
|
||||||
if key in visible_keys
|
candidate_fields = [block['field']] if block['kind'] == 'field' else (block.get('fields') or [])
|
||||||
]
|
for bound_field in candidate_fields:
|
||||||
|
widget_type = getattr(getattr(bound_field.field, 'widget', None), 'input_type', '')
|
||||||
|
if bound_field.name.startswith('custom__') and widget_type == 'checkbox':
|
||||||
|
has_custom_checkbox_fields = True
|
||||||
|
break
|
||||||
|
if has_custom_checkbox_fields:
|
||||||
|
break
|
||||||
|
sections.append(
|
||||||
|
{
|
||||||
|
'key': key,
|
||||||
|
'title': section_titles.get(key, ONBOARDING_SECTION_META.get(key, {}).get('title', key)),
|
||||||
|
'subtitle': ONBOARDING_SECTION_META.get(key, {}).get('subtitle', ''),
|
||||||
|
'blocks': blocks_for_section,
|
||||||
|
'is_custom': key in custom_section_keys,
|
||||||
|
'has_custom_checkbox_fields': has_custom_checkbox_fields,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return sections
|
||||||
|
|
||||||
|
|
||||||
OFFBOARDING_SECTION_META = {
|
OFFBOARDING_SECTION_META = {
|
||||||
@@ -2182,6 +2199,7 @@ def form_builder_page(request):
|
|||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
delete_option_id = request.POST.get('delete_option_id', '').strip()
|
delete_option_id = request.POST.get('delete_option_id', '').strip()
|
||||||
delete_custom_field_id = request.POST.get('delete_custom_field_id', '').strip()
|
delete_custom_field_id = request.POST.get('delete_custom_field_id', '').strip()
|
||||||
|
delete_custom_section_id = request.POST.get('delete_custom_section_id', '').strip()
|
||||||
if delete_option_id:
|
if delete_option_id:
|
||||||
option = FormOption.objects.filter(id=delete_option_id).first()
|
option = FormOption.objects.filter(id=delete_option_id).first()
|
||||||
if not option:
|
if not option:
|
||||||
@@ -2205,6 +2223,34 @@ def form_builder_page(request):
|
|||||||
_audit(request, 'form_custom_field_deleted', target_type='form_custom_field', target_id=deleted_id, target_label=deleted_label)
|
_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}&panel=builder-content&subpanel=custom-fields#builder-content")
|
return redirect(f"/admin-tools/form-builder/?form_type={form_type}&option_category={option_category}&panel=builder-content&subpanel=custom-fields#builder-content")
|
||||||
|
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.')
|
||||||
|
else:
|
||||||
|
deleted_label = custom_section.title
|
||||||
|
deleted_id = custom_section.id
|
||||||
|
section_key = custom_section.section_key
|
||||||
|
custom_fields = list(FormCustomFieldConfig.objects.filter(form_type=form_type, section_key=section_key))
|
||||||
|
deleted_field_count = len(custom_fields)
|
||||||
|
if custom_fields:
|
||||||
|
field_keys = [item.field_key for item in custom_fields]
|
||||||
|
FormConditionalRuleConfig.objects.filter(
|
||||||
|
form_type=form_type,
|
||||||
|
target_key__in=[f'custom__{field_key}' for field_key in field_keys],
|
||||||
|
).delete()
|
||||||
|
FormCustomFieldConfig.objects.filter(id__in=[item.id for item in custom_fields]).delete()
|
||||||
|
custom_section.delete()
|
||||||
|
_audit(
|
||||||
|
request,
|
||||||
|
'form_custom_section_deleted',
|
||||||
|
target_type='form_custom_section',
|
||||||
|
target_id=deleted_id,
|
||||||
|
target_label=deleted_label,
|
||||||
|
details={'section_key': section_key, 'deleted_field_count': deleted_field_count},
|
||||||
|
)
|
||||||
|
messages.success(request, 'Benutzerdefinierter Abschnitt wurde gelöscht.')
|
||||||
|
return redirect(f"/admin-tools/form-builder/?form_type={form_type}&option_category={option_category}&panel=builder-content&subpanel=custom-sections#builder-content")
|
||||||
|
|
||||||
action = request.POST.get('builder_action', '')
|
action = request.POST.get('builder_action', '')
|
||||||
if action == 'add_option':
|
if action == 'add_option':
|
||||||
|
|||||||
Reference in New Issue
Block a user