OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

vouchers.php

40 KB
<?php $this->layout('layouts/main', ['page_title' => $page_title, 'current_route' => $current_route, 'full_width' => true, 'extra_css' => ['/assets/css/ssd.css']]); ?>
<script src="<?= $this->asset('/assets/js/ssd.js') ?>"></script>
<script src="<?= $this->asset('/assets/js/voucher-label.js') ?>"></script>
<script>window.VoucherLabelConfig = <?= json_encode([
    'printMethod' => $voucher_label['print_method'] ?? 'both',
    'logoUrl'     => $voucher_label['logo_url'] ?? '',
    'geo'         => $voucher_label['geo'] ?? [],
    'apiBase'     => '/unifi/vouchers/dymo-label',
], JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP) ?>;</script>
<style>
.unifi-wrap   { display: flex; flex-direction: column; }
.vc-grid      { display: grid; grid-template-columns: 360px 1fr; gap: 1.5rem; align-items: start; }
@media (max-width: 900px) { .vc-grid { grid-template-columns: 1fr; } }

/* ---- Card ---- */
.vc-card { background: var(--color-bg); border: 1px solid var(--gray-200); border-radius:0.25rem; overflow: hidden; }
.vc-card-header { padding: 1rem 1.25rem; background: var(--slate-50); border-bottom: 1px solid var(--gray-200); font-weight: 600; font-size: .95rem; display:flex;align-items:center;gap:.5rem; }
.vc-card-body   { padding: 1.25rem; }

/* ---- Formular ---- */

/* ---- Preset-Buttons ---- */
.vc-presets { display: flex; gap: .4rem; flex-wrap: wrap; margin-bottom: .5rem; }
.vc-preset  { padding: .3rem .7rem; border: 1px solid var(--gray-300); border-radius: .35rem; font-size: .8rem; cursor: pointer; background: var(--gray-50); color: var(--gray-700); }
.vc-preset:hover, .vc-preset.active { background: var(--color-primary); color: var(--color-on-accent); border-color: var(--color-primary-dark); }

/* ---- Karte mit Kopfzeile + Tabelle (modul-spezifisch) ---- */
.vc-table-wrap { background: var(--color-bg); border: 1px solid var(--gray-200); border-radius:0.25rem; overflow: hidden; }

/* ---- Code-Badge ---- */
.vc-code { font-family: ui-monospace, monospace; font-size: .88rem; background: var(--gray-100); padding: .2rem .5rem; border-radius: .3rem; letter-spacing: .05em; }
.vc-copy-btn.copied { background: var(--green-100); border-color: var(--green-300); color: var(--green-800); }

/* Searchable Dropdown: gemeinsame Basis in /assets/css/ssd.css */
.ssd__trigger { padding: .32rem .75rem; min-width: 180px; }

/* ---- Neue Vouchers ---- */
.vc-new-badge { background: var(--green-100); color: var(--green-800); font-size: .72rem; padding: .1rem .45rem; border-radius: 9999px; margin-left: .4rem; font-weight: 600; }

/* ---- Toast ---- */
.vc-toast { position: fixed; bottom: 1.5rem; right: 1.5rem; z-index: 10000; display: flex; flex-direction: column; gap: .5rem; }
.vc-toast-item { padding: .7rem 1.15rem; border-radius: .5rem; font-size: .85rem; font-weight: 500; box-shadow: 0 4px 12px rgba(0,0,0,.15); animation: vcSlideIn .25s ease; max-width: 360px; }
.vc-toast-item.ok  { background: var(--emerald-600); color: var(--color-on-accent); }
.vc-toast-item.err { background: var(--red-600); color: var(--color-on-accent); }
@keyframes vcSlideIn { from { transform: translateX(120%); } to { transform: translateX(0); } }

/* ---- Modal: Basis aus app.css; hier Zustands-Toggle, Ebene und Breite ---- */
.vc-modal-backdrop { display: none; z-index: 10001; }
.vc-modal-backdrop.open { display: flex; }
.vc-modal-backdrop .modal-card { max-width: 420px; }

/* ---- Diagnose ---- */
.vc-diag { margin-top: .75rem; }
.vc-diag pre { background: var(--slate-900); color: var(--slate-200); padding: .75rem; border-radius: .4rem; font-size: .72rem; overflow: auto; max-height: 260px; }
</style>

<div class="unifi-wrap">

    <div class="content-header">
        <div>
            <h1><?= $this->te('UniFi Voucher') ?></h1>
            <ol class="breadcrumb">
                <li class="breadcrumb-item"><a href="/"><?= $this->te('Home') ?></a></li>
                <li class="breadcrumb-item"><a href="/unifi"><?= $this->te('UniFi-Integration') ?></a></li>
                <li class="breadcrumb-item active"><?= $this->te('Voucher') ?></li>
            </ol>
        </div>
        <div class="content-header-actions">
            <a href="/unifi" class="btn btn-default btn-sm"><?= $this->te('&#8592; UniFi Übersicht') ?></a>
        </div>
    </div>


