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 = '
+ ';*/
+
+$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 = '
+';
+
+$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 = '
+
+
+
+
+
+
+ |
+
+ |
+ '.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;
+}