Unifi-Voucher-Tool/admin/reports.php
Friederich Loheide 498c7e28e0
Some checks failed
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) Has been cancelled
CI / PHP Lint-1 (pull_request) Has been cancelled
CI / Unit Tests & Static Analysis (pull_request) Has been cancelled
Redesign: gemeinsames Design-System für Frontend und Backend
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>
2026-09-22 20:33:52 +00:00

174 lines
7.1 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
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/I18n.php';
$auth = new Auth();
$auth->requireAdmin();
I18n::init();
$db = Database::getInstance();
$appTitle = $db->getSetting('app_title', 'UniFi Voucher System');
// Zeitraum (Tage)
$days = max(1, min(365, (int)($_GET['days'] ?? 30)));
// CSV-Export
if (isset($_GET['export'])) {
$auth->requireAdmin();
header('Content-Type: text/csv; charset=utf-8');
$fn = 'report-' . $_GET['export'] . '-' . date('Y-m-d') . '.csv';
header('Content-Disposition: attachment; filename="' . $fn . '"');
$out = fopen('php://output', 'w');
fprintf($out, "\xEF\xBB\xBF"); // UTF-8 BOM für Excel
if ($_GET['export'] === 'per_site') {
fputcsv($out, ['Site', 'Gesamt', 'Gültig', 'Verwendet', 'Abgelaufen']);
$rows = $db->fetchAll(
"SELECT s.name,
COUNT(v.id) total,
SUM(v.status='valid') valid,
SUM(v.status='used') used,
SUM(v.status='expired') expired
FROM sites s LEFT JOIN vouchers v ON v.site_id=s.id
GROUP BY s.id ORDER BY total DESC"
);
foreach ($rows as $r) fputcsv($out, [$r['name'], (int)$r['total'], (int)$r['valid'], (int)$r['used'], (int)$r['expired']]);
} elseif ($_GET['export'] === 'per_user') {
fputcsv($out, ['Benutzer', 'E-Mail', 'Voucher erstellt']);
$rows = $db->fetchAll(
"SELECT u.name, u.email, COUNT(v.id) c FROM users u
LEFT JOIN vouchers v ON v.user_id=u.id GROUP BY u.id ORDER BY c DESC"
);
foreach ($rows as $r) fputcsv($out, [$r['name'], $r['email'], (int)$r['c']]);
} else { // daily
fputcsv($out, ['Datum', 'Erstellte Voucher']);
$rows = $db->fetchAll(
"SELECT DATE(created_at) d, COUNT(*) c FROM vouchers
WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL ? DAY)
GROUP BY DATE(created_at) ORDER BY d", [$days]
);
foreach ($rows as $r) fputcsv($out, [$r['d'], (int)$r['c']]);
}
fclose($out);
exit;
}
// Kennzahlen
$totals = $db->fetchOne(
"SELECT COUNT(*) total,
SUM(status='valid') valid, SUM(status='used') used, SUM(status='expired') expired
FROM vouchers"
);
$inPeriod = (int)($db->fetchOne(
"SELECT COUNT(*) c FROM vouchers WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL ? DAY)", [$days]
)['c'] ?? 0);
$perSite = $db->fetchAll(
"SELECT s.name,
COUNT(v.id) total, SUM(v.status='valid') valid, SUM(v.status='used') used, SUM(v.status='expired') expired
FROM sites s LEFT JOIN vouchers v ON v.site_id=s.id GROUP BY s.id ORDER BY total DESC"
);
$perUser = $db->fetchAll(
"SELECT u.name, COUNT(v.id) c FROM users u LEFT JOIN vouchers v ON v.user_id=u.id
GROUP BY u.id HAVING c > 0 ORDER BY c DESC LIMIT 10"
);
$daily = $db->fetchAll(
"SELECT DATE(created_at) d, COUNT(*) c FROM vouchers
WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL ? DAY)
GROUP BY DATE(created_at) ORDER BY d", [$days]
);
$chartLabels = array_map(fn($r) => date('d.m', strtotime($r['d'])), $daily);
$chartData = array_map(fn($r) => (int)$r['c'], $daily);
$csrf = $auth->getCsrfToken();
$currentPage = 'reports';
$adminBase = '';
?>
<!DOCTYPE html>
<html lang="<?= I18n::getLanguage() ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Reporting <?= htmlspecialchars($appTitle) ?></title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<?php require __DIR__ . '/../includes/admin_nav.php'; ?>
<div class="page-header">
<div>
<h1 class="page-title">Reporting</h1>
<p class="page-subtitle">Auswertungen nach Zeitraum, Site und Benutzer.</p>
</div>
</div>
<div class="toolbar no-print">
<form method="get" style="display:flex;gap:8px;align-items:center;">
<label style="margin:0;">Zeitraum</label>
<select class="input" name="days" onchange="this.form.submit()">
<?php foreach ([7,30,90,365] as $d): ?>
<option value="<?= $d ?>" <?= $days===$d?'selected':'' ?>><?= $d ?> Tage</option>
<?php endforeach; ?>
</select>
</form>
<a class="btn btn-secondary" href="?export=daily&days=<?= $days ?>"><i class="fas fa-download"></i> CSV (täglich)</a>
<a class="btn btn-secondary" href="?export=per_site"><i class="fas fa-download"></i> CSV (pro Site)</a>
<a class="btn btn-secondary" href="?export=per_user"><i class="fas fa-download"></i> CSV (pro Nutzer)</a>
<button class="btn btn-secondary" onclick="window.print()"><i class="fas fa-print"></i> Drucken/PDF</button>
</div>
<div class="grid4">
<div class="stat"><div class="n"><?= (int)$totals['total'] ?></div><div class="l">Vouchers gesamt</div></div>
<div class="stat"><div class="n"><?= (int)$totals['valid'] ?></div><div class="l">Gültig</div></div>
<div class="stat"><div class="n"><?= (int)$totals['used'] ?></div><div class="l">Verwendet</div></div>
<div class="stat"><div class="n"><?= $inPeriod ?></div><div class="l">In <?= $days ?> Tagen erstellt</div></div>
</div>
<div class="card">
<h2>Erstellte Voucher (<?= $days ?> Tage)</h2>
<canvas id="chart" height="90"></canvas>
</div>
<div class="card">
<h2>Pro Site</h2>
<table><tr><th>Site</th><th>Gesamt</th><th>Gültig</th><th>Verwendet</th><th>Abgelaufen</th></tr>
<?php foreach ($perSite as $r): ?>
<tr><td><?= htmlspecialchars($r['name']) ?></td><td><?= (int)$r['total'] ?></td><td><?= (int)$r['valid'] ?></td><td><?= (int)$r['used'] ?></td><td><?= (int)$r['expired'] ?></td></tr>
<?php endforeach; ?>
</table>
</div>
<div class="card">
<h2>Top-Nutzer</h2>
<table><tr><th>Benutzer</th><th>Voucher erstellt</th></tr>
<?php foreach ($perUser as $r): ?>
<tr><td><?= htmlspecialchars($r['name'] ?? '') ?></td><td><?= (int)$r['c'] ?></td></tr>
<?php endforeach; ?>
<?php if (empty($perUser)): ?><tr><td colspan="2" style="color:var(--text-muted);">Keine Daten</td></tr><?php endif; ?>
</table>
</div>
</main>
<script src="../assets/global.js"></script>
<script>
const styles = getComputedStyle(document.documentElement);
const accent = styles.getPropertyValue('--accent').trim();
const grid = styles.getPropertyValue('--border-color').trim();
const muted = styles.getPropertyValue('--text-muted').trim();
new Chart(document.getElementById('chart'), {
type:'line',
data:{ labels: <?= json_encode($chartLabels) ?>, datasets:[{ label:'Voucher', data: <?= json_encode($chartData) ?>, borderColor: accent, backgroundColor: accent + '22', fill:true, tension:.35, pointRadius:0, pointHoverRadius:4, borderWidth:2 }] },
options:{
plugins:{legend:{display:false}},
scales:{
y:{ beginAtZero:true, border:{display:false}, grid:{color:grid, drawTicks:false}, ticks:{precision:0, color:muted, padding:10, font:{size:11}} },
x:{ border:{display:false}, grid:{display:false}, ticks:{color:muted, padding:8, font:{size:11}} }
}
}
});
</script>
</body>
</html>