From 3334913915861470009be28986ca0cb4cb441270 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20Gla=CC=88ser?= Date: Sun, 6 Sep 2026 07:33:39 +0200 Subject: [PATCH] Lager-Tab mit Produkt- und Artikel-Tabellen erweitern --- modules/erp/artikel-mapping/service.php | 26 +++++++++ modules/erp/lager/service.php | 32 ++++++++++ modules/shared/auth/ui/home.php | 77 ++++++++++++++++++++++++- public/assets/styles.portal.css | 8 +++ public/index.php | 5 +- 5 files changed, 146 insertions(+), 2 deletions(-) diff --git a/modules/erp/artikel-mapping/service.php b/modules/erp/artikel-mapping/service.php index fa0b2f5..bdc0017 100644 --- a/modules/erp/artikel-mapping/service.php +++ b/modules/erp/artikel-mapping/service.php @@ -1,6 +1,32 @@ query( + "SELECT + si.id, + si.item_code, + si.display_name + FROM sellable_item si + INNER JOIN sellable_item_component sic + ON sic.sellable_item_id = si.id + WHERE si.status = 'active' + GROUP BY si.id, si.item_code, si.display_name + HAVING COUNT(DISTINCT sic.product_id) > 1 + ORDER BY si.display_name ASC, si.id ASC" + ); + + return array_map( + static fn (array $row): array => [ + 'id' => (int) $row['id'], + 'item_code' => (string) $row['item_code'], + 'display_name' => (string) $row['display_name'], + ], + $stmt->fetchAll() + ); +} + function normalize_title_key(string $value): string { $value = trim(mb_strtolower($value, 'UTF-8')); diff --git a/modules/erp/lager/service.php b/modules/erp/lager/service.php index 9083733..384a176 100644 --- a/modules/erp/lager/service.php +++ b/modules/erp/lager/service.php @@ -50,6 +50,38 @@ function get_otc_order_form_products(PDO $pdo): array return $products; } +function get_lager_product_overview(PDO $pdo): array +{ + $stmt = $pdo->query( + "SELECT + p.id, + p.name, + p.sku, + p.uom, + COALESCE(SUM(v.qty_net), 0) AS available_qty + FROM product p + LEFT JOIN stock_lot sl + ON sl.product_id = p.id + AND sl.status = 'current' + LEFT JOIN v_stock_lot_balance v + ON v.stock_lot_id = sl.id + WHERE p.status = 'active' + GROUP BY p.id, p.name, p.sku, p.uom + ORDER BY p.name ASC, p.id ASC" + ); + + return 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'], + ], + $stmt->fetchAll() + ); +} + function get_current_lot_balance_for_update(PDO $pdo, int $productId): array { $stmt = $pdo->prepare( diff --git a/modules/shared/auth/ui/home.php b/modules/shared/auth/ui/home.php index c78741f..fdd3a65 100644 --- a/modules/shared/auth/ui/home.php +++ b/modules/shared/auth/ui/home.php @@ -201,7 +201,58 @@ function auth_render_bestellungen_content(array $bestellungenTable): string . ''; } -function render_auth_home_page(array $user, array $otcProducts = [], array $bestellungenTable = []): void +function auth_render_lager_table(string $title, array $rows, array $columns, string $tableKey): string +{ + $html = []; + $html[] = '
'; + $html[] = '
' . auth_escape_html($title) . '
'; + $html[] = '
'; + foreach ($columns as $label) { + $html[] = '
' . auth_escape_html($label) . '
'; + } + $html[] = '
'; + + foreach ($rows as $index => $row) { + $rowClass = 'sg-card-segment sg-card-segment--body sg-card-segment--white sg-large-table__row'; + if ($index % 2 === 1) { + $rowClass .= ' sg-large-table__row--striped-light'; + } + + $id = (int) ($row['id'] ?? 0); + $name = (string) ($row['name'] ?? $row['display_name'] ?? ''); + $value = (string) max(0, (int) round((float) ($row['available_qty'] ?? 0))); + $nameData = ' data-lager-drawer-name="' . auth_escape_html($name) . '"'; + $valueData = ' data-lager-drawer-value="' . auth_escape_html($value) . '"'; + $typeData = ' data-lager-drawer-type="' . auth_escape_html($tableKey === 'products' ? 'Produkt' : 'Artikel') . '"'; + + $html[] = '
'; + $html[] = ''; + if ($tableKey === 'products') { + $html[] = ''; + } + $html[] = '
'; + } + + $html[] = '
'; + return implode('', $html); +} + +function auth_render_lager_content(array $lagerProducts, array $lagerBundles): string +{ + return '
' + . '
' + . auth_render_lager_table('Produkte', $lagerProducts, ['Produktname', 'Anzahl im Lager'], 'products') + . auth_render_lager_table('Artikel', $lagerBundles, ['Artikel-Name'], 'bundles') + . '
' + . '' + . '' + . '
'; +} + +function render_auth_home_page(array $user, array $otcProducts = [], array $bestellungenTable = [], array $lagerProducts = [], array $lagerBundles = []): void { $otcProductRows = ''; foreach ($otcProducts as $product) { @@ -230,9 +281,11 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ); $bestellungenContent = auth_render_bestellungen_content($bestellungenTable); + $lagerContent = auth_render_lager_content($lagerProducts, $lagerBundles); $moduleContentCards = json_encode( [ 'Bestellungen' => $bestellungenContent, + 'Lager' => $lagerContent, ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ); @@ -417,6 +470,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best echo " if (contentTitle) { contentTitle.textContent = activeEntry; }"; echo " if (contentBody) { contentBody.innerHTML = portalModuleContentCards[activeEntry] || ''; }"; echo " if (activeEntry === 'Bestellungen') { initBestellungenBindings(); }"; + echo " if (activeEntry === 'Lager') { initLagerBindings(); }"; echo " menu.innerHTML = '';"; echo " entries.forEach((entry, index) => {"; echo " const button = document.createElement('button');"; @@ -437,6 +491,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best echo " if (contentTitle) { contentTitle.textContent = entryName; }"; echo " if (contentBody) { contentBody.innerHTML = portalModuleContentCards[entryName] || ''; }"; echo " if (entryName === 'Bestellungen') { initBestellungenBindings(); }"; + echo " if (entryName === 'Lager') { initLagerBindings(); }"; echo " });"; echo " });"; echo "};"; @@ -516,6 +571,26 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best echo "})();"; echo "let bestellungenTableHandlersInstalled = false;"; echo "let bestellungenRealtimeSource = null;"; + echo "let lagerBindingsInstalled = false;"; + echo "function initLagerBindings() {"; + echo " if (lagerBindingsInstalled) { return; }"; + echo " const contentRoot = document.querySelector('[data-left-navigation-content-body]');"; + echo " if (!contentRoot || !contentRoot.querySelector('[data-lager-content]')) { return; }"; + echo " lagerBindingsInstalled = true;"; + echo " const getDrawer = () => contentRoot.querySelector('[data-lager-drawer]');"; + echo " const setDrawerState = (open) => {"; + echo " const drawer = getDrawer(); const backdrop = contentRoot.querySelector('[data-lager-drawer-backdrop]');"; + echo " if (!drawer || !backdrop) { return; }"; + echo " drawer.dataset.open = String(open); drawer.setAttribute('aria-hidden', String(!open)); backdrop.dataset.open = String(open); backdrop.setAttribute('aria-hidden', String(!open));"; + echo " };"; + echo " const closeDrawer = () => { setDrawerState(false); contentRoot.querySelectorAll('[data-lager-drawer-open]').forEach((trigger) => trigger.setAttribute('aria-expanded', 'false')); };"; + echo " contentRoot.addEventListener('click', (event) => {"; + echo " const trigger = event.target.closest('[data-lager-drawer-open]');"; + echo " if (trigger) { event.preventDefault(); 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 = type !== 'Produkt'; } drawer.querySelector('[data-lager-drawer-value-label]').textContent = 'Anzahl im Lager:'; drawer.querySelector('[data-lager-drawer-value-value]').textContent = value; setDrawerState(true); contentRoot.querySelectorAll('[data-lager-drawer-open]').forEach((button) => button.setAttribute('aria-expanded', String(button === trigger))); return; }"; + echo " if (event.target.closest('[data-lager-drawer-close]') || event.target.closest('[data-lager-drawer-backdrop]')) { closeDrawer(); }"; + echo " });"; + echo " document.addEventListener('keydown', (event) => { if (event.key === 'Escape') { closeDrawer(); } });"; + echo "}"; echo "function initBestellungenBindings() {"; echo " const contentRoot = document.querySelector('[data-left-navigation-content-body]');"; echo " if (!contentRoot) { return; }"; diff --git a/public/assets/styles.portal.css b/public/assets/styles.portal.css index 0156fb4..89e1a24 100644 --- a/public/assets/styles.portal.css +++ b/public/assets/styles.portal.css @@ -157,6 +157,14 @@ body.sg-bestellungen-mobile-detail-open > :not([data-bestellungen-mobile-detail- justify-content: flex-end; } +.sg-large-table--lager .sg-large-table__row { + grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); +} + +.sg-large-table--lager .sg-large-table__cell:last-child { + text-align: right; +} + [data-bestellungen-mobile-detail] .sg-navigation-card-layout { margin-top: 0; } diff --git a/public/index.php b/public/index.php index 920f65a..85cc1f6 100644 --- a/public/index.php +++ b/public/index.php @@ -6,6 +6,7 @@ require_once __DIR__ . '/../modules/shared/auth/ui/login.php'; require_once __DIR__ . '/../modules/shared/auth/ui/home.php'; require_once __DIR__ . '/../modules/erp/bestellungen/service.php'; require_once __DIR__ . '/../modules/erp/lager/service.php'; +require_once __DIR__ . '/../modules/erp/artikel-mapping/service.php'; $env = expand_env_values(parse_env_file(__DIR__ . '/../.env')); $pdo = connect_database($env); @@ -46,6 +47,8 @@ if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') { $currentUser = auth_current_user($pdo); if ($currentUser !== null) { $otcProducts = get_otc_order_form_products($pdo); + $lagerProducts = get_lager_product_overview($pdo); + $lagerBundles = get_lager_bundle_overview($pdo); $bestellungenTable = get_sales_order_overview($pdo, [ 'search' => (string) ($_GET['bestellungen_search'] ?? ''), 'sort_column' => (string) ($_GET['bestellungen_sort'] ?? 'order_date'), @@ -57,7 +60,7 @@ if ($currentUser !== null) { echo auth_render_bestellungen_content($bestellungenTable); exit; } - render_auth_home_page($currentUser, $otcProducts, $bestellungenTable); + render_auth_home_page($currentUser, $otcProducts, $bestellungenTable, $lagerProducts, $lagerBundles); exit; }