| Server IP : 202.61.199.114 / Your IP : 216.73.217.139 Web Server : nginx/1.22.1 System : Linux de.arni-solutions.de 6.1.0-49-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.174-1 (2026-05-26) x86_64 User : web20 ( 1018) PHP Version : 8.4.23 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : ON Directory : /var/www/seaside2.pacim.de/web/app/services/ |
Upload File : |
<?php
/**
* Rendert E-Mail-Templates: ersetzt Platzhalter durch konkrete Buchungs-Daten
* und produziert Subject + Body + Recipient + Attachment-Pfade.
*
* Verwendung:
* $renderer = new EmailTemplateRenderer($db);
* $rendered = $renderer->renderForBooking($template, $bookingId);
* // → ['subject' => '…', 'body' => '…', 'to' => '…', 'attachments' => [['path' => '…', 'name' => '…']]]
*
* Platzhalter (zwischen geschweiften Klammern, ASCII-Variante {placeholder}):
* Kunde: {customer.first_name}, {customer.last_name}, {customer.company},
* {customer.email}, {customer.phone}, {customer.greeting}
* Buchung: {booking.no}, {booking.travel_from}, {booking.travel_to}, {booking.duration_days},
* {booking.arrival_time}, {booking.license_plate}, {booking.total_gross},
* {booking.total_net}, {booking.event_title}, {booking.event_ship},
* {booking.event_terminal}, {booking.event_starts_at}, {booking.event_ends_at},
* {booking.status}, {booking.payment_status}, {booking.checkin_status}
* Sonst: {invoice.no}, {company.name}, {company.email}
* Liste: {products_list} — formatierte HTML-Tabelle (Produkt · Menge · Preis · Summe)
* {products_list_text} — Plain-Text-Variante (pro Zeile: 1x Außen … 90,00 €)
*/
class EmailTemplateRenderer
{
private PDO $pdo;
private Booking $bookingRepo;
private Invoice $invoiceRepo;
private Settings $settingsRepo;
public function __construct(Database $db, Booking $b, Invoice $i, Settings $s)
{
$this->pdo = $db->getConnection();
$this->bookingRepo = $b;
$this->invoiceRepo = $i;
$this->settingsRepo = $s;
}
public function renderForBooking(array $template, int $bookingId): array
{
$booking = $this->bookingRepo->find($bookingId);
if (!$booking) {
throw new RuntimeException('Buchung nicht gefunden.');
}
$items = $this->bookingRepo->products($bookingId);
$invoice = $this->invoiceRepo->findByBooking($bookingId);
$vars = $this->buildVars($booking, $items, $invoice);
$subject = $this->replaceAll((string)$template['subject'], $vars);
$body = $this->replaceAll((string)$template['body_html'], $vars);
$attachments = [];
if (!empty($template['attach_confirmation']) && !empty($booking['confirmation_path'])) {
$abs = PdfRenderer::absolutePath((string)$booking['confirmation_path']);
if ($abs && is_file($abs)) {
$attachments[] = ['path' => $abs, 'name' => 'Buchungsbestaetigung-' . ($booking['booking_no'] ?? $bookingId) . (str_ends_with($abs, '.pdf') ? '.pdf' : '.html')];
}
}
if (!empty($template['attach_invoice']) && $invoice) {
$pdf = !empty($invoice['pdf_path']) ? PdfRenderer::absolutePath((string)$invoice['pdf_path']) : null;
if ($pdf && is_file($pdf)) {
$attachments[] = ['path' => $pdf, 'name' => 'Rechnung-' . ($invoice['invoice_no'] ?? $invoice['id']) . '.pdf'];
} elseif (!empty($invoice['html_path'])) {
$html = PdfRenderer::absolutePath((string)$invoice['html_path']);
if ($html && is_file($html)) {
$attachments[] = ['path' => $html, 'name' => 'Rechnung-' . ($invoice['invoice_no'] ?? $invoice['id']) . '.html'];
}
}
}
return [
'subject' => $subject,
'body' => $body,
'to' => (string)($booking['c_email'] ?? ''),
'cc' => $this->splitAddresses((string)($template['cc_addresses'] ?? '')),
'bcc' => $this->splitAddresses((string)($template['bcc_addresses'] ?? '')),
'attachments' => $attachments,
'booking' => $booking,
'vars' => $vars,
];
}
/**
* Liefert die für die UI dokumentierte Liste der Platzhalter (Anzeige im Editor).
* @return array<string,string> Map placeholder → kurze Beschreibung
*/
public static function placeholderList(): array
{
return [
'{customer.first_name}' => 'Vorname',
'{customer.last_name}' => 'Nachname',
'{customer.company}' => 'Firma (falls vorhanden)',
'{customer.email}' => 'E-Mail',
'{customer.phone}' => 'Telefon',
'{customer.greeting}' => 'Anrede ("Sehr geehrter Herr X" o.ä.)',
'{booking.no}' => 'Buchungs-Nr.',
'{booking.travel_from}' => 'Reisebeginn (TT.MM.JJJJ)',
'{booking.travel_to}' => 'Reiseende',
'{booking.duration_days}'=> 'Reisedauer in Tagen',
'{booking.arrival_time}' => 'Anreisezeit',
'{booking.license_plate}'=> 'Kennzeichen',
'{booking.total_gross}' => 'Gesamtbetrag (brutto)',
'{booking.total_net}' => 'Gesamtbetrag (netto)',
'{booking.event_title}' => 'Event-Titel',
'{booking.event_ship}' => 'Schiff',
'{booking.event_terminal}'=> 'Terminal',
'{booking.event_starts_at}'=> 'Event-Start',
'{booking.event_ends_at}' => 'Event-Ende',
'{booking.status}' => 'Buchungs-Status',
'{booking.payment_status}'=> 'Zahlungs-Status',
'{booking.checkin_status}'=> 'Check-in-Status',
'{invoice.no}' => 'Rechnungs-Nr. (falls vorhanden)',
'{company.name}' => 'Firmenname (aus Einstellungen)',
'{company.email}' => 'Firmen-E-Mail',
'{products_list}' => 'Tabelle der gebuchten Leistungen (HTML)',
'{products_list_text}' => 'Auflistung der Leistungen (Plain-Text)',
];
}
private function buildVars(array $b, array $items, ?array $invoice): array
{
$name = trim(($b['c_first_name'] ?? '') . ' ' . ($b['c_last_name'] ?? ''));
$greeting = $this->buildGreeting($b);
$days = 0;
if (!empty($b['travel_from']) && !empty($b['travel_to'])) {
$f = strtotime((string)$b['travel_from']);
$t = strtotime((string)$b['travel_to']);
if ($f && $t && $t >= $f) {
$days = (int)round((strtotime(date('Y-m-d', $t) . ' 00:00:00') - strtotime(date('Y-m-d', $f) . ' 00:00:00')) / 86400);
}
}
$company = $this->settingsRepo->company();
return [
'customer.first_name' => (string)($b['c_first_name'] ?? ''),
'customer.last_name' => (string)($b['c_last_name'] ?? ''),
'customer.company' => (string)($b['c_company'] ?? ''),
'customer.email' => (string)($b['c_email'] ?? ''),
'customer.phone' => (string)($b['c_phone'] ?? ''),
'customer.greeting' => $greeting,
'booking.no' => (string)($b['booking_no'] ?? ''),
'booking.travel_from' => $this->fmtDate($b['travel_from'] ?? null),
'booking.travel_to' => $this->fmtDate($b['travel_to'] ?? null),
'booking.duration_days' => (string)$days,
'booking.arrival_time' => $this->fmtTime($b['arrival_time'] ?? null),
'booking.license_plate' => (string)($b['license_plate'] ?? ''),
'booking.total_gross' => $this->fmtMoney($b['total_gross'] ?? 0),
'booking.total_net' => $this->fmtMoney($b['total_net'] ?? 0),
'booking.event_title' => (string)($b['event_title'] ?? ''),
'booking.event_ship' => (string)($b['event_ship'] ?? ''),
'booking.event_terminal' => (string)($b['event_terminal'] ?? ''),
'booking.event_starts_at' => $this->fmtDateTime($b['event_starts_at'] ?? null),
'booking.event_ends_at' => $this->fmtDateTime($b['event_ends_at'] ?? null),
'booking.status' => (string)($b['status'] ?? ''),
'booking.payment_status' => (string)($b['payment_status'] ?? ''),
'booking.checkin_status' => (string)($b['checkin_status'] ?? ''),
'invoice.no' => $invoice ? (string)($invoice['invoice_no'] ?? '') : '',
'company.name' => (string)($company['name'] ?? ''),
'company.email' => (string)($company['email'] ?? ''),
'products_list' => $this->renderProductsHtml($items),
'products_list_text' => $this->renderProductsText($items),
];
}
private function buildGreeting(array $b): string
{
$first = trim((string)($b['c_first_name'] ?? ''));
$last = trim((string)($b['c_last_name'] ?? ''));
if ($last === '') {
return 'Sehr geehrte Damen und Herren';
}
// Geschlecht ist im Booking-Row nicht zuverlässig — neutral grüßen.
return 'Sehr geehrte/r ' . trim($first . ' ' . $last);
}
private function renderProductsHtml(array $items): string
{
if (empty($items)) return '<p><em>Keine Positionen.</em></p>';
$rows = '';
$sum = 0.0;
foreach ($items as $it) {
$line = (float)$it['total_gross'];
$sum += $line;
$rows .= sprintf(
'<tr><td style="padding:4px 8px;border-bottom:1px solid #e9ecef;">%s</td>'
. '<td style="padding:4px 8px;text-align:right;border-bottom:1px solid #e9ecef;">%dx</td>'
. '<td style="padding:4px 8px;text-align:right;border-bottom:1px solid #e9ecef;">%s</td>'
. '<td style="padding:4px 8px;text-align:right;border-bottom:1px solid #e9ecef;font-weight:600;">%s</td></tr>',
htmlspecialchars((string)($it['product_title'] ?? ''), ENT_QUOTES, 'UTF-8'),
(int)$it['quantity'],
$this->fmtMoney($it['price_gross'] ?? 0),
$this->fmtMoney($line)
);
}
return '<table cellpadding="0" cellspacing="0" style="border-collapse:collapse;font-family:inherit;width:100%;max-width:560px;">'
. '<thead><tr>'
. '<th style="padding:6px 8px;text-align:left;border-bottom:2px solid #1a1a1a;">Leistung</th>'
. '<th style="padding:6px 8px;text-align:right;border-bottom:2px solid #1a1a1a;">Menge</th>'
. '<th style="padding:6px 8px;text-align:right;border-bottom:2px solid #1a1a1a;">Einzelpreis</th>'
. '<th style="padding:6px 8px;text-align:right;border-bottom:2px solid #1a1a1a;">Summe</th>'
. '</tr></thead>'
. '<tbody>' . $rows . '</tbody>'
. '<tfoot><tr><td colspan="3" style="padding:8px;text-align:right;font-weight:700;">Gesamt</td>'
. '<td style="padding:8px;text-align:right;font-weight:700;">' . $this->fmtMoney($sum) . '</td></tr></tfoot>'
. '</table>';
}
private function renderProductsText(array $items): string
{
if (empty($items)) return 'Keine Positionen.';
$lines = [];
foreach ($items as $it) {
$lines[] = sprintf('%dx %s · %s',
(int)$it['quantity'],
(string)($it['product_title'] ?? ''),
$this->fmtMoney($it['total_gross'] ?? 0)
);
}
return implode("\n", $lines);
}
private function replaceAll(string $template, array $vars): string
{
$search = $replace = [];
foreach ($vars as $key => $val) {
$search[] = '{' . $key . '}';
$replace[] = (string)$val;
}
return str_replace($search, $replace, $template);
}
private function splitAddresses(string $csv): array
{
return array_values(array_filter(array_map('trim', preg_split('/[,;\s]+/', $csv) ?: [])));
}
private function fmtDate($v): string
{
if (!$v) return '';
$ts = strtotime((string)$v);
return $ts ? date('d.m.Y', $ts) : (string)$v;
}
private function fmtTime($v): string
{
if (!$v) return '';
$ts = strtotime((string)$v);
return $ts ? date('H:i', $ts) . ' Uhr' : (string)$v;
}
private function fmtDateTime($v): string
{
if (!$v) return '';
$ts = strtotime((string)$v);
return $ts ? date('d.m.Y H:i', $ts) : (string)$v;
}
private function fmtMoney($v): string
{
return number_format((float)$v, 2, ',', '.') . ' €';
}
}