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;
|
||||
}
|
||||
|
||||
/* =========================
|
||||
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
|
||||
========================= */
|
||||
@@ -318,6 +390,41 @@ $allYears = [];
|
||||
// Auftragsbasierte "Slots" (direct/incl) pro Produkt
|
||||
$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 {
|
||||
global $meta;
|
||||
if (!isset($meta[$productPk])) {
|
||||
@@ -600,7 +707,8 @@ function processLineItem(
|
||||
?int $chapterId,
|
||||
object $li,
|
||||
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 {
|
||||
global $pivot, $pivotB, $pivotExt, $pivotExtB, $allYears, $eventsDirect, $eventsIncl;
|
||||
|
||||
@@ -640,6 +748,11 @@ function processLineItem(
|
||||
// Direct Extern-Umsatz
|
||||
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)
|
||||
// -> qtySigned sorgt dafür, dass Storno zeitgleich aufhebt
|
||||
if ($dateStart && $dateEnd && $qtySigned != 0.0) {
|
||||
@@ -681,9 +794,11 @@ function processLineItem(
|
||||
|
||||
if ($isVirtualBundle) {
|
||||
// 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) {
|
||||
$alloc = allocateBundleRevenue($Epi, $productPk, $revenueNet);
|
||||
foreach ($alloc as $leafPk => $leafRevenue) {
|
||||
foreach ($allocForCustomer as $leafPk => $leafRevenue) {
|
||||
$leafPk = (int)$leafPk;
|
||||
ensureMeta($Epi, $leafPk, 0, '');
|
||||
addRevenue($pivotB, $leafPk, $year, (float)$leafRevenue);
|
||||
@@ -692,14 +807,25 @@ function processLineItem(
|
||||
|
||||
// Extern-Umsatz allokieren (inkl Bundle-Anteil)
|
||||
if ($extRevenueNet != 0.0 && $year) {
|
||||
$allocExt = allocateBundleRevenue($Epi, $productPk, $extRevenueNet);
|
||||
foreach ($allocExt as $leafPk => $leafRevenue) {
|
||||
foreach ($allocExtForCustomer as $leafPk => $leafRevenue) {
|
||||
$leafPk = (int)$leafPk;
|
||||
ensureMeta($Epi, $leafPk, 0, '');
|
||||
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!
|
||||
if ($dateStart && $dateEnd && $qtySigned != 0.0) {
|
||||
foreach ($leafMap as $leafPk => $amt) {
|
||||
@@ -713,16 +839,14 @@ function processLineItem(
|
||||
|
||||
// Slotting allokieren (inkl Bundle) – signed qty!
|
||||
if ($dateStart && $dateEnd && $qtySigned != 0.0) {
|
||||
$alloc = ($revenueNet != 0.0) ? allocateBundleRevenue($Epi, $productPk, $revenueNet) : [];
|
||||
$allocExt = ($extRevenueNet != 0.0) ? allocateBundleRevenue($Epi, $productPk, $extRevenueNet) : [];
|
||||
|
||||
// Bundle-Allokationen aus dem Umsatz-Block wiederverwenden
|
||||
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);
|
||||
$leafRev = (float)($allocForCustomer[$leafPk] ?? 0.0);
|
||||
$leafExt = (float)($allocExtForCustomer[$leafPk] ?? 0.0);
|
||||
|
||||
$qtyLeafSigned = $qtySigned * $amt;
|
||||
|
||||
@@ -783,6 +907,9 @@ foreach ($invoiceList as $inv) {
|
||||
// Storno-Erkennung (Credit Note): sum_net < 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 ?? [];
|
||||
if (!is_array($orderItems)) $orderItems = [];
|
||||
|
||||
@@ -796,14 +923,14 @@ foreach ($invoiceList as $inv) {
|
||||
$journalItems = getJournalByChapter($Epi, $chapterId);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Direkte Artikelposition ohne Kapitel (type=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;
|
||||
}
|
||||
}
|
||||
@@ -870,8 +997,14 @@ foreach ($meta as $productPk => $m) {
|
||||
'slots_incl' => $slotAgg[$productPk]['incl'] ?? [],
|
||||
'slots_direct_ext' => $slotAgg[$productPk]['direct_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;
|
||||
$sumIncl = 0.0;
|
||||
$sumExtDirect = 0.0;
|
||||
@@ -1064,6 +1197,7 @@ function slotsToDisplay(array $slotMap): array {
|
||||
|
||||
<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/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="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/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>
|
||||
|
||||
@@ -1170,6 +1305,7 @@ function slotsToDisplay(array $slotMap): array {
|
||||
<?php endforeach; ?>
|
||||
<th class="nowrap">Histogramm</th>
|
||||
<th class="nowrap">Slots</th>
|
||||
<th class="nowrap">Kunden</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -1199,6 +1335,10 @@ function slotsToDisplay(array $slotMap): array {
|
||||
'incl_ext' => $slotsInclExt,
|
||||
];
|
||||
$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>
|
||||
<td class="mono"><?php echo (int)$r['product_pk']; ?></td>
|
||||
@@ -1274,6 +1414,16 @@ function slotsToDisplay(array $slotMap): array {
|
||||
Slots
|
||||
</button>
|
||||
</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>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
@@ -1416,13 +1566,64 @@ function slotsToDisplay(array $slotMap): array {
|
||||
</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>
|
||||
let histChart = null;
|
||||
let slotsChart = null;
|
||||
|
||||
function decodeB64Json(b64) {
|
||||
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);
|
||||
} catch (e) {
|
||||
return {};
|
||||
@@ -1549,11 +1750,18 @@ function attachTooltips(context) {
|
||||
$(function() {
|
||||
// scrollX: DataTables kümmert sich selbst um horizontales Scrollen der Tabelle,
|
||||
// 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 });
|
||||
|
||||
// Tooltips (z.B. für Namens-Historie) auch nach Pagination/Sortierung erneut anhängen
|
||||
pivotDT.on('draw', () => attachTooltips('#pivotTable'));
|
||||
// Tooltips (z.B. für Namens-Historie) auch nach Pagination/Sortierung erneut anhängen.
|
||||
// Ohne Kontext-Selector werden auch die FixedColumns-Overlay-Tabellen erfasst.
|
||||
pivotDT.on('draw', () => attachTooltips());
|
||||
attachTooltips();
|
||||
|
||||
$(document).on('click', '.js-hist', function() {
|
||||
@@ -1571,7 +1779,58 @@ $(function() {
|
||||
const payload = decodeB64Json(b64);
|
||||
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>
|
||||
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user