M5 abschliessen: Schreibseiten aufs Ledger umstellen, Storno statt Delete
- stricheintragen.php und einzahlung.php erhielten bisher gar keine Zugriffskontrolle (nur CSRF), jetzt Rollen-/Legacy-Fallback wie in kaffeeliste.php; Sammeleintraege spiegeln transaktional ins Ledger (ledger_mirror_legacy_payment neu ergaenzt). - letzteneintraege.php war rein ueber Legacy-Admin gesperrt und haette neue SaaS-Mandanten ausgeschlossen; Loeschen markiert den gespiegelten Ledger-Eintrag jetzt per voided_at statt ihn zu entfernen (ledger_void_entry_by_legacy_id). - check-m4-ledger-migration.php an das Storno-Modell angepasst: verwaiste Ledger-Zeilen sind nur noch ein Fehler, wenn sie nicht voided sind. - Nebenbei: PHP-Warning bei Sammelerfassung behoben, toten Code entfernt, veraltete README (verwies auf nicht existierende saas-app/-Struktur) korrigiert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,25 +1,36 @@
|
|||||||
# Legacy App Archiv
|
# Kaffeeliste
|
||||||
|
|
||||||
Dieses Verzeichnis enthält den bisherigen PHP-Bestand der Kaffeeliste.
|
Dieses Repository enthält den laufenden Umbau der Kaffeelisten-App von einer
|
||||||
|
Windows/IIS/LDAP-gebundenen Legacy-App zu einer mehrkundenfähigen SaaS-
|
||||||
|
Anwendung. Der Umbau erfolgt schrittweise im selben Repository, nicht in
|
||||||
|
einem getrennten Zielverzeichnis: Legacy-Seiten und neue SaaS-Bausteine
|
||||||
|
liegen nebeneinander, bis eine Seite vollständig auf das neue Modell
|
||||||
|
umgestellt ist.
|
||||||
|
|
||||||
Es bleibt aus drei Gründen im Repository:
|
Den vollständigen Plan mit Zielbild, Datenmodell, Rollenmodell und
|
||||||
|
Meilensteinen beschreibt `docs/saas-umstrukturierungsplan.md`. Der
|
||||||
|
Fortschritt je Meilenstein steht in `docs/m2-technical-foundation.md` bis
|
||||||
|
`docs/m5-app-kern.md`.
|
||||||
|
|
||||||
- als Referenz für die bestehende Fachlogik
|
## Struktur
|
||||||
- als Quelle für Datenmigrationen in die SaaS-Version
|
|
||||||
- als Rückfalloption während der Übergangsphase
|
|
||||||
|
|
||||||
## Wichtige Legacy-Bereiche
|
- Legacy- und SaaS-Seiten liegen als flache PHP-Dateien im Webroot, zum
|
||||||
|
Beispiel `index.php`, `stricheintragen.php`, `kaffeeliste.php`,
|
||||||
|
`mitarbeiterverwalten.php`.
|
||||||
|
- `app/`: zentrale Bausteine für Bootstrap, DB-Zugriff, Auth, Mail und das
|
||||||
|
neue Ledger-Modell (`app/ledger.php`).
|
||||||
|
- `database/migrations/`: versionierte Schemaänderungen, anzuwenden über
|
||||||
|
`scripts/migrate.php`.
|
||||||
|
- `scripts/`: Migrations-, Backfill- und Prüfskripte (Golden Master,
|
||||||
|
HTTP-Smoke, M3/M4-Checks).
|
||||||
|
- `docs/`: Planungs- und Meilensteindokumentation.
|
||||||
|
|
||||||
- `index.php`: persönliches Dashboard
|
## Umgang mit Legacy-Seiten
|
||||||
- `stricheintragen.php`: Sammelerfassung für Kaffee-Striche
|
|
||||||
- `einzahlung.php`: Sammelerfassung für Einzahlungen
|
|
||||||
- `kaffeeliste.php`: operative Gesamtübersicht
|
|
||||||
- `mitarbeiterverwalten.php`: Mitglieder- und Rollenpflege
|
|
||||||
- `letzteneintraege.php`: Korrektur letzter Buchungen
|
|
||||||
- `hinweise.php`: Banner/Hinweise
|
|
||||||
|
|
||||||
## Umgang Mit Dem Archiv
|
- Seiten, die noch direkt auf `kl_*`-Tabellen schreiben, werden schrittweise
|
||||||
|
auf tenant-sicheres Lesen/Schreiben über `app/ledger.php` und
|
||||||
- Keine neuen Produktfunktionen mehr hier entwickeln.
|
`app/saas-auth.php` umgestellt; siehe die Meilensteindokumente für den
|
||||||
- Nur noch für Referenz, Datenabgleich oder Notfallbetrieb verwenden.
|
aktuellen Stand je Seite.
|
||||||
- Neue Arbeit findet ausschließlich in `../saas-app/` und `../docs/` statt.
|
- Neue Produktfunktionen entstehen gegen das neue Modell (Tenants, Users,
|
||||||
|
Participants, Ledger), nicht mehr direkt gegen die `kl_*`-Tabellen.
|
||||||
|
- Lokale Entwicklung gegen MySQL ist in `docs/dev-mysql.md` beschrieben.
|
||||||
|
|||||||
@@ -319,6 +319,74 @@ function ledger_mirror_legacy_consumption(PDO $pdo, int $tenantId, int $legacyCo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ledger_mirror_legacy_payment(PDO $pdo, int $tenantId, int $legacyPaymentId): void
|
||||||
|
{
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"INSERT INTO ledger_entries
|
||||||
|
(tenant_id, participant_id, type, amount_cents, marks_count,
|
||||||
|
unit_price_cents, booked_at, source, note, legacy_table, legacy_id)
|
||||||
|
SELECT
|
||||||
|
p.tenant_id,
|
||||||
|
p.id,
|
||||||
|
'payment',
|
||||||
|
CAST(ROUND(e.Betrag * 100) AS SIGNED),
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
e.Datum,
|
||||||
|
'legacy_payment',
|
||||||
|
NULL,
|
||||||
|
'kl_Einzahlungen',
|
||||||
|
e.EinzahlungsID
|
||||||
|
FROM kl_Einzahlungen e
|
||||||
|
JOIN participants p
|
||||||
|
ON p.tenant_id = ?
|
||||||
|
AND p.legacy_mitarbeiter_id = e.MitarbeiterID
|
||||||
|
WHERE e.EinzahlungsID = ?
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
participant_id = VALUES(participant_id),
|
||||||
|
type = VALUES(type),
|
||||||
|
amount_cents = VALUES(amount_cents),
|
||||||
|
marks_count = VALUES(marks_count),
|
||||||
|
unit_price_cents = VALUES(unit_price_cents),
|
||||||
|
booked_at = VALUES(booked_at),
|
||||||
|
source = VALUES(source),
|
||||||
|
note = VALUES(note)"
|
||||||
|
);
|
||||||
|
$stmt->execute([$tenantId, $legacyPaymentId]);
|
||||||
|
|
||||||
|
$check = $pdo->prepare(
|
||||||
|
"SELECT COUNT(*)
|
||||||
|
FROM ledger_entries
|
||||||
|
WHERE tenant_id = ?
|
||||||
|
AND legacy_table = 'kl_Einzahlungen'
|
||||||
|
AND legacy_id = ?"
|
||||||
|
);
|
||||||
|
$check->execute([$tenantId, $legacyPaymentId]);
|
||||||
|
if ((int)$check->fetchColumn() !== 1) {
|
||||||
|
throw new RuntimeException('Die Legacy-Einzahlung konnte nicht ins Ledger gespiegelt werden.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks the ledger entry mirrored from a legacy row as voided instead of
|
||||||
|
* deleting it, so corrections stay auditable. Idempotent: voiding an
|
||||||
|
* already-voided or missing entry is a no-op and returns false.
|
||||||
|
*/
|
||||||
|
function ledger_void_entry_by_legacy_id(PDO $pdo, int $tenantId, string $legacyTable, int $legacyId): bool
|
||||||
|
{
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"UPDATE ledger_entries
|
||||||
|
SET voided_at = NOW()
|
||||||
|
WHERE tenant_id = ?
|
||||||
|
AND legacy_table = ?
|
||||||
|
AND legacy_id = ?
|
||||||
|
AND voided_at IS NULL"
|
||||||
|
);
|
||||||
|
$stmt->execute([$tenantId, $legacyTable, $legacyId]);
|
||||||
|
|
||||||
|
return $stmt->rowCount() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options:
|
* Options:
|
||||||
* - participant_id: int
|
* - participant_id: int
|
||||||
|
|||||||
+69
-4
@@ -1,6 +1,6 @@
|
|||||||
# M5 App-Kern
|
# M5 App-Kern
|
||||||
|
|
||||||
Stand: 2026-07-14
|
Stand: 2026-07-14 (fortgeführt)
|
||||||
|
|
||||||
M5 stellt die operativen App-Seiten schrittweise auf das neue tenant-sichere
|
M5 stellt die operativen App-Seiten schrittweise auf das neue tenant-sichere
|
||||||
Modell um. Der Start erfolgt bewusst read-only, damit Summen, Links und
|
Modell um. Der Start erfolgt bewusst read-only, damit Summen, Links und
|
||||||
@@ -67,15 +67,80 @@ Umgesetzter Umfang `index.php`:
|
|||||||
- Eigene Web-Striche bleiben mit dem bestehenden Legacy-Datensatz kompatibel
|
- Eigene Web-Striche bleiben mit dem bestehenden Legacy-Datensatz kompatibel
|
||||||
und werden direkt ins Ledger gespiegelt.
|
und werden direkt ins Ledger gespiegelt.
|
||||||
|
|
||||||
|
## Fortsetzung: Schreibseiten und Storno
|
||||||
|
|
||||||
|
Umgesetzte Dateien:
|
||||||
|
|
||||||
|
```text
|
||||||
|
stricheintragen.php
|
||||||
|
einzahlung.php
|
||||||
|
letzteneintraege.php
|
||||||
|
app/ledger.php
|
||||||
|
scripts/check-m4-ledger-migration.php
|
||||||
|
```
|
||||||
|
|
||||||
|
Umgesetzter Umfang `stricheintragen.php` und `einzahlung.php`:
|
||||||
|
|
||||||
|
- Beide Sammelerfassungsseiten waren zuvor ohne jede Zugriffsprüfung
|
||||||
|
erreichbar (nur CSRF-Schutz, kein `checkKaffeelisteAdmin`- oder Rollen-Check).
|
||||||
|
Das ist jetzt behoben: Zugriff erfordert SaaS-Rolle `owner`, `admin` oder
|
||||||
|
`treasurer`, oder im Legacy-/Dev-Fallback `checkKaffeelisteAdmin` mit dem
|
||||||
|
Default-Tenant.
|
||||||
|
- Jede eingetragene Legacy-Zeile (`kl_Kaffeeverbrauch` beziehungsweise
|
||||||
|
`kl_Einzahlungen`) wird innerhalb derselben Transaktion sofort über
|
||||||
|
`ledger_mirror_legacy_consumption()` beziehungsweise die neue
|
||||||
|
`ledger_mirror_legacy_payment()` ins Ledger gespiegelt. Schlägt die
|
||||||
|
Spiegelung fehl, wird die gesamte Sammelerfassung zurückgerollt statt
|
||||||
|
teilweise gespeichert zu werden.
|
||||||
|
- Ein vorbestehender Bug wurde nebenbei behoben: Bei einer POST-Anfrage blieb
|
||||||
|
`$sqlMitarbeiter` unbelegt, was einen PHP-Warning erzeugte und die
|
||||||
|
Mitarbeiterliste nach dem Speichern leer ließ.
|
||||||
|
|
||||||
|
Umgesetzter Umfang `letzteneintraege.php`:
|
||||||
|
|
||||||
|
- Der Seitenzugriff nutzte bisher ausschließlich den Legacy-Check
|
||||||
|
`checkKaffeelisteAdmin`, wodurch neu registrierte SaaS-Mandanten (ohne
|
||||||
|
Legacy-`kl_Mitarbeiter`-Zeile) die Seite nie hätten nutzen können. Jetzt gilt
|
||||||
|
dieselbe Rollen-/Fallback-Logik wie in `kaffeeliste.php`.
|
||||||
|
- Löschen erzeugt keinen harten Delete im Ledger mehr. Neue Funktion
|
||||||
|
`ledger_void_entry_by_legacy_id()` setzt `voided_at` auf dem gespiegelten
|
||||||
|
Ledger-Eintrag, bevor die Legacy-Zeile aus `kl_Einzahlungen` oder
|
||||||
|
`kl_Kaffeeverbrauch` entfernt wird (in einer Transaktion). Der Ledger-Eintrag
|
||||||
|
bleibt damit für die Revision erhalten; alle Lesepfade filtern bereits
|
||||||
|
konsistent auf `voided_at IS NULL`.
|
||||||
|
- Zwei nie aufgerufene Funktionen (`berechneGesamtausgabe`,
|
||||||
|
`berechneGesamtstriche`, `berechneGesamteinzahlungen`) wurden als toten Code
|
||||||
|
entfernt.
|
||||||
|
|
||||||
|
Angepasstes Check-Skript:
|
||||||
|
|
||||||
|
- `scripts/check-m4-ledger-migration.php` ging bisher davon aus, dass jeder
|
||||||
|
gespiegelte Ledger-Eintrag eine noch existierende Legacy-Zeile hat. Das ist
|
||||||
|
durch das Storno-Modell nicht mehr korrekt: Ein stornierter Eintrag hat
|
||||||
|
absichtlich keine Legacy-Zeile mehr, bleibt aber im Ledger stehen. Die
|
||||||
|
Prüfungen filtern jetzt zusätzlich auf `voided_at IS NULL`, sodass echte
|
||||||
|
Dateninkonsistenzen weiterhin erkannt werden, stornierte Einträge aber nicht
|
||||||
|
mehr als Fehler zählen.
|
||||||
|
|
||||||
|
Alle drei Flows wurden gegen die Remote-Dev-Datenbank live per HTTP getestet
|
||||||
|
(Sammeleintrag Striche, Sammeleinzahlung, Storno beider Buchungsarten über
|
||||||
|
`letzteneintraege.php`) und anschließend wieder auf den Ausgangsstand
|
||||||
|
zurückgesetzt.
|
||||||
|
|
||||||
## Noch offen
|
## Noch offen
|
||||||
|
|
||||||
- Schreibseiten `stricheintragen.php` und `einzahlung.php` erst danach mit
|
- Eigene PayPal-/Zahlungsbereich als eigenständiger App-Screen (aktuell nur im
|
||||||
Storno-/Reversal-Strategie vorbereiten.
|
Dashboard integriert).
|
||||||
|
- Mitgliederverwaltung tenant- und rollenbasiert umsetzen
|
||||||
|
(`mitarbeiterverwalten.php` ist noch nicht angefasst).
|
||||||
|
- Hinweise als tenant-spezifische Notices umsetzen.
|
||||||
- Export, Mail und Jahresprozesse bleiben M6-Themen.
|
- Export, Mail und Jahresprozesse bleiben M6-Themen.
|
||||||
|
|
||||||
## Aktueller Prüfstatus
|
## Aktueller Prüfstatus
|
||||||
|
|
||||||
- M4 Ledger-Migration: grün mit 73 Assertions.
|
- M4 Ledger-Migration: grün mit 73 Assertions (Storno-fähig).
|
||||||
- M4 Ledger-Service: grün mit 115 Assertions.
|
- M4 Ledger-Service: grün mit 115 Assertions.
|
||||||
- Golden Master: grün mit 104 Assertions.
|
- Golden Master: grün mit 104 Assertions.
|
||||||
- HTTP-Smoke: grün mit 23 geprüften Seiten.
|
- HTTP-Smoke: grün mit 23 geprüften Seiten.
|
||||||
|
- Live-Test der Schreibflows gegen die Dev-DB: Sammelstriche, Sammeleinzahlung
|
||||||
|
und Storno beider Buchungsarten erfolgreich geprüft.
|
||||||
|
|||||||
+57
-18
@@ -1,6 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
include "functions.php";
|
include "functions.php";
|
||||||
|
require_once __DIR__ . "/app/ledger.php";
|
||||||
app_require_csrf();
|
app_require_csrf();
|
||||||
include "header.php";
|
include "header.php";
|
||||||
|
|
||||||
@@ -17,33 +18,71 @@ include "nav.php";
|
|||||||
|
|
||||||
<?php
|
<?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;
|
||||||
|
}
|
||||||
|
|
||||||
// Verarbeitung des Formulars, wenn es gesendet wurde
|
// Verarbeitung des Formulars, wenn es gesendet wurde
|
||||||
if ($_SERVER["REQUEST_METHOD"] == "POST" ) {
|
if ($_SERVER["REQUEST_METHOD"] == "POST" ) {
|
||||||
// Iteriere über alle Mitarbeiter
|
$datum = date("Y-m-d H:i:s"); // Das aktuelle Datum verwenden
|
||||||
foreach ($_POST["anzahlBetrag"] as $mitarbeiterID => $anzahlBetrag) {
|
$eingetragen = 0;
|
||||||
// Validierung könnte hier hinzugefügt werden
|
$fehlgeschlagen = false;
|
||||||
#$anzahlBetrag = $anzahlBetrag;
|
|
||||||
$anzahlBetrag = floatval($anzahlBetrag);
|
|
||||||
|
|
||||||
if($anzahlBetrag != 0){
|
|
||||||
$datum = date("Y-m-d H:i:s"); // Das aktuelle Datum verwenden
|
|
||||||
|
|
||||||
// SQL-Abfrage zum Einfügen der Daten
|
try {
|
||||||
$sql = "INSERT INTO kl_Einzahlungen (MitarbeiterID, Betrag, Datum) VALUES (?, ?, ?)";
|
$pdo->beginTransaction();
|
||||||
$params = array($mitarbeiterID, $anzahlBetrag, $datum);
|
$insert = $pdo->prepare(
|
||||||
|
"INSERT INTO kl_Einzahlungen (MitarbeiterID, Betrag, Datum) VALUES (?, ?, ?)"
|
||||||
|
);
|
||||||
|
|
||||||
$stmt = sqlsrv_query($conn, $sql, $params);
|
foreach ($_POST["anzahlBetrag"] ?? [] as $mitarbeiterID => $anzahlBetrag) {
|
||||||
|
$mitarbeiterID = (int)$mitarbeiterID;
|
||||||
if ($stmt === false) {
|
$anzahlBetrag = floatval($anzahlBetrag);
|
||||||
die(print_r(sqlsrv_errors(), true));
|
if ($mitarbeiterID <= 0 || $anzahlBetrag == 0.0) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "Einträge erfolgreich hinzugefügt.";
|
$insert->execute([$mitarbeiterID, $anzahlBetrag, $datum]);
|
||||||
|
$legacyPaymentId = (int)$pdo->lastInsertId();
|
||||||
|
ledger_mirror_legacy_payment($pdo, $tenantId, $legacyPaymentId);
|
||||||
|
$eingetragen++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
if ($pdo->inTransaction()) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
}
|
||||||
|
$fehlgeschlagen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($fehlgeschlagen) {
|
||||||
|
echo "Die Einzahlungen konnten nicht gespeichert werden.";
|
||||||
|
} else {
|
||||||
|
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"])){
|
}elseif($_SERVER["REQUEST_METHOD"] == "GET" AND isset($_GET["action"])){
|
||||||
|
|
||||||
if($_GET["action"] == "vorderseite"){
|
if($_GET["action"] == "vorderseite"){
|
||||||
|
|||||||
+51
-55
@@ -1,6 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
include "functions.php";
|
include "functions.php";
|
||||||
|
require_once __DIR__ . "/app/ledger.php";
|
||||||
app_require_csrf();
|
app_require_csrf();
|
||||||
include "header.php";
|
include "header.php";
|
||||||
include "headerline.php";
|
include "headerline.php";
|
||||||
@@ -14,65 +15,55 @@ include "nav.php";
|
|||||||
<div class="content">
|
<div class="content">
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
if(checkKaffeelisteAdmin($conn, $mailadress)){
|
|
||||||
|
|
||||||
// Funktion zum Berechnen der Gesamtausgabe und Gesamtstriche pro Mitarbeiter
|
$pdo = app_db_pdo();
|
||||||
function berechneGesamtausgabe($mitarbeiterID, $conn) {
|
$saasUser = saas_current_user($pdo);
|
||||||
// Gesamtausgabe für Kaffeeverbrauch pro Mitarbeiter
|
$tenantId = 0;
|
||||||
$sqlAusgabe = "SELECT SUM(Kosten) AS Gesamtausgabe FROM kl_Kaffeeverbrauch WHERE MitarbeiterID = ?";
|
$hasAccess = false;
|
||||||
$stmtAusgabe = sqlsrv_query($conn, $sqlAusgabe, array($mitarbeiterID));
|
|
||||||
$rowAusgabe = sqlsrv_fetch_array($stmtAusgabe, SQLSRV_FETCH_ASSOC);
|
|
||||||
$gesamtausgabe = $rowAusgabe['Gesamtausgabe'];
|
|
||||||
|
|
||||||
return $gesamtausgabe;
|
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;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Funktion zum Berechnen der Gesamtstriche pro Mitarbeiter
|
if($hasAccess){
|
||||||
function berechneGesamtstriche($mitarbeiterID, $conn) {
|
|
||||||
// Gesamtstriche pro Mitarbeiter
|
|
||||||
$sqlStriche = "SELECT SUM(AnzahlStriche) AS Gesamtstriche FROM kl_Kaffeeverbrauch WHERE MitarbeiterID = ?";
|
|
||||||
$stmtStriche = sqlsrv_query($conn, $sqlStriche, array($mitarbeiterID));
|
|
||||||
$rowStriche = sqlsrv_fetch_array($stmtStriche, SQLSRV_FETCH_ASSOC);
|
|
||||||
$gesamtstriche = $rowStriche['Gesamtstriche'];
|
|
||||||
|
|
||||||
return $gesamtstriche;
|
// Funktion zum Löschen einer Einzahlung: der Ledger-Eintrag wird als Storno
|
||||||
}
|
// markiert statt geloescht, damit die Buchungshistorie nachvollziehbar bleibt.
|
||||||
// Funktion zum Berechnen der Gesamteinzahlungen pro Mitarbeiter
|
function loescheEinzahlung($einzahlungID, PDO $pdo, int $tenantId) {
|
||||||
function berechneGesamteinzahlungen($mitarbeiterID, $conn) {
|
|
||||||
// Gesamteinzahlungen pro Mitarbeiter
|
|
||||||
$sqlEinzahlungen = "SELECT SUM(Betrag) AS Gesamteinzahlungen FROM kl_Einzahlungen WHERE MitarbeiterID = ?";
|
|
||||||
$stmtEinzahlungen = sqlsrv_query($conn, $sqlEinzahlungen, array($mitarbeiterID));
|
|
||||||
$rowEinzahlungen = sqlsrv_fetch_array($stmtEinzahlungen, SQLSRV_FETCH_ASSOC);
|
|
||||||
$gesamteinzahlungen = $rowEinzahlungen['Gesamteinzahlungen'];
|
|
||||||
|
|
||||||
return $gesamteinzahlungen;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Funktion zum Löschen einer Einzahlung
|
|
||||||
function loescheEinzahlung($einzahlungID, $conn) {
|
|
||||||
try {
|
try {
|
||||||
$sql = "DELETE FROM kl_Einzahlungen WHERE EinzahlungsID = ?";
|
$pdo->beginTransaction();
|
||||||
$params = array($einzahlungID);
|
|
||||||
|
|
||||||
$stmt = sqlsrv_query($conn, $sql, $params);
|
ledger_void_entry_by_legacy_id($pdo, $tenantId, 'kl_Einzahlungen', $einzahlungID);
|
||||||
|
|
||||||
if ($stmt === false) {
|
$stmt = $pdo->prepare('DELETE FROM kl_Einzahlungen WHERE EinzahlungsID = ?');
|
||||||
throw new Exception(print_r(sqlsrv_errors(), true));
|
$stmt->execute([$einzahlungID]);
|
||||||
}
|
|
||||||
|
$pdo->commit();
|
||||||
|
|
||||||
return true; // Erfolgreich gelöscht
|
return true; // Erfolgreich gelöscht
|
||||||
} catch (Exception $e) {
|
} catch (Throwable $e) {
|
||||||
return $e->getMessage(); // Fehlermeldung zurückgeben
|
if ($pdo->inTransaction()) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Die Einzahlung konnte nicht gelöscht werden.';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Überprüfen, ob ein Löschvorgang angefordert wurde
|
// Überprüfen, ob ein Löschvorgang angefordert wurde
|
||||||
if ($_SERVER["REQUEST_METHOD"] == "POST" && isset($_POST["aktion"]) && $_POST["aktion"] == "loescheneinzahlung") {
|
if ($_SERVER["REQUEST_METHOD"] == "POST" && isset($_POST["aktion"]) && $_POST["aktion"] == "loescheneinzahlung") {
|
||||||
$einzahlungID = $_POST["einzahlungID"];
|
$einzahlungID = (int)$_POST["einzahlungID"];
|
||||||
|
|
||||||
$ergebnis = loescheEinzahlung($einzahlungID, $conn);
|
$ergebnis = loescheEinzahlung($einzahlungID, $pdo, $tenantId);
|
||||||
|
|
||||||
if ($ergebnis === true) {
|
if ($ergebnis === true) {
|
||||||
echo "Einzahlung erfolgreich gelöscht.";
|
echo "Einzahlung erfolgreich gelöscht.";
|
||||||
@@ -136,29 +127,34 @@ $stmtEinzahlungen = sqlsrv_query($conn, $sqlEinzahlungen);
|
|||||||
|
|
||||||
<br><br>
|
<br><br>
|
||||||
<?php
|
<?php
|
||||||
// Funktion zum Löschen eines Strich-Eintrags
|
// Funktion zum Löschen eines Strich-Eintrags: der Ledger-Eintrag wird als
|
||||||
function loescheStrichEintrag($strichID, $conn) {
|
// Storno markiert statt geloescht, damit die Buchungshistorie nachvollziehbar bleibt.
|
||||||
|
function loescheStrichEintrag($strichID, PDO $pdo, int $tenantId) {
|
||||||
try {
|
try {
|
||||||
$sql = "DELETE FROM kl_Kaffeeverbrauch WHERE VerbrauchID = ?";
|
$pdo->beginTransaction();
|
||||||
$params = array($strichID);
|
|
||||||
|
|
||||||
$stmt = sqlsrv_query($conn, $sql, $params);
|
ledger_void_entry_by_legacy_id($pdo, $tenantId, 'kl_Kaffeeverbrauch', $strichID);
|
||||||
|
|
||||||
if ($stmt === false) {
|
$stmt = $pdo->prepare('DELETE FROM kl_Kaffeeverbrauch WHERE VerbrauchID = ?');
|
||||||
throw new Exception(print_r(sqlsrv_errors(), true));
|
$stmt->execute([$strichID]);
|
||||||
}
|
|
||||||
|
$pdo->commit();
|
||||||
|
|
||||||
return true; // Erfolgreich gelöscht
|
return true; // Erfolgreich gelöscht
|
||||||
} catch (Exception $e) {
|
} catch (Throwable $e) {
|
||||||
return $e->getMessage(); // Fehlermeldung zurückgeben
|
if ($pdo->inTransaction()) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Der Strich-Eintrag konnte nicht gelöscht werden.';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Überprüfen, ob ein Löschvorgang angefordert wurde
|
// Überprüfen, ob ein Löschvorgang angefordert wurde
|
||||||
if ($_SERVER["REQUEST_METHOD"] == "POST" && isset($_POST["aktion"]) && $_POST["aktion"] == "loeschen") {
|
if ($_SERVER["REQUEST_METHOD"] == "POST" && isset($_POST["aktion"]) && $_POST["aktion"] == "loeschen") {
|
||||||
$strichID = $_POST["strichID"];
|
$strichID = (int)$_POST["strichID"];
|
||||||
|
|
||||||
$ergebnis = loescheStrichEintrag($strichID, $conn);
|
$ergebnis = loescheStrichEintrag($strichID, $pdo, $tenantId);
|
||||||
|
|
||||||
if ($ergebnis === true) {
|
if ($ergebnis === true) {
|
||||||
echo "Strich-Eintrag erfolgreich gelöscht.";
|
echo "Strich-Eintrag erfolgreich gelöscht.";
|
||||||
|
|||||||
@@ -101,12 +101,13 @@ $stmt = $pdo->prepare(
|
|||||||
"SELECT COUNT(*)
|
"SELECT COUNT(*)
|
||||||
FROM ledger_entries
|
FROM ledger_entries
|
||||||
WHERE tenant_id = ?
|
WHERE tenant_id = ?
|
||||||
AND legacy_table IN ('kl_Einzahlungen', 'kl_Kaffeeverbrauch')"
|
AND legacy_table IN ('kl_Einzahlungen', 'kl_Kaffeeverbrauch')
|
||||||
|
AND voided_at IS NULL"
|
||||||
);
|
);
|
||||||
$stmt->execute([$tenantId]);
|
$stmt->execute([$tenantId]);
|
||||||
$ledgerLegacyCount = (int)$stmt->fetchColumn();
|
$ledgerLegacyCount = (int)$stmt->fetchColumn();
|
||||||
m4_check_assert_equal(
|
m4_check_assert_equal(
|
||||||
'legacy ledger row count matches source rows',
|
'active legacy ledger row count matches source rows',
|
||||||
$legacyPaymentCount + $legacyConsumptionCount,
|
$legacyPaymentCount + $legacyConsumptionCount,
|
||||||
$ledgerLegacyCount,
|
$ledgerLegacyCount,
|
||||||
$failures,
|
$failures,
|
||||||
@@ -131,16 +132,20 @@ m4_check_assert_equal(
|
|||||||
$passes
|
$passes
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Ledger rows without a matching legacy row are only expected when they were
|
||||||
|
// voided through the storno flow in letzteneintraege.php. A missing legacy row
|
||||||
|
// on a non-voided ledger entry indicates real drift between the tables.
|
||||||
$stmt = $pdo->prepare(
|
$stmt = $pdo->prepare(
|
||||||
"SELECT COUNT(*)
|
"SELECT COUNT(*)
|
||||||
FROM ledger_entries le
|
FROM ledger_entries le
|
||||||
LEFT JOIN kl_Einzahlungen e ON e.EinzahlungsID = le.legacy_id
|
LEFT JOIN kl_Einzahlungen e ON e.EinzahlungsID = le.legacy_id
|
||||||
WHERE le.tenant_id = ?
|
WHERE le.tenant_id = ?
|
||||||
AND le.legacy_table = 'kl_Einzahlungen'
|
AND le.legacy_table = 'kl_Einzahlungen'
|
||||||
|
AND le.voided_at IS NULL
|
||||||
AND e.EinzahlungsID IS NULL"
|
AND e.EinzahlungsID IS NULL"
|
||||||
);
|
);
|
||||||
$stmt->execute([$tenantId]);
|
$stmt->execute([$tenantId]);
|
||||||
m4_check_assert_equal('no stale payment ledger rows', 0, (int)$stmt->fetchColumn(), $failures, $passes);
|
m4_check_assert_equal('no stale non-voided payment ledger rows', 0, (int)$stmt->fetchColumn(), $failures, $passes);
|
||||||
|
|
||||||
$stmt = $pdo->prepare(
|
$stmt = $pdo->prepare(
|
||||||
"SELECT COUNT(*)
|
"SELECT COUNT(*)
|
||||||
@@ -148,10 +153,11 @@ $stmt = $pdo->prepare(
|
|||||||
LEFT JOIN kl_Kaffeeverbrauch v ON v.VerbrauchID = le.legacy_id
|
LEFT JOIN kl_Kaffeeverbrauch v ON v.VerbrauchID = le.legacy_id
|
||||||
WHERE le.tenant_id = ?
|
WHERE le.tenant_id = ?
|
||||||
AND le.legacy_table = 'kl_Kaffeeverbrauch'
|
AND le.legacy_table = 'kl_Kaffeeverbrauch'
|
||||||
|
AND le.voided_at IS NULL
|
||||||
AND v.VerbrauchID IS NULL"
|
AND v.VerbrauchID IS NULL"
|
||||||
);
|
);
|
||||||
$stmt->execute([$tenantId]);
|
$stmt->execute([$tenantId]);
|
||||||
m4_check_assert_equal('no stale consumption ledger rows', 0, (int)$stmt->fetchColumn(), $failures, $passes);
|
m4_check_assert_equal('no stale non-voided consumption ledger rows', 0, (int)$stmt->fetchColumn(), $failures, $passes);
|
||||||
|
|
||||||
$stmt = $pdo->prepare("SELECT COUNT(*) FROM ledger_entries WHERE tenant_id = ? AND type = 'payment' AND amount_cents <= 0");
|
$stmt = $pdo->prepare("SELECT COUNT(*) FROM ledger_entries WHERE tenant_id = ? AND type = 'payment' AND amount_cents <= 0");
|
||||||
$stmt->execute([$tenantId]);
|
$stmt->execute([$tenantId]);
|
||||||
|
|||||||
+59
-20
@@ -1,6 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
include "functions.php";
|
include "functions.php";
|
||||||
|
require_once __DIR__ . "/app/ledger.php";
|
||||||
app_require_csrf();
|
app_require_csrf();
|
||||||
include "header.php";
|
include "header.php";
|
||||||
include "headerline.php";
|
include "headerline.php";
|
||||||
@@ -18,35 +19,73 @@ include "nav.php";
|
|||||||
|
|
||||||
<?php
|
<?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;
|
||||||
|
}
|
||||||
|
|
||||||
// Verarbeitung des Formulars, wenn es gesendet wurde
|
// Verarbeitung des Formulars, wenn es gesendet wurde
|
||||||
if ($_SERVER["REQUEST_METHOD"] == "POST") {
|
if ($_SERVER["REQUEST_METHOD"] == "POST") {
|
||||||
// Kosten pro Strich übernehmen:
|
$kostenproStrich = floatval($_POST["kostenproStrich"] ?? 0);
|
||||||
$kostenproStrich = $_POST["kostenproStrich"];
|
$datum = date("Y-m-d H:i:s"); // Das aktuelle Datum verwenden
|
||||||
|
$eingetragen = 0;
|
||||||
// Iteriere über alle Mitarbeiter
|
$fehlgeschlagen = false;
|
||||||
foreach ($_POST["anzahlStriche"] as $mitarbeiterID => $anzahlStriche) {
|
|
||||||
// Validierung könnte hier hinzugefügt werden
|
|
||||||
$anzahlStriche = (int)$anzahlStriche;
|
|
||||||
$kostenproStrich = floatval($kostenproStrich);
|
|
||||||
$kosten = floatval($anzahlStriche * $kostenproStrich);
|
|
||||||
$datum = date("Y-m-d H:i:s"); // Das aktuelle Datum verwenden
|
|
||||||
if($anzahlStriche != 0){
|
|
||||||
// SQL-Abfrage zum Einfügen der Daten
|
|
||||||
$sql = "INSERT INTO kl_Kaffeeverbrauch (MitarbeiterID, AnzahlStriche, Kosten, KostenproStrich, Datum) VALUES (?, ?, ?, ?, ?)";
|
|
||||||
$params = array($mitarbeiterID, $anzahlStriche, $kosten, $kostenproStrich, $datum);
|
|
||||||
|
|
||||||
$stmt = sqlsrv_query($conn, $sql, $params);
|
try {
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
$insert = $pdo->prepare(
|
||||||
|
"INSERT INTO kl_Kaffeeverbrauch (MitarbeiterID, AnzahlStriche, Kosten, KostenproStrich, Datum) VALUES (?, ?, ?, ?, ?)"
|
||||||
|
);
|
||||||
|
|
||||||
if ($stmt === false) {
|
foreach ($_POST["anzahlStriche"] ?? [] as $mitarbeiterID => $anzahlStriche) {
|
||||||
die(print_r(sqlsrv_errors(), true));
|
$mitarbeiterID = (int)$mitarbeiterID;
|
||||||
|
$anzahlStriche = (int)$anzahlStriche;
|
||||||
|
if ($mitarbeiterID <= 0 || $anzahlStriche === 0) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "Einträge erfolgreich hinzugefügt.";
|
$kosten = $anzahlStriche * $kostenproStrich;
|
||||||
|
$insert->execute([$mitarbeiterID, $anzahlStriche, $kosten, $kostenproStrich, $datum]);
|
||||||
|
$legacyConsumptionId = (int)$pdo->lastInsertId();
|
||||||
|
ledger_mirror_legacy_consumption($pdo, $tenantId, $legacyConsumptionId);
|
||||||
|
$eingetragen++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
if ($pdo->inTransaction()) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
}
|
||||||
|
$fehlgeschlagen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($fehlgeschlagen) {
|
||||||
|
echo "Die Einträge konnten nicht gespeichert werden.";
|
||||||
|
} else {
|
||||||
|
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"])){
|
}elseif($_SERVER["REQUEST_METHOD"] == "GET" AND isset($_GET["action"])){
|
||||||
|
|
||||||
if($_GET["action"] == "vorderseite"){
|
if($_GET["action"] == "vorderseite"){
|
||||||
|
|||||||
Reference in New Issue
Block a user