diff --git a/modules/erp/lager/service.php b/modules/erp/lager/service.php
index f151734..ebbcd0e 100644
--- a/modules/erp/lager/service.php
+++ b/modules/erp/lager/service.php
@@ -76,6 +76,7 @@ function get_lager_product_overview(PDO $pdo): array
p.sku,
p.uom,
COALESCE(SUM(v.qty_net), 0) AS available_qty,
+ COALESCE(v.qty_in, 0) AS qty_in,
sl.lot_number,
sl.id AS lot_id,
sl.manufacturer_party_id,
@@ -108,13 +109,14 @@ function get_lager_product_overview(PDO $pdo): array
ORDER BY p.name ASC, p.id ASC"
);
- return array_map(
+ $products = array_map(
static fn (array $row): array => [
'id' => (int) $row['id'],
'name' => (string) $row['name'],
'sku' => (string) $row['sku'],
'uom' => (string) $row['uom'],
'available_qty' => (float) $row['available_qty'],
+ 'qty_in' => (float) $row['qty_in'],
'lot_id' => $row['lot_id'] === null ? null : (int) $row['lot_id'],
'lot_number' => $row['lot_number'],
'manufacturer_party_id' => $row['manufacturer_party_id'] === null ? null : (int) $row['manufacturer_party_id'],
@@ -129,6 +131,57 @@ function get_lager_product_overview(PDO $pdo): array
],
$stmt->fetchAll()
);
+
+ $historicalStmt = $pdo->query(
+ "SELECT
+ sl.product_id,
+ sl.id AS lot_id,
+ sl.lot_number,
+ sl.mfg_date,
+ sl.expiry_date,
+ sl.purchase_date,
+ sl.manufacturer_lot_number,
+ mp.name AS manufacturer_name,
+ COALESCE(v.qty_in, 0) AS qty_in,
+ COALESCE(v.qty_out, 0) AS qty_out,
+ COALESCE(v.qty_net, 0) AS qty_net,
+ MIN(sm.move_date) FILTER (WHERE sm.move_type = 'in') AS first_in_at,
+ MAX(sm.move_date) FILTER (WHERE sm.move_type = 'out') AS last_out_at
+ FROM stock_lot sl
+ LEFT JOIN party mp ON mp.id = sl.manufacturer_party_id
+ LEFT JOIN v_stock_lot_balance v ON v.stock_lot_id = sl.id
+ LEFT JOIN stock_move sm ON sm.lot_id = sl.id
+ WHERE sl.status = 'closed'
+ GROUP BY sl.product_id, sl.id, sl.lot_number, sl.mfg_date, sl.expiry_date,
+ sl.purchase_date, sl.manufacturer_lot_number, mp.name,
+ v.qty_in, v.qty_out, v.qty_net
+ ORDER BY sl.product_id, sl.id DESC"
+ );
+
+ $historicalLotsByProduct = [];
+ foreach ($historicalStmt->fetchAll() as $row) {
+ $historicalLotsByProduct[(int) $row['product_id']][] = [
+ 'lot_id' => (int) $row['lot_id'],
+ 'lot_number' => $row['lot_number'],
+ 'mfg_date' => $row['mfg_date'],
+ 'expiry_date' => $row['expiry_date'],
+ 'purchase_date' => $row['purchase_date'],
+ 'manufacturer_lot_number' => $row['manufacturer_lot_number'],
+ 'manufacturer_name' => $row['manufacturer_name'],
+ 'qty_in' => (float) $row['qty_in'],
+ 'qty_out' => (float) $row['qty_out'],
+ 'qty_net' => (float) $row['qty_net'],
+ 'first_in_at' => $row['first_in_at'],
+ 'last_out_at' => $row['last_out_at'],
+ ];
+ }
+
+ foreach ($products as &$product) {
+ $product['historical_lots'] = $historicalLotsByProduct[$product['id']] ?? [];
+ }
+ unset($product);
+
+ return $products;
}
function update_stock_lot_traceability(
diff --git a/modules/shared/auth/ui/home.php b/modules/shared/auth/ui/home.php
index 6231b89..85adf47 100644
--- a/modules/shared/auth/ui/home.php
+++ b/modules/shared/auth/ui/home.php
@@ -242,6 +242,8 @@ function auth_render_lager_table(string $title, array $rows, array $columns, str
$detailData = ' data-lager-drawer-forecast="' . auth_escape_html($forecastText) . '"'
. ' 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-qty-in="' . auth_escape_html(auth_format_lager_quantity((float) ($row['qty_in'] ?? 0))) . '"'
. ' data-lager-drawer-lot-id="' . auth_escape_html((string) ($row['lot_id'] ?? '')) . '"'
. ' data-lager-drawer-lot-number="' . auth_escape_html((string) ($row['lot_number'] ?? '')) . '"'
. ' data-lager-drawer-manufacturer-party-id="' . auth_escape_html((string) ($row['manufacturer_party_id'] ?? '')) . '"'
@@ -269,6 +271,51 @@ function auth_render_lager_table(string $title, array $rows, array $columns, str
return implode('', $html);
}
+function auth_format_lager_date(?string $value): string
+{
+ if ($value === null || $value === '') {
+ return 'Nicht hinterlegt';
+ }
+
+ return (new DateTimeImmutable($value))->format(str_contains($value, ':') ? 'd.m.Y H:i' : 'd.m.Y');
+}
+
+function auth_format_lager_quantity(float $value): string
+{
+ return fmod($value, 1.0) === 0.0
+ ? number_format($value, 0, ',', '.')
+ : number_format($value, 2, ',', '.');
+}
+
+function auth_render_lager_historical_lots(array $lagerProducts): string
+{
+ $html = [];
+ foreach ($lagerProducts as $product) {
+ $productId = (int) ($product['id'] ?? 0);
+ $html[] = '
';
+ foreach (($product['historical_lots'] ?? []) as $lot) {
+ $qtyNet = (float) ($lot['qty_net'] ?? 0);
+ $emptyDate = $qtyNet <= 0 ? auth_format_lager_date($lot['last_out_at'] ?? null) : 'Nicht eindeutig';
+ $html[] = '
'
+ . ''
+ . ''
+ . '
Naurua-Chargennummer: ' . auth_escape_html((string) ($lot['lot_number'] ?? 'Nicht hinterlegt')) . '
'
+ . 'Status: Abgeschlossen
'
+ . 'Eingangsmenge: ' . auth_format_lager_quantity((float) ($lot['qty_in'] ?? 0)) . '
'
+ . 'Verbrauchte Menge: ' . auth_format_lager_quantity((float) ($lot['qty_out'] ?? 0)) . '
'
+ . 'Verbrauchszeitraum: ' . auth_format_lager_date($lot['first_in_at'] ?? null) . ' – ' . auth_format_lager_date($lot['last_out_at'] ?? null) . '
'
+ . 'Aufgebraucht am: ' . $emptyDate . '
'
+ . 'MHD: ' . auth_format_lager_date($lot['expiry_date'] ?? null) . '
'
+ . 'Hersteller: ' . auth_escape_html((string) ($lot['manufacturer_name'] ?? 'Nicht hinterlegt')) . '
'
+ . 'Hersteller-Chargennummer: ' . auth_escape_html((string) ($lot['manufacturer_lot_number'] ?? 'Nicht hinterlegt')) . '
'
+ . '
';
+ }
+ $html[] = '
';
+ }
+
+ return implode('', $html);
+}
+
function auth_render_lager_content(array $lagerProducts, array $lagerArtikel, array $lagerManufacturers, string $csrfToken): string
{
$manufacturerOptions = '';
@@ -293,9 +340,9 @@ function auth_render_lager_content(array $lagerProducts, array $lagerArtikel, ar
. ''
. ''
. ''
- . ''
- . ''
- . 'Naurua-Chargennummer:
Hersteller:
Hersteller-Chargennummer:
Herstellungsdatum:
MHD:
Einkaufsdatum:
'
+ . ''
+ . ''
+ . 'Naurua-Chargennummer:
Aktueller Bestand:
Eingangsmenge:
Hersteller:
Hersteller-Chargennummer:
Herstellungsdatum:
MHD:
Einkaufsdatum:
'
. ''
. ''
+ . '' . auth_render_lager_historical_lots($lagerProducts) . '
'
. ''
. ''
. ''
@@ -702,6 +750,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
echo " if (event.target.closest('[data-lager-drawer-close]') || event.target.closest('[data-lager-drawer-backdrop]')) { closeDrawer(); }";
echo " });";
echo " contentRoot.addEventListener('click', (event) => { const trigger = event.target.closest('[data-lager-drawer-open]'); if (!trigger || trigger.dataset.lagerDrawerType !== 'Produkt' || window.matchMedia('(max-width: 767px)').matches) { return; } const header = contentRoot.querySelector('[data-lager-drawer-warning-header]'); const title = contentRoot.querySelector('[data-lager-drawer-warning-title]'); if (!header || !title) { return; } const forecast = trigger.dataset.lagerDrawerForecast || 'Keine Prognose'; const forecastWarning = trigger.dataset.lagerDrawerForecastWarning === 'true'; const mhdWarning = trigger.dataset.lagerDrawerForecastMhdWarning === 'true'; title.textContent = getLagerWarningTitle(trigger, forecast); header.classList.toggle('sg-card-segment--darkgreen', !forecastWarning && !mhdWarning); header.classList.toggle('sg-card-segment--signal-red', forecastWarning); header.classList.toggle('sg-card-segment--signal-yellow', mhdWarning); });";
+ echo " contentRoot.addEventListener('click', (event) => { const trigger = event.target.closest('[data-lager-drawer-open]'); if (!trigger || trigger.dataset.lagerDrawerType !== 'Produkt' || window.matchMedia('(max-width: 767px)').matches) { return; } const currentQty = contentRoot.querySelector('[data-lager-drawer-current-qty]'); const qtyIn = contentRoot.querySelector('[data-lager-drawer-qty-in]'); if (currentQty) { currentQty.textContent = trigger.dataset.lagerDrawerCurrentQty || '0'; } if (qtyIn) { qtyIn.textContent = trigger.dataset.lagerDrawerQtyIn || '0'; } const productId = trigger.closest('[data-lager-row]')?.dataset.lagerId || ''; contentRoot.querySelectorAll('[data-lager-historical-product]').forEach((product) => { product.hidden = product.dataset.lagerHistoricalProduct !== productId; }); });";
echo " const postLagerChange = async (payload) => { const response = await fetch('/api/lager-charge.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...payload, csrfToken: contentRoot.querySelector('[data-lager-csrf-token]')?.dataset.lagerCsrfToken || '' }) }); const result = await response.json(); if (!response.ok || result.ok !== true) { throw new Error(result.error || 'Speichern fehlgeschlagen'); } return result; };";
echo " contentRoot.addEventListener('click', (event) => { const editButton = event.target.closest('[data-lager-edit-open]'); if (editButton) { const drawer = getDrawer(); const editForm = drawer?.querySelector('[data-lager-edit-form]'); if (editForm) { editForm.hidden = false; } return; } const closeButton = event.target.closest('[data-lager-edit-close]'); if (closeButton) { const drawer = getDrawer(); const editForm = drawer?.querySelector('[data-lager-edit-form]'); if (editForm) { editForm.hidden = true; } } });";
echo " contentRoot.addEventListener('submit', async (event) => { const editForm = event.target.closest('[data-lager-edit-form-element]'); const manufacturerForm = event.target.closest('[data-lager-manufacturer-form]'); if (!editForm && !manufacturerForm) { return; } event.preventDefault(); const drawer = getDrawer(); const status = drawer?.querySelector('[data-lager-edit-status]'); if (status) { status.textContent = 'Speichern läuft …'; } try { if (manufacturerForm) { const result = await postLagerChange({ action: 'create_manufacturer', name: manufacturerForm.querySelector('[data-lager-new-manufacturer-name]')?.value || '', email: manufacturerForm.querySelector('[data-lager-new-manufacturer-email]')?.value || '', phone: manufacturerForm.querySelector('[data-lager-new-manufacturer-phone]')?.value || '' }); const select = drawer?.querySelector('[data-lager-manufacturer]'); if (select && result.manufacturer) { const option = document.createElement('option'); option.value = String(result.manufacturer.id); option.textContent = result.manufacturer.name; select.appendChild(option); select.value = String(result.manufacturer.id); } manufacturerForm.reset(); if (status) { status.textContent = 'Hersteller angelegt. Bitte Charge speichern.'; } return; } await postLagerChange({ action: 'update_lot', lotId: drawer?.dataset.lagerActiveLotId || '', manufacturerPartyId: drawer?.querySelector('[data-lager-manufacturer]')?.value || null, manufacturerLotNumber: drawer?.querySelector('[data-lager-manufacturer-lot]')?.value || '', mfgDate: drawer?.querySelector('[data-lager-mfg-date]')?.value || '', expiryDate: drawer?.querySelector('[data-lager-expiry-date]')?.value || '', purchaseDate: drawer?.querySelector('[data-lager-purchase-date]')?.value || '' }); window.location.reload(); } catch (error) { if (status) { status.textContent = error instanceof Error ? error.message : 'Speichern fehlgeschlagen'; } } });";