CoreCashbook V1.0.0
This commit is contained in:
commit
7dbdd2dc5b
18 files changed
+1149
No files matched your search
Executable
+27
@@ -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
|
||||||
Binary file not shown.
@@ -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.
|
||||||
@@ -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
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Executable
+13
@@ -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
|
||||||
Executable
+4
@@ -0,0 +1,4 @@
|
|||||||
|
RewriteEngine On
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteRule ^(.*)$ index.php [QSA,L]
|
||||||
+476
@@ -0,0 +1,476 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>CoreCashbook</title>
|
||||||
|
<!-- Chart.js CDN -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg-color: #12141c; --surface-color: #1e212b; --surface-hover: #2a2e3d;
|
||||||
|
--text-main: #ffffff; --text-muted: #8b92a5;
|
||||||
|
--accent-blue: #3b82f6; --accent-red: #ef4444; --accent-green: #10b981; --accent-orange: #f59e0b;
|
||||||
|
}
|
||||||
|
body { background-color: var(--bg-color); color: var(--text-main); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; display: flex; justify-content: center; align-items: flex-start; min-height: 100vh; margin: 0; padding: 20px; box-sizing: border-box; }
|
||||||
|
.container { background-color: var(--surface-color); padding: 2rem; border-radius: 12px; box-shadow: 0 10px 25px rgba(0,0,0,0.5); width: 100%; max-width: 400px; margin-top: 10vh; }
|
||||||
|
.dashboard-container { max-width: 1400px; width: 100%; margin-top: 0; }
|
||||||
|
h1, h2, h3 { margin-top: 0; } h1 { font-size: 1.5rem; text-align: center; }
|
||||||
|
.form-group { margin-bottom: 1rem; } label { display: block; margin-bottom: 0.5rem; color: var(--text-muted); font-size: 0.9rem; }
|
||||||
|
input, select { width: 100%; box-sizing: border-box; padding: 0.75rem; background-color: var(--bg-color); border: 1px solid var(--surface-hover); color: white; border-radius: 6px; outline: none; }
|
||||||
|
input:focus, select:focus { border-color: var(--accent-blue); }
|
||||||
|
button { width: 100%; padding: 0.75rem; background-color: var(--accent-blue); color: white; border: none; border-radius: 6px; font-weight: bold; cursor: pointer; transition: opacity 0.2s; }
|
||||||
|
button:hover { opacity: 0.9; }
|
||||||
|
.error-msg { color: var(--accent-red); font-size: 0.85rem; text-align: center; margin-bottom: 1rem; display: none; }
|
||||||
|
.hidden { display: none !important; }
|
||||||
|
|
||||||
|
.panel { background-color: var(--bg-color); border-radius: 8px; padding: 1.5rem; margin-top: 1.5rem; overflow-x: auto; }
|
||||||
|
table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
|
||||||
|
th, td { padding: 0.75rem; text-align: left; border-bottom: 1px solid var(--surface-hover); }
|
||||||
|
th { color: var(--text-muted); text-transform: uppercase; font-size: 0.75rem; }
|
||||||
|
tr:hover { background-color: rgba(255,255,255,0.02); }
|
||||||
|
.btn-delete { background: transparent; color: var(--accent-red); border: none; padding: 0; width: auto; font-size: 1.1rem; cursor: pointer; }
|
||||||
|
.btn-edit { background: transparent; color: var(--accent-blue); border: none; padding: 0; width: auto; font-size: 1.1rem; cursor: pointer; margin-right: 5px; }
|
||||||
|
.btn-export { background-color: var(--surface-hover); border: 1px solid var(--text-muted); color: var(--text-main); width: auto; padding: 0.5rem 1rem; text-decoration: none; display: inline-block; border-radius: 6px; font-size: 0.85rem; cursor: pointer;}
|
||||||
|
|
||||||
|
.balance-card { background: rgba(59, 130, 246, 0.1); border-left: 4px solid var(--accent-blue); padding: 1.5rem; border-radius: 8px; margin-bottom: 1.5rem; text-align: center; }
|
||||||
|
.balance-amount { font-size: 2.5rem; font-weight: bold; margin: 10px 0; }
|
||||||
|
.text-green { color: var(--accent-green); } .text-red { color: var(--accent-red); }
|
||||||
|
|
||||||
|
.modal-overlay { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.85); z-index: 2000; display: flex; justify-content: center; align-items: center; }
|
||||||
|
.modal-content { background: var(--surface-color); width: 95%; max-width: 1000px; height: 90vh; border-radius: 12px; display: flex; flex-direction: column; padding: 1.5rem; box-shadow: 0 10px 30px rgba(0,0,0,0.8); box-sizing: border-box;}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="pdfModal" class="modal-overlay hidden">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; border-bottom: 1px solid var(--surface-hover); padding-bottom: 1rem;">
|
||||||
|
<h2 style="margin:0;">Beleg Ansicht</h2>
|
||||||
|
<button onclick="document.getElementById('pdfModal').classList.add('hidden')" style="background: transparent; color: var(--text-muted); border: none; font-size: 1.5rem; cursor: pointer; padding: 0; width: auto;">✖</button>
|
||||||
|
</div>
|
||||||
|
<iframe id="pdfFrame" style="flex-grow: 1; width: 100%; border: none; border-radius: 8px; background: white;" src=""></iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="editTxModal" class="modal-overlay hidden">
|
||||||
|
<div class="modal-content" style="max-width: 600px; height: auto;">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; border-bottom: 1px solid var(--surface-hover); padding-bottom: 1rem;">
|
||||||
|
<h2 style="margin:0;">Buchung bearbeiten</h2>
|
||||||
|
<button onclick="document.getElementById('editTxModal').classList.add('hidden')" style="background: transparent; color: var(--text-muted); border: none; font-size: 1.5rem; cursor: pointer; padding: 0; width: auto;">✖</button>
|
||||||
|
</div>
|
||||||
|
<form id="editTxForm" style="display:flex; flex-direction:column; gap:10px;">
|
||||||
|
<input type="hidden" id="editTxId">
|
||||||
|
<div style="display:flex; gap:10px;">
|
||||||
|
<select id="editTxType" style="flex:1;" required><option value="income">📈 Einnahme</option><option value="expense">📉 Ausgabe</option></select>
|
||||||
|
<input type="date" id="editTxDate" style="flex:1;" required>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex; gap:10px;">
|
||||||
|
<input type="text" id="editTxTitle" placeholder="Kurztitel" required style="flex:1;">
|
||||||
|
<input type="number" id="editTxAmount" placeholder="Betrag (€)" step="0.01" required style="flex:1;">
|
||||||
|
</div>
|
||||||
|
<div style="display:flex; gap:10px;">
|
||||||
|
<input type="text" id="editTxCompany" placeholder="Firma" style="flex:1;">
|
||||||
|
<input type="text" id="editTxCategory" placeholder="Kategorie" style="flex:1;">
|
||||||
|
</div>
|
||||||
|
<input type="text" id="editTxDescription" placeholder="Beschreibung / Artikel" style="width:100%;">
|
||||||
|
|
||||||
|
<div style="display:flex; gap:10px;">
|
||||||
|
<select id="editTxRecurring" style="flex:1;"><option value="0">Einmalig</option><option value="1">Wiederkehrend</option></select>
|
||||||
|
<select id="editTxFreq" style="flex:1;"><option value="">- Intervall -</option><option value="monthly">Monatlich</option><option value="yearly">Jährlich</option></select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label style="display:flex; align-items:center; gap:5px; margin-top:5px; cursor:pointer;"><input type="checkbox" id="editTxIsPaid" style="width:auto;"> Bereits bezahlt?</label>
|
||||||
|
|
||||||
|
<div style="background:rgba(255,255,255,0.05); padding:10px; border-radius:6px; margin-top:10px;">
|
||||||
|
<label>Beleg verwalten (PDF)</label>
|
||||||
|
<div id="currentReceiptInfo" style="margin-bottom:10px; font-size:0.9rem;"></div>
|
||||||
|
<label style="display:flex; align-items:center; gap:10px; cursor:pointer;"><input type="checkbox" id="editTxRemoveReceipt" style="width:auto;"> Aktuellen Beleg löschen</label>
|
||||||
|
<input type="file" id="editTxReceipt" accept="application/pdf" style="margin-top:10px; background: rgba(0,0,0,0.2);">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" style="margin-top:10px;">Änderungen speichern</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- LOGIN -->
|
||||||
|
<div class="container" id="loginView">
|
||||||
|
<h1>CoreCashbook</h1>
|
||||||
|
<div class="error-msg" id="loginError">Login fehlgeschlagen.</div>
|
||||||
|
<form id="loginForm">
|
||||||
|
<div class="form-group"><label>Benutzername</label><input type="text" id="username" required></div>
|
||||||
|
<div class="form-group"><label>Passwort</label><input type="password" id="password" required></div>
|
||||||
|
<button type="submit">Anmelden</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MAIN APP -->
|
||||||
|
<div class="container dashboard-container hidden" id="dashboardView">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--surface-hover); padding-bottom: 1rem; flex-wrap: wrap; gap: 10px;">
|
||||||
|
<h1 style="margin: 0;">CoreCashbook</h1>
|
||||||
|
<div style="display: flex; gap: 10px;">
|
||||||
|
<button onclick="toggleView('dashboard')" style="width:auto; background:transparent; border:1px solid var(--text-muted);">Dashboard</button>
|
||||||
|
<button onclick="toggleView('fibu')" style="width:auto; background:transparent; border:1px solid var(--text-muted);">FiBu</button>
|
||||||
|
<button onclick="toggleView('logbook')" style="width:auto; background:transparent; border:1px solid var(--text-muted);">Fahrtenbuch</button>
|
||||||
|
<button onclick="logout()" style="width:auto; background:var(--accent-red);">Logout</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="tabDashboard" class="panel">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
|
||||||
|
<h3 style="margin:0; color:var(--text-muted);">Dashboard (Nur bezahlte Buchungen)</h3>
|
||||||
|
<select id="filterDashboardCat" onchange="loadDashboard()" style="width:auto; min-width: 200px;">
|
||||||
|
<option value="">Alle Kategorien</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="balance-card">
|
||||||
|
<h3 style="margin:0; color:var(--text-muted);">Gewinn / Verlust (gefiltert)</h3>
|
||||||
|
<div class="balance-amount" id="displayBalance">0,00 €</div>
|
||||||
|
</div>
|
||||||
|
<div style="height: 300px; width: 100%;">
|
||||||
|
<canvas id="cashflowChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="tabFibu" class="panel hidden">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; flex-wrap: wrap; gap: 10px;">
|
||||||
|
<h3 style="margin:0;">Einnahmen & Ausgaben</h3>
|
||||||
|
<div style="display: flex; gap: 10px;">
|
||||||
|
<select id="filterFibuCat" onchange="loadTransactions()" style="width:auto; min-width: 200px;">
|
||||||
|
<option value="">Alle Kategorien</option>
|
||||||
|
</select>
|
||||||
|
<a href="/api/cashbook/export/transactions" class="btn-export" target="_blank">📄 CSV Export (FiBu)</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="txForm" style="display:flex; gap:10px; flex-wrap:wrap; background:rgba(0,0,0,0.2); padding:15px; border-radius:8px; margin-bottom: 1.5rem;">
|
||||||
|
<select id="txType" style="flex:1; min-width:120px;" required><option value="income">📈 Einnahme</option><option value="expense" selected>📉 Ausgabe</option></select>
|
||||||
|
<input type="date" id="txDate" style="flex:1; min-width:130px;" required>
|
||||||
|
<input type="text" id="txTitle" placeholder="Kurztitel" required style="flex:1; min-width:150px;">
|
||||||
|
<input type="text" id="txCompany" placeholder="Firma (z.B. Strato AG)" style="flex:1; min-width:150px;">
|
||||||
|
<input type="text" id="txDescription" placeholder="Beschreibung / Artikel" style="flex:2; min-width:200px;">
|
||||||
|
|
||||||
|
<input type="number" id="txAmount" placeholder="Betrag (€)" step="0.01" required style="flex:1; min-width:100px;">
|
||||||
|
<input type="text" id="txCategory" placeholder="Kategorie" style="flex:1; min-width:120px;">
|
||||||
|
<select id="txRecurring" style="flex:1; min-width:130px;"><option value="0">Einmalig</option><option value="1">Wiederkehrend</option></select>
|
||||||
|
<select id="txFreq" style="flex:1; min-width:120px;"><option value="">- Intervall -</option><option value="monthly">Monatlich</option><option value="yearly">Jährlich</option></select>
|
||||||
|
<label style="display:flex; align-items:center; gap:5px; margin-top:10px; cursor:pointer;"><input type="checkbox" id="txIsPaid" style="width:auto;" checked> Bereits bezahlt?</label>
|
||||||
|
<input type="file" id="txReceipt" accept="application/pdf" style="flex:1; min-width:180px; background: rgba(0,0,0,0.1);" title="Optionaler PDF Beleg">
|
||||||
|
<button type="submit" style="width:auto; padding: 0.75rem 1.5rem;">Buchen</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Datum</th><th>Typ</th><th>Titel</th><th>Firma</th><th>Beschreibung</th><th>Kategorie</th><th>Betrag</th><th>Status</th><th>Wiederkehrend</th><th style="text-align:center;">Beleg</th><th style="text-align:right;">Aktion</th></tr></thead>
|
||||||
|
<tbody id="txTableBody"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="tabLogbook" class="panel hidden">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
|
||||||
|
<h3 style="margin:0;">Fahrtenbuch</h3>
|
||||||
|
<a href="/api/cashbook/export/logbook" class="btn-export" target="_blank">📄 CSV Export (Fahrtenbuch)</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="lbForm" style="display:flex; gap:10px; flex-wrap:wrap; background:rgba(0,0,0,0.2); padding:15px; border-radius:8px; margin-bottom: 1.5rem;">
|
||||||
|
<input type="date" id="lbDate" required style="flex:1; min-width:130px;">
|
||||||
|
<input type="text" id="lbStart" placeholder="Start (z.B. Grünheide)" required style="flex:2; min-width:150px;">
|
||||||
|
<input type="text" id="lbEnd" placeholder="Ziel (z.B. Berlin)" required style="flex:2; min-width:150px;">
|
||||||
|
<input type="number" id="lbKm" placeholder="Distanz (km)" step="0.1" required style="flex:1; min-width:100px;">
|
||||||
|
<input type="text" id="lbPurpose" placeholder="Zweck / Kunde" required style="flex:2; min-width:150px;">
|
||||||
|
<button type="submit" style="width:auto; padding: 0.75rem 1.5rem;">Eintragen</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Datum</th><th>Start</th><th>Ziel</th><th>Zweck</th><th>Distanz</th><th>Aktion</th></tr></thead>
|
||||||
|
<tbody id="lbTableBody"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let cashflowChartObj = null;
|
||||||
|
let globalTransactions = [];
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', checkSession);
|
||||||
|
|
||||||
|
function checkSession() {
|
||||||
|
fetch('/api/me').then(res => res.json()).then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
loadCategories();
|
||||||
|
showDashboard();
|
||||||
|
} else {
|
||||||
|
document.getElementById('loginView').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadCategories() {
|
||||||
|
fetch('/api/cashbook/categories').then(res=>res.json()).then(data => {
|
||||||
|
if(data.success) {
|
||||||
|
let dashSelect = document.getElementById('filterDashboardCat');
|
||||||
|
let fibuSelect = document.getElementById('filterFibuCat');
|
||||||
|
let dashVal = dashSelect.value;
|
||||||
|
let fibuVal = fibuSelect.value;
|
||||||
|
let options = '<option value="">Alle Kategorien</option>';
|
||||||
|
data.data.forEach(cat => { options += `<option value="${cat}">${cat}</option>`; });
|
||||||
|
dashSelect.innerHTML = options;
|
||||||
|
fibuSelect.innerHTML = options;
|
||||||
|
dashSelect.value = dashVal;
|
||||||
|
fibuSelect.value = fibuVal;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('loginForm').addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
let fd = new FormData(); fd.append('username', document.getElementById('username').value); fd.append('password', document.getElementById('password').value);
|
||||||
|
fetch('/api/login', { method: 'POST', body: fd }).then(res => res.json()).then(data => {
|
||||||
|
if (data.success) { document.getElementById('loginError').style.display = 'none'; checkSession(); }
|
||||||
|
else { document.getElementById('loginError').style.display = 'block'; }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function logout() { fetch('/api/logout').then(() => { location.reload(); }); }
|
||||||
|
|
||||||
|
function showDashboard() {
|
||||||
|
document.getElementById('loginView').classList.add('hidden');
|
||||||
|
document.getElementById('dashboardView').classList.remove('hidden');
|
||||||
|
document.getElementById('txDate').valueAsDate = new Date();
|
||||||
|
document.getElementById('lbDate').valueAsDate = new Date();
|
||||||
|
toggleView('dashboard');
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleView(tab) {
|
||||||
|
document.getElementById('tabDashboard').classList.add('hidden');
|
||||||
|
document.getElementById('tabFibu').classList.add('hidden');
|
||||||
|
document.getElementById('tabLogbook').classList.add('hidden');
|
||||||
|
|
||||||
|
if (tab === 'dashboard') { document.getElementById('tabDashboard').classList.remove('hidden'); loadDashboard(); }
|
||||||
|
if (tab === 'fibu') { document.getElementById('tabFibu').classList.remove('hidden'); loadTransactions(); }
|
||||||
|
if (tab === 'logbook') { document.getElementById('tabLogbook').classList.remove('hidden'); loadLogbook(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function openPdfModal(id) {
|
||||||
|
document.getElementById('pdfFrame').src = `/api/cashbook/receipt?id=${id}`;
|
||||||
|
document.getElementById('pdfModal').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadDashboard() {
|
||||||
|
let cat = document.getElementById('filterDashboardCat').value;
|
||||||
|
fetch('/api/cashbook/dashboard?category=' + encodeURIComponent(cat)).then(res=>res.json()).then(data => {
|
||||||
|
if(data.success) {
|
||||||
|
let balEl = document.getElementById('displayBalance');
|
||||||
|
balEl.innerText = parseFloat(data.balance).toFixed(2).replace('.', ',') + ' €';
|
||||||
|
balEl.className = 'balance-amount ' + (data.balance >= 0 ? 'text-green' : 'text-red');
|
||||||
|
renderChart(data.chart);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderChart(chartData) {
|
||||||
|
const ctx = document.getElementById('cashflowChart').getContext('2d');
|
||||||
|
let labels = Object.keys(chartData);
|
||||||
|
|
||||||
|
let incomes = labels.map(m => chartData[m].income || 0);
|
||||||
|
let expenses = labels.map(m => chartData[m].expense || 0);
|
||||||
|
let profits = labels.map(m => (chartData[m].income || 0) - (chartData[m].expense || 0));
|
||||||
|
|
||||||
|
if (cashflowChartObj) { cashflowChartObj.destroy(); }
|
||||||
|
|
||||||
|
cashflowChartObj = new Chart(ctx, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: labels,
|
||||||
|
datasets: [
|
||||||
|
{ label: 'Einnahmen', data: incomes, backgroundColor: '#10b981' },
|
||||||
|
{ label: 'Ausgaben', data: expenses, backgroundColor: '#ef4444' },
|
||||||
|
{ label: 'Gewinn / Verlust', data: profits, backgroundColor: '#3b82f6' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true, maintainAspectRatio: false,
|
||||||
|
scales: {
|
||||||
|
y: { grid: { color: 'rgba(255,255,255,0.1)' }, ticks: { color: '#8b92a5' } },
|
||||||
|
x: { grid: { display: false }, ticks: { color: '#8b92a5' } }
|
||||||
|
},
|
||||||
|
plugins: { legend: { labels: { color: '#ffffff' } } }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function togglePaid(id, newStatus) {
|
||||||
|
let fd = new FormData();
|
||||||
|
fd.append('id', id);
|
||||||
|
fd.append('status', newStatus);
|
||||||
|
fetch('/api/cashbook/transactions/toggle-paid', { method: 'POST', body: fd }).then(res => res.json()).then(data => {
|
||||||
|
if(data.success) { loadTransactions(); loadDashboard(); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadTransactions() {
|
||||||
|
let cat = document.getElementById('filterFibuCat').value;
|
||||||
|
fetch('/api/cashbook/transactions?category=' + encodeURIComponent(cat)).then(res=>res.json()).then(data => {
|
||||||
|
let tbody = document.getElementById('txTableBody'); tbody.innerHTML = '';
|
||||||
|
if(data.success) {
|
||||||
|
globalTransactions = data.data;
|
||||||
|
globalTransactions.forEach(tx => {
|
||||||
|
let amountStr = parseFloat(tx.amount).toFixed(2).replace('.', ',') + ' €';
|
||||||
|
let amountHtml = tx.type === 'income' ? `<span class="text-green">+${amountStr}</span>` : `<span class="text-red">-${amountStr}</span>`;
|
||||||
|
let recStr = tx.is_recurring == 1 ? `Ja (${tx.frequency})` : '-';
|
||||||
|
let receiptHtml = tx.receipt_path ? `<span onclick="openPdfModal(${tx.id})" style="cursor:pointer; font-size:1.2rem;" title="Rechnung ansehen">📄</span>` : '<span style="color:var(--text-muted);">-</span>';
|
||||||
|
|
||||||
|
// Status Badge
|
||||||
|
let paidStatus = tx.is_paid == 1 ? 1 : 0;
|
||||||
|
let newStatus = paidStatus == 1 ? 0 : 1;
|
||||||
|
let statusHtml = paidStatus == 1
|
||||||
|
? `<span onclick="togglePaid(${tx.id}, ${newStatus})" style="cursor:pointer;" title="Auf 'Offen' setzen">✅ Bezahlt</span>`
|
||||||
|
: `<span onclick="togglePaid(${tx.id}, ${newStatus})" style="cursor:pointer; color:var(--accent-orange);" title="Als 'Bezahlt' markieren">⏳ Offen</span>`;
|
||||||
|
|
||||||
|
tbody.innerHTML += `<tr><td>${tx.date}</td><td>${tx.type === 'income' ? '📈' : '📉'}</td><td><strong>${tx.title}</strong></td><td>${tx.company||'-'}</td><td>${tx.description||'-'}</td><td>${tx.category||'-'}</td><td>${amountHtml}</td><td>${statusHtml}</td><td>${recStr}</td><td style="text-align:center;">${receiptHtml}</td><td style="text-align:right;"><button class="btn-edit" onclick="openEditTxModal(${tx.id})" title="Bearbeiten">✏️</button><button class="btn-delete" onclick="deleteTx(${tx.id})" title="Löschen">✖</button></td></tr>`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditTxModal(id) {
|
||||||
|
let tx = globalTransactions.find(t => t.id == id);
|
||||||
|
if (!tx) return;
|
||||||
|
|
||||||
|
document.getElementById('editTxId').value = tx.id;
|
||||||
|
document.getElementById('editTxType').value = tx.type;
|
||||||
|
document.getElementById('editTxDate').value = tx.date;
|
||||||
|
document.getElementById('editTxTitle').value = tx.title;
|
||||||
|
document.getElementById('editTxAmount').value = tx.amount;
|
||||||
|
document.getElementById('editTxCompany').value = tx.company || '';
|
||||||
|
document.getElementById('editTxCategory').value = tx.category || '';
|
||||||
|
document.getElementById('editTxDescription').value = tx.description || '';
|
||||||
|
document.getElementById('editTxRecurring').value = tx.is_recurring;
|
||||||
|
document.getElementById('editTxFreq').value = tx.frequency || '';
|
||||||
|
document.getElementById('editTxIsPaid').checked = (tx.is_paid == 1);
|
||||||
|
|
||||||
|
document.getElementById('editTxRemoveReceipt').checked = false;
|
||||||
|
document.getElementById('editTxReceipt').value = '';
|
||||||
|
|
||||||
|
let info = document.getElementById('currentReceiptInfo');
|
||||||
|
if(tx.receipt_path) {
|
||||||
|
info.innerHTML = `<span style="color:var(--accent-green);">Aktuell hinterlegt: 📄 ${tx.receipt_path}</span>`;
|
||||||
|
} else {
|
||||||
|
info.innerHTML = `<span style="color:var(--text-muted);">Kein Beleg hinterlegt.</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('editTxModal').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('editTxForm').addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
let fd = new FormData();
|
||||||
|
fd.append('id', document.getElementById('editTxId').value);
|
||||||
|
fd.append('type', document.getElementById('editTxType').value);
|
||||||
|
fd.append('date', document.getElementById('editTxDate').value);
|
||||||
|
fd.append('title', document.getElementById('editTxTitle').value);
|
||||||
|
fd.append('company', document.getElementById('editTxCompany').value);
|
||||||
|
fd.append('description', document.getElementById('editTxDescription').value);
|
||||||
|
fd.append('amount', document.getElementById('editTxAmount').value);
|
||||||
|
fd.append('category', document.getElementById('editTxCategory').value);
|
||||||
|
fd.append('is_recurring', document.getElementById('editTxRecurring').value);
|
||||||
|
fd.append('frequency', document.getElementById('editTxFreq').value);
|
||||||
|
fd.append('is_paid', document.getElementById('editTxIsPaid').checked ? '1' : '0');
|
||||||
|
fd.append('remove_receipt', document.getElementById('editTxRemoveReceipt').checked ? '1' : '0');
|
||||||
|
|
||||||
|
let receiptFile = document.getElementById('editTxReceipt').files[0];
|
||||||
|
if(receiptFile) fd.append('receipt', receiptFile);
|
||||||
|
|
||||||
|
fetch('/api/cashbook/transactions/edit', { method: 'POST', body: fd }).then(res=>res.json()).then(data => {
|
||||||
|
if(data.success) {
|
||||||
|
document.getElementById('editTxModal').classList.add('hidden');
|
||||||
|
loadTransactions();
|
||||||
|
loadCategories();
|
||||||
|
loadDashboard();
|
||||||
|
} else {
|
||||||
|
alert(data.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('txForm').addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
let fd = new FormData();
|
||||||
|
fd.append('type', document.getElementById('txType').value);
|
||||||
|
fd.append('date', document.getElementById('txDate').value);
|
||||||
|
fd.append('title', document.getElementById('txTitle').value);
|
||||||
|
fd.append('company', document.getElementById('txCompany').value);
|
||||||
|
fd.append('description', document.getElementById('txDescription').value);
|
||||||
|
fd.append('amount', document.getElementById('txAmount').value);
|
||||||
|
fd.append('category', document.getElementById('txCategory').value);
|
||||||
|
fd.append('is_recurring', document.getElementById('txRecurring').value);
|
||||||
|
fd.append('frequency', document.getElementById('txFreq').value);
|
||||||
|
fd.append('is_paid', document.getElementById('txIsPaid').checked ? '1' : '0');
|
||||||
|
|
||||||
|
let receiptFile = document.getElementById('txReceipt').files[0];
|
||||||
|
if(receiptFile) fd.append('receipt', receiptFile);
|
||||||
|
|
||||||
|
fetch('/api/cashbook/transactions', { method: 'POST', body: fd }).then(res=>res.json()).then(data => {
|
||||||
|
if(data.success) {
|
||||||
|
document.getElementById('txTitle').value = '';
|
||||||
|
document.getElementById('txCompany').value = '';
|
||||||
|
document.getElementById('txDescription').value = '';
|
||||||
|
document.getElementById('txAmount').value = '';
|
||||||
|
document.getElementById('txReceipt').value = '';
|
||||||
|
loadTransactions();
|
||||||
|
loadCategories();
|
||||||
|
loadDashboard();
|
||||||
|
} else {
|
||||||
|
alert(data.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function deleteTx(id) {
|
||||||
|
if(!confirm('Buchung wirklich löschen? Der verknüpfte Beleg wird ebenfalls unwiderruflich gelöscht!')) return;
|
||||||
|
let fd = new FormData(); fd.append('id', id);
|
||||||
|
fetch('/api/cashbook/transactions/delete', { method: 'POST', body: fd }).then(res=>res.json()).then(data => {
|
||||||
|
if(data.success) {
|
||||||
|
loadTransactions();
|
||||||
|
loadCategories();
|
||||||
|
loadDashboard();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- FAHRTENBUCH ---
|
||||||
|
function loadLogbook() {
|
||||||
|
fetch('/api/cashbook/logbook').then(res=>res.json()).then(data => {
|
||||||
|
let tbody = document.getElementById('lbTableBody'); tbody.innerHTML = '';
|
||||||
|
if(data.success) {
|
||||||
|
data.data.forEach(lb => {
|
||||||
|
let kmStr = parseFloat(lb.distance_km).toFixed(1).replace('.', ',') + ' km';
|
||||||
|
tbody.innerHTML += `<tr><td>${lb.date}</td><td>${lb.start_location}</td><td>${lb.end_location}</td><td>${lb.purpose}</td><td><strong>${kmStr}</strong></td><td><button class="btn-delete" onclick="deleteLb(${lb.id})">✖</button></td></tr>`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('lbForm').addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
let fd = new FormData();
|
||||||
|
fd.append('date', document.getElementById('lbDate').value);
|
||||||
|
fd.append('start_location', document.getElementById('lbStart').value);
|
||||||
|
fd.append('end_location', document.getElementById('lbEnd').value);
|
||||||
|
fd.append('distance_km', document.getElementById('lbKm').value);
|
||||||
|
fd.append('purpose', document.getElementById('lbPurpose').value);
|
||||||
|
|
||||||
|
fetch('/api/cashbook/logbook', { method: 'POST', body: fd }).then(res=>res.json()).then(data => {
|
||||||
|
if(data.success) { document.getElementById('lbStart').value = ''; document.getElementById('lbEnd').value = ''; document.getElementById('lbKm').value = ''; document.getElementById('lbPurpose').value = ''; loadLogbook(); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function deleteLb(id) {
|
||||||
|
if(!confirm('Fahrt wirklich löschen?')) return;
|
||||||
|
let fd = new FormData(); fd.append('id', id);
|
||||||
|
fetch('/api/cashbook/logbook/delete', { method: 'POST', body: fd }).then(res=>res.json()).then(data => { if(data.success) loadLogbook(); });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Executable
+147
@@ -0,0 +1,147 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
function handleLogin() {
|
||||||
|
global $pdo; @session_start();
|
||||||
|
$username = trim($_POST['username'] ?? '');
|
||||||
|
$password = $_POST['password'] ?? '';
|
||||||
|
|
||||||
|
$stmt = $pdo->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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
<?php
|
||||||
|
// /var/www/html/cashbook.php
|
||||||
|
|
||||||
|
function isAuthenticated() {
|
||||||
|
@session_start();
|
||||||
|
if (!isset($_SESSION['user_id'])) { http_response_code(401); exit; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleGetCategories() {
|
||||||
|
global $pdo; isAuthenticated();
|
||||||
|
$stmt = $pdo->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;
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
// /var/www/html/cron.php
|
||||||
|
require_once 'database.php';
|
||||||
|
|
||||||
|
echo "Starte CoreCashbook Cronjob...\n";
|
||||||
|
|
||||||
|
$currentDateStr = date('Y-m-d');
|
||||||
|
$currentYear = date('Y');
|
||||||
|
$currentMonth = date('m');
|
||||||
|
|
||||||
|
// Wir holen nur die AKTUELLSTE Vorlage pro Titel.
|
||||||
|
// Dadurch können die erzeugten Kopien wiederkehrend sein, ohne dass es zu einer Endlosschleife kommt!
|
||||||
|
$stmt = $pdo->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";
|
||||||
|
?>
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
// /var/www/data/database.php
|
||||||
|
$dbPath = '/var/www/data/core.sqlite';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = new PDO("sqlite:" . $dbPath);
|
||||||
|
$pdo->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()]));
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
ini_set('display_errors', 0);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
ini_set('log_errors', 1);
|
||||||
|
ini_set('error_log', '/var/www/data/php_errors.log');
|
||||||
|
|
||||||
|
set_error_handler(function($errno, $errstr, $errfile, $errline) {
|
||||||
|
error_log("Error [$errno]: $errstr in $errfile on line $errline");
|
||||||
|
if (error_reporting() & $errno) {
|
||||||
|
throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
set_exception_handler(function($e) {
|
||||||
|
error_log("Exception: " . $e->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 "<h1 style='color:white; font-family:sans-serif; text-align:center; margin-top:50px;'>500 - Systemfehler</h1>";
|
||||||
|
}
|
||||||
|
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';
|
||||||
|
?>
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
// /var/www/html/router.php
|
||||||
|
$request = $_SERVER['REQUEST_URI'];
|
||||||
|
$path = parse_url($request, PHP_URL_PATH);
|
||||||
|
$method = $_SERVER['REQUEST_METHOD'];
|
||||||
|
|
||||||
|
if ($path === '/' || $path === '') {
|
||||||
|
header('Content-Type: text/html');
|
||||||
|
readfile('app.html');
|
||||||
|
exit;
|
||||||
|
} elseif ($path === '/api/login' && $method === 'POST') { handleLogin();
|
||||||
|
} elseif ($path === '/api/logout') { handleLogout();
|
||||||
|
} elseif ($path === '/api/status' && $method === 'GET') { echo json_encode(['success' => 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']);
|
||||||
|
}
|
||||||
|
?>
|
||||||
Reference in new issue
Block a user