requestEpiApi('/v1/product/'.$id.'?cl=' . Epirent_Mandant))->payload[0]; $productImage =json_decode($Epi->requestEpiApi('/v1/product/image/'.$id.'?cl=' . Epirent_Mandant))->payload[0]->image_data; function s(?string $v): string { return trim((string)$v); } function fnum(?float $v, int $dec = 2): string { if ($v === null) return ''; $n = number_format((float)$v, $dec, ',', '.'); return rtrim(rtrim($n, '0'), ','); } //Set Variables $productName = $product->name; $productNumber = $product->product_no; $bruttoWeightSum = $product->tech_data->weight_gro; $bundle = buildBundleForProduct($Epi, $product); /** Test-Daten */ $dimsBHT =$product->tech_data->width_gro." x ".$product->tech_data->depth_gro." x ".$product->tech_data->height_gro; $volume = ((float)$product->tech_data->height_gro * (float)$product->tech_data->width_gro * (float)$product->tech_data->depth_gro)* 0.000001; $storageLoc = $product->product_data->storage_location_nearby; /** Layout */ $PAGE_SIZE = 'A4'; $MARGIN_LEFT = 8; $MARGIN_TOP = 8; $MARGIN_RIGHT = 8; $MARGIN_BOTTOM = 10; $CELL_PAD = 1.6; $LINE_GRAY = '#efefef'; $LEVEL_MIN = 1; $LEVEL_MAX = 5; $INDENT_PX_PER_LEVEL = 10; $BASE_FONT_PX = 10; $FONT_STEP_PX = 1.5; /** TCPDF */ $pdf = new TCPDF('P', 'mm', $PAGE_SIZE, true, 'UTF-8', false); // 1) Pfade sauber $fontDir = realpath(__DIR__ . '/../src/assets/font') . '/'; $tcpdfFontsDir = defined('K_PATH_FONTS') ? K_PATH_FONTS : (dirname((new \ReflectionClass('TCPDF'))->getFileName()) . '/fonts/'); // 2) Rechte (einmalig sicherstellen) // -> hast du schon gefixt // 3) TTFs registrieren und Rückgabewerte merken $fontReg = file_exists($fontDir.'Hurme3/HurmeGeometricSans3-Regular.ttf') ? TCPDF_FONTS::addTTFfont($fontDir.'Hurme3/HurmeGeometricSans3-Regular.ttf', 'TrueTypeUnicode', '', 96) : false; $fontBold = file_exists($fontDir.'Hurme1/HurmeGeometricSans1-Bold.ttf') ? TCPDF_FONTS::addTTFfont($fontDir.'Hurme1/HurmeGeometricSans1-Bold.ttf', 'TrueTypeUnicode', '', 96) : false; $fontItal = file_exists($fontDir.'Hurme3/HurmeGeometricSans3-Italic.ttf') ? TCPDF_FONTS::addTTFfont($fontDir.'Hurme3/HurmeGeometricSans3-Italic.ttf', 'TrueTypeUnicode', '', 96) : false; $fontBI = file_exists($fontDir.'Hurme1/HurmeGeometricSans1-BoldItalic.ttf') ? TCPDF_FONTS::addTTFfont($fontDir.'Hurme1/HurmeGeometricSans1-BoldItalic.ttf', 'TrueTypeUnicode', '', 96) : false; // 4) Fallback setzen $baseFont = $fontReg ?: 'dejavusans'; // 5) Header/Footer und Defaults $pdf->setHeaderFont([$baseFont, '', 10]); $pdf->setFooterFont([$baseFont, '', 9]); $pdf->SetDefaultMonospacedFont('courier'); // 6) *** AB JETZT KEIN dejavusans MEHR SETZEN! *** $pdf->SetFont($baseFont, '', 10.5); $pdf->SetCreator('VT-Media EpiWebview'); $pdf->SetAuthor('VT-Media'); $pdf->SetTitle('Kistenetikett '.$productName); $pdf->SetSubject('Kistenetikett'); $pdf->setPrintHeader(false); $pdf->setPrintFooter(false); $pdf->SetMargins($MARGIN_LEFT, $MARGIN_TOP, $MARGIN_RIGHT); $pdf->SetAutoPageBreak(true, $MARGIN_BOTTOM); $pdf->setImageScale(1.0); $pdf->setFontSubsetting(true); $pdf->setCellPadding($CELL_PAD); $pdf->AddPage(); // --- LOGO in fester Box mit Rahmen, zentriert & verzerrungsfrei --- $logoPath = __DIR__ . Labelprint_Logopath; $startX = $pdf->GetX(); $startY = $pdf->GetY(); // feste Rahmen-Größe (in mm) $frameW = 38.0; // Gesamtbreite des Rahmens $frameH = 20; // Gesamthöhe des Rahmens $frameOffsetX = 1.4; // Abstand vom linken Margin (wie vorher genutzt) $frameOffsetY = 1.5; // Abstand von oben (wie vorher genutzt) // Rahmen-Position $frameX = $startX + $frameOffsetX; $frameY = $startY + $frameOffsetY; // Rahmen zeichnen $pdf->SetLineWidth(0.3); $pdf->Rect($frameX, $frameY, $frameW, $frameH, 'D'); // Logo innerhalb des Rahmens mit Innenabstand $innerPad = 1.2; // mm Luft im Rahmen $maxW = $frameW - 2 * $innerPad; $maxH = $frameH - 2 * $innerPad; if (is_file($logoPath)) { // Seitenverhältnis ermitteln $imgWpx = $imgHpx = 0; if (@list($imgWpx, $imgHpx) = @getimagesize($logoPath)) { $ratio = ($imgHpx > 0) ? ($imgWpx / $imgHpx) : 1.0; // proportional in den Rahmen einpassen if ($maxW / $maxH > $ratio) { $drawH = $maxH; $drawW = $maxH * $ratio; } else { $drawW = $maxW; $drawH = $maxW / $ratio; } // zentrieren $imgX = $frameX + ($frameW - $drawW) / 2; $imgY = $frameY + ($frameH - $drawH) / 2; // ausgeben $pdf->Image($logoPath, $imgX, $imgY, $drawW, $drawH, '', '', '', false, 300); } } $afterLogoX = $frameX + $frameW - 1.4; // gleicher Abstand wie zuvor, jetzt basierend auf Rahmenbreite $afterLogoBottomY = $frameY + $frameH; // falls du unten bündig etwas brauchst $pdf->SetXY($afterLogoX, $startY); // Cursor rechts neben dem Logo positionieren $addrHtml = '
'.Labelprint_Addresstext.'
'; // Feste Box rechts neben dem Logo (direkt bündig) $addrBoxW = 30.0; // feste Breite (mm) $addrBoxH = 20.0; // feste Höhe (mm) $addrPad = 0.5; // Innenabstand Inhalt → Rahmen (mm) // bündig rechts neben dem Logo-Rahmen starten $addrX = $frameX + $frameW; // kein zusätzlicher Abstand $addrY = $startY+1.5; // gleiche Oberkante wie Logo // 1) festen Rahmen zeichnen $pdf->SetLineWidth(0.3); $pdf->Rect($addrX, $addrY, $addrBoxW, $addrBoxH, 'D'); // 2) HTML-Inhalt in die Box schreiben, mit Clipping (Überhang wird abgeschnitten) $pdf->StartTransform(); $pdf->Rect($addrX, $addrY, $addrBoxW, $addrBoxH, 'CNZ'); // Clip auf Box setzen $pdf->writeHTMLCell( $addrBoxW - 2*$addrPad, // feste Breite des Inhalts $addrBoxH - 2*$addrPad, // feste Höhe des Inhalts $addrX + $addrPad, // x mit Innenabstand $addrY + $addrPad, // y mit Innenabstand $addrHtml, // dein HTML 0, // kein Border (Rahmen haben wir schon) 0, // ln false, // fill true, // reseth 'L', // align true // autopadding ); $pdf->StopTransform(); // Cursor steht bereits auf: $pdf->SetXY($addrX + $addrBoxW, $addrY); // 1) HTML OHNE border (sonst Doppelrahmen) $productionHtml = '
Produktion



