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).
211 lines
6.0 KiB
PHP
211 lines
6.0 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. Siehe stricheintragen.php fuer dieselbe Logik: Default-
|
|
* Mandant nutzt die Legacy-Historie, andere Mandanten die Ledger-native
|
|
* Fensterpruefung mit dem konfigurierten Listenfenster.
|
|
*
|
|
* @return list<array>
|
|
*/
|
|
function einzahlung_fetch_participants(PDO $pdo, int $tenantId, string $action): array
|
|
{
|
|
$isDefault = ledger_is_default_tenant($pdo, $tenantId);
|
|
|
|
if ($action === 'vorderseite' || $action === 'rueckseite') {
|
|
if ($isDefault) {
|
|
$vergleich = $action === 'vorderseite' ? '>= 10' : '< 10';
|
|
$join = $action === 'vorderseite' ? 'JOIN' : 'LEFT JOIN';
|
|
$stmt = $pdo->query(
|
|
"SELECT M.MitarbeiterID
|
|
FROM kl_Mitarbeiter M
|
|
{$join} kl_Kaffeeverbrauch V ON M.MitarbeiterID = V.MitarbeiterID AND V.Datum >= DATE_SUB(NOW(), INTERVAL 100 DAY)
|
|
WHERE M.aktiv = 1
|
|
GROUP BY M.MitarbeiterID
|
|
HAVING COALESCE(SUM(V.AnzahlStriche), 0) {$vergleich}"
|
|
);
|
|
$legacyIds = array_map('intval', $stmt->fetchAll(PDO::FETCH_COLUMN));
|
|
|
|
return ledger_fetch_participant_summaries($pdo, $tenantId, ['legacy_mitarbeiter_ids' => $legacyIds]);
|
|
}
|
|
|
|
$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;
|
|
$datum = date("Y-m-d H:i:s"); // Das aktuelle Datum verwenden
|
|
|
|
// Legacy-Verknuepfung je Teilnehmer vorab laden, damit pro Zeile
|
|
// entschieden werden kann, ob (Default-Mandant) zusaetzlich in
|
|
// kl_Einzahlungen 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_Einzahlungen (MitarbeiterID, Betrag, Datum) VALUES (?, ?, ?)"
|
|
);
|
|
|
|
foreach ($_POST["anzahlBetrag"] ?? [] as $participantId => $anzahlBetrag) {
|
|
$participantId = (int)$participantId;
|
|
$anzahlBetrag = floatval($anzahlBetrag);
|
|
if ($participantId <= 0 || $anzahlBetrag == 0.0 || !array_key_exists($participantId, $legacyMap)) {
|
|
continue;
|
|
}
|
|
|
|
$legacyMitarbeiterId = $legacyMap[$participantId];
|
|
if ($legacyMitarbeiterId !== null) {
|
|
$insertLegacy->execute([$legacyMitarbeiterId, $anzahlBetrag, $datum]);
|
|
$legacyPaymentId = (int)$pdo->lastInsertId();
|
|
ledger_mirror_legacy_payment($pdo, $tenantId, $legacyPaymentId);
|
|
} else {
|
|
ledger_record_payment($pdo, $tenantId, $participantId, (int)round($anzahlBetrag * 100), 'manual_bulk');
|
|
}
|
|
$eingetragen++;
|
|
}
|
|
|
|
$pdo->commit();
|
|
} catch (Throwable $e) {
|
|
if ($pdo->inTransaction()) {
|
|
$pdo->rollBack();
|
|
}
|
|
$fehlgeschlagen = true;
|
|
}
|
|
|
|
$action = 'alle';
|
|
} else {
|
|
$action = (string)($_GET['action'] ?? 'alle');
|
|
}
|
|
|
|
$mitarbeiter = einzahlung_fetch_participants($pdo, $tenantId, $action);
|
|
|
|
?>
|
|
|
|
<h2>Einzahlungen für alle Mitarbeiter</h2>
|
|
|
|
<?php if ($hatGespeichert): ?>
|
|
<?php if ($fehlgeschlagen): ?>
|
|
<div class="hint-box error"><p>Die Einzahlungen 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="einzahlung.php" method="get">
|
|
<input type="hidden" name="action" value="vorderseite">
|
|
<button type="submit">Vorderseite</button>
|
|
</form>
|
|
</li>
|
|
<li>
|
|
<form action="einzahlung.php" method="get">
|
|
<input type="hidden" name="action" value="rueckseite">
|
|
<button type="submit">Rückseite</button>
|
|
</form>
|
|
</li>
|
|
<li>
|
|
<form action="einzahlung.php" method="get">
|
|
<input type="hidden" name="action" value="alle">
|
|
<button type="submit">Alle</button>
|
|
</form>
|
|
</li>
|
|
<li>
|
|
<form action="letzteneintraege.php" method="get">
|
|
<button type="submit">Letzten Einträge</button>
|
|
</form>
|
|
</li>
|
|
<li>
|
|
<form action="csvupload.php" method="get">
|
|
<button type="submit">CSV Upload</button>
|
|
</form>
|
|
</li>
|
|
</ul>
|
|
<form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>">
|
|
<?php echo app_csrf_field(); ?>
|
|
<?php
|
|
|
|
echo "<table>";
|
|
echo " <tr>
|
|
<th>Mitarbeiter</th>
|
|
<th>Betrag</th>
|
|
</tr>";
|
|
foreach ($mitarbeiter as $teilnehmer) {
|
|
$participantId = $teilnehmer['participant_id'];
|
|
$name = saas_html($teilnehmer['display_name']);
|
|
echo "<tr>";
|
|
echo "<td><label for='anzahlBetrag[$participantId]'>$name </label></td>";
|
|
echo "<td><input type='number' name='anzahlBetrag[$participantId]' step='0.01'></td>";
|
|
echo "</tr>";
|
|
}
|
|
echo "</table>";
|
|
?>
|
|
|
|
<button type="submit" name="submit" value="Speichern" >Eintragen</button>
|
|
</form>
|
|
|
|
</div>
|
|
</section>
|
|
|
|
<?php include "footer.php"; ?>
|