Files
2026-07-22 01:12:45 +02:00

507 lines
18 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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>