0, 'path' => '/', 'secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off', 'httponly' => true, 'samesite' => 'Lax', ]); } session_start(); $localConfig = []; $localConfigFile = dirname(__DIR__, 2) . '/config.php'; if (is_file($localConfigFile)) { $loaded = require $localConfigFile; if (is_array($loaded)) { $localConfig = $loaded; } } $config = [ 'app_name' => 'TC Ingelfingen Arbeitszeiterfassung', 'app_version' => (string)($localConfig['app_version'] ?? '1.0.0'), 'author' => (string)($localConfig['author'] ?? 'OpenCode'), 'copyright' => (string)($localConfig['copyright'] ?? 'Copyright 2026 TC-Ingelfingen'), 'hours_target' => (int)($localConfig['hours_target'] ?? (getenv('HOURS_TARGET') ?: 12)), 'hourly_rate_eur' => (float)($localConfig['hourly_rate_eur'] ?? (getenv('HOURLY_RATE_EUR') ?: 10)), 'dashboard_info' => (string)($localConfig['dashboard_info'] ?? ''), ]; $smtpConfig = [ 'host' => (string)($localConfig['smtp']['host'] ?? ''), 'port' => (int)($localConfig['smtp']['port'] ?? 587), 'username' => (string)($localConfig['smtp']['username'] ?? ''), 'password' => (string)($localConfig['smtp']['password'] ?? ''), 'encryption' => (string)($localConfig['smtp']['encryption'] ?? 'tls'), 'from_email' => (string)($localConfig['smtp']['from_email'] ?? ''), 'from_name' => (string)($localConfig['smtp']['from_name'] ?? $config['app_name']), ]; $dbConfig = [ 'host' => (string)($localConfig['db']['host'] ?? (getenv('DB_HOST') ?: '')), 'name' => (string)($localConfig['db']['name'] ?? (getenv('DB_NAME') ?: '')), 'user' => (string)($localConfig['db']['user'] ?? (getenv('DB_USER') ?: '')), 'pass' => (string)($localConfig['db']['pass'] ?? (getenv('DB_PASS') ?: '')), ]; $pdo = null; if ($dbConfig['host'] !== '' && $dbConfig['name'] !== '' && $dbConfig['user'] !== '') { try { $dsn = sprintf('mysql:host=%s;dbname=%s;charset=utf8mb4', $dbConfig['host'], $dbConfig['name']); $pdo = new PDO($dsn, $dbConfig['user'], $dbConfig['pass'], [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); } catch (Throwable $e) { $pdo = null; } } function money(float $value): string { return number_format($value, 2, ',', '.') . ' EUR'; } function formatDateTime(string $value): string { $timestamp = strtotime($value); if ($timestamp === false) { return $value; } return date('d.m.Y H:i', $timestamp); } function renderSimpleMarkdown(string $text): string { $escaped = htmlspecialchars(trim($text), ENT_QUOTES, 'UTF-8'); if ($escaped === '') { return ''; } $escaped = preg_replace('/\*\*(.+?)\*\*/s', '$1', $escaped); $escaped = preg_replace('/\*(.+?)\*/s', '$1', $escaped); $blocks = preg_split('/\R{2,}/', $escaped) ?: []; $html = []; foreach ($blocks as $block) { $block = trim($block); if ($block === '') { continue; } if (preg_match('/^(?:- .*(?:\R|$))+$/', $block)) { $items = preg_split('/\R/', $block) ?: []; $listItems = []; foreach ($items as $item) { $item = trim($item); if (str_starts_with($item, '- ')) { $listItems[] = '
' . nl2br($block) . '
'; } return implode('', $html); } function appUrl(): string { $scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http'; $host = $_SERVER['HTTP_HOST'] ?? 'localhost'; return $scheme . '://' . $host; } function sendPasswordResetMail(array $smtpConfig, string $toEmail, string $resetUrl): bool { if (($smtpConfig['from_email'] ?? '') === '') { return false; } $subject = 'Passwort zuruecksetzen'; $message = "Hallo,\n\n" . "bitte nutzen Sie den folgenden Link, um Ihr Passwort zurueckzusetzen:\n\n" . $resetUrl . "\n\n" . "Der Link ist 60 Minuten gueltig.\n"; $headers = [ 'From: ' . ($smtpConfig['from_name'] !== '' ? $smtpConfig['from_name'] . ' <' . $smtpConfig['from_email'] . '>' : $smtpConfig['from_email']), 'Content-Type: text/plain; charset=UTF-8', ]; return mail($toEmail, $subject, $message, implode("\r\n", $headers)); } function pageTitle(string $page): string { return match ($page) { 'login' => 'Anmeldung', 'reset_password' => 'Passwort zuruecksetzen', 'profile' => 'Profil', 'members' => 'Mitglieder', 'booking' => 'Buchung', 'admin' => 'Vereinskonfiguration', 'administration' => 'Administration', default => 'Dashboard', }; } function currentUser(): ?array { return $_SESSION['user'] ?? null; } function hasRole(array $user, array $roles): bool { return in_array($user['role'], $roles, true); } function canCreateMembers(array $user): bool { return hasRole($user, ['editor', 'admin']); } function canManageUsers(array $user): bool { return $user['role'] === 'admin'; } function canManageMembers(array $user): bool { return hasRole($user, ['editor', 'admin']); } function displayName(array $user): string { return trim(((string)($user['firstname'] ?? '')) . ' ' . ((string)($user['lastname'] ?? ''))); } function roleLabel(string $role): string { return match ($role) { 'member' => 'Mitglied', 'editor' => 'Bearbeiter', 'admin' => 'Admin', default => $role, }; } function isInstalled(?PDO $pdo): bool { if (!$pdo) { return false; } try { return (int)$pdo->query('SELECT COUNT(*) FROM users')->fetchColumn() > 0; } catch (Throwable $e) { return false; } } function initSchema(PDO $pdo): void { $pdo->exec('CREATE TABLE IF NOT EXISTS users ( id INT AUTO_INCREMENT PRIMARY KEY, firstname VARCHAR(100) NOT NULL, lastname VARCHAR(100) NOT NULL, email VARCHAR(190) NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL, role ENUM("member", "editor", "admin") NOT NULL DEFAULT "member", created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4'); $columns = $pdo->query('SHOW COLUMNS FROM users')->fetchAll(PDO::FETCH_COLUMN); if (!in_array('firstname', $columns, true)) { $pdo->exec('ALTER TABLE users ADD COLUMN firstname VARCHAR(100) NOT NULL DEFAULT "" AFTER id'); } if (!in_array('lastname', $columns, true)) { $pdo->exec('ALTER TABLE users ADD COLUMN lastname VARCHAR(100) NOT NULL DEFAULT "" AFTER firstname'); } if (in_array('name', $columns, true)) { $stmt = $pdo->query('SELECT id, name, firstname, lastname FROM users'); foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) { if (trim((string)$row['firstname']) !== '' || trim((string)$row['lastname']) !== '') { continue; } $parts = preg_split('/\s+/', trim((string)$row['name'])) ?: []; $lastname = array_pop($parts) ?: ''; $firstname = trim(implode(' ', $parts)); $update = $pdo->prepare('UPDATE users SET firstname = ?, lastname = ? WHERE id = ?'); $update->execute([$firstname, $lastname, (int)$row['id']]); } } $pdo->exec('CREATE TABLE IF NOT EXISTS work_logs ( id INT AUTO_INCREMENT PRIMARY KEY, member_id INT NOT NULL, actor_id INT NOT NULL, hours DECIMAL(5,2) NOT NULL, note VARCHAR(255) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX (member_id), INDEX (actor_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4'); $pdo->exec('CREATE TABLE IF NOT EXISTS settings ( setting_key VARCHAR(100) PRIMARY KEY, setting_value TEXT NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4'); $pdo->exec('CREATE TABLE IF NOT EXISTS password_resets ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, token_hash VARCHAR(255) NOT NULL, expires_at DATETIME NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX (user_id), INDEX (expires_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4'); $pdo->exec('CREATE TABLE IF NOT EXISTS audit_logs ( id INT AUTO_INCREMENT PRIMARY KEY, actor_id INT NOT NULL, action VARCHAR(255) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX (actor_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4'); $settingColumns = $pdo->query('SHOW COLUMNS FROM settings')->fetchAll(PDO::FETCH_ASSOC); foreach ($settingColumns as $settingColumn) { if (($settingColumn['Field'] ?? '') === 'setting_value' && stripos((string)($settingColumn['Type'] ?? ''), 'text') === false) { $pdo->exec('ALTER TABLE settings MODIFY setting_value TEXT NOT NULL'); break; } } } if ($pdo) { initSchema($pdo); $hoursTarget = $pdo->query('SELECT setting_value FROM settings WHERE setting_key = "hours_target" LIMIT 1')->fetchColumn(); $hourlyRate = $pdo->query('SELECT setting_value FROM settings WHERE setting_key = "hourly_rate_eur" LIMIT 1')->fetchColumn(); $dashboardInfo = $pdo->query('SELECT setting_value FROM settings WHERE setting_key = "dashboard_info" LIMIT 1')->fetchColumn(); if ($hoursTarget !== false) { $config['hours_target'] = (int)$hoursTarget; } if ($hourlyRate !== false) { $config['hourly_rate_eur'] = (float)$hourlyRate; } if ($dashboardInfo !== false) { $config['dashboard_info'] = (string)$dashboardInfo; } } function fetchUsers(?PDO $pdo): array { if (!$pdo) { return [ ['id' => 1, 'firstname' => 'Max', 'lastname' => 'Mustermann', 'name' => 'Max Mustermann', 'email' => 'max@example.com', 'role' => 'admin', 'hours_worked' => 8.5], ['id' => 2, 'firstname' => 'Lisa', 'lastname' => 'Beispiel', 'name' => 'Lisa Beispiel', 'email' => 'lisa@example.com', 'role' => 'editor', 'hours_worked' => 12.0], ['id' => 3, 'firstname' => 'Tom', 'lastname' => 'Mitglied', 'name' => 'Tom Mitglied', 'email' => 'tom@example.com', 'role' => 'member', 'hours_worked' => 6.0], ]; } return $pdo->query('SELECT u.id, u.firstname, u.lastname, CONCAT(u.firstname, " ", u.lastname) AS name, u.email, u.role, COALESCE(SUM(w.hours), 0) AS hours_worked FROM users u LEFT JOIN work_logs w ON w.member_id = u.id GROUP BY u.id, u.firstname, u.lastname, u.email, u.role ORDER BY u.lastname, u.firstname')->fetchAll(PDO::FETCH_ASSOC); } function fetchUsersPage(?PDO $pdo, int $page = 1, int $perPage = 50): array { $page = max(1, $page); $perPage = max(1, $perPage); $offset = ($page - 1) * $perPage; if (!$pdo) { $rows = fetchUsers($pdo); return [ 'entries' => array_slice($rows, $offset, $perPage), 'total' => count($rows), 'page' => $page, 'per_page' => $perPage, 'total_pages' => max(1, (int)ceil(count($rows) / $perPage)), ]; } $total = (int)$pdo->query('SELECT COUNT(*) FROM users')->fetchColumn(); $stmt = $pdo->prepare('SELECT u.id, u.firstname, u.lastname, CONCAT(u.firstname, " ", u.lastname) AS name, u.email, u.role, COALESCE(SUM(w.hours), 0) AS hours_worked FROM users u LEFT JOIN work_logs w ON w.member_id = u.id GROUP BY u.id, u.firstname, u.lastname, u.email, u.role ORDER BY u.lastname, u.firstname LIMIT ? OFFSET ?'); $stmt->bindValue(1, $perPage, PDO::PARAM_INT); $stmt->bindValue(2, $offset, PDO::PARAM_INT); $stmt->execute(); return [ 'entries' => $stmt->fetchAll(PDO::FETCH_ASSOC), 'total' => $total, 'page' => $page, 'per_page' => $perPage, 'total_pages' => max(1, (int)ceil($total / $perPage)), ]; } function fetchLogs(?PDO $pdo, int $page = 1, int $perPage = 50): array { $page = max(1, $page); $perPage = max(1, $perPage); $offset = ($page - 1) * $perPage; if (!$pdo) { $rows = [ ['ts' => '2026-05-19 14:00', 'actor' => 'Admin', 'action' => 'Lisa Beispiel als Bearbeiter angelegt'], ['ts' => '2026-05-18 09:30', 'actor' => 'Lisa Beispiel', 'action' => '2.0 Stunden für Tom Mitglied gebucht'], ['ts' => '2026-05-17 18:15', 'actor' => 'Admin', 'action' => 'Stundensatz auf 10 EUR gesetzt'], ]; return [ 'entries' => array_slice($rows, $offset, $perPage), 'total' => count($rows), 'page' => $page, 'per_page' => $perPage, 'total_pages' => max(1, (int)ceil(count($rows) / $perPage)), ]; } $countSql = 'SELECT COUNT(*) FROM ((SELECT w.id FROM work_logs w) UNION ALL (SELECT l.id FROM audit_logs l)) combined_logs'; $total = (int)$pdo->query($countSql)->fetchColumn(); $stmt = $pdo->prepare('(SELECT w.created_at AS ts, CONCAT(a.firstname, " ", a.lastname) AS actor, CONCAT(w.hours, " Stunden für ", m.firstname, " ", m.lastname, " gebucht: ", w.note) AS action FROM work_logs w JOIN users a ON a.id = w.actor_id JOIN users m ON m.id = w.member_id) UNION ALL (SELECT l.created_at AS ts, CONCAT(a.firstname, " ", a.lastname) AS actor, l.action FROM audit_logs l JOIN users a ON a.id = l.actor_id) ORDER BY ts DESC LIMIT ? OFFSET ?'); $stmt->bindValue(1, $perPage, PDO::PARAM_INT); $stmt->bindValue(2, $offset, PDO::PARAM_INT); $stmt->execute(); return [ 'entries' => $stmt->fetchAll(PDO::FETCH_ASSOC), 'total' => $total, 'page' => $page, 'per_page' => $perPage, 'total_pages' => max(1, (int)ceil($total / $perPage)), ]; } function addAuditLog(?PDO $pdo, int $actorId, string $action): void { if (!$pdo || $actorId <= 0 || trim($action) === '') { return; } $stmt = $pdo->prepare('INSERT INTO audit_logs (actor_id, action) VALUES (?, ?)'); $stmt->execute([$actorId, $action]); } function sqlValue(PDO $pdo, mixed $value): string { if ($value === null) { return 'NULL'; } return $pdo->quote((string)$value); } function fetchBookings(?PDO $pdo, array $user): array { if (!$pdo) { $rows = [ ['id' => 1, 'ts' => '2026-05-19 14:00', 'member_id' => 3, 'member' => 'Tom Mitglied', 'actor' => 'Lisa Beispiel', 'hours' => 2.0, 'note' => 'Fruehjahrsputz'], ['id' => 2, 'ts' => '2026-05-18 09:30', 'member_id' => 2, 'member' => 'Lisa Beispiel', 'actor' => 'Max Mustermann', 'hours' => 1.5, 'note' => 'Platzpflege'], ['id' => 3, 'ts' => '2026-05-17 18:15', 'member_id' => 3, 'member' => 'Tom Mitglied', 'actor' => 'Max Mustermann', 'hours' => 2.5, 'note' => 'Turnierhilfe'], ]; if (($user['role'] ?? '') === 'member') { return array_values(array_filter($rows, static fn (array $row): bool => (int)$row['member_id'] === (int)($user['id'] ?? 0))); } return $rows; } if (($user['role'] ?? '') === 'member') { $stmt = $pdo->prepare('SELECT w.id, w.created_at AS ts, w.member_id, CONCAT(m.firstname, " ", m.lastname) AS member, CONCAT(a.firstname, " ", a.lastname) AS actor, w.hours, w.note FROM work_logs w JOIN users a ON a.id = w.actor_id JOIN users m ON m.id = w.member_id WHERE w.member_id = ? ORDER BY w.created_at DESC'); $stmt->execute([(int)($user['id'] ?? 0)]); return $stmt->fetchAll(PDO::FETCH_ASSOC); } return $pdo->query('SELECT w.id, w.created_at AS ts, w.member_id, CONCAT(m.firstname, " ", m.lastname) AS member, CONCAT(a.firstname, " ", a.lastname) AS actor, w.hours, w.note FROM work_logs w JOIN users a ON a.id = w.actor_id JOIN users m ON m.id = w.member_id ORDER BY w.created_at DESC')->fetchAll(PDO::FETCH_ASSOC); } function fetchBookingsPage(?PDO $pdo, array $user, int $page = 1, int $perPage = 50): array { $page = max(1, $page); $perPage = max(1, $perPage); $offset = ($page - 1) * $perPage; if (!$pdo) { $rows = fetchBookings($pdo, $user); return [ 'entries' => array_slice($rows, $offset, $perPage), 'total' => count($rows), 'page' => $page, 'per_page' => $perPage, 'total_pages' => max(1, (int)ceil(count($rows) / $perPage)), ]; } if (($user['role'] ?? '') === 'member') { $countStmt = $pdo->prepare('SELECT COUNT(*) FROM work_logs WHERE member_id = ?'); $countStmt->execute([(int)($user['id'] ?? 0)]); $total = (int)$countStmt->fetchColumn(); $stmt = $pdo->prepare('SELECT w.id, w.created_at AS ts, w.member_id, CONCAT(m.firstname, " ", m.lastname) AS member, CONCAT(a.firstname, " ", a.lastname) AS actor, w.hours, w.note FROM work_logs w JOIN users a ON a.id = w.actor_id JOIN users m ON m.id = w.member_id WHERE w.member_id = ? ORDER BY w.created_at DESC LIMIT ? OFFSET ?'); $stmt->bindValue(1, (int)($user['id'] ?? 0), PDO::PARAM_INT); $stmt->bindValue(2, $perPage, PDO::PARAM_INT); $stmt->bindValue(3, $offset, PDO::PARAM_INT); $stmt->execute(); } else { $total = (int)$pdo->query('SELECT COUNT(*) FROM work_logs')->fetchColumn(); $stmt = $pdo->prepare('SELECT w.id, w.created_at AS ts, w.member_id, CONCAT(m.firstname, " ", m.lastname) AS member, CONCAT(a.firstname, " ", a.lastname) AS actor, w.hours, w.note FROM work_logs w JOIN users a ON a.id = w.actor_id JOIN users m ON m.id = w.member_id ORDER BY w.created_at DESC LIMIT ? OFFSET ?'); $stmt->bindValue(1, $perPage, PDO::PARAM_INT); $stmt->bindValue(2, $offset, PDO::PARAM_INT); $stmt->execute(); } return [ 'entries' => $stmt->fetchAll(PDO::FETCH_ASSOC), 'total' => $total, 'page' => $page, 'per_page' => $perPage, 'total_pages' => max(1, (int)ceil($total / $perPage)), ]; } function findUserByEmail(?PDO $pdo, string $email): ?array { if (!$pdo) { $demoUsers = [ ['id' => 1, 'firstname' => 'Max', 'lastname' => 'Mustermann', 'name' => 'Max Mustermann', 'email' => 'max@example.com', 'password_hash' => password_hash('admin123', PASSWORD_DEFAULT), 'role' => 'admin'], ['id' => 2, 'firstname' => 'Lisa', 'lastname' => 'Beispiel', 'name' => 'Lisa Beispiel', 'email' => 'lisa@example.com', 'password_hash' => password_hash('editor123', PASSWORD_DEFAULT), 'role' => 'editor'], ['id' => 3, 'firstname' => 'Tom', 'lastname' => 'Mitglied', 'name' => 'Tom Mitglied', 'email' => 'tom@example.com', 'password_hash' => password_hash('member123', PASSWORD_DEFAULT), 'role' => 'member'], ]; foreach ($demoUsers as $demoUser) { if (strcasecmp($demoUser['email'], $email) === 0) { return $demoUser; } } return null; } $stmt = $pdo->prepare('SELECT * FROM users WHERE email = ? LIMIT 1'); $stmt->execute([$email]); $user = $stmt->fetch(PDO::FETCH_ASSOC); return $user ?: null; } $error = null; $notice = null; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $action = $_POST['action'] ?? ''; $user = currentUser(); if ($action === 'login') { $found = findUserByEmail($pdo, trim((string)($_POST['email'] ?? ''))); if ($found && password_verify((string)($_POST['password'] ?? ''), $found['password_hash'])) { addAuditLog($pdo, (int)$found['id'], 'Anmeldung erfolgreich.'); session_regenerate_id(true); $_SESSION['user'] = ['id' => (int)$found['id'], 'firstname' => $found['firstname'] ?? '', 'lastname' => $found['lastname'] ?? '', 'name' => displayName($found), 'email' => $found['email'], 'role' => $found['role']]; header('Location: /?page=dashboard'); exit; } $error = 'Login fehlgeschlagen.'; } if ($action === 'request_password_reset') { $email = trim((string)($_POST['email'] ?? '')); if (!$pdo) { $error = 'Passwort-Reset ist ohne Datenbank nicht verfuegbar.'; } elseif (($smtpConfig['from_email'] ?? '') === '') { $error = 'SMTP/Absender ist noch nicht konfiguriert.'; } else { $found = findUserByEmail($pdo, $email); if ($found) { $token = bin2hex(random_bytes(32)); $tokenHash = password_hash($token, PASSWORD_DEFAULT); $expiresAt = date('Y-m-d H:i:s', time() + 3600); $stmt = $pdo->prepare('DELETE FROM password_resets WHERE user_id = ?'); $stmt->execute([(int)$found['id']]); $stmt = $pdo->prepare('INSERT INTO password_resets (user_id, token_hash, expires_at) VALUES (?, ?, ?)'); $stmt->execute([(int)$found['id'], $tokenHash, $expiresAt]); $resetUrl = appUrl() . '/?page=reset_password&token=' . urlencode($token); sendPasswordResetMail($smtpConfig, (string)$found['email'], $resetUrl); } $notice = 'Wenn die E-Mail-Adresse vorhanden ist, wurde ein Link zum Zuruecksetzen versendet.'; } } if ($action === 'reset_password') { $token = trim((string)($_POST['token'] ?? '')); $newPassword = (string)($_POST['new_password'] ?? ''); $confirmPassword = (string)($_POST['confirm_password'] ?? ''); if (!$pdo) { $error = 'Passwort-Reset ist ohne Datenbank nicht verfuegbar.'; } elseif ($newPassword === '' || strlen($newPassword) < 6) { $error = 'Das neue Passwort muss mindestens 6 Zeichen lang sein.'; } elseif ($newPassword !== $confirmPassword) { $error = 'Die neuen Passwoerter stimmen nicht ueberein.'; } else { $stmt = $pdo->query('SELECT id, user_id, token_hash, expires_at FROM password_resets ORDER BY id DESC'); $resetRow = null; foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $candidate) { if (strtotime((string)$candidate['expires_at']) < time()) { continue; } if (password_verify($token, (string)$candidate['token_hash'])) { $resetRow = $candidate; break; } } if (!$resetRow) { $error = 'Der Reset-Link ist ungueltig oder abgelaufen.'; } else { $stmt = $pdo->prepare('UPDATE users SET password_hash = ? WHERE id = ?'); $stmt->execute([password_hash($newPassword, PASSWORD_DEFAULT), (int)$resetRow['user_id']]); $stmt = $pdo->prepare('DELETE FROM password_resets WHERE user_id = ?'); $stmt->execute([(int)$resetRow['user_id']]); addAuditLog($pdo, (int)$resetRow['user_id'], 'Passwort über Reset-Link neu gesetzt.'); $_SESSION['login_notice'] = 'Passwort erfolgreich zurueckgesetzt. Bitte anmelden.'; header('Location: /?page=login'); exit; } } } if ($user && $action === 'create_member' && canCreateMembers($user) && $pdo) { $firstname = trim((string)$_POST['firstname']); $lastname = trim((string)$_POST['lastname']); $email = trim((string)$_POST['email']); $role = trim((string)($_POST['role'] ?? 'member')); if (!in_array($role, ['member', 'editor', 'admin'], true)) { $role = 'member'; } if (!canManageUsers($user)) { $role = 'member'; } $stmt = $pdo->prepare('INSERT INTO users (firstname, lastname, email, password_hash, role) VALUES (?, ?, ?, ?, ?)'); $stmt->execute([$firstname, $lastname, $email, password_hash('mitglied123', PASSWORD_DEFAULT), $role]); addAuditLog($pdo, (int)$user['id'], trim($firstname . ' ' . $lastname) . ' (' . $email . ') als ' . roleLabel($role) . ' angelegt.'); $notice = $role === 'admin' ? 'Benutzer als Admin angelegt.' : ($role === 'editor' ? 'Benutzer als Bearbeiter angelegt.' : 'Mitglied angelegt.'); } if ($user && $action === 'update_user_role' && canManageUsers($user) && $pdo) { $userId = (int)($_POST['user_id'] ?? 0); $role = trim((string)($_POST['role'] ?? 'member')); if (in_array($role, ['member', 'editor', 'admin'], true)) { $stmt = $pdo->prepare('SELECT firstname, lastname FROM users WHERE id = ? LIMIT 1'); $stmt->execute([$userId]); $targetUser = $stmt->fetch(PDO::FETCH_ASSOC) ?: null; $stmt = $pdo->prepare('UPDATE users SET role = ? WHERE id = ?'); $stmt->execute([$role, $userId]); if ($targetUser) { addAuditLog($pdo, (int)$user['id'], trim(displayName($targetUser)) . ' Rolle auf ' . roleLabel($role) . ' gesetzt.'); } $notice = 'Rolle gespeichert.'; } } if ($user && $action === 'reset_user_password' && canManageMembers($user) && $pdo) { $userId = (int)($_POST['user_id'] ?? 0); $stmt = $pdo->prepare('SELECT firstname, lastname FROM users WHERE id = ? LIMIT 1'); $stmt->execute([$userId]); $targetUser = $stmt->fetch(PDO::FETCH_ASSOC) ?: null; $stmt = $pdo->prepare('UPDATE users SET password_hash = ? WHERE id = ? AND role <> "admin"'); $stmt->execute([password_hash('mitglied123', PASSWORD_DEFAULT), $userId]); $targetName = $targetUser ? displayName($targetUser) : 'Der Benutzer'; addAuditLog($pdo, (int)$user['id'], trim($targetName) . ' Passwort zurückgesetzt.'); $notice = trim($targetName) . ' wurde auf das Passwort mitglied123 zurueckgesetzt.'; } if ($user && $action === 'delete_user' && canManageMembers($user) && $pdo) { $userId = (int)($_POST['user_id'] ?? 0); $stmt = $pdo->prepare('SELECT firstname, lastname, email FROM users WHERE id = ? LIMIT 1'); $stmt->execute([$userId]); $targetUser = $stmt->fetch(PDO::FETCH_ASSOC) ?: null; $stmt = $pdo->prepare('DELETE FROM work_logs WHERE member_id = ?'); $stmt->execute([$userId]); $stmt = $pdo->prepare('DELETE FROM users WHERE id = ? AND role <> "admin"'); $stmt->execute([$userId]); if ($targetUser) { addAuditLog($pdo, (int)$user['id'], trim(displayName($targetUser)) . ' (' . (string)$targetUser['email'] . ') gelöscht.'); } $notice = 'Benutzer geloescht.'; } if ($user && $action === 'book_hours' && hasRole($user, ['editor', 'admin']) && $pdo) { $stmt = $pdo->prepare('INSERT INTO work_logs (member_id, actor_id, hours, note) VALUES (?, ?, ?, ?)'); $stmt->execute([(int)$_POST['member_id'], (int)$user['id'], (float)$_POST['hours'], trim((string)$_POST['note'])]); $notice = 'Stunden gebucht.'; } if ($user && $action === 'delete_booking' && hasRole($user, ['editor', 'admin']) && $pdo) { $bookingId = (int)($_POST['booking_id'] ?? 0); $stmt = $pdo->prepare('DELETE FROM work_logs WHERE id = ?'); $stmt->execute([$bookingId]); $notice = 'Stundenbuchung geloescht.'; } if ($user && $action === 'update_profile') { $firstname = trim((string)($_POST['firstname'] ?? '')); $lastname = trim((string)($_POST['lastname'] ?? '')); if ($firstname === '' || $lastname === '') { $error = 'Vorname und Nachname sind erforderlich.'; } elseif ($pdo) { $stmt = $pdo->prepare('UPDATE users SET firstname = ?, lastname = ? WHERE id = ?'); $stmt->execute([$firstname, $lastname, (int)$user['id']]); $_SESSION['user']['firstname'] = $firstname; $_SESSION['user']['lastname'] = $lastname; $_SESSION['user']['name'] = trim($firstname . ' ' . $lastname); $notice = 'Profil gespeichert.'; } else { $_SESSION['user']['firstname'] = $firstname; $_SESSION['user']['lastname'] = $lastname; $_SESSION['user']['name'] = trim($firstname . ' ' . $lastname); $notice = 'Profil im Demo-Modus nur fuer diese Sitzung aktualisiert.'; } } if ($user && $action === 'change_password') { $currentPassword = (string)($_POST['current_password'] ?? ''); $newPassword = (string)($_POST['new_password'] ?? ''); $confirmPassword = (string)($_POST['confirm_password'] ?? ''); if ($newPassword === '' || strlen($newPassword) < 6) { $error = 'Das neue Passwort muss mindestens 6 Zeichen lang sein.'; } elseif ($newPassword !== $confirmPassword) { $error = 'Die neuen Passwoerter stimmen nicht ueberein.'; } elseif ($pdo) { $stmt = $pdo->prepare('SELECT password_hash FROM users WHERE id = ? LIMIT 1'); $stmt->execute([(int)$user['id']]); $passwordHash = $stmt->fetchColumn(); if (!$passwordHash || !password_verify($currentPassword, (string)$passwordHash)) { $error = 'Das aktuelle Passwort ist nicht korrekt.'; } else { $stmt = $pdo->prepare('UPDATE users SET password_hash = ? WHERE id = ?'); $stmt->execute([password_hash($newPassword, PASSWORD_DEFAULT), (int)$user['id']]); addAuditLog($pdo, (int)$user['id'], 'Eigenes Passwort geändert.'); $notice = 'Passwort geaendert.'; } } else { $demoUser = findUserByEmail($pdo, (string)$user['email']); if (!$demoUser || !password_verify($currentPassword, (string)$demoUser['password_hash'])) { $error = 'Das aktuelle Passwort ist nicht korrekt.'; } else { $notice = 'Im Demo-Modus kann das Passwort nicht dauerhaft gespeichert werden.'; } } } if ($user && $action === 'update_settings' && hasRole($user, ['editor', 'admin'])) { $previousHoursTarget = (int)$config['hours_target']; $previousHourlyRate = (float)$config['hourly_rate_eur']; $previousDashboardInfo = trim((string)$config['dashboard_info']); $hoursTarget = max(0, (int)($_POST['hours_target'] ?? $config['hours_target'])); $hourlyRate = max(0, (float)($_POST['hourly_rate_eur'] ?? $config['hourly_rate_eur'])); $dashboardInfo = trim((string)($_POST['dashboard_info'] ?? $config['dashboard_info'])); if ($pdo) { $stmt = $pdo->prepare('INSERT INTO settings (setting_key, setting_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)'); $stmt->execute(['hours_target', (string)$hoursTarget]); $stmt->execute(['hourly_rate_eur', (string)$hourlyRate]); $stmt->execute(['dashboard_info', $dashboardInfo]); $config['hours_target'] = $hoursTarget; $config['hourly_rate_eur'] = $hourlyRate; $config['dashboard_info'] = $dashboardInfo; if ($previousHoursTarget !== $hoursTarget || abs($previousHourlyRate - $hourlyRate) > 0.0001) { addAuditLog($pdo, (int)$user['id'], 'Vereinswerte gespeichert. Pflichtstunden: ' . $hoursTarget . ', Stundenwert: ' . number_format($hourlyRate, 2, ',', '.') . ' EUR.'); } if ($previousDashboardInfo !== $dashboardInfo) { addAuditLog($pdo, (int)$user['id'], 'Informationskasten im Dashboard aktualisiert.'); } $notice = 'Vereinswerte gespeichert.'; } else { $error = 'Vereinswerte koennen ohne Datenbank nicht gespeichert werden.'; } } if ($user && $action === 'reset_all_hours' && $user['role'] === 'admin') { if ($pdo) { $pdo->exec('DELETE FROM work_logs'); addAuditLog($pdo, (int)$user['id'], 'Alle Arbeitsstunden auf 0 zurückgesetzt.'); $notice = 'Alle Arbeitsstunden wurden auf 0 zurueckgesetzt.'; } else { $error = 'Arbeitsstunden koennen ohne Datenbank nicht zurueckgesetzt werden.'; } } if ($user && $action === 'export_csv' && hasRole($user, ['editor', 'admin'])) { if ($pdo) { addAuditLog($pdo, (int)$user['id'], 'Benutzerdaten als CSV exportiert.'); $filename = 'arbeitsstunden-export-' . date('Y-m-d-His') . '.csv'; header('Content-Type: text/csv; charset=utf-8'); header('Content-Disposition: attachment; filename="' . $filename . '"'); $output = fopen('php://output', 'wb'); if ($output !== false) { fwrite($output, "\xEF\xBB\xBF"); $stmt = $pdo->query('SELECT u.id, u.firstname, u.lastname, u.email, u.role, COALESCE(SUM(w.hours), 0) AS hours_worked, u.created_at FROM users u LEFT JOIN work_logs w ON w.member_id = u.id GROUP BY u.id, u.firstname, u.lastname, u.email, u.role, u.created_at ORDER BY u.lastname, u.firstname'); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); if ($rows) { fputcsv($output, array_keys($rows[0])); foreach ($rows as $row) { fputcsv($output, $row); } } fclose($output); } exit; } $error = 'CSV-Export ist ohne Datenbank nicht verfuegbar.'; } if ($user && $action === 'export_bookings_csv' && hasRole($user, ['editor', 'admin'])) { if ($pdo) { addAuditLog($pdo, (int)$user['id'], 'Stundenbuchungen als CSV exportiert.'); $filename = 'stundenbuchungen-export-' . date('Y-m-d-His') . '.csv'; header('Content-Type: text/csv; charset=utf-8'); header('Content-Disposition: attachment; filename="' . $filename . '"'); $output = fopen('php://output', 'wb'); if ($output !== false) { fwrite($output, "\xEF\xBB\xBF"); $stmt = $pdo->query('SELECT w.id, CONCAT(m.lastname, ", ", m.firstname) AS member, CONCAT(a.lastname, ", ", a.firstname) AS actor, w.hours, w.note, w.created_at FROM work_logs w JOIN users a ON a.id = w.actor_id JOIN users m ON m.id = w.member_id ORDER BY w.created_at DESC'); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); if ($rows) { fputcsv($output, array_keys($rows[0])); foreach ($rows as $row) { fputcsv($output, $row); } } fclose($output); } exit; } $error = 'CSV-Export ist ohne Datenbank nicht verfuegbar.'; } if ($user && $action === 'export_sql_dump' && $user['role'] === 'admin') { if ($pdo) { addAuditLog($pdo, (int)$user['id'], 'SQL-Dump exportiert.'); $filename = 'arbeitsstunden-backup-' . date('Y-m-d-His') . '.sql'; header('Content-Type: application/sql; charset=utf-8'); header('Content-Disposition: attachment; filename="' . $filename . '"'); $output = fopen('php://output', 'wb'); if ($output !== false) { fwrite($output, "-- Arbeitsstunden SQL Dump\n"); fwrite($output, '-- Erstellt am ' . date('Y-m-d H:i:s') . "\n\n"); $tables = ['users', 'work_logs', 'settings', 'audit_logs', 'password_resets']; foreach ($tables as $table) { $createStmt = $pdo->query('SHOW CREATE TABLE ' . $table)->fetch(PDO::FETCH_ASSOC); $createSql = $createStmt['Create Table'] ?? ''; fwrite($output, 'DROP TABLE IF EXISTS `' . $table . '`;' . "\n"); fwrite($output, $createSql . ';' . "\n\n"); $rows = $pdo->query('SELECT * FROM ' . $table)->fetchAll(PDO::FETCH_ASSOC); foreach ($rows as $row) { $columns = array_map(static fn (string $column): string => '`' . $column . '`', array_keys($row)); $values = array_map(static fn ($value): string => sqlValue($pdo, $value), array_values($row)); fwrite($output, 'INSERT INTO `' . $table . '` (' . implode(', ', $columns) . ') VALUES (' . implode(', ', $values) . ');' . "\n"); } fwrite($output, "\n"); } fclose($output); } exit; } $error = 'SQL-Dump ist ohne Datenbank nicht verfuegbar.'; } if ($user && $action === 'export_logs_csv' && $user['role'] === 'admin') { if ($pdo) { addAuditLog($pdo, (int)$user['id'], 'Logansicht als CSV exportiert.'); $filename = 'log-export-' . date('Y-m-d-His') . '.csv'; header('Content-Type: text/csv; charset=utf-8'); header('Content-Disposition: attachment; filename="' . $filename . '"'); $output = fopen('php://output', 'wb'); if ($output !== false) { fwrite($output, "\xEF\xBB\xBF"); $rows = $pdo->query('(SELECT w.created_at AS ts, CONCAT(a.firstname, " ", a.lastname) AS actor, CONCAT(w.hours, " Stunden für ", m.firstname, " ", m.lastname, " gebucht: ", w.note) AS action FROM work_logs w JOIN users a ON a.id = w.actor_id JOIN users m ON m.id = w.member_id) UNION ALL (SELECT l.created_at AS ts, CONCAT(a.firstname, " ", a.lastname) AS actor, l.action FROM audit_logs l JOIN users a ON a.id = l.actor_id) ORDER BY ts DESC')->fetchAll(PDO::FETCH_ASSOC); if ($rows) { fputcsv($output, array_keys($rows[0])); foreach ($rows as $row) { fputcsv($output, $row); } } fclose($output); } exit; } $error = 'Log-Export ist ohne Datenbank nicht verfuegbar.'; } if ($user && $action === 'import_sql_dump' && $user['role'] === 'admin') { if (!$pdo) { $error = 'SQL-Wiederherstellung ist ohne Datenbank nicht verfuegbar.'; } elseif (!isset($_FILES['sql_file']) || (int)($_FILES['sql_file']['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) { $error = 'Bitte eine gueltige SQL-Datei auswaehlen.'; } else { $sql = file_get_contents((string)$_FILES['sql_file']['tmp_name']); if ($sql === false || trim($sql) === '') { $error = 'Die SQL-Datei konnte nicht gelesen werden.'; } else { try { $pdo->beginTransaction(); $pdo->exec('SET FOREIGN_KEY_CHECKS=0'); $statements = preg_split('/;\s*(?:\R|$)/', $sql) ?: []; foreach ($statements as $statement) { $statement = trim($statement); if ($statement === '' || str_starts_with($statement, '--')) { continue; } $pdo->exec($statement); } $pdo->exec('SET FOREIGN_KEY_CHECKS=1'); $pdo->commit(); addAuditLog($pdo, (int)$user['id'], 'SQL-Dump importiert und Datenbank wiederhergestellt.'); $notice = 'SQL-Dump erfolgreich importiert.'; } catch (Throwable $e) { if ($pdo->inTransaction()) { $pdo->rollBack(); } $pdo->exec('SET FOREIGN_KEY_CHECKS=1'); $error = 'Die SQL-Datei konnte nicht importiert werden.'; } } } } if ($user && $action === 'import_csv' && $user['role'] === 'admin') { if (!$pdo) { $error = 'CSV-Import ist ohne Datenbank nicht verfuegbar.'; } elseif (!isset($_FILES['csv_file']) || (int)($_FILES['csv_file']['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) { $error = 'Bitte eine gueltige CSV-Datei auswaehlen.'; } else { $handle = fopen((string)$_FILES['csv_file']['tmp_name'], 'rb'); if ($handle === false) { $error = 'Die CSV-Datei konnte nicht gelesen werden.'; } else { $header = fgetcsv($handle); $requiredColumns = ['firstname', 'lastname', 'email', 'role', 'hours_worked']; if (!$header || array_diff($requiredColumns, $header)) { $error = 'Die CSV-Datei hat nicht das erwartete Format.'; } else { $columns = array_flip($header); $created = 0; $updated = 0; while (($row = fgetcsv($handle)) !== false) { $firstname = trim((string)($row[$columns['firstname']] ?? '')); $lastname = trim((string)($row[$columns['lastname']] ?? '')); $email = trim((string)($row[$columns['email']] ?? '')); $role = trim((string)($row[$columns['role']] ?? 'member')); $hoursWorked = (float)str_replace(',', '.', trim((string)($row[$columns['hours_worked']] ?? '0'))); if ($firstname === '' || $lastname === '' || $email === '') { continue; } if (!in_array($role, ['member', 'editor', 'admin'], true)) { $role = 'member'; } $stmt = $pdo->prepare('SELECT id FROM users WHERE email = ? LIMIT 1'); $stmt->execute([$email]); $existingId = (int)($stmt->fetchColumn() ?: 0); if ($existingId > 0) { $stmt = $pdo->prepare('UPDATE users SET firstname = ?, lastname = ?, role = ? WHERE id = ?'); $stmt->execute([$firstname, $lastname, $role, $existingId]); $userId = $existingId; $updated++; } else { $stmt = $pdo->prepare('INSERT INTO users (firstname, lastname, email, password_hash, role) VALUES (?, ?, ?, ?, ?)'); $stmt->execute([$firstname, $lastname, $email, password_hash('mitglied123', PASSWORD_DEFAULT), $role]); $userId = (int)$pdo->lastInsertId(); $created++; } $stmt = $pdo->prepare('DELETE FROM work_logs WHERE member_id = ?'); $stmt->execute([$userId]); if ($hoursWorked > 0) { $stmt = $pdo->prepare('INSERT INTO work_logs (member_id, actor_id, hours, note) VALUES (?, ?, ?, ?)'); $stmt->execute([$userId, (int)$user['id'], $hoursWorked, 'CSV-Import']); } } addAuditLog($pdo, (int)$user['id'], $created . ' Benutzer importiert, ' . $updated . ' Benutzer per CSV aktualisiert.'); $notice = $created . ' Benutzer importiert, ' . $updated . ' Benutzer aktualisiert.'; } fclose($handle); } } } if ($user && $action === 'logout') { addAuditLog($pdo, (int)$user['id'], 'Abmeldung erfolgt.'); session_destroy(); header('Location: /?page=login'); exit; } }