Umsatz pro Artikel pro Kund ein ROI, Pivot table angepasst
This commit is contained in:
Vendored
+275
-16
@@ -217,6 +217,78 @@ function allocateBundleRevenue(Epirent $Epi, int $bundlePk, float $bundleRevenue
|
|||||||
return $alloc;
|
return $alloc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
Customer Cache
|
||||||
|
– Zieht den Kunden aus dem Invoice-Objekt, fällt bei Bedarf auf den Order-Endpoint zurück.
|
||||||
|
– Ergebnis pro Order gecached, damit mehrere Rechnungen desselben Auftrags nur 1x API-Kosten haben.
|
||||||
|
========================= */
|
||||||
|
|
||||||
|
$orderCustomerCache = []; // [orderPk => ['pk'=>int, 'name'=>string]]
|
||||||
|
|
||||||
|
function resolveInvoiceCustomer(Epirent $Epi, $invObj, int $orderPk): array {
|
||||||
|
global $orderCustomerCache;
|
||||||
|
|
||||||
|
$pk = 0; $name = '';
|
||||||
|
|
||||||
|
$pickInt = function($obj, array $fields) {
|
||||||
|
foreach ($fields as $f) {
|
||||||
|
$v = (int)($obj->{$f} ?? 0);
|
||||||
|
if ($v > 0) return $v;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
$pickStr = function($obj, array $fields) {
|
||||||
|
foreach ($fields as $f) {
|
||||||
|
$v = trim((string)($obj->{$f} ?? ''));
|
||||||
|
if ($v !== '') return $v;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
if (is_object($invObj)) {
|
||||||
|
$pk = $pickInt($invObj, ['customer_pk','contact_pk','pk_customer','kunde_pk']);
|
||||||
|
$name = $pickStr($invObj, ['customer_name','contact_name','kunde_name','name_customer']);
|
||||||
|
|
||||||
|
foreach (['address_invoice','address_delivery','customer','contact'] as $af) {
|
||||||
|
if ($pk > 0 && $name !== '') break;
|
||||||
|
$sub = $invObj->{$af} ?? null;
|
||||||
|
if (is_object($sub)) {
|
||||||
|
if ($pk <= 0) $pk = $pickInt($sub, ['contact_pk','primary_key','pk','id']);
|
||||||
|
if ($name === '') $name = $pickStr($sub, ['company','name','display_name','name1']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($pk <= 0 || $name === '') && $orderPk > 0) {
|
||||||
|
if (!array_key_exists($orderPk, $orderCustomerCache)) {
|
||||||
|
$res = apiJsonDecode($Epi->requestEpiApi('/v1/order/' . $orderPk . '?cl=' . Epirent_Mandant));
|
||||||
|
$ord = ($res && ($res->success ?? false) && !empty($res->payload[0])) ? $res->payload[0] : null;
|
||||||
|
$c = ['pk' => 0, 'name' => ''];
|
||||||
|
if (is_object($ord)) {
|
||||||
|
$c['pk'] = $pickInt($ord, ['customer_pk','contact_pk','pk_customer']);
|
||||||
|
$c['name'] = $pickStr($ord, ['customer_name','contact_name']);
|
||||||
|
foreach (['address_invoice','address_delivery','customer','contact'] as $af) {
|
||||||
|
if ($c['pk'] > 0 && $c['name'] !== '') break;
|
||||||
|
$sub = $ord->{$af} ?? null;
|
||||||
|
if (is_object($sub)) {
|
||||||
|
if ($c['pk'] <= 0) $c['pk'] = $pickInt($sub, ['contact_pk','primary_key','pk','id']);
|
||||||
|
if ($c['name'] === '') $c['name'] = $pickStr($sub, ['company','name','display_name','name1']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$orderCustomerCache[$orderPk] = $c;
|
||||||
|
}
|
||||||
|
$c = $orderCustomerCache[$orderPk];
|
||||||
|
if ($pk <= 0) $pk = (int)$c['pk'];
|
||||||
|
if ($name === '') $name = (string)$c['name'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($pk <= 0) $pk = 0;
|
||||||
|
if ($name === '') $name = $pk > 0 ? ('Kunde #' . $pk) : 'Unbekannt';
|
||||||
|
|
||||||
|
return ['pk' => $pk, 'name' => $name];
|
||||||
|
}
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
Journal Cache
|
Journal Cache
|
||||||
========================= */
|
========================= */
|
||||||
@@ -318,6 +390,41 @@ $allYears = [];
|
|||||||
// Auftragsbasierte "Slots" (direct/incl) pro Produkt
|
// Auftragsbasierte "Slots" (direct/incl) pro Produkt
|
||||||
$slotAgg = [];
|
$slotAgg = [];
|
||||||
|
|
||||||
|
// Kunden-Aggregation pro Produkt: [productPk][customerPk] = ['pk','name','direct','incl','ext_direct','ext_incl']
|
||||||
|
// Semantik analog zu $pivot / $pivotB:
|
||||||
|
// direct = Umsatz aus Rechnungen die diesen productPk DIREKT abgerechnet haben
|
||||||
|
// incl = direct + Umsatz aus virtuellen Bundles, deren Leaf dieser productPk ist
|
||||||
|
$customerAgg = [];
|
||||||
|
|
||||||
|
function addCustomerRevenue(int $productPk, array $customer, float $direct, float $incl, float $extDirect, float $extIncl): void {
|
||||||
|
global $customerAgg;
|
||||||
|
if ($productPk <= 0) return;
|
||||||
|
if ($direct == 0.0 && $incl == 0.0 && $extDirect == 0.0 && $extIncl == 0.0) return;
|
||||||
|
|
||||||
|
$cpk = (int)($customer['pk'] ?? 0);
|
||||||
|
$name = (string)($customer['name'] ?? '');
|
||||||
|
|
||||||
|
if (!isset($customerAgg[$productPk])) $customerAgg[$productPk] = [];
|
||||||
|
if (!isset($customerAgg[$productPk][$cpk])) {
|
||||||
|
$customerAgg[$productPk][$cpk] = [
|
||||||
|
'pk' => $cpk,
|
||||||
|
'name' => $name,
|
||||||
|
'direct' => 0.0,
|
||||||
|
'incl' => 0.0,
|
||||||
|
'ext_direct' => 0.0,
|
||||||
|
'ext_incl' => 0.0,
|
||||||
|
];
|
||||||
|
} elseif ($customerAgg[$productPk][$cpk]['name'] === '' && $name !== '') {
|
||||||
|
// Nachträglich Namen ergänzen, falls anfangs leer
|
||||||
|
$customerAgg[$productPk][$cpk]['name'] = $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
$customerAgg[$productPk][$cpk]['direct'] += $direct;
|
||||||
|
$customerAgg[$productPk][$cpk]['incl'] += $incl;
|
||||||
|
$customerAgg[$productPk][$cpk]['ext_direct'] += $extDirect;
|
||||||
|
$customerAgg[$productPk][$cpk]['ext_incl'] += $extIncl;
|
||||||
|
}
|
||||||
|
|
||||||
function ensureMeta(Epirent $Epi, int $productPk, int $productNo, string $title, string $invoiceDate = ''): void {
|
function ensureMeta(Epirent $Epi, int $productPk, int $productNo, string $title, string $invoiceDate = ''): void {
|
||||||
global $meta;
|
global $meta;
|
||||||
if (!isset($meta[$productPk])) {
|
if (!isset($meta[$productPk])) {
|
||||||
@@ -600,7 +707,8 @@ function processLineItem(
|
|||||||
?int $chapterId,
|
?int $chapterId,
|
||||||
object $li,
|
object $li,
|
||||||
bool $isFromJournal,
|
bool $isFromJournal,
|
||||||
bool $invoiceIsCredit // <-- wichtig: Storno-Rechnung (z.B. sum_net < 0)
|
bool $invoiceIsCredit, // <-- wichtig: Storno-Rechnung (z.B. sum_net < 0)
|
||||||
|
array $customer = ['pk'=>0, 'name'=>'Unbekannt']
|
||||||
): void {
|
): void {
|
||||||
global $pivot, $pivotB, $pivotExt, $pivotExtB, $allYears, $eventsDirect, $eventsIncl;
|
global $pivot, $pivotB, $pivotExt, $pivotExtB, $allYears, $eventsDirect, $eventsIncl;
|
||||||
|
|
||||||
@@ -640,6 +748,11 @@ function processLineItem(
|
|||||||
// Direct Extern-Umsatz
|
// Direct Extern-Umsatz
|
||||||
if ($extRevenueNet != 0.0 && $year) addRevenue($pivotExt, $productPk, $year, $extRevenueNet);
|
if ($extRevenueNet != 0.0 && $year) addRevenue($pivotExt, $productPk, $year, $extRevenueNet);
|
||||||
|
|
||||||
|
// Kunden-Aggregation für direkten Umsatz (direct wird IMMER auch in incl gezählt)
|
||||||
|
if ($revenueNet != 0.0 || $extRevenueNet != 0.0) {
|
||||||
|
addCustomerRevenue($productPk, $customer, $revenueNet, $revenueNet, $extRevenueNet, $extRevenueNet);
|
||||||
|
}
|
||||||
|
|
||||||
// Peak/Histogram direct + incl (tagesbasiert)
|
// Peak/Histogram direct + incl (tagesbasiert)
|
||||||
// -> qtySigned sorgt dafür, dass Storno zeitgleich aufhebt
|
// -> qtySigned sorgt dafür, dass Storno zeitgleich aufhebt
|
||||||
if ($dateStart && $dateEnd && $qtySigned != 0.0) {
|
if ($dateStart && $dateEnd && $qtySigned != 0.0) {
|
||||||
@@ -681,9 +794,11 @@ function processLineItem(
|
|||||||
|
|
||||||
if ($isVirtualBundle) {
|
if ($isVirtualBundle) {
|
||||||
// Umsatz allokieren (inkl Bundle-Anteil)
|
// Umsatz allokieren (inkl Bundle-Anteil)
|
||||||
|
$allocForCustomer = ($revenueNet != 0.0) ? allocateBundleRevenue($Epi, $productPk, $revenueNet) : [];
|
||||||
|
$allocExtForCustomer = ($extRevenueNet != 0.0) ? allocateBundleRevenue($Epi, $productPk, $extRevenueNet) : [];
|
||||||
|
|
||||||
if ($revenueNet != 0.0 && $year) {
|
if ($revenueNet != 0.0 && $year) {
|
||||||
$alloc = allocateBundleRevenue($Epi, $productPk, $revenueNet);
|
foreach ($allocForCustomer as $leafPk => $leafRevenue) {
|
||||||
foreach ($alloc as $leafPk => $leafRevenue) {
|
|
||||||
$leafPk = (int)$leafPk;
|
$leafPk = (int)$leafPk;
|
||||||
ensureMeta($Epi, $leafPk, 0, '');
|
ensureMeta($Epi, $leafPk, 0, '');
|
||||||
addRevenue($pivotB, $leafPk, $year, (float)$leafRevenue);
|
addRevenue($pivotB, $leafPk, $year, (float)$leafRevenue);
|
||||||
@@ -692,14 +807,25 @@ function processLineItem(
|
|||||||
|
|
||||||
// Extern-Umsatz allokieren (inkl Bundle-Anteil)
|
// Extern-Umsatz allokieren (inkl Bundle-Anteil)
|
||||||
if ($extRevenueNet != 0.0 && $year) {
|
if ($extRevenueNet != 0.0 && $year) {
|
||||||
$allocExt = allocateBundleRevenue($Epi, $productPk, $extRevenueNet);
|
foreach ($allocExtForCustomer as $leafPk => $leafRevenue) {
|
||||||
foreach ($allocExt as $leafPk => $leafRevenue) {
|
|
||||||
$leafPk = (int)$leafPk;
|
$leafPk = (int)$leafPk;
|
||||||
ensureMeta($Epi, $leafPk, 0, '');
|
ensureMeta($Epi, $leafPk, 0, '');
|
||||||
addRevenue($pivotExtB, $leafPk, $year, (float)$leafRevenue);
|
addRevenue($pivotExtB, $leafPk, $year, (float)$leafRevenue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Kunden-Aggregation für Leafs (nur "incl", direct ist 0, da nicht direkt abgerechnet)
|
||||||
|
if (!empty($allocForCustomer) || !empty($allocExtForCustomer)) {
|
||||||
|
$leafKeys = array_unique(array_merge(array_keys($allocForCustomer), array_keys($allocExtForCustomer)));
|
||||||
|
foreach ($leafKeys as $leafPk) {
|
||||||
|
$leafPk = (int)$leafPk;
|
||||||
|
if ($leafPk <= 0) continue;
|
||||||
|
$leafRev = (float)($allocForCustomer[$leafPk] ?? 0.0);
|
||||||
|
$leafExt = (float)($allocExtForCustomer[$leafPk] ?? 0.0);
|
||||||
|
addCustomerRevenue($leafPk, $customer, 0.0, $leafRev, 0.0, $leafExt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Auslastung allokieren (Peak/Histogramm inkl Bundle) – signed qty!
|
// Auslastung allokieren (Peak/Histogramm inkl Bundle) – signed qty!
|
||||||
if ($dateStart && $dateEnd && $qtySigned != 0.0) {
|
if ($dateStart && $dateEnd && $qtySigned != 0.0) {
|
||||||
foreach ($leafMap as $leafPk => $amt) {
|
foreach ($leafMap as $leafPk => $amt) {
|
||||||
@@ -713,16 +839,14 @@ function processLineItem(
|
|||||||
|
|
||||||
// Slotting allokieren (inkl Bundle) – signed qty!
|
// Slotting allokieren (inkl Bundle) – signed qty!
|
||||||
if ($dateStart && $dateEnd && $qtySigned != 0.0) {
|
if ($dateStart && $dateEnd && $qtySigned != 0.0) {
|
||||||
$alloc = ($revenueNet != 0.0) ? allocateBundleRevenue($Epi, $productPk, $revenueNet) : [];
|
// Bundle-Allokationen aus dem Umsatz-Block wiederverwenden
|
||||||
$allocExt = ($extRevenueNet != 0.0) ? allocateBundleRevenue($Epi, $productPk, $extRevenueNet) : [];
|
|
||||||
|
|
||||||
foreach ($leafMap as $leafPk => $amt) {
|
foreach ($leafMap as $leafPk => $amt) {
|
||||||
$leafPk = (int)$leafPk;
|
$leafPk = (int)$leafPk;
|
||||||
$amt = (float)$amt;
|
$amt = (float)$amt;
|
||||||
if ($leafPk <= 0 || $amt <= 0) continue;
|
if ($leafPk <= 0 || $amt <= 0) continue;
|
||||||
|
|
||||||
$leafRev = (float)($alloc[$leafPk] ?? 0.0);
|
$leafRev = (float)($allocForCustomer[$leafPk] ?? 0.0);
|
||||||
$leafExt = (float)($allocExt[$leafPk] ?? 0.0);
|
$leafExt = (float)($allocExtForCustomer[$leafPk] ?? 0.0);
|
||||||
|
|
||||||
$qtyLeafSigned = $qtySigned * $amt;
|
$qtyLeafSigned = $qtySigned * $amt;
|
||||||
|
|
||||||
@@ -783,6 +907,9 @@ foreach ($invoiceList as $inv) {
|
|||||||
// Storno-Erkennung (Credit Note): sum_net < 0
|
// Storno-Erkennung (Credit Note): sum_net < 0
|
||||||
$invoiceIsCredit = ((float)($invObj->sum_net ?? 0.0)) < 0.0;
|
$invoiceIsCredit = ((float)($invObj->sum_net ?? 0.0)) < 0.0;
|
||||||
|
|
||||||
|
// Kunde einmal pro Rechnung ermitteln (mit Order-Fallback + Cache)
|
||||||
|
$customer = resolveInvoiceCustomer($Epi, $invObj, $orderPk);
|
||||||
|
|
||||||
$orderItems = $invObj->order_items ?? [];
|
$orderItems = $invObj->order_items ?? [];
|
||||||
if (!is_array($orderItems)) $orderItems = [];
|
if (!is_array($orderItems)) $orderItems = [];
|
||||||
|
|
||||||
@@ -796,14 +923,14 @@ foreach ($invoiceList as $inv) {
|
|||||||
$journalItems = getJournalByChapter($Epi, $chapterId);
|
$journalItems = getJournalByChapter($Epi, $chapterId);
|
||||||
|
|
||||||
foreach ($journalItems as $ji) {
|
foreach ($journalItems as $ji) {
|
||||||
processLineItem($Epi, $rows, $slotItemsDirect, $slotItemsIncl, $orderPk, $invoicePk, $invoiceNo, $invoiceDate, $invoiceYear, $chapterId, $ji, true, $invoiceIsCredit);
|
processLineItem($Epi, $rows, $slotItemsDirect, $slotItemsIncl, $orderPk, $invoicePk, $invoiceNo, $invoiceDate, $invoiceYear, $chapterId, $ji, true, $invoiceIsCredit, $customer);
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Direkte Artikelposition ohne Kapitel (type=0)
|
// Direkte Artikelposition ohne Kapitel (type=0)
|
||||||
if ($oiType === 0 && (int)($oi->product_pk ?? 0) > 0) {
|
if ($oiType === 0 && (int)($oi->product_pk ?? 0) > 0) {
|
||||||
processLineItem($Epi, $rows, $slotItemsDirect, $slotItemsIncl, $orderPk, $invoicePk, $invoiceNo, $invoiceDate, $invoiceYear, null, $oi, false, $invoiceIsCredit);
|
processLineItem($Epi, $rows, $slotItemsDirect, $slotItemsIncl, $orderPk, $invoicePk, $invoiceNo, $invoiceDate, $invoiceYear, null, $oi, false, $invoiceIsCredit, $customer);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -870,8 +997,14 @@ foreach ($meta as $productPk => $m) {
|
|||||||
'slots_incl' => $slotAgg[$productPk]['incl'] ?? [],
|
'slots_incl' => $slotAgg[$productPk]['incl'] ?? [],
|
||||||
'slots_direct_ext' => $slotAgg[$productPk]['direct_ext'] ?? [],
|
'slots_direct_ext' => $slotAgg[$productPk]['direct_ext'] ?? [],
|
||||||
'slots_incl_ext' => $slotAgg[$productPk]['incl_ext'] ?? [],
|
'slots_incl_ext' => $slotAgg[$productPk]['incl_ext'] ?? [],
|
||||||
|
'customers' => [],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Kundenliste absteigend nach "incl" sortieren – größte Kunden zuerst
|
||||||
|
$custList = array_values($customerAgg[$productPk] ?? []);
|
||||||
|
usort($custList, fn($a,$b) => ($b['incl'] <=> $a['incl']));
|
||||||
|
$row['customers'] = $custList;
|
||||||
|
|
||||||
$sumDirect = 0.0;
|
$sumDirect = 0.0;
|
||||||
$sumIncl = 0.0;
|
$sumIncl = 0.0;
|
||||||
$sumExtDirect = 0.0;
|
$sumExtDirect = 0.0;
|
||||||
@@ -1064,6 +1197,7 @@ function slotsToDisplay(array $slotMap): array {
|
|||||||
|
|
||||||
<link href="css/styles.css" rel="stylesheet" />
|
<link href="css/styles.css" rel="stylesheet" />
|
||||||
<link href="https://cdn.datatables.net/1.10.20/css/dataTables.bootstrap4.min.css" rel="stylesheet" crossorigin="anonymous" />
|
<link href="https://cdn.datatables.net/1.10.20/css/dataTables.bootstrap4.min.css" rel="stylesheet" crossorigin="anonymous" />
|
||||||
|
<link href="https://cdn.datatables.net/fixedcolumns/3.3.3/css/fixedColumns.bootstrap4.min.css" rel="stylesheet" crossorigin="anonymous" />
|
||||||
|
|
||||||
<script src="js/jquery-3.5.1.min.js"></script>
|
<script src="js/jquery-3.5.1.min.js"></script>
|
||||||
<script src="https://kit.fontawesome.com/93d71de8bc.js" crossorigin="anonymous"></script>
|
<script src="https://kit.fontawesome.com/93d71de8bc.js" crossorigin="anonymous"></script>
|
||||||
@@ -1072,6 +1206,7 @@ function slotsToDisplay(array $slotMap): array {
|
|||||||
|
|
||||||
<script src="https://cdn.datatables.net/1.10.20/js/jquery.dataTables.min.js" crossorigin="anonymous"></script>
|
<script src="https://cdn.datatables.net/1.10.20/js/jquery.dataTables.min.js" crossorigin="anonymous"></script>
|
||||||
<script src="https://cdn.datatables.net/1.10.20/js/dataTables.bootstrap4.min.js" crossorigin="anonymous"></script>
|
<script src="https://cdn.datatables.net/1.10.20/js/dataTables.bootstrap4.min.js" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://cdn.datatables.net/fixedcolumns/3.3.3/js/dataTables.fixedColumns.min.js" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js" crossorigin="anonymous"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
@@ -1170,6 +1305,7 @@ function slotsToDisplay(array $slotMap): array {
|
|||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<th class="nowrap">Histogramm</th>
|
<th class="nowrap">Histogramm</th>
|
||||||
<th class="nowrap">Slots</th>
|
<th class="nowrap">Slots</th>
|
||||||
|
<th class="nowrap">Kunden</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -1199,6 +1335,10 @@ function slotsToDisplay(array $slotMap): array {
|
|||||||
'incl_ext' => $slotsInclExt,
|
'incl_ext' => $slotsInclExt,
|
||||||
];
|
];
|
||||||
$slotsB64 = base64Json($slotsPayload);
|
$slotsB64 = base64Json($slotsPayload);
|
||||||
|
|
||||||
|
// Kunden-Payload für Modal (bereits nach 'incl' desc sortiert)
|
||||||
|
$customersB64 = base64Json($r['customers'] ?? []);
|
||||||
|
$customersCount = is_array($r['customers'] ?? null) ? count($r['customers']) : 0;
|
||||||
?>
|
?>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="mono"><?php echo (int)$r['product_pk']; ?></td>
|
<td class="mono"><?php echo (int)$r['product_pk']; ?></td>
|
||||||
@@ -1274,6 +1414,16 @@ function slotsToDisplay(array $slotMap): array {
|
|||||||
Slots
|
Slots
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
<td class="nowrap">
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-info btn-xs js-customers"
|
||||||
|
data-title="<?php echo htmlspecialchars((string)$r['title']); ?>"
|
||||||
|
data-customers="<?php echo htmlspecialchars($customersB64); ?>"
|
||||||
|
<?php if ($customersCount === 0) echo 'disabled'; ?>>
|
||||||
|
Kunden <?php if ($customersCount > 0) echo '(' . (int)$customersCount . ')'; ?>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -1416,13 +1566,64 @@ function slotsToDisplay(array $slotMap): array {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Customers Modal -->
|
||||||
|
<div class="modal fade" id="customersModal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-xl" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Kunden-Umsatz</h5>
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-2 text-muted">
|
||||||
|
Umsatz pro Kunde für diesen Artikel, gefiltert auf den oben gewählten Zeitraum.
|
||||||
|
<b>Umsatz</b> = direkt auf diesem Artikel abgerechnet.
|
||||||
|
<b>inkl. Bundle</b> = zusätzlich der Anteil, wenn der Artikel Bestandteil eines virtuellen Bundles war.
|
||||||
|
</div>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm table-bordered" id="customersTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="nowrap">#</th>
|
||||||
|
<th class="nowrap">Kunden-PK</th>
|
||||||
|
<th>Kunde</th>
|
||||||
|
<th class="nowrap text-right">Umsatz</th>
|
||||||
|
<th class="nowrap text-right">Umsatz inkl. Bundle</th>
|
||||||
|
<th class="nowrap text-right">davon extern</th>
|
||||||
|
<th class="nowrap text-right">davon extern inkl. Bundle</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr class="font-weight-bold" style="background:#f8f9fa;">
|
||||||
|
<td colspan="3" class="text-right">Summe:</td>
|
||||||
|
<td class="nowrap text-right" id="cust_sum_direct"></td>
|
||||||
|
<td class="nowrap text-right" id="cust_sum_incl"></td>
|
||||||
|
<td class="nowrap text-right" id="cust_sum_ext_direct"></td>
|
||||||
|
<td class="nowrap text-right" id="cust_sum_ext_incl"></td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
let histChart = null;
|
let histChart = null;
|
||||||
let slotsChart = null;
|
let slotsChart = null;
|
||||||
|
|
||||||
function decodeB64Json(b64) {
|
function decodeB64Json(b64) {
|
||||||
try {
|
try {
|
||||||
const json = atob(b64);
|
// atob() liefert Bytes als Latin-1-String zurück. Wir müssen die Rohbytes
|
||||||
|
// extrahieren und explizit als UTF-8 dekodieren, sonst wird "ü" zu "ü".
|
||||||
|
const binary = atob(b64);
|
||||||
|
const bytes = new Uint8Array(binary.length);
|
||||||
|
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
||||||
|
const json = new TextDecoder('utf-8').decode(bytes);
|
||||||
return JSON.parse(json);
|
return JSON.parse(json);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return {};
|
return {};
|
||||||
@@ -1549,11 +1750,18 @@ function attachTooltips(context) {
|
|||||||
$(function() {
|
$(function() {
|
||||||
// scrollX: DataTables kümmert sich selbst um horizontales Scrollen der Tabelle,
|
// scrollX: DataTables kümmert sich selbst um horizontales Scrollen der Tabelle,
|
||||||
// damit Search/Length/Pagination NICHT mit dem Scrollbalken mitwandern.
|
// damit Search/Length/Pagination NICHT mit dem Scrollbalken mitwandern.
|
||||||
const pivotDT = $('#pivotTable').DataTable({ pageLength: 50, order: [[4,'desc']], scrollX: true });
|
// fixedColumns: linke Spalten bis inkl. "Summe" bleiben beim horizontalen Scrollen stehen.
|
||||||
|
const pivotDT = $('#pivotTable').DataTable({
|
||||||
|
pageLength: 50,
|
||||||
|
order: [[4,'desc']],
|
||||||
|
scrollX: true,
|
||||||
|
fixedColumns: { leftColumns: 5 }
|
||||||
|
});
|
||||||
const detailDT = $('#detailTable').DataTable({ pageLength: 50, order: [[1,'desc']], scrollX: true });
|
const detailDT = $('#detailTable').DataTable({ pageLength: 50, order: [[1,'desc']], scrollX: true });
|
||||||
|
|
||||||
// Tooltips (z.B. für Namens-Historie) auch nach Pagination/Sortierung erneut anhängen
|
// Tooltips (z.B. für Namens-Historie) auch nach Pagination/Sortierung erneut anhängen.
|
||||||
pivotDT.on('draw', () => attachTooltips('#pivotTable'));
|
// Ohne Kontext-Selector werden auch die FixedColumns-Overlay-Tabellen erfasst.
|
||||||
|
pivotDT.on('draw', () => attachTooltips());
|
||||||
attachTooltips();
|
attachTooltips();
|
||||||
|
|
||||||
$(document).on('click', '.js-hist', function() {
|
$(document).on('click', '.js-hist', function() {
|
||||||
@@ -1571,7 +1779,58 @@ $(function() {
|
|||||||
const payload = decodeB64Json(b64);
|
const payload = decodeB64Json(b64);
|
||||||
openSlots(title, payload);
|
openSlots(title, payload);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '.js-customers', function() {
|
||||||
|
const title = $(this).data('title') || '';
|
||||||
|
const b64 = $(this).data('customers') || '';
|
||||||
|
const payload = decodeB64Json(b64);
|
||||||
|
openCustomers(title, payload);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function openCustomers(title, rows) {
|
||||||
|
$('#customersModal .modal-title').text(title + ' – Umsatz pro Kunde');
|
||||||
|
|
||||||
|
const list = Array.isArray(rows) ? rows.slice() : [];
|
||||||
|
list.sort((a, b) => Number(b.incl || 0) - Number(a.incl || 0));
|
||||||
|
|
||||||
|
let sumD = 0, sumI = 0, sumED = 0, sumEI = 0;
|
||||||
|
const $tb = $('#customersTable tbody').empty();
|
||||||
|
|
||||||
|
if (list.length === 0) {
|
||||||
|
$tb.append('<tr><td colspan="7" class="text-muted text-center">Keine Kunden-Daten vorhanden.</td></tr>');
|
||||||
|
} else {
|
||||||
|
list.forEach((c, idx) => {
|
||||||
|
const d = Number(c.direct || 0);
|
||||||
|
const i = Number(c.incl || 0);
|
||||||
|
const ed = Number(c.ext_direct || 0);
|
||||||
|
const ei = Number(c.ext_incl || 0);
|
||||||
|
sumD += d; sumI += i; sumED += ed; sumEI += ei;
|
||||||
|
|
||||||
|
const pk = c.pk ? String(c.pk) : '';
|
||||||
|
const name = c.name ? String(c.name) : (pk ? ('Kunde #' + pk) : 'Unbekannt');
|
||||||
|
|
||||||
|
$tb.append(`
|
||||||
|
<tr>
|
||||||
|
<td class="mono">${idx + 1}</td>
|
||||||
|
<td class="mono">${pk}</td>
|
||||||
|
<td>${$('<div>').text(name).html()}</td>
|
||||||
|
<td class="nowrap text-right">${fmtEuro(d)}</td>
|
||||||
|
<td class="nowrap text-right">${fmtEuro(i)}</td>
|
||||||
|
<td class="nowrap text-right">${fmtEuro(ed)}</td>
|
||||||
|
<td class="nowrap text-right">${fmtEuro(ei)}</td>
|
||||||
|
</tr>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#cust_sum_direct').text(fmtEuro(sumD));
|
||||||
|
$('#cust_sum_incl').text(fmtEuro(sumI));
|
||||||
|
$('#cust_sum_ext_direct').text(fmtEuro(sumED));
|
||||||
|
$('#cust_sum_ext_incl').text(fmtEuro(sumEI));
|
||||||
|
|
||||||
|
$('#customersModal').modal('show');
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user