Quelltext des aktuellen Standes
<?php
$this->layout('layouts/admin', [
'page_title' => 'Statistiken',
'current_admin_page' => 'statistics',
'breadcrumbs' => [['label' => 'Statistiken']],
]);
$stats = $stats ?? [];
$pages_per_topic = $pages_per_topic ?? [];
$activity_per_day = $activity_per_day ?? [];
$top_authors = $top_authors ?? [];
$review_stats = $review_stats ?? [];
$recent_edits = $recent_edits ?? [];
$action_distribution = $action_distribution ?? [];
$logins_per_day = $logins_per_day ?? [];
$maxPagesPerTopic = max(array_column($pages_per_topic, 'page_count') ?: [1]);
$maxActivity = max(array_column($activity_per_day, 'count') ?: [1]);
$maxVersions = max(array_column($top_authors, 'version_count') ?: [1]);
$maxActions = max(array_column($action_distribution, 'count') ?: [1]);
$maxLogins = max(array_column($logins_per_day, 'unique_users') ?: [1]);
function formatBytes(int $bytes): string {
if ($bytes >= 1073741824) return number_format($bytes / 1073741824, 1) . ' GB';
if ($bytes >= 1048576) return number_format($bytes / 1048576, 1) . ' MB';
if ($bytes >= 1024) return number_format($bytes / 1024, 0) . ' KB';
return $bytes . ' B';
}
?>
<!-- KPI Small Boxes (6 Kennzahlen) -->
<div class="kpi-row">
<div class="kpi-col">
<div class="small-box bg-info">
<div class="inner">
<h3><?= number_format($stats['active_users'] ?? 0) ?></h3>
<p><?= $this->te('Aktive Benutzer') ?> <small style="opacity:.8;"><?= $this->te('von {gesamt}', ['gesamt' => number_format($stats['total_users'] ?? 0)]) ?></small></p>
</div>
<div class="icon"><i class="fas fa-users"></i></div>
</div>
</div>
<div class="kpi-col">
<div class="small-box bg-success">
<div class="inner">
<h3><?= number_format($stats['total_topics'] ?? 0) ?></h3>
<p><?= $this->te('Themengebiete') ?></p>
</div>
<div class="icon"><i class="fas fa-folder"></i></div>
</div>
</div>
<div class="kpi-col">
<div class="small-box bg-primary">
<div class="inner">
<h3><?= number_format($stats['total_pages'] ?? 0) ?></h3>
<p><?= $this->te('Seiten') ?></p>
</div>
<div class="icon"><i class="fas fa-file-alt"></i></div>
</div>
</div>
<div class="kpi-col">
<div class="small-box bg-warning">
<div class="inner">
<h3><?= number_format($stats['total_versions'] ?? 0) ?></h3>
<p><?= $this->te('Versionen') ?></p>
</div>
<div class="icon"><i class="fas fa-code-branch"></i></div>
</div>
</div>
<div class="kpi-col">
<div class="small-box bg-teal">
<div class="inner">
<h3><?= number_format($stats['total_files'] ?? 0) ?></h3>
<p><?= $this->te('Dateien') ?> <small style="opacity:.8;"><?= formatBytes($stats['total_file_size'] ?? 0) ?></small></p>
</div>
<div class="icon"><i class="fas fa-paperclip"></i></div>
</div>
</div>
<div class="kpi-col">
<div class="small-box bg-secondary">
<div class="inner">
<h3><?= number_format($stats['total_companies'] ?? 0) ?></h3>
<p><?= $this->te('Firmen') ?> <small style="opacity:.8;"><?= $this->te('{anzahl} Kontakte', ['anzahl' => number_format($stats['total_contacts'] ?? 0)]) ?></small></p>
</div>
<div class="icon"><i class="fas fa-building"></i></div>
</div>
</div>
</div>
<!-- Aktivitäts-Charts -->
<div class="row">
<div class="col-lg-6">
<div class="card">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-chart-bar"></i> <?= $this->te('Aktivität (letzte 30 Tage)') ?></h3>
</div>
<div class="card-body">
<div class="bar-chart mini-chart">
<?php foreach ($activity_per_day as $day): ?>
<div class="bar-item-v" title="<?= $this->te('{tag}: {anzahl} Aktionen', ['tag' => date('d.m.', strtotime($day['day'])), 'anzahl' => $day['count']]) ?>">
<div class="bar-fill-v" style="height: <?= $maxActivity > 0 ? round(($day['count'] / $maxActivity) * 100) : 0 ?>%"></div>
<span class="bar-label-v"><?= date('d.', strtotime($day['day'])) ?></span>
</div>
<?php endforeach; ?>
<?php if (empty($activity_per_day)): ?>
<div class="chart-empty"><?= $this->te('Keine Daten') ?></div>
<?php endif; ?>
</div>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-sign-in-alt"></i> <?= $this->te('Eindeutige Logins (letzte 30 Tage)') ?></h3>
</div>
<div class="card-body">
<div class="bar-chart mini-chart">
<?php foreach ($logins_per_day as $day): ?>
<div class="bar-item-v" title="<?= $this->te('{tag}: {anzahl} Benutzer', ['tag' => date('d.m.', strtotime($day['day'])), 'anzahl' => $day['unique_users']]) ?>">
<div class="bar-fill-v bar-fill-green" style="height: <?= $maxLogins > 0 ? round(($day['unique_users'] / $maxLogins) * 100) : 0 ?>%"></div>
<span class="bar-label-v"><?= date('d.', strtotime($day['day'])) ?></span>
</div>
<?php endforeach; ?>
<?php if (empty($logins_per_day)): ?>
<div class="chart-empty"><?= $this->te('Keine Daten') ?></div>
<?php endif; ?>
</div>
</div>
</div>
</div>
</div>
<!-- Horizontale Balken-Charts -->
<div class="row">
<div class="col-lg-6">
<div class="card">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-folder"></i> <?= $this->te('Seiten pro Themengebiet') ?></h3>
</div>
<div class="card-body">
<div class="bar-chart-h">
<?php foreach ($pages_per_topic as $t): ?>
<div class="bar-item-h">
<span class="bar-label-h" title="<?= htmlspecialchars($t['name']) ?>"><?= htmlspecialchars(mb_substr($t['name'], 0, 25)) ?><?= mb_strlen($t['name']) > 25 ? '...' : '' ?></span>
<div class="bar-track">
<div class="bar-fill-h" style="width: <?= $maxPagesPerTopic > 0 ? round(($t['page_count'] / $maxPagesPerTopic) * 100) : 0 ?>%"></div>
</div>
<span class="bar-value-h"><?= $t['page_count'] ?></span>
</div>
<?php endforeach; ?>
<?php if (empty($pages_per_topic)): ?>
<div class="chart-empty"><?= $this->te('Keine Daten') ?></div>
<?php endif; ?>
</div>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-user-edit"></i> <?= $this->te('Top Autoren (nach Versionen)') ?></h3>
</div>
<div class="card-body">
<div class="bar-chart-h">
<?php foreach ($top_authors as $a): ?>
<div class="bar-item-h">
<span class="bar-label-h"><?= htmlspecialchars($a['display_name']) ?></span>
<div class="bar-track">
<div class="bar-fill-h bar-fill-purple" style="width: <?= $maxVersions > 0 ? round(($a['version_count'] / $maxVersions) * 100) : 0 ?>%"></div>
</div>
<span class="bar-value-h"><?= $a['version_count'] ?></span>
</div>
<?php endforeach; ?>
<?php if (empty($top_authors)): ?>
<div class="chart-empty"><?= $this->te('Keine Daten') ?></div>
<?php endif; ?>
</div>
</div>
</div>
</div>
</div>
<!-- Weitere Auswertungen -->
<div class="row">
<div class="col-lg-4">
<div class="card">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-sync"></i> <?= $this->te('Review-Status') ?></h3>
</div>
<div class="card-body">
<?php if (($review_stats['total'] ?? 0) > 0):
$pAktuell = round((($review_stats['aktuell'] ?? 0) / $review_stats['total']) * 100);
$pPruef = round((($review_stats['pruefpflichtig'] ?? 0) / $review_stats['total']) * 100);
$pUeber = round((($review_stats['ueberfaellig'] ?? 0) / $review_stats['total']) * 100);
?>
<div class="review-donut">
<div class="donut-bar">
<?php if ($pAktuell > 0): ?><div class="donut-seg donut-green" style="width:<?= $pAktuell ?>%"></div><?php endif; ?>
<?php if ($pPruef > 0): ?><div class="donut-seg donut-amber" style="width:<?= $pPruef ?>%"></div><?php endif; ?>
<?php if ($pUeber > 0): ?><div class="donut-seg donut-red" style="width:<?= $pUeber ?>%"></div><?php endif; ?>
</div>
<div class="donut-legend">
<div class="legend-item"><span class="legend-dot green"></span> <?= $this->te('Aktuell: {anzahl}', ['anzahl' => $review_stats['aktuell'] ?? 0]) ?></div>
<div class="legend-item"><span class="legend-dot amber"></span> <?= $this->te('Prüfpflichtig: {anzahl}', ['anzahl' => $review_stats['pruefpflichtig'] ?? 0]) ?></div>
<div class="legend-item"><span class="legend-dot red"></span> <?= $this->te('Überfällig: {anzahl}', ['anzahl' => $review_stats['ueberfaellig'] ?? 0]) ?></div>
</div>
</div>
<?php else: ?>
<div class="chart-empty"><?= $this->te('Keine Reviews konfiguriert') ?></div>
<?php endif; ?>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-list"></i> <?= $this->te('Häufigste Aktionen (30 Tage)') ?></h3>
</div>
<div class="card-body">
<div class="bar-chart-h compact">
<?php foreach ($action_distribution as $a): ?>
<div class="bar-item-h">
<span class="bar-label-h"><?= htmlspecialchars($a['action_type']) ?></span>
<div class="bar-track">
<div class="bar-fill-h bar-fill-teal" style="width: <?= $maxActions > 0 ? round(($a['count'] / $maxActions) * 100) : 0 ?>%"></div>
</div>
<span class="bar-value-h"><?= number_format($a['count']) ?></span>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-history"></i> <?= $this->te('Letzte Bearbeitungen') ?></h3>
</div>
<div class="card-body p-0">
<div class="recent-edits-list" style="padding:0.75rem 1.25rem;">
<?php foreach ($recent_edits as $edit): ?>
<div class="recent-edit-item">
<a href="/pages/<?= $edit['id'] ?>" class="re-title"><?= htmlspecialchars(mb_substr($edit['title'], 0, 40)) ?><?= mb_strlen($edit['title']) > 40 ? '...' : '' ?></a>
<div class="re-meta">
<span class="re-author"><?= htmlspecialchars($edit['author']) ?></span>
<span class="re-date"><?= date('d.m. H:i', strtotime($edit['created_at'])) ?></span>
</div>
</div>
<?php endforeach; ?>
<?php if (empty($recent_edits)): ?>
<div class="chart-empty"><?= $this->te('Keine Bearbeitungen') ?></div>
<?php endif; ?>
</div>
</div>
</div>
</div>
</div>
<style>
/* Vertikale Balken (Aktivitäts-Charts) */
.bar-chart.mini-chart {
display: flex;
align-items: flex-end;
gap: 2px;
height: 120px;
padding-bottom: 1.5rem;
position: relative;
}
.bar-item-v {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
height: 100%;
justify-content: flex-end;
position: relative;
}
.bar-fill-v {
width: 100%;
background: var(--blue-500);
border-radius: 2px 2px 0 0;
min-height: 2px;
transition: height 0.3s;
}
.bar-fill-v.bar-fill-green { background: var(--emerald-500); }
.bar-label-v {
position: absolute;
bottom: -1.25rem;
font-size: 0.55rem;
color: var(--gray-400);
white-space: nowrap;
}
.bar-item-v .bar-label-v { display: none; }
.bar-item-v:nth-child(5n+1) .bar-label-v,
.bar-item-v:first-child .bar-label-v,
.bar-item-v:last-child .bar-label-v { display: block; }
/* Horizontale Balken */
.bar-chart-h { display: flex; flex-direction: column; gap: 0.5rem; }
.bar-chart-h.compact { gap: 0.35rem; }
.bar-item-h { display: grid; grid-template-columns: 120px 1fr 40px; align-items: center; gap: 0.5rem; }
.bar-label-h { font-size: 0.8rem; color: var(--gray-700); overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.bar-track { height: 20px; background: var(--gray-100); border-radius: 0.25rem; overflow: hidden; }
.bar-fill-h { height: 100%; background: var(--blue-500); border-radius: 0.25rem; transition: width 0.5s; min-width: 2px; }
.bar-fill-h.bar-fill-purple { background: var(--violet-500); }
.bar-fill-h.bar-fill-teal { background: var(--teal-500); }
.bar-value-h { font-size: 0.75rem; font-weight: 600; color: var(--gray-500); text-align: right; }
/* Review-Donut */
.review-donut { display: flex; flex-direction: column; gap: 1rem; }
.donut-bar { display: flex; height: 24px; border-radius: 0.5rem; overflow: hidden; background: var(--gray-100); }
.donut-seg { transition: width 0.5s; }
.donut-green { background: var(--emerald-500); }
.donut-amber { background: var(--amber-500); }
.donut-red { background: var(--red-500); }
.donut-legend { display: flex; flex-direction: column; gap: 0.4rem; }
.legend-item { display: flex; align-items: center; gap: 0.5rem; font-size: 0.8rem; color: var(--gray-700); }
.legend-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
.legend-dot.green { background: var(--emerald-500); }
.legend-dot.amber { background: var(--amber-500); }
.legend-dot.red { background: var(--red-500); }
/* Letzte Bearbeitungen */
.recent-edits-list { display: flex; flex-direction: column; gap: 0.5rem; }
.recent-edit-item { padding: 0.5rem 0; border-bottom: 1px solid var(--gray-100); }
.recent-edit-item:last-child { border-bottom: none; }
.re-title { font-size: 0.85rem; font-weight: 500; color: var(--gray-900); text-decoration: none; }
.re-title:hover { color: var(--blue-600); }
.re-meta { display: flex; gap: 0.75rem; font-size: 0.75rem; color: var(--gray-400); margin-top: 0.2rem; }
.chart-empty { text-align: center; color: var(--gray-400); font-size: 0.85rem; padding: 2rem 0; }
@media (max-width: 768px) {
.bar-item-h { grid-template-columns: 80px 1fr 35px; }
}
</style>