- Preisvergleich zwischen Lieferanten mit prozentualem Unterschied - Günstigster-Lieferant-Badge (grünes Kürzel) in Kundenaufträgen - Preis-Indikator (Grün/Orange/Rot) in Lieferantenbestellungen - Lagerbestand-Anzeige: Physisch | Theoretisch (Dolibarr-Standard) - Tooltip mit HTML Entity für Zeilenumbrüche korrigiert - Verwendet Dolibarrs native stock_theorique Berechnung Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
212 lines
10 KiB
JavaScript
Executable file
212 lines
10 KiB
JavaScript
Executable file
/**
|
|
* SupplierLink3 - Replenish page enhancements
|
|
* Ersetzt die "Aktueller Lagerbestand"-Spalte mit Badge + Shop-Link
|
|
* Version 2.3: Lagerbestand (Physisch|Theoretisch) wie Dolibarr Standard
|
|
*/
|
|
console.log('SL3: replenish.js loaded');
|
|
|
|
$(document).ready(function() {
|
|
console.log('SL3: DOM ready, sl3Data exists:', typeof window.sl3Data !== 'undefined');
|
|
|
|
if (typeof window.sl3Data === 'undefined') {
|
|
console.log('SL3: No data found, exiting');
|
|
return;
|
|
}
|
|
|
|
var productSuppliers = window.sl3Data;
|
|
var shopIcon = window.sl3ShopIcon || 'fas fa-store';
|
|
console.log('SL3: Processing', Object.keys(productSuppliers).length, 'products');
|
|
|
|
// CSS einfügen
|
|
$('head').append('<style>' +
|
|
'.badge.badge-danger { background-color: #dc3545 !important; color: #fff !important; }' +
|
|
'.badge.badge-warning { background-color: #fd7e14 !important; color: #fff !important; }' +
|
|
'.badge.badge-secondary { background-color: #6c757d !important; color: #fff !important; }' +
|
|
'.badge.badge-success { background-color: #28a745 !important; color: #fff !important; }' +
|
|
'.sl3-stock-wrapper { display: inline-flex; align-items: center; justify-content: flex-end; }' +
|
|
'.sl3-shop-col { display: inline-block; width: 28px; text-align: center; }' +
|
|
'.sl3-supplier-badge { display: inline-block; background-color: #28a745; color: #fff; font-size: 9px; padding: 2px 4px; border-radius: 3px; margin-left: 3px; }' +
|
|
'.sl3-badge-col { display: inline-block; min-width: 70px; text-align: right; }' +
|
|
'.sl3-modal-item { padding: 10px; border-bottom: 1px solid #eee; }' +
|
|
'.sl3-modal-item:hover { background-color: #f5f5f5; }' +
|
|
'.sl3-modal-item:last-child { border-bottom: none; }' +
|
|
'.sl3-modal-item.cheapest { background-color: #d4edda; }' +
|
|
'</style>');
|
|
|
|
// Modal-Handler mit Preisvergleich
|
|
$(document).on('click', '.sl3-modal-trigger', function(e) {
|
|
e.preventDefault();
|
|
var suppliers = $(this).data('suppliers');
|
|
if (!suppliers || suppliers.length === 0) return;
|
|
|
|
var minPrice = suppliers[0].price;
|
|
var content = '';
|
|
|
|
for (var i = 0; i < suppliers.length; i++) {
|
|
var sup = suppliers[i];
|
|
var isFirst = (i === 0);
|
|
var bgClass = isFirst ? 'cheapest' : '';
|
|
var star = isFirst ? '<i class="fas fa-star" style="color: gold; margin-right: 5px;"></i>' : '';
|
|
|
|
// Prozentuale Differenz zum günstigsten
|
|
var percentDiff = 0;
|
|
var percentHtml = '';
|
|
if (minPrice > 0 && sup.price > minPrice) {
|
|
percentDiff = ((sup.price - minPrice) / minPrice * 100).toFixed(1);
|
|
var diffColor = percentDiff > 20 ? '#dc3545' : (percentDiff > 10 ? '#fd7e14' : '#6c757d');
|
|
percentHtml = '<span style="font-size: 10px; color: ' + diffColor + '; font-weight: bold;">+' + percentDiff + '%</span>';
|
|
}
|
|
|
|
content += '<div class="sl3-modal-item ' + bgClass + '">';
|
|
content += '<div style="font-weight: ' + (isFirst ? 'bold' : 'normal') + '; margin-bottom: 5px; display: flex; justify-content: space-between; align-items: center;">';
|
|
content += '<span>' + star + sup.supplier_name + '</span>';
|
|
content += percentHtml;
|
|
content += '</div>';
|
|
content += '<div style="font-size: 12px; color: #666; margin-bottom: 8px;">';
|
|
content += '<strong style="font-size: 13px;">' + parseFloat(sup.price).toFixed(2).replace('.', ',') + ' EUR</strong>';
|
|
content += ' · ab ' + sup.min_qty + ' St.';
|
|
content += ' · Art.-Nr: ' + sup.ref_fourn;
|
|
content += '</div>';
|
|
if (sup.full_url) {
|
|
content += '<a href="' + sup.full_url + '" target="supplier_' + sup.supplier_id + '" class="button small">Im Shop öffnen</a>';
|
|
}
|
|
content += '</div>';
|
|
}
|
|
|
|
$('<div>').html(content).dialog({
|
|
modal: true,
|
|
title: 'Lieferanten-Vergleich',
|
|
width: 420,
|
|
buttons: { 'Schließen': function() { $(this).dialog('close'); } }
|
|
});
|
|
});
|
|
|
|
// Spalten-Indizes aus der Header-Zeile ermitteln
|
|
var colIndex = { stock: -1, desired: -1, alert: -1 };
|
|
$('table.liste tr.liste_titre th, table.liste tr.liste_titre td').each(function(idx) {
|
|
var text = $(this).text().trim().toLowerCase();
|
|
// Stock-Spalte (Aktueller Lagerbestand / Physical stock / Physischer Bestand)
|
|
if (text.indexOf('lagerbestand') >= 0 || text.indexOf('physical stock') >= 0 ||
|
|
text.indexOf('stock physique') >= 0 || text.indexOf('physischer bestand') >= 0) {
|
|
colIndex.stock = idx;
|
|
}
|
|
// Desired Stock (Gewünschter Lagerbestand / Wunschbestand)
|
|
if (text.indexOf('gewünscht') >= 0 || text.indexOf('wunsch') >= 0 ||
|
|
text.indexOf('desired') >= 0 || text.indexOf('souhaité') >= 0) {
|
|
colIndex.desired = idx;
|
|
}
|
|
// Alert Stock (Grenzwert für Warnung / Mindestbestand / Stock limit)
|
|
if (text.indexOf('grenzwert') >= 0 || text.indexOf('warnung') >= 0 ||
|
|
text.indexOf('mindest') >= 0 || text.indexOf('limit') >= 0 ||
|
|
text.indexOf('alerte') >= 0 || text.indexOf('alert') >= 0) {
|
|
colIndex.alert = idx;
|
|
}
|
|
});
|
|
|
|
console.log('SL3: Column indices found:', colIndex);
|
|
|
|
// Fallback auf feste Indizes wenn nicht gefunden
|
|
if (colIndex.stock < 0) colIndex.stock = 5;
|
|
if (colIndex.desired < 0) colIndex.desired = 3;
|
|
if (colIndex.alert < 0) colIndex.alert = 4;
|
|
|
|
// Jede Datenzeile in der Tabelle durchgehen
|
|
var rowsFound = 0;
|
|
var rowsProcessed = 0;
|
|
$('table.liste tr').not('.liste_titre').each(function() {
|
|
rowsFound++;
|
|
var $row = $(this);
|
|
|
|
// Produkt-Link finden um die Produkt-ID zu extrahieren
|
|
var $productLink = $row.find('td a[href*="product/card.php?id="]').first();
|
|
if ($productLink.length === 0) {
|
|
$productLink = $row.find('td a[href*="product.php?id="]').first();
|
|
}
|
|
if ($productLink.length === 0) {
|
|
return;
|
|
}
|
|
|
|
var href = $productLink.attr('href');
|
|
var match = href.match(/id=(\d+)/);
|
|
if (!match) {
|
|
return;
|
|
}
|
|
var productId = match[1];
|
|
rowsProcessed++;
|
|
|
|
// Stock-Spalte dynamisch finden
|
|
var $stockCell = $row.find('td').eq(colIndex.stock);
|
|
if ($stockCell.length === 0) return;
|
|
|
|
// Stock-Wert aus der Zelle lesen (physischer Bestand)
|
|
var physicalStock = parseFloat($stockCell.text().trim()) || 0;
|
|
|
|
// Desired-Stock und Alert-Stock
|
|
var desired = parseFloat($row.find('td').eq(colIndex.desired).text().trim()) || 0;
|
|
var alert = parseFloat($row.find('td').eq(colIndex.alert).text().trim()) || 0;
|
|
|
|
// In der Replenish-Liste haben wir nur den physischen Bestand
|
|
// Theoretischer Bestand wird hier gleich dem physischen gesetzt
|
|
// (Der echte theoretische Bestand wird nur in den Orders angezeigt)
|
|
var theoreticalStock = physicalStock;
|
|
|
|
// Badge-Klasse basierend auf physischem Bestand
|
|
var badgeClass = 'badge-success';
|
|
var icon = 'fa-check-circle';
|
|
var tooltip = 'Ausreichend verfügbar';
|
|
|
|
if (physicalStock < 1) {
|
|
badgeClass = 'badge-danger';
|
|
icon = 'fa-times-circle';
|
|
tooltip = 'Nicht verfügbar';
|
|
} else if (alert > 0 && physicalStock <= alert) {
|
|
badgeClass = 'badge-warning';
|
|
icon = 'fa-exclamation-triangle';
|
|
tooltip = 'Unter Mindestmenge (' + alert + ')';
|
|
} else if (desired > 0 && physicalStock < desired) {
|
|
badgeClass = 'badge-secondary';
|
|
icon = 'fa-box-open';
|
|
tooltip = 'Unter Wunschmenge (' + desired + ')';
|
|
}
|
|
|
|
// Shop-Link und günstigster Lieferant ermitteln
|
|
var shopIconHtml = '';
|
|
var supplierBadgeHtml = '';
|
|
var suppliers = productSuppliers[productId];
|
|
|
|
if (suppliers && suppliers.length > 0) {
|
|
// Günstigster Lieferant Badge
|
|
var cheapest = suppliers[0];
|
|
var shortCode = cheapest.supplier_name.replace(/[^a-zA-Z0-9]/g, '').substring(0, 3).toUpperCase();
|
|
supplierBadgeHtml = '<span class="sl3-supplier-badge classfortooltip" title="Günstigster: ' +
|
|
cheapest.supplier_name + ' (' + parseFloat(cheapest.price).toFixed(2).replace('.', ',') + ' EUR)">' +
|
|
shortCode + '</span>';
|
|
|
|
if (suppliers.length === 1) {
|
|
shopIconHtml = '<a href="' + cheapest.full_url + '" target="supplier_' + cheapest.supplier_id + '" ' +
|
|
'class="classfortooltip" title="' + cheapest.supplier_name + '" style="color: #0077b6; font-size: 14px;">' +
|
|
'<i class="' + shopIcon + '"></i></a>';
|
|
} else {
|
|
shopIconHtml = '<a href="#" class="sl3-modal-trigger" data-suppliers=\'' + JSON.stringify(suppliers) + '\' ' +
|
|
'title="' + suppliers.length + ' Lieferanten" style="color: #0077b6; font-size: 14px;">' +
|
|
'<i class="' + shopIcon + '"></i><i class="fas fa-caret-down" style="font-size:8px;margin-left:2px;"></i></a>';
|
|
}
|
|
}
|
|
|
|
// Stock-Zelle ersetzen mit neuem Format: Physisch | Theoretisch
|
|
var html = '<div class="sl3-stock-wrapper">' +
|
|
'<span class="sl3-shop-col">' + shopIconHtml + '</span>' +
|
|
supplierBadgeHtml +
|
|
'<span class="sl3-badge-col">' +
|
|
'<span class="badge ' + badgeClass + ' classfortooltip" title="' + tooltip + '" style="font-size: 11px; white-space: nowrap;">' +
|
|
'<i class="fas ' + icon + '" style="margin-right: 3px;"></i>' +
|
|
'<span style="font-weight: bold;">' + Math.floor(physicalStock) + '</span>' +
|
|
'<span style="opacity: 0.7; margin: 0 2px;">|</span>' +
|
|
'<span style="opacity: 0.8;">' + Math.floor(theoreticalStock) + '</span>' +
|
|
'</span></span>' +
|
|
'</div>';
|
|
$stockCell.html(html).addClass('right').css('text-align', 'right');
|
|
});
|
|
|
|
console.log('SL3: Processing complete - rows found:', rowsFound, 'rows processed:', rowsProcessed);
|
|
});
|