snapshot: preserve app state before onboarding pdf layout restore

This commit is contained in:
Md Bayazid Bostame
2026-03-27 23:52:37 +01:00
parent 631886a763
commit 5cb7ef78f8
13 changed files with 318 additions and 306 deletions

View File

@@ -134,9 +134,6 @@ textarea { min-height: 120px; font-family: ui-monospace, SFMono-Regular, Menlo,
.hint { margin-top: 6px; color: #64748b; font-size: 12px; } .hint { margin-top: 6px; color: #64748b; font-size: 12px; }
.toolbar { display: flex; justify-content: space-between; align-items: center; gap: 10px; margin-bottom: 10px; flex-wrap: wrap; } .toolbar { display: flex; justify-content: space-between; align-items: center; gap: 10px; margin-bottom: 10px; flex-wrap: wrap; }
.switch { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; } .switch { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
.switch .tab { border: 1px solid #c9d6e7; border-radius: 999px; padding: 8px 14px; text-decoration: none; color: #1f2f49; font-weight: 700; background: #f6f9ff; transition: background-color 180ms cubic-bezier(0.2, 0.8, 0.2, 1), border-color 180ms cubic-bezier(0.2, 0.8, 0.2, 1), color 180ms cubic-bezier(0.2, 0.8, 0.2, 1), transform 180ms cubic-bezier(0.2, 0.8, 0.2, 1), box-shadow 180ms cubic-bezier(0.2, 0.8, 0.2, 1); }
.switch .tab.active { background: #000078; color: #fff; border-color: #000078; }
.switch .tab:hover { transform: translateY(-1px); box-shadow: 0 8px 16px rgba(16, 32, 57, 0.06); }
.check-row { margin-top: 8px; display: flex; gap: 12px; flex-wrap: wrap; } .check-row { margin-top: 8px; display: flex; gap: 12px; flex-wrap: wrap; }
.check-row label { display: inline-flex; align-items: center; gap: 6px; margin: 0; font-size: 13px; } .check-row label { display: inline-flex; align-items: center; gap: 6px; margin: 0; font-size: 13px; }
.check-row input[type="checkbox"] { width: auto; } .check-row input[type="checkbox"] { width: auto; }

View File

@@ -150,6 +150,183 @@ body {
gap: 14px; gap: 14px;
} }
.app-workspace {
display: grid;
grid-template-columns: 280px minmax(0, 1fr);
gap: 18px;
align-items: start;
padding: 24px;
}
.app-sidebar {
position: sticky;
top: 18px;
display: grid;
gap: 14px;
}
.app-sidebar-card {
padding: 16px;
border: 1px solid rgba(216, 226, 239, 0.94);
border-radius: 18px;
background:
radial-gradient(120% 120% at 100% 0%, rgba(31, 79, 214, 0.05), rgba(31, 79, 214, 0)),
var(--ds-surface-soft);
box-shadow: inset 0 1px 0 rgba(255,255,255,0.94), var(--ds-shadow-card);
}
.app-sidebar-card h1,
.app-sidebar-card h2 {
margin: 6px 0 0;
color: var(--ds-ink-strong);
line-height: 1.08;
}
.app-sidebar-card p {
margin: 8px 0 0;
color: var(--ds-muted);
font-size: 13px;
line-height: 1.55;
}
.app-sidebar-eyebrow {
display: inline-flex;
align-items: center;
min-height: 24px;
padding: 0 9px;
border-radius: 999px;
background: rgba(0, 0, 120, 0.07);
color: var(--ds-brand-strong);
font-size: 11px;
font-weight: 800;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.app-side-nav {
display: grid;
gap: 8px;
}
.app-side-link {
display: grid;
gap: 4px;
padding: 14px 16px;
border: 1px solid rgba(216, 226, 239, 0.94);
border-radius: 16px;
background: linear-gradient(180deg, #fbfdff, #ffffff);
color: var(--ds-ink-strong);
text-decoration: none;
transition:
transform var(--ds-motion-fast) var(--ds-ease),
border-color var(--ds-motion-fast) var(--ds-ease),
box-shadow var(--ds-motion-fast) var(--ds-ease),
background var(--ds-motion-fast) var(--ds-ease);
}
.app-side-link:hover {
transform: translateY(-1px);
border-color: #b8cae0;
box-shadow: var(--ds-shadow-hover);
}
.app-side-link.is-active {
border-color: #9eb6d8;
background: linear-gradient(180deg, #eef5ff, #ffffff);
box-shadow: 0 12px 24px rgba(15, 23, 42, 0.08);
}
.app-side-link-title {
font-size: 14px;
font-weight: 800;
}
.app-side-link-meta {
color: var(--ds-muted);
font-size: 12px;
font-weight: 700;
}
.app-sidebar-stats {
display: grid;
gap: 12px;
}
.app-side-stat {
display: grid;
gap: 2px;
}
.app-side-stat strong {
font-size: 22px;
line-height: 1;
color: #163566;
}
.app-side-stat span {
color: var(--ds-muted);
font-size: 12px;
font-weight: 700;
}
.app-flow-list {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 8px;
}
.app-flow-item {
display: flex;
gap: 10px;
align-items: flex-start;
padding: 10px;
border: 1px solid rgba(216, 226, 239, 0.94);
border-radius: 14px;
background: linear-gradient(160deg, #f8faff, #fcfdff);
transition:
border-color var(--ds-motion-fast) var(--ds-ease),
transform var(--ds-motion-fast) var(--ds-ease),
box-shadow var(--ds-motion-fast) var(--ds-ease);
}
.app-flow-item.is-active {
border-color: #9db4ff;
background: linear-gradient(160deg, #eaf0ff, #f4f7ff);
box-shadow: 0 6px 16px rgba(0, 0, 120, 0.08);
}
.app-flow-item:hover {
border-color: #b2c3ff;
}
.app-flow-dot {
width: 24px;
height: 24px;
border-radius: 999px;
display: inline-flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 120, 0.06);
border: 1px solid rgba(0, 0, 120, 0.14);
color: var(--ds-brand);
font-size: 12px;
font-weight: 700;
flex: 0 0 auto;
}
.app-flow-title {
font-weight: 700;
color: #1d2c68;
margin-bottom: 2px;
}
.app-flow-sub {
font-size: 12px;
color: var(--ds-muted);
}
.metric-grid { .metric-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
@@ -226,6 +403,46 @@ table th {
align-items: center; align-items: center;
} }
.app-module-nav {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.app-module-link,
.tab {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 36px;
padding: 0 14px;
border: 1px solid #c6d1e1;
border-radius: 999px;
text-decoration: none;
color: #1c2a41;
background: #f8fbff;
font-weight: 700;
transition:
transform var(--ds-motion-fast) var(--ds-ease),
border-color var(--ds-motion-fast) var(--ds-ease),
background var(--ds-motion-fast) var(--ds-ease),
box-shadow var(--ds-motion-fast) var(--ds-ease);
}
.app-module-link:hover,
.tab:hover {
transform: translateY(-1px);
border-color: #9db4d2;
box-shadow: 0 8px 16px rgba(16, 32, 57, 0.06);
}
.app-module-link.is-active,
.tab.active {
background: linear-gradient(135deg, #0f3b7a 0%, #1759b8 100%);
color: #fff;
border-color: #1759b8;
}
.inline-action-form { .inline-action-form {
display: inline; display: inline;
} }
@@ -259,6 +476,17 @@ table th {
color: #8e1e1e; color: #8e1e1e;
} }
@media (max-width: 980px) {
.app-workspace {
grid-template-columns: 1fr;
padding: 18px;
}
.app-sidebar {
position: static;
}
}
.status-note-error + .status-note-error { .status-note-error + .status-note-error {
margin-top: 6px; margin-top: 6px;
} }

View File

@@ -1,108 +1,38 @@
:root {
--off-ink: #182233;
--off-muted: #5e6f85;
--off-brand: #000078;
--off-brand-soft: #eef1ff;
--off-line: #d7dfeb;
--off-card: #ffffff;
}
.offboarding-shell-body {
display: grid;
grid-template-columns: 290px 1fr;
gap: 16px;
padding: 18px;
}
.offboarding-panel,
.offboarding-main, .offboarding-main,
.offboarding-search-card, .offboarding-search-card,
.offboarding-section-card { .offboarding-section-card {
background: var(--off-card); background: #ffffff;
border: 1px solid var(--off-line); border: 1px solid #d7dfeb;
box-shadow: 0 12px 28px rgba(30, 52, 87, 0.08); box-shadow: 0 12px 28px rgba(30, 52, 87, 0.08);
} }
.offboarding-panel,
.offboarding-search-card, .offboarding-search-card,
.offboarding-section-card { .offboarding-section-card {
border-radius: 16px; border-radius: 16px;
} }
.offboarding-panel { .workflow-sidebar-card h1 {
padding: 18px;
height: fit-content;
position: sticky;
top: 20px;
}
.offboarding-main {
padding: 22px;
border-radius: 16px;
background: linear-gradient(180deg, #ffffff 0%, #fbfdff 100%);
}
.offboarding-panel h1 {
margin: 0 0 8px; margin: 0 0 8px;
font-size: 28px; font-size: 28px;
letter-spacing: -0.02em; letter-spacing: -0.02em;
color: var(--off-ink);
} }
.offboarding-sub { .offboarding-sub {
margin: 0 0 16px; margin: 0 0 16px;
color: var(--off-muted);
font-size: 14px; font-size: 14px;
line-height: 1.55; line-height: 1.55;
} }
.offboarding-step-list {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 8px;
}
.offboarding-step-item {
display: flex;
gap: 10px;
align-items: flex-start;
border: 1px solid #d8e0f4;
border-radius: 12px;
padding: 10px;
background: linear-gradient(160deg, #f8faff, #fcfdff);
}
.offboarding-dot {
width: 24px;
height: 24px;
border-radius: 999px;
display: inline-flex;
align-items: center;
justify-content: center;
background: var(--off-brand-soft);
border: 1px solid #c4cdf7;
color: var(--off-brand);
font-size: 12px;
font-weight: 700;
}
.offboarding-step-title {
font-weight: 700;
color: #1d2c68;
margin-bottom: 2px;
}
.offboarding-step-sub {
font-size: 12px;
color: var(--off-muted);
}
.offboarding-main form { .offboarding-main form {
margin: 0; margin: 0;
} }
.workflow-form-main.offboarding-main {
padding: 22px;
border-radius: 16px;
background: linear-gradient(180deg, #ffffff 0%, #fbfdff 100%);
}
.offboarding-search-card { .offboarding-search-card {
padding: 14px; padding: 14px;
margin-bottom: 16px; margin-bottom: 16px;
@@ -269,16 +199,6 @@ textarea {
color: #475569; color: #475569;
} }
@media (max-width: 980px) {
.offboarding-shell-body {
grid-template-columns: 1fr;
}
.offboarding-panel {
position: static;
}
}
@media (max-width: 820px) { @media (max-width: 820px) {
.grid { .grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;

View File

@@ -1,61 +1,22 @@
:root { .workflow-sidebar-card h1 {
--bg-a: #d3e3ff;
--bg-b: #eef4ff;
--ink: #182233;
--muted: #5e6f85;
--brand: #000078;
--brand-soft: #eef1ff;
--line: #d7dfeb;
--danger: #c53030;
--warn-bg: #fff7ed;
--warn-border: #fdba74;
--card: #ffffff;
}
.shell-body {
display: grid;
grid-template-columns: 290px 1fr;
gap: 16px;
padding: 18px;
}
.top-wrap {
width: min(var(--app-shell-width), 100%);
margin: 0 auto 10px;
}
.panel,
.main {
background: var(--card);
border: 1px solid var(--line);
border-radius: 16px;
box-shadow: 0 12px 28px rgba(30, 52, 87, 0.08);
}
.panel {
padding: 18px;
height: fit-content;
position: sticky;
top: 20px;
}
.main {
padding: 22px;
background: linear-gradient(180deg, #ffffff 0%, #fbfdff 100%);
}
h1 {
margin: 0 0 8px; margin: 0 0 8px;
font-size: 28px; font-size: 28px;
letter-spacing: -0.02em; letter-spacing: -0.02em;
} }
.sub { .workflow-sidebar-card .sub {
margin: 0 0 16px; margin: 0 0 16px;
color: var(--muted);
font-size: 14px; font-size: 14px;
} }
.workflow-form-main {
padding: 22px;
border: 1px solid #d7dfeb;
border-radius: 16px;
box-shadow: 0 12px 28px rgba(30, 52, 87, 0.08);
background: linear-gradient(180deg, #ffffff 0%, #fbfdff 100%);
}
.brand-logo { .brand-logo {
width: 180px; width: 180px;
max-width: 100%; max-width: 100%;
@@ -68,66 +29,15 @@ h1 {
margin: 0 0 10px; margin: 0 0 10px;
} }
.step-list { .app-flow-item {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 8px;
}
.step-item {
display: flex;
gap: 10px;
align-items: flex-start;
border: 1px solid #d8e0f4;
border-radius: 12px;
padding: 10px;
background: linear-gradient(160deg, #f8faff, #fcfdff);
cursor: pointer; cursor: pointer;
transition: border-color 0.15s ease, transform 0.08s ease, box-shadow 0.15s ease;
} }
.step-item.active { .app-flow-item:focus-visible {
border-color: #9db4ff;
background: linear-gradient(160deg, #eaf0ff, #f4f7ff);
box-shadow: 0 6px 16px rgba(0, 0, 120, 0.08);
}
.step-item:hover {
border-color: #b2c3ff;
}
.step-item:focus-visible {
outline: 3px solid rgba(0, 0, 120, 0.18); outline: 3px solid rgba(0, 0, 120, 0.18);
outline-offset: 2px; outline-offset: 2px;
} }
.dot {
width: 24px;
height: 24px;
border-radius: 999px;
display: inline-flex;
align-items: center;
justify-content: center;
background: var(--brand-soft);
border: 1px solid #c4cdf7;
color: var(--brand);
font-size: 12px;
font-weight: 700;
}
.step-title {
font-weight: 700;
color: #1d2c68;
margin-bottom: 2px;
}
.step-sub {
font-size: 12px;
color: var(--muted);
}
.page { .page {
display: none !important; display: none !important;
} }
@@ -632,12 +542,8 @@ select:focus {
} }
@media (max-width: 920px) { @media (max-width: 920px) {
.shell { .workflow-form-main {
grid-template-columns: 1fr; padding: 18px;
}
.panel {
position: static;
} }
.grid-2 { .grid-2 {

View File

@@ -1,6 +1,6 @@
(function () { (function () {
const pages = Array.from(document.querySelectorAll('.page')); const pages = Array.from(document.querySelectorAll('.page'));
const navItems = Array.from(document.querySelectorAll('.step-item')); const navItems = Array.from(document.querySelectorAll('.app-flow-item'));
const btnPrev = document.getElementById('btn-prev'); const btnPrev = document.getElementById('btn-prev');
const btnNext = document.getElementById('btn-next'); const btnNext = document.getElementById('btn-next');
const btnSubmit = document.getElementById('btn-submit'); const btnSubmit = document.getElementById('btn-submit');
@@ -270,7 +270,7 @@
function updateStep() { function updateStep() {
pages.forEach((p, i) => p.classList.toggle('active', i === current)); pages.forEach((p, i) => p.classList.toggle('active', i === current));
navItems.forEach((n, i) => n.classList.toggle('active', i === current)); navItems.forEach((n, i) => n.classList.toggle('is-active', i === current));
btnPrev.disabled = current === 0; btnPrev.disabled = current === 0;
const last = current === pages.length - 1; const last = current === pages.length - 1;
btnNext.classList.toggle('hidden', last); btnNext.classList.toggle('hidden', last);

View File

@@ -11,38 +11,38 @@
{% block shell_body %} {% block shell_body %}
{% include 'workflows/includes/app_header.html' with header_show_home=1 header_inside_shell=1 %} {% include 'workflows/includes/app_header.html' with header_show_home=1 header_inside_shell=1 %}
<div class="builder-workspace"> <div class="builder-workspace app-workspace">
<aside class="builder-sidebar"> <aside class="builder-sidebar app-sidebar">
<div class="builder-sidebar-card"> <div class="builder-sidebar-card app-sidebar-card">
<span class="builder-sidebar-eyebrow">{% trans "Arbeitsbereich" %}</span> <span class="builder-sidebar-eyebrow app-sidebar-eyebrow">{% trans "Arbeitsbereich" %}</span>
<h2>{% trans "Formularsteuerung" %}</h2> <h2>{% trans "Formularsteuerung" %}</h2>
</div> </div>
<nav class="builder-side-nav" aria-label="{% trans 'Builder Navigation' %}"> <nav class="builder-side-nav app-side-nav" aria-label="{% trans 'Builder Navigation' %}">
<a class="builder-side-link{% if active_module == 'structure' %} is-active{% endif %}" href="/admin-tools/form-builder/?form_type={{ form_type }}&option_category={{ selected_option_category }}&module=structure"> <a class="builder-side-link app-side-link{% if active_module == 'structure' %} is-active{% endif %}" href="/admin-tools/form-builder/?form_type={{ form_type }}&option_category={{ selected_option_category }}&module=structure">
<span class="builder-side-link-title">{% trans "Struktur & Reihenfolge" %}</span> <span class="builder-side-link-title app-side-link-title">{% trans "Struktur & Reihenfolge" %}</span>
<span class="builder-side-link-meta">{{ columns|length }} {% trans "Abschnitte" %}</span> <span class="builder-side-link-meta app-side-link-meta">{{ columns|length }} {% trans "Abschnitte" %}</span>
</a> </a>
<a class="builder-side-link{% if active_module == 'section-rules' or active_module == 'field-rules' or active_module == 'conditional-rules' %} is-active{% endif %}" href="/admin-tools/form-builder/?form_type={{ form_type }}&option_category={{ selected_option_category }}&module=field-rules"> <a class="builder-side-link app-side-link{% if active_module == 'section-rules' or active_module == 'field-rules' or active_module == 'conditional-rules' %} is-active{% endif %}" href="/admin-tools/form-builder/?form_type={{ form_type }}&option_category={{ selected_option_category }}&module=field-rules">
<span class="builder-side-link-title">{% trans "Sichtbarkeit & Regeln" %}</span> <span class="builder-side-link-title app-side-link-title">{% trans "Sichtbarkeit & Regeln" %}</span>
<span class="builder-side-link-meta">{{ builder_summary.hidden_field_count }} {% trans "ausgeblendet" %}</span> <span class="builder-side-link-meta app-side-link-meta">{{ builder_summary.hidden_field_count }} {% trans "ausgeblendet" %}</span>
</a> </a>
<a class="builder-side-link{% if active_module == 'options' or active_module == 'field-texts' or active_module == 'custom-sections' or active_module == 'custom-fields' or active_module == 'preview' %} is-active{% endif %}" href="/admin-tools/form-builder/?form_type={{ form_type }}&option_category={{ selected_option_category }}&module=options"> <a class="builder-side-link app-side-link{% if active_module == 'options' or active_module == 'field-texts' or active_module == 'custom-sections' or active_module == 'custom-fields' or active_module == 'preview' %} is-active{% endif %}" href="/admin-tools/form-builder/?form_type={{ form_type }}&option_category={{ selected_option_category }}&module=options">
<span class="builder-side-link-title">{% trans "Optionen & Texte" %}</span> <span class="builder-side-link-title app-side-link-title">{% trans "Optionen & Texte" %}</span>
<span class="builder-side-link-meta">{{ builder_summary.custom_field_count }} {% trans "eigene Felder" %}</span> <span class="builder-side-link-meta app-side-link-meta">{{ builder_summary.custom_field_count }} {% trans "eigene Felder" %}</span>
</a> </a>
</nav> </nav>
<div class="builder-sidebar-card builder-sidebar-stats"> <div class="builder-sidebar-card builder-sidebar-stats app-sidebar-card app-sidebar-stats">
<div class="builder-side-stat"> <div class="builder-side-stat app-side-stat">
<strong>{{ builder_summary.configurable_field_count }}</strong> <strong>{{ builder_summary.configurable_field_count }}</strong>
<span>{% trans "konfigurierbare Felder" %}</span> <span>{% trans "konfigurierbare Felder" %}</span>
</div> </div>
<div class="builder-side-stat"> <div class="builder-side-stat app-side-stat">
<strong>{{ builder_summary.custom_section_count }}</strong> <strong>{{ builder_summary.custom_section_count }}</strong>
<span>{% trans "eigene Abschnitte" %}</span> <span>{% trans "eigene Abschnitte" %}</span>
</div> </div>
<div class="builder-side-stat"> <div class="builder-side-stat app-side-stat">
<strong>{{ builder_summary.custom_field_count }}</strong> <strong>{{ builder_summary.custom_field_count }}</strong>
<span>{% trans "eigene Felder" %}</span> <span>{% trans "eigene Felder" %}</span>
</div> </div>
@@ -152,11 +152,11 @@
</div> </div>
</div> </div>
<nav class="builder-module-nav" aria-label="{% trans 'Regelmodule' %}"> <nav class="builder-module-nav app-module-nav" aria-label="{% trans 'Regelmodule' %}">
<a class="builder-module-link{% if active_module == 'section-rules' %} is-active{% endif %}" href="/admin-tools/form-builder/?form_type={{ form_type }}&option_category={{ selected_option_category }}&module=section-rules">{% trans "Abschnitte" %}</a> <a class="builder-module-link app-module-link{% if active_module == 'section-rules' %} is-active{% endif %}" href="/admin-tools/form-builder/?form_type={{ form_type }}&option_category={{ selected_option_category }}&module=section-rules">{% trans "Abschnitte" %}</a>
<a class="builder-module-link{% if active_module == 'field-rules' %} is-active{% endif %}" href="/admin-tools/form-builder/?form_type={{ form_type }}&option_category={{ selected_option_category }}&module=field-rules">{% trans "Feldregeln" %}</a> <a class="builder-module-link app-module-link{% if active_module == 'field-rules' %} is-active{% endif %}" href="/admin-tools/form-builder/?form_type={{ form_type }}&option_category={{ selected_option_category }}&module=field-rules">{% trans "Feldregeln" %}</a>
{% if form_type == 'onboarding' %} {% if form_type == 'onboarding' %}
<a class="builder-module-link{% if active_module == 'conditional-rules' %} is-active{% endif %}" href="/admin-tools/form-builder/?form_type={{ form_type }}&option_category={{ selected_option_category }}&module=conditional-rules">{% trans "Bedingte Logik" %}</a> <a class="builder-module-link app-module-link{% if active_module == 'conditional-rules' %} is-active{% endif %}" href="/admin-tools/form-builder/?form_type={{ form_type }}&option_category={{ selected_option_category }}&module=conditional-rules">{% trans "Bedingte Logik" %}</a>
{% endif %} {% endif %}
</nav> </nav>
@@ -441,14 +441,14 @@
</div> </div>
</div> </div>
<nav class="builder-module-nav" aria-label="{% trans 'Inhaltsmodule' %}"> <nav class="builder-module-nav app-module-nav" aria-label="{% trans 'Inhaltsmodule' %}">
<a class="builder-module-link{% if active_module == 'options' %} is-active{% endif %}" href="/admin-tools/form-builder/?form_type={{ form_type }}&option_category={{ selected_option_category }}&module=options">{% trans "Optionen" %}</a> <a class="builder-module-link app-module-link{% if active_module == 'options' %} is-active{% endif %}" href="/admin-tools/form-builder/?form_type={{ form_type }}&option_category={{ selected_option_category }}&module=options">{% trans "Optionen" %}</a>
<a class="builder-module-link{% if active_module == 'field-texts' %} is-active{% endif %}" href="/admin-tools/form-builder/?form_type={{ form_type }}&option_category={{ selected_option_category }}&module=field-texts">{% trans "Feldtexte" %}</a> <a class="builder-module-link app-module-link{% if active_module == 'field-texts' %} is-active{% endif %}" href="/admin-tools/form-builder/?form_type={{ form_type }}&option_category={{ selected_option_category }}&module=field-texts">{% trans "Feldtexte" %}</a>
{% if form_type == 'onboarding' %} {% if form_type == 'onboarding' %}
<a class="builder-module-link{% if active_module == 'custom-sections' %} is-active{% endif %}" href="/admin-tools/form-builder/?form_type={{ form_type }}&option_category={{ selected_option_category }}&module=custom-sections">{% trans "Eigene Abschnitte" %}</a> <a class="builder-module-link app-module-link{% if active_module == 'custom-sections' %} is-active{% endif %}" href="/admin-tools/form-builder/?form_type={{ form_type }}&option_category={{ selected_option_category }}&module=custom-sections">{% trans "Eigene Abschnitte" %}</a>
{% endif %} {% endif %}
<a class="builder-module-link{% if active_module == 'custom-fields' %} is-active{% endif %}" href="/admin-tools/form-builder/?form_type={{ form_type }}&option_category={{ selected_option_category }}&module=custom-fields">{% trans "Eigene Felder" %}</a> <a class="builder-module-link app-module-link{% if active_module == 'custom-fields' %} is-active{% endif %}" href="/admin-tools/form-builder/?form_type={{ form_type }}&option_category={{ selected_option_category }}&module=custom-fields">{% trans "Eigene Felder" %}</a>
<a class="builder-module-link{% if active_module == 'preview' %} is-active{% endif %}" href="/admin-tools/form-builder/?form_type={{ form_type }}&option_category={{ selected_option_category }}&module=preview">{% trans "Vorschau" %}</a> <a class="builder-module-link app-module-link{% if active_module == 'preview' %} is-active{% endif %}" href="/admin-tools/form-builder/?form_type={{ form_type }}&option_category={{ selected_option_category }}&module=preview">{% trans "Vorschau" %}</a>
</nav> </nav>
<div class="builder-stack-layout"> <div class="builder-stack-layout">

View File

@@ -37,57 +37,6 @@
<main class="main"> <main class="main">
{% include 'workflows/includes/messages.html' %} {% 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 %} {% for section in portal_app_sections %}
{% if not forloop.first %} {% if not forloop.first %}
<div class="section-divider" aria-hidden="true"></div> <div class="section-divider" aria-hidden="true"></div>

View File

@@ -23,7 +23,7 @@
</div> </div>
</header> </header>
<div class="switch"> <div class="switch app-module-nav">
<a class="tab {% if kind == 'nextcloud' %}active{% endif %}" href="/admin-tools/integrations/?kind=nextcloud">{% trans "Setup Nextcloud" %}</a> <a class="tab {% if kind == 'nextcloud' %}active{% endif %}" href="/admin-tools/integrations/?kind=nextcloud">{% trans "Setup Nextcloud" %}</a>
<a class="tab {% if kind == 'mail' %}active{% endif %}" href="/admin-tools/integrations/?kind=mail">{% trans "Setup Mail" %}</a> <a class="tab {% if kind == 'mail' %}active{% endif %}" href="/admin-tools/integrations/?kind=mail">{% trans "Setup Mail" %}</a>
<a class="tab {% if kind == 'emails' %}active{% endif %}" href="/admin-tools/integrations/?kind=emails">{% trans "E-Mail Routing & Vorlagen" %}</a> <a class="tab {% if kind == 'emails' %}active{% endif %}" href="/admin-tools/integrations/?kind=emails">{% trans "E-Mail Routing & Vorlagen" %}</a>

View File

@@ -37,6 +37,15 @@
<label>{% trans "Offene Starts (24h)" %}</label> <label>{% trans "Offene Starts (24h)" %}</label>
<div class="branding-inline-value">{{ job_summary.started_count_24h }}</div> <div class="branding-inline-value">{{ job_summary.started_count_24h }}</div>
</div> </div>
{% if job_summary.can_manage_backups and job_summary.backup_health %}
<div class="field">
<label>{% trans "Backup-Status" %}</label>
<div class="branding-inline-value">
<strong>{{ job_summary.backup_health.label }}</strong>
<div class="mini">{{ job_summary.backup_health.summary }}</div>
</div>
</div>
{% endif %}
</div> </div>
{% if job_summary.recent_failed %} {% if job_summary.recent_failed %}
<div class="table-wrap u-mt-12 app-table-shell"> <div class="table-wrap u-mt-12 app-table-shell">

View File

@@ -20,24 +20,26 @@
{% block shell_body %} {% block shell_body %}
{% include 'workflows/includes/app_header.html' with header_show_lang=1 header_show_home=1 header_inside_shell=1 %} {% include 'workflows/includes/app_header.html' with header_show_lang=1 header_show_home=1 header_inside_shell=1 %}
<div class="offboarding-shell-body"> <div class="app-workspace workflow-workspace">
<aside class="offboarding-panel"> <aside class="app-sidebar">
<div class="app-sidebar-card workflow-sidebar-card">
<h1>{% trans "Offboarding" %}</h1> <h1>{% trans "Offboarding" %}</h1>
<p class="offboarding-sub">{% trans "Strukturierte Austrittsanfrage mit denselben klaren Oberflächenmustern wie im Onboarding." %}</p> <p class="offboarding-sub">{% trans "Strukturierte Austrittsanfrage mit denselben klaren Oberflächenmustern wie im Onboarding." %}</p>
<ol class="offboarding-step-list"> <ol class="app-flow-list">
{% for section in offboarding_sections %} {% for section in offboarding_sections %}
<li class="offboarding-step-item"> <li class="app-flow-item">
<span class="offboarding-dot">{{ forloop.counter }}</span> <span class="app-flow-dot">{{ forloop.counter }}</span>
<div> <div>
<div class="offboarding-step-title">{{ section.title }}</div> <div class="app-flow-title">{{ section.title }}</div>
<div class="offboarding-step-sub">{{ section.subtitle }}</div> <div class="app-flow-sub">{{ section.subtitle }}</div>
</div> </div>
</li> </li>
{% endfor %} {% endfor %}
</ol> </ol>
</div>
</aside> </aside>
<main class="offboarding-main"> <main class="workflow-form-main offboarding-main">
<section class="offboarding-search-card"> <section class="offboarding-search-card">
<div class="offboarding-section-head offboarding-search-head"> <div class="offboarding-section-head offboarding-search-head">
<div> <div>

View File

@@ -20,24 +20,26 @@
{% block shell_body %} {% block shell_body %}
{% include 'workflows/includes/app_header.html' with header_show_lang=1 header_show_home=1 header_inside_shell=1 %} {% include 'workflows/includes/app_header.html' with header_show_lang=1 header_show_home=1 header_inside_shell=1 %}
<div class="shell-body"> <div class="app-workspace workflow-workspace">
<aside class="panel"> <aside class="app-sidebar">
<div class="app-sidebar-card workflow-sidebar-card">
<h1>{% trans "Onboarding" %}</h1> <h1>{% trans "Onboarding" %}</h1>
<p class="sub">{% trans "Mehrseitiges Formular mit konfigurierbaren Feldern aus dem Admin." %}</p> <p class="sub">{% trans "Mehrseitiges Formular mit konfigurierbaren Feldern aus dem Admin." %}</p>
<ol class="step-list"> <ol class="app-flow-list">
{% for section in onboarding_sections %} {% for section in onboarding_sections %}
<li class="step-item {% if forloop.first %}active{% endif %}" data-nav-step="{{ forloop.counter }}" role="button" tabindex="0" aria-label="{{ section.title }}"> <li class="app-flow-item {% if forloop.first %}is-active{% endif %}" data-nav-step="{{ forloop.counter }}" role="button" tabindex="0" aria-label="{{ section.title }}">
<span class="dot">{{ forloop.counter }}</span> <span class="app-flow-dot">{{ forloop.counter }}</span>
<div> <div>
<div class="step-title">{{ section.title }}</div> <div class="app-flow-title">{{ section.title }}</div>
<div class="step-sub">{{ section.subtitle }}</div> <div class="app-flow-sub">{{ section.subtitle }}</div>
</div> </div>
</li> </li>
{% endfor %} {% endfor %}
</ol> </ol>
</div>
</aside> </aside>
<main class="main"> <main class="workflow-form-main">
{% if form.errors %} {% if form.errors %}
<div class="error-banner">{% trans "Bitte prüfen Sie die markierten Felder. Ungültige Eingaben wurden erkannt." %}</div> <div class="error-banner">{% trans "Bitte prüfen Sie die markierten Felder. Ungültige Eingaben wurden erkannt." %}</div>
{% endif %} {% endif %}

View File

@@ -40,7 +40,7 @@ class ObservabilityUITests(TestCase):
) )
return AsyncTaskLog.objects.get(id=log.id) return AsyncTaskLog.objects.get(id=log.id)
def test_home_shows_operations_overview_for_admin(self): def test_home_hides_operations_overview_for_admin(self):
self._create_log( self._create_log(
status='failed', status='failed',
task_name='send_scheduled_welcome_email', task_name='send_scheduled_welcome_email',
@@ -63,11 +63,8 @@ class ObservabilityUITests(TestCase):
response = client.get('/', HTTP_HOST='localhost') response = client.get('/', HTTP_HOST='localhost')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Operations Overview') self.assertNotContains(response, 'Operations Overview')
self.assertContains(response, 'Fehlgeschlagene Jobs (24h)') self.assertNotContains(response, 'Job Monitor öffnen')
self.assertContains(response, '<strong class="is-error">1</strong>', html=True)
self.assertContains(response, 'send_scheduled_welcome_email')
self.assertContains(response, 'Backup-Status')
def test_home_hides_operations_overview_for_staff(self): def test_home_hides_operations_overview_for_staff(self):
self._create_log( self._create_log(
@@ -113,6 +110,7 @@ class ObservabilityUITests(TestCase):
self.assertContains(response, 'Offene Starts (24h)') self.assertContains(response, 'Offene Starts (24h)')
self.assertContains(response, 'Zuletzt fehlgeschlagen') self.assertContains(response, 'Zuletzt fehlgeschlagen')
self.assertContains(response, 'pdf failed') self.assertContains(response, 'pdf failed')
self.assertContains(response, 'Backup-Status')
def test_job_monitor_requires_capability(self): def test_job_monitor_requires_capability(self):
client = Client() client = Client()

View File

@@ -743,7 +743,6 @@ def _ops_summary_for_user(user) -> dict[str, object]:
def home(request): def home(request):
config, _ = WorkflowConfig.objects.get_or_create(name='Default') config, _ = WorkflowConfig.objects.get_or_create(name='Default')
role_key = get_user_role_key(request.user) role_key = get_user_role_key(request.user)
ops_summary = _ops_summary_for_user(request.user)
return render( return render(
request, request,
'workflows/home.html', 'workflows/home.html',
@@ -754,7 +753,6 @@ def home(request):
'role_label': get_user_role_label(request.user), 'role_label': get_user_role_label(request.user),
'role_key': role_key, 'role_key': role_key,
'portal_app_sections': build_portal_app_sections(request.user), 'portal_app_sections': build_portal_app_sections(request.user),
'ops_summary': ops_summary,
}, },
) )
@@ -789,6 +787,7 @@ def job_monitor_page(request):
for row in recent_logs.values('status').annotate(count=Count('id')) for row in recent_logs.values('status').annotate(count=Count('id'))
} }
recent_failed = list(AsyncTaskLog.objects.filter(status='failed').order_by('-started_at', '-id')[:5]) recent_failed = list(AsyncTaskLog.objects.filter(status='failed').order_by('-started_at', '-id')[:5])
can_manage_backups = user_has_capability(request.user, 'manage_backups')
return render( return render(
request, request,
'workflows/job_monitor.html', 'workflows/job_monitor.html',
@@ -803,6 +802,8 @@ def job_monitor_page(request):
'success_count_24h': counts.get('succeeded', 0), 'success_count_24h': counts.get('succeeded', 0),
'failed_count_24h': counts.get('failed', 0), 'failed_count_24h': counts.get('failed', 0),
'recent_failed': recent_failed, 'recent_failed': recent_failed,
'can_manage_backups': can_manage_backups,
'backup_health': latest_backup_health_snapshot() if can_manage_backups else None,
}, },
}, },
) )