chore: initial snapshot of tubco people portal

This commit is contained in:
Md Bayazid Bostame
2026-03-19 10:22:20 +01:00
commit 9fe3c2ea82
81 changed files with 8698 additions and 0 deletions

View File

@@ -0,0 +1,131 @@
{% load static %}
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Form Builder</title>
<link rel="stylesheet" href="{% static 'workflows/css/buttons.css' %}" />
<link rel="stylesheet" href="{% static 'workflows/css/form_builder.css' %}" />
</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>
<header class="header">
<h1>Form Builder</h1>
<p>Felder per Drag-and-Drop sortieren und pro Schritt gruppieren.</p>
</header>
{% if messages %}
{% for message in messages %}
<div class="flash {% if message.tags == 'error' %}error{% endif %}">{{ message }}</div>
{% endfor %}
{% endif %}
<div class="toolbar">
{% for key, label in form_types %}
<a
class="tab {% if form_type == key %}active{% endif %}"
href="/admin-tools/form-builder/?form_type={{ key }}"
>
{{ label }}
</a>
{% endfor %}
<button id="save-order" class="btn btn-primary" type="button">Reihenfolge speichern</button>
</div>
<div id="status-message" class="status" aria-live="polite"></div>
<div class="columns {% if form_type == 'offboarding' %}single{% endif %}" id="builder-columns" data-form-type="{{ form_type }}">
{% for column in columns %}
<section class="column" data-column-key="{{ column.key }}">
<h2>{{ column.title }}</h2>
<div class="dropzone" data-column-key="{{ column.key }}">
{% for item in column.items %}
<article class="field-card" draggable="true" data-field-name="{{ item.field_name }}">
<div class="field-main">
<div class="field-label">{{ item.label }}</div>
<div class="field-name">{{ item.field_name }}</div>
</div>
<div class="badges">
{% if item.locked %}<span class="badge locked">Fix</span>{% endif %}
{% if not item.is_visible %}<span class="badge hidden">Hidden</span>{% endif %}
{% if item.is_required %}<span class="badge required">Pflicht</span>{% endif %}
</div>
</article>
{% endfor %}
</div>
</section>
{% endfor %}
</div>
<section class="options-panel">
<div class="options-head">
<h2>Optionen verwalten</h2>
<form class="category-switch" method="get" action="/admin-tools/form-builder/">
<input type="hidden" name="form_type" value="{{ form_type }}" />
<label for="option_category">Kategorie</label>
<select id="option_category" name="option_category" onchange="this.form.submit()">
{% for value, label in option_categories %}
<option value="{{ value }}" {% if value == selected_option_category %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</form>
</div>
<form class="add-option-form" method="post" action="/admin-tools/form-builder/?form_type={{ form_type }}&option_category={{ selected_option_category }}">
{% csrf_token %}
<input type="hidden" name="builder_action" value="add_option" />
<input type="hidden" name="category" value="{{ selected_option_category }}" />
<input type="text" name="label" placeholder="Neuer Optionsname" required />
<input type="text" name="value" placeholder="Technischer Wert (optional)" />
<button class="btn btn-primary" type="submit">Option hinzufügen</button>
</form>
<form method="post" action="/admin-tools/form-builder/?form_type={{ form_type }}&option_category={{ selected_option_category }}">
{% csrf_token %}
<div class="option-table-wrap">
<table class="option-table">
<thead>
<tr>
<th>Sortierung</th>
<th>Label</th>
<th>Value</th>
<th>Aktiv</th>
<th>Löschen</th>
</tr>
</thead>
<tbody id="option-table-body">
{% for item in option_items %}
<tr class="option-row" draggable="true" data-option-row="1">
<td>
<input type="hidden" name="option_ids" value="{{ item.id }}" />
<span class="drag-handle" title="Ziehen zum Sortieren">⋮⋮</span>
</td>
<td><input type="text" name="label_{{ item.id }}" value="{{ item.label }}" required /></td>
<td><input type="text" name="value_{{ item.id }}" value="{{ item.value }}" /></td>
<td><input type="checkbox" name="active_{{ item.id }}" {% if item.is_active %}checked{% endif %} /></td>
<td>
<button class="btn btn-secondary" type="submit" name="delete_option_id" value="{{ item.id }}" onclick="return confirm('Option wirklich löschen?');">Löschen</button>
</td>
</tr>
{% empty %}
<tr><td colspan="5">Keine Optionen in dieser Kategorie.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="options-actions">
<button class="btn btn-primary" type="submit" name="builder_action" value="save_options">Optionen speichern</button>
</div>
</form>
</section>
</div>
<script src="{% static 'workflows/js/form_builder.js' %}"></script>
</body>
</html>

View File

@@ -0,0 +1,454 @@
{% load static %}
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Onboarding/Offboarding Portal</title>
<link rel="stylesheet" href="{% static 'workflows/css/buttons.css' %}" />
<style>
:root {
--brand-blue: #000078;
--brand-red: #8c1d1d;
--ink: #102039;
--muted: #5f6f85;
--line: #d8e1ee;
--panel: #ffffff;
--bg-soft: #eff4ff;
--ok-bg: #effaf2;
--ok-ink: #166534;
--warn-bg: #fff6ea;
--warn-ink: #8a4f00;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
color: var(--ink);
background:
radial-gradient(80% 120% at 85% 8%, rgba(0, 0, 120, 0.12), rgba(0, 0, 120, 0)),
radial-gradient(70% 90% at 8% 92%, rgba(140, 29, 29, 0.10), rgba(140, 29, 29, 0)),
linear-gradient(165deg, #eef3ff, #f7f9ff 48%, #f0f5ff);
min-height: 100vh;
padding: 24px;
}
.shell {
width: min(1220px, 100%);
margin: 0 auto;
background: var(--panel);
border: 1px solid var(--line);
border-radius: 20px;
box-shadow: 0 20px 44px rgba(16, 32, 57, 0.13);
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;
margin: 0;
}
.quick-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
justify-content: flex-end;
}
.hero {
padding: 22px;
border-bottom: 1px solid var(--line);
background:
linear-gradient(135deg, rgba(0, 0, 120, 0.06), rgba(0, 0, 120, 0) 48%),
linear-gradient(180deg, #ffffff, #f8fbff);
}
.hero h1 {
margin: 0;
font-size: 34px;
line-height: 1.05;
letter-spacing: -0.02em;
color: var(--brand-blue);
}
.hero p {
margin: 8px 0 0;
color: var(--muted);
max-width: 820px;
font-size: 15px;
}
.status-row {
margin-top: 16px;
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.status-pill {
border-radius: 999px;
border: 1px solid var(--line);
background: #fff;
color: #30445f;
padding: 6px 10px;
font-size: 12px;
font-weight: 700;
}
.status-pill.ok {
background: var(--ok-bg);
color: var(--ok-ink);
border-color: #c7ecd2;
}
.status-pill.warn {
background: var(--warn-bg);
color: var(--warn-ink);
border-color: #f7dfbb;
}
.main {
padding: 20px 22px 24px;
}
.section-head {
margin: 0 0 12px;
display: flex;
justify-content: space-between;
gap: 10px;
align-items: flex-end;
flex-wrap: wrap;
}
.section-head h2 {
margin: 0;
font-size: 19px;
color: #172b4a;
}
.section-head p {
margin: 0;
color: var(--muted);
font-size: 13px;
}
.apps-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px;
margin-bottom: 18px;
}
.app-card {
border: 1px solid var(--line);
border-radius: 14px;
background: #fff;
padding: 14px;
min-height: 200px;
display: flex;
flex-direction: column;
justify-content: space-between;
box-shadow: inset 0 1px 0 #fff;
}
.app-card.primary {
background: linear-gradient(180deg, #ffffff, #f5f9ff);
}
.app-card.red {
background: linear-gradient(180deg, #ffffff, #fff6f6);
}
.accent {
width: 56px;
height: 4px;
border-radius: 999px;
background: var(--brand-blue);
margin-bottom: 10px;
}
.accent.red { background: var(--brand-red); }
.app-title {
margin: 0;
font-size: 22px;
line-height: 1.1;
}
.app-text {
margin: 8px 0 10px;
color: #5a6a81;
font-size: 14px;
line-height: 1.45;
}
.tag-row {
display: flex;
gap: 6px;
flex-wrap: wrap;
margin-bottom: 12px;
}
.tag {
border: 1px solid #d3deed;
border-radius: 999px;
padding: 3px 8px;
background: #f6f9ff;
color: #486183;
font-size: 11px;
font-weight: 700;
}
.card-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.admin-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
}
.admin-card {
border: 1px solid var(--line);
border-radius: 12px;
padding: 11px;
background: linear-gradient(180deg, #ffffff, #f7faff);
}
.admin-card h3 {
margin: 0 0 5px;
font-size: 14px;
color: #18345f;
}
.admin-card p {
margin: 0 0 9px;
font-size: 12px;
color: #607088;
min-height: 30px;
}
.msg {
border-radius: 10px;
padding: 10px 12px;
margin: 0 0 14px;
border: 1px solid #d6e1ef;
background: #f8fbff;
color: #1f3a5f;
font-size: 14px;
}
.msg.error {
border-color: #fecaca;
background: #fff1f2;
color: #991b1b;
}
.footer-note {
margin-top: 16px;
border-top: 1px solid var(--line);
padding-top: 12px;
color: var(--muted);
font-size: 13px;
}
@media (max-width: 1080px) {
.apps-grid { grid-template-columns: 1fr 1fr; }
.admin-grid { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 760px) {
body { padding: 12px; }
.topbar, .hero, .main { padding-left: 14px; padding-right: 14px; }
.hero h1 { font-size: 28px; }
.apps-grid, .admin-grid { grid-template-columns: 1fr; }
.quick-actions { justify-content: flex-start; }
}
</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="quick-actions">
<form method="post" action="/accounts/logout/" style="display:inline;">
{% csrf_token %}
<button class="btn btn-secondary" type="submit">Abmelden</button>
</form>
</div>
</div>
<div class="hero">
<h1>Onboarding/Offboarding Portal</h1>
<p>Zentrale Arbeitsfläche für Anfragen, PDF-Generierung, E-Mail-Workflows und Ablage in Nextcloud.</p>
<div class="status-row">
<span class="status-pill">Rolle: {% if request.user.is_staff %}Admin{% else %}Mitarbeiter{% endif %}</span>
<span class="status-pill {% if nextcloud_enabled %}ok{% else %}warn{% endif %}">
Nextcloud: {% if nextcloud_enabled %}aktiv{% else %}inaktiv{% endif %}
</span>
<span class="status-pill {% if email_test_mode %}warn{% else %}ok{% endif %}">
E-Mail: {% if email_test_mode %}Testmodus{% else %}Produktion{% endif %}
</span>
<span class="status-pill">PDF + Email Workflow Ready</span>
</div>
</div>
<main class="main">
{% if messages %}
{% for message in messages %}
<div class="msg {% if message.tags == 'error' %}error{% endif %}">{{ message }}</div>
{% endfor %}
{% endif %}
<div class="section-head">
<h2>Apps</h2>
<p>Wählen Sie den gewünschten Prozess.</p>
</div>
<div class="apps-grid">
<section class="app-card primary">
<div>
<div class="accent"></div>
<h3 class="app-title">Onboarding</h3>
<p class="app-text">Neue Mitarbeitende erfassen, PDF mit Briefkopf erstellen, Benachrichtigungen senden und in Nextcloud ablegen.</p>
<div class="tag-row">
<span class="tag">Mehrschritt-Formular</span>
<span class="tag">PDF</span>
<span class="tag">E-Mail Routing</span>
</div>
</div>
<div class="card-actions">
<a class="btn btn-primary" href="/onboarding/new/">Onboarding starten</a>
</div>
</section>
<section class="app-card red">
<div>
<div class="accent red"></div>
<h3 class="app-title">Offboarding</h3>
<p class="app-text">Mitarbeitende suchen, Daten vorbefüllen, Offboarding-Dokumente erzeugen und Rückgabe-Prozess starten.</p>
<div class="tag-row">
<span class="tag">Profile-Suche</span>
<span class="tag">Hardware-Liste</span>
<span class="tag">IT-Rückgabe</span>
</div>
</div>
<div class="card-actions">
<a class="btn btn-primary" href="/offboarding/new/">Offboarding starten</a>
</div>
</section>
<section class="app-card">
<div>
<div class="accent"></div>
<h3 class="app-title">Anfragen Dashboard</h3>
<p class="app-text">Status, Suchfunktion, PDF-Links und Verlauf aller Onboarding-/Offboarding-Anfragen.</p>
<div class="tag-row">
<span class="tag">Suche</span>
<span class="tag">Status</span>
<span class="tag">PDF Zugriff</span>
</div>
</div>
<div class="card-actions">
<a class="btn btn-secondary" href="/requests/">Dashboard öffnen</a>
</div>
</section>
</div>
{% if request.user.is_staff %}
<div class="section-head">
<h2>Admin Apps</h2>
<p>Konfiguration, Tests und Steuerung.</p>
</div>
<div class="admin-grid">
<section class="admin-card">
<h3>Form Builder</h3>
<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>Projekt Wiki</h3>
<p>Dokumentation, Architektur und Runbook.</p>
<a class="btn btn-secondary" href="/admin-tools/wiki/">Öffnen</a>
</section>
<section class="admin-card">
<h3>Integrationen</h3>
<p>Nextcloud- und E-Mail-Setup.</p>
<a class="btn btn-secondary" href="/admin-tools/integrations/?kind=nextcloud">Öffnen</a>
</section>
<section class="admin-card">
<h3>Welcome E-Mails</h3>
<p>Geplante Welcome Mails verwalten.</p>
<a class="btn btn-secondary" href="/admin-tools/welcome-emails/">Öffnen</a>
</section>
<section class="admin-card">
<h3>Django Admin</h3>
<p>Vollständige Datenverwaltung.</p>
<a class="btn btn-secondary" href="/admin/">Öffnen</a>
</section>
<section class="admin-card">
<h3>SMTP Einstellungen</h3>
<p>Server und Absender in der Backend-UI.</p>
<a class="btn btn-secondary" href="/admin/workflows/systememailconfig/">Öffnen</a>
</section>
<section class="admin-card">
<h3>Nextcloud schalten</h3>
<p>Aktiv/Inaktiv direkt umschalten.</p>
<form method="post" action="/admin-tools/nextcloud/toggle/" style="display:inline;">
{% csrf_token %}
<button class="btn btn-secondary" type="submit">
{% if nextcloud_enabled %}Deaktivieren{% else %}Aktivieren{% endif %}
</button>
</form>
</section>
<section class="admin-card">
<h3>E-Mail Modus</h3>
<p>Zwischen Testmodus und Produktion wechseln.</p>
<form method="post" action="/admin-tools/email-mode/toggle/" style="display:inline;">
{% csrf_token %}
<button class="btn btn-secondary" type="submit">
Auf {% if email_test_mode %}Produktion{% else %}Testmodus{% endif %}
</button>
</form>
</section>
<section class="admin-card">
<h3>Verbindungstests</h3>
<p>Testupload und Testmail auslösen.</p>
<div class="card-actions">
<form method="post" action="/test/nextcloud/" style="display:inline;">
{% csrf_token %}
<button class="btn btn-secondary" type="submit">Nextcloud-Test</button>
</form>
<form method="post" action="/test/email/" style="display:inline;">
{% csrf_token %}
<button class="btn btn-secondary" type="submit">SMTP-Test</button>
</form>
</div>
</section>
</div>
{% endif %}
<div class="footer-note">
Tipp: Die letzten Vorgänge sehen Sie jederzeit im Anfragen Dashboard.
</div>
</main>
</div>
</body>
</html>

View File

@@ -0,0 +1,348 @@
{% load static %}
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Integrationen Setup</title>
<link rel="stylesheet" href="{% static 'workflows/css/buttons.css' %}" />
<style>
body { margin: 0; font-family: Arial, sans-serif; background: #f4f8ff; color: #0f172a; padding: 20px; }
.shell { max-width: 980px; margin: 0 auto; background: #fff; border: 1px solid #d8e3f0; border-radius: 14px; padding: 16px; }
.topbar { display: flex; justify-content: space-between; align-items: center; gap: 10px; flex-wrap: wrap; }
.brand-logo { width: 190px; max-width: 100%; height: auto; display: block; }
h1 { margin: 12px 0 6px; color: #000078; }
.sub { margin: 0 0 12px; color: #54657c; }
.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;
}
.switch .tab.active { background: #000078; color: #fff; border-color: #000078; }
.msg { border-radius: 10px; padding: 10px 12px; margin: 0 0 12px; border: 1px solid #d6e1ef; background: #f8fbff; color: #1f3a5f; }
.msg.error { border-color: #fecaca; background: #fff1f2; color: #991b1b; }
.card { border: 1px solid #d8e3f0; border-radius: 12px; background: #fbfdff; padding: 12px; }
.grid { display: grid; grid-template-columns: repeat(2, minmax(240px, 1fr)); gap: 10px; }
label { display: block; margin-bottom: 4px; font-size: 12px; color: #334155; font-weight: 700; }
input { width: 100%; box-sizing: border-box; border: 1px solid #cbd5e1; border-radius: 8px; padding: 8px 9px; }
select { width: 100%; box-sizing: border-box; border: 1px solid #cbd5e1; border-radius: 8px; padding: 8px 9px; background: #fff; }
.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 input[type="checkbox"] { width: auto; }
.actions { margin-top: 10px; display: flex; gap: 8px; flex-wrap: wrap; }
.hint { margin-top: 6px; color: #64748b; font-size: 12px; }
.template-block {
border: 1px solid #d8e3f0;
border-radius: 10px;
background: #fff;
padding: 10px;
margin-top: 10px;
}
.template-title {
margin: 0 0 8px;
color: #24344e;
font-weight: 700;
font-size: 14px;
}
.rule-card {
margin-top: 12px;
border: 1px solid #d8e3f0;
border-radius: 12px;
padding: 10px;
background: #fff;
}
.rule-title {
margin: 0 0 8px;
color: #23344f;
font-weight: 700;
font-size: 14px;
}
textarea {
width: 100%;
box-sizing: border-box;
border: 1px solid #cbd5e1;
border-radius: 8px;
padding: 8px 9px;
min-height: 120px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 12px;
}
@media (max-width: 760px) { .grid { grid-template-columns: 1fr; } }
</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>
<h1>Integrationen Setup</h1>
<p class="sub">Verwalten Sie Nextcloud- und Mail-Konfiguration ohne Backend-Wechsel.</p>
<div class="switch">
<a class="tab {% if kind == 'nextcloud' %}active{% endif %}" href="/admin-tools/integrations/?kind=nextcloud">Setup Nextcloud</a>
<a class="tab {% if kind == 'mail' %}active{% endif %}" href="/admin-tools/integrations/?kind=mail">Setup Mail</a>
<a class="tab {% if kind == 'emails' %}active{% endif %}" href="/admin-tools/integrations/?kind=emails">E-Mail Routing & Vorlagen</a>
</div>
{% if messages %}
{% for message in messages %}
<div class="msg {% if message.tags == 'error' %}error{% endif %}">{{ message }}</div>
{% endfor %}
{% endif %}
{% if kind == 'nextcloud' %}
<form class="card" method="post" action="/admin-tools/integrations/save-nextcloud/">
{% csrf_token %}
<div class="grid">
<div>
<label for="nc_base">NEXTCLOUD_BASE_URL</label>
<input id="nc_base" name="nextcloud_base_url_override" value="{{ workflow_config.nextcloud_base_url_override }}" />
</div>
<div>
<label for="nc_dir">NEXTCLOUD_DIRECTORY</label>
<input id="nc_dir" name="nextcloud_directory_override" value="{{ workflow_config.nextcloud_directory_override }}" />
</div>
<div>
<label for="nc_user">NEXTCLOUD_USERNAME</label>
<input id="nc_user" name="nextcloud_username_override" value="{{ workflow_config.nextcloud_username_override }}" />
</div>
<div>
<label for="nc_pass">NEXTCLOUD_PASSWORD</label>
<input id="nc_pass" name="nextcloud_password_override" type="password" placeholder="Leer lassen = unverändert" />
</div>
<div>
<label for="sync_interval">SYNC_INTERVAL (Sekunden)</label>
<input id="sync_interval" name="sync_interval_seconds" type="number" min="10" step="1" value="{{ workflow_config.sync_interval_seconds }}" />
</div>
</div>
<div class="actions">
<button class="btn btn-primary" type="submit">Nextcloud speichern</button>
</div>
<div class="hint">Leeres Passwortfeld lässt das bestehende Passwort unverändert.</div>
</form>
{% endif %}
{% if kind == 'mail' %}
<form class="card" method="post" action="/admin-tools/integrations/save-mail/">
{% csrf_token %}
<div class="grid">
<div>
<label for="imap_server">IMAP_SERVER</label>
<input id="imap_server" name="imap_server" value="{{ workflow_config.imap_server }}" />
</div>
<div>
<label for="mailbox">MAILBOX</label>
<input id="mailbox" name="mailbox" value="{{ workflow_config.mailbox }}" />
</div>
<div>
<label for="smtp_server">SMTP_SERVER</label>
<input id="smtp_server" name="smtp_server" value="{{ workflow_config.smtp_server }}" />
</div>
<div>
<label for="smtp_port">EMAIL_PORT</label>
<input id="smtp_port" name="smtp_port" type="number" min="1" step="1" value="{{ workflow_config.smtp_port }}" />
</div>
<div>
<label for="email_account">EMAIL_ACCOUNT</label>
<input id="email_account" name="email_account" value="{{ workflow_config.email_account }}" />
</div>
<div>
<label for="email_password">PASSWORD</label>
<input id="email_password" name="email_password" type="password" placeholder="Leer lassen = unverändert" />
</div>
</div>
<div class="check-row">
<label><input type="checkbox" name="smtp_use_ssl" {% if workflow_config.smtp_use_ssl %}checked{% endif %} /> SMTP SSL</label>
<label><input type="checkbox" name="smtp_use_tls" {% if workflow_config.smtp_use_tls %}checked{% endif %} /> SMTP TLS</label>
</div>
<div class="actions">
<button class="btn btn-primary" type="submit">Mail speichern</button>
</div>
<div class="hint">Leeres Passwortfeld lässt das bestehende Passwort unverändert.</div>
</form>
{% endif %}
{% if kind == 'emails' %}
<form class="card" method="post" action="/admin-tools/integrations/save-emails/">
{% csrf_token %}
<div class="grid">
<div>
<label for="it_onboarding_email">It onboarding email</label>
<input id="it_onboarding_email" name="it_onboarding_email" value="{{ workflow_config.it_onboarding_email }}" />
</div>
<div>
<label for="general_info_email">General info email</label>
<input id="general_info_email" name="general_info_email" value="{{ workflow_config.general_info_email }}" />
</div>
<div>
<label for="business_card_email">Business card email</label>
<input id="business_card_email" name="business_card_email" value="{{ workflow_config.business_card_email }}" />
</div>
<div>
<label for="hr_works_email">Hr works email</label>
<input id="hr_works_email" name="hr_works_email" value="{{ workflow_config.hr_works_email }}" />
</div>
<div>
<label for="key_notification_email">Key notification email</label>
<input id="key_notification_email" name="key_notification_email" value="{{ workflow_config.key_notification_email }}" />
</div>
</div>
<div class="hint">Diese Empfänger werden für condition-based E-Mail Routing genutzt.</div>
{% for tpl in templates %}
<div class="template-block">
<p class="template-title">{{ tpl.get_key_display }} ({{ tpl.key }})</p>
<div class="grid">
<div>
<label for="subject_{{ tpl.key }}">Subject</label>
<input id="subject_{{ tpl.key }}" name="subject_{{ tpl.key }}" value="{{ tpl.subject_template }}" />
</div>
<div>
<label for="body_{{ tpl.key }}">Body</label>
<textarea id="body_{{ tpl.key }}" name="body_{{ tpl.key }}">{{ tpl.body_template }}</textarea>
</div>
</div>
</div>
{% endfor %}
<div class="actions">
<button class="btn btn-primary" type="submit">E-Mail Routing & Vorlagen speichern</button>
</div>
</form>
<form class="rule-card" method="post" action="/admin-tools/integrations/save-rules/">
{% csrf_token %}
<p class="rule-title">Bedingungsregeln für zusätzliche E-Mails</p>
<div class="hint">Zusätzliche Regeln laufen nach dem Standard-Routing.</div>
{% for rule in notification_rules %}
<div class="template-block">
<input type="hidden" name="rule_ids" value="{{ rule.id }}" />
<div class="grid">
<div>
<label for="name_{{ rule.id }}">Regelname</label>
<input id="name_{{ rule.id }}" name="name_{{ rule.id }}" value="{{ rule.name }}" />
</div>
<div>
<label for="event_type_{{ rule.id }}">Event</label>
<select id="event_type_{{ rule.id }}" name="event_type_{{ rule.id }}">
{% for key, label in rule_event_choices %}
<option value="{{ key }}" {% if key == rule.event_type %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
<div>
<label for="field_name_{{ rule.id }}">Feldname</label>
<input id="field_name_{{ rule.id }}" name="field_name_{{ rule.id }}" value="{{ rule.field_name }}" placeholder="z. B. needed_devices" />
</div>
<div>
<label for="operator_{{ rule.id }}">Operator</label>
<select id="operator_{{ rule.id }}" name="operator_{{ rule.id }}">
{% for key, label in rule_operator_choices %}
<option value="{{ key }}" {% if key == rule.operator %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
<div>
<label for="expected_value_{{ rule.id }}">Vergleichswert</label>
<input id="expected_value_{{ rule.id }}" name="expected_value_{{ rule.id }}" value="{{ rule.expected_value }}" placeholder="z. B. Schlüssel" />
</div>
<div>
<label for="recipients_{{ rule.id }}">Empfänger</label>
<input id="recipients_{{ rule.id }}" name="recipients_{{ rule.id }}" value="{{ rule.recipients }}" placeholder="a@b.de, c@d.de" />
</div>
<div>
<label for="template_key_{{ rule.id }}">Template Key (optional)</label>
<select id="template_key_{{ rule.id }}" name="template_key_{{ rule.id }}">
<option value="">-- Custom Betreff/Body verwenden --</option>
{% for key, label in template_choices %}
<option value="{{ key }}" {% if key == rule.template_key %}selected{% endif %}>{{ label }} ({{ key }})</option>
{% endfor %}
</select>
</div>
<div>
<label for="custom_subject_{{ rule.id }}">Custom Subject (optional)</label>
<input id="custom_subject_{{ rule.id }}" name="custom_subject_{{ rule.id }}" value="{{ rule.custom_subject }}" />
</div>
<div>
<label for="custom_body_{{ rule.id }}">Custom Body (optional)</label>
<textarea id="custom_body_{{ rule.id }}" name="custom_body_{{ rule.id }}">{{ rule.custom_body }}</textarea>
</div>
</div>
<div class="check-row">
<label><input type="checkbox" name="active_{{ rule.id }}" {% if rule.is_active %}checked{% endif %} /> Aktiv</label>
<label><input type="checkbox" name="include_pdf_{{ rule.id }}" {% if rule.include_pdf_attachment %}checked{% endif %} /> PDF anhängen</label>
<label><input type="checkbox" name="delete_{{ rule.id }}" /> Löschen</label>
</div>
</div>
{% empty %}
<div class="hint">Noch keine zusätzlichen Regeln vorhanden.</div>
{% endfor %}
<div class="template-block">
<p class="template-title">Neue Regel hinzufügen</p>
<div class="grid">
<div>
<label for="new_name">Regelname</label>
<input id="new_name" name="new_name" placeholder="z. B. Extra Schlüssel-Mail" />
</div>
<div>
<label for="new_event_type">Event</label>
<select id="new_event_type" name="new_event_type">
{% for key, label in rule_event_choices %}
<option value="{{ key }}">{{ label }}</option>
{% endfor %}
</select>
</div>
<div>
<label for="new_field_name">Feldname</label>
<input id="new_field_name" name="new_field_name" placeholder="z. B. needed_devices" />
</div>
<div>
<label for="new_operator">Operator</label>
<select id="new_operator" name="new_operator">
{% for key, label in rule_operator_choices %}
<option value="{{ key }}">{{ label }}</option>
{% endfor %}
</select>
</div>
<div>
<label for="new_expected_value">Vergleichswert</label>
<input id="new_expected_value" name="new_expected_value" placeholder="z. B. Schlüssel" />
</div>
<div>
<label for="new_recipients">Empfänger</label>
<input id="new_recipients" name="new_recipients" placeholder="a@b.de, c@d.de" />
</div>
<div>
<label for="new_template_key">Template Key (optional)</label>
<select id="new_template_key" name="new_template_key">
<option value="">-- Custom Betreff/Body verwenden --</option>
{% for key, label in template_choices %}
<option value="{{ key }}">{{ label }} ({{ key }})</option>
{% endfor %}
</select>
</div>
<div>
<label for="new_custom_subject">Custom Subject (optional)</label>
<input id="new_custom_subject" name="new_custom_subject" />
</div>
<div>
<label for="new_custom_body">Custom Body (optional)</label>
<textarea id="new_custom_body" name="new_custom_body"></textarea>
</div>
</div>
<div class="check-row">
<label><input type="checkbox" name="new_include_pdf" /> PDF anhängen</label>
</div>
</div>
<div class="actions">
<button class="btn btn-primary" type="submit">Regeln speichern</button>
</div>
</form>
{% endif %}
</div>
</body>
</html>

View File

@@ -0,0 +1,66 @@
{% load static %}
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Offboarding-Anfrage</title>
<link rel="stylesheet" href="{% static 'workflows/css/buttons.css' %}" />
<link rel="stylesheet" href="{% static 'workflows/css/offboarding_form.css' %}" />
</head>
<body>
<div id="saved-popup" class="popup-backdrop {% if saved %}show{% endif %}">
<div class="popup">
<h3>Anfrage gespeichert</h3>
<p>Offboarding wurde erfolgreich gespeichert (ID: {{ saved_request_id }}). Das PDF wird im Hintergrund erzeugt.</p>
<button class="btn btn-secondary" type="button" onclick="document.getElementById('saved-popup').classList.remove('show')">Schließen</button>
</div>
</div>
<div class="wrap">
<img class="brand-logo" src="https://tub.co/media/site/0856bfc615-1750234287/tubco-wortbildmarke.svg" alt="TUB/CO Logo" />
<div class="top-link"><a class="btn btn-secondary" href="/">Zur Startseite</a></div>
<div class="card">
<h1>Offboarding-Anfrage</h1>
<form method="get" action="/offboarding/new/">
<div class="field">
<label for="q">Mitarbeitende suchen (Name oder E-Mail)</label>
<input id="q" name="q" value="{{ search_query }}" placeholder="z. B. max.mustermann@tub.co" />
</div>
<button class="btn btn-primary" type="submit">Suchen</button>
</form>
{% if search_results %}
<div class="results" style="margin-top:10px;">
{% for p in search_results %}
<a href="/offboarding/new/?profile={{ p.id }}">{{ p.full_name }} ({{ p.work_email }})</a>
{% endfor %}
</div>
{% endif %}
{% if selected_profile %}
<p style="margin-top:10px; color:#2563eb;">Vorbefüllt aus: <strong>{{ selected_profile.full_name }}</strong> ({{ selected_profile.work_email }})</p>
{% endif %}
</div>
<div class="card">
<form method="post">
{% csrf_token %}
<div class="grid">
{% for field in form.visible_fields %}
{% if field.name != 'search_query' %}
<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>
<button class="btn btn-primary" type="submit">Offboarding-Anfrage speichern</button>
</form>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,27 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Offboarding gespeichert</title>
<style>
body { font-family: Arial, sans-serif; margin: 24px; }
code { background: #f4f4f4; padding: 2px 6px; }
</style>
</head>
<body>
<h1>Offboarding gespeichert</h1>
<p>Vorgangs-ID: <code>{{ obj.id }}</code></p>
<p>Name: <code>{{ obj.full_name }}</code></p>
<p>E-Mail: <code>{{ obj.work_email }}</code></p>
<p>Letzter Arbeitstag: <code>{{ obj.last_working_day }}</code></p>
{% if pdf_url %}
<p>PDF: <a href="{{ pdf_url }}" target="_blank" rel="noopener">PDF öffnen</a></p>
<p>Datei: <code>{{ obj.generated_pdf_path }}</code></p>
{% else %}
<p>PDF wird im Hintergrund erstellt.</p>
{% endif %}
<p><a href="/">Zur Startseite</a></p>
<p><a href="/offboarding/new/">Neue Offboarding-Anfrage erfassen</a></p>
</body>
</html>

View File

@@ -0,0 +1,341 @@
{% load static %}
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Onboarding-Anfrage</title>
<link rel="stylesheet" href="{% static 'workflows/css/buttons.css' %}" />
<link rel="stylesheet" href="{% static 'workflows/css/onboarding_form.css' %}" />
</head>
<body>
<div id="saved-popup" class="popup-backdrop {% if saved %}show{% endif %}">
<div class="popup">
<h3>Anfrage gespeichert</h3>
<p>Onboarding wurde erfolgreich gespeichert (ID: {{ saved_request_id }}). Das PDF wird im Hintergrund erzeugt.</p>
<button class="btn btn-secondary" type="button" onclick="document.getElementById('saved-popup').classList.remove('show')">Schließen</button>
</div>
</div>
<div class="top-wrap">
<img class="brand-logo" src="https://tub.co/media/site/0856bfc615-1750234287/tubco-wortbildmarke.svg" alt="TUB/CO Logo" />
<div class="top-link"><a class="btn btn-secondary" href="/">Zur Startseite</a></div>
</div>
<div class="shell">
<aside class="panel">
<h1>Onboarding</h1>
<p class="sub">Mehrseitiges Formular mit konfigurierbaren Feldern aus dem Admin.</p>
<ol class="step-list">
{% 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 }}">
<span class="dot">{{ forloop.counter }}</span>
<div>
<div class="step-title">{{ section.title }}</div>
<div class="step-sub">{{ section.subtitle }}</div>
</div>
</li>
{% endfor %}
</ol>
</aside>
<main class="main">
{% if form.errors %}
<div class="error-banner">Bitte prüfen Sie die markierten Felder. Ungültige Eingaben wurden erkannt.</div>
{% endif %}
<form method="post" id="onboarding-form" enctype="multipart/form-data">
{% csrf_token %}
{% for section in onboarding_sections %}
<section class="page {% if forloop.first %}active{% endif %}" data-step="{{ forloop.counter }}">
<div class="section-card section-{{ section.key }}">
<div class="section-head">
<h2>{{ section.title }}</h2>
<p>{{ section.subtitle }}</p>
</div>
<div class="grid-2">
{% for block in section.blocks %}
{% if block.kind == 'field' %}
{% with field=block.field %}
{% if field.is_hidden %}
{{ field }}
{% elif field.name in onboarding_inline_checks %}
<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 %}
{{ field.errors }}
</div>
{% else %}
<div class="field {% if section.key == 'abschluss' %}finish-field{% endif %} {% if field.name in onboarding_checkbox_lists or field.name == 'gender' %}field-full{% endif %} {% if field.name in onboarding_checkbox_lists %}checkbox-list{% endif %}">
{{ field.label_tag }}
{{ field }}
{% if field.help_text %}<div class="hint">{{ field.help_text }}</div>{% endif %}
{{ field.errors }}
</div>
{% endif %}
{% endwith %}
{% else %}
<div id="{{ block.id }}" class="field-group field-full {% if block.hidden_default %}hidden{% endif %}">
<div class="grid-2">
{% for field in block.fields %}
{% if field.is_hidden %}
{{ field }}
{% elif field.name in onboarding_inline_checks %}
<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 %}
{{ field.errors }}
</div>
{% else %}
<div class="field {% if section.key == 'abschluss' %}finish-field{% endif %} {% if field.name in onboarding_checkbox_lists or field.name == 'gender' %}field-full{% endif %} {% if field.name in onboarding_checkbox_lists %}checkbox-list{% endif %}">
{{ field.label_tag }}
{{ field }}
{% if field.help_text %}<div class="hint">{{ field.help_text }}</div>{% endif %}
{{ field.errors }}
</div>
{% endif %}
{% endfor %}
</div>
</div>
{% endif %}
{% endfor %}
{% if not section.blocks %}
<div class="field field-full empty-step">Keine konfigurierten Felder in diesem Schritt.</div>
{% endif %}
{% if section.key == 'abschluss' %}
<div class="field-full finish-note">
Fast geschafft. Bitte Abschlussdaten prüfen und die Anfrage absenden.
</div>
<div class="field-full">
<div class="legal">{{ legal_text }}</div>
</div>
{% endif %}
</div>
</div>
</section>
{% endfor %}
<div class="actions">
<button class="btn btn-secondary" type="button" id="btn-prev">Zurück</button>
<button class="btn btn-primary" type="button" id="btn-next">Weiter</button>
<button type="submit" id="btn-submit" class="btn btn-primary hidden">Onboarding-Anfrage absenden</button>
</div>
</form>
</main>
</div>
<script>
(function () {
const pages = Array.from(document.querySelectorAll('.page'));
const navItems = Array.from(document.querySelectorAll('.step-item'));
const btnPrev = document.getElementById('btn-prev');
const btnNext = document.getElementById('btn-next');
const btnSubmit = document.getElementById('btn-submit');
const form = document.getElementById('onboarding-form');
let current = 0;
form.setAttribute('novalidate', 'novalidate');
function byName(name) { return document.querySelector('[name="' + name + '"]'); }
function toggle(id, state) {
const el = document.getElementById(id);
if (!el) return;
el.classList.toggle('hidden', !state);
}
function syncConditionals() {
const orderCards = byName('order_business_cards');
toggle('business-card-box', orderCards && orderCards.checked);
const employmentType = byName('employment_type');
toggle('employment-end-box', employmentType && employmentType.value === 'befristet');
const groupMailbox = byName('group_mailboxes_required_choice');
toggle('group-mailboxes-box', groupMailbox && groupMailbox.value === 'ja');
const extraHardware = byName('additional_hardware_needed_choice');
toggle('extra-hardware-box', extraHardware && extraHardware.value === 'ja');
const extraSoftware = byName('additional_software_needed_choice');
toggle('extra-software-box', extraSoftware && extraSoftware.value === 'ja');
const extraAccess = byName('additional_access_needed_choice');
toggle('extra-access-box', extraAccess && extraAccess.value === 'ja');
const successor = byName('successor_required_choice');
const showSuccessor = successor && successor.value === 'ja';
toggle('successor-box', showSuccessor);
const inheritPhone = byName('inherit_phone_number_choice');
const hidePhone = showSuccessor && inheritPhone && inheritPhone.value === 'ja';
toggle('phone-box', !hidePhone);
// Hidden conditional groups must not block submit with invisible required fields.
document.querySelectorAll('.field-group').forEach(function (group) {
const hidden = group.classList.contains('hidden');
group.querySelectorAll('input, select, textarea').forEach(function (el) {
if (el.type === 'hidden' || el.disabled) return;
if (hidden) {
if (el.required) {
el.dataset.requiredOriginal = '1';
el.required = false;
}
} else if (el.dataset.requiredOriginal === '1') {
el.required = true;
}
});
});
}
function slugifyForEmail(value) {
const map = { 'ä': 'ae', 'ö': 'oe', 'ü': 'ue', 'ß': 'ss' };
const lower = (value || '').toLowerCase();
const mapped = lower.replace(/[äöüß]/g, function (m) { return map[m] || m; });
return mapped
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/[^a-z0-9]+/g, '.')
.replace(/^\.+|\.+$/g, '')
.replace(/\.{2,}/g, '.');
}
function setupWorkEmailAutofill() {
const lastName = byName('last_name');
const workEmail = byName('work_email');
if (!lastName || !workEmail) return;
let lastSuggested = '';
let userEditedEmail = !!(workEmail.value && workEmail.value.trim());
function suggestEmail() {
const slug = slugifyForEmail(lastName.value);
if (!slug) return;
const suggestion = slug + '@tub.co';
if (!userEditedEmail || workEmail.value === '' || workEmail.value === lastSuggested) {
workEmail.value = suggestion;
lastSuggested = suggestion;
}
}
workEmail.addEventListener('input', function () {
const current = (workEmail.value || '').trim();
userEditedEmail = current !== '' && current !== lastSuggested;
});
lastName.addEventListener('input', suggestEmail);
suggestEmail();
}
function setupBusinessCardAutofill() {
const checkbox = byName('order_business_cards');
const firstName = byName('first_name');
const lastName = byName('last_name');
const jobTitle = byName('job_title');
const workEmail = byName('work_email');
const cardName = byName('business_card_name');
const cardTitle = byName('business_card_title');
const cardEmail = byName('business_card_email');
if (!checkbox || !cardName || !cardTitle || !cardEmail) return;
const cardBox = document.getElementById('business-card-box');
function suggestionName() {
const first = (firstName && firstName.value || '').trim();
const last = (lastName && lastName.value || '').trim();
return [first, last].filter(Boolean).join(' ').trim();
}
function setField(field, value, force) {
if (!field || !value) return;
const current = (field.value || '').trim();
const autoMarked = field.dataset.autofilled === '1';
if (force || !current || autoMarked) {
field.value = value;
field.dataset.autofilled = '1';
}
}
function applyDefaults(force) {
if (!checkbox.checked) return;
const name = suggestionName();
const title = (jobTitle && jobTitle.value || '').trim();
const email = (workEmail && workEmail.value || '').trim();
setField(cardName, name, force);
setField(cardTitle, title, force);
setField(cardEmail, email, force);
}
[cardName, cardTitle, cardEmail].forEach(function (field) {
field.addEventListener('input', function () {
field.dataset.autofilled = '0';
});
});
if (cardBox) {
const grid = cardBox.querySelector('.grid-2');
if (grid && !document.getElementById('business-card-autofill-btn')) {
const actionWrap = document.createElement('div');
actionWrap.className = 'field field-full';
const btn = document.createElement('button');
btn.type = 'button';
btn.id = 'business-card-autofill-btn';
btn.className = 'btn btn-secondary';
btn.textContent = 'Visitenkarten-Felder automatisch ausfüllen';
btn.addEventListener('click', function () {
applyDefaults(true);
});
actionWrap.appendChild(btn);
grid.insertBefore(actionWrap, grid.firstChild);
}
}
// Manual-only behavior: fill only when button is clicked.
}
function updateStep() {
pages.forEach((p, i) => p.classList.toggle('active', i === current));
navItems.forEach((n, i) => n.classList.toggle('active', i === current));
btnPrev.disabled = current === 0;
const last = current === pages.length - 1;
btnNext.classList.toggle('hidden', last);
btnSubmit.classList.toggle('hidden', !last);
}
function jumpToFirstErrorPage() {
const firstError = document.querySelector('.errorlist');
if (!firstError) return;
const page = firstError.closest('.page');
if (!page) return;
const step = Number(page.getAttribute('data-step') || '1') - 1;
current = Math.max(0, step);
}
document.addEventListener('change', syncConditionals);
navItems.forEach((n, idx) => {
n.addEventListener('click', function () { current = idx; updateStep(); });
n.addEventListener('keydown', function (e) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
current = idx;
updateStep();
}
});
});
btnPrev.addEventListener('click', function () { if (current > 0) { current -= 1; updateStep(); } });
btnNext.addEventListener('click', function () { if (current < pages.length - 1) { current += 1; updateStep(); } });
btnSubmit.addEventListener('click', function () {
syncConditionals();
btnSubmit.disabled = true;
btnSubmit.textContent = 'Wird gesendet...';
form.submit();
});
syncConditionals();
setupWorkEmailAutofill();
setupBusinessCardAutofill();
jumpToFirstErrorPage();
updateStep();
})();
</script>
</body>
</html>

View File

@@ -0,0 +1,26 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Onboarding gespeichert</title>
<style>
body { font-family: Arial, sans-serif; margin: 24px; color: #222; }
code { background: #f4f4f4; padding: 2px 6px; }
</style>
</head>
<body>
<h1>Anfrage erfolgreich gespeichert</h1>
<p>Vorgangs-ID: <code>{{ obj.id }}</code></p>
<p>Name: <code>{{ obj.full_name }}</code></p>
<p>E-Mail: <code>{{ obj.work_email }}</code></p>
{% if pdf_url %}
<p>PDF: <a href="{{ pdf_url }}" target="_blank" rel="noopener">PDF öffnen</a></p>
<p>Datei: <code>{{ obj.generated_pdf_path }}</code></p>
{% else %}
<p>PDF wird im Hintergrund erstellt.</p>
{% endif %}
<p><a href="/">Zur Startseite</a></p>
<p><a href="/onboarding/new/">Neue Anfrage erfassen</a></p>
</body>
</html>

View File

@@ -0,0 +1,246 @@
{% load static %}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Project Wiki</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: 1120px; margin: 0 auto; background: #fff; border: 1px solid #d7e0ea; border-radius: 14px; padding: 18px; }
.brand-logo { width: 190px; max-width: 100%; height: auto; margin: 0 0 10px; display: block; }
.top { display: flex; justify-content: space-between; gap: 10px; align-items: center; flex-wrap: wrap; margin-bottom: 8px; }
h1 { margin: 0; color: #000078; font-size: 30px; }
.sub { margin: 8px 0 16px; color: #5f6f85; }
.toc { border: 1px solid #d7e0ea; border-radius: 10px; padding: 10px; background: #f7fbff; margin-bottom: 16px; }
.toc a { color: #0b4da2; text-decoration: none; margin-right: 10px; white-space: nowrap; }
h2 { margin: 20px 0 8px; color: #113a74; border-bottom: 1px solid #e1e8f2; padding-bottom: 4px; }
h3 { margin: 14px 0 6px; color: #183f77; }
ul { margin: 8px 0 12px 20px; }
li { margin: 4px 0; }
code { background: #f1f5fb; border: 1px solid #dce6f3; border-radius: 6px; padding: 2px 6px; }
.box { border: 1px solid #d7e0ea; border-radius: 10px; padding: 10px; background: #fcfdff; margin: 8px 0 12px; }
.note { border-left: 4px solid #000078; padding: 8px 10px; background: #f4f8ff; margin: 10px 0; }
</style>
</head>
<body>
<div class="shell">
<img class="brand-logo" src="https://tub.co/media/site/0856bfc615-1750234287/tubco-wortbildmarke.svg" alt="TUB/CO Logo" />
<div class="top">
<h1>Project Wiki</h1>
<a class="btn btn-secondary" href="/">Back to Home</a>
</div>
<p class="sub">Operational and technical documentation for the Onboarding/Offboarding platform.</p>
<div class="toc">
<a href="#overview">Overview</a>
<a href="#architecture">Architecture</a>
<a href="#model">Data Model</a>
<a href="#onboarding">Onboarding Flow</a>
<a href="#offboarding">Offboarding Flow</a>
<a href="#emails">Email Engine</a>
<a href="#pdfs">PDF Engine</a>
<a href="#integrations">Integrations</a>
<a href="#admin">Admin Apps</a>
<a href="#operations">Operations</a>
<a href="#hardening">Hardening</a>
<a href="#troubleshooting">Troubleshooting</a>
<a href="#security">Security</a>
</div>
<h2 id="overview">1) Overview</h2>
<div class="box">
<p>
This system handles employee onboarding and offboarding requests from web forms. It generates branded PDF documents,
sends condition-based notifications, stores records in the database, and optionally uploads PDFs to Nextcloud.
</p>
<ul>
<li>Primary users: regular staff (submit forms) and admin users (configure and operate).</li>
<li>Main entry points: Home, Onboarding form, Offboarding form, Requests Dashboard, Admin Apps.</li>
<li>Asynchronous processing: heavy tasks (PDF/email/upload) are executed by Celery worker jobs.</li>
</ul>
</div>
<h2 id="architecture">2) Architecture</h2>
<h3>Runtime Components</h3>
<ul>
<li><strong>Web:</strong> Django app served by Gunicorn</li>
<li><strong>Worker:</strong> Celery worker for async processing</li>
<li><strong>Broker:</strong> Redis (Celery queue)</li>
<li><strong>DB:</strong> PostgreSQL</li>
<li><strong>Email Sink (test):</strong> MailHog (when test mode is active)</li>
</ul>
<h3>Request Processing Pattern</h3>
<ul>
<li>User submits form in web UI.</li>
<li>Form data is validated and stored as request object.</li>
<li>Background task is queued with request ID.</li>
<li>Worker generates PDF, sends notifications, uploads to Nextcloud (if enabled), and updates record.</li>
</ul>
<h2 id="model">3) Data Model (Key Entities)</h2>
<ul>
<li><code>OnboardingRequest</code>: onboarding data, generated PDF path, requester info, signature file/url, conditional fields.</li>
<li><code>OffboardingRequest</code>: offboarding data, requester info, generated PDF path.</li>
<li><code>EmployeeProfile</code>: searchable profile for offboarding prefill.</li>
<li><code>WorkflowConfig</code>: routing emails, integration overrides, feature flags, welcome email delay.</li>
<li><code>NotificationTemplate</code>: subject/body templates with placeholders.</li>
<li><code>NotificationRule</code>: condition-based custom routing rules.</li>
<li><code>FormFieldConfig</code> + <code>FormOption</code>: Form Builder configuration and selectable options.</li>
<li><code>ScheduledWelcomeEmail</code>: delayed welcome email queue state.</li>
</ul>
<h2 id="onboarding">4) Onboarding Flow</h2>
<ol>
<li>User opens <code>/onboarding/new/</code> and completes multi-step form.</li>
<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>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>
</ol>
<h2 id="offboarding">5) Offboarding Flow</h2>
<ol>
<li>User opens <code>/offboarding/new/</code> and can search existing profile first.</li>
<li>Form saves request with requester name/email from logged-in user.</li>
<li>Task <code>process_offboarding_request</code> runs in worker.</li>
<li>PDF is generated (hardware section can be derived from latest onboarding request).</li>
<li>Notification emails are sent (default + rules).</li>
<li>PDF upload to Nextcloud runs if enabled.</li>
</ol>
<h2 id="emails">6) Email Engine</h2>
<h3>Modes</h3>
<ul>
<li><strong>Production mode:</strong> real recipients are used.</li>
<li><strong>Test mode:</strong> recipients are redirected to test mailbox; original recipients are included in body.</li>
</ul>
<h3>Template Placeholders</h3>
<p>Examples: <code>{{ VORNAME }}</code>, <code>{{ NACHNAME }}</code>, <code>{{ FULL_NAME }}</code>, <code>{{ EMAIL }}</code>, <code>{{ DEPARTMENT }}</code>, <code>{{ CONTRACT_START }}</code>.</p>
<h3>Condition-based Rules</h3>
<ul>
<li>Configured in Admin Integrations page.</li>
<li>Rule supports operators such as <code>always</code>, <code>equals</code>, <code>contains</code>, <code>is_true</code>, <code>is_false</code>.</li>
<li>Can use template-based emails or custom subject/body.</li>
</ul>
<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>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>
</ul>
<h2 id="integrations">8) Integrations</h2>
<h3>Nextcloud</h3>
<ul>
<li>Configured from Admin Integrations UI (base URL, user, password, target directory).</li>
<li>Can be globally enabled/disabled from Home Admin Apps.</li>
</ul>
<h3>Mail Server</h3>
<ul>
<li>SMTP host/port/account configured in Admin Integrations UI / backend config.</li>
<li>Use SMTP test action before switching to production mode.</li>
</ul>
<h2 id="admin">9) Admin Apps (Home)</h2>
<ul>
<li><strong>Form Builder:</strong> manage field visibility/order/options.</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>Project Wiki:</strong> this documentation page.</li>
</ul>
<h2 id="operations">10) Operations Runbook</h2>
<h3>Health Checks</h3>
<ul>
<li>App endpoint: <code>/healthz/</code></li>
<li>If <code>db=error</code>, verify DB container and connection settings.</li>
</ul>
<h3>Where to Find Generated PDFs</h3>
<ul>
<li>Container path: <code>/app/media/pdfs/</code></li>
<li>Host path: project <code>backend/media/pdfs/</code> via mounted volume.</li>
</ul>
<h3>Deployment Notes</h3>
<ul>
<li>Use Docker Compose for web + worker + db + redis services.</li>
<li>After template/form/task changes, restart web and worker containers.</li>
<li>Run <code>python manage.py check</code> before release.</li>
</ul>
<h2 id="hardening">11) Security & Reliability Hardening (Current)</h2>
<ul>
<li><strong>Cookie + header hardening:</strong> HTTPOnly cookies, SameSite cookies, <code>X-Content-Type-Options: nosniff</code>, stricter referrer policy, and frame protection.</li>
<li><strong>Optional secure-cookie mode:</strong> can be enabled via environment for HTTPS deployments.</li>
<li><strong>Upload guards:</strong> server-side upload size limits plus signature image magic-byte validation for PNG/JPEG.</li>
<li><strong>SMTP reliability:</strong> explicit SMTP timeout to avoid hanging worker/web on mail network issues.</li>
<li><strong>Nextcloud reliability:</strong> retry/backoff on upload errors, bounded timeouts, and graceful failure return instead of crashing flow.</li>
<li><strong>Filename safety:</strong> PDF filenames are sanitized to safe filesystem characters.</li>
<li><strong>Least privilege runtime:</strong> web and worker containers run as non-root <code>app</code> user.</li>
</ul>
<div class="note">
Recommended for production: set secure cookies, explicit allowed hosts, CSRF trusted origins, and a strong secret key via environment variables.
</div>
<h2 id="troubleshooting">12) Troubleshooting</h2>
<div class="box">
<h3>Browser timeout or page hangs</h3>
<ul>
<li>Try <code>http://127.0.0.1:8088/</code> instead of <code>localhost</code> if local DNS/proxy is unstable.</li>
<li>Check <code>/healthz/</code> and web logs.</li>
</ul>
<h3>Onboarding submit does nothing</h3>
<ul>
<li>Check required/hidden conditional fields and form errors.</li>
<li>Open browser dev tools for JS errors.</li>
</ul>
<h3>PDF not generated</h3>
<ul>
<li>Check Celery worker logs for PDF task errors.</li>
<li>Verify template files exist and media folder permissions are correct.</li>
</ul>
<h3>Email not received</h3>
<ul>
<li>Verify email mode (test vs production).</li>
<li>Run SMTP test from Admin Apps.</li>
<li>Check SMTP settings and worker logs.</li>
</ul>
<h3>Nextcloud upload missing</h3>
<ul>
<li>Verify Nextcloud is enabled.</li>
<li>Test upload from Admin Apps.</li>
<li>Check credentials and destination directory path.</li>
</ul>
</div>
<h2 id="security">13) Security and Access Notes</h2>
<ul>
<li>Do not expose secrets in UI screenshots or logs.</li>
<li>Only staff users should access Admin Apps and this wiki page.</li>
<li>Use environment variables and admin overrides carefully in production.</li>
<li>Prefer production SMTP only after successful test-mode verification.</li>
</ul>
<div class="note">
Last updated for current system behavior as of March 10, 2026.
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,173 @@
{% load static %}
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Anfragen Dashboard</title>
<link rel="stylesheet" href="{% static 'workflows/css/buttons.css' %}" />
<style>
body { margin: 0; font-family: Arial, sans-serif; background: #f7fafc; color: #1f2937; padding: 20px; }
.shell { max-width: 1100px; margin: 0 auto; background: #fff; border: 1px solid #d7e0ea; border-radius: 12px; padding: 18px; }
.brand-logo { width: 190px; max-width: 100%; height: auto; margin-bottom: 10px; display: block; }
.top-actions { margin: 0 0 10px; }
h1 { margin: 0 0 6px; }
.sub { margin: 0 0 14px; color: #5b6b7f; }
.stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 14px; }
.stat { border: 1px solid #e2e8f0; border-radius: 10px; padding: 10px; background: #f9fbff; }
.stat .n { font-size: 22px; font-weight: 700; color: #0f5fcf; }
.stat .l { color: #64748b; font-size: 12px; }
.chart-wrap { border: 1px solid #e2e8f0; border-radius: 10px; padding: 10px; margin-bottom: 14px; background: #fcfdff; }
.chart-title { margin: 0 0 8px; font-size: 13px; color: #334155; }
.chart { display: grid; grid-template-columns: repeat(14, 1fr); gap: 5px; align-items: end; min-height: 120px; }
.bar { background: linear-gradient(180deg, #0f5fcf, #0b7fb8); border-radius: 6px 6px 2px 2px; }
.bar-label { font-size: 10px; color: #64748b; margin-top: 4px; text-align: center; }
.bar-value { font-size: 10px; color: #334155; text-align: center; }
.search { display: flex; gap: 8px; margin: 0 0 14px; }
.search input { flex: 1; border: 1px solid #cbd5e1; border-radius: 8px; padding: 9px 10px; }
table { width: 100%; border-collapse: collapse; font-size: 14px; }
th, td { border: 1px solid #e2e8f0; padding: 8px; text-align: left; }
th { background: #f6f8fb; }
.badge { display: inline-block; padding: 3px 8px; border-radius: 999px; font-size: 12px; font-weight: 700; }
.ok { background: #e8f7ec; color: #1f7a3f; }
.pending { background: #fff6e5; color: #9a6400; }
.toolbar { margin-top: 14px; }
.actions-cell { white-space: nowrap; }
.inline-delete { display: inline; }
.flash { margin: 0 0 12px; padding: 10px; border-radius: 8px; border: 1px solid #dbe5f2; background: #f8fbff; }
.flash.success { border-color: #bfe6c9; background: #edf9f1; color: #116634; }
.flash.warning { border-color: #f5d8a8; background: #fff8ea; color: #8a5a00; }
.flash.error { border-color: #f4c7c7; background: #fff1f1; color: #8e1e1e; }
.bulk-toolbar { display: flex; align-items: center; gap: 10px; margin: 0 0 10px; }
.bulk-info { color: #5b6b7f; font-size: 13px; }
.select-col { width: 42px; text-align: center; }
</style>
</head>
<body>
<div class="shell">
<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="/">Zur Startseite</a>
</div>
<h1>Anfragen Dashboard</h1>
<p class="sub">Neueste Onboarding- und Offboarding-Vorgänge inklusive PDF-Status.</p>
{% if messages %}
{% for message in messages %}
<div class="flash {{ message.tags }}">{{ message }}</div>
{% endfor %}
{% endif %}
<div class="stats">
<div class="stat"><div class="n">{{ onboarding_total }}</div><div class="l">Onboarding gesamt</div></div>
<div class="stat"><div class="n">{{ offboarding_total }}</div><div class="l">Offboarding gesamt</div></div>
<div class="stat"><div class="n">{{ combined_total }}</div><div class="l">Gesamtvorgänge</div></div>
</div>
<div class="chart-wrap">
<p class="chart-title">Aktivität der letzten 14 Tage (Onboarding + Offboarding)</p>
<div class="chart">
{% for p in chart_points %}
<div>
<div class="bar" style="height: {{ p.height }}px;" title="{{ p.label }} | On: {{ p.onboarding }} | Off: {{ p.offboarding }}"></div>
<div class="bar-value">{{ p.total }}</div>
<div class="bar-label">{{ p.label }}</div>
</div>
{% endfor %}
</div>
</div>
<form class="search" method="get" action="/requests/">
<input name="q" value="{{ search_query }}" placeholder="Suche nach Name oder E-Mail" />
<button class="btn btn-primary" type="submit">Suchen</button>
</form>
{% if request.user.is_staff %}
<form method="post" action="/requests/" id="bulk-delete-form" onsubmit="return confirm('Ausgewählte Einträge wirklich löschen?');">
{% csrf_token %}
<div class="bulk-toolbar">
<button class="btn btn-secondary" type="submit">Auswahl löschen</button>
<span class="bulk-info"><span id="selected-count">0</span> ausgewählt</span>
</div>
{% endif %}
<table>
<thead>
<tr>
{% if request.user.is_staff %}<th class="select-col"><input type="checkbox" id="select-all" aria-label="Alle auswählen" /></th>{% endif %}
<th>Typ</th>
<th>Name</th>
<th>E-Mail</th>
<th>Erstellt</th>
<th>Status</th>
<th>PDF</th>
{% if request.user.is_staff %}<th>Aktion</th>{% endif %}
</tr>
</thead>
<tbody>
{% for row in rows %}
<tr>
{% if request.user.is_staff %}
<td class="select-col">
<input type="checkbox" class="row-select" name="selected_requests" value="{{ row.kind_slug }}:{{ row.id }}" aria-label="{{ row.kind }} {{ row.name }}" />
</td>
{% endif %}
<td>{{ row.kind }}</td>
<td>{{ row.name }}</td>
<td>{{ row.work_email }}</td>
<td>{{ row.created_at|date:"Y-m-d H:i" }}</td>
<td>
{% if row.pdf_url %}
<span class="badge ok">{{ row.status }}</span>
{% else %}
<span class="badge pending">{{ row.status }}</span>
{% endif %}
</td>
<td>
{% if row.pdf_url %}
<a href="{{ row.pdf_url }}" target="_blank" rel="noopener">PDF öffnen</a>
{% else %}
-
{% endif %}
</td>
{% if request.user.is_staff %}
<td class="actions-cell">
<button class="btn btn-secondary" type="submit" name="single_delete" value="{{ row.kind_slug }}:{{ row.id }}" onclick="return confirm('Eintrag wirklich löschen?');">Löschen</button>
</td>
{% endif %}
</tr>
{% empty %}
<tr><td colspan="{% if request.user.is_staff %}8{% else %}6{% endif %}">Noch keine Vorgänge vorhanden.</td></tr>
{% endfor %}
</tbody>
</table>
{% if request.user.is_staff %}
</form>
{% endif %}
<div class="toolbar">
<a class="btn btn-secondary" href="/">Zur Startseite</a>
</div>
</div>
{% if request.user.is_staff %}
<script>
(function () {
const selectAll = document.getElementById('select-all');
const rowChecks = Array.from(document.querySelectorAll('.row-select'));
const selectedCount = document.getElementById('selected-count');
if (!selectAll || !selectedCount || !rowChecks.length) return;
function updateCount() {
const checked = rowChecks.filter((c) => c.checked).length;
selectedCount.textContent = String(checked);
selectAll.checked = checked > 0 && checked === rowChecks.length;
selectAll.indeterminate = checked > 0 && checked < rowChecks.length;
}
selectAll.addEventListener('change', function () {
rowChecks.forEach((c) => { c.checked = selectAll.checked; });
updateCount();
});
rowChecks.forEach((c) => c.addEventListener('change', updateCount));
updateCount();
})();
</script>
{% endif %}
</body>
</html>

View File

@@ -0,0 +1,233 @@
{% load static %}
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Welcome E-Mails</title>
<link rel="stylesheet" href="{% static 'workflows/css/buttons.css' %}" />
<style>
body { margin: 0; font-family: Arial, sans-serif; background: #f4f8ff; color: #0f172a; padding: 20px; }
.shell { max-width: 1100px; margin: 0 auto; background: #fff; border: 1px solid #d8e3f0; border-radius: 14px; padding: 16px; }
.topbar { display: flex; justify-content: space-between; align-items: center; gap: 10px; flex-wrap: wrap; }
.brand-logo { width: 190px; max-width: 100%; height: auto; display: block; }
h1 { margin: 12px 0 6px; color: #000078; }
.sub { margin: 0 0 12px; color: #54657c; }
.msg { border-radius: 10px; padding: 10px 12px; margin: 0 0 12px; border: 1px solid #d6e1ef; background: #f8fbff; color: #1f3a5f; }
.msg.error { border-color: #fecaca; background: #fff1f2; color: #991b1b; }
.card { border: 1px solid #d8e3f0; border-radius: 12px; background: #fbfdff; padding: 12px; margin-bottom: 14px; }
.grid { display: grid; grid-template-columns: repeat(2, minmax(260px, 1fr)); gap: 10px; }
label { display: block; margin-bottom: 4px; font-size: 12px; color: #334155; font-weight: 700; }
input, textarea { width: 100%; box-sizing: border-box; border: 1px solid #cbd5e1; border-radius: 8px; padding: 8px 9px; }
textarea { min-height: 120px; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; }
.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 input[type="checkbox"] { width: auto; }
table { width: 100%; border-collapse: collapse; font-size: 14px; }
th, td { border: 1px solid #dce5f1; padding: 8px; text-align: left; vertical-align: top; }
th { background: #f6f9ff; }
.badge { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 12px; font-weight: 700; }
.scheduled { background: #eff6ff; color: #1d4ed8; }
.paused { background: #fef9c3; color: #854d0e; }
.cancelled { background: #f1f5f9; color: #334155; }
.sent { background: #ecfdf3; color: #166534; }
.failed { background: #fff1f2; color: #991b1b; }
.actions { margin-top: 10px; display: flex; gap: 8px; flex-wrap: wrap; }
.table-actions { display: flex; gap: 6px; flex-wrap: wrap; }
.hint { margin-top: 6px; color: #64748b; font-size: 12px; }
.bulk-bar { margin: 0 0 10px; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.bulk-bar select { width: auto; min-width: 180px; }
.select-col { width: 42px; text-align: center; }
.bulk-note { color: #64748b; font-size: 12px; }
</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>
<h1>Geplante Welcome E-Mails</h1>
<p class="sub">Welcome-Mails konfigurieren und geplante Mails steuern (sofort senden, pausieren, fortsetzen, abbrechen).</p>
{% if messages %}
{% for message in messages %}
<div class="msg {% if message.tags == 'error' %}error{% endif %}">{{ message }}</div>
{% endfor %}
{% endif %}
<form class="card" method="post" action="/admin-tools/welcome-emails/settings/">
{% csrf_token %}
<div class="grid">
<div>
<label for="welcome_email_delay_days">Verzögerung in Tagen</label>
<input id="welcome_email_delay_days" name="welcome_email_delay_days" type="number" min="0" step="1" value="{{ workflow_config.welcome_email_delay_days }}" />
</div>
<div>
<label for="welcome_sender_email">Absenderadresse (optional)</label>
<input id="welcome_sender_email" name="welcome_sender_email" type="email" value="{{ workflow_config.welcome_sender_email }}" placeholder="Leer = System-Absender" />
</div>
<div>
<label for="welcome_subject">Welcome Subject</label>
<input id="welcome_subject" name="welcome_subject" value="{{ welcome_subject_value }}" />
</div>
<div>
<label for="welcome_body">Welcome Text</label>
<textarea id="welcome_body" name="welcome_body">{{ welcome_body_value }}</textarea>
</div>
</div>
<div class="check-row">
<label><input type="checkbox" name="welcome_include_pdf" {% if workflow_config.welcome_include_pdf %}checked{% endif %} /> Onboarding-PDF anhängen</label>
</div>
<div class="hint">
Verfügbare Keywords:
{% for key in welcome_keywords %}
<code>{{ key }}</code>{% if not forloop.last %}, {% endif %}
{% endfor %}
</div>
<div class="actions">
<button class="btn btn-primary" type="submit">Welcome-Einstellungen speichern</button>
</div>
</form>
<form class="bulk-bar" id="welcome-bulk-form" method="post" action="/admin-tools/welcome-emails/bulk-action/" onsubmit="return confirmBulkAction();">
{% csrf_token %}
<label style="display:inline-flex; align-items:center; gap:6px; margin:0;">
<input type="checkbox" id="select-all-welcome" />
Alle auswählen
</label>
<select name="bulk_action" id="bulk_action">
<option value="pause">Pausieren</option>
<option value="send_now">Sofort senden</option>
<option value="delete">Löschen</option>
</select>
<input type="hidden" name="selected_ids" id="selected_ids" />
<button class="btn btn-secondary" type="submit">Bulk ausführen</button>
<span class="bulk-note"><span id="selected-count">0</span> ausgewählt</span>
</form>
<table>
<thead>
<tr>
<th class="select-col">Auswahl</th>
<th>ID</th>
<th>Mitarbeitende Person</th>
<th>Empfänger</th>
<th>Geplant für</th>
<th>Status</th>
<th>Gesendet am</th>
<th>Aktion</th>
</tr>
</thead>
<tbody>
{% for row in rows %}
<tr>
<td class="select-col"><input type="checkbox" class="welcome-select" value="{{ row.id }}" /></td>
<td>{{ row.id }}</td>
<td>{{ row.onboarding_request.full_name }}</td>
<td>{{ row.recipient_email }}</td>
<td>{{ row.send_at|date:"Y-m-d H:i" }}</td>
<td>
{% if row.status == 'scheduled' %}
<span class="badge scheduled">Geplant</span>
{% elif row.status == 'paused' %}
<span class="badge paused">Pausiert</span>
{% elif row.status == 'cancelled' %}
<span class="badge cancelled">Abgebrochen</span>
{% elif row.status == 'sent' %}
<span class="badge sent">Gesendet</span>
{% else %}
<span class="badge failed">Fehlgeschlagen</span>
{% endif %}
</td>
<td>{% if row.sent_at %}{{ row.sent_at|date:"Y-m-d H:i" }}{% else %}-{% endif %}</td>
<td>
<div class="table-actions">
{% if row.status != 'sent' and row.status != 'cancelled' %}
<form method="post" action="/admin-tools/welcome-emails/{{ row.id }}/trigger-now/" style="display:inline;">
{% csrf_token %}
<button class="btn btn-secondary" type="submit">Sofort senden</button>
</form>
{% endif %}
{% if row.status == 'scheduled' %}
<form method="post" action="/admin-tools/welcome-emails/{{ row.id }}/pause/" style="display:inline;">
{% csrf_token %}
<button class="btn btn-secondary" type="submit">Pausieren</button>
</form>
{% elif row.status == 'paused' %}
<form method="post" action="/admin-tools/welcome-emails/{{ row.id }}/resume/" style="display:inline;">
{% csrf_token %}
<button class="btn btn-secondary" type="submit">Fortsetzen</button>
</form>
{% endif %}
{% if row.status != 'sent' and row.status != 'cancelled' %}
<form method="post" action="/admin-tools/welcome-emails/{{ row.id }}/cancel/" style="display:inline;">
{% csrf_token %}
<button class="btn btn-secondary" type="submit">Abbrechen</button>
</form>
{% endif %}
</div>
</td>
</tr>
{% empty %}
<tr><td colspan="8">Keine geplanten Welcome E-Mails vorhanden.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<script>
(function () {
const selectAll = document.getElementById('select-all-welcome');
const rowChecks = Array.from(document.querySelectorAll('.welcome-select'));
const selectedCount = document.getElementById('selected-count');
const selectedIds = document.getElementById('selected_ids');
const bulkForm = document.getElementById('welcome-bulk-form');
const bulkAction = document.getElementById('bulk_action');
function currentSelected() {
return rowChecks.filter((c) => c.checked).map((c) => c.value);
}
function syncState() {
const ids = currentSelected();
selectedCount.textContent = String(ids.length);
selectedIds.value = ids.join(',');
if (!rowChecks.length) {
selectAll.checked = false;
selectAll.indeterminate = false;
return;
}
selectAll.checked = ids.length > 0 && ids.length === rowChecks.length;
selectAll.indeterminate = ids.length > 0 && ids.length < rowChecks.length;
}
selectAll.addEventListener('change', function () {
rowChecks.forEach((c) => { c.checked = selectAll.checked; });
syncState();
});
rowChecks.forEach((c) => c.addEventListener('change', syncState));
bulkForm.addEventListener('submit', syncState);
window.confirmBulkAction = function () {
syncState();
const count = currentSelected().length;
if (!count) {
alert('Bitte mindestens einen Welcome-Eintrag auswählen.');
return false;
}
const action = bulkAction.value;
if (action === 'delete') {
return confirm('Ausgewählte Welcome-Einträge wirklich löschen?');
}
if (action === 'pause') {
return confirm('Ausgewählte Welcome-Einträge pausieren?');
}
return confirm('Ausgewählte Welcome-Einträge sofort senden?');
};
syncState();
})();
</script>
</body>
</html>