Show sourcecode
The following files exists in this folder. Click to view.
public_html/smartkortet/admin/
index.php
login.php
logout.php
statistik.php
statistik.php
254 lines UTF-8 Windows (CRLF)
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
<?php
declare(strict_types=1);
require_once __DIR__ . '/../includes/functions.php';
startAppSession();
require_once __DIR__ . '/../includes/auth.php';
require_once __DIR__ . '/../config/database.php';
$admin = requireAdminAuth();
$kpi = [
'total_spent' => 0.0,
'month_spent' => 0.0,
'total_starting_balances' => 0.0,
'estimated_remaining' => 0.0,
];
$kpi['total_spent'] = (float) db()->query('SELECT COALESCE(SUM(amount), 0) FROM spend_entries')->fetchColumn();
$kpi['month_spent'] = (float) db()->query('SELECT COALESCE(SUM(amount), 0) FROM spend_entries WHERE spent_on BETWEEN DATE_FORMAT(CURDATE(), "%Y-%m-01") AND LAST_DAY(CURDATE())')->fetchColumn();
$kpi['total_starting_balances'] = (float) db()->query('SELECT COALESCE(SUM(starting_balance), 0) FROM user_settings')->fetchColumn();
$kpi['estimated_remaining'] = (float) db()->query('SELECT COALESCE(SUM(t.remaining), 0) FROM (SELECT us.user_id, GREATEST(us.starting_balance - COALESCE(SUM(se.amount), 0), 0) AS remaining FROM user_settings us LEFT JOIN spend_entries se ON se.user_id = us.user_id AND se.spent_on BETWEEN us.period_start AND us.period_end GROUP BY us.user_id, us.starting_balance) t')->fetchColumn();
$restaurantRows = db()->query("SELECT COALESCE(NULLIF(TRIM(place_name), ''), 'Okänd') AS label, COUNT(*) AS visits, SUM(amount) AS total FROM spend_entries GROUP BY label ORDER BY visits DESC, total DESC LIMIT 10")->fetchAll();
$topUsers = db()->query('SELECT u.name, u.email, COALESCE(SUM(se.amount), 0) AS total_spent FROM users u LEFT JOIN spend_entries se ON se.user_id = u.id GROUP BY u.id, u.name, u.email ORDER BY total_spent DESC LIMIT 10')->fetchAll();
$dailySpend = db()->query('SELECT spent_on AS day_key, SUM(amount) AS total FROM spend_entries WHERE spent_on >= DATE_SUB(CURDATE(), INTERVAL 29 DAY) GROUP BY day_key ORDER BY day_key')->fetchAll();
$userGrowth = db()->query('SELECT DATE(created_at) AS day_key, COUNT(*) AS total FROM users WHERE DATE(created_at) >= DATE_SUB(CURDATE(), INTERVAL 29 DAY) GROUP BY day_key ORDER BY day_key')->fetchAll();
$restaurantLabels = [];
$restaurantVisits = [];
foreach ($restaurantRows as $row) {
$restaurantLabels[] = $row['label'];
$restaurantVisits[] = (int) $row['visits'];
}
$dailySpendMap = [];
foreach ($dailySpend as $row) {
$dailySpendMap[(string) $row['day_key']] = (float) $row['total'];
}
$userGrowthMap = [];
foreach ($userGrowth as $row) {
$userGrowthMap[(string) $row['day_key']] = (int) $row['total'];
}
$dailyLabels = [];
$dailySpendValues = [];
$growthValues = [];
for ($i = 29; $i >= 0; $i--) {
$date = (new DateTimeImmutable('today'))->modify("-{$i} days");
$key = $date->format('Y-m-d');
$dailyLabels[] = $date->format('d/m');
$dailySpendValues[] = $dailySpendMap[$key] ?? 0.0;
$growthValues[] = $userGrowthMap[$key] ?? 0;
}
?>
<!doctype html>
<html lang="sv">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="theme-color" content="#0b1220">
<title>Admin Statistik | Matkortet</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="<?= e(url('assets/css/style.css')) ?>">
</head>
<body data-theme="dark">
<main class="app-shell">
<header class="card panel topbar">
<div>
<h1>Admin Statistik</h1>
<p class="subtitle">Överblick för ekonomi, restauranger och användartillväxt</p>
</div>
<div class="admin-actions">
<a class="btn btn-secondary" href="<?= e(url('index.php')) ?>">Till appen</a>
<a class="btn btn-secondary" href="<?= e(url('database/createdb.php')) ?>">DB Console</a>
<a class="btn btn-danger" href="<?= e(url('admin/logout.php')) ?>">Logga ut admin</a>
</div>
</header>
<nav class="card panel admin-subnav">
<a class="tab-link" href="<?= e(url('admin/index.php')) ?>">Dashboard</a>
<a class="tab-link active" href="<?= e(url('admin/statistik.php')) ?>">Statistik</a>
</nav>
<section class="kpi-grid">
<article class="card kpi">
<span>Total spendering (all-time)</span>
<strong><?= e(formatCurrency($kpi['total_spent'])) ?></strong>
</article>
<article class="card kpi">
<span>Spendering denna månad</span>
<strong><?= e(formatCurrency($kpi['month_spent'])) ?></strong>
</article>
<article class="card kpi">
<span>Totalt startsaldo</span>
<strong><?= e(formatCurrency($kpi['total_starting_balances'])) ?></strong>
</article>
<article class="card kpi">
<span>Beräknat kvar totalt</span>
<strong><?= e(formatCurrency($kpi['estimated_remaining'])) ?></strong>
</article>
</section>
<section class="card panel" style="margin-top:16px;">
<h3>Diagram</h3>
<div class="chart-grid admin-chart-grid">
<article class="chart-card">
<h4>Dagsvis spendering (30 dagar)</h4>
<canvas id="admin-spend-line"></canvas>
</article>
<article class="chart-card">
<h4>Populäraste restauranger (besök)</h4>
<canvas id="admin-restaurants-bar"></canvas>
</article>
<article class="chart-card">
<h4>Nya användare per dag (30 dagar)</h4>
<canvas id="admin-growth-line"></canvas>
</article>
</div>
</section>
<section class="card panel" style="margin-top:16px;">
<h3>Topp 10 restauranger</h3>
<div class="admin-table-wrap">
<table class="admin-table">
<thead>
<tr>
<th>Restaurang</th>
<th>Besök</th>
<th>Total spendering</th>
</tr>
</thead>
<tbody>
<?php foreach ($restaurantRows as $row): ?>
<tr>
<td data-label="Restaurang"><?= e($row['label']) ?></td>
<td data-label="Besök"><?= e((string) $row['visits']) ?></td>
<td data-label="Total spendering"><?= e(formatCurrency((float) $row['total'])) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</section>
<section class="card panel" style="margin-top:16px;">
<h3>Topp 10 användare (spendering)</h3>
<div class="admin-table-wrap">
<table class="admin-table">
<thead>
<tr>
<th>Namn</th>
<th>E-post</th>
<th>Total spendering</th>
</tr>
</thead>
<tbody>
<?php foreach ($topUsers as $u): ?>
<tr>
<td data-label="Namn"><?= e($u['name']) ?></td>
<td data-label="E-post"><?= e($u['email']) ?></td>
<td data-label="Total spendering"><?= e(formatCurrency((float) $u['total_spent'])) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</section>
</main>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
(() => {
const palette = ['#14b8a6', '#38bdf8', '#f59e0b', '#ef4444', '#8b5cf6', '#22c55e', '#06b6d4', '#f97316', '#84cc16', '#eab308'];
const spendCtx = document.getElementById('admin-spend-line');
if (spendCtx) {
new Chart(spendCtx, {
type: 'line',
data: {
labels: <?= json_encode($dailyLabels, JSON_UNESCAPED_UNICODE) ?>,
datasets: [{
label: 'kr',
data: <?= json_encode($dailySpendValues, JSON_UNESCAPED_UNICODE) ?>,
borderColor: '#38bdf8',
backgroundColor: 'rgba(56, 189, 248, 0.16)',
fill: true,
tension: 0.25,
borderWidth: 3
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: { y: { beginAtZero: true } }
}
});
}
const restCtx = document.getElementById('admin-restaurants-bar');
if (restCtx) {
new Chart(restCtx, {
type: 'bar',
data: {
labels: <?= json_encode($restaurantLabels, JSON_UNESCAPED_UNICODE) ?>,
datasets: [{
label: 'Besök',
data: <?= json_encode($restaurantVisits, JSON_UNESCAPED_UNICODE) ?>,
backgroundColor: palette,
borderWidth: 0
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: { y: { beginAtZero: true } },
plugins: { legend: { display: false } }
}
});
}
const growthCtx = document.getElementById('admin-growth-line');
if (growthCtx) {
new Chart(growthCtx, {
type: 'line',
data: {
labels: <?= json_encode($dailyLabels, JSON_UNESCAPED_UNICODE) ?>,
datasets: [{
label: 'Nya användare',
data: <?= json_encode($growthValues, JSON_UNESCAPED_UNICODE) ?>,
borderColor: '#22c55e',
backgroundColor: 'rgba(34, 197, 94, 0.15)',
fill: true,
tension: 0.25,
borderWidth: 3
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: { y: { beginAtZero: true, precision: 0 } }
}
});
}
})();
</script>
</body>
</html>