commit 7dbdd2dc5b97277e65c80260f25f0a5ad5a83426 Author: root Date: Wed Sep 9 08:23:02 2026 +0200 CoreCashbook V1.0.0 diff --git a/Dockerfile b/Dockerfile new file mode 100755 index 0000000..f2509b3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +# Wir nutzen ein schlankes, offizielles PHP 8.2 Image mit Apache +FROM php:8.2-apache + +# Aktiviere Apache Rewrite und SSL +RUN a2enmod rewrite ssl + +# ERLAUBE .htaccess OVERRIDES +RUN sed -i 's/AllowOverride None/AllowOverride All/g' /etc/apache2/apache2.conf + +# Installiere SQLite, PDO und OpenSSL +RUN apt-get update && apt-get install -y libsqlite3-dev openssl \ + && docker-php-ext-install pdo pdo_sqlite \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Generiere ein Self-Signed Zertifikat (Gebrandet auf Tessmann Digital) +# Die Pfade entsprechen genau denen, die Apache in der default-ssl.conf erwartet +RUN openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ + -keyout /etc/ssl/private/ssl-cert-snakeoil.key \ + -out /etc/ssl/certs/ssl-cert-snakeoil.pem \ + -subj "/C=DE/ST=Brandenburg/L=Gruenheide/O=Tessmann Digital/CN=localhost" + +# Aktiviere den virtuellen SSL-Host von Apache +RUN a2ensite default-ssl + +# Setze das Arbeitsverzeichnis +WORKDIR /var/www/html \ No newline at end of file diff --git a/data/core.sqlite b/data/core.sqlite new file mode 100644 index 0000000..12c7ad7 Binary files /dev/null and b/data/core.sqlite differ diff --git a/data/cron.log b/data/cron.log new file mode 100644 index 0000000..5d38e54 --- /dev/null +++ b/data/cron.log @@ -0,0 +1,16 @@ +Starte CoreCashbook Cronjob... +- Gebucht: Dedicated Server (30 EUR) +- Gebucht: Dedicated Server (95.32 EUR) +- Gebucht: Domain (12 EUR) +- Gebucht: Handy-Tarif (27 EUR) +- Gebucht: Netcup VPS (5.64 EUR) +Cronjob beendet. Es wurden 5 neue Buchungen erzeugt. +Starte CoreCashbook Cronjob... + +Fatal error: Uncaught PDOException: SQLSTATE[HY000]: General error: 1 table transactions has no column named is_paid in /var/www/html/cron.php:41 +Stack trace: +#0 /var/www/html/cron.php(41): PDO->prepare('INSERT INTO tra...') +#1 {main} + thrown in /var/www/html/cron.php on line 41 +Starte CoreCashbook Cronjob... +Cronjob beendet. Es wurden 0 neue Buchungen erzeugt. diff --git a/data/php_errors.log b/data/php_errors.log new file mode 100644 index 0000000..3b38c5e --- /dev/null +++ b/data/php_errors.log @@ -0,0 +1,8 @@ +[08-Sep-2026 14:04:36 UTC] Exception: Call to undefined function handleGetDashboard() in /var/www/html/router.php on line 23 +[08-Sep-2026 14:04:44 UTC] Exception: Call to undefined function handleGetTransactions() in /var/www/html/router.php on line 24 +[08-Sep-2026 14:59:28 UTC] Exception: Call to undefined function handleGetDashboard() in /var/www/html/router.php on line 23 +[08-Sep-2026 14:59:30 UTC] Exception: Call to undefined function handleGetTransactions() in /var/www/html/router.php on line 24 +[08-Sep-2026 15:46:39 UTC] Exception: Call to undefined function handleGetDashboard() in /var/www/html/router.php on line 23 +[08-Sep-2026 15:46:43 UTC] Exception: Call to undefined function handleGetTransactions() in /var/www/html/router.php on line 24 +[08-Sep-2026 15:48:20 UTC] Exception: Call to undefined function handleCreateTransaction() in /var/www/html/router.php on line 25 +[08-Sep-2026 15:48:33 UTC] Exception: Call to undefined function handleCreateTransaction() in /var/www/html/router.php on line 25 diff --git a/data/receipts/rechnung_6aa030a5ae2c3.pdf b/data/receipts/rechnung_6aa030a5ae2c3.pdf new file mode 100644 index 0000000..7778ec2 Binary files /dev/null and b/data/receipts/rechnung_6aa030a5ae2c3.pdf differ diff --git a/data/receipts/rechnung_6aa0315de4fa6.pdf b/data/receipts/rechnung_6aa0315de4fa6.pdf new file mode 100644 index 0000000..97607bf Binary files /dev/null and b/data/receipts/rechnung_6aa0315de4fa6.pdf differ diff --git a/data/receipts/rechnung_6aa031e34521d.pdf b/data/receipts/rechnung_6aa031e34521d.pdf new file mode 100644 index 0000000..8e5ddf3 Binary files /dev/null and b/data/receipts/rechnung_6aa031e34521d.pdf differ diff --git a/data/receipts/rechnung_6aa0327955014.pdf b/data/receipts/rechnung_6aa0327955014.pdf new file mode 100644 index 0000000..0716609 Binary files /dev/null and b/data/receipts/rechnung_6aa0327955014.pdf differ diff --git a/data/receipts/rechnung_6aa0efce2bcdd.pdf b/data/receipts/rechnung_6aa0efce2bcdd.pdf new file mode 100644 index 0000000..eb1f4bb Binary files /dev/null and b/data/receipts/rechnung_6aa0efce2bcdd.pdf differ diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100755 index 0000000..81427ad --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,13 @@ +version: '3.8' + +services: + coretemplate: + build: . + container_name: corecashbook_app + ports: + # Außen 8443, intern 443 (HTTPS) + - "8445:443" + volumes: + - ./src:/var/www/html + - ./data:/var/www/data + restart: unless-stopped diff --git a/src/.htaccess b/src/.htaccess new file mode 100755 index 0000000..307cde9 --- /dev/null +++ b/src/.htaccess @@ -0,0 +1,4 @@ +RewriteEngine On +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d +RewriteRule ^(.*)$ index.php [QSA,L] \ No newline at end of file diff --git a/src/app.html b/src/app.html new file mode 100644 index 0000000..ff4ac9b --- /dev/null +++ b/src/app.html @@ -0,0 +1,476 @@ + + + + + + CoreCashbook + + + + + + + + + + + +
+

