M6: Jahresabschluss als generisches Feature statt AOK-spezifischem Bonus-Skript
jahresauswertung.php verband sich bisher mit fest codierten (kaputten) Zugangsdaten selbst zur Datenbank statt ueber config.php, hatte keine Zugriffskontrolle und kein CSRF, und verteilte bei jedem Aufruf sofort einen hart codierten Bonus-Topf (490 Striche a 0,20 Euro) per PHPMailer (dessen Quelldateien im Repo fehlen) mit AOK-spezifischem Mailtext. Nach Abstimmung mit dem Kunden als generisches, mandantenfaehiges Feature neu gebaut statt nur deaktiviert oder rein lesend umgesetzt: - Admin gibt einen frei waehlbaren Gesamtbetrag ein, das System verteilt ihn proportional zu den Jahresstrichen auf alle aktiven Mitglieder. - Standardmaessig aktive Dry-Run-Checkbox zeigt die Verteilung, ohne zu buchen oder Mails zu verschicken. - Bestaetigter Lauf bucht ueber dasselbe Zweig-Muster wie ueberall (Default-Mandant Dual-Write, andere Mandanten ledger_record_payment) und verschickt personalisierte Mails ueber saas_send_mail(), protokolliert im outbound_emails-Versandlog. - Zugriffskontrolle ergaenzt (owner/admin/treasurer + Legacy-Fallback). - http-smoke.php: jahresauswertung.php jetzt regulaerer Check statt uebersprungenem unsicherem Aufruf; damit sind keine Seiten mehr uebersprungen oder als bekannter offener Punkt markiert (26/26 gruen). Live getestet: Dry-Run mit korrekter proportionaler Verteilung (Summe ergibt exakt den Gesamtbetrag), Live-Lauf bucht und versendet korrekt, Testdaten anschliessend vollstaendig entfernt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+184
-172
@@ -1,177 +1,189 @@
|
||||
<?php
|
||||
// Einstellungen
|
||||
// Verbindung zur Datenbank herstellen (ersetze die Platzhalter durch deine Daten)
|
||||
$serverName = "server";
|
||||
$connectionOptions = array(
|
||||
"Database" => "database",
|
||||
"Uid" => "user",
|
||||
"PWD" => "password",
|
||||
"TrustServerCertificate"=>true
|
||||
);
|
||||
|
||||
$stricheAnzupassen = 490; // Anzahl der neuen Striche
|
||||
$betragProStrich = 0.20; // Betrag pro Strich in Euro
|
||||
include "functions.php";
|
||||
require_once __DIR__ . "/app/ledger.php";
|
||||
require_once __DIR__ . "/app/saas-mail.php";
|
||||
app_require_csrf();
|
||||
include "header.php";
|
||||
include "headerline.php";
|
||||
include "nav.php";
|
||||
|
||||
// Testmodus aktivieren
|
||||
$testmodus = false;
|
||||
|
||||
|
||||
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use PHPMailer\PHPMailer\SMTP;
|
||||
use PHPMailer\PHPMailer\Exception;
|
||||
|
||||
require 'PHPMailer/src/Exception.php';
|
||||
require 'PHPMailer/src/PHPMailer.php';
|
||||
require 'PHPMailer/src/SMTP.php';
|
||||
|
||||
// PHPMailer konfigurieren
|
||||
function sendeMail($empfaenger, $betreff, $inhalt, $testmodus)
|
||||
{
|
||||
$mail = new PHPMailer(true);
|
||||
|
||||
try {
|
||||
// Server-Einstellungen
|
||||
$mail->isSMTP();
|
||||
$mail->Host = 'smtpv.aoknds.aok'; // SMTP-Server
|
||||
$mail->Timeout = 180;
|
||||
$mail->SMTPAuth = false;
|
||||
$mail->Port = 25;
|
||||
|
||||
// Absender
|
||||
$mail->setFrom('kaffeelistesb3@nds.aok.de', 'Kaffeeliste');
|
||||
|
||||
// Empfänger
|
||||
if ($testmodus) {
|
||||
$mail->addAddress('kaffeelistesb3@nds.aok.de'); // Testadresse
|
||||
} else {
|
||||
$mail->addAddress($empfaenger); // Tatsächlicher Empfänger
|
||||
}
|
||||
|
||||
// Inhalt
|
||||
$mail->isHTML(true);
|
||||
$mail->Subject = $betreff;
|
||||
$mail->Body = utf8_decode($inhalt);
|
||||
|
||||
// Senden
|
||||
$mail->send();
|
||||
echo "E-Mail erfolgreich gesendet an: " . ($testmodus ? 'kaffeelistesb3@nds.aok.de' : $empfaenger) . "\n";
|
||||
} catch (Exception $e) {
|
||||
echo "E-Mail konnte nicht gesendet werden. Fehler: {$mail->ErrorInfo}\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Verbindung herstellen
|
||||
$conn = sqlsrv_connect($serverName, $connectionOptions);
|
||||
if ($conn === false) {
|
||||
die(print_r(sqlsrv_errors(), true));
|
||||
}
|
||||
|
||||
// Aktuelles Jahr ermitteln
|
||||
$currentYear = date("Y");
|
||||
|
||||
// SQL-Abfrage: Gesamtanzahl der Striche pro Mitarbeiter im aktuellen Jahr mit Namen und E-Mail
|
||||
$sql = "
|
||||
SELECT
|
||||
m.MitarbeiterID,
|
||||
m.Name,
|
||||
m.Email,
|
||||
SUM(v.AnzahlStriche) AS GesamtStriche
|
||||
FROM kl_Kaffeeverbrauch v
|
||||
JOIN kl_Mitarbeiter m ON v.MitarbeiterID = m.MitarbeiterID
|
||||
WHERE YEAR(v.Datum) = ? AND m.aktiv = 1
|
||||
GROUP BY m.MitarbeiterID, m.Name, m.Email
|
||||
";
|
||||
$params = [$currentYear];
|
||||
$stmt = sqlsrv_query($conn, $sql, $params);
|
||||
|
||||
if ($stmt === false) {
|
||||
die(print_r(sqlsrv_errors(), true));
|
||||
}
|
||||
|
||||
// Ergebnisse verarbeiten
|
||||
$mitarbeiterDaten = [];
|
||||
$gesamtStriche = 0;
|
||||
|
||||
while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) {
|
||||
$mitarbeiterDaten[] = $row;
|
||||
$gesamtStriche += $row['GesamtStriche'];
|
||||
}
|
||||
|
||||
echo $gesamtStriche;
|
||||
|
||||
// Neue Striche proportional verteilen
|
||||
$verteilung = [];
|
||||
|
||||
foreach ($mitarbeiterDaten as $mitarbeiter) {
|
||||
$mitarbeiterID = $mitarbeiter['MitarbeiterID'];
|
||||
$anteil = $mitarbeiter['GesamtStriche'] / $gesamtStriche;
|
||||
$neueStriche = round($anteil * $stricheAnzupassen);
|
||||
$betrag = $neueStriche * $betragProStrich;
|
||||
|
||||
$verteilung[] = [
|
||||
'MitarbeiterID' => $mitarbeiterID,
|
||||
'Name' => $mitarbeiter['Name'],
|
||||
'Email' => $mitarbeiter['Email'],
|
||||
'JahrStriche' => $mitarbeiter['GesamtStriche'],
|
||||
'NeueStriche' => $neueStriche,
|
||||
'Betrag' => $betrag
|
||||
];
|
||||
|
||||
// SQL-Befehl vorbereiten
|
||||
$insertSql = "
|
||||
INSERT INTO kl_Einzahlungen (MitarbeiterID, Betrag, Datum)
|
||||
VALUES (?, ?, GETDATE())
|
||||
";
|
||||
$insertParams = [$mitarbeiterID, $betrag];
|
||||
|
||||
if ($testmodus) {
|
||||
// SQL-Befehl und Parameter anzeigen
|
||||
echo "SQL-Befehl: $insertSql\n";
|
||||
echo "Parameter: " . json_encode($insertParams) . "\n";
|
||||
// E-Mail vorbereiten
|
||||
|
||||
} else {
|
||||
// SQL-Befehl ausführen
|
||||
$insertStmt = sqlsrv_query($conn, $insertSql, $insertParams);
|
||||
if ($insertStmt === false) {
|
||||
die(print_r(sqlsrv_errors(), true));
|
||||
}
|
||||
// E-Mail vorbereiten
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
$betreff = "Kaffeeliste - Weihnachten";
|
||||
$inhalt = "
|
||||
<p>Hallo {$mitarbeiter['Name']},</p>
|
||||
<p>vielen Dank für deine Nutzung der Kaffeeliste in diesem Jahr!</p>
|
||||
<p>Du hast dieses Jahr {$mitarbeiter['GesamtStriche']} Kaffee bezogen.</p>
|
||||
<p>Deswegen wurden dir $betrag Euro auf deinem Konto gutgeschrieben.</p>
|
||||
<br><br>
|
||||
<p>Wir wünschen dir eine frohe Weihnachtszeit und einen guten Rutsch ins neue Jahr.</p>
|
||||
<p>Deine ARGE Kaffeeliste</p>
|
||||
";
|
||||
$empfaenger = $mitarbeiter['Email'];
|
||||
// E-Mail senden
|
||||
sendeMail($empfaenger, $betreff, $inhalt, $testmodus);
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Sortiere die Verteilung nach JahrStriche (absteigend)
|
||||
usort($verteilung, function ($a, $b) {
|
||||
return $b['JahrStriche'] <=> $a['JahrStriche'];
|
||||
});
|
||||
|
||||
// Ergebnisse ausgeben
|
||||
header('Content-Type: application/json');
|
||||
#echo json_encode($verteilung, JSON_PRETTY_PRINT);
|
||||
|
||||
// Verbindung schließen
|
||||
sqlsrv_close($conn);
|
||||
?>
|
||||
|
||||
<!-- 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;
|
||||
}
|
||||
|
||||
$jahr = (int)date('Y');
|
||||
$teilnehmer = ledger_fetch_participant_summaries($pdo, $tenantId, ['active_only' => true, 'year' => $jahr]);
|
||||
$gesamtJahresstriche = array_sum(array_column($teilnehmer, 'year_marks'));
|
||||
|
||||
$fehler = null;
|
||||
$ergebnisse = null;
|
||||
$dryRun = true;
|
||||
$gesamtbetragEingabe = '';
|
||||
|
||||
if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
||||
$dryRun = !empty($_POST['dry_run']);
|
||||
$gesamtbetragEingabe = (string)($_POST['gesamtbetrag'] ?? '');
|
||||
$gesamtbetrag = filter_var(str_replace(',', '.', $gesamtbetragEingabe), FILTER_VALIDATE_FLOAT);
|
||||
$totalCents = $gesamtbetrag !== false ? (int)round($gesamtbetrag * 100) : 0;
|
||||
|
||||
if ($totalCents <= 0) {
|
||||
$fehler = 'Bitte einen Gesamtbetrag größer 0 eingeben.';
|
||||
} elseif ($gesamtJahresstriche <= 0) {
|
||||
$fehler = "Für {$jahr} gibt es noch keine Striche, daher kann nichts verteilt werden.";
|
||||
} else {
|
||||
$ergebnisse = [];
|
||||
$settings = saas_fetch_tenant_settings($pdo, $tenantId);
|
||||
$dashboardUrl = saas_app_url('index.php');
|
||||
$createdByUserId = $saasUser['user_id'] ?? null;
|
||||
|
||||
// Legacy-Verknuepfung je Teilnehmer vorab laden (gleiches Zweig-Muster
|
||||
// wie bei Sammelerfassung und CSV-Import: Default-Mandant bucht
|
||||
// zusaetzlich in kl_Einzahlungen, alle anderen Mandanten rein Ledger-nativ).
|
||||
$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 {
|
||||
if (!$dryRun) {
|
||||
$pdo->beginTransaction();
|
||||
}
|
||||
$insertLegacy = $pdo->prepare('INSERT INTO kl_Einzahlungen (MitarbeiterID, Betrag, Datum) VALUES (?, ?, ?)');
|
||||
$jetzt = date('Y-m-d H:i:s');
|
||||
|
||||
foreach ($teilnehmer as $person) {
|
||||
$yearMarks = (int)$person['year_marks'];
|
||||
if ($yearMarks <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$anteil = $yearMarks / $gesamtJahresstriche;
|
||||
$bonusCents = (int)round($totalCents * $anteil);
|
||||
if ($bonusCents <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$status = 'vorschau';
|
||||
|
||||
if (!$dryRun) {
|
||||
$legacyMitarbeiterId = $legacyMap[$person['participant_id']] ?? null;
|
||||
if ($legacyMitarbeiterId !== null) {
|
||||
$insertLegacy->execute([$legacyMitarbeiterId, $bonusCents / 100, $jetzt]);
|
||||
$legacyPaymentId = (int)$pdo->lastInsertId();
|
||||
ledger_mirror_legacy_payment($pdo, $tenantId, $legacyPaymentId);
|
||||
} else {
|
||||
ledger_record_payment($pdo, $tenantId, $person['participant_id'], $bonusCents, 'year_end_bonus');
|
||||
}
|
||||
$status = 'gebucht';
|
||||
|
||||
$email = trim((string)($person['email'] ?? ''));
|
||||
if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
$subject = "Kaffeeliste - Dein Jahresbonus {$jahr}";
|
||||
$body = saas_render_year_end_bonus_mail_body($person['display_name'], $yearMarks, $bonusCents, $dashboardUrl);
|
||||
$sendResult = saas_send_mail($email, $subject, $body);
|
||||
saas_log_outbound_email(
|
||||
$pdo, $tenantId, $person['participant_id'], 'year_end_bonus', $subject,
|
||||
$sendResult['ok'] ? 'sent' : 'failed', $sendResult['error'] ?? null, $createdByUserId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$ergebnisse[] = [
|
||||
'name' => $person['display_name'],
|
||||
'year_marks' => $yearMarks,
|
||||
'anteil' => $anteil,
|
||||
'bonus_cents' => $bonusCents,
|
||||
'status' => $status,
|
||||
];
|
||||
}
|
||||
|
||||
if (!$dryRun) {
|
||||
$pdo->commit();
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
$fehler = 'Die Verteilung konnte nicht gespeichert werden.';
|
||||
$ergebnisse = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
<h2>Jahresabschluss <?php echo (int)$jahr; ?></h2>
|
||||
<p>Verteilt einen frei wählbaren Gesamtbetrag proportional zu den in diesem
|
||||
Jahr gemachten Strichen als Guthaben an alle aktiven Mitglieder und
|
||||
benachrichtigt sie per Mail. Im Dry-Run wird nur eine Vorschau berechnet,
|
||||
es wird nichts gebucht und keine Mail verschickt.</p>
|
||||
|
||||
<p>Jahresstriche gesamt: <?php echo number_format($gesamtJahresstriche, 0, ',', '.'); ?></p>
|
||||
|
||||
<?php if ($fehler !== null): ?>
|
||||
<div class="hint-box error"><p><?php echo saas_html($fehler); ?></p></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>">
|
||||
<?php echo app_csrf_field(); ?>
|
||||
<label for="gesamtbetrag">Gesamtbetrag (€):</label>
|
||||
<input type="text" name="gesamtbetrag" id="gesamtbetrag" value="<?php echo saas_html($gesamtbetragEingabe); ?>" required>
|
||||
<br>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="dry_run" id="dry_run" <?php echo $dryRun ? 'checked' : ''; ?>>
|
||||
<label class="form-check-label" for="dry_run">Dry-Run (nur Vorschau, nichts buchen, keine Mails)</label>
|
||||
</div>
|
||||
<button type="submit">Berechnen</button>
|
||||
</form>
|
||||
|
||||
<?php if ($ergebnisse !== null): ?>
|
||||
<h3><?php echo $dryRun ? 'Vorschau' : 'Gebucht'; ?></h3>
|
||||
<table>
|
||||
<tr><th>Name</th><th>Jahresstriche</th><th>Anteil</th><th>Bonus</th><th>Status</th></tr>
|
||||
<?php foreach ($ergebnisse as $eintrag): ?>
|
||||
<tr>
|
||||
<td><?php echo saas_html($eintrag['name']); ?></td>
|
||||
<td><?php echo number_format($eintrag['year_marks'], 0, ',', '.'); ?></td>
|
||||
<td><?php echo number_format($eintrag['anteil'] * 100, 1, ',', '.'); ?> %</td>
|
||||
<td><?php echo saas_html(saas_format_money_cents($eintrag['bonus_cents'])); ?> €</td>
|
||||
<td><?php echo $eintrag['status'] === 'gebucht' ? 'Gebucht' : 'Vorschau'; ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php include "footer.php"; ?>
|
||||
|
||||
Reference in New Issue
Block a user