'; // 2) Feste Box rechts neben der Adress-Box $prodX = $addrX + $addrBoxW; // bündig rechts neben Adresse $prodY = $addrY; // gleiche Oberkante // Breite: restliche Seitenbreite bis zum rechten Rand $prodW = $pdf->getPageWidth() - $MARGIN_RIGHT - $prodX+2; // Höhe: fix (anpassen nach Wunsch) $prodH = 20.0; // mm $prodPad = 0; // Innenabstand // 3) EIN sichtbarer Rahmen zeichnen $pdf->SetLineWidth(0.3); $pdf->Rect($prodX, $prodY, $prodW, $prodH, 'D'); // 4) Inhalt in die Box schreiben und clippen (Überhang abgeschnitten) $pdf->StartTransform(); $pdf->Rect($prodX, $prodY, $prodW, $prodH, 'CNZ'); // Clip auf Box setzen $pdf->writeHTMLCell( $prodW - 2*$prodPad, // feste Breite des Inhalts $prodH - 2*$prodPad, // feste Höhe des Inhalts $prodX + $prodPad, // x mit Innenabstand $prodY + $prodPad, // y mit Innenabstand $productionHtml, // Inhalt (ohne border!) 0, // kein Border (Rahmen haben wir schon) 0, // ln false, // fill true, // reseth 'L', // align true // autopadding ); $pdf->StopTransform(); // 5) (Optional) Cursor direkt rechts neben der Box positionieren $pdf->SetXY($prodX + $prodW, $prodY); // === Name-Box: feste Größe, Text bricht, kein Doppelrahmen === // Breite: gesamte Restbreite bis zum rechten Rand (unter Adresse + Produktion) $nameX = $frameX; // beginnt rechts neben dem Logo/Adressbereich $nameW = $pdf->getPageWidth() - $MARGIN_RIGHT - $nameX-65.2; // Höhe & Padding der Box (anpassen nach Wunsch) $nameBoxH = 18.2; // mm - feste Höhe $namePad = 1.5; // mm - Innenabstand // Y: direkt unter die höhere der beiden Boxen (Adresse vs. Produktion) $yAfterRow = max($addrY + $addrBoxH, $prodY + $prodH) + 0.0; // +2 mm Abstand $nameY = $yAfterRow; // Inhalt OHNE border (sonst Doppelrahmen) $nameHtml = '
' . htmlspecialchars($productName) . '
'; // 1) Sichtbaren Rahmen zeichnen (feste Größe) $pdf->SetLineWidth(0.3); $pdf->Rect($nameX, $nameY, $nameW, $nameBoxH, 'D'); // 2) Inhalt in die Box schreiben und clippen $pdf->StartTransform(); $pdf->Rect($nameX, $nameY, $nameW, $nameBoxH, 'CNZ'); // Clip auf Box setzen $pdf->writeHTMLCell( $nameW - 2*$namePad, // feste Breite für den Text $nameBoxH - 2*$namePad, // feste Höhe für den Text $nameX + $namePad, // x mit Innenabstand $nameY + $namePad, // y mit Innenabstand $nameHtml, 0, // kein HTML-Border 0, // ln false, // fill true, // reseth 'L', // align true // autopadding ); $pdf->StopTransform(); // Optional: Cursor unter die Box setzen (falls du danach im Fluss weitermachen willst) $pdf->SetXY($nameX, $nameY + $nameBoxH); // 1) Box-Geometrie (immer gleich groß) $pnBoxW = 67.0; // mm – Gesamtbreite der Box $pnBoxH = 18.2; // mm – Gesamthöhe der Box $pnBoxPad = 2.0; // mm – Innenabstand innerhalb der Box // Position der Box (aktuell am rechten Rand oben; passe frei an) $pnBoxX = $pdf->getPageWidth() - $MARGIN_RIGHT - $pnBoxW+1.8; // bündig am rechten Rand $pnBoxY = $startY+21.5; // gleiche Oberkante wie Kopf // 2) Sichtbaren Rahmen zeichnen (einmalig) $pdf->SetLineWidth(0.3); $pdf->Rect($pnBoxX, $pnBoxY, $pnBoxW, $pnBoxH, 'D'); // 3) Clipping aktivieren, damit Inhalt in der Box bleibt $pdf->StartTransform(); $pdf->Rect($pnBoxX, $pnBoxY, $pnBoxW, $pnBoxH, 'CNZ'); // Clip auf die Box // 4) Produktnummer: frei verschiebbar innerhalb der Box $pnCombined = trim($productNumber . $deviceNumber); $pnText = htmlspecialchars($pnCombined !== '' ? $pnCombined : 'N/A'); // Offsets innerhalb der Box – hier stellst du die Position ein $pnTextOffsetX = 2.0; // mm von linker Innenkante $pnTextOffsetY = 1; // mm von oberer Innenkante $pnTextW = $pnBoxW - 2*$pnBoxPad; // nutzbare Breite $pnTextH = 8.0; // feste Höhe für den PN-Textbereich (mm) $pnHtml = '
'.$pnText.'
'; // PN-Text rendern (ohne HTML-Border!) $pdf->writeHTMLCell( $pnTextW, // Breite Textbereich $pnTextH, // Höhe Textbereich $pnBoxX + $pnBoxPad + $pnTextOffsetX, // X in der Box $pnBoxY + $pnBoxPad + $pnTextOffsetY, // Y in der Box $pnHtml, 0, 0, false, true, 'L', true ); // 5) QR-Code: frei verschiebbar innerhalb der Box, verzerrungsfrei per size $qrData = trim((string)($productNumber . $deviceNumber)); if ($qrData === '') { $qrData = 'N/A'; } $qrSize = 16.0; // mm – Kantenlänge des QR $qrOffsetX = $pnBoxW - $pnBoxPad - $qrSize - 2.0; // mm von linker Innenkante $qrOffsetY = $pnBoxH - $pnBoxPad - $qrSize +0.6; // mm von oberer Innenkante $qrX = $pnBoxX + $qrOffsetX; $qrY = $pnBoxY + $qrOffsetY; $qrStyle = [ 'border' => 0, 'vpadding' => 0, 'hpadding' => 0, 'fgcolor' => [0,0,0], 'bgcolor' => false, ]; // QR rendern (Warnings/Deprecated für diesen Block stumm schalten) $_old_reporting = error_reporting(); error_reporting($_old_reporting & ~(E_WARNING | E_DEPRECATED | E_NOTICE)); $pdf->write2DBarcode($qrData, 'QRCODE,H', $qrX, $qrY, $qrSize, $qrSize, $qrStyle, 'N'); error_reporting($_old_reporting); // 6) Clipping beenden $pdf->StopTransform(); // 7) Optional: Cursor unter die PN-Box setzen, falls du im Fluss weiter willst $pdf->SetXY($pnBoxX, $pnBoxY + $pnBoxH); /** Bundle */ $bundleHeader = '
 Bundleinhalt
