Historische Chargen im Produkt-Drawer anzeigen
This commit is contained in:
1 parent
ad5d23f6be
commit
d747600e1c
2 files changed
+106
-4
No files matched your search
@@ -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(
|
||||
|
||||
@@ -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[] = '<div data-lager-historical-product="' . $productId . '" hidden>';
|
||||
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[] = '<article class="sg-card sg-card--content-card" data-component="content-card" aria-label="Vergangene Charge">'
|
||||
. '<div class="sg-card-segment sg-card-segment--header sg-card-segment--darkgreen" data-component-part="card-header"><div class="sg-strong sg-text-on-dark">Vergangene Charge</div></div>'
|
||||
. '<div class="sg-card-segment sg-card-segment--body" data-component-part="card-body">'
|
||||
. '<p class="sg-body"><strong>Naurua-Chargennummer:</strong> ' . auth_escape_html((string) ($lot['lot_number'] ?? 'Nicht hinterlegt')) . '<br>'
|
||||
. '<strong>Status:</strong> Abgeschlossen<br>'
|
||||
. '<strong>Eingangsmenge:</strong> ' . auth_format_lager_quantity((float) ($lot['qty_in'] ?? 0)) . '<br>'
|
||||
. '<strong>Verbrauchte Menge:</strong> ' . auth_format_lager_quantity((float) ($lot['qty_out'] ?? 0)) . '<br>'
|
||||
. '<strong>Verbrauchszeitraum:</strong> ' . auth_format_lager_date($lot['first_in_at'] ?? null) . ' – ' . auth_format_lager_date($lot['last_out_at'] ?? null) . '<br>'
|
||||
. '<strong>Aufgebraucht am:</strong> ' . $emptyDate . '<br>'
|
||||
. '<strong>MHD:</strong> ' . auth_format_lager_date($lot['expiry_date'] ?? null) . '<br>'
|
||||
. '<strong>Hersteller:</strong> ' . auth_escape_html((string) ($lot['manufacturer_name'] ?? 'Nicht hinterlegt')) . '<br>'
|
||||
. '<strong>Hersteller-Chargennummer:</strong> ' . auth_escape_html((string) ($lot['manufacturer_lot_number'] ?? 'Nicht hinterlegt')) . '</p>'
|
||||
. '</div></article>';
|
||||
}
|
||||
$html[] = '</div>';
|
||||
}
|
||||
|
||||
return implode('', $html);
|
||||
}
|
||||
|
||||
function auth_render_lager_content(array $lagerProducts, array $lagerArtikel, array $lagerManufacturers, string $csrfToken): string
|
||||
{
|
||||
$manufacturerOptions = '<option value="">Hersteller auswählen</option>';
|
||||
@@ -293,9 +340,9 @@ function auth_render_lager_content(array $lagerProducts, array $lagerArtikel, ar
|
||||
. '<div class="sg-card-segment sg-card-segment--header sg-card-segment--darkgreen" data-component-part="card-header" data-lager-drawer-warning-header><div class="sg-strong sg-text-on-dark" data-lager-drawer-warning-title>Keine Warnung</div></div>'
|
||||
. '<div class="sg-card-segment sg-card-segment--body" data-component-part="card-body"><p class="sg-body" data-lager-drawer-warning-value></p></div>'
|
||||
. '</article>'
|
||||
. '<article class="sg-card sg-card--content-card" data-component="content-card" data-lager-drawer-topic="lot" aria-label="Lagercharge">'
|
||||
. '<div class="sg-card-segment sg-card-segment--header sg-card-segment--darkgreen" data-component-part="card-header"><div class="sg-strong sg-text-on-dark">Lagercharge</div></div>'
|
||||
. '<div class="sg-card-segment sg-card-segment--body" data-component-part="card-body"><p class="sg-body"><strong>Naurua-Chargennummer:</strong> <span data-lager-drawer-lot-number></span><br><strong>Hersteller:</strong> <span data-lager-drawer-manufacturer-name></span><br><strong>Hersteller-Chargennummer:</strong> <span data-lager-drawer-manufacturer-lot-number></span><br><strong>Herstellungsdatum:</strong> <span data-lager-drawer-mfg-date></span><br><strong>MHD:</strong> <span data-lager-drawer-expiry-date></span><br><strong>Einkaufsdatum:</strong> <span data-lager-drawer-purchase-date></span></p><button class="sg-interaction-element sg-button sg-button--active" type="button" data-lager-edit-open>Charge bearbeiten</button></div>'
|
||||
. '<article class="sg-card sg-card--content-card" data-component="content-card" data-lager-drawer-topic="lot" aria-label="Aktive Charge">'
|
||||
. '<div class="sg-card-segment sg-card-segment--header sg-card-segment--darkgreen" data-component-part="card-header"><div class="sg-strong sg-text-on-dark">Aktive Charge</div></div>'
|
||||
. '<div class="sg-card-segment sg-card-segment--body" data-component-part="card-body"><p class="sg-body"><strong>Naurua-Chargennummer:</strong> <span data-lager-drawer-lot-number></span><br><strong>Aktueller Bestand:</strong> <span data-lager-drawer-current-qty></span><br><strong>Eingangsmenge:</strong> <span data-lager-drawer-qty-in></span><br><strong>Hersteller:</strong> <span data-lager-drawer-manufacturer-name></span><br><strong>Hersteller-Chargennummer:</strong> <span data-lager-drawer-manufacturer-lot-number></span><br><strong>Herstellungsdatum:</strong> <span data-lager-drawer-mfg-date></span><br><strong>MHD:</strong> <span data-lager-drawer-expiry-date></span><br><strong>Einkaufsdatum:</strong> <span data-lager-drawer-purchase-date></span></p><button class="sg-interaction-element sg-button sg-button--active" type="button" data-lager-edit-open>Charge bearbeiten</button></div>'
|
||||
. '<div class="sg-card-segment sg-card-segment--body sg-card-segment--gray" data-lager-edit-form hidden>'
|
||||
. '<form class="sg-form-sections-card" data-lager-edit-form-element><div class="sg-form-sections-card__body">'
|
||||
. '<div class="sg-form-sections-card__field-group"><label class="sg-label" for="lager-manufacturer">Hersteller</label><select class="sg-interaction-element sg-input-single-line" id="lager-manufacturer" data-lager-manufacturer>' . $manufacturerOptions . '</select></div>'
|
||||
@@ -312,6 +359,7 @@ function auth_render_lager_content(array $lagerProducts, array $lagerArtikel, ar
|
||||
. '<div class="sg-component-row sg-basic-card__actions"><button class="sg-interaction-element sg-button sg-button--active" type="submit">Hersteller anlegen</button></div></div></form>'
|
||||
. '</div></article>'
|
||||
. '</article>'
|
||||
. '<div data-lager-drawer-topic="historical-lots" aria-label="Vergangene Chargen">' . auth_render_lager_historical_lots($lagerProducts) . '</div>'
|
||||
. '<article class="sg-card sg-card--content-card" data-component="content-card" data-lager-drawer-topic="virtual-lot" aria-label="Virtuelle Charge">'
|
||||
. '<div class="sg-card-segment sg-card-segment--header sg-card-segment--darkgreen" data-component-part="card-header"><div class="sg-strong sg-text-on-dark">Virtuelle Charge</div></div>'
|
||||
. '<div class="sg-card-segment sg-card-segment--body" data-component-part="card-body"><p class="sg-body" data-lager-drawer-virtual-lot-value></p></div>'
|
||||
@@ -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'; } } });";
|
||||
|
||||
Reference in new issue
Block a user