diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d4bc386 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,52 @@ +# AGENTS + +## Stack And Entry Points +- This repo is a plain PHP app for Strato-style hosting. There is no Node, Composer, container, or build step. +- Live web root is `httpdocs/`. +- Main app entrypoint: `httpdocs/index.php`. +- First-time setup entrypoint: `httpdocs/install.php`. +- App wiring and request handling live in `httpdocs/app/bootstrap.php`. +- All HTML/UI rendering lives in `httpdocs/app/views.php`. + +## Deployment Reality +- Changes in this repo do nothing until the contents of `httpdocs/` are uploaded to the server. +- If the user says “nothing changed”, verify whether the live site is serving the current workspace contents before changing logic again. +- The live site can be checked directly at `http://arbeitsstunden.tc-ingelfingen.de`. + +## Runtime Behavior +- The app supports two modes: + - With DB: MySQL/MariaDB via `DB_HOST`, `DB_NAME`, `DB_USER`, `DB_PASS`. + - Without DB: demo mode with in-code users from `findUserByEmail()` and in-code member/hour data from `fetchUsers()` / `fetchLogs()`. +- `index.php` redirects to `install.php` only when a DB connection exists and no users are installed. +- `install.php` only works with a DB connection; without DB it only shows an error. + +## Roles And UI Rules +- Roles are `member`, `editor`, `admin`. +- `member` should only see their own hours on the dashboard. +- `editor` and `admin` should see their own hours plus all members in the dashboard table. +- Admin-only menu item is `Vereinskonfiguration`. +- Header should keep club name on the left and current user + logout on the right. +- Navigation is implemented as a left sidebar inside `renderAppShell()`. + +## Persistence Gotchas +- Club settings are persisted in DB table `settings` and read into `$config` in `bootstrap.php`. +- If settings appear not to save, check DB mode first. In demo mode there is no DB persistence. +- Schema is created in `initSchema()` at runtime. Keep `schema.sql` aligned with runtime-created tables. +- `schema.sql` currently contains seed password hashes that may drift from runtime-generated demo passwords; trust runtime behavior in `bootstrap.php` over static SQL prose. + +## Session And Auth Gotchas +- Session handling is configured manually in `bootstrap.php` with `session_set_cookie_params()` before `session_start()`. +- Login redirects to `/?page=dashboard` after setting `$_SESSION['user']`. +- If auth appears broken, inspect `currentUser()` and the session flow in `bootstrap.php` before changing UI conditionals. + +## Editing Guidance +- Most app behavior changes require editing `httpdocs/app/views.php` and sometimes `httpdocs/app/bootstrap.php` together. +- Prefer minimal inline changes over adding new abstractions; the app is intentionally small and file-local. +- When changing page layout, update only `renderAppShell()` unless the login or install flow is involved. + +## Verification +- There is no automated test suite in the repo. +- Best available verification is: + - read the affected PHP files, + - check role-gated branches in `views.php` and action handling in `bootstrap.php`, + - optionally fetch the live URL to compare deployed output versus workspace output. diff --git a/ANLEITUNG_ADMIN.md b/ANLEITUNG_ADMIN.md new file mode 100644 index 0000000..9375e77 --- /dev/null +++ b/ANLEITUNG_ADMIN.md @@ -0,0 +1,153 @@ +# TC Ingelfingen + +## Arbeitsstundenverwaltung + +### Benutzeranleitung Für Administratoren + +Diese Anleitung beschreibt die wichtigsten Funktionen der Arbeitsstundenverwaltung aus Sicht eines Administrators. + +Administratoren verwalten die Anwendung, pflegen Vereinswerte und führen Sicherungs- und Wiederherstellungsaufgaben durch. + +## 1. Anmeldung + +Melden Sie sich mit Ihrer E-Mail-Adresse und Ihrem Passwort an. + +Falls Sie Ihr Passwort vergessen haben, können Sie den Passwort-Reset über die Anmeldeseite nutzen. + +## 2. Dashboard + +Als Administrator sehen Sie im Dashboard die Gesamtübersicht über alle Mitglieder. + +Angezeigt werden insbesondere: + +- Pflichtstunden gesamt +- geleistete Stunden gesamt +- noch offene Stunden gesamt +- die jeweiligen Werte in EUR +- der Fortschritt der geleisteten Arbeitsstunden in Prozent + +Zusätzlich wird ein Informationskasten angezeigt, sofern dieser vom Verein gepflegt wurde. + +## 3. Profil + +Über Ihren Namen oben rechts gelangen Sie in Ihr Profil. + +Dort können Sie: + +- Ihren Vornamen ändern +- Ihren Nachnamen ändern +- Ihr eigenes Passwort ändern + +## 4. Benutzerverwaltung + +Im Bereich `Benutzerverwaltung` können Sie: + +- neue Benutzer anlegen +- Benutzer löschen +- Benutzerpasswörter zurücksetzen +- Benutzerrollen ändern + +Die Benutzerliste ist nach Nachnamen sortiert und auf 50 Einträge pro Seite paginiert. + +## 5. Stundenbuchung + +Im Bereich `Stundenbuchung` können Sie: + +- Stunden für Mitglieder buchen +- alle Stundenbuchungen einsehen +- einzelne Stundenbuchungen löschen + +Die Liste der Stundenbuchungen ist auf 50 Einträge pro Seite paginiert. + +## 6. Vereinskonfiguration + +Im Bereich `Vereinskonfiguration` können Sie: + +- Pflichtstunden pro Mitglied festlegen +- den Wert pro Stunde festlegen +- den Informationskasten im Dashboard pflegen +- Benutzerdaten als CSV exportieren +- Stundenbuchungen als CSV exportieren + +## 7. Administration + +Im Bereich `Administration` finden Sie erweiterte Verwaltungsfunktionen. + +Dazu gehören: + +- SQL-Dump herunterladen +- SQL-Dump wiederherstellen +- Benutzerdaten per CSV importieren +- alle Arbeitsstunden auf 0 zurücksetzen +- Logansicht aller Verwaltungsaktionen + +Die Logansicht ist auf 50 Einträge pro Seite paginiert. + +## 8. SQL-Backup Und Wiederherstellung + +Für vollständige Datensicherungen steht ein SQL-Dump zur Verfügung. + +Empfohlene Vorgehensweise: + +1. Laden Sie vor größeren Änderungen einen SQL-Dump herunter. +2. Bewahren Sie die Datei an einem sicheren Ort auf. +3. Verwenden Sie für eine Wiederherstellung ausschließlich vertrauenswürdige SQL-Dateien. + +Wichtiger Hinweis: + +- Die Wiederherstellung eines SQL-Dumps überschreibt die bestehenden App-Daten. + +## 9. CSV-Import + +Der CSV-Import dient zur Übernahme von Benutzerdaten. + +Erwartet werden folgende Spalten: + +- `firstname` +- `lastname` +- `email` +- `role` +- `hours_worked` + +Benutzer werden anhand der E-Mail-Adresse erkannt. + +## 10. Arbeitsstunden Zurücksetzen + +Mit dieser Funktion werden alle gebuchten Arbeitsstunden aller Mitglieder auf 0 gesetzt. + +Diese Funktion ist insbesondere für den Jahreswechsel vorgesehen und sollte nur mit entsprechender Sorgfalt verwendet werden. + +## 11. Logansicht + +In der Logansicht werden unter anderem folgende Aktionen protokolliert: + +- Benutzer anlegen +- Benutzer löschen +- Rollen ändern +- Passwort zurücksetzen +- eigenes Passwort ändern +- Passwort per Reset-Link neu setzen +- Speichern von Vereinswerten +- CSV-Exporte +- CSV-Import +- Arbeitsstunden auf 0 setzen +- SQL-Dump exportieren und wiederherstellen + +## 12. Passwort Vergessen + +Falls Sie Ihr Passwort vergessen haben, können Sie den Reset-Link über die Anmeldeseite anfordern und anschließend ein neues Passwort vergeben. + +## 13. Darkmode + +Über den Schalter links neben `Abmelden` kann zwischen hellem und dunklem Farbschema gewechselt werden. + +## 14. Abmelden + +Bitte melden Sie sich nach Abschluss Ihrer Arbeiten stets über `Abmelden` ab. + +## 15. Empfehlungen Für Den Betrieb + +- Erstellen Sie regelmäßig SQL-Backups. +- Nutzen Sie CSV-Exporte für Auswertungen und Weitergaben. +- Prüfen Sie Importe vor der Ausführung sorgfältig. +- Dokumentieren Sie größere organisatorische oder technische Änderungen zusätzlich außerhalb der Anwendung. diff --git a/ANLEITUNG_BEARBEITER.md b/ANLEITUNG_BEARBEITER.md new file mode 100644 index 0000000..1732ffc --- /dev/null +++ b/ANLEITUNG_BEARBEITER.md @@ -0,0 +1,146 @@ +# TC Ingelfingen + +## Arbeitsstundenverwaltung + +### Benutzeranleitung Für Bearbeiter + +Diese Anleitung beschreibt die wichtigsten Funktionen der Arbeitsstundenverwaltung aus Sicht eines Bearbeiters. + +Bearbeiter unterstützen den Verein insbesondere bei der Erfassung von Arbeitsstunden und bei der Pflege von Benutzerdaten. + +## 1. Anmeldung + +Rufen Sie die Arbeitsstundenverwaltung über den Vereinslink auf und melden Sie sich mit Ihrer E-Mail-Adresse und Ihrem Passwort an. + +Falls Sie Ihr Passwort vergessen haben, können Sie über `Passwort vergessen?` auf der Anmeldeseite einen Reset-Link anfordern. + +## 2. Dashboard + +Nach der Anmeldung gelangen Sie auf das Dashboard. + +Als Bearbeiter sehen Sie dort nicht nur persönliche Daten, sondern die Gesamtübersicht über alle Mitglieder. + +Angezeigt werden insbesondere: + +- Pflichtstunden gesamt +- geleistete Stunden gesamt +- noch offene Stunden gesamt +- die jeweiligen Werte in EUR + +Zusätzlich wird ein Fortschrittsbalken angezeigt, der die geleisteten Arbeitsstunden in Prozent darstellt. + +Falls vom Verein gepflegt, erscheint außerdem ein Informationskasten mit aktuellen Mitteilungen. + +## 3. Profil + +Über Ihren Namen oben rechts gelangen Sie in den Bereich `Profil`. + +Dort können Sie: + +- Ihren Vornamen ändern +- Ihren Nachnamen ändern +- Ihr Passwort ändern + +Die hinterlegte E-Mail-Adresse wird angezeigt, kann jedoch nicht geändert werden. + +## 4. Benutzerverwaltung + +Über den Menüpunkt `Benutzerverwaltung` können Sie Benutzer verwalten. + +Zu Ihren Aufgaben gehören dort insbesondere: + +- neue Benutzer anlegen +- die Benutzerliste einsehen +- Passwörter von Benutzern zurücksetzen +- Benutzer löschen + +Die Benutzerliste ist nach Nachnamen sortiert und in Seiten mit jeweils 50 Einträgen gegliedert. + +Hinweis: + +- Das Ändern von Rollen ist ausschließlich Administratoren vorbehalten. + +## 5. Stundenbuchung + +Über den Menüpunkt `Stundenbuchung` können Sie Arbeitsstunden für Mitglieder erfassen. + +Für jede Buchung werden erfasst: + +- der Benutzer +- die Anzahl der Stunden +- eine kurze Notiz + +Unterhalb des Formulars sehen Sie die vollständige Liste der Stundenbuchungen. + +Dort werden angezeigt: + +- Datum +- Mitglied +- gebucht von +- Stunden +- Notiz + +Die Liste ist in Seiten mit jeweils 50 Einträgen aufgeteilt. + +Hinweis: + +- Das Löschen von Stundenbuchungen ist ausschließlich Administratoren möglich. + +## 6. Vereinskonfiguration + +Als Bearbeiter haben Sie Zugriff auf den Bereich `Vereinskonfiguration`. + +Dort können Sie: + +- die Pflichtstunden pro Mitglied anpassen +- den Wert pro Stunde festlegen +- den Informationskasten für das Dashboard pflegen +- Benutzerdaten als CSV exportieren +- Stundenbuchungen als CSV exportieren + +Der Informationskasten im Dashboard unterstützt eine einfache Markdown-Formatierung, zum Beispiel: + +- Absätze +- Listen mit `- ` +- `*kursiv*` +- `**fett**` + +## 7. Nicht Verfügbare Funktionen Für Bearbeiter + +Der Bereich `Administration` ist nur für Administratoren vorgesehen. + +Dort befinden sich Funktionen wie: + +- SQL-Datensicherung +- SQL-Wiederherstellung +- CSV-Import +- Zurücksetzen aller Arbeitsstunden auf 0 +- Logansicht der Verwaltungsaktionen + +## 8. Passwort Vergessen + +Wenn Sie Ihr Passwort vergessen haben, gehen Sie bitte wie folgt vor: + +1. Klicken Sie auf `Passwort vergessen?` +2. Geben Sie Ihre E-Mail-Adresse ein +3. Fordern Sie den Reset-Link an +4. Öffnen Sie die E-Mail des Systems +5. Vergeben Sie ein neues Passwort +6. Melden Sie sich anschließend erneut an + +## 9. Darkmode + +Über den Schalter links neben `Abmelden` können Sie zwischen hellem und dunklem Farbschema wechseln. + +Die Auswahl wird im Browser gespeichert. + +## 10. Abmelden + +Bitte melden Sie sich nach Abschluss Ihrer Arbeiten über den Button `Abmelden` oben rechts ordnungsgemäß ab. + +## 11. Hinweise Für Die Praxis + +- Achten Sie bei der Stundenbuchung auf nachvollziehbare und kurze Notizen. +- Prüfen Sie neue Benutzer sorgfältig auf korrekte Schreibweise von Namen und E-Mail-Adressen. +- Nutzen Sie die Exportfunktionen regelmäßig für Auswertungen oder organisatorische Weitergaben. +- Informieren Sie die Vereinsadministration bei Unklarheiten oder bei größeren Änderungen. diff --git a/ANLEITUNG_MITGLIEDER.md b/ANLEITUNG_MITGLIEDER.md new file mode 100644 index 0000000..abc2e6a --- /dev/null +++ b/ANLEITUNG_MITGLIEDER.md @@ -0,0 +1,85 @@ +# TC Ingelfingen + +## Arbeitsstundenverwaltung + +### Benutzeranleitung Für Mitglieder + +Diese Anleitung dient als kurze Einführung in die Nutzung der Arbeitsstundenverwaltung für Mitglieder. + +## 1. Anmeldung + +Rufen Sie die Arbeitsstundenverwaltung über den vom Verein bereitgestellten Link auf. + +Für die Anmeldung benötigen Sie: + +- Ihre hinterlegte E-Mail-Adresse +- Ihr persönliches Passwort + +Geben Sie beide Angaben auf der Anmeldeseite ein und klicken Sie anschließend auf `Anmelden`. + +## 2. Dashboard + +Nach der Anmeldung gelangen Sie direkt auf Ihr persönliches Dashboard. + +Dort erhalten Sie einen Überblick über: + +- Ihre Pflichtstunden +- Ihre bereits geleisteten Arbeitsstunden +- Ihre noch offenen Arbeitsstunden +- den rechnerischen Geldwert der noch offenen Stunden + +Zusätzlich kann auf dem Dashboard ein Informationskasten des Vereins angezeigt werden. In diesem Bereich veröffentlicht der Verein wichtige Hinweise, organisatorische Informationen oder aktuelle Mitteilungen. + +## 3. Stundenbuchungen Einsehen + +Über den Menüpunkt `Stundenbuchung` können Sie Ihre eigenen gebuchten Arbeitsstunden einsehen. + +In der Übersicht werden unter anderem folgende Informationen angezeigt: + +- Datum der Buchung +- Anzahl der gebuchten Stunden +- Notiz zur Buchung +- Person, die die Buchung eingetragen hat + +Als Mitglied sehen Sie ausschließlich Ihre eigenen Stundenbuchungen. + +## 4. Persönliche Daten Und Passwort + +Oben rechts in der Kopfzeile finden Sie Ihren Namen. Über diesen Button gelangen Sie in den Bereich `Profil`. + +Dort können Sie: + +- Ihren Vornamen ändern +- Ihren Nachnamen ändern +- Ihr Passwort ändern + +Ihre E-Mail-Adresse wird dort ebenfalls angezeigt, kann jedoch nicht geändert werden. + +## 5. Passwort Vergessen + +Wenn Sie Ihr Passwort nicht mehr kennen, nutzen Sie auf der Anmeldeseite den Link `Passwort vergessen?`. + +Gehen Sie dabei bitte wie folgt vor: + +1. Klicken Sie auf `Passwort vergessen?` +2. Geben Sie Ihre E-Mail-Adresse ein +3. Fordern Sie den Reset-Link an +4. Öffnen Sie die E-Mail des Systems +5. Vergeben Sie über den enthaltenen Link ein neues Passwort +6. Melden Sie sich anschließend mit dem neuen Passwort erneut an + +## 6. Darkmode + +Links neben dem Button `Abmelden` befindet sich ein Schalter für den Darkmode. + +Damit können Sie zwischen einer hellen und einer dunklen Darstellung der Anwendung wechseln. Die gewählte Einstellung wird auf Ihrem Gerät gespeichert. + +## 7. Abmelden + +Wenn Sie Ihre Arbeit beendet haben, melden Sie sich bitte über den Button `Abmelden` oben rechts wieder ab. + +Dadurch wird Ihre Sitzung sicher beendet. + +## 8. Unterstützung + +Falls Ihnen Daten fehlen, Stunden nicht korrekt angezeigt werden oder Sie keinen Zugriff auf Ihr Konto haben, wenden Sie sich bitte an einen Bearbeiter oder an die Vereinsadministration. diff --git a/README.md b/README.md index aa05641..0fc9fca 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ Strato-taugliche PHP-Webanwendung für die Arbeitszeiterfassung des TC Ingelfing - Audit-Log für Verwaltungsaktionen wie Benutzer anlegen, löschen, Rollen ändern und Passwortaktionen - CSV-Export und CSV-Import für Benutzerdaten inklusive geleisteter Stunden - Separater CSV-Export für alle Stundenbuchungen +- SQL-Dump-Export und SQL-Dump-Wiederherstellung für vollständige App-Backups - Admin-Funktion zum Zurücksetzen aller Arbeitsstunden auf 0 - Tabler CSS via CDN für UI, Cards, Tabellen und Formulare @@ -79,6 +80,8 @@ Die Anwendung ist so aufgebaut, dass sie mit oder ohne DB läuft. Bei gesetzter - Informationskasten für das Dashboard pflegen - Benutzerdaten als CSV exportieren - Alle Stundenbuchungen als CSV exportieren + - SQL-Dump für vollständige Datensicherung herunterladen + - SQL-Dump zur Wiederherstellung hochladen - Benutzerdaten aus CSV importieren - Alle Arbeitsstunden auf 0 zurücksetzen - Logansicht mit 50 Einträgen pro Seite @@ -115,6 +118,24 @@ Beispiel-Kopfzeile: `firstname,lastname,email,role,hours_worked` +## Backup Und Wiederherstellung + +Zusätzlich zu den CSV-Exporten gibt es in der Vereinskonfiguration einen SQL-Dump für vollständige Backups der App-Datenbank. + +Enthalten sind die App-Tabellen: + +- `users` +- `work_logs` +- `settings` +- `audit_logs` +- `password_resets` + +Hinweise: + +- Der SQL-Dump ist für vollständige Sicherungen und Wiederherstellung gedacht. +- Die SQL-Wiederherstellung überschreibt die bestehenden App-Daten. +- CSV-Exporte bleiben zusätzlich für Auswertungen und Teilimporte erhalten. + ## Passwort-Reset Und Konfiguration Für den Passwort-Reset per E-Mail wird eine `config.php` im Projektwurzelverzeichnis verwendet. Eine Vorlage liegt als `config.php.example` vor. @@ -147,6 +168,14 @@ Hinweis: Wenn noch kein Benutzer vorhanden ist, leite `index.php` automatisch auf `install.php` weiter. Dort kannst du ein erstes Admin-Konto anlegen. +## Handouts + +Für die Weitergabe an Anwender stehen folgende Anleitungen bereit: + +- `ANLEITUNG_MITGLIEDER.md` +- `ANLEITUNG_BEARBEITER.md` +- `ANLEITUNG_ADMIN.md` + ## Dateien - `httpdocs/index.php` Einstieg @@ -154,3 +183,6 @@ Wenn noch kein Benutzer vorhanden ist, leite `index.php` automatisch auf `instal - `httpdocs/app/bootstrap.php` DB, Login und Business-Logik - `httpdocs/app/views.php` Tabler-UI - `config.php.example` Vorlage für DB- und Mail-Konfiguration +- `ANLEITUNG_MITGLIEDER.md` Handout für Mitglieder +- `ANLEITUNG_BEARBEITER.md` Handout für Bearbeiter +- `ANLEITUNG_ADMIN.md` Handout für Administratoren diff --git a/config.php b/config.php new file mode 100644 index 0000000..a2a1a1a --- /dev/null +++ b/config.php @@ -0,0 +1,17 @@ + 'OpenCode', + 'copyright' => 'Copyright 2026 TC-Ingelfingen', + 'app_version' => '1.0.0', + 'db' => [ + 'host' => 'database-5020507124.webspace-host.com', + 'name' => 'dbs15701183', + 'user' => 'dbu747436', + 'pass' => 'KuTq0PHto5izfQ', + ], + 'smtp' => [ + 'from_email' => 'webmaster@tc-ingelfingen.de', + 'from_name' => 'TC-Ingelfingen', + ], +]; diff --git a/config.php.old b/config.php.old new file mode 100644 index 0000000..07cf3cd --- /dev/null +++ b/config.php.old @@ -0,0 +1,13 @@ + [ + 'host' => 'database-5020507124.webspace-host.com', + 'name' => 'dbs15701183', + 'user' => 'dbu747436', + 'pass' => 'KuTq0PHto5izfQ', + ], + 'hours_target' => 12, + 'hourly_rate_eur' => 10, +]; diff --git a/httpdocs/app/bootstrap.php b/httpdocs/app/bootstrap.php new file mode 100644 index 0000000..7043068 --- /dev/null +++ b/httpdocs/app/bootstrap.php @@ -0,0 +1,909 @@ + 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[] = '
  • ' . substr($item, 2) . '
  • '; + } + } + if ($listItems) { + $html[] = ''; + } + continue; + } + $html[] = '

    ' . 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'])) { + 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' && ($user['role'] ?? '') === '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 === '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') { + session_destroy(); + header('Location: /?page=login'); + exit; + } +} diff --git a/httpdocs/app/views.php b/httpdocs/app/views.php new file mode 100644 index 0000000..c40695f --- /dev/null +++ b/httpdocs/app/views.php @@ -0,0 +1,689 @@ + + + + + + + <?= htmlspecialchars($config['app_name'] . ' - ' . $title) ?> + + + + + + + + + + + + +
    +
    +
    TC Ingelfingen Logo
    +

    TC Ingelfingen

    +

    Arbeitsstundenverwaltung

    +
    +
    Keine Datenbankverbindung. Die App läuft nur im Demo-Modus, bis DB_HOST, DB_NAME, DB_USER und DB_PASS gesetzt sind.
    +
    +
    +
    +
    +
    + +
    +
    + +
    + +
    +
    Mit DatenbankOhne Datenbank
    +
    +
    +
    + +
    +
    +
    +
    +
    +

    Passwort zurücksetzen

    +
    + + +
    +
    + +
    + +
    +
    +
    + (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); + ?> +
    +
    + +
    + +
    +
    +
    +
    + +
    +
    h
    +
    Pro Stunde
    Stundensatz
    +
    h /
    +
    h /
    +
    +
    +

    Dashboard

    %

    Geleistete Arbeitsstunden in %.

    +
    +
    +

    Meine Stunden / Mitglieder

    NameRolleSollIstRestBetrag
    h h h
    +
    + +
    +

    Information

    +
    + + + + +
    +

    Profil

    +

    Passwort ändern

    +
    +
    +

    Hinweis

    Jeder Benutzer kann Vorname, Nachname und das eigene Passwort selbst ändern.

    Die E-Mail-Adresse wird angezeigt, kann aber nicht bearbeitet werden. Das neue Passwort muss mindestens 6 Zeichen lang sein.

    +
    + + + +
    +

    Benutzer anlegen

    Benutzer werden mit dem Startpasswort mitglied123 angelegt.
    +

    Hinweis

    Nur Admins dürfen Rollen setzen oder ändern.

    Neue Benutzer erhalten automatisch das Startpasswort mitglied123.

    +
    +
    +

    Benutzerliste

    NachnameVornameE-MailRolleGeleistetAktionen
    h
    Keine Aktionen
    Keine Benutzer vorhanden.
    1): ?>
    Zurück
    Seite von , insgesamt Einträge
    Weiter
    +
    + + + +
    + +

    Stunden für Benutzer buchen

    +

    Hinweis

    Nur Bearbeiter und Admins können Stunden buchen.

    + +

    Meine Stundenbuchungen

    Hier sehen Sie Ihre eigenen Stundenbuchungen.

    + +
    +
    +

    Stundenbuchungen

    DatumMitgliedGebucht vonStundenNotizAktionen
    h
    Keine Aktionen
    Keine Stundenbuchungen vorhanden.
    1): ?>
    Zurück
    Seite von , insgesamt Einträge
    Weiter
    +
    + + + +
    +
    +

    Vereinskonfiguration

    +
    +

    Vereinswerte

    +

    Informationskasten im Dashboard

    Unterstützt Absätze, - Listen, *kursiv* und **fett**.
    +

    Exporte

    Exportfunktionen für Benutzerdaten und Stundenbuchungen.

    +
    +
    +
    + + + +
    +
    +

    Administration

    +
    +

    SQL-Backup

    Vollständige Datensicherung und Wiederherstellung der App-Datenbank.

    +

    CSV-Import

    Importiert Benutzerdaten aus einer CSV-Datei mit den Spalten firstname, lastname, email, role und hours_worked.

    Beispiel-Kopfzeile: firstname,lastname,email,role,hours_worked
    +

    Arbeitsstunden zurücksetzen

    Setzt alle gebuchten Arbeitsstunden für alle Mitglieder auf 0. Diese Aktion ist für den Jahreswechsel gedacht.

    +
    +

    Logansicht


    • :
    • Keine Logeinträge vorhanden.
    1): ?>
    Zurück
    Seite von , insgesamt Einträge
    Weiter
    +
    +
    + + + +
    + +
    + + + PDO::ERRMODE_EXCEPTION]); + echo "DB Verbindung: ok\n"; + + $version = $pdo->query('SELECT VERSION()')->fetchColumn(); + echo 'DB Version: ' . $version . "\n"; + + $hasUsers = $pdo->query("SHOW TABLES LIKE 'users'")->fetchColumn(); + echo 'Tabelle users vorhanden: ' . ($hasUsers ? 'ja' : 'nein') . "\n"; + + if ($hasUsers) { + $count = $pdo->query('SELECT COUNT(*) FROM users')->fetchColumn(); + echo 'Benutzer in users: ' . $count . "\n"; + } +} catch (Throwable $e) { + echo "DB Verbindung: fehlgeschlagen\n"; + echo 'Fehler: ' . $e->getMessage() . "\n"; +} diff --git a/httpdocs/index.php b/httpdocs/index.php new file mode 100644 index 0000000..2b9ba27 --- /dev/null +++ b/httpdocs/index.php @@ -0,0 +1,36 @@ + +
    +
    Keine Datenbankverbindung vorhanden. Prüfe DB_HOST, DB_NAME, DB_USER und DB_PASS.
    +
    + prepare('INSERT INTO users (firstname, lastname, email, password_hash, role) VALUES (?, ?, ?, ?, "admin")'); + $stmt->execute([ + trim((string)$_POST['firstname']), + trim((string)$_POST['lastname']), + trim((string)$_POST['email']), + password_hash((string)$_POST['password'], PASSWORD_DEFAULT), + ]); + header('Location: /?page=login'); + exit; + } catch (Throwable $e) { + $installError = $e->getMessage(); + } +} + +renderHeader('Installation'); +?> +
    +
    +

    Installation

    +

    Erstes Admin-Konto anlegen

    +
    + +
    + + +
    Keine Datenbankverbindung vorhanden. Prüfe DB_HOST, DB_NAME, DB_USER und DB_PASS.
    + +
    +
    +
    + +
    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +exec('DELETE FROM work_logs'); + $pdo->exec('DELETE FROM users'); + echo "Installation zurueckgesetzt. users und work_logs wurden geleert.\n"; + echo "Jetzt install.php aufrufen.\n"; +} catch (Throwable $e) { + echo 'Fehler: ' . $e->getMessage() . "\n"; +} diff --git a/schema.sql b/schema.sql new file mode 100644 index 0000000..764aa3b --- /dev/null +++ b/schema.sql @@ -0,0 +1,49 @@ +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; + +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; + +CREATE TABLE IF NOT EXISTS settings ( + setting_key VARCHAR(100) PRIMARY KEY, + setting_value TEXT NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +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; + +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; + +INSERT INTO users (firstname, lastname, email, password_hash, role) +VALUES +('Max', 'Mustermann', 'max@example.com', '$2y$10$Q4NnU8X3uGQfM4p4RkWnOe7d5Vw5O3p5uT4Q2a5W0uX1fJ1cR5h3C', 'admin'), +('Lisa', 'Beispiel', 'lisa@example.com', '$2y$10$Q4NnU8X3uGQfM4p4RkWnOe7d5Vw5O3p5uT4Q2a5W0uX1fJ1cR5h3C', 'editor'), +('Tom', 'Mitglied', 'tom@example.com', '$2y$10$Q4NnU8X3uGQfM4p4RkWnOe7d5Vw5O3p5uT4Q2a5W0uX1fJ1cR5h3C', 'member'); diff --git a/session-ses_1b8d.md b/session-ses_1b8d.md new file mode 100644 index 0000000..391fd3a --- /dev/null +++ b/session-ses_1b8d.md @@ -0,0 +1,3832 @@ +# New session - 2026-05-20T20:56:30.700Z + +**Session ID:** ses_1b8d30513ffeCrTldSmFfZYDlF +**Created:** 5/20/2026, 10:56:30 PM +**Updated:** 5/22/2026, 2:55:49 PM + +--- + +## Assistant (Build · GPT-5.4 · 5.3s) + +Alles klar. + +--- + +## User + +das Balken Diagramm im Dashboard: der untere Text ersetzen durch z.B. geleistete Arbeitsstunden in % und am Balkendiagramm den % Wert hinzufügen (bei Tabler möglich) + +--- + +## Assistant (Build · GPT-5.4 · 21.0s) + +Ich passe jetzt den Dashboard-Progress-Block an: neuer Erklärungstext und Prozentwert direkt im Balken. + +**Tool: apply_patch** + +**Input:** +```json +{ + "patchText": "*** Begin Patch\n*** Update File: /Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/views.php\n@@\n $dashboardWorkedAmount = $dashboardWorkedHours * $config['hourly_rate_eur'];\n $dashboardMissingAmount = $dashboardMissingHours * $config['hourly_rate_eur'];\n $dashboardScopeLabel = $canSeeAllMembers ? 'Alle Mitglieder' : (trim(((string)($current['firstname'] ?? '')) . ' ' . ((string)($current['lastname'] ?? ''))) ?: 'Mein Konto');\n+ $dashboardProgressPercent = min(100, ($dashboardWorkedHours / max(1, $dashboardTargetHours)) * 100);\n ?>\n@@\n-

    Dashboard

    %\">

    Mitglieder sehen ihren Stand, Bearbeiter und Admins die Gesamtwerte aller Mitglieder.

    \n+

    Dashboard

    %\">%

    Geleistete Arbeitsstunden in %.

    \n*** End Patch" +} +``` + +**Output:** +``` +Success. Updated the following files: +M Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/views.php +``` + +--- + +## Assistant (Build · GPT-5.4 · 9.3s) + +Erledigt. + +Im Dashboard wurde der Progress-Bereich angepasst: +- Text darunter jetzt: + - `Geleistete Arbeitsstunden in %.` +- Im Balken selbst wird jetzt der Prozentwert angezeigt + +Geändert in: +- `httpdocs/app/views.php` + +--- + +## User + +den %-Wert Oberhalb vom Balken rechts auch mit so einem ZickZack Pfeil Icon + +--- + +## Assistant (Build · GPT-5.4 · 16.9s) + +Ich ergänze den Prozentwert oberhalb des Balkens rechts mit einem kleinen Trend-/Zickzack-Icon und lasse die Anzeige im Balken bestehen. + +**Tool: apply_patch** + +**Input:** +```json +{ + "patchText": "*** Begin Patch\n*** Update File: /Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/views.php\n@@\n-

    Dashboard

    %\">%

    Geleistete Arbeitsstunden in %.

    \n+

    Dashboard

    %
    %\">%

    Geleistete Arbeitsstunden in %.

    \n*** End Patch" +} +``` + +**Output:** +``` +Success. Updated the following files: +M Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/views.php +``` + +--- + +## Assistant (Build · GPT-5.4 · 8.2s) + +Erledigt. + +Oberhalb des Balkens steht der Prozentwert jetzt rechts mit Trend-Icon: +- Icon: `ti ti-chart-line` +- daneben der Prozentwert + +Geändert in: +- `httpdocs/app/views.php` + +Der Prozentwert im Balken selbst bleibt zusätzlich erhalten. + +--- + +## User + +die % Angabe im Balken kann jetzt entfallen + +--- + +## Assistant (Build · GPT-5.4 · 17.4s) + +Ich entferne jetzt den Prozenttext direkt im Balken und lasse die Anzeige nur noch oberhalb rechts stehen. + +**Tool: apply_patch** + +**Input:** +```json +{ + "patchText": "*** Begin Patch\n*** Update File: /Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/views.php\n@@\n-

    Dashboard

    %
    %\">%

    Geleistete Arbeitsstunden in %.

    \n+

    Dashboard

    %
    %\">

    Geleistete Arbeitsstunden in %.

    \n*** End Patch" +} +``` + +**Output:** +``` +Success. Updated the following files: +M Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/views.php +``` + +--- + +## Assistant (Build · GPT-5.4 · 5.6s) + +Erledigt. + +Die Prozentangabe im Balken selbst wurde entfernt. +Der Wert bleibt nur noch oberhalb rechts neben dem Trend-Icon sichtbar. + +Geändert in: +- `httpdocs/app/views.php` + +--- + +## User + +wie viele Log Einträge sehe ich in der LOG Ansicht + +--- + +## Assistant (Build · GPT-5.4 · 10.2s) + +Aktuell gibt es in der Logansicht kein Limit im Code. + +In `httpdocs/app/bootstrap.php` lädt `fetchLogs()`: +- alle Stundenbuchungen aus `work_logs` +- plus alle Audit-Einträge aus `audit_logs` +- sortiert nach `ts DESC` + +Das heißt: +- du siehst grundsätzlich alle vorhandenen Log-Einträge +- zuerst die neuesten + +Relevante Stelle: +- `httpdocs/app/bootstrap.php`, Funktion `fetchLogs()` + +Wenn du willst, kann ich ein Limit einbauen, z. B.: +1. nur die letzten 50 Einträge +2. nur die letzten 100 Einträge +3. Paginierung mit `Mehr laden` oder Seitenzahl + +--- + +## User + +ja, 50 Einträge mit Pageinierung + +--- + +## Assistant (Build · GPT-5.4 · 15.4s) + +Ich ergänze jetzt eine Paginierung für die Logansicht mit 50 Einträgen pro Seite. Dazu erweitere ich `fetchLogs()` und die Admin-Ansicht minimal. + +**Tool: read** + +**Input:** +```json +{ + "filePath": "/Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/bootstrap.php", + "offset": 300, + "limit": 28 +} +``` + +**Output:** +``` +/Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/bootstrap.php +file + +300: ['id' => 2, 'firstname' => 'Lisa', 'lastname' => 'Beispiel', 'name' => 'Lisa Beispiel', 'email' => 'lisa@example.com', 'role' => 'editor', 'hours_worked' => 12.0], +301: ['id' => 3, 'firstname' => 'Tom', 'lastname' => 'Mitglied', 'name' => 'Tom Mitglied', 'email' => 'tom@example.com', 'role' => 'member', 'hours_worked' => 6.0], +302: ]; +303: } +304: +305: 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); +306: } +307: +308: function fetchLogs(?PDO $pdo): array +309: { +310: if (!$pdo) { +311: return [ +312: ['ts' => '2026-05-19 14:00', 'actor' => 'Admin', 'action' => 'Lisa Beispiel als Bearbeiter angelegt'], +313: ['ts' => '2026-05-18 09:30', 'actor' => 'Lisa Beispiel', 'action' => '2.0 Stunden für Tom Mitglied gebucht'], +314: ['ts' => '2026-05-17 18:15', 'actor' => 'Admin', 'action' => 'Stundensatz auf 10 EUR gesetzt'], +315: ]; +316: } +317: +318: return $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); +319: } +320: +321: function addAuditLog(?PDO $pdo, int $actorId, string $action): void +322: { +323: if (!$pdo || $actorId <= 0 || trim($action) === '') { +324: return; +325: } +326: $stmt = $pdo->prepare('INSERT INTO audit_logs (actor_id, action) VALUES (?, ?)'); +327: $stmt->execute([$actorId, $action]); + +(Showing lines 300-327 of 721. Use offset=328 to continue.) + +``` + +**Tool: read** + +**Input:** +```json +{ + "filePath": "/Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/views.php", + "offset": 620, + "limit": 18 +} +``` + +**Output:** +``` +/Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/views.php +file + +620: +621:

    Meine Stundenbuchungen

    Hier sehen Sie Ihre eigenen Stundenbuchungen.

    +622: +623: +624:
    +625:

    Stundenbuchungen

    DatumMitgliedGebucht vonStundenNotizAktionen
    h
    Keine Aktionen
    Keine Stundenbuchungen vorhanden.
    +626:
    +627: +628: +629: +630:
    +631:

    Vereinskonfiguration

    +632:
    +633:
    +634:

    Vereinswerte

    Unterstützt Absätze, - Listen, *kursiv* und **fett**.

    CSV-Export Benutzer

    Exportiert die Benutzerdaten inklusive geleisteter Stunden als CSV-Datei.


    CSV-Export Stundenbuchungen

    Exportiert alle Stundenbuchungen aller Mitglieder als CSV-Datei.


    Logansicht


    • :

    +637:
    + +(Showing lines 620-637 of 646. Use offset=638 to continue.) + +``` + +**Tool: read** + +**Input:** +```json +{ + "filePath": "/Users/erik/Documents/DEV/arbeitsstunden/httpdocs/index.php", + "offset": 1, + "limit": 40 +} +``` + +**Output:** +``` +/Users/erik/Documents/DEV/arbeitsstunden/httpdocs/index.php +file + +1: +``` + +--- + +## Assistant (Build · GPT-5.4 · 32.8s) + +Ich erweitere `fetchLogs()` um Limit/Offset plus Gesamtzahl und ergänze in der Logansicht einfache Vor/Zurück-Pagination mit 50 Einträgen pro Seite. + +**Tool: apply_patch** + +**Input:** +```json +{ + "patchText": "*** Begin Patch\n*** Update File: /Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/bootstrap.php\n@@\n-function fetchLogs(?PDO $pdo): array\n+function fetchLogs(?PDO $pdo, int $page = 1, int $perPage = 50): array\n {\n+ $page = max(1, $page);\n+ $perPage = max(1, $perPage);\n+ $offset = ($page - 1) * $perPage;\n+\n if (!$pdo) {\n- return [\n+ $rows = [\n ['ts' => '2026-05-19 14:00', 'actor' => 'Admin', 'action' => 'Lisa Beispiel als Bearbeiter angelegt'],\n ['ts' => '2026-05-18 09:30', 'actor' => 'Lisa Beispiel', 'action' => '2.0 Stunden für Tom Mitglied gebucht'],\n ['ts' => '2026-05-17 18:15', 'actor' => 'Admin', 'action' => 'Stundensatz auf 10 EUR gesetzt'],\n ];\n+ return [\n+ 'entries' => array_slice($rows, $offset, $perPage),\n+ 'total' => count($rows),\n+ 'page' => $page,\n+ 'per_page' => $perPage,\n+ 'total_pages' => max(1, (int)ceil(count($rows) / $perPage)),\n+ ];\n }\n \n- return $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);\n+ $countSql = 'SELECT COUNT(*) FROM ((SELECT w.id FROM work_logs w) UNION ALL (SELECT l.id FROM audit_logs l)) combined_logs';\n+ $total = (int)$pdo->query($countSql)->fetchColumn();\n+\n+ $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 ?');\n+ $stmt->bindValue(1, $perPage, PDO::PARAM_INT);\n+ $stmt->bindValue(2, $offset, PDO::PARAM_INT);\n+ $stmt->execute();\n+\n+ return [\n+ 'entries' => $stmt->fetchAll(PDO::FETCH_ASSOC),\n+ 'total' => $total,\n+ 'page' => $page,\n+ 'per_page' => $perPage,\n+ 'total_pages' => max(1, (int)ceil($total / $perPage)),\n+ ];\n }\n*** End Patch" +} +``` + +**Output:** +``` +Success. Updated the following files: +M Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/bootstrap.php +``` + +--- + +## Assistant (Build · GPT-5.4 · 30.2s) + +**Tool: apply_patch** + +**Input:** +```json +{ + "patchText": "*** Begin Patch\n*** Update File: /Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/views.php\n@@\n global $config, $notice, $user, $pdo;\n $current = currentUser() ?? [];\n $users = fetchUsers($pdo);\n- $logs = fetchLogs($pdo);\n+ $logPage = max(1, (int)($_GET['log_page'] ?? 1));\n+ $logs = fetchLogs($pdo, $logPage, 50);\n $bookings = fetchBookings($pdo, $current);\n@@\n-

    Logansicht


    • :
    \n+ \n*** End Patch" +} +``` + +**Output:** +``` +Success. Updated the following files: +M Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/views.php +``` + +--- + +## Assistant (Build · GPT-5.4 · 11.0s) + +Die Logansicht ist jetzt auf 50 Einträge pro Seite begrenzt und hat Paginierung. + +Geändert: +- `httpdocs/app/bootstrap.php` +- `httpdocs/app/views.php` + +Verhalten: +- 50 Logeinträge pro Seite +- `Zurück` / `Weiter` in der `Vereinskonfiguration` +- Anzeige von: + - aktueller Seite + - Gesamtseiten + - Gesamtanzahl der Einträge + +Technisch: +- `fetchLogs($pdo, $page, 50)` liefert jetzt: + - `entries` + - `total` + - `page` + - `per_page` + - `total_pages` + +Aufruf: +- Seite 1: `/?page=admin` +- weitere Seiten: `/?page=admin&log_page=2` usw. + +--- + +## User + +In der Benutzerliste in der Benutzerverwaltung bitte die gleiche Pageinierung einführen + +--- + +## Assistant (Build · GPT-5.4 · 14.6s) + +Ich prüfe die aktuelle Benutzerliste und erweitere sie dann analog mit 50 Einträgen pro Seite, ohne andere Verwendungen von `fetchUsers()` zu brechen. + +**Tool: read** + +**Input:** +```json +{ + "filePath": "/Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/bootstrap.php", + "offset": 287, + "limit": 24 +} +``` + +**Output:** +``` +/Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/bootstrap.php +file + +287: if ($hourlyRate !== false) { +288: $config['hourly_rate_eur'] = (float)$hourlyRate; +289: } +290: if ($dashboardInfo !== false) { +291: $config['dashboard_info'] = (string)$dashboardInfo; +292: } +293: } +294: +295: function fetchUsers(?PDO $pdo): array +296: { +297: if (!$pdo) { +298: return [ +299: ['id' => 1, 'firstname' => 'Max', 'lastname' => 'Mustermann', 'name' => 'Max Mustermann', 'email' => 'max@example.com', 'role' => 'admin', 'hours_worked' => 8.5], +300: ['id' => 2, 'firstname' => 'Lisa', 'lastname' => 'Beispiel', 'name' => 'Lisa Beispiel', 'email' => 'lisa@example.com', 'role' => 'editor', 'hours_worked' => 12.0], +301: ['id' => 3, 'firstname' => 'Tom', 'lastname' => 'Mitglied', 'name' => 'Tom Mitglied', 'email' => 'tom@example.com', 'role' => 'member', 'hours_worked' => 6.0], +302: ]; +303: } +304: +305: 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); +306: } +307: +308: function fetchLogs(?PDO $pdo, int $page = 1, int $perPage = 50): array +309: { +310: $page = max(1, $page); + +(Showing lines 287-310 of 746. Use offset=311 to continue.) + +``` + +**Tool: read** + +**Input:** +```json +{ + "filePath": "/Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/views.php", + "offset": 500, + "limit": 120 +} +``` + +**Output:** +``` +/Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/views.php +file + +500:
    +501:
    +502: +503: +504: +505:
    +506:
    +507:
    +508: (float)$item['hours_worked'], $users)) : $totalWorked; +531: $dashboardMissingHours = 0.0; +532: foreach ($users as $item) { +533: if (!$canSeeAllMembers && (int)$item['id'] !== (int)($current['id'] ?? 0)) { +534: continue; +535: } +536: $dashboardMissingHours += max(0, $config['hours_target'] - (float)$item['hours_worked']); +537: } +538: $dashboardWorkedAmount = $dashboardWorkedHours * $config['hourly_rate_eur']; +539: $dashboardMissingAmount = $dashboardMissingHours * $config['hourly_rate_eur']; +540: $dashboardScopeLabel = $canSeeAllMembers ? 'Alle Mitglieder' : (trim(((string)($current['firstname'] ?? '')) . ' ' . ((string)($current['lastname'] ?? ''))) ?: 'Mein Konto'); +541: $dashboardProgressPercent = min(100, ($dashboardWorkedHours / max(1, $dashboardTargetHours)) * 100); +542: ?> +543:
    +544:
    +545: +561:
    +562: +572:
    +573:
    +574:
    +575:
    +576: +577:
    +578:
    h
    +579:
    Pro Stunde
    Stundensatz
    +580:
    h /
    +581:
    h /
    +582:
    +583:
    +584:

    Dashboard

    %

    Geleistete Arbeitsstunden in %.

    +585:
    +586:
    +587:

    Meine Stunden / Mitglieder

    NameRolleSollIstRestBetrag
    h h h
    +588:
    +589: +590:
    +591:

    Information

    +592:
    +593: +594: +595: +596: +597:
    +598:

    Profil

    +599:

    Passwort ändern

    +600:
    +601:
    +602:

    Hinweis

    Jeder Benutzer kann Vorname, Nachname und das eigene Passwort selbst ändern.

    Die E-Mail-Adresse wird angezeigt, kann aber nicht bearbeitet werden. Das neue Passwort muss mindestens 6 Zeichen lang sein.

    +603:
    +604: +605: +606: +607:
    +608:

    Benutzer anlegen

    Benutzer werden mit dem Startpasswort mitglied123 angelegt.
    +609:

    Hinweis

    Nur Admins dürfen Rollen setzen oder ändern.

    Neue Benutzer erhalten automatisch das Startpasswort mitglied123.

    +610:
    +611:
    +612:

    Benutzerliste

    NachnameVornameE-MailRolleGeleistetAktionen
    Eigener Benutzer
    h
    +614: +615: +616: +617:
    +618: +619:

    Stunden für Benutzer buchen

    + +(Showing lines 500-619 of 647. Use offset=620 to continue.) + +``` + +--- + +## Assistant (Build · GPT-5.4 · 45.2s) + +Ich ergänze eine paginierte Benutzer-Fetch-Funktion nur für die Verwaltung und binde sie an die Mitgliederseite mit 50 Einträgen plus Vor/Zurück an. + +**Tool: apply_patch** + +**Input:** +```json +{ + "patchText": "*** Begin Patch\n*** Update File: /Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/bootstrap.php\n@@\n function fetchUsers(?PDO $pdo): array\n {\n@@\n 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);\n }\n+\n+function fetchUsersPage(?PDO $pdo, int $page = 1, int $perPage = 50): array\n+{\n+ $page = max(1, $page);\n+ $perPage = max(1, $perPage);\n+ $offset = ($page - 1) * $perPage;\n+\n+ if (!$pdo) {\n+ $rows = fetchUsers($pdo);\n+ return [\n+ 'entries' => array_slice($rows, $offset, $perPage),\n+ 'total' => count($rows),\n+ 'page' => $page,\n+ 'per_page' => $perPage,\n+ 'total_pages' => max(1, (int)ceil(count($rows) / $perPage)),\n+ ];\n+ }\n+\n+ $total = (int)$pdo->query('SELECT COUNT(*) FROM users')->fetchColumn();\n+ $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 ?');\n+ $stmt->bindValue(1, $perPage, PDO::PARAM_INT);\n+ $stmt->bindValue(2, $offset, PDO::PARAM_INT);\n+ $stmt->execute();\n+\n+ return [\n+ 'entries' => $stmt->fetchAll(PDO::FETCH_ASSOC),\n+ 'total' => $total,\n+ 'page' => $page,\n+ 'per_page' => $perPage,\n+ 'total_pages' => max(1, (int)ceil($total / $perPage)),\n+ ];\n+}\n \n function fetchLogs(?PDO $pdo, int $page = 1, int $perPage = 50): array\n*** End Patch" +} +``` + +**Output:** +``` +Success. Updated the following files: +M Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/bootstrap.php +``` + +--- + +## Assistant (Build · GPT-5.4 · 73.2s) + +**Tool: apply_patch** + +**Input:** +```json +{ + "patchText": "*** Begin Patch\n*** Update File: /Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/views.php\n@@\n global $config, $notice, $user, $pdo;\n $current = currentUser() ?? [];\n $users = fetchUsers($pdo);\n+ $memberPage = max(1, (int)($_GET['member_page'] ?? 1));\n+ $memberUsers = fetchUsersPage($pdo, $memberPage, 50);\n $logPage = max(1, (int)($_GET['log_page'] ?? 1));\n $logs = fetchLogs($pdo, $logPage, 50);\n@@\n-

    Benutzerliste

    NachnameVornameE-MailRolleGeleistetAktionen
    \">Eigener Benutzer
    h
    \">
    \">
    Keine Aktionen
    \n+

    Benutzerliste

    NachnameVornameE-MailRolleGeleistetAktionen
    \">Eigener Benutzer
    h
    \">
    \">
    Keine Aktionen
    Keine Benutzer vorhanden.
    1): ?>
    \n*** End Patch" +} +``` + +**Output:** +``` +Success. Updated the following files: +M Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/views.php +``` + +--- + +## Assistant (Build · GPT-5.4 · 13.2s) + +Die Benutzerliste in der Benutzerverwaltung hat jetzt ebenfalls Paginierung mit 50 Einträgen pro Seite. + +Geändert: +- `httpdocs/app/bootstrap.php` +- `httpdocs/app/views.php` + +Neu: +- paginierte Benutzerliste über `fetchUsersPage($pdo, $page, 50)` +- `Zurück` / `Weiter` +- Anzeige von: + - aktueller Seite + - Gesamtseiten + - Gesamtanzahl der Benutzer + +Wichtig: +- Für Dashboard, Buchungsformular usw. wird weiterhin die komplette Benutzerliste über `fetchUsers()` genutzt +- Nur die Tabelle in der Benutzerverwaltung ist paginiert + +Aufruf: +- `/?page=members` +- weitere Seiten: `/?page=members&member_page=2` + +--- + +## User + +in der Stundenbuchung in der Liste der Stundenbuchungen soll ebenso eine Pageinierung eingeführt werden + +--- + +## Assistant (Build · GPT-5.4 · 9.8s) + +Ich ergänze jetzt auch für die Stundenbuchungsliste eine 50er-Paginierung, analog zu Log- und Benutzerliste, ohne die übrige Buchungslogik zu verändern. + +**Tool: read** + +**Input:** +```json +{ + "filePath": "/Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/bootstrap.php", + "offset": 333, + "limit": 28 +} +``` + +**Output:** +``` +/Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/bootstrap.php +file + +333: 'total' => $total, +334: 'page' => $page, +335: 'per_page' => $perPage, +336: 'total_pages' => max(1, (int)ceil($total / $perPage)), +337: ]; +338: } +339: +340: function fetchLogs(?PDO $pdo, int $page = 1, int $perPage = 50): array +341: { +342: $page = max(1, $page); +343: $perPage = max(1, $perPage); +344: $offset = ($page - 1) * $perPage; +345: +346: if (!$pdo) { +347: $rows = [ +348: ['ts' => '2026-05-19 14:00', 'actor' => 'Admin', 'action' => 'Lisa Beispiel als Bearbeiter angelegt'], +349: ['ts' => '2026-05-18 09:30', 'actor' => 'Lisa Beispiel', 'action' => '2.0 Stunden für Tom Mitglied gebucht'], +350: ['ts' => '2026-05-17 18:15', 'actor' => 'Admin', 'action' => 'Stundensatz auf 10 EUR gesetzt'], +351: ]; +352: return [ +353: 'entries' => array_slice($rows, $offset, $perPage), +354: 'total' => count($rows), +355: 'page' => $page, +356: 'per_page' => $perPage, +357: 'total_pages' => max(1, (int)ceil(count($rows) / $perPage)), +358: ]; +359: } +360: + +(Showing lines 333-360 of 778. Use offset=361 to continue.) + +``` + +**Tool: read** + +**Input:** +```json +{ + "filePath": "/Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/views.php", + "offset": 620, + "limit": 24 +} +``` + +**Output:** +``` +/Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/views.php +file + +620: +621:

    Stunden für Benutzer buchen

    +622:

    Hinweis

    Nur Bearbeiter und Admins können Stunden buchen.

    +623: +624:

    Meine Stundenbuchungen

    Hier sehen Sie Ihre eigenen Stundenbuchungen.

    +625: +626:
    +627:
    +628:

    Stundenbuchungen

    DatumMitgliedGebucht vonStundenNotizAktionen
    h
    Keine Aktionen
    Keine Stundenbuchungen vorhanden.
    +629:
    +630: +631: +632: +633:
    +634:

    Vereinskonfiguration

    +635:
    +636:
    +637:

    Vereinswerte

    Unterstützt Absätze, - Listen, *kursiv* und **fett**.

    CSV-Export Benutzer

    Exportiert die Benutzerdaten inklusive geleisteter Stunden als CSV-Datei.


    CSV-Export Stundenbuchungen

    Exportiert alle Stundenbuchungen aller Mitglieder als CSV-Datei.


    Logansicht


    • :
    • Keine Logeinträge vorhanden.
    1): ?>
    Zurück
    Seite von , insgesamt Einträge
    Weiter

    +640:
    +641:
    +642: +643:
    + +(Showing lines 620-643 of 649. Use offset=644 to continue.) + +``` + +--- + +## Assistant (Build · GPT-5.4 · 34.7s) + +Ich ergänze eine paginierte Buchungs-Fetch-Funktion und binde sie in die Stundenbuchungsseite mit 50 Einträgen plus Navigation ein. + +**Tool: apply_patch** + +**Input:** +```json +{ + "patchText": "*** Begin Patch\n*** Update File: /Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/bootstrap.php\n@@\n function fetchBookings(?PDO $pdo, array $user): array\n {\n@@\n 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);\n }\n+\n+function fetchBookingsPage(?PDO $pdo, array $user, int $page = 1, int $perPage = 50): array\n+{\n+ $page = max(1, $page);\n+ $perPage = max(1, $perPage);\n+ $offset = ($page - 1) * $perPage;\n+\n+ if (!$pdo) {\n+ $rows = fetchBookings($pdo, $user);\n+ return [\n+ 'entries' => array_slice($rows, $offset, $perPage),\n+ 'total' => count($rows),\n+ 'page' => $page,\n+ 'per_page' => $perPage,\n+ 'total_pages' => max(1, (int)ceil(count($rows) / $perPage)),\n+ ];\n+ }\n+\n+ if (($user['role'] ?? '') === 'member') {\n+ $countStmt = $pdo->prepare('SELECT COUNT(*) FROM work_logs WHERE member_id = ?');\n+ $countStmt->execute([(int)($user['id'] ?? 0)]);\n+ $total = (int)$countStmt->fetchColumn();\n+\n+ $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 ?');\n+ $stmt->bindValue(1, (int)($user['id'] ?? 0), PDO::PARAM_INT);\n+ $stmt->bindValue(2, $perPage, PDO::PARAM_INT);\n+ $stmt->bindValue(3, $offset, PDO::PARAM_INT);\n+ $stmt->execute();\n+ } else {\n+ $total = (int)$pdo->query('SELECT COUNT(*) FROM work_logs')->fetchColumn();\n+ $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 ?');\n+ $stmt->bindValue(1, $perPage, PDO::PARAM_INT);\n+ $stmt->bindValue(2, $offset, PDO::PARAM_INT);\n+ $stmt->execute();\n+ }\n+\n+ return [\n+ 'entries' => $stmt->fetchAll(PDO::FETCH_ASSOC),\n+ 'total' => $total,\n+ 'page' => $page,\n+ 'per_page' => $perPage,\n+ 'total_pages' => max(1, (int)ceil($total / $perPage)),\n+ ];\n+}\n \n function findUserByEmail(?PDO $pdo, string $email): ?array\n*** End Patch" +} +``` + +**Output:** +``` +Success. Updated the following files: +M Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/bootstrap.php +``` + +--- + +## Assistant (Build · GPT-5.4 · 46.8s) + +**Tool: apply_patch** + +**Input:** +```json +{ + "patchText": "*** Begin Patch\n*** Update File: /Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/views.php\n@@\n $users = fetchUsers($pdo);\n $memberPage = max(1, (int)($_GET['member_page'] ?? 1));\n $memberUsers = fetchUsersPage($pdo, $memberPage, 50);\n $logPage = max(1, (int)($_GET['log_page'] ?? 1));\n $logs = fetchLogs($pdo, $logPage, 50);\n- $bookings = fetchBookings($pdo, $current);\n+ $bookingPage = max(1, (int)($_GET['booking_page'] ?? 1));\n+ $bookings = fetchBookingsPage($pdo, $current, $bookingPage, 50);\n@@\n-

    Stundenbuchungen

    DatumMitgliedGebucht vonStundenNotizAktionen
    h
    \">
    Keine Aktionen
    Keine Stundenbuchungen vorhanden.
    \n+

    Stundenbuchungen

    DatumMitgliedGebucht vonStundenNotizAktionen
    h
    \">
    Keine Aktionen
    Keine Stundenbuchungen vorhanden.
    1): ?>
    \n*** End Patch" +} +``` + +**Output:** +``` +Success. Updated the following files: +M Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/views.php +``` + +--- + +## Assistant (Build · GPT-5.4 · 11.3s) + +Die Liste der Stundenbuchungen hat jetzt ebenfalls Paginierung mit 50 Einträgen pro Seite. + +Geändert: +- `httpdocs/app/bootstrap.php` +- `httpdocs/app/views.php` + +Neu: +- paginierte Buchungsabfrage über `fetchBookingsPage(...)` +- funktioniert rollenabhängig weiter korrekt: + - `member`: nur eigene Buchungen + - `editor` / `admin`: alle Buchungen +- `Zurück` / `Weiter` +- Anzeige von: + - aktueller Seite + - Gesamtseiten + - Gesamtanzahl der Einträge + +Aufruf: +- `/?page=booking` +- weitere Seiten: `/?page=booking&booking_page=2` + +Wichtig: +- Das Buchungsformular oben bleibt unverändert +- Nur die Liste darunter ist jetzt paginiert + +--- + +## User + +bitte die readme anpassen + +--- + +## Assistant (Build · GPT-5.4 · 6.7s) + +Ich lese die aktuelle README kurz ein und ergänze die neuen Punkte zu Passwort-Reset, Audit-Logs und den drei Paginierungen. + +**Tool: read** + +**Input:** +```json +{ + "filePath": "/Users/erik/Documents/DEV/arbeitsstunden/README.md", + "offset": 1, + "limit": 260 +} +``` + +**Output:** +``` +/Users/erik/Documents/DEV/arbeitsstunden/README.md +file + +1: # TC Ingelfingen Arbeitszeiterfassung +2: +3: Strato-taugliche PHP-Webanwendung für die Arbeitszeiterfassung des TC Ingelfingen. +4: +5: ## Eigenschaften +6: +7: - Läuft direkt aus `httpdocs` ohne Shell, Node oder Container +8: - Optional an externe MySQL/MariaDB-Datenbank anbindbar +9: - Mitglieder-Login mit Rollenmodell +10: - Mitglieder sehen Stundenstand, Reststunden und möglichen EUR-Abzug +11: - Bearbeiter buchen Stunden und legen Mitglieder an +12: - Admins können alles, inklusive Mitglieder anlegen, Rollen vergeben und Logs ansehen +13: - Eigene Profilseite für Vorname, Nachname und Passwortänderung +14: - Passwort-Reset über E-Mail-Link auf der Anmeldeseite +15: - Darkmode-Schalter in der Kopfzeile mit lokaler Speicherung +16: - Dashboard-Infokasten für alle Mitglieder, pflegbar durch Admins mit einfacher Markdown-Unterstützung +17: - Aggregierte Dashboard-Übersicht für Bearbeiter und Admins über alle Mitglieder +18: - Stundenbuchungen mit Rollenansicht: Mitglieder nur eigene, Bearbeiter und Admins alle +19: - Admins können einzelne Stundenbuchungen löschen +20: - CSV-Export und CSV-Import für Benutzerdaten inklusive geleisteter Stunden +21: - Separater CSV-Export für alle Stundenbuchungen +22: - Admin-Funktion zum Zurücksetzen aller Arbeitsstunden auf 0 +23: - Tabler CSS via CDN für UI, Cards, Tabellen und Formulare +24: +25: ## Strato-Setup +26: +27: 1. Inhalt von `httpdocs/` auf das Strato-Webverzeichnis hochladen +28: 2. Optional diese Umgebungsvariablen oder Konfigurationswerte setzen: +29: - `DB_HOST` +30: - `DB_NAME` +31: - `DB_USER` +32: - `DB_PASS` +33: - `SETUP_KEY` optional, schützt die Ersteinrichtung +34: 3. PHP 8.1+ und `pdo_mysql` aktivieren +35: +36: ## Strato Schritte +37: +38: 1. DB in Strato anlegen +39: 2. `DB_HOST`, `DB_NAME`, `DB_USER`, `DB_PASS` setzen +40: 3. `httpdocs/install.php` öffnen und erstes Admin-Konto anlegen +41: 4. Danach mit dem neuen Admin einloggen +42: 5. Falls keine DB konfiguriert ist, nutzt die App Demo-Zugänge nur zum Anzeigen +43: +44: ## Datenbank +45: +46: Die Anwendung ist so aufgebaut, dass sie mit oder ohne DB läuft. Bei gesetzter DB werden Tabellen automatisch angelegt, sonst werden Demo-Daten genutzt. +47: +48: - Mitglieder +49: - Benutzer/Rollen +50: - Arbeitsstunden +51: - Audit-Log +52: - Einstellungen +53: +54: ## Rollen +55: +56: - `member`: sieht das eigene Dashboard, die eigene Profilseite und die eigenen Stundenbuchungen +57: - `editor`: darf Stunden buchen, Mitglieder anlegen und alle Stundenbuchungen sehen +58: - `admin`: darf alles, inklusive Mitglieder anlegen, Rollen vergeben, CSV-Import/Export und globalem Stunden-Reset +59: +60: ## Funktionen +61: +62: - Profil +63: - Jeder Benutzer kann Vorname, Nachname und das eigene Passwort ändern +64: - Die E-Mail-Adresse wird im Profil angezeigt, ist aber nicht editierbar +65: - Für vergessene Passwörter gibt es einen Reset-Link per E-Mail +66: - Dashboard +67: - `member` sieht die eigenen Pflichtstunden, geleisteten Stunden und offenen Stunden +68: - `editor` und `admin` sehen aggregierte Gesamtwerte aller Mitglieder inklusive EUR-Werten +69: - Ein zusätzlicher Informationskasten kann durch Admins gepflegt werden +70: - Stundenbuchungen +71: - `member` sieht nur eigene Buchungen +72: - `editor` und `admin` sehen alle Buchungen +73: - `admin` kann einzelne Buchungen löschen +74: - Vereinskonfiguration +75: - Pflichtstunden und Stundenwert pflegen +76: - Informationskasten für das Dashboard pflegen +77: - Benutzerdaten als CSV exportieren +78: - Alle Stundenbuchungen als CSV exportieren +79: - Benutzerdaten aus CSV importieren +80: - Alle Arbeitsstunden auf 0 zurücksetzen +81: +82: ## Markdown Im Informationskasten +83: +84: Der Informationskasten im Dashboard unterstützt eine einfache Markdown-Teilmenge: +85: +86: - Absätze +87: - Listen mit `- ` +88: - `*kursiv*` +89: - `**fett**` +90: +91: ## CSV-Format +92: +93: Der CSV-Export und CSV-Import für Benutzerdaten verwenden diese Spalten: +94: +95: - `firstname` +96: - `lastname` +97: - `email` +98: - `role` +99: - `hours_worked` +100: +101: Beispiel-Kopfzeile: +102: +103: `firstname,lastname,email,role,hours_worked` +104: +105: ## Passwort-Reset Und Konfiguration +106: +107: Für den Passwort-Reset per E-Mail wird eine `config.php` im Projektwurzelverzeichnis verwendet. Eine Vorlage liegt als `config.php.example` vor. +108: +109: Mindestens sinnvoll ist ein Absender für E-Mails: +110: +111: ```php +112: [ +115: 'host' => '127.0.0.1', +116: 'name' => 'arbeitsstunden', +117: 'user' => 'dbuser', +118: 'pass' => 'dbpass', +119: ], +120: 'smtp' => [ +121: 'from_email' => 'noreply@example.de', +122: 'from_name' => 'TC Ingelfingen', +123: ], +124: ]; +125: ``` +126: +127: Hinweis: +128: +129: - Die aktuelle Implementierung nutzt PHP `mail()` fuer den Versand. +130: - Wenn das Hosting E-Mails direkt versenden kann, funktioniert der Passwort-Reset auch ohne vollstaendige SMTP-Implementierung. +131: - Die Datei `config.php` sollte nicht in `httpdocs/`, sondern im Projektwurzelverzeichnis liegen. +132: +133: ## Ersteinrichtung +134: +135: Wenn noch kein Benutzer vorhanden ist, leite `index.php` automatisch auf `install.php` weiter. Dort kannst du ein erstes Admin-Konto anlegen. +136: +137: ## Dateien +138: +139: - `httpdocs/index.php` Einstieg +140: - `httpdocs/install.php` Ersteinrichtung +141: - `httpdocs/app/bootstrap.php` DB, Login und Business-Logik +142: - `httpdocs/app/views.php` Tabler-UI +143: - `config.php.example` Vorlage für DB- und Mail-Konfiguration + +(End of file - total 143 lines) + +``` + +--- + +## Assistant (Build · GPT-5.4 · 21.8s) + +Ich ergänze README jetzt um Audit-Logs und die neuen Paginierungen in Log-, Benutzer- und Buchungslisten. + +**Tool: apply_patch** + +**Input:** +```json +{ + "patchText": "*** Begin Patch\n*** Update File: /Users/erik/Documents/DEV/arbeitsstunden/README.md\n@@\n - Aggregierte Dashboard-Übersicht für Bearbeiter und Admins über alle Mitglieder\n - Stundenbuchungen mit Rollenansicht: Mitglieder nur eigene, Bearbeiter und Admins alle\n - Admins können einzelne Stundenbuchungen löschen\n+- Paginierung mit 50 Einträgen pro Seite in Benutzerliste, Stundenbuchungen und Logansicht\n+- Audit-Log für Verwaltungsaktionen wie Benutzer anlegen, löschen, Rollen ändern und Passwortaktionen\n - CSV-Export und CSV-Import für Benutzerdaten inklusive geleisteter Stunden\n - Separater CSV-Export für alle Stundenbuchungen\n@@\n - Stundenbuchungen\n - `member` sieht nur eigene Buchungen\n - `editor` und `admin` sehen alle Buchungen\n - `admin` kann einzelne Buchungen löschen\n+ - Die Liste ist auf 50 Einträge pro Seite paginiert\n - Vereinskonfiguration\n - Pflichtstunden und Stundenwert pflegen\n - Informationskasten für das Dashboard pflegen\n - Benutzerdaten als CSV exportieren\n - Alle Stundenbuchungen als CSV exportieren\n - Benutzerdaten aus CSV importieren\n - Alle Arbeitsstunden auf 0 zurücksetzen\n+ - Logansicht mit 50 Einträgen pro Seite\n+ - Audit-Log für Verwaltungsaktionen\n+\n+## Listen Und Paginierung\n+\n+Die folgenden Bereiche nutzen eine Paginierung mit 50 Einträgen pro Seite:\n+\n+- Benutzerliste in der Benutzerverwaltung\n+- Stundenbuchungen\n+- Logansicht in der Vereinskonfiguration\n*** End Patch" +} +``` + +**Output:** +``` +Success. Updated the following files: +M Users/erik/Documents/DEV/arbeitsstunden/README.md +``` + +--- + +## Assistant (Build · GPT-5.4 · 11.3s) + +Die README ist angepasst. + +Neu dokumentiert: +- Paginierung mit 50 Einträgen pro Seite +- Audit-Log für Verwaltungsaktionen +- Paginierung in: + - Benutzerliste + - Stundenbuchungen + - Logansicht + +Geändert in: +- `README.md` + +--- + +## User + +in der Benutzerliste der Benutzerverwaltung kann der Text eigener Benuter beim eigenen Benutzer entfallen + +--- + +## Assistant (Build · GPT-5.4 · 76.4s) + +Ich entferne den Hinweis `Eigener Benutzer` aus der Benutzerliste und lasse für den eigenen Benutzer an dieser Stelle einfach keine Zusatzanzeige stehen. + +**Tool: apply_patch** + +**Input:** +```json +{ + "patchText": "*** Begin Patch\n*** Update File: /Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/views.php\n@@\n-

    Benutzerliste

    NachnameVornameE-MailRolleGeleistetAktionen
    \">Eigener Benutzer
    h
    \">
    \">
    Keine Aktionen
    Keine Benutzer vorhanden.
    1): ?>
    \n+

    Benutzerliste

    NachnameVornameE-MailRolleGeleistetAktionen
    \">
    h
    \">
    \">
    Keine Aktionen
    Keine Benutzer vorhanden.
    1): ?>
    \n*** End Patch" +} +``` + +**Output:** +``` +Success. Updated the following files: +M Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/views.php +``` + +--- + +## Assistant (Build · GPT-5.4 · 7.8s) + +Erledigt. + +In der Benutzerliste der Benutzerverwaltung entfällt jetzt beim eigenen Benutzer der Text: +- `Eigener Benutzer` + +Geändert in: +- `httpdocs/app/views.php` + +Verhalten jetzt: +- Beim eigenen Benutzer bleibt das Rollenfeld deaktiviert +- Es wird dort aber kein zusätzlicher Hinweistext mehr angezeigt + +--- + +## User + +die Export Funktionen in der Verreinskonfiguration in eine extra Box Exporte nehmen + +--- + +## Assistant (Build · GPT-5.4 · 9.1s) + +Ich trenne jetzt die Exportfunktionen in der Vereinskonfiguration in eine eigene Karte `Exporte` aus, ohne die restlichen Bereiche umzubauen. + +**Tool: read** + +**Input:** +```json +{ + "filePath": "/Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/views.php", + "offset": 636, + "limit": 18 +} +``` + +**Output:** +``` +/Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/views.php +file + +636:
    +637:
    +638:

    Vereinswerte

    Unterstützt Absätze, - Listen, *kursiv* und **fett**.

    CSV-Export Benutzer

    Exportiert die Benutzerdaten inklusive geleisteter Stunden als CSV-Datei.


    CSV-Export Stundenbuchungen

    Exportiert alle Stundenbuchungen aller Mitglieder als CSV-Datei.


    Logansicht


    • :
    • Keine Logeinträge vorhanden.
    1): ?>
    Zurück
    Seite von , insgesamt Einträge
    Weiter

    +641:
    +642:
    +643: +644:
    +645: +646:
    +647: +648: +649: +``` + +--- + +## Assistant (Build · GPT-5.4 · 57.6s) + +Ich teile den bisherigen Block jetzt in `Vereinswerte`, `Exporte`, `CSV-Import` und `Arbeitsstunden zurücksetzen`, damit die Exporte separat stehen. + +**Tool: apply_patch** + +**Input:** +```json +{ + "patchText": "*** Begin Patch\n*** Update File: /Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/views.php\n@@\n-

    Vereinswerte

    \">
    \">
    Unterstützt Absätze, - Listen, *kursiv* und **fett**.

    CSV-Export Benutzer

    Exportiert die Benutzerdaten inklusive geleisteter Stunden als CSV-Datei.


    CSV-Export Stundenbuchungen

    Exportiert alle Stundenbuchungen aller Mitglieder als CSV-Datei.


    CSV-Import

    Importiert Benutzerdaten aus einer CSV-Datei mit den Spalten firstname, lastname, email, role und hours_worked.

    Beispiel-Kopfzeile: firstname,lastname,email,role,hours_worked

    Arbeitsstunden zurücksetzen

    Setzt alle gebuchten Arbeitsstunden für alle Mitglieder auf 0. Diese Aktion ist für den Jahreswechsel gedacht.

    \n+

    Vereinswerte

    \">
    \">
    Unterstützt Absätze, - Listen, *kursiv* und **fett**.
    \n+

    Exporte

    Exportfunktionen für Benutzerdaten und Stundenbuchungen.

    \n+

    CSV-Import

    Importiert Benutzerdaten aus einer CSV-Datei mit den Spalten firstname, lastname, email, role und hours_worked.

    Beispiel-Kopfzeile: firstname,lastname,email,role,hours_worked
    \n+

    Arbeitsstunden zurücksetzen

    Setzt alle gebuchten Arbeitsstunden für alle Mitglieder auf 0. Diese Aktion ist für den Jahreswechsel gedacht.

    \n*** End Patch" +} +``` + +**Output:** +``` +Success. Updated the following files: +M Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/views.php +``` + +--- + +## Assistant (Build · GPT-5.4 · 7.9s) + +Erledigt. + +In der `Vereinskonfiguration` sind die Exportfunktionen jetzt in einer eigenen Box: +- `Exporte` + +Dort liegen jetzt: +- `Benutzerdaten als CSV exportieren` +- `Stundenbuchungen als CSV exportieren` + +Die übrigen Bereiche sind getrennt: +- `Vereinswerte` +- `CSV-Import` +- `Arbeitsstunden zurücksetzen` + +Geändert in: +- `httpdocs/app/views.php` + +--- + +## User + +für den Informationskasten im Dashboard in der VEreinskonfiguration auch einen eigene Box + +--- + +## Assistant (Build · GPT-5.4 · 7.6s) + +Ich trenne jetzt auch den Dashboard-Informationskasten in der Vereinskonfiguration in eine eigene Box vom Block `Vereinswerte`. + +**Tool: read** + +**Input:** +```json +{ + "filePath": "/Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/views.php", + "offset": 636, + "limit": 18 +} +``` + +**Output:** +``` +/Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/views.php +file + +636:
    +637:
    +638:

    Vereinswerte

    Unterstützt Absätze, - Listen, *kursiv* und **fett**.
    +639:

    Exporte

    Exportfunktionen für Benutzerdaten und Stundenbuchungen.

    +640:

    CSV-Import

    Importiert Benutzerdaten aus einer CSV-Datei mit den Spalten firstname, lastname, email, role und hours_worked.

    Beispiel-Kopfzeile: firstname,lastname,email,role,hours_worked
    +641:

    Arbeitsstunden zurücksetzen

    Setzt alle gebuchten Arbeitsstunden für alle Mitglieder auf 0. Diese Aktion ist für den Jahreswechsel gedacht.

    +642:
    +643:

    Logansicht


    • :
    • Keine Logeinträge vorhanden.
    1): ?>
    Zurück
    Seite von , insgesamt Einträge
    Weiter
    +644:
    +645: +646: +647: +648: +649: +650: +651: +652: +``` + +--- + +## Assistant (Build · GPT-5.4 · 29.6s) + +Ich ziehe das Textfeld aus `Vereinswerte` heraus und gebe ihm eine eigene Box `Informationskasten im Dashboard`. + +**Tool: apply_patch** + +**Input:** +```json +{ + "patchText": "*** Begin Patch\n*** Update File: /Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/views.php\n@@\n-

    Vereinswerte

    \">
    \">
    Unterstützt Absätze, - Listen, *kursiv* und **fett**.
    \n+

    Vereinswerte

    \">
    \">
    \n+

    Informationskasten im Dashboard

    \">\">
    Unterstützt Absätze, - Listen, *kursiv* und **fett**.
    \n*** End Patch" +} +``` + +**Output:** +``` +Success. Updated the following files: +M Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/views.php +``` + +--- + +## Assistant (Build · GPT-5.4 · 14.0s) + +Erledigt. + +Der `Informationskasten im Dashboard` hat jetzt in der `Vereinskonfiguration` eine eigene Box. + +Aufteilung jetzt: +- `Vereinswerte` +- `Informationskasten im Dashboard` +- `Exporte` +- `CSV-Import` +- `Arbeitsstunden zurücksetzen` +- `Logansicht` + +Geändert in: +- `httpdocs/app/views.php` + +Wichtiger technischer Hinweis: +- Die neue Info-Box speichert weiterhin über `action=update_settings` +- Damit Pflichtstunden und Stundenwert beim separaten Speichern nicht verloren gehen, werden diese Werte als Hidden Fields mitgegeben + +--- + +## User + +bei der Box Vereinskonfiguration ist direkt darunter eine Linie, diese wird nicht benötigt + +--- + +## Assistant (Build · GPT-5.4 · 12.0s) + +Ich entferne die unnötige Trennlinie unter der Überschrift der Vereinskonfiguration, indem ich den unteren Padding des ersten Card-Body wieder normalisiere. + +**Tool: apply_patch** + +**Input:** +```json +{ + "patchText": "*** Begin Patch\n*** Update File: /Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/views.php\n@@\n-
    \n-

    Vereinskonfiguration

    \n+
    \n+

    Vereinskonfiguration

    \n
    \n*** End Patch" +} +``` + +**Output:** +``` +Success. Updated the following files: +M Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/views.php +``` + +--- + +## Assistant (Build · GPT-5.4 · 8.0s) + +Erledigt. + +Die unnötige Linie direkt unter `Vereinskonfiguration` ist entfernt. + +Geändert in: +- `httpdocs/app/views.php` + +--- + +## User + +ich sehe noch eine Trennlinie, die jetzt aber etwas tiefer dargestellt wird wie bisher + +--- + +## Assistant (Build · GPT-5.4 · 8.5s) + +Ich prüfe den Admin-Container und entferne die doppelte Card-Body-Struktur, die Tabler dort als Trennlinie zwischen den Bereichen rendert. + +**Tool: read** + +**Input:** +```json +{ + "filePath": "/Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/views.php", + "offset": 634, + "limit": 14 +} +``` + +**Output:** +``` +/Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/views.php +file + +634:
    +635:

    Vereinskonfiguration

    +636:
    +637:
    +638:

    Vereinswerte

    +639:

    Informationskasten im Dashboard

    Unterstützt Absätze, - Listen, *kursiv* und **fett**.
    +640:

    Exporte

    Exportfunktionen für Benutzerdaten und Stundenbuchungen.

    +641:

    CSV-Import

    Importiert Benutzerdaten aus einer CSV-Datei mit den Spalten firstname, lastname, email, role und hours_worked.

    Beispiel-Kopfzeile: firstname,lastname,email,role,hours_worked
    +642:

    Arbeitsstunden zurücksetzen

    Setzt alle gebuchten Arbeitsstunden für alle Mitglieder auf 0. Diese Aktion ist für den Jahreswechsel gedacht.

    +643:
    +644:

    Logansicht


    • :
    • Keine Logeinträge vorhanden.
    1): ?>
    Zurück
    Seite von , insgesamt Einträge
    Weiter
    +645:
    +646:
    +647: + +(Showing lines 634-647 of 654. Use offset=648 to continue.) +
    +``` + +--- + +## Assistant (Build · GPT-5.4 · 13.4s) + +Ich fasse Überschrift und Inhalt in einen einzigen `card-body` zusammen, damit Tabler keine Trennlinie zwischen zwei Card-Bodies rendert. + +**Tool: apply_patch** + +**Input:** +```json +{ + "patchText": "*** Begin Patch\n*** Update File: /Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/views.php\n@@\n-
    \n-

    Vereinskonfiguration

    \n-
    \n+
    \n+
    \n+

    Vereinskonfiguration

    \n
    \n@@\n-
    \n+
    \n
    \n*** End Patch" +} +``` + +**Output:** +``` +Success. Updated the following files: +M Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/views.php +``` + +--- + +## Assistant (Build · GPT-5.4 · 8.0s) + +Ich habe die Ursache entfernt. + +Problem: +- Die äußere Karte `Vereinskonfiguration` hatte zwei direkte `card-body`-Blöcke. +- Tabler rendert dazwischen eine Trennlinie. + +Fix: +- Überschrift und Inhalt liegen jetzt im selben `card-body`. + +Geändert in: +- `httpdocs/app/views.php` + +Damit sollte die Linie jetzt komplett weg sein. + +--- + +## User + +kannst du versuchen alle Buttons in der Vereinskonfiguration auf eine eiheitliche Größe zu bekommen + +--- + +## Assistant (Build · GPT-5.4 · 10.4s) + +Ich vereinheitliche jetzt die Button-Größen in der Vereinskonfiguration per gemeinsamer CSS-Regel, statt jeden Button einzeln weiter anzufassen. + +**Tool: grep** + +**Input:** +```json +{ + "pattern": "btn-success|btn-primary|btn-outline-danger|btn-outline-secondary|btn-outline-primary", + "path": "/Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/views.php", + "include": "*.php" +} +``` + +**Output:** +``` +Found 33 matches +/Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/views.php: + Line 108: .btn-primary, + + Line 109: .btn-success { + + Line 114: .btn-primary:hover, + + Line 115: .btn-success:hover { + + Line 120: .btn-outline-secondary { + + Line 125: .app-main .btn-outline-secondary { + + Line 130: .app-main .btn-outline-secondary:hover { + + Line 136: .app-main .btn-outline-primary { + + Line 141: .app-main .btn-outline-primary:hover { + + Line 147: .app-main .btn-outline-danger { + + Line 152: .app-main .btn-outline-danger:hover { + + Line 380: [data-bs-theme="dark"] .app-main .btn-outline-secondary, + + Line 381: [data-bs-theme="dark"] .app-main .btn-outline-primary { + + Line 386: [data-bs-theme="dark"] .app-main .btn-outline-secondary:hover, + + Line 387: [data-bs-theme="dark"] .app-main .btn-outline-primary:hover { + + Line 475: + + Line 478:
    + + Line 502: + + Line 555: + + Line 559: + + Line 560:
    + + Line 601:

    Profil

    + + Line 602:

    Passwort ändern

    + + Line 611:

    Benutzer anlegen

    Benutzer werden mit dem Startpasswort mitglied123 angelegt.
    + + Line 615:

    Benutzerliste

    NachnameVornameE-MailRolleGeleistetAktionen
    h

    Stunden für Benutzer buchen

    + + Line 629:

    Stundenbuchungen

    DatumMitgliedGebucht vonStundenNotizAktionen
    h
    Keine Aktionen
    Keine Stundenbuchungen vorhanden.
    1): ?> + + Line 639:

    Informationskasten im Dashboard

    Unterstützt Absätze, - Listen, *kursiv* und **fett**.
    + + Line 640:

    Exporte

    Exportfunktionen für Benutzerdaten und Stundenbuchungen.

    + + Line 641:

    CSV-Import

    Importiert Benutzerdaten aus einer CSV-Datei mit den Spalten firstname, lastname, email, role und hours_worked.

    Beispiel-Kopfzeile: firstname,lastname,email,role,hours_worked
    + + Line 642:

    Arbeitsstunden zurücksetzen

    Setzt alle gebuchten Arbeitsstunden für alle Mitglieder auf 0. Diese Aktion ist für den Jahreswechsel gedacht.

    + + Line 644:
    + +``` + +**Tool: read** + +**Input:** +```json +{ + "filePath": "/Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/views.php", + "offset": 16, + "limit": 140 +} +``` + +**Output:** +``` +/Users/erik/Documents/DEV/arbeitsstunden/httpdocs/app/views.php +file + +16: