From dbdf081738b4f87e75b47b6263fbc14fb6f59d1a Mon Sep 17 00:00:00 2001 From: Leopold Strobl Date: Tue, 24 Mar 2026 17:53:11 +0100 Subject: [PATCH] =?UTF-8?q?Kleine=20Labels=20f=C3=BCr=20kleine=20koffer=20?= =?UTF-8?q?zb=20WAG=20Beat=20hinzugef=C3=BCgt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dist/labelprint.php | 21 + sources/getProductLabelSmall.php | 795 +++++++++++++++++++++++++++++++ 2 files changed, 816 insertions(+) create mode 100644 sources/getProductLabelSmall.php diff --git a/dist/labelprint.php b/dist/labelprint.php index a75538e..76adf19 100644 --- a/dist/labelprint.php +++ b/dist/labelprint.php @@ -102,6 +102,11 @@ $productList = json_decode($Epi->requestEpiApi('/v1/product/all?ia=true&ir=true& href='../sources/getProductLabel.php?id=" . urlencode($product->primary_key) . "'> Export + + Export small + requestEpiApi('/v1/product/all?ia=true&ir=true& \"> Export mit ID + + primary_key) . "').value; + if(nr) { + this.href = '../sources/getProductLabelSmall.php?id=" . urlencode($product->primary_key) . "&nr=' + encodeURIComponent(nr); + } else { + alert('Bitte eine Nummer zwischen 1 und 9999 eingeben.'); + return false; + } + \"> + Export mit ID small + "; echo ""; } diff --git a/sources/getProductLabelSmall.php b/sources/getProductLabelSmall.php new file mode 100644 index 0000000..5fee71a --- /dev/null +++ b/sources/getProductLabelSmall.php @@ -0,0 +1,795 @@ +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 small'.$productName); +$pdf->SetSubject('Kistenetikett small'); + +$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 = 20.0; // Gesamtbreite des Rahmens +$frameH = 9.8; // 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 + + + + +// Feste Box rechts neben dem Logo (direkt bündig) +$addrBoxW = 0.0; // feste Breite (mm) +$addrBoxH = 0.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 + + + + +$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 = 27; +// Höhe: fix (anpassen nach Wunsch) +$prodH = 10.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 = 67.1; + +// Höhe & Padding der Box (anpassen nach Wunsch) +$nameBoxH = 8; // mm - feste Höhe +$namePad = 0.4; // 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 = 20.0; // mm – Gesamtbreite der Box +$pnBoxH = 10.0; // mm – Gesamthöhe der Box +$pnBoxPad = 0.2; // mm – Innenabstand innerhalb der Box + +// Position der Box (aktuell am rechten Rand oben; passe frei an) +$pnBoxX = 56.5; // bündig am rechten Rand +$pnBoxY = $startY+1.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.9; // mm von linker Innenkante +$pnTextOffsetY = 2; // mm von oberer Innenkante +$pnTextW = 0; // 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 = 7.8; // mm – Kantenlänge des QR +$qrOffsetX = 11; // mm von linker Innenkante +$qrOffsetY = 1.2; // 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(40, 50, 7.65, 26, $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, 46.0, 27.5, 30.5, 3.45); + + +$bundleRows = ''; + +$BASE_FONT_PX = 7.0; // Level 1 +$FONT_STEP_PX = 1; // je Ebene kleiner +$MIN_FONT_PX = 3.0; +$LINE_HEIGHT_BASE = 0.35; // Basis Zeilenhöhe +$LINE_HEIGHT_STEP = 0.05; // je Ebene etwas kompakter +$ROW_PAD_V_MM = 0.15; // vertikales Padding je Zeile +$ROW_PAD_H_MM = 0.0; // horizontales Padding + +$WEIGHT_PAD_RIGHT_MM = 1.6; // mehr Abstand zum rechten Rand +$INDENT_MM_PER_LVL = 1.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.'
';*/ + +$indentMm = max(0, ($lvl - 1) * $INDENT_MM_PER_LVL); +$indentSpaces = str_repeat(' ', max(0, ($lvl - 1) * 4)); + +$nameCellHtml = ' + '.$indentSpaces.$text.''; + + //Ende ersetzter Teil + +} else { + // Level 1: direkt rendern + $nameCellHtml = ''.$text.''; +} + + + $bundleRows .= ' + + + + '.$qty.' + + + + + '.$nameCellHtml.' + + + + '; +} + +$bundleHtml = ' + + '.$bundleRows.' +
'; + +$pdf->writeHTMLCell( + 73.1, + 0, + 7.8, + 29.3, + $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 = 26.2+ $bundleH; // 50 = dein bisheriges Y der Bundle-Tabelle +$metaX = $MARGIN_LEFT; + + +// 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 = ' + + + + + + + + + + + +
+ + + + +
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 = 111.2; +// $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 ---- + +$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 = 33.7; // 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 = 67.5; +$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; + } + // Nach "position" aufsteigend sortieren (pro Level) + usort($materials, function ($a, $b) { + $posA = isset($a->position) ? (int)$a->position : PHP_INT_MAX; + $posB = isset($b->position) ? (int)$b->position : PHP_INT_MAX; + return $posA <=> $posB; + }); + + 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; +}