Quelltext des aktuellen Standes
<?php
$this->layout('layouts/main', [
'page_title' => $page_title ?? 'Antrag',
'current_route' => 'workflows',
'full_width' => true,
'extra_css' => ['/assets/css/workflows.css', '/assets/css/person-search.css'],
'extra_js' => ['/assets/js/person-search.js'],
]);
$wf = $workflow;
$fields = $fields ?? [];
$persons = $persons ?? [];
$view = $this;
// Rendert ein einzelnes Feld (inkl. Layout-Breite, Validierung, Sichtbarkeitsregel)
$lookups = $lookups ?? [];
$prefill = $prefill ?? [];
$renderField = function (array $f) use ($view, $lookups, $prefill) {
$name = 'field_' . $f['field_key'];
$req = (int)$f['is_required'] === 1;
$opts = !empty($f['options_json']) ? (json_decode($f['options_json'], true) ?: []) : [];
$width = ($f['width'] ?? 'full') === 'half' ? 'wf-col-half' : 'wf-col-full';
$pre = $prefill[$f['field_key']] ?? '';
$attrs = '';
if ($req) { $attrs .= ' required'; }
if (!empty($f['max_length'])) { $attrs .= ' maxlength="' . (int)$f['max_length'] . '"'; }
if (!empty($f['validation_regex'])) { $attrs .= ' pattern="' . $view->e($f['validation_regex']) . '"'; }
if (in_array($f['field_type'], ['number','date','currency'], true)) {
if (($f['min_value'] ?? '') !== '') { $attrs .= ' min="' . $view->e($f['min_value']) . '"'; }
if (($f['max_value'] ?? '') !== '') { $attrs .= ' max="' . $view->e($f['max_value']) . '"'; }
}
// Sichtbarkeitsregel als data-Attribute (JS wertet aus)
$cond = '';
if (!empty($f['visible_if_field'])) {
$cond = ' data-visible-if-field="' . $view->e($f['visible_if_field']) . '"'
. ' data-visible-if-op="' . $view->e($f['visible_if_operator'] ?? 'eq') . '"'
. ' data-visible-if-value="' . $view->e($f['visible_if_value'] ?? '') . '"';
}
// Reine Anzeige-Typen: Überschrift, HTML-Block und Zustimmung rendern
// ohne das Standard-Label-Muster. Der Anzeige-Inhalt (options_json.html)
// stammt aus der Formularverwaltung. Formatierung/Links bleiben erhalten,
// aktive Inhalte werden über eine Positivliste entfernt – dasselbe Formular
// wird auch im Portal und öffentlich gerendert.
$contentHtml = is_array($opts) && isset($opts['html'])
? \OpenNIT\Security\XSS::purifyHtml((string)$opts['html'])
: '';
if ($f['field_type'] === 'heading') { ?>
<div class="wf-field wf-col-full wf-section-title" data-field="<?= $view->e($f['field_key']) ?>"<?= $cond ?>>
<?= $view->e($f['label']) ?>
<?php if (!empty($f['help_text'])): ?><div style="font-size:.78rem;font-weight:400;color:var(--gray-500);text-transform:none;letter-spacing:0;margin-top:.15rem;"><?= $view->e($f['help_text']) ?></div><?php endif; ?>
</div>
<?php return;
}
if ($f['field_type'] === 'html') { ?>
<div class="wf-field wf-col-full wf-html-block" data-field="<?= $view->e($f['field_key']) ?>"<?= $cond ?>>
<?= $contentHtml ?>
</div>
<?php return;
}
if ($f['field_type'] === 'consent') { ?>
<div class="form-group wf-field wf-col-full" data-field="<?= $view->e($f['field_key']) ?>"<?= $cond ?>>
<label class="checkbox-inline" style="display:flex;gap:.55rem;align-items:flex-start;font-weight:400;cursor:pointer;">
<input type="checkbox" name="<?= $view->e($name) ?>" value="1" style="margin-top:.22rem;flex:0 0 auto;"<?= $req ? ' required' : '' ?>>
<span><?= $contentHtml !== '' ? $contentHtml : $view->e($f['label']) ?><?php if ($req): ?> <span class="text-danger">*</span><?php endif; ?></span>
</label>
</div>
<?php return;
}
?>
<div class="form-group wf-field <?= $width ?>" data-field="<?= $view->e($f['field_key']) ?>"<?= $cond ?>>
<label for="f-e8e6-----------------------"><?= $view->e($f['label']) ?><?php if ($req): ?> <span class="text-danger">*</span><?php endif; ?></label>
<?php if ($f['field_type'] === 'textarea'): ?>
<textarea id="f-e8e6-----------------------" class="form-control" name="<?= $view->e($name) ?>" rows="4"<?= $attrs ?>><?= $view->e($pre) ?></textarea>
<?php elseif ($f['field_type'] === 'dropdown'): ?>
<select class="form-control" name="<?= $view->e($name) ?>"<?= $req ? ' required' : '' ?>>
<option value=""><?= $this->te('– bitte wählen –') ?></option>
<?php foreach ($opts as $o): ?><option value="<?= $view->e($o) ?>"><?= $view->e($o) ?></option><?php endforeach; ?>
</select>
<?php elseif ($f['field_type'] === 'checkbox'): ?>
<label class="checkbox-inline"><input type="checkbox" name="<?= $view->e($name) ?>" value="1"> <?= $view->te('Ja') ?></label>
<?php elseif ($f['field_type'] === 'number'): ?>
<input class="form-control" type="number" name="<?= $view->e($name) ?>"<?= $attrs ?>>
<?php elseif ($f['field_type'] === 'currency'): ?>
<div style="display:flex;align-items:center;gap:.4rem;">
<input class="form-control" type="number" step="0.01" name="<?= $view->e($name) ?>"<?= $attrs ?>>
<span class="text-muted"><?= $view->e($view->currency()) ?></span>
</div>
<?php elseif ($f['field_type'] === 'lookup'): ?>
<input class="form-control" list="wf-lk-<?= $view->e($f['field_key']) ?>" name="<?= $view->e($name) ?>"<?= $attrs ?> autocomplete="off">
<datalist id="wf-lk-<?= $view->e($f['field_key']) ?>">
<?php foreach (($lookups[$f['field_key']] ?? []) as $lv): ?><option value="<?= $view->e($lv) ?>"></option><?php endforeach; ?>
</datalist>
<?php elseif ($f['field_type'] === 'repeater'): ?>
<div class="wf-repeater" data-name="<?= $view->e($name) ?>">
<div class="wf-rep-rows">
<div class="wf-rep-row" style="display:flex;gap:.4rem;margin-bottom:.35rem;">
<input class="form-control form-control-sm" type="text" name="<?= $view->e($name) ?>[]"<?= $req ? ' required' : '' ?>>
<button type="button" class="btn btn-xs btn-secondary wf-rep-del" title="<?= $this->te('Zeile entfernen') ?>"><i class="fas fa-minus"></i></button>
</div>
</div>
<button type="button" class="btn btn-xs btn-secondary wf-rep-add"><i class="fas fa-plus"></i> <?= $this->te('Zeile hinzufügen') ?></button>
</div>
<?php elseif ($f['field_type'] === 'date'): ?>
<input class="form-control" type="date" name="<?= $view->e($name) ?>"<?= $attrs ?> value="<?= $view->e($pre) ?>">
<?php elseif ($f['field_type'] === 'file'): ?>
<input class="form-control" type="file" name="<?= $view->e($name) ?>"<?= $req ? ' required' : '' ?><?= !empty($f['accept']) ? ' accept="' . $view->e($f['accept']) . '"' : '' ?>>
<?php elseif ($f['field_type'] === 'email'): ?>
<input class="form-control" type="email" name="<?= $view->e($name) ?>"<?= $attrs ?> value="<?= $view->e($pre) ?>">
<?php else: ?>
<input class="form-control" type="text" name="<?= $view->e($name) ?>"<?= $attrs ?> value="<?= $view->e($pre) ?>">
<?php endif; ?>
<?php if (!empty($f['help_text'])): ?><small class="text-muted"><?= $view->e($f['help_text']) ?></small><?php endif; ?>
</div>
<?php
};
?>
<div class="content-header">
<div>
<h1><?= $this->e($wf['name']) ?></h1>
<?php if (!empty($wf['description'])): ?><p class="text-muted"><?= $this->e($wf['description']) ?></p><?php endif; ?>
</div>
<div><a class="btn btn-secondary btn-sm" href="/antraege/neu"><i class="fas fa-arrow-left"></i> <?= $this->te('Zurück') ?></a></div>
</div>
<div class="page-body">
<?php if (!empty($preview)): ?>
<div style="max-width:1100px;margin:0 auto 1rem;padding:.65rem 1rem;background:var(--amber-50);border:1px solid var(--amber-200);border-radius:.5rem;font-size:.85rem;color:var(--amber-800);">
<i class="fas fa-eye"></i> <?= $this->t('<strong>Vorschau:</strong> So sehen Antragsteller dieses Formular. Einreichen ist deaktiviert –') ?>
<a href="/admin/workflows/<?= (int)$wf['id'] ?>" style="color:var(--amber-800);text-decoration:underline;"><?= $this->te('zurück zum Vorlagen-Editor') ?></a>.
</div>
<?php endif; ?>
<div class="detail-card" style="max-width:1100px;margin:0 auto;">
<div class="detail-card-body">
<form method="post" action="/antraege/neu/<?= (int)$wf['id'] ?>" enctype="multipart/form-data" id="wfStartForm"<?= !empty($preview) ? ' onsubmit="return false"' : '' ?>>
<input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
<div class="form-group">
<label for="f-e8e6-_title"><?= $this->te('Titel des Antrags') ?> <span class="text-danger">*</span></label>
<input id="f-e8e6-_title" class="form-control" type="text" name="_title" required value="<?= $this->e($wf['name']) ?>">
</div>
<?php if ($wf['subject_mode'] === 'person'): ?>
<?php if ($directory_search ?? true): ?>
<div class="form-group person-form">
<label><?= $this->te('Betroffene Person') ?></label>
<input type="hidden" name="_subject_person_id" id="wf-subject-pid" value="">
<input class="form-control person-search-input" type="text" name="_subject_text" id="wf-subject-search"
placeholder="<?= $this->te('Name oder E-Mail im Verzeichnis suchen…') ?>" autocomplete="off"
data-person-search="/api/persons/search?workflow=<?= (int)$wf['id'] ?>" data-ps-set="name"
data-ps-fill-id="wf-subject-pid">
<small class="text-muted"><?= $this->te('Tippen zum Suchen – Auswahl verknüpft die Person aus dem Verzeichnis.') ?></small>
</div>
<?php else: ?>
<div class="form-group">
<label><?= $this->te('Betroffene Person') ?></label>
<input class="form-control" type="text" name="_subject_text" placeholder="<?= $this->te('Name oder E-Mail') ?>">
</div>
<?php endif; ?>
<?php elseif ($wf['subject_mode'] === 'free'): ?>
<div class="form-group">
<label><?= $this->te('Betroffene Person (Freitext)') ?></label>
<input class="form-control" type="text" name="_subject_text" placeholder="<?= $this->te('z. B. Name der neuen Mitarbeiterin') ?>">
</div>
<?php endif; ?>
<?php
$currentSection = null;
echo '<div class="wf-form-grid">';
foreach ($fields as $f) {
$sec = $f['section'] ?? '';
if ($sec !== ($currentSection ?? '')) {
$currentSection = $sec;
if ($sec !== '') {
echo '<div class="wf-section-title">' . $this->e($sec) . '</div>';
}
}
$renderField($f);
}
echo '</div>';
?>
<?php if (empty($preview)): ?>
<div class="form-actions" style="display:flex;gap:.5rem;margin-top:1rem;">
<button class="btn btn-primary" type="submit"><i class="fas fa-paper-plane"></i> <?= $this->te('Antrag einreichen') ?></button>
<button class="btn btn-secondary" type="submit" name="_draft" value="1" formnovalidate><i class="fas fa-save"></i> <?= $this->te('Als Entwurf speichern') ?></button>
</div>
<?php endif; ?>
</form>
</div>
</div>
</div>
<script>
(function () {
var form = document.getElementById('wfStartForm');
if (!form) return;
function val(field) {
var el = form.querySelector('[name="field_' + field + '"]');
if (!el) return '';
if (el.type === 'checkbox') return el.checked ? '1' : '';
return el.value || '';
}
function evalCond(box) {
var f = box.getAttribute('data-visible-if-field');
if (!f) return true;
var op = box.getAttribute('data-visible-if-op') || 'eq';
var v = box.getAttribute('data-visible-if-value') || '';
var cur = val(f);
switch (op) {
case 'neq': return cur !== v;
case 'contains': return v !== '' && cur.indexOf(v) !== -1;
case 'checked': return cur === '1';
default: return cur === v;
}
}
var conditional = Array.prototype.slice.call(form.querySelectorAll('[data-visible-if-field]'));
function apply() {
conditional.forEach(function (box) {
var show = evalCond(box);
box.style.display = show ? '' : 'none';
box.querySelectorAll('input,select,textarea').forEach(function (inp) {
if (!show) {
if (inp.required) { inp.dataset.wasRequired = '1'; inp.required = false; }
} else if (inp.dataset.wasRequired === '1') {
inp.required = true;
}
});
});
}
form.addEventListener('input', apply);
form.addEventListener('change', apply);
apply();
// ── Autosave (localStorage): schützt vor Datenverlust beim Ausfüllen ──
<?php if (empty($preview)): ?>
(function () {
var KEY = 'wfDraft:<?= (int)$wf['id'] ?>';
var saveTimer = null;
function collect() {
var data = {};
form.querySelectorAll('input, select, textarea').forEach(function (el) {
if (!el.name || el.type === 'file' || el.type === 'hidden') { return; }
if (el.type === 'checkbox') { data[el.name] = el.checked ? '1' : ''; }
else { data[el.name] = el.value; }
});
return data;
}
function restore(data) {
Object.keys(data).forEach(function (name) {
var el = form.querySelector('[name="' + CSS.escape(name) + '"]');
if (!el || el.type === 'file' || el.type === 'hidden') { return; }
if (el.type === 'checkbox') { el.checked = data[name] === '1'; }
else { el.value = data[name]; }
});
apply();
}
// Beim Laden: gespeicherten Zwischenstand anbieten
try {
var saved = JSON.parse(localStorage.getItem(KEY) || 'null');
if (saved && saved.data && Object.keys(saved.data).some(function (k) { return saved.data[k] !== '' && k !== '_title'; })) {
var bar = document.createElement('div');
bar.style.cssText = 'max-width:1100px;margin:0 auto 1rem;padding:.6rem 1rem;background:#eff6ff;border:1px solid #bfdbfe;border-radius:.5rem;font-size:.85rem;color:#1e40af;display:flex;gap:.75rem;align-items:center;flex-wrap:wrap;';
bar.innerHTML = '<span>📝 Es gibt einen nicht eingereichten Zwischenstand vom ' + (saved.at || '') + '.</span>';
var btnR = document.createElement('button');
btnR.type = 'button'; btnR.className = 'btn btn-sm btn-primary'; btnR.textContent = 'Wiederherstellen';
btnR.addEventListener('click', function () { restore(saved.data); bar.remove(); });
var btnD = document.createElement('button');
btnD.type = 'button'; btnD.className = 'btn btn-sm btn-secondary'; btnD.textContent = 'Verwerfen';
btnD.addEventListener('click', function () { localStorage.removeItem(KEY); bar.remove(); });
bar.appendChild(btnR); bar.appendChild(btnD);
form.closest('.detail-card').parentNode.insertBefore(bar, form.closest('.detail-card'));
}
} catch (e) {}
// Laufend speichern (entprellt)
form.addEventListener('input', function () {
clearTimeout(saveTimer);
saveTimer = setTimeout(function () {
try {
var now = new Date();
localStorage.setItem(KEY, JSON.stringify({
at: now.toLocaleDateString('de-DE') + ', ' + now.toLocaleTimeString('de-DE', {hour: '2-digit', minute: '2-digit'}) + ' Uhr',
data: collect()
}));
} catch (e) {}
}, 800);
});
// Nach dem Absenden (Einreichen ODER Entwurf) den Zwischenstand löschen
form.addEventListener('submit', function () {
try { localStorage.removeItem(KEY); } catch (e) {}
});
})();
<?php endif; ?>
// Repeater-Felder: Zeilen hinzufügen/entfernen
form.addEventListener('click', function (e) {
var addBtn = e.target.closest('.wf-rep-add');
if (addBtn) {
var rep = addBtn.closest('.wf-repeater');
var rows = rep.querySelector('.wf-rep-rows');
var first = rows.querySelector('.wf-rep-row');
var clone = first.cloneNode(true);
clone.querySelector('input').value = '';
clone.querySelector('input').removeAttribute('required');
rows.appendChild(clone);
return;
}
var delBtn = e.target.closest('.wf-rep-del');
if (delBtn) {
var rowsWrap = delBtn.closest('.wf-rep-rows');
if (rowsWrap.querySelectorAll('.wf-rep-row').length > 1) {
delBtn.closest('.wf-rep-row').remove();
} else {
delBtn.closest('.wf-rep-row').querySelector('input').value = '';
}
}
});
})();
</script>