Files
deskhub/DeskHub-Webseite/staff/department_form.php
T
2026-10-19 18:06:00 +00:00

290 lines
21 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
require_once __DIR__ . '/../includes/bootstrap.php';
$staff = require_admin_or_permission('manage_departments');
$pdo = get_db();
$deptId = (int) ($_GET['dept'] ?? $_POST['dept'] ?? 0);
$stmt = $pdo->prepare('SELECT * FROM departments WHERE id = ?');
$stmt->execute([$deptId]);
$dept = $stmt->fetch();
if (!$dept) {
flash('error', 'Abteilung nicht gefunden.');
redirect('/staff/departments.php');
}
$fieldTypes = [
'text' => 'Einzeiliger Text',
'textarea' => 'Mehrzeiliger Text',
'select' => 'Dropdown-Auswahl',
'radio' => 'Radio-Auswahl (Punkte)',
'checkbox' => 'Checkbox (Ja/Nein)',
'number' => 'Zahl',
'date' => 'Datum',
];
$errors = [];
// Feldpositionen: Schlüssel => [Gruppe, Beschriftung]; "@"-Schlüssel stehen in custom_fields.form_section
$positions = [
'@topic_top' => ['Abschnitt 1 – Themenauswahl', 'vor der Themenauswahl'],
'@topic' => ['Abschnitt 1 – Themenauswahl', 'nach der Themenauswahl'],
'@contact_top' => ['Abschnitt 2 – Kontaktdaten', 'vor Name/E-Mail'],
'@contact' => ['Abschnitt 2 – Kontaktdaten', 'nach Name/E-Mail'],
'@details_top' => ['Abschnitt 3 – Betreff und Beschreibung', 'vor dem Betreff'],
'@details_mid' => ['Abschnitt 3 – Betreff und Beschreibung', 'zwischen Betreff und Beschreibung'],
'@details' => ['Abschnitt 3 – Betreff und Beschreibung', 'nach der Beschreibung'],
];
$self = '/staff/department_form.php?dept=' . $deptId;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
verify_csrf();
$action = $_POST['action'] ?? '';
if ($action === 'save_form') {
$clip = fn(string $k, int $max) => mb_substr(trim((string) ($_POST[$k] ?? '')), 0, $max);
$cfg = [
'title' => $clip('title', 150),
'contact_title' => $clip('contact_title', 100),
'issue_title' => $clip('issue_title', 100),
'details_title' => $clip('details_title', 100),
'intro' => $clip('intro', 1000),
'subject_show' => !empty($_POST['subject_show']),
'subject_label' => $clip('subject_label', 100),
'message_label' => $clip('message_label', 100),
'message_show' => !empty($_POST['message_show']),
'subject_width' => in_array($_POST['subject_width'] ?? '', ['half', 'third', 'twothirds'], true) ? $_POST['subject_width'] : 'full',
'message_width' => in_array($_POST['message_width'] ?? '', ['half', 'third', 'twothirds'], true) ? $_POST['message_width'] : 'full',
'message_hint' => $clip('message_hint', 200),
'attachment_show' => !empty($_POST['attachment_show']),
'submit_label' => $clip('submit_label', 60),
];
$pdo->prepare('UPDATE departments SET own_form = ?, form_config = ?, updated_at = ? WHERE id = ?')
->execute([!empty($_POST['own_form']) ? 1 : 0, json_encode($cfg, JSON_UNESCAPED_UNICODE), db_now(), $deptId]);
audit_log($staff['id'], 'Abteilungsformular gespeichert: ' . $dept['name']);
flash('success', 'Formular gespeichert.');
redirect($self);
}
if ($action === 'save_field') {
$fid = (int) ($_POST['id'] ?? 0);
$label = trim((string) ($_POST['label'] ?? ''));
$type = array_key_exists($_POST['field_type'] ?? '', $fieldTypes) ? $_POST['field_type'] : 'text';
$options = trim((string) ($_POST['options'] ?? ''));
$required = !empty($_POST['is_required']) ? 1 : 0;
$active = !empty($_POST['is_active']) ? 1 : 0;
$sort = (int) ($_POST['sort_order'] ?? 0);
$radioInline = !empty($_POST['radio_inline']) ? 1 : 0;
$helpText = form_help_compose((string) ($_POST['help_text'] ?? ''), (string) ($_POST['link_url'] ?? ''), (string) ($_POST['link_word'] ?? ''), $label);
$width = in_array($_POST['form_width'] ?? '', ['half', 'third', 'twothirds'], true) ? $_POST['form_width'] : null;
$secPick = (string) ($_POST['form_section'] ?? '');
$section = ($secPick === '' || isset($positions[$secPick])) ? $secPick : mb_substr(trim((string) ($_POST['form_section_custom'] ?? '')), 0, 100);
if ($label === '') $errors[] = 'Bitte eine Bezeichnung angeben.';
if (in_array($type, ['select', 'radio'], true) && $options === '') $errors[] = 'Bitte mindestens eine Option angeben (eine pro Zeile).';
if (!$errors) {
if ($fid) {
$pdo->prepare('UPDATE custom_fields SET label=?, field_type=?, options=?, is_required=?, is_active=?, sort_order=?, form_section=?, form_width=?, radio_inline=?, help_text=? WHERE id=? AND department_id=?')
->execute([$label, $type, $options ?: null, $required, $active, $sort, $section ?: null, $width, $radioInline, $helpText ?: null, $fid, $deptId]);
} else {
$pdo->prepare('INSERT INTO custom_fields (label, field_type, options, help_topic_id, department_id, is_required, is_active, sort_order, form_section, form_width, radio_inline, help_text, created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)')
->execute([$label, $type, $options ?: null, null, $deptId, $required, $active, $sort, $section ?: null, $width, $radioInline, $helpText ?: null, db_now()]);
}
audit_log($staff['id'], "Abteilungsfeld gespeichert ({$dept['name']}): {$label}");
flash('success', 'Feld gespeichert.');
redirect($self);
}
}
if ($action === 'delete_field') {
$pdo->prepare('DELETE FROM custom_fields WHERE id = ? AND department_id = ?')->execute([(int) ($_POST['id'] ?? 0), $deptId]);
audit_log($staff['id'], "Abteilungsfeld gelöscht ({$dept['name']})");
flash('success', 'Feld gelöscht (bereits erfasste Werte in Tickets bleiben erhalten).');
redirect($self);
}
}
$cfg = json_decode((string) ($dept['form_config'] ?? ''), true);
$cfg = is_array($cfg) ? $cfg : [];
$cv = fn(string $k, string $def = '') => (string) ($cfg[$k] ?? $def);
$flag = fn(string $k) => !array_key_exists($k, $cfg) || !empty($cfg[$k]);
$fieldsStmt = $pdo->prepare('SELECT * FROM custom_fields WHERE department_id = ? ORDER BY sort_order, label');
$fieldsStmt->execute([$deptId]);
$fields = $fieldsStmt->fetchAll();
$editField = null;
if (!empty($_GET['edit'])) {
foreach ($fields as $f) if ((int) $f['id'] === (int) $_GET['edit']) $editField = $f;
}
$pageTitle = 'Formular: ' . $dept['name'];
$activePage = 'departments';
require __DIR__ . '/_header.php';
?>
<p><a href="/staff/departments.php?edit=<?= $deptId ?>">← Zurück zur Abteilung</a></p>
<h1>Ticket-Formular <span class="muted" style="font-size:1rem;font-weight:400">— <?= e($dept['name']) ?></span></h1>
<p class="muted">Hier legst du fest, wie das Formular „Neues Ticket“ aussieht, wenn ein Kunde diese Abteilung wählt (direkt oder über ein Hilfe-Thema dieser Abteilung).</p>
<?php foreach ($errors as $err): ?><div class="flash error"><?= e($err) ?></div><?php endforeach; ?>
<form method="post" class="card" style="margin-bottom:20px">
<?= csrf_field() ?>
<input type="hidden" name="action" value="save_form">
<input type="hidden" name="dept" value="<?= $deptId ?>">
<label style="display:flex;align-items:center;gap:8px;margin-top:0">
<input type="checkbox" name="own_form" style="width:auto" <?= !empty($dept['own_form']) ? 'checked' : '' ?>>
<strong>Eigenes Formular für diese Abteilung verwenden</strong>
</label>
<p class="muted" style="font-size:.82rem;margin:4px 0 12px">
Ist das aktiv, erscheinen für diese Abteilung <strong>nur</strong> die unten festgelegten Felder. Die allgemeinen Ticket-Felder und die Felder der Hilfe-Themen werden dann nicht verwendet.
Name und E-Mail-Adresse werden immer abgefragt, damit wir antworten können.
</p>
<div class="field-row">
<div><label for="title">Überschrift (optional)</label><input type="text" id="title" name="title" value="<?= e($cv('title')) ?>" placeholder="z. B. Störungsmeldung Technik"></div>
<div><label for="submit_label">Text des Absende-Knopfs</label><input type="text" id="submit_label" name="submit_label" value="<?= e($cv('submit_label')) ?>" placeholder="Ticket erstellen"></div>
</div>
<div class="field-row">
<div><label for="contact_title">Überschrift Abschnitt „Kontaktdaten“</label><input type="text" id="contact_title" name="contact_title" value="<?= e($cv('contact_title')) ?>" placeholder="Ihre Kontaktdaten"></div>
<div><label for="issue_title">Überschrift Abschnitt 1 (Themenauswahl)</label><input type="text" id="issue_title" name="issue_title" value="<?= e($cv('issue_title')) ?>" placeholder="Ihr Anliegen"></div>
</div>
<label for="details_title">Überschrift Abschnitt „Betreff und Beschreibung“</label>
<input type="text" id="details_title" name="details_title" value="<?= e($cv('details_title')) ?>" placeholder="Ihr Anliegen">
<label for="intro">Einleitungstext (optional)</label>
<textarea id="intro" name="intro" rows="2" placeholder="Wird oberhalb der Felder angezeigt"><?= e($cv('intro')) ?></textarea>
<h3>Standardfelder</h3>
<label style="display:flex;align-items:center;gap:8px"><input type="checkbox" name="subject_show" style="width:auto" <?= $flag('subject_show') ? 'checked' : '' ?>> Betreff abfragen</label>
<p class="muted" style="font-size:.78rem;margin:2px 0 8px">Ohne Betrefffeld wird der Betreff automatisch „Anfrage <?= e($dept['name']) ?>“.</p>
<div class="field-row">
<div><label for="subject_label">Bezeichnung Betreff</label><input type="text" id="subject_label" name="subject_label" value="<?= e($cv('subject_label')) ?>" placeholder="Betreff"></div>
<div><label for="message_label">Bezeichnung Beschreibung</label><input type="text" id="message_label" name="message_label" value="<?= e($cv('message_label')) ?>" placeholder="Beschreibung"></div>
</div>
<?php $widthOpts = ['full' => 'Ganze Zeile', 'half' => 'Halbe Zeile', 'third' => 'Ein Drittel', 'twothirds' => 'Zwei Drittel']; ?>
<div class="field-row">
<div><label for="subject_width">Breite des Betreff-Feldes</label>
<select id="subject_width" name="subject_width"><?php foreach ($widthOpts as $k => $l): ?><option value="<?= $k ?>" <?= $cv('subject_width', 'full') === $k ? 'selected' : '' ?>><?= e($l) ?></option><?php endforeach; ?></select></div>
<div><label for="message_width">Breite des Beschreibungs-Feldes</label>
<select id="message_width" name="message_width"><?php foreach ($widthOpts as $k => $l): ?><option value="<?= $k ?>" <?= $cv('message_width', 'full') === $k ? 'selected' : '' ?>><?= e($l) ?></option><?php endforeach; ?></select></div>
</div>
<p class="muted" style="font-size:.78rem;margin:2px 0 0">Mit „Halbe Zeile“ steht der Betreff neben einem benachbarten eigenen Feld (zum Beispiel einem Zahlenfeld direkt vor oder nach dem Betreff).</p>
<label style="display:flex;align-items:center;gap:8px;margin-top:12px"><input type="checkbox" name="message_show" style="width:auto" <?= $flag('message_show') ? 'checked' : '' ?>> Beschreibung (großes Textfeld) abfragen</label>
<p class="muted" style="font-size:.78rem;margin:2px 0 8px">Ohne Beschreibung wird der Text des Tickets aus deinen eigenen Feldern zusammengesetzt („Bezeichnung: Eingabe“).</p>
<label for="message_hint">Platzhaltertext der Beschreibung</label>
<input type="text" id="message_hint" name="message_hint" value="<?= e($cv('message_hint')) ?>" placeholder="Beschreiben Sie Ihr Anliegen so detailliert wie möglich…">
<label style="display:flex;align-items:center;gap:8px;margin-top:12px"><input type="checkbox" name="attachment_show" style="width:auto" <?= $flag('attachment_show') ? 'checked' : '' ?>> Datei-Anhang erlauben</label>
<div class="btn-row"><button type="submit" class="btn">Formular speichern</button></div>
</form>
<div style="display:flex;gap:24px;align-items:flex-start;flex-wrap:wrap">
<div style="flex:2;min-width:320px">
<div class="card">
<h3 style="margin-top:0">Felder dieser Abteilung</h3>
<table>
<thead><tr><th>Reihenfolge</th><th>Abschnitt</th><th>Bezeichnung</th><th>Typ</th><th>Pflicht</th><th>Aktiv</th><th></th></tr></thead>
<tbody>
<?php foreach ($fields as $f): ?>
<tr>
<td><?= (int) $f['sort_order'] ?></td>
<td class="muted"><?php $fs = trim((string) ($f['form_section'] ?? '')); echo e($fs === '' ? 'Zusätzliche Angaben' : (isset($positions[$fs]) ? $positions[$fs][0] . ': ' . $positions[$fs][1] : $fs)); ?></td>
<td><?= e($f['label']) ?></td>
<td><?= e($fieldTypes[$f['field_type']] ?? $f['field_type']) ?></td>
<td><?= $f['is_required'] ? '✓' : '–' ?></td>
<td><?= $f['is_active'] ? '✓' : '–' ?></td>
<td style="white-space:nowrap">
<a href="<?= e($self) ?>&edit=<?= (int) $f['id'] ?>">Bearbeiten</a> ·
<form method="post" style="display:inline" onsubmit="return confirm('Feld wirklich löschen?');">
<?= csrf_field() ?><input type="hidden" name="action" value="delete_field"><input type="hidden" name="dept" value="<?= $deptId ?>"><input type="hidden" name="id" value="<?= (int) $f['id'] ?>">
<button type="submit" style="background:none;border:none;color:#dc2626;cursor:pointer;padding:0;font-size:.9rem">Löschen</button>
</form>
</td>
</tr>
<?php endforeach; ?>
<?php if (!$fields): ?><tr><td colspan="7" class="muted">Noch keine eigenen Felder. Rechts kannst du das erste anlegen.</td></tr><?php endif; ?>
</tbody>
</table>
</div>
</div>
<div style="flex:1;min-width:300px">
<div class="card">
<h3 style="margin-top:0"><?= $editField ? 'Feld bearbeiten' : 'Neues Feld' ?></h3>
<form method="post">
<?= csrf_field() ?>
<input type="hidden" name="action" value="save_field">
<input type="hidden" name="dept" value="<?= $deptId ?>">
<?php if ($editField): ?><input type="hidden" name="id" value="<?= (int) $editField['id'] ?>"><?php endif; ?>
<label for="label">Bezeichnung</label>
<input type="text" id="label" name="label" value="<?= e($_POST['label'] ?? $editField['label'] ?? '') ?>" placeholder="z. B. Gerätenummer" required>
<label for="field_type">Feldtyp</label>
<select name="field_type" id="field_type" onchange="document.getElementById('options-row').style.display = (this.value==='select' || this.value==='radio') ? 'block' : 'none'; document.getElementById('radio-row').style.display = this.value==='radio' ? 'block' : 'none'">
<?php foreach ($fieldTypes as $val => $lbl): ?>
<option value="<?= $val ?>" <?= ($_POST['field_type'] ?? $editField['field_type'] ?? 'text') === $val ? 'selected' : '' ?>><?= e($lbl) ?></option>
<?php endforeach; ?>
</select>
<div id="options-row" style="display:<?= in_array($_POST['field_type'] ?? $editField['field_type'] ?? '', ['select', 'radio'], true) ? 'block' : 'none' ?>">
<label for="options">Optionen (eine pro Zeile)</label>
<textarea id="options" name="options" placeholder="Option 1&#10;Option 2"><?= e($_POST['options'] ?? $editField['options'] ?? '') ?></textarea>
</div>
<?php
$curSec = (string) ($_POST['form_section'] ?? ($editField['form_section'] ?? ''));
$special = array_merge([''], array_keys($positions));
$isCustom = $curSec !== '' && !in_array($curSec, $special, true);
$customText = $_POST['form_section_custom'] ?? ($isCustom ? $curSec : '');
$pick = $isCustom ? '__custom' : $curSec;
?>
<label for="form_section">Wo im Formular erscheint das Feld?</label>
<select id="form_section" name="form_section" onchange="document.getElementById('custom-section-row').style.display = this.value==='__custom' ? 'block' : 'none'">
<?php $lastGroup = ''; foreach ($positions as $key => [$grp, $lbl]):
if ($grp !== $lastGroup) { if ($lastGroup !== '') echo '</optgroup>'; echo '<optgroup label="' . e($grp) . '">'; $lastGroup = $grp; } ?>
<option value="<?= e($key) ?>" <?= $pick === $key ? 'selected' : '' ?>><?= e($lbl) ?></option>
<?php endforeach; echo '</optgroup>'; ?>
<option value="" <?= $pick === '' ? 'selected' : '' ?>>Zusätzliche Angaben (eigener Abschnitt danach)</option>
<option value="__custom" <?= $pick === '__custom' ? 'selected' : '' ?>>Eigener Abschnitt mit eigenem Namen …</option>
</select>
<div id="custom-section-row" style="display:<?= $pick === '__custom' ? 'block' : 'none' ?>">
<input type="text" name="form_section_custom" list="sections" value="<?= e($customText) ?>" placeholder="z. B. Abrechnung" style="margin-top:6px">
<datalist id="sections">
<?php foreach (array_unique(array_filter(array_map(fn($x) => trim((string) ($x['form_section'] ?? '')), $fields), fn($v) => $v !== '' && $v[0] !== '@')) as $sn): ?><option value="<?= e($sn) ?>"><?php endforeach; ?>
</datalist>
<p class="muted" style="font-size:.78rem;margin:3px 0 0">Felder mit gleichem Namen landen im selben Abschnitt.</p>
</div>
<?php $__rt = $_POST['field_type'] ?? $editField['field_type'] ?? ''; $__ri = $_POST['radio_inline'] ?? $editField['radio_inline'] ?? 0; ?>
<div id="radio-row" style="display:<?= ($__rt === 'radio') ? 'block' : 'none' ?>">
<label for="radio_inline">Anordnung der Optionen</label>
<select id="radio_inline" name="radio_inline">
<option value="0" <?= empty($__ri) ? 'selected' : '' ?>>Untereinander</option>
<option value="1" <?= !empty($__ri) ? 'selected' : '' ?>>Nebeneinander</option>
</select>
</div>
<?php $__hStored = $_POST ? form_help_compose((string) ($_POST['help_text'] ?? ''), (string) ($_POST['link_url'] ?? ''), (string) ($_POST['link_word'] ?? '')) : ($editField['help_text'] ?? ''); ?>
<?php [$__hPlain, $__hUrl, $__hWord] = form_help_split($__hStored); ?>
<label for="help_text">Hinweistext (optional)</label>
<textarea id="help_text" name="help_text" rows="3" placeholder="z. B. Ich habe die Nutzerbedingungen gelesen und akzeptiert."><?= e($__hPlain) ?></textarea>
<div class="field-row">
<div><label for="link_word">Verlinkter Teil des Textes</label><input type="text" id="link_word" name="link_word" value="<?= e($__hWord) ?>" placeholder="z. B. Nutzerbedingungen"></div>
<div><label for="link_url">Link-Adresse</label><input type="text" id="link_url" name="link_url" value="<?= e($__hUrl) ?>" placeholder="https://…"></div>
</div>
<p class="muted" style="font-size:.78rem;margin:3px 0 0">Bei einer <strong>Checkbox</strong> steht der Text neben dem Kästchen, bei anderen Feldern unter dem Feld. Das Wort aus „Verlinkter Teil“ wird im Text zum Link auf die Adresse. Eine Checkbox als <strong>Pflichtfeld</strong> muss angehakt werden.</p>
<label for="form_width">Breite im Formular</label>
<?php $curW = (string) ($_POST['form_width'] ?? ($editField['form_width'] ?? '')); ?>
<select id="form_width" name="form_width">
<option value="" <?= $curW === '' ? 'selected' : '' ?>>Ganze Zeile</option>
<option value="half" <?= $curW === 'half' ? 'selected' : '' ?>>Halbe Zeile (zwei Felder nebeneinander)</option>
<option value="third" <?= $curW === 'third' ? 'selected' : '' ?>>Ein Drittel (drei Felder nebeneinander)</option>
<option value="twothirds" <?= $curW === 'twothirds' ? 'selected' : '' ?>>Zwei Drittel (mit einem Drittel-Feld)</option>
</select>
<p class="muted" style="font-size:.78rem;margin:3px 0 0">Aufeinanderfolgende Felder im selben Abschnitt, die zusammen in eine Zeile passen, stehen nebeneinander.</p>
<label for="sort_order">Reihenfolge (kleinere Zahl zuerst)</label>
<input type="text" id="sort_order" name="sort_order" value="<?= e((string) ($_POST['sort_order'] ?? $editField['sort_order'] ?? count($fields) * 10 + 10)) ?>">
<label style="display:flex;align-items:center;gap:8px;margin-top:14px"><input type="checkbox" name="is_required" style="width:auto" <?= !empty($editField['is_required']) ? 'checked' : '' ?>> Pflichtfeld</label>
<label style="display:flex;align-items:center;gap:8px;margin-top:8px"><input type="checkbox" name="is_active" style="width:auto" <?= ($editField['is_active'] ?? 1) ? 'checked' : '' ?>> Aktiv</label>
<div class="btn-row">
<button type="submit" class="btn"><?= $editField ? 'Speichern' : 'Feld anlegen' ?></button>
<?php if ($editField): ?><a href="<?= e($self) ?>" class="btn secondary">Abbrechen</a><?php endif; ?>
</div>
</form>
</div>
</div>
</div>
<?php require __DIR__ . '/_footer.php'; ?>