Files
kaffeekasse-saas/einzahlung.php
T
clemensandClaude Opus 5 d24ee9bf91 Betreiber-Zentralstelle, Mandanten-Design und schlankerer Einstieg
Funktions-Schalter je Mandant (app/features.php, tenant_features): der
Betreiber schaltet FAQ, Selbsteintrag, PDF, PayPal-Eingang, CSV-Import,
Mailversand, Jahresabschluss, Datenexport und eigenes Design pro Mandant
frei. Gesperrte Funktionen verschwinden aus Menue und Schaltflaechen, ihre
Seiten weisen Aufrufe und POSTs ab. Das Back-Office ist jetzt fuer
Platform-Admins im Menue verlinkt statt nur per URL erreichbar.

Eigenes Design je Mandant: Akzentfarbe und Logo in den Mandant-
Einstellungen, eingebettet ueber app/branding.php; das Logo liegt
geschuetzt in var/tenant_logos und wird nur ueber
tenant-logo-anzeigen.php an den eigenen Mandanten ausgeliefert.

Weniger Startinformationen: Startpaket neuer Mandanten auf zwei
Beispielfragen gekuerzt, Anleitung von ~1400 auf ~750 Woerter gestrafft
und um Abschnitte zu gesperrten Funktionen bereinigt.

Vorder-/Rueckseite erst bei mehr als 50 Personen (vorher schon ab 50).
Die Schwelle liegt jetzt gemeinsam in app/ledger.php und gilt auch fuer
die Vorder-/Rueckseiten-Auswahl beim Erfassen, wo sie bisher unabhaengig
von der Teamgroesse angeboten wurde.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 22:52:55 +02:00

302 lines
9.0 KiB
PHP

