chore: initial snapshot of tubco people portal
This commit is contained in:
39
backend/workflows/templates/registration/login.html
Normal file
39
backend/workflows/templates/registration/login.html
Normal file
@@ -0,0 +1,39 @@
|
||||
{% load static %}
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Anmeldung</title>
|
||||
<link rel="stylesheet" href="{% static 'workflows/css/buttons.css' %}" />
|
||||
<style>
|
||||
body { margin: 0; font-family: Arial, sans-serif; min-height: 100vh; display: grid; place-items: center; background: linear-gradient(160deg, #eef6ff, #fff3f3); }
|
||||
.card { width: min(420px, calc(100% - 28px)); background: #fff; border: 1px solid #d9e3f0; border-radius: 14px; padding: 20px; box-shadow: 0 12px 30px rgba(28, 45, 79, 0.12); }
|
||||
.logo { width: 190px; max-width: 100%; height: auto; display: block; margin-bottom: 12px; }
|
||||
h1 { margin: 0 0 8px; font-size: 24px; }
|
||||
p { margin: 0 0 14px; color: #607086; }
|
||||
.field { margin-bottom: 12px; }
|
||||
label { display: block; font-weight: 600; margin-bottom: 6px; }
|
||||
input { width: 100%; padding: 10px; box-sizing: border-box; border: 1px solid #cbd5e1; border-radius: 8px; }
|
||||
.btn { width: 100%; }
|
||||
.errorlist { color: #b91c1c; margin: 6px 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<img class="logo" src="https://tub.co/media/site/0856bfc615-1750234287/tubco-wortbildmarke.svg" alt="TUB/CO Logo" />
|
||||
<h1>Anmeldung</h1>
|
||||
<p>Bitte melden Sie sich mit Ihrem Benutzerkonto an.</p>
|
||||
|
||||
<form method="post" action="/accounts/login/">
|
||||
{% csrf_token %}
|
||||
{% if form.errors %}
|
||||
<div class="errorlist">Anmeldung fehlgeschlagen. Bitte Zugangsdaten prüfen.</div>
|
||||
{% endif %}
|
||||
<div class="field">{{ form.username.label_tag }}{{ form.username }}</div>
|
||||
<div class="field">{{ form.password.label_tag }}{{ form.password }}</div>
|
||||
<button class="btn btn-primary" type="submit">Anmelden</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
131
backend/workflows/templates/workflows/form_builder.html
Normal file
131
backend/workflows/templates/workflows/form_builder.html
Normal 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>
|
||||
454
backend/workflows/templates/workflows/home.html
Normal file
454
backend/workflows/templates/workflows/home.html
Normal 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>
|
||||
348
backend/workflows/templates/workflows/integrations_setup.html
Normal file
348
backend/workflows/templates/workflows/integrations_setup.html
Normal 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>
|
||||
66
backend/workflows/templates/workflows/offboarding_form.html
Normal file
66
backend/workflows/templates/workflows/offboarding_form.html
Normal 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>
|
||||
@@ -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>
|
||||
341
backend/workflows/templates/workflows/onboarding_form.html
Normal file
341
backend/workflows/templates/workflows/onboarding_form.html
Normal 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>
|
||||
@@ -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>
|
||||
246
backend/workflows/templates/workflows/project_wiki.html
Normal file
246
backend/workflows/templates/workflows/project_wiki.html
Normal 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>
|
||||
173
backend/workflows/templates/workflows/requests_dashboard.html
Normal file
173
backend/workflows/templates/workflows/requests_dashboard.html
Normal 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>
|
||||
233
backend/workflows/templates/workflows/welcome_emails.html
Normal file
233
backend/workflows/templates/workflows/welcome_emails.html
Normal 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>
|
||||
Reference in New Issue
Block a user