M5: Sammelerfassung (Striche/Einzahlungen) tenant-nativ machen

stricheintragen.php und einzahlung.php lasen ihren Mitarbeiter-Picker
bisher direkt aus der global unscoped kl_Mitarbeiter-Tabelle. Fuer jeden
Mandanten ausser dem Default-Mandanten zeigte das fremde Namen und
Schreiben schlug (sicher, aber unverstaendlich) am Tenant-Check in
ledger_mirror_legacy_* fehl.

- Picker kommt jetzt aus participants (tenant-scoped), Formularfelder
  nutzen participant_id statt MitarbeiterID.
- Schreibpfad pro Teilnehmer: mit legacy_mitarbeiter_id (Default-Mandant)
  weiterhin Dual-Write nach kl_Kaffeeverbrauch/kl_Einzahlungen plus
  Ledger-Spiegelung; ohne Legacy-Verknuepfung (jeder andere Mandant) direkt
  ueber neue ledger_record_consumption()/ledger_record_payment().
- Vorderseite/Rueckseite-Filter (100-Tage-Regel) bleiben fuer den
  Default-Mandanten exakt auf der bisherigen Legacy-Logik; andere
  Mandanten nutzen die neue ledger_fetch_participants_by_window_marks()
  mit tenant_settings.sheet_window_days.
- Nebenbei behoben: einzahlung.php verlinkte auf ?aktion=... statt
  ?action=..., wodurch die Vorderseite/Rueckseite-Buttons nie griffen.
  Preis-pro-Strich-Vorbelegung kommt jetzt aus tenant_settings statt der
  seit M3 nicht mehr gepflegten kl_config-Tabelle.
- Live getestet: isolierter Test-Mandant, Picker zeigt nur eigene
  Teilnehmer, Buchungen rein Ledger-nativ mit korrektem Saldo, 10-Striche-
  Schwelle korrekt sortiert, Default-Mandant-Dual-Write weiterhin gruen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 00:57:51 +02:00
