diff --git a/modules/erp/lager/api/lot.php b/modules/erp/lager/api/lot.php
index 7ff1873..0bf9dc1 100644
--- a/modules/erp/lager/api/lot.php
+++ b/modules/erp/lager/api/lot.php
@@ -66,12 +66,12 @@ try {
}
if ($action === 'delete_open_lot') {
- delete_open_stock_lot(
+ $productId = delete_open_stock_lot(
$pdo,
(int) ($data['lotId'] ?? 0),
(string) ($currentUser['username'] ?? $currentUser['email'] ?? 'unknown')
);
- json_response(200, ['ok' => true]);
+ json_response(200, ['ok' => true, 'productId' => $productId, 'availableQty' => get_lager_product_available_qty($pdo, $productId)]);
}
if ($action === 'prepare_lot') {
@@ -116,7 +116,7 @@ try {
$targetQty,
$lotId > 0 && $lotNumber === ''
);
- json_response(200, ['ok' => true, 'lot' => $preparedLot, 'balance' => get_stock_lot_balance($pdo, $lotId)]);
+ json_response(200, ['ok' => true, 'lot' => $preparedLot, 'balance' => get_stock_lot_balance($pdo, $lotId), 'availableQty' => get_lager_product_available_qty($pdo, $productId)]);
}
json_response(422, ['ok' => false, 'error' => 'Unsupported action']);
diff --git a/modules/erp/lager/service.php b/modules/erp/lager/service.php
index 988bd59..28c5db8 100644
--- a/modules/erp/lager/service.php
+++ b/modules/erp/lager/service.php
@@ -75,7 +75,14 @@ function get_lager_product_overview(PDO $pdo): array
p.name,
p.sku,
p.uom,
- COALESCE(SUM(v.qty_net), 0) AS available_qty,
+ COALESCE(SUM(v.qty_net), 0) + COALESCE((
+ SELECT SUM(open_v.qty_net)
+ FROM stock_lot open_sl
+ INNER JOIN v_stock_lot_balance open_v ON open_v.stock_lot_id = open_sl.id
+ WHERE open_sl.product_id = p.id
+ AND open_sl.status = 'open'
+ ), 0) AS available_qty,
+ COALESCE(v.qty_net, 0) AS current_qty,
COALESCE(v.qty_in, 0) AS qty_in,
sl.lot_number,
sl.id AS lot_id,
@@ -113,7 +120,7 @@ function get_lager_product_overview(PDO $pdo): array
WHERE p.status = 'active'
GROUP BY p.id, p.name, p.sku, p.uom, sl.id, sl.lot_number, sl.manufacturer_party_id,
sl.manufacturer_lot_number, mp.name, sl.mfg_date, sl.expiry_date, sl.purchase_date,
- sl.sellout_date, sl.sellout_forecast_status, v.qty_in
+ sl.sellout_date, sl.sellout_forecast_status, v.qty_net, v.qty_in
ORDER BY p.name ASC, p.id ASC"
);
@@ -124,6 +131,7 @@ function get_lager_product_overview(PDO $pdo): array
'sku' => (string) $row['sku'],
'uom' => (string) $row['uom'],
'available_qty' => (float) $row['available_qty'],
+ 'current_qty' => (float) $row['current_qty'],
'qty_in' => (float) $row['qty_in'],
'lot_id' => $row['lot_id'] === null ? null : (int) $row['lot_id'],
'lot_number' => $row['lot_number'],
@@ -230,6 +238,19 @@ function get_lager_product_overview(PDO $pdo): array
return $products;
}
+function get_lager_product_available_qty(PDO $pdo, int $productId): float
+{
+ $stmt = $pdo->prepare(
+ "SELECT COALESCE(SUM(v.qty_net), 0)
+ FROM stock_lot sl
+ INNER JOIN v_stock_lot_balance v ON v.stock_lot_id = sl.id
+ WHERE sl.product_id = :product_id
+ AND sl.status IN ('current', 'open')"
+ );
+ $stmt->execute([':product_id' => $productId]);
+ return (float) $stmt->fetchColumn();
+}
+
function update_stock_lot_traceability(
PDO $pdo,
int $lotId,
@@ -809,7 +830,7 @@ function get_or_create_open_lot(PDO $pdo, int $productId): int
return (int) $newLotId;
}
-function delete_open_stock_lot(PDO $pdo, int $lotId, string $changedBy): void
+function delete_open_stock_lot(PDO $pdo, int $lotId, string $changedBy): int
{
if ($lotId <= 0 || trim($changedBy) === '') {
throw new InvalidArgumentException('Offene Charge und Bediener sind erforderlich');
@@ -850,9 +871,11 @@ function delete_open_stock_lot(PDO $pdo, int $lotId, string $changedBy): void
throw new RuntimeException('Offene Charge konnte nicht geloescht werden');
}
+ $productId = (int) $lot['product_id'];
if ($startedTransaction) {
$pdo->commit();
}
+ return $productId;
} catch (Throwable $error) {
if ($startedTransaction && $pdo->inTransaction()) {
$pdo->rollBack();
diff --git a/modules/shared/auth/ui/home.php b/modules/shared/auth/ui/home.php
index fc30e20..e7e9c70 100644
--- a/modules/shared/auth/ui/home.php
+++ b/modules/shared/auth/ui/home.php
@@ -243,7 +243,7 @@ function auth_render_lager_table(string $title, array $rows, array $columns, str
. ' data-lager-drawer-forecast-status="' . auth_escape_html($forecastStatus) . '"'
. ' data-lager-drawer-forecast-warning="' . ($forecastWarning ? 'true' : 'false') . '"'
. ' data-lager-drawer-forecast-mhd-warning="' . ($forecastMhdWarning ? 'true' : 'false') . '"'
- . ' data-lager-drawer-current-qty="' . auth_escape_html(auth_format_lager_quantity((float) ($row['available_qty'] ?? 0))) . '"'
+ . ' data-lager-drawer-current-qty="' . auth_escape_html(auth_format_lager_quantity((float) ($row['current_qty'] ?? 0))) . '"'
. ' data-lager-drawer-qty-in="' . auth_escape_html(auth_format_lager_quantity((float) ($row['qty_in'] ?? 0))) . '"'
. ' data-lager-drawer-virtual-shortage-qty="' . auth_escape_html(auth_format_lager_quantity((float) ($row['virtual_shortage_qty'] ?? 0))) . '"'
. ' data-lager-drawer-lot-id="' . auth_escape_html((string) ($row['lot_id'] ?? '')) . '"'
@@ -334,7 +334,7 @@ function auth_render_lager_open_lots(array $lagerProducts, string $manufacturerO
$html[] = '
';
foreach (($product['open_lots'] ?? []) as $lot) {
$lotId = (int) ($lot['lot_id'] ?? 0);
- $html[] = '
'
+ $html[] = ''
. ''
. 'Naurua-Chargennummer: ' . auth_escape_html((string) ($lot['lot_number'] ?? 'Nicht hinterlegt')) . '
'
. 'Status: Vorbereitet
'
@@ -356,7 +356,7 @@ function auth_render_lager_open_lots(array $lagerProducts, string $manufacturerO
. '
'
. '
'
. 'Leere Felder speichern?
Die folgenden Felder werden leer gespeichert:
'
- . 'Möchtest du diese offene Charge wirklich löschen?
Du kannst das nicht rückgängig machen. Bestätige durch Eingabe von DELETE.
';
+ . 'Möchtest du diese offene Charge wirklich löschen?
Du kannst das nicht rückgängig machen. Bestätige durch Eingabe von DELETE.
';
}
$html[] = '';
}
@@ -385,7 +385,7 @@ function auth_render_lager_content(array $lagerProducts, array $lagerArtikel, ar
. ''
. '
'
. ''
- . ''
+ . 'Name:
Wert:
Voraussichtlich leer: Keine Prognose
'
. ''
. '
'
. ''
@@ -809,6 +809,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
echo " };";
echo " const getLagerWarningTitle = (trigger) => trigger.dataset.lagerDrawerForecastStatus === 'depleted' ? 'Warnung: Charge ist aufgebraucht' : (trigger.dataset.lagerDrawerForecastMhdWarning === 'true' ? 'Warnung: MHD wird vor Chargenende erreicht' : (trigger.dataset.lagerDrawerForecastWarning === 'true' ? 'Warnung: Charge wird innerhalb von drei Monaten aufgebraucht sein' : 'Keine Warnung'));";
echo " const closeDrawer = () => { setDrawerState(false); contentRoot.querySelectorAll('[data-lager-drawer-open]').forEach((trigger) => trigger.setAttribute('aria-expanded', 'false')); };";
+ echo " document.addEventListener('click', (event) => { const trigger = event.target.closest('[data-lager-drawer-open]'); if (!trigger || trigger.dataset.lagerDrawerType !== 'Produkt') { return; } const drawer = getDrawer(); const forecast = drawer?.querySelector('[data-lager-drawer-forecast-value]'); if (forecast) { forecast.textContent = trigger.dataset.lagerDrawerForecast || 'Keine Prognose'; } });";
echo " document.addEventListener('click', (event) => {";
echo " const trigger = event.target.closest('[data-lager-drawer-open]');";
echo " if (trigger) { event.preventDefault(); if (window.matchMedia('(max-width: 767px)').matches) { openLagerMobileDetail(trigger); return; } const drawer = getDrawer(); if (!drawer) { return; } const type = trigger.dataset.lagerDrawerType || 'Lager'; const name = trigger.dataset.lagerDrawerName || ''; const value = trigger.dataset.lagerDrawerValue || ''; drawer.querySelector('[data-lager-drawer-title]').textContent = type + ': ' + name; drawer.querySelector('[data-lager-drawer-name-value]').textContent = name; const valueRow = drawer.querySelector('[data-lager-drawer-value-row]'); if (valueRow) { valueRow.hidden = false; } drawer.querySelector('[data-lager-drawer-value-label]').textContent = type === 'Produkt' ? 'Anzahl im Lager:' : 'Artikelnummer:'; drawer.querySelector('[data-lager-drawer-value-value]').textContent = value; drawer.querySelectorAll('[data-lager-drawer-topic]').forEach((card) => { card.hidden = type !== 'Produkt'; }); if (type === 'Produkt') { const forecast = trigger.dataset.lagerDrawerForecast || 'Keine Prognose'; const warning = trigger.dataset.lagerDrawerForecastWarning === 'true' || trigger.dataset.lagerDrawerForecastMhdWarning === 'true'; const warningValue = drawer.querySelector('[data-lager-drawer-warning-value]'); if (warningValue) { warningValue.textContent = warning ? forecast : 'Keine akute Warnung.'; warningValue.classList.toggle('sg-strong', warning); warningValue.classList.toggle('lager-forecast-warning', trigger.dataset.lagerDrawerForecastWarning === 'true'); warningValue.classList.toggle('lager-forecast-mhd-warning', trigger.dataset.lagerDrawerForecastMhdWarning === 'true'); } drawer.querySelector('[data-lager-drawer-lot-number]').textContent = trigger.dataset.lagerDrawerLotNumber || 'Nicht hinterlegt'; drawer.querySelector('[data-lager-drawer-manufacturer-name]').textContent = trigger.dataset.lagerDrawerManufacturerName || 'Nicht hinterlegt'; drawer.querySelector('[data-lager-drawer-manufacturer-lot-number]').textContent = trigger.dataset.lagerDrawerManufacturerLotNumber || 'Nicht hinterlegt'; drawer.querySelector('[data-lager-drawer-mfg-date]').textContent = trigger.dataset.lagerDrawerMfgDate || 'Nicht hinterlegt'; drawer.querySelector('[data-lager-drawer-expiry-date]').textContent = trigger.dataset.lagerDrawerExpiryDate || 'Nicht hinterlegt'; drawer.querySelector('[data-lager-drawer-purchase-date]').textContent = trigger.dataset.lagerDrawerPurchaseDate || 'Nicht hinterlegt'; const virtualLot = trigger.dataset.lagerDrawerVirtualLotNumber || ''; drawer.querySelector('[data-lager-drawer-virtual-lot-value]').textContent = virtualLot ? 'Virtuelle Charge: ' + virtualLot : 'Keine virtuelle Fehlbestandscharge vorhanden.'; const manufacturer = drawer.querySelector('[data-lager-manufacturer]'); if (manufacturer) { manufacturer.value = trigger.dataset.lagerDrawerManufacturerPartyId || ''; } const manufacturerLot = drawer.querySelector('[data-lager-manufacturer-lot]'); if (manufacturerLot) { manufacturerLot.value = trigger.dataset.lagerDrawerManufacturerLotNumber || ''; } const mfgDate = drawer.querySelector('[data-lager-mfg-date]'); if (mfgDate) { mfgDate.value = trigger.dataset.lagerDrawerMfgDate || ''; } const expiryDate = drawer.querySelector('[data-lager-expiry-date]'); if (expiryDate) { expiryDate.value = trigger.dataset.lagerDrawerExpiryDate || ''; } const purchaseDate = drawer.querySelector('[data-lager-purchase-date]'); if (purchaseDate) { purchaseDate.value = trigger.dataset.lagerDrawerPurchaseDate || ''; } const editForm = drawer.querySelector('[data-lager-edit-form]'); if (editForm) { editForm.hidden = true; } drawer.dataset.lagerActiveLotId = trigger.dataset.lagerDrawerLotId || ''; } setDrawerState(true); contentRoot.querySelectorAll('[data-lager-drawer-open]').forEach((button) => button.setAttribute('aria-expanded', String(button === trigger))); return; }";
@@ -836,6 +837,9 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
echo " document.addEventListener('click', (event) => { if (event.target.closest('[data-lager-detail-back]')) { event.preventDefault(); closeLagerMobileDetail(); } });";
echo " document.addEventListener('keydown', (event) => { if (event.key === 'Escape') { closeDrawer(); closeLagerMobileDetail(); } });";
echo " window.addEventListener('popstate', (event) => { if (lagerMobileDetailOpen && (!event.state || event.state.lagerView !== 'detail')) { restoreLagerMobileDetail(); } });";
+ echo " const syncLagerDisplayedTotal = () => { const drawer = getDrawer(); const trigger = contentRoot.querySelector('[data-lager-drawer-open][aria-expanded=\"true\"]'); if (!drawer || !trigger || trigger.dataset.lagerDrawerType !== 'Produkt') { return; } const active = Number((drawer.querySelector('[data-lager-drawer-current-qty]')?.textContent || '0').replace(',', '.')) || 0; const open = Array.from(drawer.querySelectorAll('[data-lager-open-card-qty-net]')).reduce((sum, node) => sum + (Number((node.textContent || '0').replace(',', '.')) || 0), 0); const total = active + open; const formatted = Number.isInteger(total) ? String(total) : total.toFixed(2).replace(/\.?0+$/, ''); const value = drawer.querySelector('[data-lager-drawer-value-value]'); if (value && value.textContent !== formatted) { value.textContent = formatted; } trigger.dataset.lagerDrawerValue = formatted; };";
+ echo " const lagerTotalObserver = new MutationObserver(syncLagerDisplayedTotal);";
+ echo " lagerTotalObserver.observe(contentRoot, { childList: true, subtree: true, characterData: true });";
echo "}";
echo "function initBestellungenBindings() {";
echo " const contentRoot = document.querySelector('[data-left-navigation-content-body]');";