Files
kaffeekasse-saas/mailversenden.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

194 lines
6.5 KiB
PHP

<?php
include "functions.php";
require_once __DIR__ . "/app/ledger.php";
require_once __DIR__ . "/app/saas-mail.php";
require_once __DIR__ . "/app/audit.php";
require_once __DIR__ . "/app/features.php";
app_require_csrf();
$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;
}
// Vom Betreiber gesperrte Funktion: $hasAccess faellt zurueck, damit auch die
// POST-Verarbeitung weiter unten nicht mehr laeuft.
$gesperrteFunktion = null;
if ($hasAccess && !app_feature_enabled($pdo, $tenantId, 'mail_dispatch')) {
$gesperrteFunktion = 'mail_dispatch';
$hasAccess = false;
}
$ergebnisse = null;
$dryRun = true;
$liveFlash = false;
$verifikationHinweis = null;
// Ergebnis eines vorangegangenen Live-Versands (Post-Redirect-Get): nach dem
// echten Versand wird auf diese Seite zurueckgeleitet, damit ein Refresh nicht
// erneut an alle Mitglieder mailt.
if ($hasAccess && isset($_SESSION['flash_mailversand'])) {
$flash = $_SESSION['flash_mailversand'];
unset($_SESSION['flash_mailversand']);
if ((int)($flash['tenant_id'] ?? 0) === $tenantId) {
$ergebnisse = $flash['ergebnisse'] ?? null;
$dryRun = false;
$liveFlash = true;
}
}
if ($hasAccess && $_SERVER["REQUEST_METHOD"] === "POST") {
$dryRun = !empty($_POST['dry_run']);
// Echter Versand geht an alle Mitglieder des Mandanten. Solange die
// eigene Adresse nicht bestaetigt ist, bleibt nur der Dry-Run - der
// protokolliert, verschickt aber nichts nach aussen.
if (!$dryRun && !saas_email_verified($saasUser)) {
$dryRun = true;
$verifikationHinweis = 'Es wurde nur ein Dry-Run ausgeführt: Für den echten Versand muss deine E-Mail-Adresse bestätigt sein. Den Link kannst du unter "Kundenkonto" erneut anfordern.';
}
$settings = saas_fetch_tenant_settings($pdo, $tenantId);
$dashboardUrl = saas_app_url('index.php');
$createdByUserId = $saasUser['user_id'] ?? null;
$teilnehmer = ledger_fetch_participant_summaries($pdo, $tenantId, ['active_only' => true]);
$ergebnisse = [];
foreach ($teilnehmer as $person) {
$email = trim((string)($person['email'] ?? ''));
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$ergebnisse[] = ['name' => $person['display_name'], 'email' => $email, 'status' => 'ohne_email'];
continue;
}
$subject = 'Kaffeeliste - Dein aktueller Stand';
$body = saas_render_balance_mail_body(
$person['display_name'],
(int)$person['balance_cents'],
$settings,
$dashboardUrl
);
if ($dryRun) {
saas_log_outbound_email($pdo, $tenantId, $person['participant_id'], 'balance_notice', $subject, 'dry_run', null, $createdByUserId);
$ergebnisse[] = ['name' => $person['display_name'], 'email' => $email, 'status' => 'dry_run'];
continue;
}
$sendResult = saas_send_mail($email, $subject, $body);
if ($sendResult['ok']) {
saas_log_outbound_email($pdo, $tenantId, $person['participant_id'], 'balance_notice', $subject, 'sent', null, $createdByUserId);
$ergebnisse[] = ['name' => $person['display_name'], 'email' => $email, 'status' => 'sent'];
} else {
saas_log_outbound_email($pdo, $tenantId, $person['participant_id'], 'balance_notice', $subject, 'failed', $sendResult['error'] ?? 'unbekannter Fehler', $createdByUserId);
$ergebnisse[] = ['name' => $person['display_name'], 'email' => $email, 'status' => 'failed'];
}
}
if (!$dryRun) {
app_audit_log($pdo, $tenantId, $createdByUserId, 'mail_broadcast.sent', 'tenant', $tenantId, ['empfaenger' => count($ergebnisse)]);
// Post-Redirect-Get: Ergebnis in die Session legen und per GET erneut
// anzeigen, damit ein Browser-Refresh nicht erneut versendet.
$_SESSION['flash_mailversand'] = ['tenant_id' => $tenantId, 'ergebnisse' => $ergebnisse];
header('Location: mailversenden.php');
exit;
}
}
$log = $hasAccess ? saas_fetch_outbound_email_log($pdo, $tenantId, 20) : [];
function mailversenden_status_label(string $status): string
{
return match ($status) {
'sent' => 'Versendet',
'failed' => 'Fehlgeschlagen',
'dry_run' => 'Dry-Run (nicht versendet)',
'ohne_email' => 'Keine E-Mail-Adresse',
default => $status,
};
}
include "header.php";
include "headerline.php";
include "nav.php";
?>
<!-- Banner -->
<section id="banner">
<div class="content">
<?php if ($gesperrteFunktion !== null): ?>
<?php echo app_feature_notice_html($gesperrteFunktion); ?>
<?php elseif (!$hasAccess): ?>
<h2>Kein Zugriff</h2>
<?php else: ?>
<h2>Info-Mail versenden</h2>
<p>Verschickt an alle aktiven Mitglieder eine E-Mail mit dem aktuellen
Kaffeelisten-Stand. Im Dry-Run wird nichts wirklich versendet, aber jeder
Empfänger wird im Versandlog unten protokolliert.</p>
<?php if ($liveFlash): ?>
<div class="hint-box success"><p>Die Info-Mails wurden versendet.</p></div>
<?php endif; ?>
<?php if ($verifikationHinweis !== null): ?>
<div class="hint-box error"><p><?php echo saas_html($verifikationHinweis); ?></p></div>
<?php endif; ?>
<?php if ($ergebnisse !== null): ?>
<h3>Ergebnis dieses Laufs (<?php echo $dryRun ? 'Dry-Run' : 'Live-Versand'; ?>)</h3>
<table>
<tr><th>Name</th><th>E-Mail</th><th>Status</th></tr>
<?php foreach ($ergebnisse as $eintrag): ?>
<tr>
<td><?php echo saas_html($eintrag['name']); ?></td>
<td><?php echo saas_html($eintrag['email']); ?></td>
<td><?php echo saas_html(mailversenden_status_label($eintrag['status'])); ?></td>
</tr>
<?php endforeach; ?>
</table>
<?php endif; ?>
<form method="post" action="mailversenden.php">
<?php echo app_csrf_field(); ?>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="dry_run" id="dry_run" checked>
<label class="form-check-label" for="dry_run">Dry-Run (nichts wirklich versenden, nur protokollieren)</label>
</div>
<button type="submit">Info-Mail versenden</button>
</form>
<h3>Versandlog (letzte 20)</h3>
<table>
<tr><th>Datum</th><th>Mitglied</th><th>Betreff</th><th>Status</th><th>Fehler</th></tr>
<?php if ($log === []): ?>
<tr><td colspan="5">Noch kein Versand protokolliert.</td></tr>
<?php endif; ?>
<?php foreach ($log as $eintrag): ?>
<tr>
<td><?php echo saas_html($eintrag['created_at']); ?></td>
<td><?php echo saas_html($eintrag['display_name'] ?? '—'); ?></td>
<td><?php echo saas_html($eintrag['subject']); ?></td>
<td><?php echo saas_html(mailversenden_status_label($eintrag['status'])); ?></td>
<td><?php echo saas_html($eintrag['error'] ?? ''); ?></td>
</tr>
<?php endforeach; ?>
</table>
<?php endif; ?>
</div>
</section>
<?php include "footer.php"; ?>