initial commit

This commit is contained in:
2026-07-22 01:12:45 +02:00
commit 124f2746ac
357 changed files with 159669 additions and 0 deletions
+506
View File
@@ -0,0 +1,506 @@
<?php
require_once "config/session.php";
if (!isset($_SESSION["loggedin"]) || $_SESSION["loggedin"] !== true) {
header("location: index.php");
exit;
}
include('navbar.php');
?>
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ANJUMA • Finanzen</title>
<!-- Bootstrap 5 + DataTables via CDN (sicher, keine 404) -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/datatables.net-bs5@1.13.10/css/dataTables.bootstrap5.min.css">
<style>
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
.kpi-card { min-height: 92px; }
.dt-nowrap td { white-space: nowrap; }
@media (max-width: 576px) {
#kpiIncomeHint, #kpiExpensesHint { display:none; }
}
@media (max-width: 576px) {
.dt-nowrap td { white-space: normal !important; }
}
</style>
</head>
<body class="bg-light">
<div class="container-fluid py-4">
<div class="d-flex align-items-center justify-content-between mb-3">
<div>
<h3 class="mb-0">Finanzen</h3>
</div>
<div class="d-flex gap-2">
<button id="btnRefresh" class="btn btn-outline-secondary btn-sm">↻ Refresh</button>
<button id="btnAddExpense" class="btn btn-primary btn-sm">+ Ausgabe</button>
</div>
</div>
<!-- Filter -->
<div class="card shadow-sm mb-3">
<div class="card-body">
<div class="row g-2 align-items-end">
<div class="col-12 col-md-3">
<label class="form-label mb-1">Von</label>
<input type="date" class="form-control" id="fromDate">
</div>
<div class="col-12 col-md-3">
<label class="form-label mb-1">Bis</label>
<input type="date" class="form-control" id="toDate">
</div>
<div class="col-12 col-md-6 d-flex gap-2">
<button id="btnThisMonth" class="btn btn-outline-primary">Dieser Monat</button>
<button id="btnLastMonth" class="btn btn-outline-primary">Letzter Monat</button>
<button id="btnThisYear" class="btn btn-outline-primary">Dieses Jahr</button>
</div>
</div>
</div>
</div>
<!-- KPI -->
<div class="row g-3 mb-3">
<div class="col-12 col-md-4">
<div class="card shadow-sm kpi-card">
<div class="card-body">
<div class="text-muted small">Einnahmen</div>
<div class="fs-3 fw-semibold" id="kpiIncome"></div>
<div class="text-muted small" id="kpiIncomeHint"></div>
</div>
</div>
</div>
<div class="col-12 col-md-4">
<div class="card shadow-sm kpi-card">
<div class="card-body">
<div class="text-muted small">Ausgaben</div>
<div class="fs-3 fw-semibold" id="kpiExpenses"></div>
<div class="text-muted small" id="kpiExpensesHint"></div>
</div>
</div>
</div>
<div class="col-12 col-md-4">
<div class="card shadow-sm kpi-card">
<div class="card-body">
<div class="text-muted small">Netto</div>
<div class="fs-3 fw-semibold" id="kpiNet"></div>
<div class="text-muted small">Einnahmen Ausgaben</div>
</div>
</div>
</div>
</div>
<!-- Tabs -->
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#tabIncome" type="button" role="tab">Einnahmen (Termine)</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabExpenses" type="button" role="tab">Ausgaben</button>
</li>
</ul>
<div class="tab-content">
<!-- Income -->
<div class="tab-pane fade show active" id="tabIncome" role="tabpanel">
<div class="card shadow-sm border-top-0 rounded-top-0">
<div class="card-body">
<table id="tblIncome" class="table table-striped table-hover align-middle dt-nowrap w-100">
<thead>
<tr>
<th>Datum</th>
<th>Zeit</th>
<th>Location</th>
<th>Gage</th>
<th>Notiz</th>
<th>ID</th>
</tr>
</thead>
<tbody></tbody>
</table>
<div class="text-muted small mt-2" id="incomeInfo"></div>
</div>
</div>
</div>
<!-- Expenses -->
<div class="tab-pane fade" id="tabExpenses" role="tabpanel">
<div class="card shadow-sm border-top-0 rounded-top-0">
<div class="card-body">
<table id="tblExpenses" class="table table-striped table-hover align-middle dt-nowrap w-100">
<thead>
<tr>
<th>Datum</th>
<th>Kategorie</th>
<th>Titel</th>
<th>Betrag</th>
<th>Methode</th>
<th>Notiz</th>
<th>Aktion</th>
</tr>
</thead>
<tbody></tbody>
</table>
<div class="text-muted small mt-2" id="expensesInfo"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Expense Modal -->
<div class="modal fade" id="expenseModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<form class="modal-content" id="expenseForm">
<div class="modal-header">
<h5 class="modal-title" id="expenseModalTitle">Ausgabe</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Schließen"></button>
</div>
<div class="modal-body">
<input type="hidden" id="expId" name="id" value="">
<div class="row g-2">
<div class="col-12 col-md-6">
<label class="form-label mb-1">Datum</label>
<input type="date" class="form-control" id="expDate" name="date" required>
</div>
<div class="col-12 col-md-6">
<label class="form-label mb-1">Betrag (EUR)</label>
<input type="number" class="form-control" id="expAmount" name="amount_eur" min="1" step="1" required>
</div>
<div class="col-12 col-md-6">
<label class="form-label mb-1">Kategorie</label>
<input type="text" class="form-control" id="expCategory" name="category" placeholder="equipment / fuel_travel / marketing ..." required>
</div>
<div class="col-12 col-md-6">
<label class="form-label mb-1">Methode</label>
<select class="form-select" id="expMethod" name="method">
<option value="bank">Bank</option>
<option value="cash">Cash</option>
<option value="paypal">PayPal</option>
<option value="card">Karte</option>
<option value="other">Other</option>
</select>
</div>
<div class="col-12">
<label class="form-label mb-1">Titel</label>
<input type="text" class="form-control" id="expTitle" name="title" required>
</div>
<div class="col-12">
<label class="form-label mb-1">Notiz</label>
<textarea class="form-control" id="expNotes" name="notes" rows="3"></textarea>
</div>
</div>
<div class="alert alert-warning mt-3 mb-0 small">
Tipp: Kategorien konsistent halten (z.B. <span class="mono">equipment</span>, <span class="mono">fuel_travel</span>, <span class="mono">marketing</span>), dann sind Auswertungen später easy.
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="submit" class="btn btn-primary" id="btnSaveExpense">Speichern</button>
</div>
</form>
</div>
</div>
<!-- Delete Confirm Modal -->
<div class="modal fade" id="deleteModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Ausgabe löschen</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Schließen"></button>
</div>
<div class="modal-body" id="deleteBody">Wirklich löschen?</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="button" class="btn btn-danger" id="btnDoDelete">Löschen</button>
</div>
</div>
</div>
</div>
<!-- Toast -->
<div class="position-fixed bottom-0 end-0 p-3" style="z-index: 1080">
<div id="toast" class="toast align-items-center text-bg-dark border-0" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body" id="toastBody">...</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
</div>
<!-- JS -->
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/datatables.net@1.13.10/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/datatables.net-bs5@1.13.10/js/dataTables.bootstrap5.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
const API_INCOME = '/api/finance_income.php';
const API_EXP_LIST = '/api/finance_expenses_list.php';
const API_EXP_SAVE = '/api/finance_expenses_save.php';
const API_EXP_DEL = '/api/finance_expenses_delete.php';
let dtIncome, dtExpenses;
let deleteId = null;
const toast = new bootstrap.Toast(document.getElementById('toast'), { delay: 3500 });
function showToast(msg) {
document.getElementById('toastBody').textContent = msg;
toast.show();
}
function eur(n) {
const v = Number(n) || 0;
return v.toLocaleString('de-DE', { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 });
}
function fmtDateDE(iso) {
if (!iso) return '';
const [y, m, d] = iso.split('-');
return `${d}.${m}.${y}`;
}
function todayISO() {
const d = new Date();
const m = String(d.getMonth()+1).padStart(2,'0');
const day = String(d.getDate()).padStart(2,'0');
return `${d.getFullYear()}-${m}-${day}`;
}
function setRange(from, to) {
document.getElementById('fromDate').value = from;
document.getElementById('toDate').value = to;
}
function isoLocal(dateObj) {
const y = dateObj.getFullYear();
const m = String(dateObj.getMonth() + 1).padStart(2, '0');
const d = String(dateObj.getDate()).padStart(2, '0');
return `${y}-${m}-${d}`;
}
function thisMonthRange() {
const d = new Date();
const from = new Date(d.getFullYear(), d.getMonth(), 1);
const to = new Date(d.getFullYear(), d.getMonth() + 1, 0);
return [isoLocal(from), isoLocal(to)];
}
function lastMonthRange() {
const d = new Date();
const from = new Date(d.getFullYear(), d.getMonth() - 1, 1);
const to = new Date(d.getFullYear(), d.getMonth(), 0);
return [isoLocal(from), isoLocal(to)];
}
function thisYearRange() {
const d = new Date();
const from = new Date(d.getFullYear(), 0, 1);
const to = new Date(d.getFullYear(), 11, 31);
return [isoLocal(from), isoLocal(to)];
}
function getFilterQS() {
const from = document.getElementById('fromDate').value;
const to = document.getElementById('toDate').value;
const qs = new URLSearchParams({ from, to });
return qs.toString();
}
async function fetchJSON(url) {
const res = await fetch(url, { cache: 'no-store' });
const json = await res.json();
if (!json.ok) throw new Error(json.error || 'API error');
return json;
}
async function refreshAll() {
try {
const qs = getFilterQS();
const [income, expenses] = await Promise.all([
fetchJSON(`${API_INCOME}?${qs}`),
fetchJSON(`${API_EXP_LIST}?${qs}`),
]);
// KPI
const inc = Number(income.sum_eur || 0);
const exp = Number(expenses.sum_eur || 0);
document.getElementById('kpiIncome').textContent = eur(inc);
document.getElementById('kpiExpenses').textContent = eur(exp);
document.getElementById('kpiNet').textContent = eur(inc - exp);
document.getElementById('kpiIncomeHint').textContent = `${income.items.length} abgeschlossene Termine`;
document.getElementById('kpiExpensesHint').textContent = `${expenses.items.length} Ausgaben`;
// Tables
dtIncome.clear().rows.add(income.items || []).draw(false);
dtExpenses.clear().rows.add(expenses.items || []).draw(false);
document.getElementById('incomeInfo').textContent =
`Zeitraum ${income.from} bis ${income.to} • Summe: ${eur(inc)}`;
document.getElementById('expensesInfo').textContent =
`Zeitraum ${expenses.from} bis ${expenses.to} • Summe: ${eur(exp)}`;
} catch (e) {
showToast('Fehler: ' + e.message);
}
}
function expenseActions(row) {
const encoded = encodeURIComponent(JSON.stringify(row));
return `
<div class="btn-group btn-group-sm" role="group">
<button class="btn btn-outline-secondary btnEditExpense" data-row="${encoded}">Edit</button>
<button class="btn btn-outline-danger btnDeleteExpense" data-id="${row.id}" data-title="${encodeURIComponent(row.title)}">Delete</button>
</div>
`;
}
function openExpenseModal(row=null) {
document.getElementById('expenseModalTitle').textContent = row ? 'Ausgabe bearbeiten' : 'Neue Ausgabe';
document.getElementById('expId').value = row ? row.id : '';
document.getElementById('expDate').value = row ? row.date : todayISO();
document.getElementById('expCategory').value = row ? row.category : '';
document.getElementById('expTitle').value = row ? row.title : '';
document.getElementById('expAmount').value = row ? row.amount_eur : '';
document.getElementById('expMethod').value = row ? row.method : 'bank';
document.getElementById('expNotes').value = row ? (row.notes || '') : '';
new bootstrap.Modal(document.getElementById('expenseModal')).show();
}
function openDeleteModal(id, title) {
deleteId = id;
const t = title ? decodeURIComponent(title) : '';
document.getElementById('deleteBody').innerHTML =
`Ausgabe <b>${t}</b> (ID <span class="mono">${id}</span>) wirklich löschen?`;
new bootstrap.Modal(document.getElementById('deleteModal')).show();
}
document.getElementById('btnRefresh').addEventListener('click', refreshAll);
document.getElementById('btnThisMonth').addEventListener('click', () => {
const [f,t] = thisMonthRange(); setRange(f,t); refreshAll();
});
document.getElementById('btnLastMonth').addEventListener('click', () => {
const [f,t] = lastMonthRange(); setRange(f,t); refreshAll();
});
document.getElementById('btnThisYear').addEventListener('click', () => {
const [f,t] = thisYearRange(); setRange(f,t); refreshAll();
});
document.getElementById('btnAddExpense').addEventListener('click', () => openExpenseModal(null));
document.getElementById('expenseForm').addEventListener('submit', async (ev) => {
ev.preventDefault();
try {
const fd = new FormData(ev.target);
const res = await fetch(API_EXP_SAVE, { method: 'POST', body: fd });
const json = await res.json();
if (!json.ok) throw new Error(json.error || 'Save failed');
showToast('Gespeichert ✅');
bootstrap.Modal.getInstance(document.getElementById('expenseModal'))?.hide();
await refreshAll();
} catch (e) {
showToast('Fehler: ' + e.message);
}
});
document.getElementById('btnDoDelete').addEventListener('click', async () => {
try {
const fd = new FormData();
fd.append('id', deleteId);
const res = await fetch(API_EXP_DEL, { method: 'POST', body: fd });
const json = await res.json();
if (!json.ok) throw new Error(json.error || 'Delete failed');
showToast('Gelöscht 🗑️');
bootstrap.Modal.getInstance(document.getElementById('deleteModal'))?.hide();
deleteId = null;
await refreshAll();
} catch (e) {
showToast('Fehler: ' + e.message);
}
});
// Delegated events for table buttons
document.addEventListener('click', (ev) => {
const editBtn = ev.target.closest('.btnEditExpense');
if (editBtn) {
const row = JSON.parse(decodeURIComponent(editBtn.dataset.row));
openExpenseModal(row);
return;
}
const delBtn = ev.target.closest('.btnDeleteExpense');
if (delBtn) {
openDeleteModal(delBtn.dataset.id, delBtn.dataset.title);
return;
}
});
// Init
$(function() {
// Default range: this month
const [f,t] = thisMonthRange();
setRange(f,t);
dtIncome = $('#tblIncome').DataTable({
pageLength: 10,
order: [[0,'desc']],
columns: [
{ data: 'date', className: 'mono', render: (d)=>fmtDateDE(d) },
{ data: 'time', className: 'mono' },
{ data: 'location' },
{ data: 'gage', className: 'mono', render: (d)=>eur(d) },
{ data: 'desc', render: (d)=>(d||'').toString().slice(0,80) },
{ data: 'id', className: 'mono' },
],
columnDefs: [
{ targets: [1,4,5], visible: window.matchMedia("(max-width: 576px)").matches ? false : true },
],
});
dtExpenses = $('#tblExpenses').DataTable({
pageLength: 10,
order: [[0,'desc']],
columns: [
{ data: 'date', className: 'mono', render: (d)=>fmtDateDE(d) },
{ data: 'category', className: 'mono' },
{ data: 'title' },
{ data: 'amount_eur', className: 'mono', render: (d)=>eur(d) },
{ data: 'method', className: 'mono' },
{ data: 'notes', render: (d)=>(d||'').toString().slice(0,80) },
{ data: null, orderable:false, searchable:false, render:(row)=>expenseActions(row) },
],
columnDefs: [
{ targets: [1,4,5], visible: window.matchMedia("(max-width: 576px)").matches ? false : true },
],
});
refreshAll();
});
</script>
</body>
</html>