22-05-2026 Optimierung der User und Admin Panels
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled

This commit is contained in:
Kevin Adametz 2026-05-22 11:18:59 +02:00
parent d2ba22c0cf
commit e8c47b7553
73 changed files with 10282 additions and 1546 deletions

View file

@ -9,6 +9,14 @@ use Symfony\Component\HttpFoundation\Response;
class LegacyInvoicePdfRenderer
{
private const PAGE_WIDTH = 595;
private const PAGE_HEIGHT = 842;
private const LEFT = 91;
private const RIGHT = 524;
public function inlineResponse(LegacyInvoice $invoice): Response
{
$pdf = $this->render($invoice);
@ -38,14 +46,13 @@ class LegacyInvoicePdfRenderer
public function render(LegacyInvoice $invoice): string
{
$lines = $this->lines($invoice);
$content = "BT\n/F1 11 Tf\n50 790 Td\n14 TL\n";
$data = $this->data($invoice);
$content = '';
$isMedia = $data['is_media'];
foreach ($lines as $line) {
$content .= '('.$this->escapePdfText($line).") Tj\nT*\n";
}
$content .= "ET\n";
$this->drawHeader($content, $isMedia);
$this->drawInvoiceBody($content, $data);
$this->drawFooter($content, $isMedia);
return $this->buildPdf($content);
}
@ -61,16 +68,30 @@ class LegacyInvoicePdfRenderer
}
/**
* @return list<string>
* @return array{
* invoice: LegacyInvoice,
* billing_address: array<string, mixed>,
* invoice_data: array<string, mixed>,
* is_media: bool,
* is_netto: bool,
* tax_percent: int,
* amount: float,
* net_amount: float,
* tax_amount: float,
* service_name: string,
* service_period_begin: string,
* service_period_end: string
* }
*/
private function lines(LegacyInvoice $invoice): array
private function data(LegacyInvoice $invoice): array
{
$payload = $invoice->pdf_payload ?? [];
$billingAddress = data_get($payload, 'billing_address', []);
$billingAddress = data_get($payload, 'billing_address') ?: [];
$invoiceData = data_get($payload, 'invoice', $invoice->raw_snapshot ?? []);
$invoice->loadMissing('user.profile');
$isNetto = (bool) data_get($invoiceData, 'is_netto', false);
$isMedia = (bool) data_get($invoiceData, 'is_media', true);
$taxPercent = $this->taxPercent($invoice);
$amount = $invoice->total_cents / 100;
$netAmount = $isNetto ? $amount : $amount / (1 + ($taxPercent / 100));
@ -81,50 +102,211 @@ class LegacyInvoicePdfRenderer
?? data_get($payload, 'payment_option.article_number')
?? 'Legacy-Leistung';
return array_values(array_filter([
$invoice->legacy_portal->label(),
'adametz.media, Kevin Adametz, In der Lake 4, 33739 Bielefeld',
'www.businessportal24.com',
str_repeat('-', 68),
'',
'Legacy-Rechnung',
'Rechnungsdatum: '.$invoice->invoice_date?->format('d.m.Y'),
'',
'Rechnungsadresse',
data_get($billingAddress, 'name'),
data_get($billingAddress, 'title'),
data_get($billingAddress, 'address'),
trim((string) data_get($billingAddress, 'postal_code').' '.(string) data_get($billingAddress, 'city')),
data_get($billingAddress, 'country_name'),
return [
'invoice' => $invoice,
'billing_address' => $billingAddress,
'invoice_data' => $invoiceData,
'is_media' => $isMedia,
'is_netto' => $isNetto,
'tax_percent' => $taxPercent,
'amount' => $amount,
'net_amount' => $netAmount,
'tax_amount' => $taxAmount,
'service_name' => (string) $serviceName,
'service_period_begin' => $servicePeriodBegin,
'service_period_end' => $servicePeriodEnd,
];
}
private function drawHeader(string &$content, bool $isMedia): void
{
if ($isMedia) {
$content .= $this->text(0, 790, 'adametz.media', 21, 'F2', align: 'center');
$content .= $this->line(180, 778, 415, 778, 0.7);
$content .= $this->text(0, 762, 'adametz.media, Kevin Adametz, In der Lake 4, 33739 Bielefeld', 9, align: 'center');
$content .= $this->text(0, 748, 'www.businessportal24.com', 10, 'F2', align: 'center');
return;
}
$content .= $this->text(self::LEFT, 790, 'Stern Consulting GmbH', 19, 'F2');
$content .= $this->line(self::LEFT, 778, self::RIGHT, 778, 0.7);
$content .= $this->text(self::LEFT, 760, 'Stern Consulting GmbH * Emser Straße 3 * 10719 Berlin', 10, 'F2');
}
/**
* @param array<string, mixed> $data
*/
private function drawInvoiceBody(string &$content, array $data): void
{
/** @var LegacyInvoice $invoice */
$invoice = $data['invoice'];
$billingAddress = $data['billing_address'];
$invoiceDate = $invoice->invoice_date?->format('d.m.Y') ?? 'n/a';
$invoiceNumber = $invoice->number ?? '#'.$invoice->legacy_id;
$y = 700.0;
$content .= $this->text(self::LEFT, $y, 'Rechnungsdatum: '.$invoiceDate, 9);
$y -= 34;
foreach ($this->addressLines($invoice, $billingAddress) as $line) {
$content .= $this->text(self::LEFT, $y, $line, 9);
$y -= 13;
}
$y -= 26;
$content .= $this->text(self::LEFT, $y, 'Leistung:', 9);
$y -= 15;
$content .= $this->text(self::LEFT, $y, $data['service_name'].' auf '.$invoice->legacy_portal->label(), 10, 'F2');
$y = $this->wrappedText($content, self::LEFT, $y - 16, $this->pressReleaseTitle($data), 9, 360);
$y -= 6;
$periodLabel = $data['service_period_begin'] === $data['service_period_end']
? 'Leistungsdatum: '.$data['service_period_begin']
: 'Leistungszeitraum: '.$data['service_period_begin'].' - '.$data['service_period_end'];
$content .= $this->text(self::LEFT, $y, $periodLabel, 9);
$y -= 17;
$content .= $this->text(self::LEFT, $y, 'Rechnungsnummer: '.$invoiceNumber, 10, 'F2');
$y -= 34;
$y = $this->invoiceTable($content, self::LEFT, $y, [
['Rechnungsstellung:', $invoiceDate, false],
['Netto:', $this->formatEuro($data['net_amount']), false],
$data['is_netto'] ? null : ['MwSt. '.$data['tax_percent'].'%:', $this->formatEuro($data['tax_amount']), false],
['Rechnungsbetrag:', $this->formatEuro($data['amount']), true],
]);
$y -= 22;
foreach ($this->paymentLines($data) as $line) {
$content .= $this->text(self::LEFT, $y, $line, 9);
$y -= $line === '' ? 9 : 12;
}
}
private function drawFooter(string &$content, bool $isMedia): void
{
$content .= $this->line(self::LEFT, 76, self::RIGHT, 76, 0.5);
if ($isMedia) {
$columns = [
["adametz.media\nKevin Adametz\nIn der Lake 4\n33739 Bielefeld", 91, 62],
["Tel: +49 5206 7076721\nMail: info@businessportal24.com\nSite: www.businessportal24.com", 165, 62],
["Bankverbindung\nSparkasse Bielefeld\nIBAN: DE96 4805 0161 0065 0356 02\nBIC: SPBIDE3BXXX", 292, 62],
["Steuernummer:\n349 / 5001 / 4350\nUSt-ID: DE298729654", 445, 62],
];
} else {
$columns = [
["Stern Consulting GmbH\nEmser Straße 3\n10719 Berlin\nGF: Thomas Stern", 91, 62],
["Tel: +49 (0)30 / 700 9410 0\nFax: +49 (0)30 / 700 9410 44\nMail: info@stern-consulting.de\nSite: www.stern-consulting.de", 185, 62],
["Bankverbindung\nHypo Vereinsbank\nBLZ: 10020890\nK-Nr: 22865552", 325, 62],
["Registergericht:\nAmtsgericht Charlottenburg\nHRB 134586 B\nUSt-Id-Nr.: DE277693156", 430, 62],
];
}
foreach ($columns as [$text, $x, $y]) {
$this->wrappedText($content, $x, $y, $text, 6, 105, 8);
}
}
/**
* @param array<string, mixed> $billingAddress
* @return list<string>
*/
private function addressLines(LegacyInvoice $invoice, array $billingAddress): array
{
$salutation = trim((string) data_get($billingAddress, 'salutation'));
$name = trim($salutation.' '.(string) data_get($billingAddress, 'name'));
$city = trim((string) data_get($billingAddress, 'postal_code').' '.(string) data_get($billingAddress, 'city'));
$lines = [
$name,
(string) data_get($billingAddress, 'title'),
...$this->splitLines((string) data_get($billingAddress, 'address')),
$city,
(string) data_get($billingAddress, 'country_name'),
$invoice->user?->profile?->tax_id_number ? 'UID-Nr.: '.$invoice->user->profile->tax_id_number : null,
'',
'Leistung: '.$serviceName.' auf '.$invoice->legacy_portal->label(),
$servicePeriodBegin === $servicePeriodEnd
? 'Leistungsdatum: '.$servicePeriodBegin
: 'Leistungszeitraum: '.$servicePeriodBegin.' - '.$servicePeriodEnd,
'Rechnungsnummer: '.($invoice->number ?? '#'.$invoice->legacy_id),
'',
str_repeat('-', 68),
'Rechnungsstellung: '.$invoice->invoice_date?->format('d.m.Y'),
str_repeat('-', 68),
'Netto: '.$this->formatEuro($netAmount),
$isNetto ? null : 'MwSt. '.$taxPercent.'%: '.$this->formatEuro($taxAmount),
str_repeat('-', 68),
'Betrag: '.$this->formatEuro($amount),
str_repeat('-', 68),
'',
'Zahlart: '.($invoice->payment_method ?? 'n/a'),
'Status: '.($invoice->status ?? 'unknown'),
$invoice->paid_at ? 'Bezahlt am: '.$invoice->paid_at->format('d.m.Y') : 'Faellig am: '.$invoice->due_date?->format('d.m.Y'),
'',
'Bitte ueberweisen Sie den Rechnungsbetrag unter Angabe der Rechnungsnummer '.($invoice->number ?? '#'.$invoice->legacy_id).'.',
'Bankverbindung: Sparkasse Bielefeld, IBAN DE96 4805 0161 0065 0356 02, BIC SPBIDE3BXXX',
$isNetto ? 'Reverse Charge: Steuerschuldnerschaft des Leistungsempfaengers.' : null,
'',
str_repeat('-', 68),
'adametz.media | Tel: +49 5206 7076721 | Mail: info@businessportal24.com',
'Steuernummer: 349 / 5001 / 4350 | USt-ID: DE298729654',
], fn (mixed $line): bool => $line !== null && $line !== ''));
];
return array_values(array_filter($lines, fn (?string $line): bool => filled($line)));
}
/**
* @param array<string, mixed> $data
*/
private function pressReleaseTitle(array $data): string
{
return (string) (
data_get($data, 'invoice_data.press_release_title')
?? data_get($data, 'invoice_data.press_release.name')
?? ''
);
}
/**
* @param array<int, array{0: string, 1: string, 2: bool}|null> $rows
*/
private function invoiceTable(string &$content, float $x, float $y, array $rows): float
{
$width = 275.0;
$rowHeight = 22.0;
$valueX = $x + 160;
$content .= $this->line($x, $y, $x + $width, $y, 0.5);
foreach (array_values(array_filter($rows)) as [$label, $value, $bold]) {
$y -= $rowHeight;
$content .= $this->text($x, $y + 7, $label, 9, $bold ? 'F2' : 'F1');
$content .= $this->text($valueX, $y + 7, $value, 9, $bold ? 'F2' : 'F1');
$content .= $this->line($x, $y, $x + $width, $y, 0.5);
}
return $y;
}
/**
* @param array<string, mixed> $data
* @return list<string>
*/
private function paymentLines(array $data): array
{
/** @var LegacyInvoice $invoice */
$invoice = $data['invoice'];
$invoiceNumber = $invoice->number ?? '#'.$invoice->legacy_id;
if (! $data['is_media']) {
$lines = [
'Bitte überweisen Sie den Rechnungsbetrag innerhalb von 7 Werktagen auf unser Geschäftskonto:',
'',
'Kontoinhaber: Stern Consulting GmbH',
'IBAN: DE23100208900022865552',
'BIC: HYVEDEMM488',
'Bank: Hypo Vereinsbank',
'',
'Als Verwendungszweck geben Sie bitte "Rechungsnummer '.$invoiceNumber.'" an!',
'',
'Bitte ignorieren Sie diese Zahlungsaufforderung, falls der Betrag bereits entrichtet wurde.',
];
} else {
$lines = [
'Bitte überweisen Sie den Rechnungsbetrag innerhalb von 7 Werktagen auf unser Geschäftskonto:',
'',
'Wichtig! Ab Januar 2024 gilt die neue Bankverbindung:',
'',
'Kontoinhaber: adametz.media, Kevin Adametz',
'IBAN: DE96 4805 0161 0065 0356 02',
'BIC: SPBIDE3BXXX',
'Bank: Sparkasse Bielefeld',
'',
'Als Verwendungszweck geben Sie bitte "Rechungsnummer '.$invoiceNumber.'" an!',
'',
'Bitte ignorieren Sie diese Zahlungsaufforderung, falls der Betrag bereits entrichtet wurde.',
];
}
if ($data['is_netto']) {
$lines[] = '';
$lines[] = 'Leistungen im Reverse-Charge Verfahren - Steuerschuldnerschaft des Leistungsempfängers';
}
return $lines;
}
private function taxPercent(LegacyInvoice $invoice): int
@ -149,7 +331,7 @@ class LegacyInvoicePdfRenderer
private function formatEuro(float $amount): string
{
return number_format($amount, 2, ',', '.').' EUR';
return number_format($amount, 2, ',', '.').' ';
}
private function buildPdf(string $content): string
@ -157,9 +339,10 @@ class LegacyInvoicePdfRenderer
$objects = [
"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n",
"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n",
"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>\nendobj\n",
"4 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\nendobj\n",
"5 0 obj\n<< /Length ".strlen($content)." >>\nstream\n{$content}endstream\nendobj\n",
"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 ".self::PAGE_WIDTH.' '.self::PAGE_HEIGHT."] /Resources << /Font << /F1 4 0 R /F2 5 0 R >> >> /Contents 6 0 R >>\nendobj\n",
"4 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>\nendobj\n",
"5 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>\nendobj\n",
"6 0 obj\n<< /Length ".strlen($content)." >>\nstream\n{$content}endstream\nendobj\n",
];
$pdf = "%PDF-1.4\n";
@ -190,4 +373,95 @@ class LegacyInvoicePdfRenderer
return str_replace(['\\', '(', ')'], ['\\\\', '\(', '\)'], $encoded ?: Str::ascii($text));
}
private function text(float $x, float $y, string $text, int $size = 9, string $font = 'F1', string $align = 'left'): string
{
if ($align === 'center') {
$x = (self::PAGE_WIDTH - $this->textWidth($text, $size)) / 2;
}
return sprintf(
"BT /%s %d Tf %.2F %.2F Td (%s) Tj ET\n",
$font,
$size,
$x,
$y,
$this->escapePdfText($text),
);
}
private function line(float $x1, float $y1, float $x2, float $y2, float $width = 0.5): string
{
return sprintf("%.2F w %.2F %.2F m %.2F %.2F l S\n", $width, $x1, $y1, $x2, $y2);
}
private function wrappedText(
string &$content,
float $x,
float $y,
string $text,
int $size,
float $width,
float $lineHeight = 12,
string $font = 'F1',
): float {
foreach ($this->wrap($text, $width, $size) as $line) {
if ($line !== '') {
$content .= $this->text($x, $y, $line, $size, $font);
}
$y -= $lineHeight;
}
return $y;
}
/**
* @return list<string>
*/
private function wrap(string $text, float $width, int $size): array
{
$lines = [];
$maxCharacters = max(8, (int) floor($width / ($size * 0.48)));
foreach ($this->splitLines($text) as $paragraph) {
if ($paragraph === '') {
$lines[] = '';
continue;
}
$line = '';
foreach (preg_split('/\s+/', $paragraph) ?: [] as $word) {
$candidate = trim($line.' '.$word);
if ($line !== '' && Str::length($candidate) > $maxCharacters) {
$lines[] = $line;
$line = $word;
continue;
}
$line = $candidate;
}
if ($line !== '') {
$lines[] = $line;
}
}
return $lines;
}
/**
* @return list<string>
*/
private function splitLines(string $text): array
{
return explode("\n", str_replace(["\r\n", "\r"], "\n", $text));
}
private function textWidth(string $text, int $size): float
{
return Str::length($text) * $size * 0.48;
}
}