CoreCashbook V1.0.0

This commit is contained in:
root committed 2026-09-09 08:23:02 +02:00
commit 7dbdd2dc5b
18 files changed
+1149

No files matched your search

Executable
+27
View File
@@ -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
BIN
View File
Binary file not shown.
+16
View File
@@ -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.
+8
View File
@@ -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.
+13
View File
@@ -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
View File
@@ -0,0 +1,4 @@
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [QSA,L]
+476
View File
@@ -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
View File
@@ -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;
}
+256
View File
@@ -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;
}
?>
+72
View File
@@ -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";
?>
+52
View File
@@ -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()]));
}
?>
+33
View File
@@ -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';
?>
+45
View File
@@ -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']);
}
?>