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;
}
/**
* 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:
* - participant_id: int