<div class="page-body">

    <?php if ($configured && !empty($siteRows)):
        $firstSiteId   = $siteRows[0]['site_id'];
        $firstSiteName = htmlspecialchars($siteRows[0]['site_name'] ?: $siteRows[0]['site_id']);
    ?>
    <div class="card card-outline card-primary mb-3">
        <div class="filter-card-body">
            <div style="display:flex;gap:.6rem;align-items:center;flex-wrap:wrap;">
                <div class="ssd" id="vc-site-ssd" data-ssd-fixed>
                    <div class="ssd__trigger" onclick="ssdToggle('vc-site-ssd')">
                        <span class="ssd__value" id="vc-site-ssd-label">&#127760; <?= $firstSiteName ?></span>
                        <span class="ssd__arrow">&#9660;</span>
                    </div>
                    <div class="ssd__panel" style="display:none">
                        <input class="ssd__search" placeholder="<?= $this->te('Site suchen…') ?>" oninput="ssdFilter('vc-site-ssd', this.value)">
                        <div class="ssd__list">
                            <?php foreach ($siteRows as $site):
                                $vCount = count($vouchersBySite[$site['site_id']] ?? []);
                                $label  = htmlspecialchars($site['site_name'] ?: $site['site_id'])
                                        . ($vCount > 0 ? ' (' . $vCount . ')' : '');
                            ?>
                            <div class="ssd__item <?= $site['site_id'] === $firstSiteId ? 'selected' : '' ?>"
                                 data-value="<?= htmlspecialchars($site['site_id']) ?>"
                                 data-name="<?= htmlspecialchars($site['site_name'] ?: $site['site_id']) ?>">
                                &#127760; <?= $label ?>
                            </div>
                            <?php endforeach; ?>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <?php endif; ?>

<?php if (!$configured): ?>
<div style="background:var(--color-bg);border:1px solid var(--gray-200);border-radius:0.25rem;padding:2rem;text-align:center;color:var(--gray-500);">
    <?= $this->t('<span style="font-size:2.5rem;display:block;margin-bottom:.75rem">&#9881;</span> <strong>UniFi noch nicht konfiguriert.</strong><br> Bitte zuerst die <a href="/admin/settings/unifi" style="color:var(--color-primary);">Admin-Einstellungen</a> ausfüllen.') ?>
</div>
<?php elseif (empty($siteRows)): ?>
<div style="background:var(--color-bg);border:1px solid var(--gray-200);border-radius:0.25rem;padding:2rem;text-align:center;color:var(--gray-500);">
    <?= $this->t('<span style="font-size:2.5rem;display:block;margin-bottom:.75rem">&#127914;</span> <strong>Noch keine Sites bekannt.</strong><br> Klicke auf der <a href="/unifi" style="color:var(--color-primary);">UniFi-Übersicht</a> auf „Jetzt aktualisieren", um die Geräte zu synchronisieren.') ?>
</div>
<?php else: ?>

<?php
function fmtVoucherCode(string $code): string {
    $c = str_replace(['-', ' '], '', $code);
    return strlen($c) >= 10 ? substr($c, 0, 5) . '-' . substr($c, 5) : $code;
}
?>

<?php foreach ($siteRows as $site):
    $siteId = $site['site_id'];
    $vouchers = $vouchersBySite[$siteId] ?? [];
    $lastFetched = $lastFetchedBySite[$siteId] ?? null;
