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[] = '';
+ $html[] = '';
+ foreach ($columns as $label) {
+ $html[] = '';
+ }
+ $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;
}