diff --git a/dist/ROI.php b/dist/ROI.php
new file mode 100644
index 0000000..6a6854d
--- /dev/null
+++ b/dist/ROI.php
@@ -0,0 +1,932 @@
+getTimestamp();
+}
+
+function tsToYmd(int $ts): string {
+ return (new DateTime('@' . $ts))->setTimezone(new DateTimeZone('Europe/Berlin'))->format('Y-m-d');
+}
+
+function base64Json(array $data): string {
+ return base64_encode(json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
+}
+
+function apiJsonDecode($raw) {
+ // requestEpiApi kann je nach Implementierung string ODER Stream liefern
+ if (is_object($raw)) {
+ // Guzzle Stream etc.
+ if (method_exists($raw, 'getContents')) {
+ $raw = $raw->getContents();
+ } else {
+ $raw = (string)$raw;
+ }
+ }
+ if (!is_string($raw)) $raw = (string)$raw;
+ $obj = json_decode($raw);
+ return $obj;
+}
+
+/* =========================
+ API Caches
+ ========================= */
+
+$productCache = []; // [pk => productObj]
+$bundleLeafCache = []; // [bundlePk => [leafPk => amount]]
+$rentPriceCache = []; // [pk => float]
+
+function getProduct(Epirent $Epi, int $productPk) {
+ global $productCache;
+ if ($productPk <= 0) return null;
+ if (isset($productCache[$productPk])) return $productCache[$productPk];
+
+ $res = apiJsonDecode($Epi->requestEpiApi('/v1/product/' . $productPk . '?cl=' . Epirent_Mandant));
+ $prod = ($res && ($res->success ?? false) && !empty($res->payload[0])) ? $res->payload[0] : null;
+ $productCache[$productPk] = $prod;
+ return $prod;
+}
+
+function getRentPrice(Epirent $Epi, int $productPk): float {
+ global $rentPriceCache;
+ if (isset($rentPriceCache[$productPk])) return $rentPriceCache[$productPk];
+
+ $p = getProduct($Epi, $productPk);
+ $price = 0.0;
+ if ($p && isset($p->pricing) && isset($p->pricing->price_rent)) {
+ $price = (float)$p->pricing->price_rent;
+ }
+ $rentPriceCache[$productPk] = $price;
+ return $price;
+}
+
+/**
+ * Bundle-Komponenten rekursiv auflösen.
+ * Ergebnis: [leafProductPk => amount]
+ *
+ * WICHTIG (Fix für "UJ90 hat (0)"):
+ * - NUR virtuelle Produkte (is_virtual==true) werden expandiert.
+ * - Nicht-virtuelle Produkte zählen IMMER als leaf – auch wenn sie materials haben (Zubehör/Case/etc.)
+ *
+ * - bevorzugt materials_ext.rent_fix (weil rent_fix sauberer ist)
+ * - fallback materials
+ * - Cycle-Guard über $stack
+ */
+function resolveBundleLeafMap(Epirent $Epi, int $productPk, array &$stack = []): array {
+ global $bundleLeafCache;
+
+ if ($productPk <= 0) return [];
+ if (isset($bundleLeafCache[$productPk])) return $bundleLeafCache[$productPk];
+
+ if (isset($stack[$productPk])) {
+ // cycle detected
+ return [];
+ }
+ $stack[$productPk] = true;
+
+ $p = getProduct($Epi, $productPk);
+ if (!$p) {
+ unset($stack[$productPk]);
+ return [];
+ }
+
+ // ✅ FIX: Nur virtuelle Produkte expandieren
+ $isVirtual = (bool)($p->is_virtual ?? false);
+ if (!$isVirtual) {
+ unset($stack[$productPk]);
+ $bundleLeafCache[$productPk] = [$productPk => 1.0];
+ return $bundleLeafCache[$productPk];
+ }
+
+ // Komponentenliste auslesen (nur für virtuelle Produkte)
+ $mats = [];
+
+ // materials_ext.rent_fix bevorzugen
+ if (!empty($p->materials_ext) && !empty($p->materials_ext->rent_fix) && is_array($p->materials_ext->rent_fix)) {
+ $mats = $p->materials_ext->rent_fix;
+ } elseif (!empty($p->materials) && is_array($p->materials)) {
+ $mats = $p->materials;
+ }
+
+ // Wenn keine Komponenten => leaf
+ if (empty($mats)) {
+ unset($stack[$productPk]);
+ $bundleLeafCache[$productPk] = [$productPk => 1.0];
+ return $bundleLeafCache[$productPk];
+ }
+
+ $leaf = [];
+ foreach ($mats as $m) {
+ $childPk = (int)($m->mat_product_pk ?? 0);
+ $amt = (float)($m->amount ?? 1);
+ if ($childPk <= 0 || $amt <= 0) continue;
+
+ // rekursiv
+ $childLeaf = resolveBundleLeafMap($Epi, $childPk, $stack);
+ foreach ($childLeaf as $leafPk => $leafAmt) {
+ if (!isset($leaf[$leafPk])) $leaf[$leafPk] = 0.0;
+ $leaf[$leafPk] += $amt * (float)$leafAmt;
+ }
+ }
+
+ unset($stack[$productPk]);
+
+ // falls aus irgendeinem Grund leer => als leaf behandeln
+ if (empty($leaf)) $leaf = [$productPk => 1.0];
+
+ $bundleLeafCache[$productPk] = $leaf;
+ return $leaf;
+}
+
+/**
+ * Bundle-Umsatz anteilig auf leaf-Produkte verteilen.
+ * Gewichtung: rentPrice(leaf) * amount
+ * Fallback: gleichverteilt nach amount
+ */
+function allocateBundleRevenue(Epirent $Epi, int $bundlePk, float $bundleRevenueNet): array {
+ $leafMap = resolveBundleLeafMap($Epi, $bundlePk);
+ if (empty($leafMap)) return [];
+
+ $weights = [];
+ $sumW = 0.0;
+
+ foreach ($leafMap as $leafPk => $amt) {
+ $rp = getRentPrice($Epi, (int)$leafPk);
+ $w = $rp * (float)$amt;
+ $weights[$leafPk] = $w;
+ $sumW += $w;
+ }
+
+ // fallback: sumW=0 => gleich nach amount
+ if ($sumW <= 0.0) {
+ $sumAmt = array_sum($leafMap);
+ if ($sumAmt <= 0.0) $sumAmt = (float)count($leafMap);
+ $alloc = [];
+ foreach ($leafMap as $leafPk => $amt) {
+ $alloc[$leafPk] = $bundleRevenueNet * ((float)$amt / $sumAmt);
+ }
+ return $alloc;
+ }
+
+ $alloc = [];
+ foreach ($weights as $leafPk => $w) {
+ $alloc[$leafPk] = $bundleRevenueNet * ($w / $sumW);
+ }
+ return $alloc;
+}
+
+/* =========================
+ Journal Cache
+ ========================= */
+
+$journalCache = [];
+function getJournalByChapter(Epirent $Epi, int $chapterId): array {
+ global $journalCache;
+
+ if ($chapterId <= 0) return [];
+ if (isset($journalCache[$chapterId])) return $journalCache[$chapterId];
+
+ $url = '/v1/journal/filter?chid=' . $chapterId . '&cl=' . Epirent_Mandant;
+ $res = apiJsonDecode($Epi->requestEpiApi($url));
+ $payload = ($res && ($res->success ?? false) && isset($res->payload) && is_array($res->payload)) ? $res->payload : [];
+
+ $journalCache[$chapterId] = $payload;
+ return $payload;
+}
+
+/* =========================
+ Interval aggregation (Peak/Histogram)
+ ========================= */
+
+// eventsDirect[productPk][ymd] += deltaQty
+$eventsDirect = [];
+$eventsIncl = []; // direct + bundle
+
+function addIntervalEvent(array &$events, int $productPk, string $startYmd, string $endYmd, float $qty): void {
+ if ($productPk <= 0) return;
+ $startYmd = safeYmd($startYmd);
+ $endYmd = safeYmd($endYmd);
+ if (!$startYmd || !$endYmd) return;
+ if ($qty == 0.0) return;
+
+ $startTs = ymdToTs($startYmd);
+ $endTs = ymdToTs($endYmd);
+ if ($endTs < $startTs) return;
+
+ // end+1 day
+ $endPlusTs = $endTs + 86400;
+ $endPlusYmd = tsToYmd($endPlusTs);
+
+ if (!isset($events[$productPk])) $events[$productPk] = [];
+ if (!isset($events[$productPk][$startYmd])) $events[$productPk][$startYmd] = 0.0;
+ if (!isset($events[$productPk][$endPlusYmd])) $events[$productPk][$endPlusYmd] = 0.0;
+
+ $events[$productPk][$startYmd] += $qty;
+ $events[$productPk][$endPlusYmd] -= $qty;
+}
+
+/**
+ * Aus Events Peak + Histogramm berechnen:
+ * return ['peak'=>int, 'hist'=>[level=>days]]
+ */
+function computePeakAndHistogram(array $eventMap): array {
+ if (empty($eventMap)) return ['peak' => 0, 'hist' => []];
+
+ ksort($eventMap);
+ $dates = array_keys($eventMap);
+
+ $level = 0.0;
+ $peak = 0.0;
+ $hist = []; // [intLevel => floatDays]
+
+ for ($i = 0; $i < count($dates); $i++) {
+ $d = $dates[$i];
+ $level += (float)$eventMap[$d];
+ if ($level < 0) $level = 0;
+
+ $intLevel = (int)round($level);
+ if ($intLevel > $peak) $peak = $intLevel;
+
+ // segment length until next date
+ if ($i < count($dates) - 1) {
+ $dTs = ymdToTs($d);
+ $nTs = ymdToTs($dates[$i + 1]);
+ $days = ($nTs - $dTs) / 86400.0;
+ if ($days > 0 && $intLevel > 0) {
+ if (!isset($hist[$intLevel])) $hist[$intLevel] = 0.0;
+ $hist[$intLevel] += $days;
+ }
+ }
+ }
+
+ ksort($hist);
+ // hist days runden
+ foreach ($hist as $k => $v) $hist[$k] = (int)round($v);
+
+ return ['peak' => (int)$peak, 'hist' => $hist];
+}
+
+/* =========================
+ Data aggregation
+ ========================= */
+
+$rows = []; // Debug rows (HTML only, no excel)
+$pivot = []; // direct revenue per year
+$pivotB = []; // bundle revenue per year (alloc to leaf)
+$meta = []; // product meta [pk=>['product_no'=>..,'title'=>..]]
+
+$allYears = [];
+
+function ensureMeta(Epirent $Epi, int $productPk, int $productNo, string $title): void {
+ global $meta;
+ if (!isset($meta[$productPk])) {
+ $p = getProduct($Epi, $productPk);
+ $meta[$productPk] = [
+ 'product_pk' => $productPk,
+ 'product_no' => $productNo ?: (int)($p->product_no ?? 0),
+ 'title' => $title ?: (string)($p->name ?? ''),
+ ];
+ }
+}
+
+function addRevenue(array &$pivotRef, int $productPk, int $year, float $revenueNet): void {
+ if ($productPk <= 0 || !$year) return;
+ if (!isset($pivotRef[$productPk])) $pivotRef[$productPk] = [];
+ if (!isset($pivotRef[$productPk][$year])) $pivotRef[$productPk][$year] = 0.0;
+ $pivotRef[$productPk][$year] += $revenueNet;
+}
+
+/**
+ * Eine Artikelzeile (aus Journal ODER direkt) verarbeiten
+ */
+function processLineItem(
+ Epirent $Epi,
+ array &$rows,
+ int $invoicePk,
+ string $invoiceDate,
+ int $invoiceYear,
+ ?int $chapterId,
+ object $li,
+ bool $isFromJournal
+): void {
+ global $pivot, $pivotB, $allYears, $eventsDirect, $eventsIncl;
+
+ $type = (int)($li->type ?? -1);
+ $productPk = (int)($li->product_pk ?? 0);
+ if ($type !== 0 || $productPk <= 0) return; // nur echte Artikelpositionen
+
+ $title = (string)($li->title ?? '');
+ $productNo = (int)($li->product_no ?? 0);
+
+ $qty = (float)($li->amount_total ?? 0);
+ if ($qty <= 0) $qty = 0.0;
+
+ // Zeitraum (für Peak/Histogramm)
+ $dateStart = (string)($li->date_start ?? '');
+ $dateEnd = (string)($li->date_end ?? '');
+ $dateStart = safeYmd($dateStart) ?: null;
+ $dateEnd = safeYmd($dateEnd) ?: null;
+
+ // Umsatz netto (für Pivot)
+ $revenueNet = (float)($li->sum_total_net ?? 0);
+
+ ensureMeta($Epi, $productPk, $productNo, $title);
+
+ // Jahr: AN RECHNUNGSDATUM gebunden (dein Wunsch)
+ $year = $invoiceYear;
+ if ($year) $allYears[$year] = true;
+
+ // Direct Umsatz (auch wenn Zeitraum evtl. leer ist)
+ if ($revenueNet != 0.0 && $year) {
+ addRevenue($pivot, $productPk, $year, $revenueNet);
+ }
+
+ // Direct Auslastung (nur wenn Zeitraum da)
+ if ($dateStart && $dateEnd && $qty > 0) {
+ addIntervalEvent($eventsDirect, $productPk, $dateStart, $dateEnd, $qty);
+ addIntervalEvent($eventsIncl, $productPk, $dateStart, $dateEnd, $qty);
+ }
+
+ // Debug Row
+ $rows[] = [
+ 'year' => $year,
+ 'invoice_pk' => $invoicePk,
+ 'invoice_date'=> $invoiceDate,
+ 'chapter_id' => $chapterId ?? 0,
+ 'source' => $isFromJournal ? 'journal' : 'direct',
+ 'product_pk' => $productPk,
+ 'product_no' => $productNo,
+ 'title' => $title,
+ 'date_start' => $dateStart ?? '',
+ 'date_end' => $dateEnd ?? '',
+ 'qty' => $qty,
+ 'revenue_net' => $revenueNet,
+ ];
+
+ /* ===== Bundle-Auflösung =====
+ - Bundle-Erkennung NUR über is_virtual (Fix!)
+ - Auslastung inkl Bundle: Komponenten bekommen qty*amount über Zeitraum
+ - Umsatz inkl Bundle: Umsatz wird proportional auf Leaf-Produkte verteilt
+ */
+ $p = getProduct($Epi, $productPk);
+ $isBundle = (bool)($p->is_virtual ?? false); // ✅ FIX: nicht über materials ableiten
+
+ if ($isBundle) {
+ $leafMap = resolveBundleLeafMap($Epi, $productPk);
+
+ // Umsatz allokieren
+ if ($revenueNet != 0.0 && $year) {
+ $alloc = allocateBundleRevenue($Epi, $productPk, $revenueNet);
+ foreach ($alloc as $leafPk => $leafRevenue) {
+ $leafPk = (int)$leafPk;
+ ensureMeta($Epi, $leafPk, 0, '');
+ addRevenue($pivotB, $leafPk, $year, (float)$leafRevenue);
+ }
+ }
+
+ // Auslastung allokieren
+ if ($dateStart && $dateEnd && $qty > 0) {
+ foreach ($leafMap as $leafPk => $amt) {
+ $leafPk = (int)$leafPk;
+ $amt = (float)$amt;
+ if ($leafPk <= 0 || $amt <= 0) continue;
+ ensureMeta($Epi, $leafPk, 0, '');
+ addIntervalEvent($eventsIncl, $leafPk, $dateStart, $dateEnd, $qty * $amt);
+ }
+ }
+ }
+}
+
+/* =========================
+ 1) Invoices holen
+ ========================= */
+
+$invoiceAll = apiJsonDecode($Epi->requestEpiApi('/v1/invoice/all?ir=true&ib=true&cl=' . Epirent_Mandant));
+$invoiceList = ($invoiceAll && ($invoiceAll->success ?? false) && is_array($invoiceAll->payload ?? null)) ? $invoiceAll->payload : [];
+
+foreach ($invoiceList as $inv) {
+ $invoicePk = (int)($inv->primary_key ?? 0);
+ if ($invoicePk <= 0) continue;
+
+ $invRes = apiJsonDecode($Epi->requestEpiApi('/v1/invoice/' . $invoicePk . '?cl=' . Epirent_Mandant));
+ $invObj = ($invRes && ($invRes->success ?? false) && !empty($invRes->payload[0])) ? $invRes->payload[0] : null;
+ if (!$invObj) continue;
+
+ $invoiceDate = (string)($invObj->invoice_date ?? '');
+ $invoiceYear = yearFromDate($invoiceDate) ?? 0;
+
+ $orderItems = $invObj->order_items ?? [];
+ if (!is_array($orderItems)) $orderItems = [];
+
+ foreach ($orderItems as $oi) {
+ $oiType = (int)($oi->type ?? -1);
+ $oiPk = (int)($oi->primary_key ?? 0);
+
+ // 1) Kapitel-Zeile (type=5): chid ist primary_key der Kapitelzeile!
+ if ($oiType === 5 && $oiPk > 0) {
+ $chapterId = $oiPk;
+ $journalItems = getJournalByChapter($Epi, $chapterId);
+
+ foreach ($journalItems as $ji) {
+ processLineItem($Epi, $rows, $invoicePk, $invoiceDate, $invoiceYear, $chapterId, $ji, true);
+ }
+ continue;
+ }
+
+ // 2) Direkte Artikel-Zeile ohne Kapitel (type=0)
+ if ($oiType === 0 && (int)($oi->product_pk ?? 0) > 0) {
+ processLineItem($Epi, $rows, $invoicePk, $invoiceDate, $invoiceYear, null, $oi, false);
+ continue;
+ }
+
+ // Andere Typen ignorieren (Zwischensumme etc.)
+ }
+}
+
+/* =========================
+ 2) Years + Pivot Rows bauen
+ ========================= */
+
+$years = array_keys($allYears);
+sort($years);
+
+// Pivot Rows: pro Produkt => revenueDirect / revenueIncl und PeakDirect / PeakIncl + Histogramme
+$pivotRows = [];
+
+foreach ($meta as $productPk => $m) {
+ $productPk = (int)$productPk;
+
+ // revenue direct/incl
+ $sumDirect = 0.0;
+ $sumIncl = 0.0;
+
+ $row = [
+ 'product_pk' => $productPk,
+ 'product_no' => (int)($m['product_no'] ?? 0),
+ 'title' => (string)($m['title'] ?? ''),
+ 'years' => [],
+ 'total_direct'=> 0.0,
+ 'total_incl' => 0.0,
+ 'peak_direct' => 0,
+ 'peak_incl' => 0,
+ 'hist_direct' => [],
+ 'hist_incl' => [],
+ ];
+
+ foreach ($years as $y) {
+ $d = (float)($pivot[$productPk][$y] ?? 0.0);
+ $b = (float)($pivotB[$productPk][$y] ?? 0.0);
+ $incl = $d + $b;
+
+ $row['years'][$y] = ['direct'=>$d, 'incl'=>$incl];
+
+ $sumDirect += $d;
+ $sumIncl += $incl;
+ }
+
+ $row['total_direct'] = $sumDirect;
+ $row['total_incl'] = $sumIncl;
+
+ // Peak + Histogram
+ $pd = computePeakAndHistogram($GLOBALS['eventsDirect'][$productPk] ?? []);
+ $pi = computePeakAndHistogram($GLOBALS['eventsIncl'][$productPk] ?? []);
+
+ $row['peak_direct'] = (int)$pd['peak'];
+ $row['peak_incl'] = (int)$pi['peak'];
+ $row['hist_direct'] = $pd['hist'];
+ $row['hist_incl'] = $pi['hist'];
+
+ // Nur Produkte zeigen, die überhaupt irgendwo vorkommen (direct oder incl)
+ if ($row['total_direct'] == 0.0 && $row['total_incl'] == 0.0 && $row['peak_direct'] == 0 && $row['peak_incl'] == 0) {
+ continue;
+ }
+
+ $pivotRows[] = $row;
+}
+
+// Sort: nach Total_incl absteigend (inkl Bundle-Anteil ist meist interessanter)
+usort($pivotRows, fn($a,$b) => ($b['total_incl'] <=> $a['total_incl']));
+
+/* =========================
+ 3) Excel Export (nur Pivot)
+ ========================= */
+
+$exportDir = __DIR__ . '/exports';
+if (!is_dir($exportDir)) {
+ @mkdir($exportDir, 0775, true);
+}
+$timestamp = date('Ymd_His');
+$excelFileName = "ROI_Artikel_Jahre_{$timestamp}.xlsx";
+$excelFilePath = $exportDir . '/' . $excelFileName;
+$excelDownloadUrl = 'exports/' . $excelFileName;
+
+$spreadsheet = new Spreadsheet();
+$sheet = $spreadsheet->getActiveSheet();
+$sheet->setTitle('Pivot');
+
+$cols = ['Produkt-PK','Artikel-Nr','Artikel','Peak','Peak (inkl. Bundle)','Summe','Summe (inkl. Bundle)'];
+foreach ($years as $y) {
+ $cols[] = (string)$y;
+ $cols[] = (string)$y . ' (inkl. Bundle)';
+}
+
+$colCount = count($cols);
+$lastColLetter = Coordinate::stringFromColumnIndex($colCount);
+
+// Header
+$sheet->setCellValue('A1', 'EpiWebview – ROI / Umsatz pro Artikel & Jahr');
+$sheet->mergeCells('A1:' . $lastColLetter . '1');
+$sheet->getStyle('A1:' . $lastColLetter . '1')->getFont()->setBold(true)->setSize(14);
+$sheet->getStyle('A1:' . $lastColLetter . '1')->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
+
+$sheet->setCellValue('A2', 'Exportdatum: ' . date('d.m.Y H:i') . ' | Mandant: ' . (defined('Epirent_Mandant') ? Epirent_Mandant : ''));
+$sheet->mergeCells('A2:' . $lastColLetter . '2');
+$sheet->getStyle('A2:' . $lastColLetter . '2')->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
+
+$headerRow = 4;
+for ($i=0; $i<$colCount; $i++) {
+ $addr = Coordinate::stringFromColumnIndex($i+1) . $headerRow;
+ $sheet->setCellValue($addr, $cols[$i]);
+ $sheet->getStyle($addr)->getFont()->setBold(true);
+ $sheet->getStyle($addr)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
+ $sheet->getColumnDimension(Coordinate::stringFromColumnIndex($i+1))->setAutoSize(true);
+}
+
+$rowIdx = $headerRow + 1;
+foreach ($pivotRows as $r) {
+ $c = 1;
+ $sheet->setCellValueExplicit(Coordinate::stringFromColumnIndex($c++).$rowIdx, (int)$r['product_pk'], DataType::TYPE_NUMERIC);
+ $sheet->setCellValueExplicit(Coordinate::stringFromColumnIndex($c++).$rowIdx, (int)$r['product_no'], DataType::TYPE_NUMERIC);
+ $sheet->setCellValue(Coordinate::stringFromColumnIndex($c++).$rowIdx, (string)$r['title']);
+
+ $sheet->setCellValueExplicit(Coordinate::stringFromColumnIndex($c++).$rowIdx, (int)$r['peak_direct'], DataType::TYPE_NUMERIC);
+ $sheet->setCellValueExplicit(Coordinate::stringFromColumnIndex($c++).$rowIdx, (int)$r['peak_incl'], DataType::TYPE_NUMERIC);
+
+ $sheet->setCellValueExplicit(Coordinate::stringFromColumnIndex($c).$rowIdx, (float)$r['total_direct'], DataType::TYPE_NUMERIC);
+ $sheet->getStyle(Coordinate::stringFromColumnIndex($c).$rowIdx)->getNumberFormat()->setFormatCode('#,##0.00 [$€-de-DE]');
+ $c++;
+
+ $sheet->setCellValueExplicit(Coordinate::stringFromColumnIndex($c).$rowIdx, (float)$r['total_incl'], DataType::TYPE_NUMERIC);
+ $sheet->getStyle(Coordinate::stringFromColumnIndex($c).$rowIdx)->getNumberFormat()->setFormatCode('#,##0.00 [$€-de-DE]');
+ $c++;
+
+ foreach ($years as $y) {
+ $d = (float)$r['years'][$y]['direct'];
+ $iVal = (float)$r['years'][$y]['incl'];
+
+ $sheet->setCellValueExplicit(Coordinate::stringFromColumnIndex($c).$rowIdx, $d, DataType::TYPE_NUMERIC);
+ $sheet->getStyle(Coordinate::stringFromColumnIndex($c).$rowIdx)->getNumberFormat()->setFormatCode('#,##0.00 [$€-de-DE]');
+ $c++;
+
+ $sheet->setCellValueExplicit(Coordinate::stringFromColumnIndex($c).$rowIdx, $iVal, DataType::TYPE_NUMERIC);
+ $sheet->getStyle(Coordinate::stringFromColumnIndex($c).$rowIdx)->getNumberFormat()->setFormatCode('#,##0.00 [$€-de-DE]');
+ $c++;
+ }
+
+ $rowIdx++;
+}
+
+$sheet->setAutoFilter("A{$headerRow}:{$lastColLetter}{$headerRow}");
+$sheet->freezePane("A" . ($headerRow + 1));
+
+// Save
+$writer = new Xlsx($spreadsheet);
+$writer->save($excelFilePath);
+
+/* =========================
+ 4) HTML Ausgabe
+ ========================= */
+?>
+
+
+
+
+
+
+ ROI – Umsatz pro Artikel / Jahr
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
ROI
+
+ - Umsatz / Peak / Histogramm pro Artikel
+
+
+
+
+
+
+
+
+
+ | Produkt-PK |
+ Artikel-Nr |
+ Artikel |
+ Peak |
+ Summe |
+
+ |
+
+ Histogramm |
+
+
+
+
+
+
+ |
+ |
+ |
+
+
+ (' . $peakIncl . ')';
+ ?>
+ |
+
+
+ (' . formatEuro($sumIncl) . ')';
+ ?>
+ |
+
+
+
+
+
+ ()
+ |
+
+
+
+
+
+
+ |
+
+
+
+
+
+ Jahr-Zuordnung Umsatz: invoice_date (Rechnungsdatum). Peak/Histogramm: Zeitraum date_start..date_end.
+ Werte in Klammern = inkl. Bundle-Anteil.
+
+
+
+
+
+
+
+
+
+
+
+
+ | Jahr |
+ Invoice |
+ Rechnungsdatum |
+ Quelle |
+ Chapter |
+ Produkt-PK |
+ Artikel-Nr |
+ Artikel |
+ Start |
+ Ende |
+ Menge |
+ Umsatz netto |
+
+
+
+
+
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+
+
+
+
+
+ Wenn ein Artikel “ohne Kapitel” fehlt, muss er hier als Quelle direct auftauchen.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ X-Achse: gleichzeitig vermietete Stückzahl (1..Peak) | Y-Achse: Tage
+
+
+
+
+
+
+
+
+
+
diff --git a/sources/getSidenav.php b/sources/getSidenav.php
index e48be09..3a8d883 100644
--- a/sources/getSidenav.php
+++ b/sources/getSidenav.php
@@ -27,6 +27,10 @@
Warengruppencheck
+
+
+ Return-Of-Invest
+