diff --git a/dist/ROI.php b/dist/ROI.php
index 6a6854d..679873b 100644
--- a/dist/ROI.php
+++ b/dist/ROI.php
@@ -1,5 +1,6 @@
getTimestamp();
+ return (new DateTime($ymd . ' 00:00:00', new DateTimeZone('Europe/Berlin')))->getTimestamp();
}
function tsToYmd(int $ts): string {
return (new DateTime('@' . $ts))->setTimezone(new DateTimeZone('Europe/Berlin'))->format('Y-m-d');
}
-function base64Json(array $data): string {
+function base64Json($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 {
@@ -60,8 +57,7 @@ function apiJsonDecode($raw) {
}
}
if (!is_string($raw)) $raw = (string)$raw;
- $obj = json_decode($raw);
- return $obj;
+ return json_decode($raw);
}
/* =========================
@@ -97,15 +93,12 @@ function getRentPrice(Epirent $Epi, int $productPk): float {
}
/**
- * Bundle-Komponenten rekursiv auflösen.
+ * WICHTIG: Bundle-Auflösung NUR für virtuelle Bundles.
+ * Damit ist die anteilige Bundlepreis-Berechnung wieder wie in der funktionierenden Version.
+ *
* 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
+ * - nur wenn is_virtual == true UND rent_fix/materials vorhanden
+ * - sonst: leaf = [self=>1]
* - Cycle-Guard über $stack
*/
function resolveBundleLeafMap(Epirent $Epi, int $productPk, array &$stack = []): array {
@@ -115,7 +108,6 @@ function resolveBundleLeafMap(Epirent $Epi, int $productPk, array &$stack = []):
if (isset($bundleLeafCache[$productPk])) return $bundleLeafCache[$productPk];
if (isset($stack[$productPk])) {
- // cycle detected
return [];
}
$stack[$productPk] = true;
@@ -126,7 +118,7 @@ function resolveBundleLeafMap(Epirent $Epi, int $productPk, array &$stack = []):
return [];
}
- // ✅ FIX: Nur virtuelle Produkte expandieren
+ // NUR virtuelle Bundles zerlegen
$isVirtual = (bool)($p->is_virtual ?? false);
if (!$isVirtual) {
unset($stack[$productPk]);
@@ -134,17 +126,14 @@ function resolveBundleLeafMap(Epirent $Epi, int $productPk, array &$stack = []):
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
+ // virtuell aber ohne Komponenten => leaf
if (empty($mats)) {
unset($stack[$productPk]);
$bundleLeafCache[$productPk] = [$productPk => 1.0];
@@ -157,7 +146,6 @@ function resolveBundleLeafMap(Epirent $Epi, int $productPk, array &$stack = []):
$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;
@@ -167,7 +155,6 @@ function resolveBundleLeafMap(Epirent $Epi, int $productPk, array &$stack = []):
unset($stack[$productPk]);
- // falls aus irgendeinem Grund leer => als leaf behandeln
if (empty($leaf)) $leaf = [$productPk => 1.0];
$bundleLeafCache[$productPk] = $leaf;
@@ -183,6 +170,11 @@ function allocateBundleRevenue(Epirent $Epi, int $bundlePk, float $bundleRevenue
$leafMap = resolveBundleLeafMap($Epi, $bundlePk);
if (empty($leafMap)) return [];
+ // wenn NICHT virtuell (leafMap == self) => dann keine Allocation (weil kein Bundle)
+ if (count($leafMap) === 1 && isset($leafMap[$bundlePk])) {
+ return [];
+ }
+
$weights = [];
$sumW = 0.0;
@@ -193,7 +185,6 @@ function allocateBundleRevenue(Epirent $Epi, int $bundlePk, float $bundleRevenue
$sumW += $w;
}
- // fallback: sumW=0 => gleich nach amount
if ($sumW <= 0.0) {
$sumAmt = array_sum($leafMap);
if ($sumAmt <= 0.0) $sumAmt = (float)count($leafMap);
@@ -231,7 +222,7 @@ function getJournalByChapter(Epirent $Epi, int $chapterId): array {
}
/* =========================
- Interval aggregation (Peak/Histogram)
+ Interval aggregation (Peak/Histogram) – tagesbasiert (wie gehabt)
========================= */
// eventsDirect[productPk][ymd] += deltaQty
@@ -249,7 +240,6 @@ function addIntervalEvent(array &$events, int $productPk, string $startYmd, stri
$endTs = ymdToTs($endYmd);
if ($endTs < $startTs) return;
- // end+1 day
$endPlusTs = $endTs + 86400;
$endPlusYmd = tsToYmd($endPlusTs);
@@ -261,10 +251,6 @@ function addIntervalEvent(array &$events, int $productPk, string $startYmd, stri
$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' => []];
@@ -273,7 +259,7 @@ function computePeakAndHistogram(array $eventMap): array {
$level = 0.0;
$peak = 0.0;
- $hist = []; // [intLevel => floatDays]
+ $hist = [];
for ($i = 0; $i < count($dates); $i++) {
$d = $dates[$i];
@@ -283,7 +269,6 @@ function computePeakAndHistogram(array $eventMap): array {
$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]);
@@ -296,7 +281,6 @@ function computePeakAndHistogram(array $eventMap): array {
}
ksort($hist);
- // hist days runden
foreach ($hist as $k => $v) $hist[$k] = (int)round($v);
return ['peak' => (int)$peak, 'hist' => $hist];
@@ -306,13 +290,22 @@ function computePeakAndHistogram(array $eventMap): array {
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'=>..]]
+$rows = []; // Debug rows (HTML only)
+$pivot = []; // direct revenue per year (net)
+$pivotB = []; // bundle revenue per year allocated to leaf (net)
+$pivotExt = []; // extern revenue per year (net) direct
+$pivotExtB = []; // extern revenue per year allocated (net) for virtual bundles
+$meta = []; // product meta [pk=>['product_no'=>..,'title'=>..]]
$allYears = [];
+// Auftragsbasierte "Slots" (direct/incl) pro Produkt
+// slotAgg[productPk]['direct'][slot] += revenue
+// slotAgg[productPk]['incl'][slot] += revenue
+// slotAgg[productPk]['direct_ext'][slot] += extRevenue
+// slotAgg[productPk]['incl_ext'][slot] += extRevenue
+$slotAgg = [];
+
function ensureMeta(Epirent $Epi, int $productPk, int $productNo, string $title): void {
global $meta;
if (!isset($meta[$productPk])) {
@@ -333,19 +326,143 @@ function addRevenue(array &$pivotRef, int $productPk, int $year, float $revenueN
}
/**
- * Eine Artikelzeile (aus Journal ODER direkt) verarbeiten
+ * Extern-Logik:
+ * - Amount External ist eine ANZAHL (nicht Umsatz).
+ * - Wir berechnen extRevenue = sum_total_net * (amount_external / amount_total) (wenn amount_total>0)
+ * - fallback: 0
*/
+function calcExternalRevenueNet(object $li): float {
+ $amountTotal = (float)($li->amount_total ?? 0);
+ $amountExternal = (float)($li->amount_external ?? 0);
+ $net = (float)($li->sum_total_net ?? 0);
+
+ if ($net == 0.0) return 0.0;
+ if ($amountTotal <= 0.0) return 0.0;
+ if ($amountExternal <= 0.0) return 0.0;
+
+ if ($amountExternal > $amountTotal) $amountExternal = $amountTotal;
+
+ return $net * ($amountExternal / $amountTotal);
+}
+
+/* =========================
+ Slot (auftragsbasiert) – Intervall Coloring
+ ========================= */
+
+/**
+ * Intervall-Item für Slotting (ein "Auftragsteil" pro Produkt)
+ */
+function addSlotItem(array &$slotItems, int $productPk, int $invoicePk, int $orderNo, string $startYmd, string $endYmd, float $qty, float $revenueNet, float $extRevenueNet): void {
+ $startYmd = safeYmd($startYmd);
+ $endYmd = safeYmd($endYmd);
+ if ($productPk <= 0) return;
+ if (!$startYmd || !$endYmd) return;
+ if ($qty <= 0) return;
+
+ $slotItems[$productPk][] = [
+ 'invoice_pk' => $invoicePk,
+ 'order_no' => $orderNo,
+ 'start_ts' => ymdToTs($startYmd),
+ 'end_ts' => ymdToTs($endYmd),
+ 'qty' => (float)$qty,
+ 'rev' => (float)$revenueNet,
+ 'rev_ext' => (float)$extRevenueNet,
+ 'start_ymd' => $startYmd,
+ 'end_ymd' => $endYmd,
+ ];
+}
+
+/**
+ * Slotting-Regel:
+ * - Aufträge werden nach start_ts sortiert,
+ * - bei gleichem start: zuerst der der später endet bekommt den "zweiten" Slot (also: end_ts DESC),
+ * - wenn dann noch gleich: nach order_no (oder invoice_pk) ASC.
+ * - Belegung: für qty=2 werden 2 Slots gesucht; wenn Slots frei werden (end < start), wiederverwendbar.
+ *
+ * Umsatzverteilung:
+ * - pro Auftrag & Produkt wird der UMSATZ auf die belegten Slots gleichmäßig verteilt (rev/qty)
+ * - analog für extern-umsatz.
+ */
+function computeOrderBasedSlots(array $itemsForProduct): array {
+ if (empty($itemsForProduct)) return [
+ 'direct' => [],
+ 'direct_ext' => []
+ ];
+
+ usort($itemsForProduct, function($a, $b){
+ if ($a['start_ts'] !== $b['start_ts']) return $a['start_ts'] <=> $b['start_ts'];
+ if ($a['end_ts'] !== $b['end_ts']) return $b['end_ts'] <=> $a['end_ts']; // später endend zuerst
+ if ($a['order_no'] !== $b['order_no']) return $a['order_no'] <=> $b['order_no'];
+ return $a['invoice_pk'] <=> $b['invoice_pk'];
+ });
+
+ $slotEnd = []; // slotIndex => end_ts
+ $slotRevenue = []; // slotIndex => revenue
+ $slotRevenueExt = []; // slotIndex => revenueExt
+
+ foreach ($itemsForProduct as $it) {
+ $qty = (int)round($it['qty']);
+ if ($qty <= 0) continue;
+
+ $revPer = ($it['rev'] ?? 0.0) / $qty;
+ $extPer = ($it['rev_ext'] ?? 0.0) / $qty;
+
+ // finde freie Slots / neue Slots
+ $assigned = [];
+ for ($k=0; $k<$qty; $k++) {
+ $slot = null;
+
+ // freier Slot: end < start
+ foreach ($slotEnd as $idx => $endTs) {
+ if ($endTs < $it['start_ts']) { // streng <, damit gleicher Tag als parallel zählt
+ $slot = (int)$idx;
+ break;
+ }
+ }
+ if ($slot === null) {
+ $slot = count($slotEnd) + 1; // Slots sind 1-based
+ }
+
+ // reservieren
+ $slotEnd[$slot] = $it['end_ts'];
+ $assigned[] = $slot;
+
+ // Revenue sammeln
+ if (!isset($slotRevenue[$slot])) $slotRevenue[$slot] = 0.0;
+ if (!isset($slotRevenueExt[$slot])) $slotRevenueExt[$slot] = 0.0;
+ $slotRevenue[$slot] += $revPer;
+ $slotRevenueExt[$slot] += $extPer;
+ }
+ }
+
+ ksort($slotRevenue);
+ ksort($slotRevenueExt);
+
+ // runden (2 Nachkommastellen für Anzeige später, intern float ok)
+ return [
+ 'direct' => $slotRevenue,
+ 'direct_ext' => $slotRevenueExt
+ ];
+}
+
+/* =========================
+ Prozessierung einer Artikelzeile
+ ========================= */
+
function processLineItem(
Epirent $Epi,
array &$rows,
+ array &$slotItemsDirect,
+ array &$slotItemsIncl,
int $invoicePk,
+ int $invoiceNo,
string $invoiceDate,
int $invoiceYear,
?int $chapterId,
object $li,
bool $isFromJournal
): void {
- global $pivot, $pivotB, $allYears, $eventsDirect, $eventsIncl;
+ global $pivot, $pivotB, $pivotExt, $pivotExtB, $allYears, $eventsDirect, $eventsIncl;
$type = (int)($li->type ?? -1);
$productPk = (int)($li->product_pk ?? 0);
@@ -357,36 +474,41 @@ function processLineItem(
$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;
+ $dateStart = safeYmd((string)($li->date_start ?? '')) ?: null;
+ $dateEnd = safeYmd((string)($li->date_end ?? '')) ?: null;
- // Umsatz netto (für Pivot)
$revenueNet = (float)($li->sum_total_net ?? 0);
+ $extRevenueNet = calcExternalRevenueNet($li);
ensureMeta($Epi, $productPk, $productNo, $title);
- // Jahr: AN RECHNUNGSDATUM gebunden (dein Wunsch)
+ // Jahr: Rechnungsdatum
$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 Umsatz
+ if ($revenueNet != 0.0 && $year) addRevenue($pivot, $productPk, $year, $revenueNet);
- // Direct Auslastung (nur wenn Zeitraum da)
+ // Direct Extern-Umsatz
+ if ($extRevenueNet != 0.0 && $year) addRevenue($pivotExt, $productPk, $year, $extRevenueNet);
+
+ // Peak/Histogram direct + incl (tagesbasiert)
if ($dateStart && $dateEnd && $qty > 0) {
addIntervalEvent($eventsDirect, $productPk, $dateStart, $dateEnd, $qty);
addIntervalEvent($eventsIncl, $productPk, $dateStart, $dateEnd, $qty);
}
+ // Slotting: direct
+ if ($dateStart && $dateEnd && $qty > 0) {
+ addSlotItem($slotItemsDirect, $productPk, $invoicePk, $invoiceNo, $dateStart, $dateEnd, $qty, $revenueNet, $extRevenueNet);
+ addSlotItem($slotItemsIncl, $productPk, $invoicePk, $invoiceNo, $dateStart, $dateEnd, $qty, $revenueNet, $extRevenueNet);
+ }
+
// Debug Row
$rows[] = [
'year' => $year,
'invoice_pk' => $invoicePk,
+ 'invoice_no' => $invoiceNo,
'invoice_date'=> $invoiceDate,
'chapter_id' => $chapterId ?? 0,
'source' => $isFromJournal ? 'journal' : 'direct',
@@ -397,20 +519,17 @@ function processLineItem(
'date_end' => $dateEnd ?? '',
'qty' => $qty,
'revenue_net' => $revenueNet,
+ 'ext_rev_net' => $extRevenueNet,
+ 'amount_ext' => (float)($li->amount_external ?? 0),
+ 'amount_total'=> (float)($li->amount_total ?? 0),
];
- /* ===== 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
+ /* ===== Bundle-Auflösung (nur virtuelle Bundles!) ===== */
+ $leafMap = resolveBundleLeafMap($Epi, $productPk);
+ $isVirtualBundle = !(count($leafMap) === 1 && isset($leafMap[$productPk]) && (float)$leafMap[$productPk] === 1.0);
- if ($isBundle) {
- $leafMap = resolveBundleLeafMap($Epi, $productPk);
-
- // Umsatz allokieren
+ if ($isVirtualBundle) {
+ // Umsatz allokieren (inkl Bundle-Anteil)
if ($revenueNet != 0.0 && $year) {
$alloc = allocateBundleRevenue($Epi, $productPk, $revenueNet);
foreach ($alloc as $leafPk => $leafRevenue) {
@@ -420,7 +539,17 @@ function processLineItem(
}
}
- // Auslastung allokieren
+ // Extern-Umsatz allokieren (inkl Bundle-Anteil)
+ if ($extRevenueNet != 0.0 && $year) {
+ $allocExt = allocateBundleRevenue($Epi, $productPk, $extRevenueNet);
+ foreach ($allocExt as $leafPk => $leafRevenue) {
+ $leafPk = (int)$leafPk;
+ ensureMeta($Epi, $leafPk, 0, '');
+ addRevenue($pivotExtB, $leafPk, $year, (float)$leafRevenue);
+ }
+ }
+
+ // Auslastung allokieren (Peak/Histogramm inkl Bundle)
if ($dateStart && $dateEnd && $qty > 0) {
foreach ($leafMap as $leafPk => $amt) {
$leafPk = (int)$leafPk;
@@ -430,13 +559,38 @@ function processLineItem(
addIntervalEvent($eventsIncl, $leafPk, $dateStart, $dateEnd, $qty * $amt);
}
}
+
+ // Slotting allokieren (inkl Bundle): wir erzeugen SlotItems für Leaf-Produkte
+ if ($dateStart && $dateEnd && $qty > 0) {
+ // Revenue pro "Bundle-Item" wird in allocateBundleRevenue bereits verteilt.
+ $alloc = ($revenueNet != 0.0) ? allocateBundleRevenue($Epi, $productPk, $revenueNet) : [];
+ $allocExt = ($extRevenueNet != 0.0) ? allocateBundleRevenue($Epi, $productPk, $extRevenueNet) : [];
+
+ foreach ($leafMap as $leafPk => $amt) {
+ $leafPk = (int)$leafPk;
+ $amt = (float)$amt;
+ if ($leafPk <= 0 || $amt <= 0) continue;
+
+ $leafRev = (float)($alloc[$leafPk] ?? 0.0);
+ $leafExt = (float)($allocExt[$leafPk] ?? 0.0);
+
+ // qty_leaf = qty * amt
+ $qtyLeaf = $qty * $amt;
+
+ ensureMeta($Epi, $leafPk, 0, '');
+ addSlotItem($slotItemsIncl, $leafPk, $invoicePk, $invoiceNo, $dateStart, $dateEnd, $qtyLeaf, $leafRev, $leafExt);
+ }
+ }
}
}
/* =========================
- 1) Invoices holen
+ 1) Invoices holen + Daten sammeln
========================= */
+$slotItemsDirect = []; // productPk => list of intervals
+$slotItemsIncl = []; // productPk => list of intervals
+
$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 : [];
@@ -450,6 +604,7 @@ foreach ($invoiceList as $inv) {
$invoiceDate = (string)($invObj->invoice_date ?? '');
$invoiceYear = yearFromDate($invoiceDate) ?? 0;
+ $invoiceNo = (int)($invObj->invoice_no ?? 0);
$orderItems = $invObj->order_items ?? [];
if (!is_array($orderItems)) $orderItems = [];
@@ -458,93 +613,128 @@ foreach ($invoiceList as $inv) {
$oiType = (int)($oi->type ?? -1);
$oiPk = (int)($oi->primary_key ?? 0);
- // 1) Kapitel-Zeile (type=5): chid ist primary_key der Kapitelzeile!
+ // Kapitel (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);
+ processLineItem($Epi, $rows, $slotItemsDirect, $slotItemsIncl, $invoicePk, $invoiceNo, $invoiceDate, $invoiceYear, $chapterId, $ji, true);
}
continue;
}
- // 2) Direkte Artikel-Zeile ohne Kapitel (type=0)
+ // Direkte Artikelposition ohne Kapitel (type=0)
if ($oiType === 0 && (int)($oi->product_pk ?? 0) > 0) {
- processLineItem($Epi, $rows, $invoicePk, $invoiceDate, $invoiceYear, null, $oi, false);
+ processLineItem($Epi, $rows, $slotItemsDirect, $slotItemsIncl, $invoicePk, $invoiceNo, $invoiceDate, $invoiceYear, null, $oi, false);
continue;
}
-
- // Andere Typen ignorieren (Zwischensumme etc.)
}
}
/* =========================
- 2) Years + Pivot Rows bauen
+ 2) Slot Aggregation (auftragsbasiert)
+ ========================= */
+
+foreach ($meta as $productPk => $m) {
+ $productPk = (int)$productPk;
+
+ $resD = computeOrderBasedSlots($slotItemsDirect[$productPk] ?? []);
+ $resI = computeOrderBasedSlots($slotItemsIncl[$productPk] ?? []);
+
+ $slotAgg[$productPk] = [
+ 'direct' => $resD['direct'] ?? [],
+ 'incl' => $resI['direct'] ?? [],
+ 'direct_ext' => $resD['direct_ext'] ?? [],
+ 'incl_ext' => $resI['direct_ext'] ?? [],
+ ];
+}
+
+/* =========================
+ 3) 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' => [],
+ 'years_ext' => [],
'total_direct'=> 0.0,
'total_incl' => 0.0,
+ 'total_ext_direct'=> 0.0,
+ 'total_ext_incl' => 0.0,
'peak_direct' => 0,
'peak_incl' => 0,
'hist_direct' => [],
'hist_incl' => [],
+ 'slots_direct' => $slotAgg[$productPk]['direct'] ?? [],
+ 'slots_incl' => $slotAgg[$productPk]['incl'] ?? [],
+ 'slots_direct_ext' => $slotAgg[$productPk]['direct_ext'] ?? [],
+ 'slots_incl_ext' => $slotAgg[$productPk]['incl_ext'] ?? [],
];
+ $sumDirect = 0.0;
+ $sumIncl = 0.0;
+ $sumExtDirect = 0.0;
+ $sumExtIncl = 0.0;
+
foreach ($years as $y) {
- $d = (float)($pivot[$productPk][$y] ?? 0.0);
- $b = (float)($pivotB[$productPk][$y] ?? 0.0);
+ $d = (float)($pivot[$productPk][$y] ?? 0.0);
+ $b = (float)($pivotB[$productPk][$y] ?? 0.0);
$incl = $d + $b;
+ $de = (float)($pivotExt[$productPk][$y] ?? 0.0);
+ $be = (float)($pivotExtB[$productPk][$y] ?? 0.0);
+ $inclE = $de + $be;
+
$row['years'][$y] = ['direct'=>$d, 'incl'=>$incl];
+ $row['years_ext'][$y] = ['direct'=>$de, 'incl'=>$inclE];
$sumDirect += $d;
$sumIncl += $incl;
+ $sumExtDirect += $de;
+ $sumExtIncl += $inclE;
}
$row['total_direct'] = $sumDirect;
$row['total_incl'] = $sumIncl;
+ $row['total_ext_direct'] = $sumExtDirect;
+ $row['total_ext_incl'] = $sumExtIncl;
- // 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;
- }
+ // nur anzeigen, wenn was los ist
+ $hasAny = (
+ $row['total_direct'] != 0.0 ||
+ $row['total_incl'] != 0.0 ||
+ $row['total_ext_direct'] != 0.0 ||
+ $row['total_ext_incl'] != 0.0 ||
+ $row['peak_direct'] != 0 ||
+ $row['peak_incl'] != 0
+ );
+ if (!$hasAny) 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)
+ 4) Excel Export (nur Pivot)
========================= */
$exportDir = __DIR__ . '/exports';
@@ -560,10 +750,18 @@ $spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->setTitle('Pivot');
-$cols = ['Produkt-PK','Artikel-Nr','Artikel','Peak','Peak (inkl. Bundle)','Summe','Summe (inkl. Bundle)'];
+$cols = [
+ 'Produkt-PK','Artikel-Nr','Artikel',
+ 'Peak','Peak (inkl. Bundle)',
+ 'Summe','Summe (inkl. Bundle)',
+ 'davon extern','davon extern (inkl. Bundle)'
+];
+
foreach ($years as $y) {
$cols[] = (string)$y;
$cols[] = (string)$y . ' (inkl. Bundle)';
+ $cols[] = (string)$y . ' ext';
+ $cols[] = (string)$y . ' ext (inkl. Bundle)';
}
$colCount = count($cols);
@@ -606,10 +804,21 @@ foreach ($pivotRows as $r) {
$sheet->getStyle(Coordinate::stringFromColumnIndex($c).$rowIdx)->getNumberFormat()->setFormatCode('#,##0.00 [$€-de-DE]');
$c++;
+ $sheet->setCellValueExplicit(Coordinate::stringFromColumnIndex($c).$rowIdx, (float)$r['total_ext_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_ext_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'];
+ $de = (float)$r['years_ext'][$y]['direct'];
+ $ie = (float)$r['years_ext'][$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++;
@@ -617,6 +826,14 @@ foreach ($pivotRows as $r) {
$sheet->setCellValueExplicit(Coordinate::stringFromColumnIndex($c).$rowIdx, $iVal, DataType::TYPE_NUMERIC);
$sheet->getStyle(Coordinate::stringFromColumnIndex($c).$rowIdx)->getNumberFormat()->setFormatCode('#,##0.00 [$€-de-DE]');
$c++;
+
+ $sheet->setCellValueExplicit(Coordinate::stringFromColumnIndex($c).$rowIdx, $de, DataType::TYPE_NUMERIC);
+ $sheet->getStyle(Coordinate::stringFromColumnIndex($c).$rowIdx)->getNumberFormat()->setFormatCode('#,##0.00 [$€-de-DE]');
+ $c++;
+
+ $sheet->setCellValueExplicit(Coordinate::stringFromColumnIndex($c).$rowIdx, $ie, DataType::TYPE_NUMERIC);
+ $sheet->getStyle(Coordinate::stringFromColumnIndex($c).$rowIdx)->getNumberFormat()->setFormatCode('#,##0.00 [$€-de-DE]');
+ $c++;
}
$rowIdx++;
@@ -625,13 +842,25 @@ foreach ($pivotRows as $r) {
$sheet->setAutoFilter("A{$headerRow}:{$lastColLetter}{$headerRow}");
$sheet->freezePane("A" . ($headerRow + 1));
-// Save
$writer = new Xlsx($spreadsheet);
$writer->save($excelFilePath);
/* =========================
- 4) HTML Ausgabe
+ 5) HTML Ausgabe
========================= */
+
+function slotsToDisplay(array $slotMap): array {
+ // slotMap: [slot=>revenue]
+ ksort($slotMap);
+ $out = [];
+ foreach ($slotMap as $slot => $rev) {
+ $slot = (int)$slot;
+ if ($slot <= 0) continue;
+ $out[] = ['slot'=>$slot, 'rev'=>(float)$rev];
+ }
+ return $out;
+}
+
?>
@@ -647,14 +876,11 @@ $writer->save($excelFilePath);
-
-
-