?>
<div class="vc-site-panel" id="panel-<?= htmlspecialchars($siteId) ?>"
     style="<?= $siteId !== $firstSiteId ? 'display:none' : '' ?>">

    <div class="vc-grid">

        <!-- Linke Seite: Formular -->
        <div class="vc-card">
            <div class="vc-card-header"><?= $this->te('&#127914; Neuen Voucher erstellen') ?></div>
            <div class="vc-card-body">

                <!-- Gültigkeitsdauer -->
                <div class="form-group">
                    <label><?= $this->te('Gültigkeitsdauer') ?></label>
                    <div class="vc-presets">
                        <button type="button" class="vc-preset" onclick="setExpire(60, this)"><?= $this->te('1 Stunde') ?></button>
                        <button type="button" class="vc-preset" onclick="setExpire(240, this)"><?= $this->te('4 Stunden') ?></button>
                        <button type="button" class="vc-preset active" onclick="setExpire(1440, this)"><?= $this->te('1 Tag') ?></button>
                        <button type="button" class="vc-preset" onclick="setExpire(10080, this)"><?= $this->te('7 Tage') ?></button>
                        <button type="button" class="vc-preset" onclick="setExpire(43200, this)"><?= $this->te('30 Tage') ?></button>
                    </div>
                    <input type="number" class="form-control form-control-sm" id="expire-<?= htmlspecialchars($siteId) ?>" value="1440" min="1" placeholder="<?= $this->te('Minuten') ?>" oninput="clearPreset(this)">
                    <div class="form-hint"><?= $this->te('Gültigkeitsdauer in Minuten') ?></div>
                </div>

                <!-- Anzahl -->
                <div class="form-group">
                    <label for="n-                                "><?= $this->te('Anzahl Vouchers') ?></label>
                    <input type="number" class="form-control form-control-sm" id="n-<?= htmlspecialchars($siteId) ?>" value="1" min="1" max="100">
                    <div class="form-hint"><?= $this->te('1 – 100 Vouchers auf einmal erstellen') ?></div>
                </div>

                <!-- Max. Nutzung -->
                <div class="form-group">
                    <label for="quota-                                "><?= $this->te('Max. Nutzungen pro Voucher') ?></label>
                    <select class="form-control form-control-sm" id="quota-<?= htmlspecialchars($siteId) ?>">
                        <option value="1"><?= $this->te('1× (Einmalig)') ?></option>
                        <option value="0"><?= $this->te('Unbegrenzt') ?></option>
                        <option value="2">2×</option>
                        <option value="5">5×</option>
                        <option value="10">10×</option>
                    </select>
                </div>

                <!-- Bandbreite -->
                <div class="form-group">
                    <label for="down-                                "><?= $this->te('Download-Limit (Mbps, 0 = unbegrenzt)') ?></label>
                    <input type="number" class="form-control form-control-sm" id="down-<?= htmlspecialchars($siteId) ?>" value="0" min="0">
                </div>
                <div class="form-group">
                    <label for="up-                                "><?= $this->te('Upload-Limit (Mbps, 0 = unbegrenzt)') ?></label>
                    <input type="number" class="form-control form-control-sm" id="up-<?= htmlspecialchars($siteId) ?>" value="0" min="0">
                </div>

                <!-- Notiz -->
                <div class="form-group">
                    <label><?= $this->te('Notiz (optional)') ?></label>
                    <input type="text" id="note-<?= htmlspecialchars($siteId) ?>" maxlength="64" placeholder="<?= $this->te('z.B. Gast-Konferenz') ?>">
                </div>

                <button class="btn btn-primary btn-block vc-submit" onclick="createVoucher('<?= htmlspecialchars($siteId) ?>')">
                    <?= $this->te('&#10133; Voucher erstellen') ?>
                </button>
            </div>
        </div>

        <!-- Rechte Seite: Voucher-Liste -->
        <div>
            <div id="new-vouchers-<?= htmlspecialchars($siteId) ?>"></div>

            <div class="vc-table-wrap">
                <!-- Kopfzeile mit Aktualisieren-Button -->
                <div style="display:flex;align-items:center;gap:.6rem;padding:.65rem 1rem;background:var(--gray-50);border-bottom:1px solid var(--gray-200);">
                    <span style="font-size:.82rem;font-weight:600;color:var(--gray-700);flex:1">
                        <?= $this->te('Gespeicherte Vouchers') ?>
                        <span id="vccount-<?= htmlspecialchars($siteId) ?>"
                              style="font-weight:400;color:var(--gray-500);margin-left:.35rem">(<?= count($vouchers) ?>)</span>
                    </span>
                    <?php if ($lastFetched): ?>
                    <span style="font-size:.75rem;color:var(--gray-400)" id="vclast-<?= htmlspecialchars($siteId) ?>">
                        <?= $this->te('Stand: {zeitpunkt}', ['zeitpunkt' => date('d.m.Y H:i', strtotime($lastFetched))]) ?>
                    </span>
                    <?php else: ?>
                    <span style="font-size:.75rem;color:var(--gray-400)" id="vclast-<?= htmlspecialchars($siteId) ?>"><?= $this->te('Noch nicht abgerufen') ?></span>
                    <?php endif; ?>
                    <button class="btn btn-xs btn-default vc-copy-btn" id="vcrefresh-<?= htmlspecialchars($siteId) ?>"
                            onclick="refreshVouchers('<?= htmlspecialchars($siteId) ?>')">
                        <?= $this->te('&#8635; Aktualisieren') ?>
                    </button>
                    <button class="btn btn-xs btn-default vc-copy-btn" onclick="copyAllCodes('<?= htmlspecialchars($siteId) ?>')"
                            title="<?= $this->te('Alle Codes in die Zwischenablage') ?>"><?= $this->te('&#128203; Codes') ?></button>
                    <button class="btn btn-xs btn-default vc-copy-btn" onclick="exportCsv('<?= htmlspecialchars($siteId) ?>')"
                            title="<?= $this->te('Als CSV exportieren') ?>"><?= $this->te('&#11015; CSV') ?></button>
                    <button class="btn btn-xs btn-default vc-copy-btn" onclick="printVouchers('<?= htmlspecialchars($siteId) ?>')"
                            title="<?= $this->te('Druckansicht der gültigen Vouchers') ?>"><?= $this->te('&#128424; Drucken') ?></button>
                    <button class="btn btn-xs btn-default vc-copy-btn" onclick="diagnoseVouchers('<?= htmlspecialchars($siteId) ?>')"
                            title="<?= $this->te('Technische Diagnose der Voucher-Endpunkte') ?>"><?= $this->te('&#128295; Diagnose') ?></button>
                    <button class="btn btn-xs btn-danger" id="vcdelall-<?= htmlspecialchars($siteId) ?>"
                            onclick="deleteAllVouchers('<?= htmlspecialchars($siteId) ?>')"
                            <?= empty($vouchers) ? 'style="display:none"' : '' ?>>
                        <?= $this->te('&#128465; Alle löschen') ?>
                    </button>

                </div>
                <div class="vc-diag" id="vcdiag-<?= htmlspecialchars($siteId) ?>" style="display:none;padding:0 1rem 1rem"></div>

                <?php if (empty($vouchers)): ?>
                <div class="empty-state" id="vctbody-wrap-<?= htmlspecialchars($siteId) ?>">
                    <?= $this->te('&#127914; Noch keine Vouchers – klicke „Aktualisieren" um die Daten von UniFi zu laden.') ?>
                </div>
                <?php else: ?>
                <div id="vctbody-wrap-<?= htmlspecialchars($siteId) ?>">
                <table class="data-table">
                    <thead>
                        <tr>
                            <th scope="col"><?= $this->te('Code') ?></th>
                            <th scope="col"><?= $this->te('Notiz') ?></th>
                            <th scope="col"><?= $this->te('Erstellt') ?></th>
                            <th scope="col"><?= $this->te('Läuft ab') ?></th>
                            <th scope="col"><?= $this->te('Nutzungen') ?></th>
                            <th scope="col"><?= $this->te('Status') ?></th>
                            <th scope="col"></th>
                        </tr>
                    </thead>
                    <tbody id="voucher-tbody-<?= htmlspecialchars($siteId) ?>">
                    <?php foreach ($vouchers as $v):
                        $isExpired = $v['expires_at'] && strtotime($v['expires_at']) < time();
                        $usedUp    = $v['quota'] > 0 && $v['used'] >= $v['quota'];
                    ?>
                    <tr id="vcrow-<?= htmlspecialchars($v['voucher_id']) ?>">
                        <td>
                            <span class="vc-code"><?= htmlspecialchars(fmtVoucherCode($v['code'])) ?></span>
                            <button aria-label="<?= $this->te('Kopieren') ?>" class="btn btn-xs btn-default vc-copy-btn" onclick="copyCode(this, '<?= htmlspecialchars(fmtVoucherCode($v['code'])) ?>')">&#128203;</button>
                        </td>
                        <td style="color:var(--gray-500)"><?= htmlspecialchars($v['note'] ?: '–') ?></td>
                        <td style="font-size:.78rem;color:var(--gray-500)">
                            <?= $v['created_at'] ? htmlspecialchars(date('d.m.Y H:i', strtotime($v['created_at']))) : '–' ?>
                        </td>
                        <td style="font-size:.78rem;<?= $isExpired ? 'color:var(--red-600)' : 'color:var(--gray-500)' ?>">
                            <?= $v['expires_at'] ? htmlspecialchars(date('d.m.Y H:i', strtotime($v['expires_at']))) : '–' ?>
                        </td>
                        <td style="font-size:.82rem">
                            <?= (int)$v['used'] ?> / <?= $v['quota'] > 0 ? (int)$v['quota'] : '∞' ?>
                        </td>
                        <td>
                            <?php if ($isExpired): ?>
                            <span style="font-size:.78rem;color:var(--red-600)"><?= $this->te('&#10005; Abgelaufen') ?></span>
                            <?php elseif ($usedUp): ?>
                            <span style="font-size:.78rem;color:var(--amber-600)"><?= $this->te('&#9888; Aufgebraucht') ?></span>
                            <?php else: ?>
                            <span style="font-size:.78rem;color:var(--emerald-600)"><?= $this->te('&#10003; Gültig') ?></span>
                            <?php endif; ?>
                        </td>
                        <td style="text-align:right;white-space:nowrap">
                            <span class="vc-print-slot" data-code="<?= htmlspecialchars(fmtVoucherCode($v['code'])) ?>"></span>
                            <button class="btn btn-xs btn-danger" title="<?= $this->te('Voucher löschen') ?>"
                                    onclick="deleteVoucher('<?= htmlspecialchars($siteId) ?>', '<?= htmlspecialchars($v['voucher_id']) ?>', this)">&#128465;</button>
                        </td>
                    </tr>
                    <?php endforeach; ?>
                    </tbody>
                </table>
                </div>
                <?php endif; ?>
            </div>
        </div>
    </div><!-- /vc-grid -->
