ROI: Neuester Artikelname als Hauptname, ältere Namen als Tooltip

Bei gleicher Artikelnummer (product_pk) kann sich der Name im Laufe der
Zeit ändern. Bisher wurde der Name der ERSTEN gefundenen Rechnungszeile
festgehalten – jetzt wird pro Produkt eine Titel-Historie mit dem letzten
zugehörigen invoice_date gesammelt. In der Pivot-Tabelle erscheint der
neueste Name als Hauptname; frühere Namen werden über ein i-Icon neben
dem Namen als Hover-Tooltip angezeigt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-03 11:07:51 +02:00
co-authored by Claude Opus 4.7
parent 9818f2f0db
commit 6fd76baf90
+57 -6
View File
@@ -318,7 +318,7 @@ $allYears = [];
// Auftragsbasierte "Slots" (direct/incl) pro Produkt // Auftragsbasierte "Slots" (direct/incl) pro Produkt
$slotAgg = []; $slotAgg = [];
function ensureMeta(Epirent $Epi, int $productPk, int $productNo, string $title): 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])) {
$p = getProduct($Epi, $productPk); $p = getProduct($Epi, $productPk);
@@ -326,8 +326,19 @@ function ensureMeta(Epirent $Epi, int $productPk, int $productNo, string $title)
'product_pk' => $productPk, 'product_pk' => $productPk,
'product_no' => $productNo ?: (int)($p->product_no ?? 0), 'product_no' => $productNo ?: (int)($p->product_no ?? 0),
'title' => $title ?: (string)($p->name ?? ''), 'title' => $title ?: (string)($p->name ?? ''),
// titles: [title => letztes invoice_date, an dem dieser Titel auftauchte]
'titles' => [],
]; ];
} }
// Titel-Historie: nur Titel erfassen, die tatsächlich auf einer Rechnung standen,
// damit Umbenennungen (gleiche Artikelnummer, neuer Name) sichtbar bleiben.
if ($title !== '' && $invoiceDate !== '') {
$prev = $meta[$productPk]['titles'][$title] ?? '';
if ($invoiceDate > $prev) {
$meta[$productPk]['titles'][$title] = $invoiceDate;
}
}
} }
function addRevenue(array &$pivotRef, int $productPk, int $year, float $revenueNet): void { function addRevenue(array &$pivotRef, int $productPk, int $year, float $revenueNet): void {
@@ -617,7 +628,7 @@ function processLineItem(
$dateStart = safeYmd((string)($li->date_start ?? '')) ?: null; $dateStart = safeYmd((string)($li->date_start ?? '')) ?: null;
$dateEnd = safeYmd((string)($li->date_end ?? '')) ?: null; $dateEnd = safeYmd((string)($li->date_end ?? '')) ?: null;
ensureMeta($Epi, $productPk, $productNo, $title); ensureMeta($Epi, $productPk, $productNo, $title, $invoiceDate);
// Jahr: Rechnungsdatum // Jahr: Rechnungsdatum
$year = $invoiceYear; $year = $invoiceYear;
@@ -828,10 +839,23 @@ $pivotRows = [];
foreach ($meta as $productPk => $m) { foreach ($meta as $productPk => $m) {
$productPk = (int)$productPk; $productPk = (int)$productPk;
// Titel-Historie auswerten: neuester Titel als Hauptname, restliche als "früher"-Liste
$titlesMap = $m['titles'] ?? [];
if (!empty($titlesMap)) {
arsort($titlesMap); // nach letztem invoice_date absteigend
$orderedTitles = array_keys($titlesMap);
$newestTitle = (string)$orderedTitles[0];
$olderTitles = array_slice($orderedTitles, 1);
} else {
$newestTitle = (string)($m['title'] ?? '');
$olderTitles = [];
}
$row = [ $row = [
'product_pk' => $productPk, 'product_pk' => $productPk,
'product_no' => (int)($m['product_no'] ?? 0), 'product_no' => (int)($m['product_no'] ?? 0),
'title' => (string)($m['title'] ?? ''), 'title' => $newestTitle,
'older_titles' => $olderTitles,
'years' => [], 'years' => [],
'years_ext' => [], 'years_ext' => [],
'total_direct'=> 0.0, 'total_direct'=> 0.0,
@@ -1179,7 +1203,22 @@ function slotsToDisplay(array $slotMap): array {
<tr> <tr>
<td class="mono"><?php echo (int)$r['product_pk']; ?></td> <td class="mono"><?php echo (int)$r['product_pk']; ?></td>
<td class="mono"><?php echo (int)$r['product_no']; ?></td> <td class="mono"><?php echo (int)$r['product_no']; ?></td>
<td><?php echo htmlspecialchars((string)$r['title']); ?></td> <td>
<?php echo htmlspecialchars((string)$r['title']); ?>
<?php if (!empty($r['older_titles'])):
$ttHtml = 'Frühere Namen:<br>&bull; ' .
implode('<br>&bull; ', array_map(
fn($t) => htmlspecialchars((string)$t, ENT_QUOTES),
$r['older_titles']
));
?>
<i class="fas fa-info-circle text-muted ml-1 js-name-history"
style="cursor: help;"
data-toggle="tooltip"
data-html="true"
title="<?php echo htmlspecialchars($ttHtml, ENT_QUOTES); ?>"></i>
<?php endif; ?>
</td>
<td class="nowrap"> <td class="nowrap">
<?php echo $peakDirect; ?> <?php echo $peakDirect; ?>
@@ -1499,11 +1538,23 @@ function openSlots(title, payload) {
$('#slotsModal').modal('show'); $('#slotsModal').modal('show');
} }
function attachTooltips(context) {
$(context || document).find('[data-toggle="tooltip"]').each(function() {
if (!$(this).data('bs.tooltip')) {
$(this).tooltip({ container: 'body', html: true, boundary: 'window' });
}
});
}
$(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.
$('#pivotTable').DataTable({ pageLength: 50, order: [[4,'desc']], scrollX: true }); const pivotDT = $('#pivotTable').DataTable({ pageLength: 50, order: [[4,'desc']], scrollX: true });
$('#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
pivotDT.on('draw', () => attachTooltips('#pivotTable'));
attachTooltips();
$(document).on('click', '.js-hist', function() { $(document).on('click', '.js-hist', function() {
const title = $(this).data('title') || ''; const title = $(this).data('title') || '';