'; $pdf->writeHTMLCell(70, 50, 7.65, 46, $bundleHeader, 0, 0, 0, true, 'L', true); $bundleHeaderFillable = '
'; //$pdf->writeHTMLCell(130.8, 39.8, 74.5, 46, $bundleHeaderFillable, 0, 0, 0, false, 'L', true); drawFixedBox($pdf, 76.0, 47.7, 127.8, 4.65); $bundleRows = ''; $BASE_FONT_PX = 10.0; // Level 1 $FONT_STEP_PX = 1.5; // je Ebene kleiner $MIN_FONT_PX = 6.0; $LINE_HEIGHT_BASE = 1.15; // Basis Zeilenhöhe $LINE_HEIGHT_STEP = 0.05; // je Ebene etwas kompakter $ROW_PAD_V_MM = 0.5; // vertikales Padding je Zeile $ROW_PAD_H_MM = 0.8; // horizontales Padding $WEIGHT_PAD_RIGHT_MM = 1.6; // mehr Abstand zum rechten Rand $INDENT_MM_PER_LVL = 2.8; // Einrückung je Ebene in mm (stabil!) foreach ($bundle as $item) { $qty = (int)($item['qty'] ?? 1); $text = htmlspecialchars($item['text'] ?? ''); $lvl = max($LEVEL_MIN, min($LEVEL_MAX, (int)($item['level'] ?? 1))); $wkg = isset($item['weight_kg']) ? (float)$item['weight_kg'] : null; $textFontPx = max($MIN_FONT_PX, $BASE_FONT_PX - ($lvl - 1) * $FONT_STEP_PX); $weightFontPx = max($MIN_FONT_PX, $BASE_FONT_PX - ($lvl - 1) * $FONT_STEP_PX); $lineHeight = max(1.0, $LINE_HEIGHT_BASE - ($lvl - 1) * $LINE_HEIGHT_STEP); // Einrückung robust über mm – aber nur Tabelle nutzen, wenn > 0 mm $indentMm = max(0, ($lvl - 1) * $INDENT_MM_PER_LVL); // Name-Spalte: ohne Spacer-Tabelle bei Level 1 (indent=0), sonst mit if ($indentMm > 0) { $nameCellHtml = '
'.$text.'
'; } else { // Level 1: direkt rendern $nameCellHtml = ''.$text.''; } $bundleRows .= ' '.$qty.' '.$nameCellHtml.' '.($wkg !== null ? fnum($wkg).' kg' : '').' '; } $bundleHtml = ' '.$bundleRows.'
'; $pdf->writeHTMLCell( $pdf->getPageWidth() - 12.4, 0, 7.8, 50.8, $bundleHtml, 0, 0, 0, true, 'L', true ); /** ---------- Meta-Block dynamisch unterhalb der Bundle-Tabelle ---------- */ // Höhe der zuletzt gerenderten Bundle-Tabelle holen $bundleH = $pdf->getLastH(); // Y-Position direkt nach der Bundle-Tabelle (mit 3 mm Abstand) $metaY = 47.6 + $bundleH; // 50 = dein bisheriges Y der Bundle-Tabelle $metaX = $MARGIN_LEFT; $metaW = $pdf->getPageWidth() - $MARGIN_LEFT - $MARGIN_RIGHT; // Falls Meta-Block zu nah am Seitenende wäre → neue Seite $usableBottom = $pdf->getPageHeight() - $MARGIN_BOTTOM; if ($metaY > $usableBottom - 40) { // 40 mm Puffer für Meta $pdf->AddPage(); $metaY = $MARGIN_TOP; } // --- Inhalt wie gehabt --- $leftMeta = '
Maße L x B x H in cm
'.htmlspecialchars($dimsBHT).'
Gewicht in Kg
'.fnum($bruttoWeightSum).' Kg
Volumen in m³
'.fnum($volume, 2).' m³
Lagerplatz
'.htmlspecialchars($storageLoc).'
'; // ---- Koordinaten der Meta-Zeile bestimmen (nimm deine Werte) ---- // Falls du sie schon berechnet hast, nutze deine $metaX, $metaY, $metaW. // Beispiel, wie du sie meist setzt: $metaX = $MARGIN_LEFT; $metaW = $pdf->getPageWidth() - $MARGIN_LEFT - $MARGIN_RIGHT; // $metaY hast du zuvor als Ziel-Y für den Meta-Block berechnet: /// $metaY = ... (z.B. 50 + $bundleH + 3); // ---- Spaltenbreiten wie in deinem metaWrap: 62% | 2% | 36% ---- $leftW = $metaW * 0.62; $gapW = $metaW * 0.02; $rightW = $metaW * 0.36; // Obere linke Ecke der rechten Spalte: $rightX = $metaX + $leftW + $gapW; $rightY = $metaY; // ---- Fester Rahmen in der rechten Spalte ---- $frameOuterPad = 1.0; // mm: Abstand vom Spaltenrand $frameW = $rightW - 2*$frameOuterPad+8; // Rahmenbreite $frameH = 24.38; // mm: feste Rahmenhöhe, nach Wunsch $frameX = $rightX + $frameOuterPad-5.2; $frameY = $rightY + $frameOuterPad+0.5; $pdf->SetLineWidth(0.3); $pdf->Rect($frameX, $frameY, $frameW, $frameH, 'D'); // der feste Rahmen // ---- Base64-Bild zentriert & verzerrungsfrei in den Rahmen ---- $tmpImg = tempnam(sys_get_temp_dir(), 'epi_') . '.png'; file_put_contents($tmpImg, base64_decode($productImage)); list($imgWpx, $imgHpx) = getimagesize($tmpImg); $imgRatio = ($imgHpx > 0) ? ($imgWpx / $imgHpx) : 1.0; $innerPad = 2.0; // mm: Innenabstand im Rahmen $maxW = $frameW - 2*$innerPad; $maxH = $frameH - 2*$innerPad; // proportional einpassen if ($maxW / $maxH > $imgRatio) { $drawH = $maxH; $drawW = $maxH * $imgRatio; } else { $drawW = $maxW; $drawH = $maxW / $imgRatio; } // zentrieren $imgX = $frameX + ($frameW - $drawW) / 2; $imgY = $frameY + ($frameH - $drawH) / 2; $pdf->Image($tmpImg, $imgX, $imgY, $drawW, $drawH, '', '', '', false, 300); @unlink($tmpImg); $metaWrap = '
'.$leftMeta.' '.$rightMeta.'
'; // Meta-Block exakt an berechneter Position ausgeben $pdf->writeHTMLCell($metaW, 0, $metaX, $metaY, $metaWrap, 0, 0, 0, true, 'L', true); /** Output */ // Falls trotz allem vorher etwas ausgegeben wurde (Warnings), Buffer leeren: if (ob_get_length()) { ob_end_clean(); } // Äußerer Rahmen, Größenberechnung // 1) Höhe der META-Zelle (zuletzt geschrieben) holen: $metaH = $pdf->getLastH(); // 2) Y der Bundle-Tabelle kennst du (50.8 in deinem Code) + deren Höhe: $bundleY = 50.8; // dein fixer Y für die Bundle-Tabelle // $bundleH hast du bereits oben: $bundleH = $pdf->getLastH(); // 3) Bottom-Kanten aller Boxen berechnen: $bottoms = [ $frameY + $frameH, // Logo-Box $addrY + $addrBoxH, // Adress-Box $prodY + $prodH, // Produktion-Box $nameY + $nameBoxH, // Name-Box $pnBoxY + $pnBoxH, // PN/QR-Box $bundleY + $bundleH, // Bundle-Tabelle $metaY + $metaH, // Meta-Block (linke Tabelle + rechter Bildrahmen) ]; // 4) Start-Y des Contentbereichs: oben an deinem Kopf beginnen $outerY = min($frameY, $addrY, $prodY, $nameY, $pnBoxY, 46.0 /*Bundle-Header-Y*/, $metaY); // 5) Untere Kante ist die größte Bottom-Kante: $outerBottom = max($bottoms); // 6) Außenrahmen-Geometrie: $outerX = $MARGIN_LEFT+1.2; $outerW = $pdf->getPageWidth() - $MARGIN_LEFT - $MARGIN_RIGHT+1; $outerH = max(2, $outerBottom - $outerY)-1.2; // Mindeshöhe absichern // 7) Dicke Linie zeichnen (z.B. 1.2 mm) drawFixedBox($pdf, $outerX, $outerY, $outerW, $outerH, 1, '', 0, false); $filename = 'Kistenetikett_'.preg_replace('/\s+/', '_', trim($productName)).'_'.$productNumber.$deviceNumber.'_'.date('Ymd_His').'.pdf'; $pdf->Output($filename, 'I'); /** * Zeichnet eine flexible, leere oder beschriftete Box mit TCPDF. * * @param TCPDF $pdf Referenz auf dein TCPDF-Objekt * @param float $x Linke obere Ecke (mm) * @param float $y Obere Position (mm) * @param float $w Breite (mm) * @param float $h Höhe (mm) * @param float $border Linienstärke in mm (z. B. 0.3) * @param string $text (Optional) Inhalt oder Beschriftung * @param float $pad Innenabstand in mm * @param bool $fill true = grau hinterlegt, false = leer * @param string|null $style (Optional) Linienstil – 'solid', 'dashed', 'dotted' */ function drawFixedBox( TCPDF $pdf, float $x, float $y, float $w, float $h, float $border = 0.3, string $text = '', float $pad = 1.5, bool $fill = false, ?string $style = 'solid' ): void { // Farbe für Füllung (hellgrau, wenn aktiviert) $fillColor = [240, 240, 240]; // Linienstil (falls angegeben) switch ($style) { case 'dashed': $pdf->SetLineStyle(['width' => $border, 'dash' => '3,2']); break; case 'dotted': $pdf->SetLineStyle(['width' => $border, 'dash' => '1,2']); break; default: $pdf->SetLineWidth($border); $pdf->SetLineStyle(['width' => $border]); break; } // 1) Rahmen (und ggf. Füllung) if ($fill) { $pdf->SetFillColorArray($fillColor); $pdf->Rect($x, $y, $w, $h, 'DF'); // Draw + Fill } else { $pdf->Rect($x, $y, $w, $h, 'D'); // nur Rahmen } // 2) Optionaler Textinhalt – sauber eingepasst if (trim($text) !== '') { $pdf->StartTransform(); $pdf->Rect($x, $y, $w, $h, 'CNZ'); // Clip auf Box setzen $pdf->writeHTMLCell( $w - 2*$pad, $h - 2*$pad, $x + $pad, $y + $pad, '
'.htmlspecialchars($text).'
', 0, 0, false, true, 'L', true ); $pdf->StopTransform(); } // Linienstil zurücksetzen $pdf->SetLineStyle(['width' => 0.3, 'dash' => 0]); } /** * Produkt-Details mit Cache holen (vermeidet doppelte API-Calls). * * @param Epirent $Epi * @param int|string $productPk * @param array $cache Referenz-Array für Produkt-Cache * @return object|null */ function fetchProductDetailCached($Epi, $productPk, array &$cache) { $key = (string)$productPk; if (isset($cache[$key])) { return $cache[$key]; } try { $res = $Epi->requestEpiApi('/v1/product/' . $key . '?cl=' . Epirent_Mandant); $payload = json_decode($res, false); $prod = $payload->payload[0] ?? null; if ($prod) { $cache[$key] = $prod; return $prod; } } catch (\Throwable $e) { // optional: loggen } $cache[$key] = null; return null; } /** * Rekursive Hilfsfunktion: fügt Materialien (und deren Submaterialien) zu $bundle hinzu * und summiert alle Bruttogewichte in $bruttoWeightSum. * * @param Epirent $Epi * @param array $materials Materialliste (vom Produkt) * @param int $level aktuelle Ebene (1..MAX_BUNDLE_LEVEL) * @param array &$bundle Ergebnisliste (wird befüllt) * @param array &$cache Produkt-Cache * @param array &$seenStack Schutz gegen Zyklen (Stack von Produkt-Keys) * @param float $qtyMultiplier Multiplikator für Mengen aus übergeordneten Ebenen * @param float &$bruttoWeightSum Summiert alle weight_gro-Werte × Menge */ function addMaterialsRecursive($Epi, array $materials, int $level, array &$bundle, array &$cache, array &$seenStack, float $qtyMultiplier = 1.0): void { global $bruttoWeightSum; $MAX_BUNDLE_LEVEL = 5; if ($level > $MAX_BUNDLE_LEVEL || empty($materials)) { return; } foreach ($materials as $mat) { $isFree = !empty($mat->is_free_material); $amount = isset($mat->amount) ? (float)$mat->amount : 1.0; $effQty = $amount * $qtyMultiplier; // 🟢 1. FREIES MATERIAL → nur Name + Menge, kein weiterer Drilldown if ($isFree) { $name = isset($mat->name) ? (string)$mat->name : '(Unbenanntes freies Material)'; $bundle[] = [ 'qty' => $effQty, 'text' => $name, 'level' => $level, 'weight_kg' => null, ]; continue; // ⬅️ ganz wichtig: hier abbrechen } // 🟡 2. "Normales" Produkt-Material $childPk = $mat->mat_product_pk ?? null; if (!$childPk) { $bundle[] = [ 'qty' => $effQty, 'text' => '(Unbekanntes Material)', 'level' => $level, 'weight_kg' => null, ]; continue; } // Zyklen-Schutz $childKey = (string)$childPk; if (in_array($childKey, $seenStack, true)) { $bundle[] = [ 'qty' => $effQty, 'text' => '[Zyklus erkannt bei Produkt ' . $childKey . ']', 'level' => $level, 'weight_kg' => null, ]; continue; } // Produktdetail abrufen (Cache-basiert) $child = fetchProductDetailCached($Epi, $childPk, $cache); // Basisinfos $childName = $child->name ?? ('Produkt ' . $childKey); $childWeightNet = $child->tech_data->weight_net ?? null; $childWeightGross = $child->tech_data->weight_gro ?? null; $childNumber = $child->product_no ?? null; // 🧮 Bruttogewicht zur globalen Summe addieren if (!empty($childWeightGross)) { $bruttoWeightSum += $effQty * (float)$childWeightGross; } // 📦 Anzeige: Artikelnummer in Klammern voranstellen $displayName = $childNumber ? '(' . $childNumber . ') ' . $childName : $childName; $bundle[] = [ 'qty' => $effQty, 'text' => (string)$displayName, 'level' => $level, 'weight_kg' => $childWeightNet ? (float)$childWeightNet : null, ]; // 🔁 Rekursion für Sub-Materialien if ($child && !empty($child->materials) && $level < $MAX_BUNDLE_LEVEL) { $seenStack[] = $childKey; addMaterialsRecursive($Epi, (array)$child->materials, $level + 1, $bundle, $cache, $seenStack, $effQty); array_pop($seenStack); } } } /** * Einstieg: Baut das Bundle eines Produkts bis Tiefe MAX_BUNDLE_LEVEL * und liefert zusätzlich die Bruttogewichtsumme zurück. * * @param Epirent $Epi * @param object $product * @param float &$bruttoWeightSum Rückgabe der aufsummierten weight_gro * @return array */ function buildBundleForProduct($Epi, $product, float &$bruttoWeightSum = 0.0): array { $bundle = []; $cache = []; $seen = []; $materials = isset($product->materials) ? (array)$product->materials : []; addMaterialsRecursive($Epi, $materials, 1, $bundle, $cache, $seen, 1.0, $bruttoWeightSum); return $bundle; }