Files
kaffeekasse-saas/stricheintragen.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

227 lines
6.9 KiB
PHP

<?php
include "functions.php";
require_once __DIR__ . "/app/ledger.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) {
echo "<h2>Kein Zugriff</h2>";
include "footer.php";
exit;
}
/**
* Liefert die Mitarbeiter/Teilnehmerliste fuer die Vorder-/Rueckseiten- oder
* Alle-Auswahl. Fuer den Default-Mandanten bleibt die Fensterlogik exakt wie
* im Legacy-Bestand (Anker ist das juengste Datum in kl_Kaffeeverbrauch);
* andere Mandanten haben keine Legacy-Historie und nutzen die
* Ledger-native Fensterpruefung mit dem konfigurierten Listenfenster.
*
* @return list<array>
*/
function stricheintragen_fetch_participants(PDO $pdo, int $tenantId, string $action): array
{
$isDefault = ledger_is_default_tenant($pdo, $tenantId);
if ($isDefault && $action === 'vorderseite') {
$stmt = $pdo->query(
"SELECT M.MitarbeiterID
FROM kl_Mitarbeiter M
JOIN kl_Kaffeeverbrauch V ON M.MitarbeiterID = V.MitarbeiterID
WHERE V.Datum >= DATE_SUB((SELECT MAX(Datum) FROM kl_Kaffeeverbrauch WHERE Datum < CURDATE()), INTERVAL 100 DAY)
AND M.aktiv = 1
GROUP BY M.MitarbeiterID
HAVING SUM(V.AnzahlStriche) >= 10"
);
$legacyIds = array_map('intval', $stmt->fetchAll(PDO::FETCH_COLUMN));
return ledger_fetch_participant_summaries($pdo, $tenantId, ['legacy_mitarbeiter_ids' => $legacyIds]);
}
if ($isDefault && $action === 'rueckseite') {
$stmt = $pdo->query(
"SELECT M.MitarbeiterID
FROM kl_Mitarbeiter M
LEFT JOIN kl_Kaffeeverbrauch V ON M.MitarbeiterID = V.MitarbeiterID AND V.Datum >= DATE_SUB((SELECT MAX(Datum) FROM kl_Kaffeeverbrauch), INTERVAL 100 DAY)
WHERE M.aktiv = 1
GROUP BY M.MitarbeiterID
HAVING COALESCE(SUM(V.AnzahlStriche), 0) < 10"
);
$legacyIds = array_map('intval', $stmt->fetchAll(PDO::FETCH_COLUMN));
return ledger_fetch_participant_summaries($pdo, $tenantId, ['legacy_mitarbeiter_ids' => $legacyIds]);
}
if (!$isDefault && ($action === 'vorderseite' || $action === 'rueckseite')) {
$settings = saas_fetch_tenant_settings($pdo, $tenantId);
$windowDays = $settings['sheet_window_days'] ?? 100;
return ledger_fetch_participants_by_window_marks($pdo, $tenantId, $windowDays, $action === 'vorderseite');
}
return ledger_fetch_participant_summaries($pdo, $tenantId, ['active_only' => true]);
}
$eingetragen = 0;
$fehlgeschlagen = false;
$hatGespeichert = false;
// Verarbeitung des Formulars, wenn es gesendet wurde
if ($_SERVER["REQUEST_METHOD"] == "POST") {
$hatGespeichert = true;
$kostenproStrich = floatval($_POST["kostenproStrich"] ?? 0);
$unitPriceCents = (int)round($kostenproStrich * 100);
$datum = date("Y-m-d H:i:s");
// Legacy-Verknuepfung je Teilnehmer vorab laden, damit pro Zeile
// entschieden werden kann, ob (Default-Mandant) zusaetzlich in
// kl_Kaffeeverbrauch geschrieben oder (jeder andere Mandant) direkt
// ins Ledger gebucht wird.
$legacyMap = [];
$stmt = $pdo->prepare('SELECT id, legacy_mitarbeiter_id FROM participants WHERE tenant_id = ?');
$stmt->execute([$tenantId]);
foreach ($stmt->fetchAll() as $row) {
$legacyMap[(int)$row['id']] = $row['legacy_mitarbeiter_id'] !== null ? (int)$row['legacy_mitarbeiter_id'] : null;
}
try {
$pdo->beginTransaction();
$insertLegacy = $pdo->prepare(
"INSERT INTO kl_Kaffeeverbrauch (MitarbeiterID, AnzahlStriche, Kosten, KostenproStrich, Datum) VALUES (?, ?, ?, ?, ?)"
);
foreach ($_POST["anzahlStriche"] ?? [] as $participantId => $anzahlStriche) {
$participantId = (int)$participantId;
$anzahlStriche = (int)$anzahlStriche;
if ($participantId <= 0 || $anzahlStriche === 0 || !array_key_exists($participantId, $legacyMap)) {
continue;
}
$legacyMitarbeiterId = $legacyMap[$participantId];
if ($legacyMitarbeiterId !== null) {
$kosten = $anzahlStriche * $kostenproStrich;
$insertLegacy->execute([$legacyMitarbeiterId, $anzahlStriche, $kosten, $kostenproStrich, $datum]);
$legacyConsumptionId = (int)$pdo->lastInsertId();
ledger_mirror_legacy_consumption($pdo, $tenantId, $legacyConsumptionId);
} else {
ledger_record_consumption($pdo, $tenantId, $participantId, $anzahlStriche, $unitPriceCents, 'manual_bulk');
}
$eingetragen++;
}
$pdo->commit();
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
$fehlgeschlagen = true;
}
$action = 'alle';
} else {
$action = (string)($_GET['action'] ?? 'alle');
}
$mitarbeiter = stricheintragen_fetch_participants($pdo, $tenantId, $action);
$settings = saas_fetch_tenant_settings($pdo, $tenantId);
$kostenproStrichAnzeige = number_format(($settings['mark_price_cents'] ?? 20) / 100, 2, '.', '');
?>
<h2>Anzahl der Striche für alle Mitarbeiter</h2>
<?php if ($hatGespeichert): ?>
<?php if ($fehlgeschlagen): ?>
<div class="hint-box error"><p>Die Einträge konnten nicht gespeichert werden.</p></div>
<?php else: ?>
<div class="hint-box success"><p><?php echo (int)$eingetragen; ?> Einträge erfolgreich hinzugefügt.</p></div>
<?php endif; ?>
<?php endif; ?>
<ul class="actions">
<li>
<form action="stricheintragen.php" method="get">
<input type="hidden" name="action" value="vorderseite">
<button type="submit">Vorderseite</button>
</form>
</li>
<li>
<form action="stricheintragen.php" method="get">
<input type="hidden" name="action" value="rueckseite">
<button type="submit">Rückseite</button>
</form>
</li>
<li>
<form action="stricheintragen.php" method="get">
<input type="hidden" name="action" value="alle">
<button type="submit">Alle</button>
</form>
</li>
</ul>
<form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>">
<?php echo app_csrf_field(); ?>
<?php
echo "<label for='kostenproStrich'>Kosten pro Strich:</label>
<input type='number' name='kostenproStrich' step='0.01' value='" . saas_html($kostenproStrichAnzeige) . "'><br>";
echo "<table>";
echo " <tr>
<th>Mitarbeiter</th>
<th>Anzahl Striche</th>
</tr>";
foreach ($mitarbeiter as $teilnehmer) {
$participantId = $teilnehmer['participant_id'];
$name = saas_html($teilnehmer['display_name']);
echo "<tr>";
echo "<td><label for='anzahlStriche[$participantId]'>$name:</label></td>";
echo "<td><input type='number' name='anzahlStriche[$participantId]' ></td>";
echo "</tr>";
}
echo "</table>";
?>
<button type="submit">Eintragen</button>
</form>
</div>
</section>
<?php include "footer.php"; ?>