Files
kaffeekasse-saas/letzteneintraege.php
T
clemens ef5d0e1822 Sicherheits-/Korrektheits-Fixes und neue Funktionen
Bugfixes aus dem Code-Review:
- XSS: unescaptes $_SERVER['PHP_SELF'] in csvupload.php und
  letzteneintraege.php durch feste Seitennamen ersetzt.
- Stripe-Webhook: Event-Deduplizierung (neue Tabelle stripe_webhook_events)
  gegen doppelte Verarbeitung/Dolibarr-Rechnungen bei Retry-Zustellung.
- Stripe-Webhook: Tarifwechsel aus dem Kundenportal wird lokal nachgezogen
  (plan_code aus dem Preis-lookup_key bei subscription.updated).
- Post-Redirect-Get fuer jahresauswertung, mailversenden und die
  Selbst-Stricheintragung - verhindert Doppelbuchung/Doppelversand per
  Browser-Refresh.
- Jahresbonus: Mails erst nach erfolgreichem Commit; Restcent-Ausgleich
  beim letzten Empfaenger, damit die Summe exakt stimmt.
- Verschachtelte HTML-Dokumente in csvupload/einzahlung/stricheintragen
  entfernt (Layout kommt aus header.php).
- Rate-Limit fuer den Versand von E-Mail-Verifizierungslinks.

Neue Funktionen:
- Mitglieder koennen ihren zuletzt selbst eingetragenen Strich wieder
  stornieren (nur eigene Web-Eintraege, Kassenwart-Eintraege bleiben).
- Monatsuebersicht des eigenen Verbrauchs im Mitglieder-Dashboard.
- Automatische Zahlungserinnerung: opt-in pro Mandant ab der
  Warnschwelle, mit Intervall; Cron-Skript scripts/send-payment-reminders.php.
- CSV-Import fuer Mitgliederlisten inkl. herunterladbarer Vorlage
  (mitglieder-vorlage.php), Semikolon-/Komma- und BOM-Erkennung.
- Logo als PDF-Wasserzeichen pro Mandant (Upload in den Mandant-
  Einstellungen, geschuetzt unter var/tenant_logos/, ersetzt den
  Text-Wasserzeichen im Ausdruck).

Robusterer Teilnehmer-Lookup im Dashboard ueber user_id (Fallback E-Mail).
2026-07-20 21:54:16 +02:00

241 lines
7.0 KiB
PHP