</div><!-- /vc-site-panel -->
<?php endforeach; ?>

<?php endif; ?>

</div><!-- /page-body -->
</div><!-- /unifi-wrap -->

<input type="hidden" id="vc-csrf" value="<?= $this->csrf() ?>">

<!-- Toast-Container -->
<div class="vc-toast" id="vc-toast-container"></div>

<!-- Bestätigungs-Modal -->
<div class="modal-overlay vc-modal-backdrop" id="vc-modal">
    <div class="modal-card" role="dialog" aria-modal="true" aria-labelledby="vc-modal-title">
        <div class="modal-header"><h2 id="vc-modal-title"><?= $this->te('Bestätigung') ?></h2></div>
        <div class="modal-body" id="vc-modal-body"><?= $this->te('Sind Sie sicher?') ?></div>
        <div class="modal-footer">
            <button type="button" class="btn btn-default" id="vc-modal-cancel"><?= $this->te('Abbrechen') ?></button>
            <button type="button" class="btn btn-danger" id="vc-modal-ok"><?= $this->te('Löschen') ?></button>
        </div>
    </div>
</div>

<script>
(function() {
    var csrf = document.getElementById('vc-csrf')?.value || '';
    var activePreset = null;

    // ---- Toast ----
    function toast(msg, type) {
        var c = document.getElementById('vc-toast-container');
        if (!c) return;
        var el = document.createElement('div');
        el.className = 'vc-toast-item ' + (type === 'ok' ? 'ok' : 'err');
        el.textContent = msg;
        c.appendChild(el);
        setTimeout(function() { el.remove(); }, 4000);
    }

    // ---- Bestätigungs-Modal (Promise-basiert, ersetzt confirm()) ----
    function confirmModal(opts) {
        return new Promise(function(resolve) {
            var modal  = document.getElementById('vc-modal');
            var okBtn  = document.getElementById('vc-modal-ok');
            var cancel = document.getElementById('vc-modal-cancel');
            document.getElementById('vc-modal-title').textContent = opts.title || 'Bestätigung';
            document.getElementById('vc-modal-body').textContent  = opts.message || 'Sind Sie sicher?';
            okBtn.textContent = opts.okLabel || 'Löschen';
            modal.classList.add('open');

            function cleanup(result) {
                modal.classList.remove('open');
                okBtn.removeEventListener('click', onOk);
                cancel.removeEventListener('click', onCancel);
                modal.removeEventListener('click', onBackdrop);
                resolve(result);
            }
            function onOk()     { cleanup(true); }
            function onCancel() { cleanup(false); }
            function onBackdrop(e) { if (e.target === modal) cleanup(false); }
            okBtn.addEventListener('click', onOk);
            cancel.addEventListener('click', onCancel);
            modal.addEventListener('click', onBackdrop);
        });
    }

    // Searchable Dropdown: Mechanik liegt in /assets/js/ssd.js.
    // Hier nur die seitenspezifische Reaktion: passendes Site-Panel zeigen.
    document.addEventListener('ssd:select', function(e) {
        if (!e.detail || e.detail.id !== 'vc-site-ssd') return;
        var siteId = e.detail.value;
        document.querySelectorAll('.vc-site-panel').forEach(function(p) { p.style.display = 'none'; });
        var target = document.getElementById('panel-' + siteId);
        if (target) target.style.display = '';
    });

    // Site-Auswahl über data-Attribute statt Inline-onclick mit interpoliertem
    // Namen: Der Name wird beim Klick client-seitig escaped, bevor er als Label
    // gesetzt wird.
    var vcSiteSsd = document.getElementById('vc-site-ssd');
    if (vcSiteSsd) {
        vcSiteSsd.addEventListener('click', function(e) {
            var item = e.target.closest('.ssd__item');
            if (!item || !vcSiteSsd.contains(item)) return;
            var val  = item.getAttribute('data-value');
            var name = item.getAttribute('data-name') || val;
            window.ssdSelect('vc-site-ssd', val, '&#127760; ' + escH(name));
        });
    }

    // ---- Preset-Buttons ----
    window.setExpire = function(minutes, btn) {
        if (activePreset) activePreset.classList.remove('active');
        btn.classList.add('active');
        activePreset = btn;
        var input = btn.closest('.vc-card-body')?.querySelector('input[id^="expire-"]');
        if (input) input.value = minutes;
    };

    window.clearPreset = function(input) {
        if (activePreset) activePreset.classList.remove('active');
        activePreset = null;
    };

    // ---- Code kopieren ----
    window.copyCode = function(btn, code) {
        navigator.clipboard.writeText(code).then(function() {
            btn.textContent = '✓ Kopiert';
            btn.classList.add('copied');
            setTimeout(function() {
                btn.innerHTML = '&#128203;';
                btn.classList.remove('copied');
            }, 2000);
        });
    };

    // ---- Vouchers von UniFi laden (Aktualisieren) ----
    window.refreshVouchers = function(siteId) {
        var btn = document.getElementById('vcrefresh-' + siteId);
        if (btn) { btn.disabled = true; btn.textContent = '⏳'; }

        var body = new URLSearchParams({ _csrf: csrf, site_id: siteId });

        fetch('/unifi/vouchers/refresh', { method: 'POST', body: body })
        .then(function(r) { return r.json(); })
        .then(function(d) {
            if (d.ok) {
                renderVoucherTable(siteId, d.vouchers);
                var lastEl = document.getElementById('vclast-' + siteId);
                if (lastEl) lastEl.textContent = 'Stand: ' + d.fetched;
                setCount(siteId, d.count);
                toast(d.count + ' Vouchers geladen', 'ok');
            } else {
                toast('Fehler beim Laden: ' + (d.error || 'Unbekannt'), 'err');
            }
        })
        .catch(function(e) { toast('Netzwerkfehler: ' + e.message, 'err'); })
        .finally(function() {
            if (btn) { btn.disabled = false; btn.innerHTML = '&#8635; Aktualisieren'; }
        });
    };

    function renderVoucherTable(siteId, vouchers) {
        var wrap = document.getElementById('vctbody-wrap-' + siteId);
        if (!wrap) return;

        if (!vouchers || vouchers.length === 0) {
            wrap.innerHTML = '<div class="empty-state">&#127914; Keine Vouchers gefunden.</div>';
            return;
        }

        var now = Date.now() / 1000;
        var rows = vouchers.map(function(v) {
            var isExpired = v.expires_at && (new Date(v.expires_at).getTime() / 1000) < now;
            var usedUp    = v.quota > 0 && v.used >= v.quota;
            var statusHtml = isExpired
                ? '<span style="font-size:.78rem;color:#dc2626">&#10005; Abgelaufen</span>'
                : usedUp
                    ? '<span style="font-size:.78rem;color:#d97706">&#9888; Aufgebraucht</span>'
                    : '<span style="font-size:.78rem;color:#059669">&#10003; Gültig</span>';

            var created = v.created_at ? new Date(v.created_at).toLocaleString('de-DE', {day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'}) : '–';
            var expires = v.expires_at ? new Date(v.expires_at).toLocaleString('de-DE', {day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'}) : '–';
            var quota   = v.quota > 0 ? v.quota : '∞';

            var displayCode = fmtCode(v.code);
            return '<tr id="vcrow-' + escH(v.voucher_id) + '">'
                + '<td><span class="vc-code">' + escH(displayCode) + '</span>'
                + ' <button class="btn btn-xs btn-default vc-copy-btn" onclick="copyCode(this,\'' + escH(displayCode) + '\')">&#128203;</button></td>'
                + '<td style="color:#6b7280">' + escH(v.note || '–') + '</td>'
                + '<td style="font-size:.78rem;color:#6b7280">' + created + '</td>'
                + '<td style="font-size:.78rem;color:' + (isExpired ? '#dc2626' : '#6b7280') + '">' + expires + '</td>'
                + '<td style="font-size:.82rem">' + v.used + ' / ' + quota + '</td>'
                + '<td>' + statusHtml + '</td>'
                + '<td style="text-align:right;white-space:nowrap">' + (window.VoucherLabel ? VoucherLabel.buttons(displayCode) : '') + ' <button class="btn btn-xs btn-danger" title="Voucher löschen" onclick="deleteVoucher(\'' + escH(siteId) + '\',\'' + escH(v.voucher_id) + '\', this)">&#128465;</button></td>'
                + '</tr>';
        }).join('');

        wrap.innerHTML = '<table class="data-table"><thead><tr>'
            + '<th>Code</th><th>Notiz</th><th>Erstellt</th><th>Läuft ab</th><th>Nutzungen</th><th>Status</th><th></th>'
            + '</tr></thead><tbody id="voucher-tbody-' + escH(siteId) + '">' + rows + '</tbody></table>';
    }

    // ---- Einzelnen Voucher löschen ----
    window.deleteVoucher = function(siteId, voucherId, btn) {
        confirmModal({ title: 'Voucher löschen', message: 'Diesen Voucher wirklich löschen?' }).then(function(ok) {
            if (!ok) return;
            if (btn) { btn.disabled = true; btn.textContent = '⏳'; }

            var body = new URLSearchParams({ _csrf: csrf, site_id: siteId, voucher_id: voucherId });
            fetch('/unifi/vouchers/delete', { method: 'POST', body: body })
            .then(function(r) { return r.json(); })
            .then(function(d) {
                if (d.ok) {
                    var row = document.getElementById('vcrow-' + voucherId);
                    if (row) row.remove();
                    adjustCount(siteId, -1);
                    toast('Voucher gelöscht', 'ok');
                } else {
                    toast('Löschen fehlgeschlagen: ' + (d.error || 'Unbekannt'), 'err');
                    if (btn) { btn.disabled = false; btn.innerHTML = '&#128465;'; }
                }
            })
            .catch(function(e) {
                toast('Netzwerkfehler: ' + e.message, 'err');
                if (btn) { btn.disabled = false; btn.innerHTML = '&#128465;'; }
            });
        });
    };

    // ---- Alle Vouchers einer Site löschen ----
    window.deleteAllVouchers = function(siteId) {
        confirmModal({
            title: 'Alle Vouchers löschen',
            message: 'Wirklich ALLE Vouchers dieser Site löschen? Das kann nicht rückgängig gemacht werden.',
            okLabel: 'Alle löschen'
        }).then(function(ok) {
            if (!ok) return;
            var btn = document.getElementById('vcdelall-' + siteId);
            if (btn) { btn.disabled = true; btn.textContent = '⏳ Lösche…'; }

            var body = new URLSearchParams({ _csrf: csrf, site_id: siteId });
            fetch('/unifi/vouchers/delete-all', { method: 'POST', body: body })
            .then(function(r) { return r.json(); })
            .then(function(d) {
                if (d.ok) {
                    renderVoucherTable(siteId, []);
                    setCount(siteId, 0);
                    toast(d.deleted + ' gelöscht' + (d.errors > 0 ? ', ' + d.errors + ' fehlgeschlagen' : ''), d.errors > 0 ? 'err' : 'ok');
                } else {
                    toast('Löschen fehlgeschlagen: ' + (d.error || 'Unbekannt'), 'err');
                }
            })
            .catch(function(e) { toast('Netzwerkfehler: ' + e.message, 'err'); })
            .finally(function() {
                if (btn) { btn.disabled = false; btn.innerHTML = '&#128465; Alle löschen'; }
            });
        });
    };

    // ---- Diagnose ----
    window.diagnoseVouchers = function(siteId) {
        var box = document.getElementById('vcdiag-' + siteId);
        if (!box) return;
        if (box.style.display !== 'none' && box.dataset.loaded === '1') {
            box.style.display = 'none';
            box.dataset.loaded = '0';
            return;
        }
        box.style.display = 'block';
        box.innerHTML = '<div style="color:#6b7280;font-size:.8rem">Lade Diagnose…</div>';
        fetch('/unifi/vouchers/diagnose?site_id=' + encodeURIComponent(siteId), {
            headers: { 'X-Requested-With': 'XMLHttpRequest' }
        })
        .then(function(r) { return r.json(); })
        .then(function(d) {
            box.dataset.loaded = '1';
            box.innerHTML = '<pre>' + escH(JSON.stringify(d, null, 2)) + '</pre>';
        })
        .catch(function(e) {
            box.innerHTML = '<div style="color:#dc2626;font-size:.8rem">Fehler: ' + escH(e.message) + '</div>';
        });
    };

    // ---- Helfer: Zeilen der aktuellen Tabelle als Objekte ----
    function collectRows(siteId) {
        var tbody = document.getElementById('voucher-tbody-' + siteId);
        if (!tbody) return [];
        var out = [];
        tbody.querySelectorAll('tr').forEach(function(tr) {
            var codeEl = tr.querySelector('.vc-code');
            if (!codeEl) return;
            var c = tr.querySelectorAll('td');
            out.push({
                code:    codeEl.textContent.trim(),
                note:    c[1] ? c[1].textContent.trim() : '',
                created: c[2] ? c[2].textContent.trim() : '',
                expires: c[3] ? c[3].textContent.trim() : '',
                usage:   c[4] ? c[4].textContent.trim() : '',
                status:  c[5] ? c[5].textContent.trim() : ''
            });
        });
        return out;
    }

    // ---- Alle Codes kopieren ----
    window.copyAllCodes = function(siteId) {
        var codes = collectRows(siteId).map(function(r) { return r.code; });
        if (!codes.length) { toast('Keine Codes vorhanden', 'err'); return; }
        navigator.clipboard.writeText(codes.join('\n')).then(function() {
            toast(codes.length + ' Codes kopiert', 'ok');
        }, function() { toast('Kopieren fehlgeschlagen', 'err'); });
    };

    // ---- CSV-Export ----
    window.exportCsv = function(siteId) {
        var rows = collectRows(siteId);
        if (!rows.length) { toast('Keine Daten zum Export', 'err'); return; }
        function q(s) { return '"' + String(s).replace(/"/g, '""') + '"'; }
        var lines = [['Code','Notiz','Erstellt','Läuft ab','Nutzungen','Status'].map(q).join(';')];
        rows.forEach(function(r) {
            lines.push([r.code, r.note, r.created, r.expires, r.usage, r.status].map(q).join(';'));
        });
        // BOM für korrekte Umlaute in Excel
        var blob = new Blob(['' + lines.join('\r\n')], { type: 'text/csv;charset=utf-8;' });
        var a = document.createElement('a');
        a.href = URL.createObjectURL(blob);
        a.download = 'unifi-vouchers-' + siteId + '-' + new Date().toISOString().slice(0,10) + '.csv';
        document.body.appendChild(a); a.click();
        setTimeout(function() { URL.revokeObjectURL(a.href); a.remove(); }, 0);
        toast(rows.length + ' Zeilen exportiert', 'ok');
    };

    // ---- Druckansicht ----
    window.printVouchers = function(siteId) {
        var tbody = document.getElementById('voucher-tbody-' + siteId);
        if (!tbody) { toast('Keine Vouchers zum Drucken', 'err'); return; }
        var cards = '';
        tbody.querySelectorAll('tr').forEach(function(tr) {
            var codeEl = tr.querySelector('.vc-code');
            if (!codeEl) return;
            var cells = tr.querySelectorAll('td');
            var note    = cells[1] ? cells[1].textContent.trim() : '';
            var expires = cells[3] ? cells[3].textContent.trim() : '';
            cards += '<div class="pv-card">'
                + '<div class="pv-code">' + escH(codeEl.textContent.trim()) + '</div>'
                + (note && note !== '–' ? '<div class="pv-note">' + escH(note) + '</div>' : '')
                + (expires && expires !== '–' ? '<div class="pv-exp">Gültig bis: ' + escH(expires) + '</div>' : '')
                + '</div>';
        });
        if (!cards) { toast('Keine Vouchers zum Drucken', 'err'); return; }

        var w = window.open('', '_blank');
        if (!w) { toast('Popup blockiert – bitte erlauben', 'err'); return; }
        w.document.write(
            '<!DOCTYPE html><html lang="de"><head><meta charset="utf-8"><title>UniFi Voucher</title><style>'
            + 'body{font-family:system-ui,sans-serif;margin:1.5rem;}'
            + 'h1{font-size:1.1rem;margin:0 0 1rem;}'
            + '.pv-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:.75rem;}'
            + '.pv-card{border:1px dashed #94a3b8;border-radius:8px;padding:1rem;text-align:center;page-break-inside:avoid;}'
            + '.pv-code{font-family:ui-monospace,monospace;font-size:1.4rem;font-weight:700;letter-spacing:.08em;}'
            + '.pv-note{color:#475569;font-size:.85rem;margin-top:.35rem;}'
            + '.pv-exp{color:#64748b;font-size:.78rem;margin-top:.25rem;}'
            + '@media print{button{display:none;}}'
            + '</style></head><body>'
            + '<h1>WLAN-Gutscheine</h1>'
            + '<button onclick="window.print()" style="margin-bottom:1rem;padding:.5rem 1rem;cursor:pointer;">Drucken</button>'
            + '<div class="pv-grid">' + cards + '</div></body></html>'
        );
        w.document.close();
    };

    function setCount(siteId, n) {
        var countEl = document.getElementById('vccount-' + siteId);
        if (countEl) countEl.textContent = '(' + n + ')';
        var delAll = document.getElementById('vcdelall-' + siteId);
        if (delAll) delAll.style.display = n > 0 ? '' : 'none';
        document.querySelectorAll('#vc-site-ssd .ssd__item').forEach(function(item) {
            if (item.getAttribute('data-value') === siteId) {
                var base = item.textContent.replace(/\s*\(\d+\)\s*$/, '').trim();
                item.textContent = n > 0 ? base + ' (' + n + ')' : base;
            }
        });
    }

    function adjustCount(siteId, delta) {
        var countEl = document.getElementById('vccount-' + siteId);
        var cur = countEl ? parseInt((countEl.textContent.match(/\d+/) || ['0'])[0]) : 0;
        setCount(siteId, Math.max(0, cur + delta));
    }

    // ---- Voucher erstellen ----
    window.createVoucher = function(siteId) {
        var getVal = function(id) { return document.getElementById(id + '-' + siteId); };
        var expire = parseInt(getVal('expire')?.value || '1440');
        var n      = parseInt(getVal('n')?.value || '1');
        var quota  = parseInt(getVal('quota')?.value || '1');
        var down   = parseInt(getVal('down')?.value || '0');
        var up     = parseInt(getVal('up')?.value || '0');
        var note   = getVal('note')?.value || '';

        if (expire < 1 || n < 1 || n > 100) {
            toast('Ungültige Eingabe: Dauer ≥ 1, Anzahl 1–100.', 'err');
            return;
        }

        var submitBtn = document.querySelector('#panel-' + siteId + ' .vc-submit');
        if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = '⏳ Wird erstellt…'; }

        var body = new URLSearchParams({
            _csrf: csrf, site_id: siteId,
            expire: expire, n: n, quota: quota,
            down: down, up: up, note: note
        });

        fetch('/unifi/vouchers', { method: 'POST', body: body })
        .then(function(r) { return r.json(); })
        .then(function(d) {
            if (d.ok) {
                var codes = (d.vouchers || []).filter(function(c) { return c && c.trim() !== ''; });
                if (codes.length > 0) {
                    insertNewVouchers(siteId, codes);
                    toast(codes.length + ' Voucher erstellt', 'ok');
                } else {
                    toast('Voucher erstellt', 'ok');
                }
                // Vouchers-Liste automatisch aktualisieren
                setTimeout(function() { refreshVouchers(siteId); }, 600);
            } else {
                toast('Fehler: ' + (d.error || 'Unbekannt'), 'err');
            }
        })
        .catch(function(e) { toast('Netzwerkfehler: ' + e.message, 'err'); })
        .finally(function() {
            if (submitBtn) { submitBtn.disabled = false; submitBtn.innerHTML = '&#10133; Voucher erstellen'; }
        });
    };

    function insertNewVouchers(siteId, codes) {
        var container = document.getElementById('new-vouchers-' + siteId);
        if (!container) return;

        var html = '<div class="vc-card" style="margin-bottom:1rem;border-color:#bbf7d0">'
            + '<div class="vc-card-header" style="background:#f0fdf4;color:#166534">&#10003; Neue Vouchers <span class="vc-new-badge">' + codes.length + '</span></div>'
            + '<div class="vc-card-body">';

        codes.forEach(function(code) {
            var dc = fmtCode(code);
            html += '<div style="display:flex;align-items:center;gap:.5rem;margin-bottom:.5rem;flex-wrap:wrap">'
                + '<span class="vc-code">' + escH(dc) + '</span>'
                + '<button class="btn btn-xs btn-default vc-copy-btn" onclick="copyCode(this, \'' + escH(dc) + '\')">&#128203; Kopieren</button>'
                + (window.VoucherLabel ? VoucherLabel.buttons(dc) : '')
                + '</div>';
        });

        html += '</div></div>';
        container.innerHTML = html;
        container.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
    }

    function fmtCode(code) {
        var c = String(code).replace(/[-\s]/g, '');
        return c.length >= 10 ? c.slice(0, 5) + '-' + c.slice(5) : code;
    }

    function escH(str) {
        return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
    }

    // Etiketten-Druck-Buttons in die serverseitig gerenderten Voucher-Zeilen einsetzen
    document.querySelectorAll('.vc-print-slot').forEach(function(slot) {
        slot.innerHTML = window.VoucherLabel ? VoucherLabel.buttons(slot.dataset.code || '') : '';
    });

})();
</script>