<?php
declare(strict_types=1);
namespace Machweis\Backend;
/** Minimal dependency-free PDF renderer for immutable finalized report snapshots. */
final class ServerPdfRenderer
{
/** @param array<string,mixed> $payload */
public function render(array $payload, int $version): string
{
$lines = [
'MACHWEIS – Leistungsnachweis',
'',
'Kunde: ' . $this->text($payload['customer'] ?? ''),
'Baustelle: ' . $this->text($payload['site'] ?? ''),
'Leistungsdatum: ' . substr($this->text($payload['date'] ?? ''), 0, 10),
'Arbeitszeit: ' . $this->time($payload, 'startHour', 'startMinute') . ' – ' . $this->time($payload, 'endHour', 'endMinute') . ' Uhr',
'Pause: ' . max(0, (int) ($payload['breakMinutes'] ?? 0)) . ' Minuten',
'',
'Leistungen:',
];
$activities = is_array($payload['activityItems'] ?? null) ? $payload['activityItems'] : [$payload['activity'] ?? ''];
foreach ($activities as $index => $activity) {
$value = $this->text($activity);
if ($value !== '') $lines[] = ($index + 1) . '. ' . $value;
}
$materials = is_array($payload['materialItems'] ?? null) ? $payload['materialItems'] : [];
if ($materials !== []) {
$lines[] = '';
$lines[] = 'Material:';
foreach ($materials as $material) {
if (!is_array($material)) continue;
$lines[] = trim($this->text($material['quantity'] ?? '1') . ' ' . $this->text($material['unit'] ?? 'Stk.') . ' · ' . $this->text($material['name'] ?? ''));
}
}
$additional = $this->text($payload['additionalOrder'] ?? '');
if ($additional !== '') { $lines[] = ''; $lines[] = 'Zusatzauftrag: ' . $additional; }
$lines[] = '';
$lines[] = 'Unterschrieben · unveränderliche Version V' . $version;
return $this->pdf($lines);
}
/** @param array<string,mixed> $payload */
private function time(array $payload, string $hour, string $minute): string
{
return str_pad((string) max(0, min(23, (int) ($payload[$hour] ?? 0))), 2, '0', STR_PAD_LEFT) . ':' . str_pad((string) max(0, min(59, (int) ($payload[$minute] ?? 0))), 2, '0', STR_PAD_LEFT);
}
private function text(mixed $value): string
{
$text = trim(is_scalar($value) ? (string) $value : '');
$encoded = function_exists('iconv') ? iconv('UTF-8', 'Windows-1252//TRANSLIT//IGNORE', $text) : $text;
return is_string($encoded) ? $encoded : '';
}
/** @param list<string> $lines */
private function pdf(array $lines): string
{
$content = "BT\n/F1 16 Tf\n50 790 Td\n";
foreach ($lines as $index => $line) {
if ($index > 0) $content .= "0 -20 Td\n";
$content .= '(' . str_replace(['\\', '(', ')'], ['\\\\', '\\(', '\\)'], $line) . ") Tj\n";
}
$content .= "ET\n";
$objects = [
'<< /Type /Catalog /Pages 2 0 R >>',
'<< /Type /Pages /Kids [3 0 R] /Count 1 >>',
'<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>',
'<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>',
'<< /Length ' . strlen($content) . " >>\nstream\n" . $content . 'endstream',
];
$pdf = "%PDF-1.4\n"; $offsets = [0];
foreach ($objects as $number => $object) { $offsets[] = strlen($pdf); $pdf .= ($number + 1) . " 0 obj\n" . $object . "\nendobj\n"; }
$xref = strlen($pdf); $pdf .= 'xref' . "\n0 " . (count($objects) + 1) . "\n0000000000 65535 f \n";
foreach (array_slice($offsets, 1) as $offset) $pdf .= str_pad((string) $offset, 10, '0', STR_PAD_LEFT) . " 00000 n \n";
return $pdf . 'trailer' . "\n<< /Size " . (count($objects) + 1) . " /Root 1 0 R >>\nstartxref\n" . $xref . "\n%%EOF\n";
}
}