<?php
include "functions.php";
require_once __DIR__ . "/app/ledger.php";
require_once __DIR__ . "/app/audit.php";
app_require_csrf();
include "header.php";
include "headerline.php";
include "nav.php";
?>
<!-- Banner -->
<section id="banner">
<div class="content">
<?php
$pdo = app_db_pdo();
$saasUser = saas_current_user($pdo);
$tenantId = 0;
$hasAccess = false;
if ($saasUser !== null && saas_user_has_role(['owner', 'admin', 'treasurer'], $saasUser)) {
$tenantId = (int)$saasUser['tenant_id'];
$hasAccess = true;
}
if (!$hasAccess && $saasUser === null && checkKaffeelisteAdmin($conn, $mailadress)) {
$tenant = ledger_fetch_default_tenant($pdo);
if ($tenant !== null) {
$tenantId = (int)$tenant['id'];
$hasAccess = true;
}
}
if($hasAccess){
// Funktion zum Löschen einer Einzahlung: der Ledger-Eintrag wird als Storno
// markiert statt geloescht, damit die Buchungshistorie nachvollziehbar bleibt.
function loescheEinzahlung($einzahlungID, PDO $pdo, int $tenantId) {
try {
$pdo->beginTransaction();
ledger_void_entry_by_legacy_id($pdo, $tenantId, 'kl_Einzahlungen', $einzahlungID);
$stmt = $pdo->prepare('DELETE FROM kl_Einzahlungen WHERE EinzahlungsID = ?');
$stmt->execute([$einzahlungID]);
$pdo->commit();
return true; // Erfolgreich gelöscht
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
return 'Die Einzahlung konnte nicht gelöscht werden.';
}
}
// Überprüfen, ob ein Löschvorgang angefordert wurde
if ($_SERVER["REQUEST_METHOD"] == "POST" && isset($_POST["aktion"]) && $_POST["aktion"] == "loescheneinzahlung") {
$einzahlungID = (int)$_POST["einzahlungID"];
$ergebnis = loescheEinzahlung($einzahlungID, $pdo, $tenantId);
if ($ergebnis === true) {
app_audit_log($pdo, $tenantId, $saasUser['user_id'] ?? null, 'payment.voided', 'kl_Einzahlungen', $einzahlungID);
echo "Einzahlung erfolgreich gelöscht.";
} else {
echo "Fehler: $ergebnis";
}
}
// SQL-Abfrage für die letzten 100 Einzahlungen
$sqlEinzahlungen = "SELECT TOP 100 kl_Mitarbeiter.Name AS MitarbeiterName, kl_Einzahlungen.EinzahlungsID, kl_Einzahlungen.Betrag, kl_Einzahlungen.Datum
FROM kl_Einzahlungen
JOIN kl_Mitarbeiter ON kl_Einzahlungen.MitarbeiterID = kl_Mitarbeiter.MitarbeiterID
ORDER BY kl_Einzahlungen.Datum DESC";
$stmtEinzahlungen = sqlsrv_query($conn, $sqlEinzahlungen);
?>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Letzte 100 Einzahlungen</title>
</head>
<h2>Letzte 100 Einzahlungen</h2>
<table border="1" class="table table-striped">
<tr>
<th>Name des Mitarbeiters</th>
<th>Betrag</th>
<th>Datum</th>
<th>Aktion</th>
</tr>
<?php
while ($row = sqlsrv_fetch_array($stmtEinzahlungen, SQLSRV_FETCH_ASSOC)) {
$einzahlungID = $row['EinzahlungsID'];
$mitarbeiterName = $row['MitarbeiterName'];
$betrag = $row['Betrag'];
$datum = $row['Datum']->format('Y-m-d H:i:s'); // Das Format kann angepasst werden
$betrag = number_format($betrag, 2, ',', '.');
echo "<tr>";
echo "<td>{$mitarbeiterName}</td>";
echo "<td>{$betrag}</td>";
echo "<td>{$datum}</td>";
echo "<td>";
echo "<form method='post' action='letzteneintraege.php'>";
echo "<input type='hidden' name='aktion' value='loescheneinzahlung'>";
echo "<input type='hidden' name='einzahlungID' value='{$einzahlungID}'>";
echo app_csrf_field();
echo "<button type='submit'>Löschen</button>";
echo "</form>";
echo "</td>";
echo "</tr>";
}
?>
</table>
<br><br>
<?php
// Funktion zum Löschen eines Strich-Eintrags: der Ledger-Eintrag wird als
// Storno markiert statt geloescht, damit die Buchungshistorie nachvollziehbar bleibt.
function loescheStrichEintrag($strichID, PDO $pdo, int $tenantId) {
try {
$pdo->beginTransaction();
ledger_void_entry_by_legacy_id($pdo, $tenantId, 'kl_Kaffeeverbrauch', $strichID);
$stmt = $pdo->prepare('DELETE FROM kl_Kaffeeverbrauch WHERE VerbrauchID = ?');
$stmt->execute([$strichID]);
$pdo->commit();
return true; // Erfolgreich gelöscht
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
return 'Der Strich-Eintrag konnte nicht gelöscht werden.';
}
}
// Überprüfen, ob ein Löschvorgang angefordert wurde
if ($_SERVER["REQUEST_METHOD"] == "POST" && isset($_POST["aktion"]) && $_POST["aktion"] == "loeschen") {
$strichID = (int)$_POST["strichID"];
$ergebnis = loescheStrichEintrag($strichID, $pdo, $tenantId);
if ($ergebnis === true) {
app_audit_log($pdo, $tenantId, $saasUser['user_id'] ?? null, 'consumption.voided', 'kl_Kaffeeverbrauch', $strichID);
echo "Strich-Eintrag erfolgreich gelöscht.";
} else {
echo "Fehler: $ergebnis";
}
}
// SQL-Abfrage für die letzten 100 Strich-Einträge
$sqlStriche = "SELECT TOP 100 kl_Kaffeeverbrauch.VerbrauchID, kl_Mitarbeiter.Name AS MitarbeiterName, kl_Kaffeeverbrauch.AnzahlStriche, kl_Kaffeeverbrauch.Kosten, kl_Kaffeeverbrauch.Datum
FROM kl_Kaffeeverbrauch
JOIN kl_Mitarbeiter ON kl_Kaffeeverbrauch.MitarbeiterID = kl_Mitarbeiter.MitarbeiterID
ORDER BY kl_Kaffeeverbrauch.Datum DESC";
$stmtStriche = sqlsrv_query($conn, $sqlStriche);
?>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Letzte 100 Strich-Einträge</title>
</head>
<body>
<h2>Letzte 100 Strich-Einträge</h2>
<table border="1" class="table table-striped">
<tr>
<th>Name des Mitarbeiters</th>
<th>Anzahlstriche</th>
<th>Betrag</th>
<th>Datum</th>
<th>Aktion</th>
</tr>
<?php
while ($row = sqlsrv_fetch_array($stmtStriche, SQLSRV_FETCH_ASSOC)) {
$strichID = $row['VerbrauchID'];
$mitarbeiterName = $row['MitarbeiterName'];
$anzahlStriche = $row['AnzahlStriche'];
$betrag = $row['Kosten'];
$datum = $row['Datum']->format('Y-m-d H:i:s'); // Das Format kann angepasst werden
$anzahlStriche = number_format($anzahlStriche, 0, ',', '.');
$betrag = number_format($betrag, 0, ',', '.');
echo "<tr>";
echo "<td>{$mitarbeiterName}</td>";
echo "<td>{$anzahlStriche}</td>";
echo "<td>{$betrag}</td>";
echo "<td>{$datum}</td>";
echo "<td>";
echo "<form method='post' action='letzteneintraege.php'>";
echo "<input type='hidden' name='aktion' value='loeschen'>";
echo "<input type='hidden' name='strichID' value='{$strichID}'>";
echo app_csrf_field();
echo "<button type='submit'>Löschen</button>";
echo "</form>";
echo "</td>";
echo "</tr>";
}
?>
</table>
</body>
</html>
<?php
}else{
echo "<h2>Kein Zugriff</h2>";
}
?>
</div>
</section>
<?php include "footer.php"; ?>