snapshot: preserve dashboard redesign and live protocol workflow state

This commit is contained in:
Md Bayazid Bostame
2026-03-19 16:10:30 +01:00
parent 3bf43921ff
commit 1cb92682cf
14 changed files with 1948 additions and 121 deletions

View File

@@ -383,6 +383,11 @@
<p>Felder, Schritte und Optionen verwalten.</p>
<a class="btn btn-secondary" href="/admin-tools/form-builder/">Öffnen</a>
</section>
<section class="admin-card">
<h3>Einweisungs-Builder</h3>
<p>Checklistenpunkte für das Einweisungsprotokoll konfigurieren.</p>
<a class="btn btn-secondary" href="/admin-tools/intro-builder/">Öffnen</a>
</section>
<section class="admin-card">
<h3>Projekt Wiki</h3>
<p>Dokumentation, Architektur und Runbook.</p>

View File

@@ -0,0 +1,144 @@
{% load static %}
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Einweisungs-Builder</title>
<link rel="stylesheet" href="{% static 'workflows/css/buttons.css' %}" />
<style>
body { margin: 0; font-family: Arial, sans-serif; background: #f4f8ff; color: #1b2b43; padding: 20px; }
.shell { max-width: 1180px; margin: 0 auto; background: #fff; border: 1px solid #d7e0ea; border-radius: 14px; padding: 18px; }
.topbar { display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 10px; }
.brand-logo { width: 190px; max-width: 100%; height: auto; display: block; }
h1 { margin: 0; color: #000078; font-size: 28px; }
.sub { margin: 8px 0 14px; color: #5f6f85; }
.flash { margin: 0 0 12px; padding: 10px; border-radius: 8px; border: 1px solid #dbe5f2; background: #f8fbff; }
.flash.error { border-color: #f4c7c7; background: #fff1f1; color: #8e1e1e; }
.flash.success { border-color: #bfe6c9; background: #edf9f1; color: #116634; }
.card { border: 1px solid #d7e0ea; border-radius: 12px; padding: 14px; background: #fcfdff; margin-bottom: 14px; }
.grid { display: grid; grid-template-columns: 1.1fr 1.4fr 1fr 1fr 1fr auto; gap: 10px; align-items: end; }
.field label { display: block; font-weight: 600; margin-bottom: 6px; }
.field input, .field select { width: 100%; min-height: 40px; padding: 8px 10px; border: 1px solid #cfd9e8; border-radius: 8px; box-sizing: border-box; }
.table-wrap { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; }
th, td { border: 1px solid #e2e8f0; padding: 8px; text-align: left; vertical-align: top; }
th { background: #f6f8fb; color: #334155; }
.mini { color: #64748b; font-size: 12px; }
.hint { margin-top: 6px; color: #5f6f85; font-size: 13px; }
.table-controls input[type="text"], .table-controls select { width: 100%; min-height: 36px; padding: 7px 9px; border: 1px solid #cfd9e8; border-radius: 8px; box-sizing: border-box; }
.table-controls input[type="checkbox"] { transform: scale(1.1); }
.actions { white-space: nowrap; }
.toolbar { display: flex; justify-content: space-between; align-items: center; gap: 10px; margin-bottom: 10px; flex-wrap: wrap; }
</style>
</head>
<body>
<div class="shell">
<div class="topbar">
<img class="brand-logo" src="https://tub.co/media/site/0856bfc615-1750234287/tubco-wortbildmarke.svg" alt="TUB/CO Logo" />
<a class="btn btn-secondary" href="/">Zur Startseite</a>
</div>
<div class="toolbar">
<div>
<h1>Einweisungs-Builder</h1>
<p class="sub">Checklistenpunkte für das Einweisungs- und Übergabeprotokoll verwalten.</p>
</div>
<a class="btn btn-secondary" href="/requests/">Zum Dashboard</a>
</div>
{% if messages %}
{% for message in messages %}
<div class="flash {% if message.tags == 'error' %}error{% else %}success{% endif %}">{{ message }}</div>
{% endfor %}
{% endif %}
<form class="card" method="post" action="/admin-tools/intro-builder/">
{% csrf_token %}
<input type="hidden" name="builder_action" value="add_item" />
<div class="grid">
<div class="field">
<label for="section">Abschnitt</label>
<select id="section" name="section">
{% for value, label in section_choices %}
<option value="{{ value }}">{{ label }}</option>
{% endfor %}
</select>
</div>
<div class="field">
<label for="label">Checklistenpunkt</label>
<input id="label" name="label" placeholder="z. B. Nextcloud Ordnerstruktur erklärt" required />
</div>
<div class="actions">
<button class="btn btn-primary" type="submit">Punkt hinzufügen</button>
</div>
</div>
<div class="hint">Bedingungen und Sortierung können anschließend in der Tabelle bearbeitet werden.</div>
</form>
<form class="card" method="post" action="/admin-tools/intro-builder/">
{% csrf_token %}
<input type="hidden" name="builder_action" value="save_items" />
<div class="table-wrap">
<table class="table-controls">
<thead>
<tr>
<th>Sortierung</th>
<th>Abschnitt</th>
<th>Checklistenpunkt</th>
<th>Feld-Bedingung</th>
<th>Operator</th>
<th>Wert</th>
<th>Aktiv</th>
<th>Löschen</th>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr>
<td>
<input type="hidden" name="item_ids" value="{{ item.id }}" />
{{ forloop.counter }}
</td>
<td>
<select name="section_{{ item.id }}">
{% for value, label in section_choices %}
<option value="{{ value }}" {% if item.section == value %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</td>
<td><input type="text" name="label_{{ item.id }}" value="{{ item.label }}" required /></td>
<td>
<select name="field_{{ item.id }}">
{% for value, label in condition_field_choices %}
<option value="{{ value }}" {% if item.condition_field == value %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</td>
<td>
<select name="operator_{{ item.id }}">
{% for value, label in operator_choices %}
<option value="{{ value }}" {% if item.condition_operator == value %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</td>
<td><input type="text" name="value_{{ item.id }}" value="{{ item.condition_value }}" placeholder="z. B. HR Works" /></td>
<td><input type="checkbox" name="active_{{ item.id }}" {% if item.is_active %}checked{% endif %} /></td>
<td class="actions">
<button class="btn btn-secondary" type="submit" name="delete_item_id" value="{{ item.id }}" onclick="return confirm('Checklistenpunkt wirklich löschen?');">Löschen</button>
</td>
</tr>
{% empty %}
<tr><td colspan="8">Noch keine benutzerdefinierten Checklistenpunkte angelegt. Solange die Liste leer ist, nutzt das System die integrierten Standardpunkte.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="hint">Reihenfolge folgt derzeit der Tabellenreihenfolge beim Speichern.</div>
<div style="margin-top:12px;">
<button class="btn btn-primary" type="submit">Checkliste speichern</button>
</div>
</form>
</div>
</body>
</html>

View File

@@ -0,0 +1,161 @@
{% load static %}
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Einweisung durchführen</title>
<link rel="stylesheet" href="{% static 'workflows/css/buttons.css' %}" />
<style>
:root {
--brand-blue: #000078;
--ink: #17253b;
--muted: #607087;
--line: #d7e0ea;
--panel: #ffffff;
--soft: #f4f8ff;
--soft-strong: #eef4ff;
--ok-bg: #edf9f1;
--ok-ink: #116634;
--warn-bg: #fff8ea;
--warn-ink: #8a5a00;
}
* { box-sizing: border-box; }
body { margin: 0; font-family: "Segoe UI", Arial, sans-serif; background: linear-gradient(180deg, #eef4ff, #f8fbff); color: var(--ink); padding: 24px; }
.shell { max-width: 1180px; margin: 0 auto; background: var(--panel); border: 1px solid var(--line); border-radius: 18px; box-shadow: 0 18px 42px rgba(16,32,57,.10); overflow: hidden; }
.topbar { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; padding: 18px 22px; border-bottom: 1px solid var(--line); background: #fff; }
.brand-logo { width: 210px; max-width: 100%; height: auto; display: block; }
.top-actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
.hero { padding: 20px 22px 18px; border-bottom: 1px solid var(--line); background: linear-gradient(135deg, rgba(0,0,120,.06), rgba(0,0,120,0) 48%), linear-gradient(180deg, #ffffff, #f8fbff); }
.hero h1 { margin: 0; font-size: 32px; line-height: 1.08; color: var(--brand-blue); }
.sub { margin: 8px 0 0; color: var(--muted); max-width: 780px; }
.content { padding: 20px 22px 24px; }
.flash { margin: 0 0 12px; padding: 10px 12px; border-radius: 10px; border: 1px solid #bfe6c9; background: var(--ok-bg); color: var(--ok-ink); }
.meta { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; margin-bottom: 16px; }
.card { border: 1px solid var(--line); border-radius: 14px; background: #fff; padding: 14px; }
.card h2 { margin: 0 0 10px; font-size: 16px; color: #1b3764; }
.progress-block { border: 1px solid var(--line); border-radius: 14px; background: linear-gradient(180deg, #ffffff, #f9fbff); padding: 14px; margin-bottom: 16px; }
.progress-top { display: flex; justify-content: space-between; gap: 10px; align-items: center; flex-wrap: wrap; margin-bottom: 10px; }
.progress-label { font-size: 15px; font-weight: 700; color: #18345f; }
.progress-meta { color: var(--muted); font-size: 13px; }
.progress-bar { width: 100%; height: 12px; border-radius: 999px; overflow: hidden; background: #e7eefb; border: 1px solid #d6e1f5; }
.progress-fill { height: 100%; background: linear-gradient(90deg, #3056a3, #0f5fcf); }
.meta-grid { display: grid; grid-template-columns: 170px 1fr; gap: 8px 10px; font-size: 14px; }
.meta-grid strong { color: #334155; }
.status-pill { display: inline-block; padding: 4px 10px; border-radius: 999px; border: 1px solid #d7e0ea; background: #f8fbff; color: #486183; font-size: 12px; font-weight: 700; }
.status-pill.done { background: var(--ok-bg); color: var(--ok-ink); border-color: #bfe6c9; }
.status-pill.draft { background: var(--warn-bg); color: var(--warn-ink); border-color: #f5d8a8; }
.section { border: 1px solid var(--line); border-radius: 14px; overflow: hidden; margin-bottom: 14px; background: #fff; }
.section-head { padding: 11px 14px; font-weight: 700; color: #1f376b; background: var(--soft-strong); border-bottom: 1px solid #d5e2f9; }
.items { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0; padding: 6px 14px 10px; }
.item { display: flex; align-items: flex-start; gap: 12px; padding: 10px 0; border-bottom: 1px solid #eef3f8; }
.item:nth-last-child(-n+2) { border-bottom: 0; }
.item input { margin-top: 2px; width: 18px; height: 18px; accent-color: var(--brand-blue); }
.item span { line-height: 1.45; }
textarea { width: 100%; min-height: 150px; border: 1px solid #cfd9e8; border-radius: 10px; padding: 11px 12px; font: inherit; resize: vertical; }
.help { color: var(--muted); font-size: 13px; margin-top: 8px; }
.actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 14px; }
@media (max-width: 900px) { .meta, .items { grid-template-columns: 1fr; } .topbar { flex-direction: column; } .top-actions { justify-content: flex-start; } .meta-grid { grid-template-columns: 1fr; } .item:nth-last-child(-n+2) { border-bottom: 1px solid #eef3f8; } .item:last-child { border-bottom: 0; } }
</style>
</head>
<body>
<div class="shell">
<div class="topbar">
<img class="brand-logo" src="https://tub.co/media/site/0856bfc615-1750234287/tubco-wortbildmarke.svg" alt="TUB/CO Logo" />
<div class="top-actions">
<a class="btn btn-secondary" href="/requests/">Zum Dashboard</a>
<a class="btn btn-secondary" href="/">Zur Startseite</a>
</div>
</div>
<div class="hero">
<h1>Einweisung durchführen</h1>
<p class="sub">Einfache Live-Checkliste für das persönliche Onboarding-Gespräch. Punkte abhaken, Notizen ergänzen, als Entwurf speichern oder als abgeschlossen markieren.</p>
</div>
<div class="content">
{% if messages %}
{% for message in messages %}
<div class="flash">{{ message }}</div>
{% endfor %}
{% endif %}
<div class="meta">
<div class="card">
<h2>Mitarbeitende Person</h2>
<div class="meta-grid">
<strong>Name</strong><span>{{ display_name|default:onboarding.full_name }}</span>
<strong>Abteilung</strong><span>{{ onboarding.department|default:"-" }}</span>
<strong>Berufsbezeichnung</strong><span>{{ onboarding.job_title|default:"-" }}</span>
<strong>Dienstliche E-Mail</strong><span>{{ onboarding.work_email|default:"-" }}</span>
<strong>Vertragsbeginn</strong><span>{{ onboarding.contract_start|default:"-" }}</span>
</div>
</div>
<div class="card">
<h2>Sitzungsstatus</h2>
<div class="meta-grid">
<strong>Status</strong>
<span><span class="status-pill {% if session.status == 'completed' %}done{% else %}draft{% endif %}">{{ session.get_status_display }}</span></span>
<strong>Abgeschlossen von</strong><span>{{ session.completed_by_name|default:"-" }}</span>
<strong>Abgeschlossen am</strong><span>{% if session.completed_at %}{{ session.completed_at|date:"Y-m-d H:i" }}{% else %}-{% endif %}</span>
<strong>Letzte Änderung</strong><span>{{ session.updated_at|date:"Y-m-d H:i" }}</span>
</div>
</div>
</div>
<div class="progress-block">
<div class="progress-top">
<div>
<div class="progress-label">Fortschritt der Einweisung</div>
<div class="progress-meta">{{ checked_count }} von {{ total_count }} Punkten erledigt</div>
</div>
<div class="status-pill {% if session.status == 'completed' %}done{% else %}draft{% endif %}">{{ progress_percent }}%</div>
</div>
<div class="progress-bar"><div class="progress-fill" style="width: {{ progress_percent }}%;"></div></div>
</div>
<form method="post">
{% csrf_token %}
{% for section in sections %}
<section class="section">
<div class="section-head">{{ section.title }}</div>
<div class="items">
{% for item in section.items %}
<label class="item">
<input type="checkbox" name="checked_items" value="{{ item.id }}" {% if item.checked %}checked{% endif %} />
<span>{{ item.label }}</span>
</label>
{% endfor %}
</div>
</section>
{% endfor %}
<div class="card">
<h2>Notizen</h2>
<textarea id="notes" name="notes">{{ session.notes }}</textarea>
<div class="help">Diese Seite bleibt bewusst einfach: echte Web-Checkboxen, Notizen und ein klarer Entwurf/Abschluss-Status. Kein zusätzlicher komplexer PDF-Signatur-Workflow.</div>
<div class="actions">
<button class="btn btn-secondary" type="submit" name="session_action" value="save">Als Entwurf speichern</button>
<button class="btn btn-primary" type="submit" name="session_action" value="complete">Als abgeschlossen markieren</button>
<button class="btn btn-secondary" type="submit" name="session_action" value="reset" onclick="return confirm('Einweisung wirklich zurücksetzen?');">Alles zurücksetzen</button>
</div>
</div>
</form>
<div class="card">
<h2>Live-Protokoll</h2>
<div class="help">Erzeugt das Live-Protokoll nur aus den aktuell gespeicherten Haken und Notizen.</div>
<div class="actions">
<form method="post" action="/requests/onboarding/{{ onboarding.id }}/intro-session/pdf/" style="display:inline;">
{% csrf_token %}
<button class="btn btn-secondary" type="submit">Live-Protokoll erzeugen</button>
</form>
{% if session_pdf_url %}
<a class="btn btn-secondary" href="{{ session_pdf_url }}" target="_blank" rel="noopener">Live-Protokoll öffnen</a>
{% endif %}
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -98,6 +98,8 @@
<li>Form saves request; requester identity is taken from logged-in user.</li>
<li>Task <code>process_onboarding_request</code> runs in worker.</li>
<li>PDF is generated using HTML template + letterhead overlay.</li>
<li>Staff can additionally generate a separate <code>Einweisungs- und Übergabeprotokoll</code> from the Requests Dashboard for face-to-face employee introduction and sign-off.</li>
<li>Staff can also open a simple live <code>Einweisung durchführen</code> page with real web checkboxes, notes, and draft/completed tracking.</li>
<li>Default notification emails + optional rule-based emails are sent.</li>
<li>Welcome email job is scheduled (configurable delay).</li>
<li>PDF is uploaded to Nextcloud if enabled.</li>
@@ -134,11 +136,16 @@
<h2 id="pdfs">7) PDF Engine</h2>
<ul>
<li>Template source: <code>/backend/media/templates/onboarding_template.html</code> and <code>offboarding_template.html</code>.</li>
<li>Additional onboarding intro template: <code>/backend/media/templates/onboarding_intro_template.html</code>.</li>
<li>Letterhead: <code>/backend/media/templates/templates.pdf</code>.</li>
<li>Output folder: <code>/backend/media/pdfs/</code>.</li>
<li>Signature images are embedded for compatibility with xhtml2pdf rendering.</li>
<li>Conditional sections are hidden if no data is provided.</li>
<li>Offboarding fallback behavior: if no onboarding record is available, the PDF renders a manual onboarding review layout grouped into <code>Stammdaten</code>, <code>Vertrag</code>, <code>IT-Setup</code>, and <code>Abschluss</code>, plus checkbox grids for devices, software, accesses, workspace groups, resources, and additional IT items.</li>
<li>The onboarding intro PDF is a separate checklist-style document for live walkthroughs. It uses printable blank checkboxes and signature lines for both the employee and the trainer.</li>
<li>The onboarding intro PDF can now use admin-configured checklist items from the dedicated <code>Einweisungs-Builder</code>. If no custom items are configured, the system falls back to built-in defaults.</li>
<li>The live introduction page uses the same checklist structure as the intro PDF, so admins maintain one checklist definition and can use it both on screen and on paper.</li>
<li>The live introduction page shows progress in percent and can generate a separate PDF export of the saved live checklist state.</li>
</ul>
<h2 id="integrations">8) Integrations</h2>
@@ -157,9 +164,12 @@
<h2 id="admin">9) Admin Apps (Home)</h2>
<ul>
<li><strong>Form Builder:</strong> manage field visibility/order/options.</li>
<li><strong>Einweisungs-Builder:</strong> manage custom checklist items for the intro PDF and live introduction checklist, including section, visibility, and conditional display logic.</li>
<li><strong>Integrations:</strong> Nextcloud, SMTP, default routing addresses, notification rules.</li>
<li><strong>Welcome Emails:</strong> scheduled jobs, pause/resume/cancel/trigger now.</li>
<li><strong>Requests Dashboard:</strong> search records, open PDFs, delete records (single/bulk for staff).</li>
<li><strong>Einweisungs- und Übergabeprotokoll:</strong> staff-only <code>PDF erzeugen</code>, <code>Neu erzeugen</code>, and <code>PDF öffnen</code> actions directly on onboarding rows in the Requests Dashboard.</li>
<li><strong>Einweisung durchführen:</strong> staff-only live checklist page opened from onboarding rows, with draft/completed status, notes, progress tracking, and a separate live-status PDF export.</li>
<li><strong>Project Wiki:</strong> this documentation page.</li>
</ul>
@@ -182,6 +192,7 @@
<li>After template/form/task changes, restart web and worker containers.</li>
<li>Run <code>python manage.py check</code> before release.</li>
<li>On a fresh database, the web container boot sequence now runs <code>python manage.py bootstrap_initial_users</code> right after migrations.</li>
<li>After adding or editing intro checklist items, regenerate the intro PDF from the Requests Dashboard to reflect the updated checklist.</li>
</ul>
<h3>Initial Bootstrap Users</h3>

File diff suppressed because it is too large Load Diff