co-authored by Claude Sonnet 5
parent aeb687f42e
commit 4d5f32ae6d
5 changed files with 346 additions and 150 deletions
+79
View File
@@ -564,6 +564,85 @@ function ledger_void_entry_by_legacy_id(PDO $pdo, int $tenantId, string $legacyT
return $stmt->rowCount() > 0; return $stmt->rowCount() > 0;
} }
/**
* Records a consumption entry straight into the ledger, without any legacy
* kl_Kaffeeverbrauch row. Used for tenants that have no legacy shadow table
* (every tenant except the migrated default tenant).
*/
function ledger_record_consumption(PDO $pdo, int $tenantId, int $participantId, int $marks, int $unitPriceCents, string $source, ?int $createdByUserId = null): int
{
$stmt = $pdo->prepare(
"INSERT INTO ledger_entries
(tenant_id, participant_id, type, amount_cents, marks_count, unit_price_cents, booked_at, source, created_by_user_id)
SELECT ?, id, 'consumption', ?, ?, ?, NOW(), ?, ?
FROM participants
WHERE id = ? AND tenant_id = ?"
);
$stmt->execute([$tenantId, -($marks * $unitPriceCents), $marks, $unitPriceCents, $source, $createdByUserId, $participantId, $tenantId]);
if ($stmt->rowCount() !== 1) {
throw new RuntimeException('Der Strich-Eintrag konnte nicht gespeichert werden.');
}
return (int)$pdo->lastInsertId();
}
/**
* Records a payment entry straight into the ledger, without any legacy
* kl_Einzahlungen row. Used for tenants that have no legacy shadow table.
*/
function ledger_record_payment(PDO $pdo, int $tenantId, int $participantId, int $amountCents, string $source, ?int $createdByUserId = null): int
{
$stmt = $pdo->prepare(
"INSERT INTO ledger_entries
(tenant_id, participant_id, type, amount_cents, booked_at, source, created_by_user_id)
SELECT ?, id, 'payment', ?, NOW(), ?, ?
FROM participants
WHERE id = ? AND tenant_id = ?"
);
$stmt->execute([$tenantId, $amountCents, $source, $createdByUserId, $participantId, $tenantId]);
if ($stmt->rowCount() !== 1) {
throw new RuntimeException('Die Einzahlung konnte nicht gespeichert werden.');
}
return (int)$pdo->lastInsertId();
}
/**
* Ledger-native equivalent of the legacy "100-Tage-Liste" front/back split:
* active participants whose summed marks within the trailing window are at
* or above the ten-mark threshold ($atLeastTen), or below it. Used for
* tenants without a legacy kl_Kaffeeverbrauch history to filter against.
*
* @return list<array{participant_id: int, tenant_id: int, legacy_mitarbeiter_id: ?int, display_name: string, email: ?string, email_norm: ?string, paypal_name: ?string, active: bool, total_payments_cents: int, total_costs_cents: int, total_marks: int, balance_cents: int, year_payments_cents: int, year_costs_cents: int, year_marks: int}>
*/
function ledger_fetch_participants_by_window_marks(PDO $pdo, int $tenantId, int $windowDays, bool $atLeastTen): array
{
$stmt = $pdo->prepare(
"SELECT p.id
FROM participants p
LEFT JOIN ledger_entries le
ON le.tenant_id = p.tenant_id
AND le.participant_id = p.id
AND le.type = 'consumption'
AND le.voided_at IS NULL
AND le.booked_at >= DATE_SUB(NOW(), INTERVAL ? DAY)
WHERE p.tenant_id = ?
AND p.active = 1
GROUP BY p.id
HAVING COALESCE(SUM(le.marks_count), 0) " . ($atLeastTen ? '>= 10' : '< 10')
);
$stmt->execute([$windowDays, $tenantId]);
$ids = array_map('intval', $stmt->fetchAll(PDO::FETCH_COLUMN));
if ($ids === []) {
return [];
}
return ledger_fetch_participant_summaries($pdo, $tenantId, ['participant_ids' => $ids]);
}
/** /**
* Options: * Options:
* - participant_id: int * - participant_id: int
+53 -8
View File
@@ -247,18 +247,60 @@ gepatcht:
auf `kaffeeliste.php`), Zugang entzogen, anschließender Login-Versuch auf `kaffeeliste.php`), Zugang entzogen, anschließender Login-Versuch
korrekt mit „kein aktiver Mandant" abgelehnt. korrekt mit „kein aktiver Mandant" abgelehnt.
## Fortsetzung: Sammelerfassung tenant-nativ
Umgesetzte Dateien:
```text
stricheintragen.php (Picker + Schreibpfad neu)
einzahlung.php (Picker + Schreibpfad neu)
app/ledger.php (ledger_record_consumption, ledger_record_payment,
ledger_fetch_participants_by_window_marks)
```
`stricheintragen.php` und `einzahlung.php` lasen ihren Mitarbeiter-Picker
bisher direkt aus der global unscoped `kl_Mitarbeiter`-Tabelle. Für jeden
Mandanten außer dem Default-Mandanten war das faktisch nutzlos: Die Liste
zeigte fremde (Default-Mandanten-)Namen an, und Schreiben schlug wegen des
Tenant-Checks in `ledger_mirror_legacy_*` sicher, aber ohne verständliche
Fehlermeldung fehl.
- Der Picker kommt jetzt aus `ledger_fetch_participant_summaries()`
(tenant-scoped), Formularfelder verwenden `participant_id` statt
`MitarbeiterID`.
- Beim Speichern wird pro Teilnehmer entschieden: Hat der Teilnehmer eine
`legacy_mitarbeiter_id` (Default-Mandant), läuft der Schreibpfad wie
bisher über `kl_Kaffeeverbrauch`/`kl_Einzahlungen` plus Ledger-Spiegelung.
Ohne Legacy-Verknüpfung (jeder andere Mandant) wird direkt über die neuen
Funktionen `ledger_record_consumption()`/`ledger_record_payment()` ins
Ledger gebucht, ohne Legacy-Tabellen zu berühren.
- Die "Vorderseite"/"Rückseite"-Filter (100-Tage-Listen-Regel, `>= 10`
beziehungsweise `< 10` Striche im Fenster) bleiben für den Default-
Mandanten exakt auf der bisherigen Legacy-Logik (Anker: jüngstes Datum in
`kl_Kaffeeverbrauch`). Andere Mandanten nutzen die neue, Ledger-native
Fensterprüfung `ledger_fetch_participants_by_window_marks()` mit dem in
`tenant_settings.sheet_window_days` konfigurierten Fenster.
- Nebenbei zwei Legacy-Bugs behoben: In `einzahlung.php` verlinkten die
Vorderseite/Rückseite/Alle-Buttons auf `?aktion=...`, ausgewertet wurde
aber `$_GET['action']` die Filter griffen also nie. Und in beiden
Dateien blieb nach einem POST-Speichern `$sqlMitarbeiter` unbelegt
(behoben bereits in der vorherigen Session, jetzt strukturell nicht mehr
möglich, da die Anzeige nach dem Speichern regulär über denselben
Picker-Code läuft).
- Preis-pro-Strich-Vorbelegung kommt jetzt aus `tenant_settings.mark_price_cents`
statt aus der mandantenunabhängigen `kl_config`-Tabelle. Das ist auch für
den Default-Mandanten eine Korrektur: `kl_config` wird seit M3 nicht mehr
über `mandant-einstellungen.php` gepflegt und kann veraltet sein.
- Live gegen die Dev-Datenbank getestet: eigener isolierter Test-Mandant
angelegt, Picker zeigt nur dessen Teilnehmer, Striche und Einzahlung rein
Ledger-nativ gebucht (korrekter Saldo), Vorder-/Rückseiten-Schwelle bei
10 Strichen korrekt ein-/aussortiert, Default-Mandant weiterhin mit
Dual-Write geprüft. Alle Testdaten anschließend entfernt.
## Noch offen ## Noch offen
- Eigene PayPal-/Zahlungsbereich als eigenständiger App-Screen (aktuell nur im - Eigene PayPal-/Zahlungsbereich als eigenständiger App-Screen (aktuell nur im
Dashboard integriert). Dashboard integriert).
- `stricheintragen.php` und `einzahlung.php` lesen ihre Mitarbeiter-Picker
weiterhin aus der global unscoped `kl_Mitarbeiter`-Tabelle. Für den
Default-Mandanten funktioniert das unverändert; für jeden anderen Mandanten
ist die Liste faktisch leer beziehungsweise zeigt (nur lesend, Schreiben
schlägt dank Tenant-Scope in `ledger_mirror_legacy_*` sicher fehl) die
Namen der Default-Mandanten-Mitglieder an. Das ist ein bestehendes,
eigenständiges Scope-Thema für eine spätere Iteration, keine Regression
dieser Session.
- Export, Mail und Jahresprozesse bleiben M6-Themen. - Export, Mail und Jahresprozesse bleiben M6-Themen.
## Aktueller Prüfstatus ## Aktueller Prüfstatus
@@ -276,3 +318,6 @@ gepatcht:
(korrekt escaped), Soft-Delete, Banner-Anzeige auf Mandant geprüft. (korrekt escaped), Soft-Delete, Banner-Anzeige auf Mandant geprüft.
- Live-Test Zugangsvergabe/-entzug: kompletter Flow von Einladung bis - Live-Test Zugangsvergabe/-entzug: kompletter Flow von Einladung bis
Login-Sperre nach Entzug erfolgreich geprüft (siehe oben). Login-Sperre nach Entzug erfolgreich geprüft (siehe oben).
- Live-Test Sammelerfassung für Nicht-Default-Mandanten: eigener Test-Tenant,
Striche/Einzahlung Ledger-nativ gebucht, Vorder-/Rückseiten-Fenster
geprüft (siehe oben).
+8 -1
View File
@@ -534,7 +534,14 @@ Schritte:
die Revision erhalten. die Revision erhalten.
- Sammelerfassung (`stricheintragen.php`, `einzahlung.php`) tenant-sicher - Sammelerfassung (`stricheintragen.php`, `einzahlung.php`) tenant-sicher
absichern und ans Ledger anbinden: erledigt. Beide Seiten hatten zuvor absichern und ans Ledger anbinden: erledigt. Beide Seiten hatten zuvor
keine Zugriffskontrolle außer CSRF; das ist behoben. keine Zugriffskontrolle außer CSRF; das ist behoben. Zusätzlich lasen
beide Seiten ihre Mitarbeiterliste aus der global unscoped
`kl_Mitarbeiter`-Tabelle, was sie für jeden Mandanten außer dem
Default-Mandanten unbrauchbar machte. Der Picker kommt jetzt aus
`participants` (tenant-scoped); der Schreibpfad nutzt für den
Default-Mandanten weiterhin Dual-Write nach `kl_Kaffeeverbrauch`/
`kl_Einzahlungen`, für alle anderen Mandanten schreibt er direkt und
ausschließlich ins Ledger.
- Hinweise als tenant-spezifische Notices umsetzen: erledigt. Neue Tabelle - Hinweise als tenant-spezifische Notices umsetzen: erledigt. Neue Tabelle
`notices` mit Soft-Delete, `hinweise.php` und die Banner-Anzeige in `notices` mit Soft-Delete, `hinweise.php` und die Banner-Anzeige in
`header.php` sind tenant-scoped umgestellt; `kl_hinweise` bleibt nur noch `header.php` sind tenant-scoped umgestellt; `kl_hinweise` bleibt nur noch
+90 -61
View File
@@ -42,28 +42,85 @@ if (!$hasAccess) {
exit; 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 // Verarbeitung des Formulars, wenn es gesendet wurde
if ($_SERVER["REQUEST_METHOD"] == "POST" ) { if ($_SERVER["REQUEST_METHOD"] == "POST" ) {
$hatGespeichert = true;
$datum = date("Y-m-d H:i:s"); // Das aktuelle Datum verwenden $datum = date("Y-m-d H:i:s"); // Das aktuelle Datum verwenden
$eingetragen = 0;
$fehlgeschlagen = false; // 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 { try {
$pdo->beginTransaction(); $pdo->beginTransaction();
$insert = $pdo->prepare( $insertLegacy = $pdo->prepare(
"INSERT INTO kl_Einzahlungen (MitarbeiterID, Betrag, Datum) VALUES (?, ?, ?)" "INSERT INTO kl_Einzahlungen (MitarbeiterID, Betrag, Datum) VALUES (?, ?, ?)"
); );
foreach ($_POST["anzahlBetrag"] ?? [] as $mitarbeiterID => $anzahlBetrag) { foreach ($_POST["anzahlBetrag"] ?? [] as $participantId => $anzahlBetrag) {
$mitarbeiterID = (int)$mitarbeiterID; $participantId = (int)$participantId;
$anzahlBetrag = floatval($anzahlBetrag); $anzahlBetrag = floatval($anzahlBetrag);
if ($mitarbeiterID <= 0 || $anzahlBetrag == 0.0) { if ($participantId <= 0 || $anzahlBetrag == 0.0 || !array_key_exists($participantId, $legacyMap)) {
continue; continue;
} }
$insert->execute([$mitarbeiterID, $anzahlBetrag, $datum]); $legacyMitarbeiterId = $legacyMap[$participantId];
$legacyPaymentId = (int)$pdo->lastInsertId(); if ($legacyMitarbeiterId !== null) {
ledger_mirror_legacy_payment($pdo, $tenantId, $legacyPaymentId); $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++; $eingetragen++;
} }
@@ -75,51 +132,13 @@ if ($_SERVER["REQUEST_METHOD"] == "POST" ) {
$fehlgeschlagen = true; $fehlgeschlagen = true;
} }
if ($fehlgeschlagen) { $action = 'alle';
echo "Die Einzahlungen konnten nicht gespeichert werden."; } else {
} else { $action = (string)($_GET['action'] ?? 'alle');
echo $eingetragen . " Einträge erfolgreich hinzugefügt.";
}
// Mitarbeiter aus der Datenbank abrufen und nach Namen sortieren
$sqlMitarbeiter = "SELECT MitarbeiterID, Name FROM kl_Mitarbeiter WHERE aktiv = 1 ORDER BY Name";
}elseif($_SERVER["REQUEST_METHOD"] == "GET" AND isset($_GET["action"])){
if($_GET["action"] == "vorderseite"){
$sqlMitglieder = "SELECT M.MitarbeiterID, M.Name, M.Email
FROM kl_Mitarbeiter M
JOIN kl_Kaffeeverbrauch V ON M.MitarbeiterID = V.MitarbeiterID
WHERE V.Datum >= DATEADD(DAY, -100, GETDATE()) AND M.aktiv = 1
GROUP BY M.MitarbeiterID, M.Name, M.Email
HAVING SUM(V.AnzahlStriche) >= 10
ORDER BY Name;";
}elseif( $_GET["action"] == "rueckseite"){
$sqlMitglieder = "SELECT M.MitarbeiterID, M.Name, M.Email
FROM kl_Mitarbeiter M
LEFT JOIN kl_Kaffeeverbrauch V ON M.MitarbeiterID = V.MitarbeiterID AND V.Datum >= DATEADD(DAY, -100, GETDATE())
WHERE M.aktiv = 1
GROUP BY M.MitarbeiterID, M.Name, M.Email
HAVING COALESCE(SUM(V.AnzahlStriche), 0) < 10
ORDER BY M.Name;";
}elseif($_GET["action"] == "alle"){
// Mitarbeiter aus der Datenbank abrufen und nach Namen sortieren
$sqlMitarbeiter = "SELECT MitarbeiterID, Name FROM kl_Mitarbeiter WHERE aktiv = 1 ORDER BY Name";
}
}else{
// Mitarbeiter aus der Datenbank abrufen und nach Namen sortieren
$sqlMitarbeiter = "SELECT MitarbeiterID, Name FROM kl_Mitarbeiter WHERE aktiv = 1 ORDER BY Name";
} }
$stmtMitarbeiter = sqlsrv_query($conn, $sqlMitarbeiter); $mitarbeiter = einzahlung_fetch_participants($pdo, $tenantId, $action);
// Schließe die Verbindung zur Datenbank
#sqlsrv_close($conn);
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
@@ -132,19 +151,31 @@ $stmtMitarbeiter = sqlsrv_query($conn, $sqlMitarbeiter);
<body> <body>
<h2>Einzahlungen für alle Mitarbeiter</h2> <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"> <ul class="actions">
<li> <li>
<form action="einzahlung.php?aktion=vorderseite" method="get"> <form action="einzahlung.php" method="get">
<input type="hidden" name="action" value="vorderseite">
<button type="submit">Vorderseite</button> <button type="submit">Vorderseite</button>
</form> </form>
</li> </li>
<li> <li>
<form action="einzahlung.php?aktion=rueckseite" method="get"> <form action="einzahlung.php" method="get">
<input type="hidden" name="action" value="rueckseite">
<button type="submit">Rückseite</button> <button type="submit">Rückseite</button>
</form> </form>
</li> </li>
<li> <li>
<form action="einzahlung.php?aktion=alle" method="get"> <form action="einzahlung.php" method="get">
<input type="hidden" name="action" value="alle">
<button type="submit">Alle</button> <button type="submit">Alle</button>
</form> </form>
</li> </li>
@@ -168,14 +199,12 @@ $stmtMitarbeiter = sqlsrv_query($conn, $sqlMitarbeiter);
<th>Mitarbeiter</th> <th>Mitarbeiter</th>
<th>Betrag</th> <th>Betrag</th>
</tr>"; </tr>";
// Iteriere über alle Mitarbeiter foreach ($mitarbeiter as $teilnehmer) {
while ($row = sqlsrv_fetch_array($stmtMitarbeiter, SQLSRV_FETCH_ASSOC)) { $participantId = $teilnehmer['participant_id'];
$mitarbeiterID = $row["MitarbeiterID"]; $name = saas_html($teilnehmer['display_name']);
$mitarbeiterName = $row["Name"];
echo "<tr>"; echo "<tr>";
// Formularfeld für die Anzahl der Striche echo "<td><label for='anzahlBetrag[$participantId]'>$name </label></td>";
echo "<td><label for='anzahlBetrag[$mitarbeiterID]'>$mitarbeiterName </label></td>"; echo "<td><input type='number' name='anzahlBetrag[$participantId]' step='0.01'></td>";
echo "<td><input type='number' name='anzahlBetrag[$mitarbeiterID]' step='0.01'></td>";
echo "</tr>"; echo "</tr>";
} }
echo "</table>"; echo "</table>";
+104 -68
View File
@@ -43,30 +43,102 @@ if (!$hasAccess) {
exit; 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 // Verarbeitung des Formulars, wenn es gesendet wurde
if ($_SERVER["REQUEST_METHOD"] == "POST") { if ($_SERVER["REQUEST_METHOD"] == "POST") {
$hatGespeichert = true;
$kostenproStrich = floatval($_POST["kostenproStrich"] ?? 0); $kostenproStrich = floatval($_POST["kostenproStrich"] ?? 0);
$datum = date("Y-m-d H:i:s"); // Das aktuelle Datum verwenden $unitPriceCents = (int)round($kostenproStrich * 100);
$eingetragen = 0; $datum = date("Y-m-d H:i:s");
$fehlgeschlagen = false;
// 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 { try {
$pdo->beginTransaction(); $pdo->beginTransaction();
$insert = $pdo->prepare( $insertLegacy = $pdo->prepare(
"INSERT INTO kl_Kaffeeverbrauch (MitarbeiterID, AnzahlStriche, Kosten, KostenproStrich, Datum) VALUES (?, ?, ?, ?, ?)" "INSERT INTO kl_Kaffeeverbrauch (MitarbeiterID, AnzahlStriche, Kosten, KostenproStrich, Datum) VALUES (?, ?, ?, ?, ?)"
); );
foreach ($_POST["anzahlStriche"] ?? [] as $mitarbeiterID => $anzahlStriche) { foreach ($_POST["anzahlStriche"] ?? [] as $participantId => $anzahlStriche) {
$mitarbeiterID = (int)$mitarbeiterID; $participantId = (int)$participantId;
$anzahlStriche = (int)$anzahlStriche; $anzahlStriche = (int)$anzahlStriche;
if ($mitarbeiterID <= 0 || $anzahlStriche === 0) { if ($participantId <= 0 || $anzahlStriche === 0 || !array_key_exists($participantId, $legacyMap)) {
continue; continue;
} }
$kosten = $anzahlStriche * $kostenproStrich; $legacyMitarbeiterId = $legacyMap[$participantId];
$insert->execute([$mitarbeiterID, $anzahlStriche, $kosten, $kostenproStrich, $datum]); if ($legacyMitarbeiterId !== null) {
$legacyConsumptionId = (int)$pdo->lastInsertId(); $kosten = $anzahlStriche * $kostenproStrich;
ledger_mirror_legacy_consumption($pdo, $tenantId, $legacyConsumptionId); $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++; $eingetragen++;
} }
@@ -78,55 +150,14 @@ if ($_SERVER["REQUEST_METHOD"] == "POST") {
$fehlgeschlagen = true; $fehlgeschlagen = true;
} }
if ($fehlgeschlagen) { $action = 'alle';
echo "Die Einträge konnten nicht gespeichert werden."; } else {
} else { $action = (string)($_GET['action'] ?? 'alle');
echo $eingetragen . " Einträge erfolgreich hinzugefügt.";
}
// Mitarbeiter aus der Datenbank abrufen und nach Namen sortieren
$sqlMitarbeiter = "SELECT MitarbeiterID, Name FROM kl_Mitarbeiter WHERE aktiv = 1 ORDER BY Name";
}elseif($_SERVER["REQUEST_METHOD"] == "GET" AND isset($_GET["action"])){
if($_GET["action"] == "vorderseite"){
$sqlMitarbeiter = "SELECT M.MitarbeiterID, M.Name, M.Email
FROM kl_Mitarbeiter M
JOIN kl_Kaffeeverbrauch V ON M.MitarbeiterID = V.MitarbeiterID
WHERE V.Datum >= DATEADD(DAY, -100, (SELECT MAX(Datum) FROM kl_Kaffeeverbrauch WHERE Datum < CAST(GETDATE() AS DATE))) AND M.aktiv = 1
GROUP BY M.MitarbeiterID, M.Name, M.Email
HAVING SUM(V.AnzahlStriche) >= 10
ORDER BY Name;";
}elseif( $_GET["action"] == "rueckseite"){
$sqlMitarbeiter = "SELECT M.MitarbeiterID, M.Name, M.Email
FROM kl_Mitarbeiter M
LEFT JOIN kl_Kaffeeverbrauch V ON M.MitarbeiterID = V.MitarbeiterID AND V.Datum >= DATEADD(DAY, -100, (SELECT MAX(Datum) FROM kl_Kaffeeverbrauch))
WHERE M.aktiv = 1
GROUP BY M.MitarbeiterID, M.Name, M.Email
HAVING COALESCE(SUM(V.AnzahlStriche), 0) < 10
ORDER BY M.Name;";
}elseif($_GET["action"] == "alle"){
// Mitarbeiter aus der Datenbank abrufen und nach Namen sortieren
$sqlMitarbeiter = "SELECT MitarbeiterID, Name FROM kl_Mitarbeiter WHERE aktiv = 1 ORDER BY Name";
}
}else{
// Mitarbeiter aus der Datenbank abrufen und nach Namen sortieren
$sqlMitarbeiter = "SELECT MitarbeiterID, Name FROM kl_Mitarbeiter WHERE aktiv = 1 ORDER BY Name";
} }
$stmtMitarbeiter = sqlsrv_query($conn, $sqlMitarbeiter);
// Kosten pro Strich auslesen $mitarbeiter = stricheintragen_fetch_participants($pdo, $tenantId, $action);
$sqlKostenproStrich = "SELECT KostenproStrich FROM kl_config "; $settings = saas_fetch_tenant_settings($pdo, $tenantId);
$stmtKostenproStrich = sqlsrv_query($conn, $sqlKostenproStrich); $kostenproStrichAnzeige = number_format(($settings['mark_price_cents'] ?? 20) / 100, 2, '.', '');
$row = sqlsrv_fetch_array($stmtKostenproStrich, SQLSRV_FETCH_ASSOC);
$KostenproStrichtemp = $row["KostenproStrich"];
$KostenproStrich = number_format($KostenproStrichtemp, 2, '.', '');
?> ?>
@@ -140,6 +171,15 @@ $KostenproStrich = number_format($KostenproStrichtemp, 2, '.', '');
<body> <body>
<h2>Anzahl der Striche für alle Mitarbeiter</h2> <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"> <ul class="actions">
<li> <li>
<form action="stricheintragen.php" method="get"> <form action="stricheintragen.php" method="get">
@@ -166,7 +206,7 @@ $KostenproStrich = number_format($KostenproStrichtemp, 2, '.', '');
<?php <?php
echo "<label for='kostenproStrich'>Kosten pro Strich:</label> echo "<label for='kostenproStrich'>Kosten pro Strich:</label>
<input type='number' name='kostenproStrich' step='0.01' value='" . $KostenproStrich . "'><br>"; <input type='number' name='kostenproStrich' step='0.01' value='" . saas_html($kostenproStrichAnzeige) . "'><br>";
echo "<table>"; echo "<table>";
echo " <tr> echo " <tr>
@@ -174,17 +214,13 @@ $KostenproStrich = number_format($KostenproStrichtemp, 2, '.', '');
<th>Anzahl Striche</th> <th>Anzahl Striche</th>
</tr>"; </tr>";
// Iteriere über alle Mitarbeiter foreach ($mitarbeiter as $teilnehmer) {
while ($row = sqlsrv_fetch_array($stmtMitarbeiter, SQLSRV_FETCH_ASSOC)) { $participantId = $teilnehmer['participant_id'];
$mitarbeiterID = $row["MitarbeiterID"]; $name = saas_html($teilnehmer['display_name']);
$mitarbeiterName = $row["Name"];
echo "<tr>"; echo "<tr>";
echo "<td><label for='anzahlStriche[$participantId]'>$name:</label></td>";
// Formularfeld für die Anzahl der Striche echo "<td><input type='number' name='anzahlStriche[$participantId]' ></td>";
echo "<td><label for='anzahlStriche[$mitarbeiterID]'>$mitarbeiterName:</label></td>";
echo "<td><input type='number' name='anzahlStriche[$mitarbeiterID]' ></td>";
echo "</tr>"; echo "</tr>";
} }
echo "</table>"; echo "</table>";