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[] = ''; + } + + 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 . '
Keine Warnung
' . '

' . '' - . '
' - . '
Lagercharge
' - . '

Naurua-Chargennummer:
Hersteller:
Hersteller-Chargennummer:
Herstellungsdatum:
MHD:
Einkaufsdatum:

' + . '
' + . '
Aktive Charge
' + . '

Naurua-Chargennummer:
Aktueller Bestand:
Eingangsmenge:
Hersteller:
Hersteller-Chargennummer:
Herstellungsdatum:
MHD:
Einkaufsdatum:

' . '
' . '
' + . '
' . auth_render_lager_historical_lots($lagerProducts) . '
' . '
' . '
Virtuelle Charge
' . '

' @@ -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'; } } });";