Files
arbeitsstunden/httpdocs/app/views.php
2026-05-22 15:53:32 +02:00

690 lines
46 KiB
PHP

<?php
declare(strict_types=1);
function renderHeader(string $title): void
{
global $config;
?>
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= htmlspecialchars($config['app_name'] . ' - ' . $title) ?></title>
<link href="https://cdn.jsdelivr.net/npm/@tabler/core@latest/dist/css/tabler.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/tabler-icons.min.css" rel="stylesheet">
<style>
:root {
--tci-blue: #206bc4;
--tci-blue-2: #1a5aa5;
--tci-blue-3: #5b9def;
--tci-sand: #f6f8fb;
--tci-cream: #ffffff;
--tci-border: #dce1e7;
}
body {
background: #f5f7fb;
}
.page-wrapper .navbar {
background: #ffffff;
color: #fff;
border-bottom: 1px solid #e6ebf1;
min-height: 56px;
}
.navbar-brand-wrap {
display: flex;
align-items: center;
gap: 14px;
position: relative;
padding-left: 114px;
}
.navbar-mark {
width: 128px;
height: 128px;
border-radius: 0;
background: transparent;
border: 0;
flex: 0 0 auto;
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
pointer-events: none;
}
.navbar-mark img {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
padding: 0;
}
.navbar .navbar-brand,
.navbar .nav-link,
.navbar .text-secondary,
.navbar .btn {
color: #182433 !important;
}
.navbar-brand-text {
display: flex;
flex-direction: column;
line-height: 1.05;
margin-left: 18px;
font-size: 0.875rem;
}
.navbar-brand-text span:last-child {
text-transform: uppercase;
letter-spacing: 0.06em;
font-size: 0.9em;
}
.navbar .nav-link.active {
background: rgba(32, 107, 196, 0.08);
border-radius: 999px;
}
.card {
border-color: var(--tci-border);
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px rgba(15, 23, 42, 0.04);
background: rgba(255, 255, 255, 0.98);
border-radius: 12px;
}
.card.card-md {
overflow: hidden;
}
.card-header.bg-primary-subtle {
background: linear-gradient(90deg, rgba(15, 76, 129, 0.12), rgba(45, 110, 163, 0.12));
}
.btn-primary,
.btn-success {
background: var(--tci-blue);
border-color: var(--tci-blue);
}
.btn-primary:hover,
.btn-success:hover {
background: var(--tci-blue-2);
border-color: var(--tci-blue-2);
}
.btn-outline-secondary {
color: #182433;
border-color: #cdd6e1;
}
.app-main .btn-outline-secondary {
color: var(--tci-blue);
border-color: rgba(15, 76, 129, 0.28);
}
.app-main .btn-outline-secondary:hover {
color: #fff;
background: var(--tci-blue);
border-color: var(--tci-blue);
}
.app-main .btn-outline-primary {
color: var(--tci-blue);
border-color: rgba(15, 76, 129, 0.28);
}
.app-main .btn-outline-primary:hover {
color: #fff;
background: var(--tci-blue);
border-color: var(--tci-blue);
}
.app-main .btn-outline-danger {
color: #b42318;
border-color: rgba(180, 35, 24, 0.32);
}
.app-main .btn-outline-danger:hover {
color: #fff;
background: #b42318;
border-color: #b42318;
}
.admin-actions .btn {
min-width: 320px;
min-height: 38px;
}
.badge.bg-blue-lt,
.text-blue {
background: rgba(32, 107, 196, 0.1) !important;
color: var(--tci-blue) !important;
}
.progress-bar {
background: linear-gradient(90deg, var(--tci-blue), var(--tci-blue-2));
}
.text-primary {
color: var(--tci-blue) !important;
}
.login-hero {
border: 1px solid var(--tci-border);
border-radius: 14px;
background: linear-gradient(180deg, #ffffff 0%, #f7f9fc 100%);
padding: 18px;
}
.login-hero .section-title,
.login-hero .text-secondary {
color: #182433 !important;
}
.section-title {
color: var(--tci-blue);
letter-spacing: 0.01em;
display: flex;
align-items: center;
gap: 10px;
}
.section-title i {
font-size: 1.1rem;
}
.app-shell {
display: flex;
align-items: stretch;
min-height: calc(100vh - 56px);
}
.sidebar {
width: 250px;
background: #f5f7fb;
border-right: 1px solid #e6ebf1;
padding: 18px 14px;
flex: 0 0 auto;
box-shadow: inset -1px 0 0 rgba(255, 255, 255, 0.5);
}
.sidebar-title {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #667382;
margin-bottom: 12px;
}
.sidebar-nav {
display: flex;
flex-direction: column;
gap: 8px;
}
.sidebar-nav a {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-radius: 9px;
color: #182433;
text-decoration: none;
background: transparent;
border: 1px solid transparent;
transition: background-color 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
}
.sidebar-nav a i {
font-size: 1rem;
opacity: 0.85;
}
.sidebar-nav a:hover {
background: rgba(32, 107, 196, 0.06);
transform: translateX(2px);
box-shadow: none;
}
.sidebar-nav a.active {
background: rgba(32, 107, 196, 0.1);
color: var(--tci-blue);
font-weight: 700;
border-color: rgba(32, 107, 196, 0.12);
box-shadow: none;
}
.app-main {
flex: 1 1 auto;
min-width: 0;
}
.app-footer {
margin-top: 24px;
padding-top: 14px;
border-top: 1px solid var(--tci-border);
font-size: 0.875rem;
color: #667382;
}
[data-bs-theme="dark"] .app-footer {
border-top-color: rgba(148, 163, 184, 0.16);
color: #9fb0c3;
}
.stat-card {
position: relative;
overflow: hidden;
}
.stat-card .stat-icon {
position: absolute;
top: 16px;
right: 16px;
font-size: 1.4rem;
color: rgba(15, 76, 129, 0.22);
}
[data-bs-theme="dark"] .stat-card .stat-icon {
color: rgba(255, 255, 255, 0.78);
}
.soft-note {
background: #f8fafc;
}
[data-bs-theme="dark"] body {
background: #0f172a;
color: #e5edf7;
}
[data-bs-theme="dark"] .page-wrapper .navbar {
background: #111827;
border-bottom-color: rgba(148, 163, 184, 0.12);
}
[data-bs-theme="dark"] .navbar .navbar-brand,
[data-bs-theme="dark"] .navbar .nav-link,
[data-bs-theme="dark"] .navbar .text-secondary,
[data-bs-theme="dark"] .navbar .btn {
color: #e5edf7 !important;
}
[data-bs-theme="dark"] .card {
background: #111827;
border-color: rgba(148, 163, 184, 0.14);
box-shadow: 0 1px 2px rgba(2, 6, 23, 0.22), 0 8px 24px rgba(2, 6, 23, 0.22);
}
[data-bs-theme="dark"] .login-hero,
[data-bs-theme="dark"] .soft-note {
background: #111827;
border-color: rgba(148, 163, 184, 0.16);
}
[data-bs-theme="dark"] .sidebar {
background: #0f172a;
border-right-color: rgba(148, 163, 184, 0.16);
box-shadow: inset -1px 0 0 rgba(255, 255, 255, 0.03);
}
[data-bs-theme="dark"] .sidebar-title {
color: rgba(191, 219, 254, 0.72);
}
[data-bs-theme="dark"] .sidebar-nav a {
color: #dbe7f5;
background: transparent;
border-color: transparent;
}
[data-bs-theme="dark"] .sidebar-nav a:hover {
background: rgba(30, 41, 59, 0.72);
box-shadow: 0 6px 16px rgba(2, 6, 23, 0.28);
}
[data-bs-theme="dark"] .section-title,
[data-bs-theme="dark"] .text-primary {
color: #8fc0f0 !important;
}
[data-bs-theme="dark"] .text-secondary,
[data-bs-theme="dark"] .form-label,
[data-bs-theme="dark"] .table,
[data-bs-theme="dark"] .table th,
[data-bs-theme="dark"] .table td,
[data-bs-theme="dark"] .list-group-item,
[data-bs-theme="dark"] .card-title,
[data-bs-theme="dark"] .h1,
[data-bs-theme="dark"] strong,
[data-bs-theme="dark"] code {
color: #dbe7f5 !important;
}
[data-bs-theme="dark"] .table thead th,
[data-bs-theme="dark"] .list-group-item,
[data-bs-theme="dark"] hr {
border-color: rgba(148, 163, 184, 0.16);
}
[data-bs-theme="dark"] .table tbody tr:nth-child(odd) td {
background: rgba(15, 23, 42, 0.34);
}
[data-bs-theme="dark"] .table tbody tr:nth-child(even) td {
background: rgba(30, 41, 59, 0.34);
}
[data-bs-theme="dark"] .table tbody tr:hover td {
background: rgba(59, 130, 246, 0.14);
}
[data-bs-theme="dark"] .form-control,
[data-bs-theme="dark"] .form-select {
background: rgba(15, 23, 42, 0.88);
border-color: rgba(148, 163, 184, 0.18);
color: #e5edf7;
}
[data-bs-theme="dark"] .form-control::placeholder {
color: rgba(219, 231, 245, 0.45);
}
[data-bs-theme="dark"] .app-main .btn-outline-secondary,
[data-bs-theme="dark"] .app-main .btn-outline-primary {
color: #b9d7f4;
border-color: rgba(143, 192, 240, 0.35);
}
[data-bs-theme="dark"] .app-main .btn-outline-secondary:hover,
[data-bs-theme="dark"] .app-main .btn-outline-primary:hover {
color: #08131f;
background: #8fc0f0;
border-color: #8fc0f0;
}
[data-bs-theme="dark"] .badge.bg-blue-lt,
[data-bs-theme="dark"] .text-blue {
background: rgba(143, 192, 240, 0.16) !important;
color: #dbe7f5 !important;
}
@media (max-width: 900px) {
.app-shell {
flex-direction: column;
}
.sidebar {
width: 100%;
border-right: 0;
border-bottom: 1px solid var(--tci-border);
}
}
</style>
</head>
<body>
<script>
(function () {
var savedTheme = localStorage.getItem('tci-theme');
var theme = savedTheme === 'dark' ? 'dark' : 'light';
document.documentElement.setAttribute('data-bs-theme', theme);
})();
</script>
<?php
}
function renderFooter(): void
{
?>
<script src="https://cdn.jsdelivr.net/npm/@tabler/core@latest/dist/js/tabler.min.js"></script>
<script>
(function () {
var button = document.getElementById('theme-toggle');
if (!button) {
return;
}
function applyTheme(theme) {
document.documentElement.setAttribute('data-bs-theme', theme);
localStorage.setItem('tci-theme', theme);
button.innerHTML = theme === 'dark' ? '<i class="ti ti-sun"></i>' : '<i class="ti ti-moon-stars"></i>';
button.setAttribute('aria-label', theme === 'dark' ? 'Lightmode umschalten' : 'Darkmode umschalten');
}
applyTheme(document.documentElement.getAttribute('data-bs-theme') === 'dark' ? 'dark' : 'light');
button.addEventListener('click', function () {
applyTheme(document.documentElement.getAttribute('data-bs-theme') === 'dark' ? 'light' : 'dark');
});
})();
</script>
</body>
</html>
<?php
}
function renderLoginPage(): void
{
global $error, $notice, $pdo;
if (!$notice && isset($_SESSION['login_notice'])) {
$notice = (string)$_SESSION['login_notice'];
unset($_SESSION['login_notice']);
}
?>
<div class="container-tight py-4">
<div class="login-hero text-center mb-4">
<div class="mb-3"><img src="/logo%20neu.png" alt="TC Ingelfingen Logo" style="max-width: 140px; width: 100%; height: auto;"></div>
<h1 class="mb-1 section-title justify-content-center"><i class="ti ti-tennis"></i><span>TC Ingelfingen</span></h1>
<p class="text-secondary mb-0">Arbeitsstundenverwaltung</p>
</div>
<?php if (!$pdo): ?><div class="alert alert-warning">Keine Datenbankverbindung. Die App läuft nur im Demo-Modus, bis DB_HOST, DB_NAME, DB_USER und DB_PASS gesetzt sind.</div><?php endif; ?>
<?php if ($notice): ?><div class="alert alert-success"><?= htmlspecialchars($notice) ?></div><?php endif; ?>
<?php if ($error): ?><div class="alert alert-danger"><?= htmlspecialchars($error) ?></div><?php endif; ?>
<div class="card card-md">
<div class="card-body">
<form method="post">
<input type="hidden" name="action" value="login">
<div class="mb-3"><label class="form-label">E-Mail</label><input class="form-control" name="email" type="email" required></div>
<div class="mb-3"><label class="form-label">Passwort</label><input class="form-control" name="password" type="password" required></div>
<button class="btn btn-primary w-100" type="submit">Anmelden</button>
</form>
<div class="text-center mt-3"><a href="#forgot-password" data-bs-toggle="collapse" role="button" aria-expanded="false" aria-controls="forgot-password">Passwort vergessen?</a></div>
<div class="collapse mt-3" id="forgot-password"><form method="post"><input type="hidden" name="action" value="request_password_reset"><div class="mb-3"><label class="form-label">E-Mail für Reset-Link</label><input class="form-control" name="email" type="email" placeholder="E-Mail" required></div><button class="btn btn-outline-primary w-100" type="submit">Reset-Link anfordern</button></form></div>
<div class="text-secondary mt-3 d-flex align-items-center justify-content-center gap-2"><?php if ($pdo): ?><span class="text-success d-inline-flex align-items-center gap-2"><i class="ti ti-database"></i><span>Mit Datenbank</span></span><?php else: ?><span class="text-danger d-inline-flex align-items-center gap-2"><i class="ti ti-database-off"></i><span>Ohne Datenbank</span></span><?php endif; ?></div>
</div>
</div>
</div>
<?php
}
function renderResetPasswordPage(): void
{
global $error, $notice;
$token = trim((string)($_GET['token'] ?? ''));
?>
<div class="container-tight py-4">
<?php if ($notice): ?><div class="alert alert-success"><?= htmlspecialchars($notice) ?></div><?php endif; ?>
<?php if ($error): ?><div class="alert alert-danger"><?= htmlspecialchars($error) ?></div><?php endif; ?>
<div class="card card-md">
<div class="card-body">
<h2 class="section-title mb-3"><i class="ti ti-key"></i><span>Passwort zurücksetzen</span></h2>
<form method="post">
<input type="hidden" name="action" value="reset_password">
<input type="hidden" name="token" value="<?= htmlspecialchars($token) ?>">
<div class="mb-3"><label class="form-label">Neues Passwort</label><input class="form-control" name="new_password" type="password" minlength="6" required></div>
<div class="mb-3"><label class="form-label">Neues Passwort wiederholen</label><input class="form-control" name="confirm_password" type="password" minlength="6" required></div>
<button class="btn btn-primary w-100" type="submit">Passwort speichern</button>
</form>
<div class="text-center mt-3"><a href="/?page=login">Zurück zur Anmeldung</a></div>
</div>
</div>
</div>
<?php
}
function renderAppShell(string $page): void
{
global $config, $notice, $user, $pdo;
$current = currentUser() ?? [];
$users = fetchUsers($pdo);
$memberPage = max(1, (int)($_GET['member_page'] ?? 1));
$memberUsers = fetchUsersPage($pdo, $memberPage, 50);
$logPage = max(1, (int)($_GET['log_page'] ?? 1));
$logs = fetchLogs($pdo, $logPage, 50);
$bookingPage = max(1, (int)($_GET['booking_page'] ?? 1));
$bookings = fetchBookingsPage($pdo, $current, $bookingPage, 50);
$totalWorked = 0.0;
foreach ($users as $item) {
if ((int)$item['id'] === (int)($current['id'] ?? 0)) {
$totalWorked = (float)$item['hours_worked'];
break;
}
}
$missingHours = max(0, $config['hours_target'] - $totalWorked);
$chargeAmount = $missingHours * $config['hourly_rate_eur'];
$canSeeAllMembers = hasRole($current, ['editor', 'admin']);
$dashboardTargetHours = $canSeeAllMembers ? ((float)$config['hours_target'] * count($users)) : (float)$config['hours_target'];
$dashboardWorkedHours = $canSeeAllMembers ? array_sum(array_map(static fn (array $item): float => (float)$item['hours_worked'], $users)) : $totalWorked;
$dashboardMissingHours = 0.0;
foreach ($users as $item) {
if (!$canSeeAllMembers && (int)$item['id'] !== (int)($current['id'] ?? 0)) {
continue;
}
$dashboardMissingHours += max(0, $config['hours_target'] - (float)$item['hours_worked']);
}
$dashboardWorkedAmount = $dashboardWorkedHours * $config['hourly_rate_eur'];
$dashboardMissingAmount = $dashboardMissingHours * $config['hourly_rate_eur'];
$dashboardScopeLabel = $canSeeAllMembers ? 'Alle Mitglieder' : (trim(((string)($current['firstname'] ?? '')) . ' ' . ((string)($current['lastname'] ?? ''))) ?: 'Mein Konto');
$dashboardProgressPercent = min(100, ($dashboardWorkedHours / max(1, $dashboardTargetHours)) * 100);
?>
<div class="page">
<div class="page-wrapper">
<div class="navbar navbar-expand-md d-print-none">
<div class="container-xl">
<a class="navbar-brand navbar-brand-wrap" href="/">
<span class="navbar-mark" aria-hidden="true"><img src="/logo%20neu.png" alt=""></span>
<span class="navbar-brand-text"><span>Arbeitsstunden</span><span>Dashboard</span></span>
</a>
<div class="ms-auto d-flex align-items-center gap-2 flex-wrap justify-content-end">
<a class="btn btn-outline-secondary d-inline-flex align-items-center gap-2 text-decoration-none" href="/?page=profile" style="height: 38px;">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 12a5 5 0 1 0-5-5a5 5 0 0 0 5 5"/><path d="M4 20a8 8 0 0 1 16 0"/></svg>
<span class="d-flex flex-column align-items-start justify-content-center lh-1"><span class="text-truncate"><?= htmlspecialchars(trim(((string)($current['firstname'] ?? '')) . ' ' . ((string)($current['lastname'] ?? ''))) ?: ($current['name'] ?? 'guest')) ?></span><span class="small opacity-75"><?= htmlspecialchars(roleLabel((string)($current['role'] ?? ''))) ?></span></span>
</a>
<button class="btn btn-outline-secondary" type="button" id="theme-toggle" aria-label="Darkmode umschalten"><i class="ti ti-moon-stars"></i></button>
<form method="post"><input type="hidden" name="action" value="logout"><button class="btn btn-outline-secondary" type="submit">Abmelden</button></form>
</div>
</div>
</div>
<div class="app-shell">
<aside class="sidebar">
<div class="sidebar-title">Menü</div>
<nav class="sidebar-nav">
<a class="<?= $page === 'dashboard' ? 'active' : '' ?>" href="/?page=dashboard"><i class="ti ti-layout-dashboard"></i><span>Dashboard</span></a>
<a class="<?= $page === 'profile' ? 'active' : '' ?>" href="/?page=profile"><i class="ti ti-user-circle"></i><span>Profil</span></a>
<?php if (($current['role'] ?? '') !== 'member'): ?><a class="<?= $page === 'members' ? 'active' : '' ?>" href="/?page=members"><i class="ti ti-user-plus"></i><span>Benutzerverwaltung</span></a><?php endif; ?>
<a class="<?= $page === 'booking' ? 'active' : '' ?>" href="/?page=booking"><i class="ti ti-clock-plus"></i><span>Stundenbuchung</span></a>
<?php if (($current['role'] ?? '') !== 'member'): ?><a class="<?= $page === 'admin' ? 'active' : '' ?>" href="/?page=admin"><i class="ti ti-settings"></i><span>Vereinskonfiguration</span></a><?php endif; ?>
<?php if (($current['role'] ?? '') === 'admin'): ?><a class="<?= $page === 'administration' ? 'active' : '' ?>" href="/?page=administration"><i class="ti ti-tool"></i><span>Administration</span></a><?php endif; ?>
</nav>
</aside>
<main class="app-main page-body">
<div class="container-xl py-4">
<?php if ($notice): ?><div class="alert alert-success"><?= htmlspecialchars($notice) ?></div><?php endif; ?>
<?php if ($error): ?><div class="alert alert-danger"><?= htmlspecialchars($error) ?></div><?php endif; ?>
<?php if ($page === 'dashboard'): ?>
<div class="row row-cards mb-3">
<div class="col-md-3"><div class="card stat-card"><div class="card-body"><i class="ti ti-target stat-icon"></i><div class="text-secondary small mb-1"><?= htmlspecialchars($dashboardScopeLabel) ?></div><div class="h1 mb-1"><?= number_format($dashboardTargetHours, 1, ',', '.') ?> h</div><div class="text-secondary"><?= $canSeeAllMembers ? 'Pflichtstunden gesamt' : 'Pflichtstunden' ?></div></div></div></div>
<div class="col-md-3"><div class="card stat-card"><div class="card-body"><i class="ti ti-currency-euro stat-icon"></i><div class="text-secondary small mb-1">Pro Stunde</div><div class="h1 mb-1"><?= money((float)$config['hourly_rate_eur']) ?></div><div class="text-secondary">Stundensatz</div></div></div></div>
<div class="col-md-3"><div class="card stat-card"><div class="card-body"><i class="ti ti-bolt stat-icon"></i><div class="text-secondary small mb-1"><?= htmlspecialchars($dashboardScopeLabel) ?></div><div class="h1 mb-1"><?= number_format($dashboardWorkedHours, 1, ',', '.') ?> h / <?= money($dashboardWorkedAmount) ?></div><div class="text-secondary"><?= $canSeeAllMembers ? 'Geleistet gesamt' : 'Geleistet' ?></div></div></div></div>
<div class="col-md-3"><div class="card stat-card"><div class="card-body"><i class="ti ti-hourglass stat-icon"></i><div class="text-secondary small mb-1"><?= htmlspecialchars($dashboardScopeLabel) ?></div><div class="h1 mb-1"><?= number_format($dashboardMissingHours, 1, ',', '.') ?> h / <?= money($dashboardMissingAmount) ?></div><div class="text-secondary"><?= $canSeeAllMembers ? 'Noch offen gesamt' : 'Noch offen' ?></div></div></div></div>
</div>
<div class="row row-cards mb-3">
<div class="col-lg-12"><div class="card"><div class="card-body"><div class="d-flex align-items-start justify-content-between gap-3 mb-2"><h3 class="section-title mb-0"><i class="ti ti-chart-donut-2"></i><span>Dashboard</span></h3><div class="text-secondary d-inline-flex align-items-center gap-2"><i class="ti ti-chart-line"></i><span><?= number_format($dashboardProgressPercent, 0, ',', '.') ?>%</span></div></div><div class="progress mb-3"><div class="progress-bar" style="width: <?= $dashboardProgressPercent ?>%"></div></div><p class="text-secondary">Geleistete Arbeitsstunden in %.</p></div></div></div>
</div>
<div class="row row-cards">
<div class="col-lg-12"><div class="card"><div class="card-body"><h3 class="section-title"><i class="ti ti-list-details"></i><span>Meine Stunden / Mitglieder</span></h3><div class="table-responsive"><table class="table table-vcenter"><thead><tr><th>Name</th><th>Rolle</th><th>Soll</th><th>Ist</th><th>Rest</th><th>Betrag</th></tr></thead><tbody><?php foreach ($users as $item): ?><?php $worked = (float)$item['hours_worked']; $rest = max(0, $config['hours_target'] - $worked); $amount = $rest * $config['hourly_rate_eur']; $isOwn = (int)$item['id'] === (int)($current['id'] ?? 0); if (!$canSeeAllMembers && !$isOwn) { continue; } ?><tr><td><?= htmlspecialchars(trim(((string)($item['firstname'] ?? '')) . ' ' . ((string)($item['lastname'] ?? ''))) ?: (string)$item['name']) ?></td><td><?= htmlspecialchars(roleLabel((string)$item['role'])) ?></td><td><?= (int)$config['hours_target'] ?> h</td><td><?= number_format($worked, 1, ',', '.') ?> h</td><td><?= number_format($rest, 1, ',', '.') ?> h</td><td><?= money($amount) ?></td></tr><?php endforeach; ?></tbody></table></div></div></div></div>
</div>
<?php if (trim((string)$config['dashboard_info']) !== ''): ?>
<div class="row row-cards mt-3">
<div class="col-lg-12"><div class="card"><div class="card-body"><h3 class="section-title"><i class="ti ti-info-circle"></i><span>Information</span></h3><div class="text-secondary"><?= renderSimpleMarkdown((string)$config['dashboard_info']) ?></div></div></div></div>
</div>
<?php endif; ?>
<?php endif; ?>
<?php if ($page === 'profile'): ?>
<div class="row row-cards">
<div class="col-lg-6"><div class="card"><div class="card-body"><h3 class="section-title"><i class="ti ti-user-circle"></i><span>Profil</span></h3><form method="post"><input type="hidden" name="action" value="update_profile"><div class="mb-3"><label class="form-label">Vorname</label><input class="form-control" name="firstname" value="<?= htmlspecialchars((string)($current['firstname'] ?? '')) ?>" required></div><div class="mb-3"><label class="form-label">Nachname</label><input class="form-control" name="lastname" value="<?= htmlspecialchars((string)($current['lastname'] ?? '')) ?>" required></div><div class="mb-3"><label class="form-label">E-Mail</label><input class="form-control" value="<?= htmlspecialchars((string)($current['email'] ?? '')) ?>" disabled></div><button class="btn btn-primary" type="submit">Profil speichern</button></form></div></div></div>
<div class="col-lg-6"><div class="card"><div class="card-body"><h3 class="section-title"><i class="ti ti-lock"></i><span>Passwort ändern</span></h3><form method="post"><input type="hidden" name="action" value="change_password"><div class="mb-3"><label class="form-label">Aktuelles Passwort</label><input class="form-control" name="current_password" type="password" required></div><div class="mb-3"><label class="form-label">Neues Passwort</label><input class="form-control" name="new_password" type="password" minlength="6" required></div><div class="mb-3"><label class="form-label">Neues Passwort wiederholen</label><input class="form-control" name="confirm_password" type="password" minlength="6" required></div><button class="btn btn-primary" type="submit">Passwort speichern</button></form></div></div></div>
</div>
<div class="row row-cards mt-3">
<div class="col-lg-12"><div class="card soft-note"><div class="card-body"><h3 class="section-title"><i class="ti ti-shield-lock"></i><span>Hinweis</span></h3><p class="text-secondary mb-2">Jeder Benutzer kann Vorname, Nachname und das eigene Passwort selbst ändern.</p><p class="text-secondary mb-0">Die E-Mail-Adresse wird angezeigt, kann aber nicht bearbeitet werden. Das neue Passwort muss mindestens 6 Zeichen lang sein.</p></div></div></div>
</div>
<?php endif; ?>
<?php if ($page === 'members'): ?>
<div class="row row-cards mb-3">
<div class="col-lg-8"><div class="card"><div class="card-body"><h3 class="section-title"><i class="ti ti-user-plus"></i><span>Benutzer anlegen</span></h3><form method="post"><input type="hidden" name="action" value="create_member"><div class="row"><div class="col-md-6 mb-3"><label class="form-label">Vorname</label><input class="form-control" name="firstname" placeholder="Vorname" required></div><div class="col-md-6 mb-3"><label class="form-label">Nachname</label><input class="form-control" name="lastname" placeholder="Nachname" required></div></div><div class="mb-3"><label class="form-label">E-Mail</label><input class="form-control" name="email" type="email" placeholder="E-Mail" required></div><?php if (($current['role'] ?? '') === 'admin'): ?><div class="mb-3"><label class="form-label">Rolle</label><select class="form-select" name="role"><option value="member">Mitglied</option><option value="editor">Bearbeiter</option><option value="admin">Admin</option></select></div><?php endif; ?><div class="text-secondary small mb-3">Benutzer werden mit dem Startpasswort <strong>mitglied123</strong> angelegt.</div><button class="btn btn-primary w-100" type="submit">Benutzer speichern</button></form></div></div></div>
<div class="col-lg-4"><div class="card soft-note"><div class="card-body"><h3 class="section-title"><i class="ti ti-info-circle"></i><span>Hinweis</span></h3><p class="text-secondary mb-2">Nur Admins dürfen Rollen setzen oder ändern.</p><p class="text-secondary mb-0">Neue Benutzer erhalten automatisch das Startpasswort <strong>mitglied123</strong>.</p></div></div></div>
</div>
<div class="row row-cards">
<div class="col-lg-12"><div class="card"><div class="card-body"><h3 class="section-title"><i class="ti ti-users"></i><span>Benutzerliste</span></h3><div class="table-responsive"><table class="table table-vcenter"><thead><tr><th>Nachname</th><th>Vorname</th><th>E-Mail</th><th>Rolle</th><th>Geleistet</th><th class="text-end">Aktionen</th></tr></thead><tbody><?php foreach (($memberUsers['entries'] ?? []) as $item): ?><tr><td><?= htmlspecialchars((string)($item['lastname'] ?? '')) ?></td><td><?= htmlspecialchars((string)($item['firstname'] ?? '')) ?></td><td><?= htmlspecialchars($item['email']) ?></td><td><?php if (($current['role'] ?? '') === 'admin'): ?><form method="post" class="d-flex gap-2 align-items-center justify-content-start"><input type="hidden" name="action" value="update_user_role"><input type="hidden" name="user_id" value="<?= (int)$item['id'] ?>"><select class="form-select form-select-sm" name="role"<?= ((int)$item['id'] === (int)($current['id'] ?? 0)) ? ' disabled' : '' ?>><option value="member" <?= (string)$item['role'] === 'member' ? 'selected' : '' ?>>Mitglied</option><option value="editor" <?= (string)$item['role'] === 'editor' ? 'selected' : '' ?>>Bearbeiter</option><option value="admin" <?= (string)$item['role'] === 'admin' ? 'selected' : '' ?>>Admin</option></select><?php if ((int)$item['id'] !== (int)($current['id'] ?? 0)): ?><button class="btn btn-sm btn-outline-primary" type="submit">Speichern</button><?php endif; ?></form><?php else: ?><span class="badge bg-secondary-lt"><?= htmlspecialchars(roleLabel((string)$item['role'])) ?></span><?php endif; ?></td><td><?= number_format((float)$item['hours_worked'], 1, ',', '.') ?> h</td><td class="text-end"><div class="d-inline-flex gap-2 flex-wrap justify-content-end"><?php if (canManageMembers($current) && (string)$item['role'] !== 'admin'): ?><form method="post" class="m-0"><input type="hidden" name="action" value="reset_user_password"><input type="hidden" name="user_id" value="<?= (int)$item['id'] ?>"><button class="btn btn-sm btn-outline-secondary" type="submit">Passwort zurücksetzen</button></form><?php endif; ?><?php if (canManageMembers($current) && (string)$item['role'] !== 'admin'): ?><form method="post" class="m-0" onsubmit="return confirm('Benutzer wirklich löschen?');"><input type="hidden" name="action" value="delete_user"><input type="hidden" name="user_id" value="<?= (int)$item['id'] ?>"><button class="btn btn-sm btn-outline-danger" type="submit">Löschen</button></form><?php endif; ?><?php if (!canManageMembers($current) || (string)$item['role'] === 'admin'): ?><span class="text-secondary small">Keine Aktionen</span><?php endif; ?></div></td></tr><?php endforeach; ?><?php if (empty($memberUsers['entries'])): ?><tr><td colspan="6" class="text-secondary text-center">Keine Benutzer vorhanden.</td></tr><?php endif; ?></tbody></table></div><?php if (($memberUsers['total_pages'] ?? 1) > 1): ?><div class="d-flex align-items-center justify-content-between gap-3 mt-3"><a class="btn btn-outline-secondary<?= ($memberUsers['page'] ?? 1) <= 1 ? ' disabled' : '' ?>" href="/?page=members&member_page=<?= max(1, (int)($memberUsers['page'] ?? 1) - 1) ?>">Zurück</a><div class="text-secondary small">Seite <?= (int)($memberUsers['page'] ?? 1) ?> von <?= (int)($memberUsers['total_pages'] ?? 1) ?>, insgesamt <?= (int)($memberUsers['total'] ?? 0) ?> Einträge</div><a class="btn btn-outline-secondary<?= ($memberUsers['page'] ?? 1) >= ($memberUsers['total_pages'] ?? 1) ? ' disabled' : '' ?>" href="/?page=members&member_page=<?= min((int)($memberUsers['total_pages'] ?? 1), (int)($memberUsers['page'] ?? 1) + 1) ?>">Weiter</a></div><?php endif; ?></div></div></div></div>
</div>
<?php endif; ?>
<?php if ($page === 'booking'): ?>
<div class="row row-cards mb-3">
<?php if (($current['role'] ?? '') !== 'member'): ?>
<div class="col-lg-8"><div class="card"><div class="card-body"><h3 class="section-title"><i class="ti ti-clock-plus"></i><span>Stunden für Benutzer buchen</span></h3><form method="post"><input type="hidden" name="action" value="book_hours"><div class="mb-3"><label class="form-label">Benutzer</label><select class="form-select" name="member_id" required><?php foreach ($users as $item): ?><option value="<?= (int)$item['id'] ?>"><?= htmlspecialchars(trim(((string)($item['firstname'] ?? '')) . ' ' . ((string)($item['lastname'] ?? ''))) ?: (string)$item['email']) ?> (<?= htmlspecialchars(roleLabel((string)$item['role'])) ?>)</option><?php endforeach; ?></select></div><div class="mb-3"><label class="form-label">Stunden</label><input class="form-control" name="hours" type="number" step="0.25" min="0.25" placeholder="Stunden" required></div><div class="mb-3"><label class="form-label">Notiz</label><input class="form-control" name="note" placeholder="Notiz" required></div><button class="btn btn-primary w-100" type="submit">Stunden buchen</button></form></div></div></div>
<div class="col-lg-4"><div class="card soft-note"><div class="card-body"><h3 class="section-title"><i class="ti ti-info-circle"></i><span>Hinweis</span></h3><p class="text-secondary mb-0">Nur Bearbeiter und Admins können Stunden buchen.</p></div></div></div>
<?php else: ?>
<div class="col-lg-12"><div class="card soft-note"><div class="card-body"><h3 class="section-title"><i class="ti ti-info-circle"></i><span>Meine Stundenbuchungen</span></h3><p class="text-secondary mb-0">Hier sehen Sie Ihre eigenen Stundenbuchungen.</p></div></div></div>
<?php endif; ?>
</div>
<div class="row row-cards">
<div class="col-lg-12"><div class="card"><div class="card-body"><h3 class="section-title"><i class="ti ti-list-details"></i><span>Stundenbuchungen</span></h3><div class="table-responsive"><table class="table table-vcenter"><thead><tr><th>Datum</th><th>Mitglied</th><th>Gebucht von</th><th>Stunden</th><th>Notiz</th><th class="text-end">Aktionen</th></tr></thead><tbody><?php foreach (($bookings['entries'] ?? []) as $booking): ?><tr><td><?= htmlspecialchars(formatDateTime((string)$booking['ts'])) ?></td><td><?= htmlspecialchars((string)$booking['member']) ?></td><td><?= htmlspecialchars((string)$booking['actor']) ?></td><td><?= number_format((float)$booking['hours'], 2, ',', '.') ?> h</td><td><?= htmlspecialchars((string)$booking['note']) ?></td><td class="text-end"><?php if (($current['role'] ?? '') === 'admin'): ?><form method="post" class="m-0 d-inline-block" onsubmit="return confirm('Stundenbuchung wirklich löschen?');"><input type="hidden" name="action" value="delete_booking"><input type="hidden" name="booking_id" value="<?= (int)$booking['id'] ?>"><button class="btn btn-sm btn-outline-danger" type="submit">Löschen</button></form><?php else: ?><span class="text-secondary small">Keine Aktionen</span><?php endif; ?></td></tr><?php endforeach; ?><?php if (empty($bookings['entries'])): ?><tr><td colspan="6" class="text-secondary text-center">Keine Stundenbuchungen vorhanden.</td></tr><?php endif; ?></tbody></table></div><?php if (($bookings['total_pages'] ?? 1) > 1): ?><div class="d-flex align-items-center justify-content-between gap-3 mt-3"><a class="btn btn-outline-secondary<?= ($bookings['page'] ?? 1) <= 1 ? ' disabled' : '' ?>" href="/?page=booking&booking_page=<?= max(1, (int)($bookings['page'] ?? 1) - 1) ?>">Zurück</a><div class="text-secondary small">Seite <?= (int)($bookings['page'] ?? 1) ?> von <?= (int)($bookings['total_pages'] ?? 1) ?>, insgesamt <?= (int)($bookings['total'] ?? 0) ?> Einträge</div><a class="btn btn-outline-secondary<?= ($bookings['page'] ?? 1) >= ($bookings['total_pages'] ?? 1) ? ' disabled' : '' ?>" href="/?page=booking&booking_page=<?= min((int)($bookings['total_pages'] ?? 1), (int)($bookings['page'] ?? 1) + 1) ?>">Weiter</a></div><?php endif; ?></div></div></div></div>
</div>
<?php endif; ?>
<?php if ($page === 'admin' && ($current['role'] ?? '') !== 'member'): ?>
<div class="card mt-2">
<div class="card-body">
<h3 class="section-title"><i class="ti ti-settings"></i><span>Vereinskonfiguration</span></h3>
<div class="row row-cards">
<div class="col-lg-12"><div class="card"><div class="card-body admin-actions"><h3 class="section-title"><i class="ti ti-adjustments"></i><span>Vereinswerte</span></h3><form method="post"><input type="hidden" name="action" value="update_settings"><div class="mb-3"><label class="form-label">Pflichtstunden pro Mitglied</label><input class="form-control" name="hours_target" type="number" min="0" step="1" value="<?= (int)$config['hours_target'] ?>"></div><div class="mb-3"><label class="form-label">Wert pro Stunde</label><input class="form-control" name="hourly_rate_eur" type="number" min="0" step="0.5" value="<?= htmlspecialchars((string)$config['hourly_rate_eur']) ?>"></div><button class="btn btn-success" type="submit">Werte speichern</button></form></div></div></div>
<div class="col-lg-12"><div class="card"><div class="card-body admin-actions"><h3 class="section-title"><i class="ti ti-info-circle"></i><span>Informationskasten im Dashboard</span></h3><form method="post"><input type="hidden" name="action" value="update_settings"><input type="hidden" name="hours_target" value="<?= (int)$config['hours_target'] ?>"><input type="hidden" name="hourly_rate_eur" value="<?= htmlspecialchars((string)$config['hourly_rate_eur']) ?>"><div class="mb-3"><textarea class="form-control" name="dashboard_info" rows="8" placeholder="Markdown wird unterstützt"><?= htmlspecialchars((string)$config['dashboard_info']) ?></textarea><div class="form-hint">Unterstützt Absätze, <code>- Listen</code>, <code>*kursiv*</code> und <code>**fett**</code>.</div></div><button class="btn btn-success" type="submit">Informationskasten speichern</button></form></div></div></div>
<div class="col-lg-12"><div class="card"><div class="card-body admin-actions"><h3 class="section-title"><i class="ti ti-database-export"></i><span>Exporte</span></h3><p class="text-secondary">Exportfunktionen für Benutzerdaten und Stundenbuchungen.</p><form method="post" class="mb-3"><input type="hidden" name="action" value="export_csv"><button class="btn btn-primary" type="submit">Benutzerdaten als CSV exportieren</button></form><form method="post"><input type="hidden" name="action" value="export_bookings_csv"><button class="btn btn-primary" type="submit">Stundenbuchungen als CSV exportieren</button></form></div></div></div>
</div>
</div>
</div>
<?php endif; ?>
<?php if ($page === 'administration' && ($current['role'] ?? '') === 'admin'): ?>
<div class="card mt-2">
<div class="card-body">
<h3 class="section-title"><i class="ti ti-tool"></i><span>Administration</span></h3>
<div class="row row-cards">
<div class="col-lg-12"><div class="card"><div class="card-body admin-actions"><h3 class="section-title"><i class="ti ti-database"></i><span>SQL-Backup</span></h3><p class="text-secondary">Vollständige Datensicherung und Wiederherstellung der App-Datenbank.</p><form method="post" class="mb-3"><input type="hidden" name="action" value="export_sql_dump"><button class="btn btn-primary" type="submit">SQL-Dump herunterladen</button></form><form method="post" enctype="multipart/form-data" onsubmit="return confirm('Die Wiederherstellung überschreibt die bestehenden App-Daten. Fortfahren?');"><input type="hidden" name="action" value="import_sql_dump"><div class="mb-3"><input class="form-control" name="sql_file" type="file" accept=".sql,application/sql,text/plain" required></div><button class="btn btn-outline-danger" type="submit">SQL-Dump wiederherstellen</button></form></div></div></div>
<div class="col-lg-12"><div class="card"><div class="card-body admin-actions"><h3 class="section-title"><i class="ti ti-file-import"></i><span>CSV-Import</span></h3><p class="text-secondary">Importiert Benutzerdaten aus einer CSV-Datei mit den Spalten <strong>firstname</strong>, <strong>lastname</strong>, <strong>email</strong>, <strong>role</strong> und <strong>hours_worked</strong>.</p><div class="text-secondary small mb-3"><strong>Beispiel-Kopfzeile:</strong> <code>firstname,lastname,email,role,hours_worked</code></div><form method="post" enctype="multipart/form-data"><input type="hidden" name="action" value="import_csv"><div class="mb-3"><input class="form-control" name="csv_file" type="file" accept=".csv,text/csv" required></div><button class="btn btn-primary" type="submit">Benutzerdaten aus CSV importieren</button></form></div></div></div>
<div class="col-lg-12"><div class="card"><div class="card-body admin-actions"><h3 class="section-title"><i class="ti ti-rotate-clockwise-2"></i><span>Arbeitsstunden zurücksetzen</span></h3><p class="text-secondary">Setzt alle gebuchten Arbeitsstunden für alle Mitglieder auf 0. Diese Aktion ist für den Jahreswechsel gedacht.</p><form method="post" onsubmit="return confirm('Wirklich alle Arbeitsstunden auf 0 zurücksetzen?');"><input type="hidden" name="action" value="reset_all_hours"><button class="btn btn-outline-danger" type="submit">Alle Arbeitsstunden auf 0 setzen</button></form></div></div></div>
</div>
<div class="row row-cards mt-2"><div class="col-lg-12"><div class="card"><div class="card-body admin-actions"><h3 class="section-title"><i class="ti ti-history"></i><span>Logansicht</span></h3><form method="post" class="mb-3"><input type="hidden" name="action" value="export_logs_csv"><button class="btn btn-primary" type="submit">Log als CSV exportieren</button></form><ul class="list-group list-group-flush"><?php foreach (($logs['entries'] ?? []) as $entry): ?><li class="list-group-item px-0"><strong><?= htmlspecialchars($entry['ts']) ?></strong><br><?= htmlspecialchars($entry['actor']) ?>: <?= htmlspecialchars($entry['action']) ?></li><?php endforeach; ?><?php if (empty($logs['entries'])): ?><li class="list-group-item px-0 text-secondary">Keine Logeinträge vorhanden.</li><?php endif; ?></ul><?php if (($logs['total_pages'] ?? 1) > 1): ?><div class="d-flex align-items-center justify-content-between gap-3 mt-3"><a class="btn btn-outline-secondary<?= ($logs['page'] ?? 1) <= 1 ? ' disabled' : '' ?>" href="/?page=administration&log_page=<?= max(1, (int)($logs['page'] ?? 1) - 1) ?>">Zurück</a><div class="text-secondary small">Seite <?= (int)($logs['page'] ?? 1) ?> von <?= (int)($logs['total_pages'] ?? 1) ?>, insgesamt <?= (int)($logs['total'] ?? 0) ?> Einträge</div><a class="btn btn-outline-secondary<?= ($logs['page'] ?? 1) >= ($logs['total_pages'] ?? 1) ? ' disabled' : '' ?>" href="/?page=administration&log_page=<?= min((int)($logs['total_pages'] ?? 1), (int)($logs['page'] ?? 1) + 1) ?>">Weiter</a></div><?php endif; ?></div></div></div></div>
</div>
</div>
<?php endif; ?>
<footer class="app-footer d-flex flex-wrap justify-content-between gap-3">
<div>Author: <?= htmlspecialchars((string)$config['author']) ?> | <?= htmlspecialchars((string)$config['copyright']) ?></div>
<div>Host: <?= htmlspecialchars((string)($_SERVER['HTTP_HOST'] ?? 'localhost')) ?> | Datenbank: <?= $pdo ? 'verbunden' : 'nicht verbunden' ?> | Version: <?= htmlspecialchars((string)$config['app_version']) ?></div>
</footer>
</div>
</main>
</div>
</div>
</div>
<?php
}