<?php
ob_start();
include "functions.php";
require_once __DIR__ . "/app/ledger.php";
require_once __DIR__ . "/app/features.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) {
echo "<h2>Kein Zugriff</h2>";
include "footer.php";
exit;
}
/**
* Liefert die Teilnehmerliste fuer die Vorder-/Rueckseiten- oder Alle-Auswahl.
* Siehe stricheintragen.php fuer dieselbe Logik.
*
* @return list<array>
*/
function einzahlung_fetch_participants(PDO $pdo, int $tenantId, string $action): array
{
if ($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;
$erfolgsmeldung = null;
$validierungsFehler = [];
$eingaben = [];
if (isset($_SESSION['flash_einzahlung']) && is_array($_SESSION['flash_einzahlung'])) {
$flash = $_SESSION['flash_einzahlung'];
unset($_SESSION['flash_einzahlung']);
if (($flash['type'] ?? '') === 'success') {
$erfolgsmeldung = sprintf('%d Einträge erfolgreich hinzugefügt.', (int)($flash['count'] ?? 0));
}
}
// Obergrenze je Einzahlungszeile. Faengt Groessenordnungs-Tippfehler ab
// (z. B. 500 statt 5,00); groessere Betraege lassen sich in mehreren
// Schritten eintragen.
const EINZAHLUNG_MAX_BETRAG = 1000.00;
const EINZAHLUNG_MAX_BETRAG_CENTS = 100000;
// Verarbeitung des Formulars, wenn es gesendet wurde
if ($_SERVER["REQUEST_METHOD"] == "POST" ) {
$hatGespeichert = true;
// Teilnehmer des Mandanten: dient zugleich der Zugehoerigkeitspruefung und
// liefert die Namen fuer verstaendliche Fehlermeldungen.
$namen = [];
$stmt = $pdo->prepare('SELECT id, display_name FROM participants WHERE tenant_id = ?');
$stmt->execute([$tenantId]);
foreach ($stmt->fetchAll() as $row) {
$namen[(int)$row['id']] = (string)$row['display_name'];
}
// Erst alle Zeilen einsammeln und pruefen, dann buchen. Schlaegt eine
// Zeile fehl, wird gar nichts gebucht - sonst waere nach einem Tippfehler
// unklar, was schon drin ist und was nicht.
$zuBuchen = [];
foreach ($_POST["anzahlBetrag"] ?? [] as $participantId => $anzahlBetrag) {
$participantId = (int)$participantId;
if ($participantId <= 0 || !array_key_exists($participantId, $namen)) {
continue;
}
$rohBetrag = trim((string)$anzahlBetrag);
$bemerkung = trim((string)($_POST['bemerkung'][$participantId] ?? ''));
$eingaben[$participantId] = ['betrag' => $rohBetrag, 'bemerkung' => $bemerkung];
$name = $namen[$participantId] ?? ('Teilnehmer ' . $participantId);
if ($rohBetrag === '') {
// Leere Zeile: nur meckern, wenn trotzdem eine Bemerkung dransteht
// (sonst hat jemand getippt und den Betrag vergessen).
if ($bemerkung !== '') {
$validierungsFehler[] = sprintf(
'%s: Es wurde eine Bemerkung ohne Betrag angegeben.',
$namen[$participantId] ?? ('Teilnehmer ' . $participantId)
);
}
continue;
}
$betragCents = saas_parse_money_cents($rohBetrag);
if ($betragCents === null) {
$validierungsFehler[] = sprintf(
'%s: Der Betrag "%s" ist kein gültiger Geldbetrag.',
$name,
$rohBetrag
);
continue;
}
if ($betragCents === 0) {
continue;
}
// Abzuege muessen begruendet werden, sonst ist spaeter nicht mehr
// nachvollziehbar, warum jemandem Geld abgezogen wurde.
if ($betragCents < 0 && $bemerkung === '') {
$validierungsFehler[] = sprintf(
'%s: Bei einem Abzug (negativer Betrag) ist eine Bemerkung Pflicht.',
$name
);
continue;
}
// Groessenordnungs-Tippfehler abfangen (500 statt 5,00).
if (abs($betragCents) > EINZAHLUNG_MAX_BETRAG_CENTS) {
$validierungsFehler[] = sprintf(
'%s: %s € übersteigt die Plausibilitätsgrenze von %s €. Bitte prüfen oder in mehreren Schritten eintragen.',
$name,
saas_format_money_cents($betragCents),
number_format(EINZAHLUNG_MAX_BETRAG, 2, ',', '.')
);
continue;
}
$zuBuchen[$participantId] = [
'betrag_cents' => $betragCents,
'bemerkung' => $bemerkung !== '' ? $bemerkung : null,
];
}
if ($validierungsFehler !== []) {
$action = 'alle';
} else {
try {
$pdo->beginTransaction();
foreach ($zuBuchen as $participantId => $zeile) {
ledger_record_payment(
$pdo,
$tenantId,
$participantId,
(int)$zeile['betrag_cents'],
'manual_bulk',
$saasUser['user_id'] ?? null,
$zeile['bemerkung']
);
$eingetragen++;
}
$pdo->commit();
$eingaben = []; // Erfolgreich gebucht: Formular wieder leeren.
$_SESSION['flash_einzahlung'] = ['type' => 'success', 'count' => $eingetragen];
header('Location: einzahlung.php');
exit;
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
$fehlgeschlagen = true;
}
$action = 'alle';
}
} else {
$action = (string)($_GET['action'] ?? 'alle');
}
// Vorder-/Rueckseite nur, wenn der Ausdruck auch wirklich zwei Seiten hat
// (siehe ledger_uses_two_sided_sheet). Bei kleinen Teams gibt es nur ein Blatt.
$alleAktiven = ledger_fetch_participant_summaries($pdo, $tenantId, ['active_only' => true]);
$zweiseitig = ledger_uses_two_sided_sheet(count($alleAktiven));
if (!$zweiseitig) {
$action = 'alle';
}
$mitarbeiter = $action === 'alle'
? $alleAktiven
: einzahlung_fetch_participants($pdo, $tenantId, $action);
?>
<h2>Einzahlungen für alle Mitarbeiter</h2>
<?php if ($hatGespeichert): ?>
<?php if ($validierungsFehler !== []): ?>
<div class="hint-box error">
<p><b>Es wurde nichts gebucht.</b> Bitte korrigiere zuerst:</p>
<ul>
<?php foreach ($validierungsFehler as $fehler): ?>
<li><?php echo saas_html($fehler); ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php elseif ($fehlgeschlagen): ?>
<div class="hint-box error"><p>Die Einzahlungen konnten nicht gespeichert werden.</p></div>
<?php endif; ?>
<?php endif; ?>
<?php if ($erfolgsmeldung !== null): ?>
<div class="hint-box success"><p><?php echo saas_html($erfolgsmeldung); ?></p></div>
<?php endif; ?>
<div class="hint-box">
<p>In der Bemerkung könnt ihr festhalten, worum es bei einer Buchung ging. Ein <b>negativer Betrag</b> bucht einen Abzug (z.&nbsp;B. Auszahlung bei Austritt oder eine Erstattung) und braucht deshalb immer eine Bemerkung.</p>
<p><small>Wurde dagegen schlicht <b>falsch eingetragen</b>, ist ein Abzug der falsche Weg: Die Auswertung zählt sonst beide Buchungen als Umsatz. Solche Fehler stattdessen unter <a href="letzteneintraege.php">Letzte Einträge</a> stornieren.</small></p>
</div>
<ul class="actions">
<?php if ($zweiseitig): ?>
<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>
<?php endif; ?>
<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>
<?php if (app_feature_enabled($pdo, $tenantId, 'csv_import')): ?>
<li>
<form action="csvupload.php" method="get">
<button type="submit">CSV Upload</button>
</form>
</li>
<?php endif; ?>
</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>
<th>Bemerkung</th>
</tr>";
foreach ($mitarbeiter as $teilnehmer) {
$participantId = (int)$teilnehmer['participant_id'];
$name = saas_html($teilnehmer['display_name']);
// Nach einem Validierungsfehler die Eingaben stehen lassen.
$betragWert = saas_html($eingaben[$participantId]['betrag'] ?? '');
$bemerkungWert = saas_html($eingaben[$participantId]['bemerkung'] ?? '');
echo "<tr>";
echo "<td><label for='anzahlBetrag[$participantId]'>$name </label></td>";
echo "<td><input type='number' name='anzahlBetrag[$participantId]' step='0.01' value='$betragWert'></td>";
echo "<td><input type='text' name='bemerkung[$participantId]' maxlength='1000' value='$bemerkungWert' placeholder='optional, bei Abzug Pflicht'></td>";
echo "</tr>";
}
echo "</table>";
?>
<button type="submit" name="submit" value="Speichern" >Eintragen</button>
</form>
</div>
</section>
<?php include "footer.php"; ?>