CoreCashbook

+
Login fehlgeschlagen.
+
+
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/src/auth.php b/src/auth.php new file mode 100755 index 0000000..50ff48c --- /dev/null +++ b/src/auth.php @@ -0,0 +1,147 @@ +prepare("SELECT * FROM users WHERE username = ?"); + $stmt->execute([$username]); + $user = $stmt->fetch(); + + if ($user && password_verify($password, $user['password_hash'])) { + $_SESSION['user_id'] = $user['id']; + $_SESSION['username'] = $user['username']; + $_SESSION['role'] = $user['role']; + + $pdo->prepare("INSERT INTO audit_logs (user_id, username, action, target) VALUES (?, ?, ?, ?)") + ->execute([$user['id'], $user['username'], 'User Login', $_SERVER['REMOTE_ADDR']]); + echo json_encode(['success' => true]); + } else { + echo json_encode(['success' => false]); + } + exit; +} + +function handleLogout() { + global $pdo; @session_start(); + if (isset($_SESSION['user_id'])) { + $pdo->prepare("INSERT INTO audit_logs (user_id, username, action, target) VALUES (?, ?, ?, ?)") + ->execute([$_SESSION['user_id'], $_SESSION['username'], 'User Logout', 'System']); + } + session_destroy(); + echo json_encode(['success' => true]); + exit; +} + +function handleGetLogs() { + global $pdo; @session_start(); + if (($_SESSION['role'] ?? '') !== 'admin') { http_response_code(403); exit; } + $stmt = $pdo->query("SELECT * FROM audit_logs ORDER BY id DESC LIMIT 50"); + echo json_encode(['success' => true, 'data' => $stmt->fetchAll()]); exit; +} + +function handleGetSettings() { + global $pdo; @session_start(); + if (($_SESSION['role'] ?? '') !== 'admin') { http_response_code(403); exit; } + $stmt = $pdo->query("SELECT * FROM settings ORDER BY config_key ASC"); + echo json_encode(['success' => true, 'data' => $stmt->fetchAll()]); exit; +} + +function handleSaveSettings() { + global $pdo; @session_start(); + if (($_SESSION['role'] ?? '') !== 'admin') { http_response_code(403); exit; } + + $key = trim($_POST['config_key'] ?? ''); + $value = trim($_POST['config_value'] ?? ''); + if ($key === '') { echo json_encode(['success' => false, 'error' => 'Schlüssel darf nicht leer sein.']); exit; } + + $pdo->prepare("REPLACE INTO settings (config_key, config_value) VALUES (?, ?)")->execute([$key, $value]); + $pdo->prepare("INSERT INTO audit_logs (user_id, username, action, target) VALUES (?, ?, ?, ?)") + ->execute([$_SESSION['user_id'], $_SESSION['username'], 'Setting Update', "Key: $key"]); + + echo json_encode(['success' => true]); exit; +} + +function handleDeleteSetting() { + global $pdo; @session_start(); + if (($_SESSION['role'] ?? '') !== 'admin') { http_response_code(403); exit; } + $key = trim($_POST['config_key'] ?? ''); + $pdo->prepare("DELETE FROM settings WHERE config_key = ?")->execute([$key]); + $pdo->prepare("INSERT INTO audit_logs (user_id, username, action, target) VALUES (?, ?, ?, ?)") + ->execute([$_SESSION['user_id'], $_SESSION['username'], 'Setting gelöscht', "Key: $key"]); + echo json_encode(['success' => true]); exit; +} + +function handleChangePassword() { + global $pdo; @session_start(); + $userId = $_SESSION['user_id'] ?? 0; + if (!$userId) { http_response_code(401); exit; } + + $oldPass = $_POST['old_password'] ?? ''; + $newPass = $_POST['new_password'] ?? ''; + + $stmt = $pdo->prepare("SELECT password_hash FROM users WHERE id = ?"); + $stmt->execute([$userId]); + $user = $stmt->fetch(); + + if ($user && password_verify($oldPass, $user['password_hash'])) { + if (strlen($newPass) < 4) { echo json_encode(['success' => false, 'error' => 'Min 4 Zeichen.']); exit; } + $newHash = password_hash($newPass, PASSWORD_DEFAULT); + + $pdo->prepare("UPDATE users SET password_hash = ? WHERE id = ?")->execute([$newHash, $userId]); + $pdo->prepare("INSERT INTO audit_logs (user_id, username, action, target) VALUES (?, ?, ?, ?)") + ->execute([$userId, $_SESSION['username'], 'Passwort geändert', 'Self-Service']); + + echo json_encode(['success' => true]); + } else { + echo json_encode(['success' => false, 'error' => 'Altes Passwort falsch.']); + } + exit; +} + +function handleGetUsers() { + global $pdo; @session_start(); + if (($_SESSION['role'] ?? '') !== 'admin') { http_response_code(403); exit; } + $stmt = $pdo->query("SELECT id, username, role FROM users ORDER BY id ASC"); + echo json_encode(['success' => true, 'data' => $stmt->fetchAll()]); exit; +} + +function handleCreateUser() { + global $pdo; @session_start(); + if (($_SESSION['role'] ?? '') !== 'admin') { http_response_code(403); exit; } + + $username = trim($_POST['username'] ?? ''); + $password = $_POST['password'] ?? ''; + $role = $_POST['role'] === 'admin' ? 'admin' : 'user'; + + try { + $hash = password_hash($password, PASSWORD_DEFAULT); + $pdo->prepare("INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)")->execute([$username, $hash, $role]); + $pdo->prepare("INSERT INTO audit_logs (user_id, username, action, target) VALUES (?, ?, ?, ?)") + ->execute([$_SESSION['user_id'], $_SESSION['username'], 'User erstellt', "User: $username"]); + echo json_encode(['success' => true]); + } catch (PDOException $e) { + echo json_encode(['success' => false, 'error' => 'Fehler (evtl. existiert der Name schon).']); + } + exit; +} + +function handleDeleteUser() { + global $pdo; @session_start(); + if (($_SESSION['role'] ?? '') !== 'admin') { http_response_code(403); exit; } + + $deleteId = $_POST['id'] ?? 0; + + // Selbstschutz: Man darf sich nicht selbst löschen! + if ($deleteId == $_SESSION['user_id']) { + echo json_encode(['success' => false, 'error' => 'Du kannst dich nicht selbst löschen.']); + exit; + } + + $pdo->prepare("DELETE FROM users WHERE id = ?")->execute([$deleteId]); + $pdo->prepare("INSERT INTO audit_logs (user_id, username, action, target) VALUES (?, ?, ?, ?)") + ->execute([$_SESSION['user_id'], $_SESSION['username'], 'User gelöscht', "User ID: $deleteId"]); + + echo json_encode(['success' => true]); exit; +} \ No newline at end of file diff --git a/src/cashbook.php b/src/cashbook.php new file mode 100644 index 0000000..9349eeb --- /dev/null +++ b/src/cashbook.php @@ -0,0 +1,256 @@ +query("SELECT DISTINCT category FROM transactions WHERE category IS NOT NULL AND category != '' ORDER BY category ASC"); + $categories = $stmt->fetchAll(PDO::FETCH_COLUMN); + echo json_encode(['success' => true, 'data' => $categories]); exit; +} + +function handleGetDashboard() { + global $pdo; isAuthenticated(); + $category = $_GET['category'] ?? ''; + + // Nur BEZAHLTE Buchungen fließen ins Dashboard ein + if ($category !== '') { + $stmtBal = $pdo->prepare("SELECT SUM(CASE WHEN type = 'income' THEN amount ELSE -amount END) FROM transactions WHERE category = ? AND is_paid = 1"); + $stmtBal->execute([$category]); + $balance = $stmtBal->fetchColumn() ?: 0.00; + + $stmtChart = $pdo->prepare("SELECT strftime('%Y-%m', date) as month, type, SUM(amount) as total FROM transactions WHERE category = ? AND is_paid = 1 GROUP BY month, type ORDER BY month ASC"); + $stmtChart->execute([$category]); + $chartRaw = $stmtChart->fetchAll(); + } else { + $stmtBal = $pdo->query("SELECT SUM(CASE WHEN type = 'income' THEN amount ELSE -amount END) FROM transactions WHERE is_paid = 1"); + $balance = $stmtBal->fetchColumn() ?: 0.00; + + $stmtChart = $pdo->query("SELECT strftime('%Y-%m', date) as month, type, SUM(amount) as total FROM transactions WHERE is_paid = 1 GROUP BY month, type ORDER BY month ASC"); + $chartRaw = $stmtChart->fetchAll(); + } + + $chartData = []; + foreach ($chartRaw as $row) { + $m = $row['month']; + if (!isset($chartData[$m])) $chartData[$m] = ['income' => 0, 'expense' => 0]; + $chartData[$m][$row['type']] = (float)$row['total']; + } + + echo json_encode(['success' => true, 'balance' => $balance, 'chart' => $chartData]); exit; +} + +function handleGetTransactions() { + global $pdo; isAuthenticated(); + $category = $_GET['category'] ?? ''; + + if ($category !== '') { + $stmt = $pdo->prepare("SELECT * FROM transactions WHERE category = ? ORDER BY date DESC, id DESC"); + $stmt->execute([$category]); + } else { + $stmt = $pdo->query("SELECT * FROM transactions ORDER BY date DESC, id DESC"); + } + + echo json_encode(['success' => true, 'data' => $stmt->fetchAll()]); exit; +} + +function handleCreateTransaction() { + global $pdo; isAuthenticated(); + + $title = trim($_POST['title'] ?? ''); + $company = trim($_POST['company'] ?? ''); + $description = trim($_POST['description'] ?? ''); + $amount = (float)str_replace(',', '.', $_POST['amount'] ?? 0); + $type = $_POST['type'] ?? 'expense'; + $date = $_POST['date'] ?? date('Y-m-d'); + $category = trim($_POST['category'] ?? ''); + $is_recurring = (int)($_POST['is_recurring'] ?? 0); + $frequency = $_POST['frequency'] ?? ''; + $is_paid = (int)($_POST['is_paid'] ?? 1); // NEU + + if ($title === '' || $amount <= 0) { echo json_encode(['success' => false, 'error' => 'Titel und Betrag erforderlich.']); exit; } + + $receiptPath = null; + if (isset($_FILES['receipt']) && $_FILES['receipt']['error'] === UPLOAD_ERR_OK) { + if ($_FILES['receipt']['type'] === 'application/pdf') { + $uploadDir = '/var/www/data/receipts/'; + if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true); + $filename = uniqid('rechnung_') . '.pdf'; + if (move_uploaded_file($_FILES['receipt']['tmp_name'], $uploadDir . $filename)) { + $receiptPath = $filename; + } + } else { + echo json_encode(['success' => false, 'error' => 'Nur PDF-Dateien sind erlaubt.']); exit; + } + } + + $pdo->prepare("INSERT INTO transactions (title, amount, type, date, category, is_recurring, frequency, receipt_path, company, description, is_paid) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") + ->execute([$title, $amount, $type, $date, $category, $is_recurring, $frequency, $receiptPath, $company, $description, $is_paid]); + + echo json_encode(['success' => true]); exit; +} + +function handleEditTransaction() { + global $pdo; isAuthenticated(); + + $id = (int)($_POST['id'] ?? 0); + $title = trim($_POST['title'] ?? ''); + $company = trim($_POST['company'] ?? ''); + $description = trim($_POST['description'] ?? ''); + $amount = (float)str_replace(',', '.', $_POST['amount'] ?? 0); + $type = $_POST['type'] ?? 'expense'; + $date = $_POST['date'] ?? date('Y-m-d'); + $category = trim($_POST['category'] ?? ''); + $is_recurring = (int)($_POST['is_recurring'] ?? 0); + $frequency = $_POST['frequency'] ?? ''; + $is_paid = (int)($_POST['is_paid'] ?? 1); // NEU + $removeReceipt = (int)($_POST['remove_receipt'] ?? 0); + + if ($id <= 0 || $title === '' || $amount <= 0) { echo json_encode(['success' => false, 'error' => 'Ungültige Daten.']); exit; } + + $stmt = $pdo->prepare("SELECT receipt_path FROM transactions WHERE id = ?"); + $stmt->execute([$id]); + $tx = $stmt->fetch(); + if (!$tx) { echo json_encode(['success' => false, 'error' => 'Buchung nicht gefunden.']); exit; } + + $receiptPath = $tx['receipt_path']; + + if ($removeReceipt === 1 && !empty($receiptPath)) { + $file = '/var/www/data/receipts/' . $receiptPath; + if (file_exists($file)) unlink($file); + $receiptPath = null; + } + + if (isset($_FILES['receipt']) && $_FILES['receipt']['error'] === UPLOAD_ERR_OK) { + if ($_FILES['receipt']['type'] === 'application/pdf') { + if (!empty($receiptPath)) { + $oldFile = '/var/www/data/receipts/' . $receiptPath; + if (file_exists($oldFile)) unlink($oldFile); + } + $uploadDir = '/var/www/data/receipts/'; + if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true); + $filename = uniqid('rechnung_') . '.pdf'; + if (move_uploaded_file($_FILES['receipt']['tmp_name'], $uploadDir . $filename)) { + $receiptPath = $filename; + } + } else { + echo json_encode(['success' => false, 'error' => 'Nur PDF-Dateien sind erlaubt.']); exit; + } + } + + $pdo->prepare("UPDATE transactions SET title=?, amount=?, type=?, date=?, category=?, is_recurring=?, frequency=?, receipt_path=?, company=?, description=?, is_paid=? WHERE id=?") + ->execute([$title, $amount, $type, $date, $category, $is_recurring, $frequency, $receiptPath, $company, $description, $is_paid, $id]); + + echo json_encode(['success' => true]); exit; +} + +// NEU: Toggle Status +function handleTogglePaidTransaction() { + global $pdo; isAuthenticated(); + $id = (int)($_POST['id'] ?? 0); + $status = (int)($_POST['status'] ?? 0); + $pdo->prepare("UPDATE transactions SET is_paid = ? WHERE id = ?")->execute([$status, $id]); + echo json_encode(['success' => true]); exit; +} + +function handleGetReceipt() { + global $pdo; isAuthenticated(); + $id = $_GET['id'] ?? 0; + + $stmt = $pdo->prepare("SELECT receipt_path FROM transactions WHERE id = ?"); + $stmt->execute([$id]); + $tx = $stmt->fetch(); + + if ($tx && !empty($tx['receipt_path'])) { + $file = '/var/www/data/receipts/' . $tx['receipt_path']; + if (file_exists($file)) { + header('Content-Type: application/pdf'); + header('Content-Disposition: inline; filename="' . $tx['receipt_path'] . '"'); + readfile($file); + exit; + } + } + http_response_code(404); echo "Rechnung nicht gefunden."; exit; +} + +function handleDeleteTransaction() { + global $pdo; isAuthenticated(); + $id = $_POST['id'] ?? 0; + + $stmt = $pdo->prepare("SELECT receipt_path FROM transactions WHERE id = ?"); + $stmt->execute([$id]); + $tx = $stmt->fetch(); + if ($tx && !empty($tx['receipt_path'])) { + $file = '/var/www/data/receipts/' . $tx['receipt_path']; + if (file_exists($file)) unlink($file); + } + + $pdo->prepare("DELETE FROM transactions WHERE id = ?")->execute([$id]); + echo json_encode(['success' => true]); exit; +} + +function handleGetLogbook() { + global $pdo; isAuthenticated(); + $stmt = $pdo->query("SELECT * FROM logbook ORDER BY date DESC, id DESC"); + echo json_encode(['success' => true, 'data' => $stmt->fetchAll()]); exit; +} + +function handleCreateLogbook() { + global $pdo; isAuthenticated(); + + $date = $_POST['date'] ?? date('Y-m-d'); + $start = trim($_POST['start_location'] ?? ''); + $end = trim($_POST['end_location'] ?? ''); + $km = (float)str_replace(',', '.', $_POST['distance_km'] ?? 0); + $purpose = trim($_POST['purpose'] ?? ''); + + if ($start === '' || $end === '' || $km <= 0) { echo json_encode(['success' => false, 'error' => 'Alle Felder müssen ausgefüllt sein.']); exit; } + + $pdo->prepare("INSERT INTO logbook (date, start_location, end_location, distance_km, purpose) VALUES (?, ?, ?, ?, ?)") + ->execute([$date, $start, $end, $km, $purpose]); + + echo json_encode(['success' => true]); exit; +} + +function handleDeleteLogbook() { + global $pdo; isAuthenticated(); + $id = $_POST['id'] ?? 0; + $pdo->prepare("DELETE FROM logbook WHERE id = ?")->execute([$id]); + echo json_encode(['success' => true]); exit; +} + +function handleExportTransactions() { + global $pdo; isAuthenticated(); + header('Content-Type: text/csv; charset=utf-8'); + header('Content-Disposition: attachment; filename=CoreCashbook_Transaktionen_' . date('Y-m-d') . '.csv'); + $output = fopen('php://output', 'w'); + fputcsv($output, ['ID', 'Datum', 'Typ', 'Titel', 'Firma', 'Beschreibung', 'Kategorie', 'Betrag (EUR)', 'Status', 'Wiederkehrend', 'Intervall'], ';'); + $stmt = $pdo->query("SELECT id, date, type, title, company, description, category, amount, is_paid, is_recurring, frequency FROM transactions ORDER BY date DESC"); + while ($row = $stmt->fetch()) { + $row['amount'] = number_format($row['amount'], 2, ',', ''); + $row['type'] = $row['type'] === 'income' ? 'Einnahme' : 'Ausgabe'; + $row['is_paid'] = $row['is_paid'] ? 'Bezahlt' : 'Offen'; + $row['is_recurring'] = $row['is_recurring'] ? 'Ja' : 'Nein'; + fputcsv($output, $row, ';'); + } + fclose($output); exit; +} + +function handleExportLogbook() { + global $pdo; isAuthenticated(); + header('Content-Type: text/csv; charset=utf-8'); + header('Content-Disposition: attachment; filename=CoreCashbook_Fahrtenbuch_' . date('Y-m-d') . '.csv'); + $output = fopen('php://output', 'w'); + fputcsv($output, ['ID', 'Datum', 'Start', 'Ziel', 'Distanz (km)', 'Zweck'], ';'); + $stmt = $pdo->query("SELECT id, date, start_location, end_location, distance_km, purpose FROM logbook ORDER BY date DESC"); + while ($row = $stmt->fetch()) { + $row['distance_km'] = number_format($row['distance_km'], 1, ',', ''); + fputcsv($output, $row, ';'); + } + fclose($output); exit; +} +?> \ No newline at end of file diff --git a/src/cron.php b/src/cron.php new file mode 100644 index 0000000..91a2280 --- /dev/null +++ b/src/cron.php @@ -0,0 +1,72 @@ +query(" + SELECT t1.* FROM transactions t1 + JOIN (SELECT MAX(id) as max_id FROM transactions WHERE is_recurring = 1 GROUP BY title) t2 + ON t1.id = t2.max_id +"); +$templates = $stmt->fetchAll(); +$count = 0; + +foreach ($templates as $tpl) { + $tplTime = strtotime($tpl['date']); + $tplDay = date('d', $tplTime); + $tplMonth = date('m', $tplTime); + + $y = $currentYear; + $m = $currentMonth; + if ($tpl['frequency'] === 'yearly') { + $m = $tplMonth; + } + + // Tag-Korrektur (z.B. den 31. gibt es im Februar nicht) + $d = $tplDay; + if (!checkdate($m, $d, $y)) { + $d = date('t', strtotime("$y-$m-01")); + } + + $targetDate = sprintf('%04d-%02d-%02d', $y, $m, $d); + + // 1. Prüfen: Ist das Zieldatum heute oder vergangen? + if (strtotime($currentDateStr) < strtotime($targetDate)) { + continue; + } + + // 2. Prüfen: Gibt es diesen Monat schon EINE Buchung mit diesem Titel? + $targetYearMonth = sprintf('%04d-%02d', $y, $m); + $likeStr = $targetYearMonth . '-%'; + $checkStmt = $pdo->prepare("SELECT COUNT(*) FROM transactions WHERE title = ? AND date LIKE ?"); + $checkStmt->execute([$tpl['title'], $likeStr]); + $exists = $checkStmt->fetchColumn(); + + if ($exists == 0) { + // Neu anlegen! Wiederkehrend bleibt 1 (wie gewünscht), Status auf 0 (Offen) + $insert = $pdo->prepare("INSERT INTO transactions (title, company, description, amount, type, date, category, is_recurring, frequency, is_paid) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0)"); + $insert->execute([ + $tpl['title'], + $tpl['company'], + $tpl['description'], + $tpl['amount'], + $tpl['type'], + $targetDate, + $tpl['category'], + $tpl['is_recurring'], + $tpl['frequency'] + ]); + + echo "- Gebucht: " . $tpl['title'] . " für den $targetDate (" . $tpl['amount'] . " EUR)\n"; + $count++; + } +} +echo "Cronjob beendet. Es wurden $count neue Buchungen erzeugt.\n"; +?> \ No newline at end of file diff --git a/src/database.php b/src/database.php new file mode 100644 index 0000000..1e6477b --- /dev/null +++ b/src/database.php @@ -0,0 +1,52 @@ +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); + + $pdo->exec("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, role TEXT DEFAULT 'user')"); + $pdo->exec("CREATE TABLE IF NOT EXISTS audit_logs (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, username TEXT, action TEXT, target TEXT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP)"); + $pdo->exec("CREATE TABLE IF NOT EXISTS settings (config_key TEXT PRIMARY KEY, config_value TEXT NOT NULL)"); + + $pdo->exec("CREATE TABLE IF NOT EXISTS transactions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + type TEXT NOT NULL, + title TEXT NOT NULL, + amount REAL NOT NULL, + date DATE NOT NULL, + category TEXT, + is_recurring INTEGER DEFAULT 0, + frequency TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + )"); + + // Dynamische Spalten + try { $pdo->exec("ALTER TABLE transactions ADD COLUMN receipt_path TEXT"); } catch (PDOException $e) {} + try { $pdo->exec("ALTER TABLE transactions ADD COLUMN company TEXT"); } catch (PDOException $e) {} + try { $pdo->exec("ALTER TABLE transactions ADD COLUMN description TEXT"); } catch (PDOException $e) {} + try { $pdo->exec("ALTER TABLE transactions ADD COLUMN is_paid INTEGER DEFAULT 1"); } catch (PDOException $e) {} // NEU + + $pdo->exec("CREATE TABLE IF NOT EXISTS logbook ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + date DATE NOT NULL, + start_location TEXT NOT NULL, + end_location TEXT NOT NULL, + distance_km REAL NOT NULL, + purpose TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + )"); + + $stmt = $pdo->query("SELECT COUNT(*) FROM users"); + if ($stmt->fetchColumn() == 0) { + $hash = password_hash('admin', PASSWORD_DEFAULT); + $insert = $pdo->prepare("INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)"); + $insert->execute(['admin', $hash, 'admin']); + } + +} catch (PDOException $e) { + die(json_encode(['success' => false, 'error' => 'Datenbankfehler: ' . $e->getMessage()])); +} +?> \ No newline at end of file diff --git a/src/index.php b/src/index.php new file mode 100644 index 0000000..ec4cace --- /dev/null +++ b/src/index.php @@ -0,0 +1,33 @@ +getMessage() . " in " . $e->getFile() . " on line " . $e->getLine()); + $request = $_SERVER['REQUEST_URI'] ?? ''; + if (strpos($request, '/api/') === 0) { + http_response_code(500); + header('Content-Type: application/json'); + echo json_encode(['success' => false, 'error' => 'Interner Systemfehler. Bitte Logs prüfen.']); + } else { + http_response_code(500); + echo "

500 - Systemfehler

"; + } + exit; +}); + +header('Content-Type: application/json'); +require_once 'database.php'; +require_once 'auth.php'; +require_once 'cashbook.php'; // <--- HIER IST DER FIX +require_once 'router.php'; +?> \ No newline at end of file diff --git a/src/router.php b/src/router.php new file mode 100644 index 0000000..11a7337 --- /dev/null +++ b/src/router.php @@ -0,0 +1,45 @@ + true, 'status' => 'online', 'system' => 'CoreCashbook']); +} elseif ($path === '/api/logs' && $method === 'GET') { handleGetLogs(); +} elseif ($path === '/api/settings' && $method === 'GET') { handleGetSettings(); +} elseif ($path === '/api/settings' && $method === 'POST') { handleSaveSettings(); +} elseif ($path === '/api/settings/delete' && $method === 'POST') { handleDeleteSetting(); +} elseif ($path === '/api/password' && $method === 'POST') { handleChangePassword(); +} elseif ($path === '/api/users' && $method === 'GET') { handleGetUsers(); +} elseif ($path === '/api/users' && $method === 'POST') { handleCreateUser(); +} elseif ($path === '/api/users/delete' && $method === 'POST') { handleDeleteUser(); +// --- CORECASHBOOK ROUTEN --- +} elseif ($path === '/api/cashbook/dashboard' && $method === 'GET') { handleGetDashboard(); +} elseif ($path === '/api/cashbook/categories' && $method === 'GET') { handleGetCategories(); +} elseif ($path === '/api/cashbook/transactions' && $method === 'GET') { handleGetTransactions(); +} elseif ($path === '/api/cashbook/transactions' && $method === 'POST') { handleCreateTransaction(); +} elseif ($path === '/api/cashbook/transactions/edit' && $method === 'POST') { handleEditTransaction(); +} elseif ($path === '/api/cashbook/transactions/toggle-paid' && $method === 'POST') { handleTogglePaidTransaction(); // NEU +} elseif ($path === '/api/cashbook/transactions/delete' && $method === 'POST') { handleDeleteTransaction(); +} elseif ($path === '/api/cashbook/receipt' && $method === 'GET') { handleGetReceipt(); +} elseif ($path === '/api/cashbook/logbook' && $method === 'GET') { handleGetLogbook(); +} elseif ($path === '/api/cashbook/logbook' && $method === 'POST') { handleCreateLogbook(); +} elseif ($path === '/api/cashbook/logbook/delete' && $method === 'POST') { handleDeleteLogbook(); +} elseif ($path === '/api/cashbook/export/transactions' && $method === 'GET') { handleExportTransactions(); +} elseif ($path === '/api/cashbook/export/logbook' && $method === 'GET') { handleExportLogbook(); +} elseif ($path === '/api/me' && $method === 'GET') { + @session_start(); + if (isset($_SESSION['user_id'])) { echo json_encode(['success' => true, 'username' => $_SESSION['username']]); } + else { http_response_code(401); echo json_encode(['success' => false, 'error' => 'Not authenticated']); } + exit; +} else { + http_response_code(404); + echo json_encode(['success' => false, 'error' => 'Endpoint not found']); +} +?> \ No newline at end of file