Some checks are pending
CI / PHP Lint (push) Waiting to run
CI / PHP Lint-1 (push) Waiting to run
CI / Unit Tests & Static Analysis (push) Waiting to run
CI / PHP Lint (pull_request) Waiting to run
CI / PHP Lint-1 (pull_request) Waiting to run
CI / Unit Tests & Static Analysis (pull_request) Waiting to run
Frontend, Login/Installer/Updater und der komplette Admin-Bereich nutzen jetzt ein einziges Stylesheet (assets/global.css) statt pro Seite dupliziertem Inline-CSS. Design-System - Tokens für Flächen, Text, Linien, Marke, Status, Radien, Schatten und Layout-Maße; Dark Mode ausschließlich über Tokens (keine !important- Overrides mehr) - Komponenten: Buttons, Formularfelder, Cards, Tabellen, Badges, Alerts, Tabs, Pagination, Modals, Toasts, Statistik-Kacheln, Empty States - Schrift Inter mit System-Fallback Oberfläche - Admin-Shell neu: durchgehende Sidebar mit Marke, gruppierter Navigation und Benutzerbereich; schlanke Topbar mit Breadcrumb - Dashboard: ruhige KPI-Kacheln mit Icon-Chips, Charts an Theme-Farben gekoppelt - Öffentliche Voucher-Seite: App-Topbar, klare Formularstruktur und Ticket-Darstellung des erstellten Codes inkl. QR-Code - Login/Passwort/2FA: zweispaltiges Auth-Layout bzw. Fokus-Karten - Updater und Wartungsmodus im gleichen Look (Wartungsseite bleibt bewusst eigenständig ohne externe Abhängigkeiten) - Emoji-Icons in der UI durch Font-Awesome-Icons ersetzt Nebenbei behoben - Falscher SRI-Hash blockierte qrcode.min.js – QR-Codes wurden auf der Voucher-Seite und bei der 2FA-Einrichtung nie gerendert - assets/global.css wurde in mehreren Admin-Seiten über einen falschen Pfad eingebunden ($adminBase = '' statt '../') - TinyMCE lädt ohne API-Key jetzt die GPL-Variante von cdnjs – kein "valid API key required"-Banner mehr im Einstellungs-Editor - Tabellen in Cards scrollen horizontal statt zu überlaufen Screenshots in docs/screenshots neu erstellt, README aktualisiert (neuer Abschnitt "Design-System", Version 2.5.0). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
138 lines
6.1 KiB
PHP
138 lines
6.1 KiB
PHP
<?php
|
||
error_reporting(E_ALL);
|
||
ini_set('display_errors', 0);
|
||
ini_set('log_errors', 1);
|
||
|
||
require_once __DIR__ . '/../config.php';
|
||
require_once __DIR__ . '/../includes/Database.php';
|
||
require_once __DIR__ . '/../includes/Auth.php';
|
||
require_once __DIR__ . '/../includes/UniFiController.php';
|
||
require_once __DIR__ . '/../includes/Notifier.php';
|
||
require_once __DIR__ . '/../includes/I18n.php';
|
||
|
||
$auth = new Auth();
|
||
$auth->requireAdmin();
|
||
I18n::init();
|
||
|
||
$db = Database::getInstance();
|
||
$appTitle = $db->getSetting('app_title', 'UniFi Voucher System');
|
||
$defaultExpire = max(1, (int)$db->getSetting('default_expire_minutes', 480));
|
||
$defaultMaxUses = max(1, (int)$db->getSetting('default_max_uses', 1));
|
||
|
||
$error = '';
|
||
$success = '';
|
||
$results = [];
|
||
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['do_import'])) {
|
||
if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
|
||
$error = __('error_csrf');
|
||
} else {
|
||
try {
|
||
$siteId = (int)($_POST['site_id'] ?? 0);
|
||
$site = $db->fetchOne("SELECT * FROM sites WHERE id=? AND is_active=1", [$siteId]);
|
||
if (!$site) throw new Exception('Site nicht gefunden');
|
||
|
||
// CSV-Quelle: Datei bevorzugt, sonst Textarea
|
||
$raw = '';
|
||
if (!empty($_FILES['csv']['tmp_name'])) {
|
||
$raw = file_get_contents($_FILES['csv']['tmp_name']);
|
||
} else {
|
||
$raw = (string)($_POST['csv_text'] ?? '');
|
||
}
|
||
$lines = preg_split('/\r\n|\r|\n/', trim($raw));
|
||
if (count($lines) > 200) throw new Exception('Maximal 200 Zeilen pro Import.');
|
||
|
||
$controller = new UniFiController(
|
||
$site['unifi_controller_url'], $site['unifi_username'],
|
||
Crypto::decrypt($site['unifi_password']), $site['site_id']
|
||
);
|
||
|
||
$created = 0;
|
||
foreach ($lines as $i => $line) {
|
||
$line = trim($line);
|
||
if ($line === '') continue;
|
||
$cols = str_getcsv($line);
|
||
$name = trim((string)($cols[0] ?? ''));
|
||
if ($name === '' || strtolower($name) === 'name') continue; // Header/leer überspringen
|
||
$maxUses = isset($cols[1]) && $cols[1] !== '' ? max(1, (int)$cols[1]) : $defaultMaxUses;
|
||
$expire = isset($cols[2]) && $cols[2] !== '' ? max(1, (int)$cols[2]) : $defaultExpire;
|
||
try {
|
||
$v = $controller->createVoucher(date('Y-m-d') . '_' . $name, $maxUses, $expire);
|
||
if (!is_array($v) || empty($v['formatted_code'])) throw new Exception('ungültige Antwort');
|
||
$db->execute(
|
||
"INSERT INTO vouchers (site_id, user_id, voucher_code, voucher_name, max_uses, expire_minutes, unifi_voucher_id)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||
[$siteId, $_SESSION['user_id'], $v['code'], date('Y-m-d') . '_' . $name, $maxUses, $expire, $v['unifi_id'] ?? null]
|
||
);
|
||
$results[] = ['name' => $name, 'code' => $v['formatted_code'], 'ok' => true];
|
||
$created++;
|
||
} catch (Exception $e) {
|
||
$results[] = ['name' => $name, 'code' => $e->getMessage(), 'ok' => false];
|
||
}
|
||
}
|
||
if ($created > 0) {
|
||
Notifier::voucherCreated($created, $site['name'], $_SESSION['user_name'] ?? null);
|
||
$auth->writeAuditLog($_SESSION['user_id'], 'voucher_import', 'site', $siteId, "$created Voucher importiert");
|
||
}
|
||
$success = "$created Voucher erstellt.";
|
||
} catch (Exception $e) {
|
||
$error = $e->getMessage();
|
||
}
|
||
}
|
||
}
|
||
|
||
$sites = $db->fetchAll("SELECT * FROM sites WHERE is_active=1 ORDER BY name");
|
||
$csrf = $auth->getCsrfToken();
|
||
$currentPage = 'import';
|
||
$adminBase = '';
|
||
?>
|
||
<!DOCTYPE html>
|
||
<html lang="<?= I18n::getLanguage() ?>">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>CSV-Import – <?= htmlspecialchars($appTitle) ?></title>
|
||
<?php require __DIR__ . '/../includes/admin_nav.php'; ?>
|
||
<div class="page-header">
|
||
<div>
|
||
<h1 class="page-title">Voucher-Import</h1>
|
||
<p class="page-subtitle">Mehrere Vouchers auf einmal aus einer CSV-Liste erstellen.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<?php if ($error): ?><div class="alert alert-error"><?= htmlspecialchars($error) ?></div><?php endif; ?>
|
||
<?php if ($success): ?><div class="alert alert-ok"><?= htmlspecialchars($success) ?></div><?php endif; ?>
|
||
|
||
<div class="card">
|
||
<h2>Mehrere Voucher erstellen</h2>
|
||
<p class="muted">Eine Zeile pro Voucher: <code>Name,MaxGeräte,Minuten</code> – MaxGeräte und Minuten sind optional (Standardwerte greifen). Max. 200 Zeilen. Beispiel:<br>
|
||
<code>Gast Müller,1,480</code> · <code>Konferenzraum A,5,240</code> · <code>Tagespass</code></p>
|
||
<form method="post" enctype="multipart/form-data">
|
||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf) ?>">
|
||
<label>Standort</label>
|
||
<select class="input" name="site_id" required>
|
||
<?php foreach ($sites as $s): ?><option value="<?= (int)$s['id'] ?>"><?= htmlspecialchars($s['name']) ?></option><?php endforeach; ?>
|
||
</select>
|
||
<label>CSV-Datei (optional)</label>
|
||
<input class="input" type="file" name="csv" accept=".csv,text/csv">
|
||
<label>… oder direkt einfügen</label>
|
||
<textarea name="csv_text" placeholder="Gast Müller,1,480 Konferenzraum A,5,240"></textarea>
|
||
<button class="btn" type="submit" name="do_import" style="margin-top:14px;" onclick="return confirm('Import jetzt starten?');">Importieren</button>
|
||
</form>
|
||
</div>
|
||
|
||
<?php if (!empty($results)): ?>
|
||
<div class="card">
|
||
<h2>Ergebnis</h2>
|
||
<table><tr><th>Name</th><th>Code / Fehler</th><th>Status</th></tr>
|
||
<?php foreach ($results as $r): ?>
|
||
<tr><td><?= htmlspecialchars($r['name']) ?></td><td><code><?= htmlspecialchars($r['code']) ?></code></td><td><?= $r['ok'] ? '✅' : '❌' ?></td></tr>
|
||
<?php endforeach; ?>
|
||
</table>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
</main>
|
||
<script src="../assets/global.js"></script>
|
||
</body>
|
||
</html>
|