OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

start.php

18 KB
<?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>