Compare commits

...
Author SHA1 Message Date
gitea_admin 6c1e840c97 Rollback failed charge creation 2026-09-08 19:28:36 +02:00
gitea_admin 764b6fa886 Fix ambiguous charge forecast query 2026-09-08 19:26:11 +02:00
gitea_admin 3ae9951b13 Anchor next lot forecast after current lot 2026-09-08 19:24:34 +02:00
gitea_admin 959586ecc9 Split inventory forecast by current and next lot 2026-09-08 18:48:53 +02:00
gitea_admin 77f6fee5ac Fix spacing between historical lot cards 2026-09-08 18:26:56 +02:00
gitea_admin 8f7fb9875a Complete lot activation and virtual shortage reconciliation 2026-09-08 17:45:19 +02:00
gitea_admin 400b352522 Avoid repeating MHD in warning text 2026-09-08 17:14:46 +02:00
gitea_admin 4a8936761b Reconcile virtual shortage allocations on lot activation 2026-09-08 17:07:04 +02:00
gitea_admin 0e8df45548 Refresh active lot details after promotion 2026-09-08 17:00:13 +02:00
gitea_admin 5059bd741a Fix open lot qualification query 2026-09-08 16:57:56 +02:00
gitea_admin 8846a57b43 Fix virtual shortage lot activation and warning 2026-09-08 16:55:37 +02:00
gitea_admin 17efaee53f Finalize open lot qualification and selection 2026-09-08 15:28:01 +02:00
gitea_admin e69eda9953 Keep open lot groups hidden by product 2026-09-08 15:22:33 +02:00
gitea_admin 801695ea89 Add spacing between open lot cards 2026-09-08 15:19:51 +02:00
gitea_admin ba65b05c08 Disable selected open lot action 2026-09-08 15:18:13 +02:00
gitea_admin 15b391f1d6 Bind new lot card to API result 2026-09-08 15:13:06 +02:00
gitea_admin f8f011f96c Keep lot drawer open after creation 2026-09-08 15:08:27 +02:00
gitea_admin a9fa9955d2 Restore lager tab after lot save 2026-09-08 15:03:09 +02:00
gitea_admin f50cf944fe Reset stale open lot confirmation state 2026-09-08 14:57:57 +02:00
gitea_admin 59c953d42f Fix lot drawer state after save 2026-09-08 14:56:46 +02:00
gitea_admin b92c92f5b5 Fix incomplete lot save selection sync 2026-09-08 14:49:33 +02:00
gitea_admin 7daf8b7096 Restore incomplete lot save confirmation 2026-09-08 14:47:37 +02:00
gitea_admin 51a5e630a8 Backfill qualified lot activation selection 2026-09-08 14:41:13 +02:00
gitea_admin 3f15756596 Correct independent stock lot activation flow 2026-09-08 14:38:54 +02:00
gitea_admin 9eb9cd6e0c Fix lot handler PHP syntax 2026-09-08 14:04:47 +02:00
gitea_admin 34967b10cd Fix PHP quoting in lot deletion handler 2026-09-08 14:03:48 +02:00
gitea_admin f38b48dc53 Fix charge creation after deleting open lot 2026-09-08 13:59:18 +02:00
gitea_admin d640f3656d Revert "Add documents to stock lots"
This reverts commit 2bec934732.
2026-09-08 13:49:20 +02:00
gitea_admin f7dcbdf786 Revert "Fix document download environment path"
This reverts commit b3f52f2810.
2026-09-08 13:49:20 +02:00
gitea_admin 5903425f78 Revert "Align lot document upload layout"
This reverts commit a31057f0c0.
2026-09-08 13:49:20 +02:00
gitea_admin a422137997 Revert "Fix lot document card segment structure"
This reverts commit 382d9fe9df.
2026-09-08 13:49:20 +02:00
gitea_admin f83195c9b3 Revert "Separate lot document view and edit segments"
This reverts commit 683327c240.
2026-09-08 13:49:20 +02:00
gitea_admin b9a6d6d8cd Revert "Hide lot document edit segment in view mode"
This reverts commit 0d03de8ff2.
2026-09-08 13:49:20 +02:00
gitea_admin 520b68a433 Revert "Hide lot document editor outside edit mode"
This reverts commit c75e308f45.
2026-09-08 13:49:20 +02:00
gitea_admin 51571f6224 Revert "Align lot document upload fields"
This reverts commit 0826560373.
2026-09-08 13:49:20 +02:00
gitea_admin 6a2b163a51 Revert "Prevent lot document upload overflow"
This reverts commit e9903eec19.
2026-09-08 13:49:20 +02:00
gitea_admin bd219ac463 Revert "Align document comment label column"
This reverts commit f130092d40.
2026-09-08 13:49:20 +02:00
gitea_admin 0b39acbf30 Revert "Remove lot document comments"
This reverts commit 3a3de9da22.
2026-09-08 13:49:20 +02:00
gitea_admin 61fc46df6e Revert "Save lot documents with edit form"
This reverts commit 7491e0b7c5.
2026-09-08 13:49:20 +02:00
gitea_admin 59db50eccd Revert "Restore full-width lot document segment"
This reverts commit 4dda727f03.
2026-09-08 13:49:20 +02:00
gitea_admin c83741fca0 Revert "Fix PHP escaping in lot drawer script"
This reverts commit 8356b545c4.
2026-09-08 13:49:20 +02:00
gitea_admin 2b540b91ed Revert "Resolve lot drawer PHP parse error"
This reverts commit 0867ffa0fa.
2026-09-08 13:49:20 +02:00
gitea_admin 241e4e961c Revert "Separate lot document card segment"
This reverts commit a1f6e2edee.
2026-09-08 13:49:20 +02:00
gitea_admin f4b31af7b8 Revert "Reset document segments after lot save"
This reverts commit 1b752c0830.
2026-09-08 13:49:20 +02:00
gitea_admin 4ca6f2241e Revert "Fix lot document file picker trigger"
This reverts commit 608d91f530.
2026-09-08 13:49:20 +02:00
gitea_admin 879129cb13 Revert "Fix document file picker trigger"
This reverts commit fdcea56c26.
2026-09-08 13:49:20 +02:00
gitea_admin df3f2fa46c Revert "Fix lot document upload completion"
This reverts commit b3c84fae4c.
2026-09-08 13:49:20 +02:00
gitea_admin 7d865e335a Revert "Show selected lot document files"
This reverts commit 99ec2667da.
2026-09-08 13:49:20 +02:00
gitea_admin 2c408f2706 Revert "Complete lot document selection state"
This reverts commit f1631f5755.
2026-09-08 13:49:20 +02:00
gitea_admin 6d8508bf3e Revert "Use compact file removal buttons"
This reverts commit 485882536f.
2026-09-08 13:49:20 +02:00
gitea_admin bb5cd7d039 Revert "Use compact document removal controls"
This reverts commit ae311e4612.
2026-09-08 13:49:20 +02:00
11 changed files with 1047 additions and 417 deletions

No files matched your search

+2
View File
@@ -18,6 +18,8 @@
14. `db/migrations/0018_shopify_delta_sync_state.sql` 14. `db/migrations/0018_shopify_delta_sync_state.sql`
15. `db/migrations/0019_allow_multiple_open_stock_lots.sql` 15. `db/migrations/0019_allow_multiple_open_stock_lots.sql`
16. `db/migrations/0020_include_stock_adjustments_in_balance.sql` 16. `db/migrations/0020_include_stock_adjustments_in_balance.sql`
17. `db/migrations/0021_stock_lot_activation_selection.sql`
18. `db/migrations/0022_backfill_stock_lot_activation_selection.sql`
## Beispielausfuehrung ## Beispielausfuehrung
@@ -0,0 +1,119 @@
BEGIN;
ALTER TABLE stock_lot
ADD COLUMN IF NOT EXISTS next_active BOOLEAN NOT NULL DEFAULT FALSE;
CREATE UNIQUE INDEX IF NOT EXISTS uq_stock_lot_one_next_active_per_product
ON stock_lot(product_id)
WHERE status = 'open' AND next_active = TRUE;
CREATE OR REPLACE FUNCTION fn_auto_switch_lot_when_depleted()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
DECLARE
v_product_id BIGINT;
v_open_lot_id BIGINT;
v_qualified_count INTEGER;
v_current_net NUMERIC;
v_event_key TEXT;
BEGIN
SELECT product_id
INTO v_product_id
FROM stock_lot
WHERE id = NEW.lot_id;
IF NOT EXISTS (
SELECT 1 FROM stock_lot WHERE id = NEW.lot_id AND status = 'current'
) THEN
RETURN NEW;
END IF;
SELECT qty_net
INTO v_current_net
FROM v_stock_lot_balance
WHERE stock_lot_id = NEW.lot_id;
IF COALESCE(v_current_net, 0) > 0 THEN
RETURN NEW;
END IF;
PERFORM 1
FROM stock_lot
WHERE product_id = v_product_id
FOR UPDATE;
IF NOT EXISTS (
SELECT 1 FROM stock_lot WHERE id = NEW.lot_id AND status = 'current'
) THEN
RETURN NEW;
END IF;
SELECT sl.id
INTO v_open_lot_id
FROM stock_lot sl
INNER JOIN v_stock_lot_balance v ON v.stock_lot_id = sl.id
WHERE sl.product_id = v_product_id
AND sl.status = 'open'
AND sl.next_active = TRUE
AND NULLIF(TRIM(sl.lot_number), '') IS NOT NULL
AND sl.manufacturer_party_id IS NOT NULL
AND NULLIF(TRIM(sl.manufacturer_lot_number), '') IS NOT NULL
AND sl.mfg_date IS NOT NULL
AND sl.expiry_date IS NOT NULL
AND sl.purchase_date IS NOT NULL
AND v.qty_net > 0
LIMIT 1;
IF v_open_lot_id IS NULL THEN
SELECT COUNT(*), MIN(sl.id)
INTO v_qualified_count, v_open_lot_id
FROM stock_lot sl
INNER JOIN v_stock_lot_balance v ON v.stock_lot_id = sl.id
WHERE sl.product_id = v_product_id
AND sl.status = 'open'
AND NULLIF(TRIM(sl.lot_number), '') IS NOT NULL
AND sl.manufacturer_party_id IS NOT NULL
AND NULLIF(TRIM(sl.manufacturer_lot_number), '') IS NOT NULL
AND sl.mfg_date IS NOT NULL
AND sl.expiry_date IS NOT NULL
AND sl.purchase_date IS NOT NULL
AND v.qty_net > 0;
IF v_qualified_count <> 1 THEN
RETURN NEW;
END IF;
UPDATE stock_lot
SET next_active = TRUE, updated_at = NOW()
WHERE id = v_open_lot_id;
END IF;
UPDATE stock_lot
SET status = 'closed', updated_at = NOW()
WHERE id = NEW.lot_id;
UPDATE stock_lot
SET status = 'current', next_active = FALSE, updated_at = NOW()
WHERE id = v_open_lot_id;
v_event_key := format('lot.auto_switched:%s:%s', v_product_id, txid_current());
PERFORM fn_enqueue_event(
'lot.auto_switched',
v_event_key,
'stock_lot',
NEW.lot_id::TEXT,
jsonb_build_object(
'productId', v_product_id,
'closedLotId', NEW.lot_id,
'newCurrentLotId', v_open_lot_id,
'occurredAt', NOW()
)
);
RETURN NEW;
END;
$$;
COMMIT;
@@ -1,18 +0,0 @@
BEGIN;
CREATE TABLE stock_lot_document (
id BIGSERIAL PRIMARY KEY,
stock_lot_id BIGINT NOT NULL REFERENCES stock_lot(id) ON DELETE CASCADE,
original_filename TEXT NOT NULL,
stored_filename TEXT NOT NULL,
mime_type TEXT NOT NULL,
size_bytes BIGINT NOT NULL,
comment TEXT,
uploaded_by TEXT NOT NULL,
uploaded_at TIMESTAMP NOT NULL DEFAULT NOW(),
CONSTRAINT chk_stock_lot_document_size CHECK (size_bytes > 0 AND size_bytes <= 52428800)
);
CREATE INDEX idx_stock_lot_document_lot ON stock_lot_document(stock_lot_id, id);
COMMIT;
@@ -0,0 +1,25 @@
BEGIN;
WITH qualified AS (
SELECT
sl.id,
sl.product_id,
COUNT(*) OVER (PARTITION BY sl.product_id) AS qualified_count
FROM stock_lot sl
INNER JOIN v_stock_lot_balance v ON v.stock_lot_id = sl.id
WHERE sl.status = 'open'
AND NULLIF(TRIM(sl.lot_number), '') IS NOT NULL
AND sl.manufacturer_party_id IS NOT NULL
AND NULLIF(TRIM(sl.manufacturer_lot_number), '') IS NOT NULL
AND sl.mfg_date IS NOT NULL
AND sl.expiry_date IS NOT NULL
AND sl.purchase_date IS NOT NULL
AND v.qty_net > 0
)
UPDATE stock_lot sl
SET next_active = TRUE, updated_at = NOW()
FROM qualified q
WHERE sl.id = q.id
AND q.qualified_count = 1;
COMMIT;
@@ -4,7 +4,7 @@ Status: Verbindliche Prozess-Spezifikation
## 1. Zweck ## 1. Zweck
Tägliche Berechnung und Speicherung des voraussichtlichen Leer-Datums jeder aktuellen Charge aktiver Produkte. Tägliche Berechnung und Speicherung des voraussichtlichen Leer-Datums der aktuellen Charge und – sofern vorhanden – der qualifizierten, als nächste Charge markierten offenen Charge aktiver Produkte.
## 2. Prozess-Einbettung ## 2. Prozess-Einbettung
@@ -13,18 +13,18 @@ Direkt ausgeführter Lager-Prozess mit zwei Triggern: synchron nach jeder erfolg
## 3. Input ## 3. Input
- optional eine einzelne `product_id` bei synchronem Verkaufstrigger - optional eine einzelne `product_id` bei synchronem Verkaufstrigger
- aktive Produkte, aktuelle Chargen, eingetragene Mengen aller offenen Folgechargen, physischer Chargenbestand und gültige Verkaufsallokationen aus dem eigenen Datenbestand - aktive Produkte, aktuelle Charge, qualifizierte als nächste Charge markierte offene Charge, physische Chargenbestände und gültige Verkaufsallokationen aus dem eigenen Datenbestand
## 4. Exakter Prozessablauf ## 4. Exakter Prozessablauf
1. Bei einem Verkaufstrigger die betroffenen Produkt-IDs aus der erfolgreichen Allokation übernehmen; beim Cron-Trigger alle aktiven Produkte bestimmen. 1. Bei einem Verkaufstrigger die betroffenen Produkt-IDs aus der erfolgreichen Allokation übernehmen; beim Cron-Trigger alle aktiven Produkte bestimmen.
2. Aktive Produkte, ihre aktuelle Charge und die Nettobestände aller bereits angelegten offenen Folgechargen aus `product`, `stock_lot` und `v_stock_lot_balance` laden. Je offene Folgecharge wird nur eine positive Menge berücksichtigt. 2. Aktive Produkte, ihre aktuelle Charge und die qualifizierte, als nächste Charge markierte offene Charge aus `product`, `stock_lot` und `v_stock_lot_balance` laden. Für jede Charge wird nur ihr eigener positiver Nettobestand berücksichtigt.
3. Verkaufsallokationen der letzten 365 Tage aus `sales_order_line_lot_allocation`, `sales_order_line` und `sales_order` laden. 3. Verkaufsallokationen der letzten 365 Tage aus `sales_order_line_lot_allocation`, `sales_order_line` und `sales_order` laden.
4. `allocated`- und `reserved`-Mengen nach Bestell- beziehungsweise OTC-Verkaufsdatum je Produkt und Kalendertag aggregieren. 4. `allocated`- und `reserved`-Mengen nach Bestell- beziehungsweise OTC-Verkaufsdatum je Produkt und Kalendertag aggregieren.
5. Stornierte Auftrags- und Positionsmengen aus der Nachfrage herausrechnen. 5. Stornierte Auftrags- und Positionsmengen aus der Nachfrage herausrechnen.
6. Den gewichteten Tagesabsatz mit einer Halbwertszeit von 60 Tagen berechnen. 6. Den gewichteten Tagesabsatz mit einer Halbwertszeit von 60 Tagen berechnen.
7. Für jede aktuelle Charge das Leer-Datum aus ihrem physischen Bestand zuzüglich der eingetragenen Mengen aller offenen Folgechargen und der gewichteten Nachfrage berechnen. 7. Das Leer-Datum der aktuellen Charge aus ihrem physischen Bestand und der gewichteten Nachfrage berechnen. Das Leer-Datum der nächsten Charge wird mit derselben Nachfrage ab dem Leer-Datum der aktuellen Charge weitergerechnet; bei bereits leerer aktueller Charge beginnt es am aktuellen Datum.
8. Jede erfolgreiche Berechnung in `stock_lot_forecast_history` speichern und den aktuellen Snapshot in `stock_lot` aktualisieren. 8. Jede erfolgreiche Berechnung je Charge in `stock_lot_forecast_history` speichern und den jeweiligen Snapshot in `stock_lot` aktualisieren.
9. Im Cron Fehler einzelner Produkte sammeln und mit den übrigen Produkten fortfahren. 9. Im Cron Fehler einzelner Produkte sammeln und mit den übrigen Produkten fortfahren.
## 5. Batch, Betriebsmodell und Einbettung ## 5. Batch, Betriebsmodell und Einbettung
@@ -37,7 +37,8 @@ Ein normalisiertes JSON-Ergebnis mit Gesamtzahl, erfolgreich verarbeiteten Produ
## 7. Erfolgskriterien ## 7. Erfolgskriterien
- Jede aktuelle Charge erhält täglich einen aktuellen Prognosesnapshot, der eingetragene Mengen aller offenen Folgechargen berücksichtigt. - Jede aktuelle Charge erhält täglich einen Prognosesnapshot nur für ihren eigenen Bestand.
- Eine qualifizierte, als nächste Charge markierte offene Charge erhält einen separaten Prognosesnapshot nur für ihren eigenen Bestand.
- Verkäufe ohne Bestand über virtuelle `reserved`-Allokationen beeinflussen die Nachfrage. - Verkäufe ohne Bestand über virtuelle `reserved`-Allokationen beeinflussen die Nachfrage.
- Die vollständige Berechnungshistorie bleibt erhalten. - Die vollständige Berechnungshistorie bleibt erhalten.
- Ein einzelner Produktfehler verhindert keine weiteren Berechnungen. - Ein einzelner Produktfehler verhindert keine weiteren Berechnungen.
+77 -44
View File
@@ -20,30 +20,6 @@ try {
json_response(405, ['ok' => false, 'error' => 'Method Not Allowed']); json_response(405, ['ok' => false, 'error' => 'Method Not Allowed']);
} }
$contentType = (string) ($_SERVER['CONTENT_TYPE'] ?? '');
if (str_starts_with($contentType, 'multipart/form-data')) {
if (!auth_validate_csrf_token($_POST['csrfToken'] ?? null)) {
json_response(403, ['ok' => false, 'error' => 'Invalid security token']);
}
if (($_POST['action'] ?? '') !== 'upload_document') {
json_response(422, ['ok' => false, 'error' => 'Unsupported action']);
}
$file = $_FILES['document'] ?? null;
if (!is_array($file)) {
throw new InvalidArgumentException('Dokumentdatei fehlt');
}
$document = upload_stock_lot_document(
$pdo,
(int) ($_POST['lotId'] ?? 0),
$file,
(string) ($currentUser['username'] ?? $currentUser['email'] ?? 'unknown')
);
json_response(201, ['ok' => true, 'document' => [
'id' => (int) $document['id'],
'original_filename' => (string) $document['original_filename'],
]]);
}
$jsonInput = file_get_contents('php://input'); $jsonInput = file_get_contents('php://input');
if ($jsonInput === false || trim($jsonInput) === '') { if ($jsonInput === false || trim($jsonInput) === '') {
json_response(400, ['ok' => false, 'error' => 'Empty payload']); json_response(400, ['ok' => false, 'error' => 'Empty payload']);
@@ -87,9 +63,25 @@ try {
$outQty $outQty
); );
$productId = (int) $updatedLot['product_id']; $productId = (int) $updatedLot['product_id'];
if (($updatedLot['status'] ?? '') === 'open') {
sync_open_lot_activation_selection($pdo, $productId);
$updatedLot['next_active'] = is_open_lot_selected_for_activation($pdo, (int) $updatedLot['id']);
} else {
ensure_current_lot_and_reconcile_virtual_shortage(
$pdo,
$productId,
(string) ($currentUser['username'] ?? $currentUser['email'] ?? 'unknown'),
'Verrechnung nach Pflege der aktiven Charge'
);
}
refresh_lager_stock_lot_forecast_for_product($pdo, $productId); refresh_lager_stock_lot_forecast_for_product($pdo, $productId);
$forecast = get_lager_product_forecast($pdo, $productId); $forecast = get_lager_product_forecast($pdo, $productId);
json_response(200, ['ok' => true, 'productId' => $productId, 'lot' => $updatedLot, 'balance' => get_stock_lot_balance($pdo, (int) ($data['lotId'] ?? 0)), 'forecast' => $forecast]); $currentLot = get_current_lot_snapshot($pdo, $productId);
json_response(200, ['ok' => true, 'productId' => $productId, 'lot' => $currentLot, 'balance' => [
'qty_net' => $currentLot['qty_net'],
'qty_in' => $currentLot['qty_in'],
'qty_out' => $currentLot['qty_out'],
], 'forecast' => $forecast]);
} }
if ($action === 'delete_open_lot') { if ($action === 'delete_open_lot') {
@@ -98,21 +90,29 @@ try {
(int) ($data['lotId'] ?? 0), (int) ($data['lotId'] ?? 0),
(string) ($currentUser['username'] ?? $currentUser['email'] ?? 'unknown') (string) ($currentUser['username'] ?? $currentUser['email'] ?? 'unknown')
); );
json_response(200, ['ok' => true, 'productId' => $productId, 'availableQty' => get_lager_product_available_qty($pdo, $productId)]); sync_open_lot_activation_selection($pdo, $productId);
} ensure_current_lot_and_reconcile_virtual_shortage(
$pdo,
if ($action === 'delete_document') { $productId,
delete_stock_lot_document($pdo, (int) ($data['documentId'] ?? 0)); (string) ($currentUser['username'] ?? $currentUser['email'] ?? 'unknown'),
json_response(200, ['ok' => true]); 'Verrechnung nach Loeschung einer offenen Charge'
);
json_response(200, ['ok' => true, 'productId' => $productId, 'currentLot' => get_current_lot_snapshot($pdo, $productId), 'availableQty' => get_lager_product_available_qty($pdo, $productId)]);
} }
if ($action === 'prepare_lot') { if ($action === 'prepare_lot') {
$productId = (int) ($data['productId'] ?? 0); $productId = (int) ($data['productId'] ?? 0);
$lotNumber = trim((string) ($data['lotNumber'] ?? '')); $lotNumber = trim((string) ($data['lotNumber'] ?? ''));
$lotId = (int) ($data['lotId'] ?? 0); $lotId = (int) ($data['lotId'] ?? 0);
if ($lotNumber === '' && $lotId <= 0) { $manufacturerPartyId = $data['manufacturerPartyId'] ?? null;
throw new InvalidArgumentException('Naurua-Chargennummer fehlt'); $manufacturerPartyId = $manufacturerPartyId === null || $manufacturerPartyId === '' ? null : (int) $manufacturerPartyId;
} $manufacturerLotNumber = trim((string) ($data['manufacturerLotNumber'] ?? ''));
$mfgDate = trim((string) ($data['mfgDate'] ?? ''));
$expiryDate = trim((string) ($data['expiryDate'] ?? ''));
$purchaseDate = trim((string) ($data['purchaseDate'] ?? ''));
$receiptQty = isset($data['receiptQty']) && $data['receiptQty'] !== '' ? (float) $data['receiptQty'] : 0.0;
$targetQty = isset($data['targetQty']) && $data['targetQty'] !== '' ? (float) $data['targetQty'] : null;
$pdo->beginTransaction();
if ($lotId > 0) { if ($lotId > 0) {
$lotStmt = $pdo->prepare( $lotStmt = $pdo->prepare(
"SELECT id "SELECT id
@@ -126,38 +126,71 @@ try {
throw new InvalidArgumentException('Offene Charge nicht gefunden'); throw new InvalidArgumentException('Offene Charge nicht gefunden');
} }
} else { } else {
$lotId = get_or_create_open_lot($pdo, $productId); $lotId = create_open_lot($pdo, $productId);
} }
$manufacturerPartyId = $data['manufacturerPartyId'] ?? null;
$manufacturerPartyId = $manufacturerPartyId === null || $manufacturerPartyId === '' ? null : (int) $manufacturerPartyId;
$receiptQty = isset($data['receiptQty']) && $data['receiptQty'] !== '' ? (float) $data['receiptQty'] : 0.0;
$targetQty = isset($data['targetQty']) && $data['targetQty'] !== '' ? (float) $data['targetQty'] : null;
$preparedLot = update_stock_lot_traceability( $preparedLot = update_stock_lot_traceability(
$pdo, $pdo,
$lotId, $lotId,
$manufacturerPartyId, $manufacturerPartyId,
isset($data['manufacturerLotNumber']) ? (string) $data['manufacturerLotNumber'] : null, $manufacturerLotNumber,
isset($data['mfgDate']) ? (string) $data['mfgDate'] : null, $mfgDate,
isset($data['expiryDate']) ? (string) $data['expiryDate'] : null, $expiryDate,
isset($data['purchaseDate']) ? (string) $data['purchaseDate'] : null, $purchaseDate,
(string) ($currentUser['username'] ?? $currentUser['email'] ?? 'unknown'), (string) ($currentUser['username'] ?? $currentUser['email'] ?? 'unknown'),
$receiptQty, $receiptQty,
0.0, 0.0,
0.0, 0.0,
$lotNumber, $lotNumber,
$targetQty, $targetQty,
$lotId > 0 && $lotNumber === '' $lotNumber === ''
); );
sync_open_lot_activation_selection($pdo, $productId);
if (promote_open_lot_when_virtual_shortage_is_current($pdo, $productId, $lotId, (string) ($currentUser['username'] ?? $currentUser['email'] ?? 'unknown'))) {
$preparedLot['status'] = 'current';
$preparedLot['next_active'] = false;
} else {
$preparedLot['next_active'] = is_open_lot_selected_for_activation($pdo, $lotId);
}
refresh_lager_stock_lot_forecast_for_product($pdo, $productId); refresh_lager_stock_lot_forecast_for_product($pdo, $productId);
$forecast = get_lager_product_forecast($pdo, $productId); $forecast = get_lager_product_forecast($pdo, $productId);
$pdo->commit();
json_response(200, ['ok' => true, 'productId' => $productId, 'lot' => $preparedLot, 'balance' => get_stock_lot_balance($pdo, $lotId), 'availableQty' => get_lager_product_available_qty($pdo, $productId), 'forecast' => $forecast]); json_response(200, ['ok' => true, 'productId' => $productId, 'lot' => $preparedLot, 'balance' => get_stock_lot_balance($pdo, $lotId), 'availableQty' => get_lager_product_available_qty($pdo, $productId), 'forecast' => $forecast]);
} }
if ($action === 'select_open_lot') {
$productId = (int) ($data['productId'] ?? 0);
$lotId = (int) ($data['lotId'] ?? 0);
select_open_lot_for_activation($pdo, $productId, $lotId);
ensure_current_lot_and_reconcile_virtual_shortage(
$pdo,
$productId,
(string) ($currentUser['username'] ?? $currentUser['email'] ?? 'unknown'),
'Verrechnung nach Auswahl der naechsten aktiven Charge'
);
refresh_lager_stock_lot_forecast_for_product($pdo, $productId);
$currentLot = get_current_lot_snapshot($pdo, $productId);
json_response(200, ['ok' => true, 'productId' => $productId, 'selectedLotId' => $lotId, 'currentLot' => $currentLot, 'forecast' => get_lager_product_forecast($pdo, $productId)]);
}
json_response(422, ['ok' => false, 'error' => 'Unsupported action']); json_response(422, ['ok' => false, 'error' => 'Unsupported action']);
} catch (JsonException) { } catch (JsonException) {
json_response(400, ['ok' => false, 'error' => 'Invalid JSON payload']); json_response(400, ['ok' => false, 'error' => 'Invalid JSON payload']);
} catch (InvalidArgumentException $error) { } catch (InvalidArgumentException $error) {
json_response(422, ['ok' => false, 'error' => $error->getMessage()]); json_response(422, ['ok' => false, 'error' => $error->getMessage()]);
} catch (Throwable $error) { } catch (Throwable $error) {
if (isset($pdo) && $pdo->inTransaction()) {
$pdo->rollBack();
}
try {
system_write_audit_log($pdo, 'lager_api', 'lager-charge.php', 'request_failed', $currentUser['username'] ?? $currentUser['email'] ?? 'unknown', [], [], [
'action' => $action ?? '',
'product_id' => $data['productId'] ?? null,
'lot_id' => $data['lotId'] ?? null,
'exception_class' => get_class($error),
'exception_message' => $error->getMessage(),
]);
} catch (Throwable) {
error_log('lager-charge API failure audit logging failed');
}
json_response(500, ['ok' => false, 'error' => 'Interner Serverfehler']); json_response(500, ['ok' => false, 'error' => 'Interner Serverfehler']);
} }
+64 -14
View File
@@ -35,15 +35,25 @@ function lager_forecast_load_current_lots(PDO $pdo): array
{ {
$stmt = $pdo->query( $stmt = $pdo->query(
"SELECT p.id AS product_id, p.sku, sl.id AS lot_id, "SELECT p.id AS product_id, p.sku, sl.id AS lot_id,
COALESCE(v.qty_net, 0) + COALESCE(( COALESCE(v.qty_net, 0) AS physical_qty,
SELECT SUM(GREATEST(COALESCE(open_v.qty_net, 0), 0)) next_sl.id AS next_lot_id,
FROM stock_lot open_lot COALESCE(next_v.qty_net, 0) AS next_physical_qty
LEFT JOIN v_stock_lot_balance open_v ON open_v.stock_lot_id = open_lot.id
WHERE open_lot.product_id = p.id AND open_lot.status = 'open'
), 0) AS physical_qty
FROM product p FROM product p
LEFT JOIN stock_lot sl ON sl.product_id = p.id AND sl.status = 'current' 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 LEFT JOIN v_stock_lot_balance v ON v.stock_lot_id = sl.id
LEFT JOIN stock_lot next_sl
ON next_sl.product_id = p.id
AND next_sl.status = 'open'
AND next_sl.next_active = TRUE
AND NULLIF(TRIM(next_sl.lot_number), '') IS NOT NULL
AND next_sl.manufacturer_party_id IS NOT NULL
AND NULLIF(TRIM(next_sl.manufacturer_lot_number), '') IS NOT NULL
AND next_sl.mfg_date IS NOT NULL
AND next_sl.expiry_date IS NOT NULL
AND next_sl.purchase_date IS NOT NULL
LEFT JOIN v_stock_lot_balance next_v
ON next_v.stock_lot_id = next_sl.id
AND next_v.qty_net > 0
WHERE p.status = 'active' WHERE p.status = 'active'
ORDER BY p.id" ORDER BY p.id"
); );
@@ -54,15 +64,25 @@ function lager_forecast_load_current_lot(PDO $pdo, int $productId): ?array
{ {
$stmt = $pdo->prepare( $stmt = $pdo->prepare(
"SELECT p.id AS product_id, p.sku, sl.id AS lot_id, "SELECT p.id AS product_id, p.sku, sl.id AS lot_id,
COALESCE(v.qty_net, 0) + COALESCE(( COALESCE(v.qty_net, 0) AS physical_qty,
SELECT SUM(GREATEST(COALESCE(open_v.qty_net, 0), 0)) next_sl.id AS next_lot_id,
FROM stock_lot open_lot COALESCE(next_v.qty_net, 0) AS next_physical_qty
LEFT JOIN v_stock_lot_balance open_v ON open_v.stock_lot_id = open_lot.id
WHERE open_lot.product_id = p.id AND open_lot.status = 'open'
), 0) AS physical_qty
FROM product p FROM product p
LEFT JOIN stock_lot sl ON sl.product_id = p.id AND sl.status = 'current' 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 LEFT JOIN v_stock_lot_balance v ON v.stock_lot_id = sl.id
LEFT JOIN stock_lot next_sl
ON next_sl.product_id = p.id
AND next_sl.status = 'open'
AND next_sl.next_active = TRUE
AND NULLIF(TRIM(next_sl.lot_number), '') IS NOT NULL
AND next_sl.manufacturer_party_id IS NOT NULL
AND NULLIF(TRIM(next_sl.manufacturer_lot_number), '') IS NOT NULL
AND next_sl.mfg_date IS NOT NULL
AND next_sl.expiry_date IS NOT NULL
AND next_sl.purchase_date IS NOT NULL
LEFT JOIN v_stock_lot_balance next_v
ON next_v.stock_lot_id = next_sl.id
AND next_v.qty_net > 0
WHERE p.status = 'active' AND p.id = :product_id WHERE p.status = 'active' AND p.id = :product_id
LIMIT 1" LIMIT 1"
); );
@@ -104,7 +124,12 @@ function lager_forecast_load_daily_sales(PDO $pdo, DateTimeImmutable $asOfDate,
return $sales; return $sales;
} }
function lager_forecast_calculate(DateTimeImmutable $asOfDate, float $physicalQty, array $dailySales): array function lager_forecast_calculate(
DateTimeImmutable $asOfDate,
float $physicalQty,
array $dailySales,
?DateTimeImmutable $forecastStartDate = null
): array
{ {
$lookbackDays = 365; $lookbackDays = 365;
$halfLifeDays = 60; $halfLifeDays = 60;
@@ -156,8 +181,9 @@ function lager_forecast_calculate(DateTimeImmutable $asOfDate, float $physicalQt
? 'high' ? 'high'
: ($salesDays >= 10 ? 'medium' : 'low'); : ($salesDays >= 10 ? 'medium' : 'low');
$daysUntilDepleted = (int) ceil($physicalQty / $dailyDemand); $daysUntilDepleted = (int) ceil($physicalQty / $dailyDemand);
$forecastStartDate ??= $asOfDate;
return [ return [
'forecast_date' => $asOfDate->modify('+' . $daysUntilDepleted . ' days')->format('Y-m-d'), 'forecast_date' => $forecastStartDate->modify('+' . $daysUntilDepleted . ' days')->format('Y-m-d'),
'forecast_status' => 'forecast', 'confidence' => $confidence, 'daily_demand' => $dailyDemand, 'forecast_status' => 'forecast', 'confidence' => $confidence, 'daily_demand' => $dailyDemand,
'sales_units' => $salesUnits, 'sales_days' => $salesDays, 'observed_span_days' => $observedSpanDays, 'sales_units' => $salesUnits, 'sales_days' => $salesDays, 'observed_span_days' => $observedSpanDays,
'lookback_days' => $lookbackDays, 'half_life_days' => $halfLifeDays, 'lookback_days' => $lookbackDays, 'half_life_days' => $halfLifeDays,
@@ -235,6 +261,18 @@ function refresh_lager_stock_lot_forecast_for_product(PDO $pdo, int $productId):
$dailySales[$productId] ?? [] $dailySales[$productId] ?? []
); );
lager_forecast_persist($pdo, $lot, $forecast, $asOfDate); lager_forecast_persist($pdo, $lot, $forecast, $asOfDate);
if ($lot['next_lot_id'] !== null && $forecast['forecast_date'] !== null) {
lager_forecast_persist($pdo, [
'lot_id' => $lot['next_lot_id'],
'product_id' => $productId,
'physical_qty' => $lot['next_physical_qty'],
], lager_forecast_calculate(
$asOfDate,
(float) $lot['next_physical_qty'],
$dailySales[$productId] ?? [],
new DateTimeImmutable($forecast['forecast_date'])
), $asOfDate);
}
} }
function run_lager_stock_lot_forecast(PDO $pdo): array function run_lager_stock_lot_forecast(PDO $pdo): array
@@ -260,6 +298,18 @@ function run_lager_stock_lot_forecast(PDO $pdo): array
try { try {
$forecast = lager_forecast_calculate($asOfDate, (float) $lot['physical_qty'], $dailySales[$productId] ?? []); $forecast = lager_forecast_calculate($asOfDate, (float) $lot['physical_qty'], $dailySales[$productId] ?? []);
lager_forecast_persist($pdo, $lot, $forecast, $asOfDate); lager_forecast_persist($pdo, $lot, $forecast, $asOfDate);
if ($lot['next_lot_id'] !== null && $forecast['forecast_date'] !== null) {
lager_forecast_persist($pdo, [
'lot_id' => $lot['next_lot_id'],
'product_id' => $productId,
'physical_qty' => $lot['next_physical_qty'],
], lager_forecast_calculate(
$asOfDate,
(float) $lot['next_physical_qty'],
$dailySales[$productId] ?? [],
new DateTimeImmutable($forecast['forecast_date'])
), $asOfDate);
}
$result['processed_products']++; $result['processed_products']++;
} catch (Throwable $error) { } catch (Throwable $error) {
$result['failed_products']++; $result['failed_products']++;
+635 -174
View File
@@ -75,13 +75,7 @@ function get_lager_product_overview(PDO $pdo): array
p.name, p.name,
p.sku, p.sku,
p.uom, p.uom,
COALESCE(SUM(v.qty_net), 0) + COALESCE(( COALESCE(v.qty_net, 0) AS available_qty,
SELECT SUM(open_v.qty_net)
FROM stock_lot open_sl
INNER JOIN v_stock_lot_balance open_v ON open_v.stock_lot_id = open_sl.id
WHERE open_sl.product_id = p.id
AND open_sl.status = 'open'
), 0) AS available_qty,
COALESCE(v.qty_net, 0) AS current_qty, COALESCE(v.qty_net, 0) AS current_qty,
COALESCE(v.qty_in, 0) AS qty_in, COALESCE(v.qty_in, 0) AS qty_in,
sl.lot_number, sl.lot_number,
@@ -94,6 +88,11 @@ function get_lager_product_overview(PDO $pdo): array
sl.purchase_date, sl.purchase_date,
sl.sellout_date, sl.sellout_date,
sl.sellout_forecast_status, sl.sellout_forecast_status,
next_sl.id AS next_lot_id,
COALESCE(next_v.qty_net, 0) AS next_qty,
next_sl.sellout_date AS next_sellout_date,
next_sl.sellout_forecast_status AS next_sellout_forecast_status,
next_sl.expiry_date AS next_expiry_date,
COALESCE(( COALESCE((
SELECT SUM(a.qty) SELECT SUM(a.qty)
FROM sales_order_line_lot_allocation a FROM sales_order_line_lot_allocation a
@@ -117,10 +116,25 @@ function get_lager_product_overview(PDO $pdo): array
LEFT JOIN party mp ON mp.id = sl.manufacturer_party_id LEFT JOIN party mp ON mp.id = sl.manufacturer_party_id
LEFT JOIN v_stock_lot_balance v LEFT JOIN v_stock_lot_balance v
ON v.stock_lot_id = sl.id ON v.stock_lot_id = sl.id
LEFT JOIN stock_lot next_sl
ON next_sl.product_id = p.id
AND next_sl.status = 'open'
AND next_sl.next_active = TRUE
AND NULLIF(TRIM(next_sl.lot_number), '') IS NOT NULL
AND next_sl.manufacturer_party_id IS NOT NULL
AND NULLIF(TRIM(next_sl.manufacturer_lot_number), '') IS NOT NULL
AND next_sl.mfg_date IS NOT NULL
AND next_sl.expiry_date IS NOT NULL
AND next_sl.purchase_date IS NOT NULL
LEFT JOIN v_stock_lot_balance next_v
ON next_v.stock_lot_id = next_sl.id
AND next_v.qty_net > 0
WHERE p.status = 'active' WHERE p.status = 'active'
GROUP BY p.id, p.name, p.sku, p.uom, sl.id, sl.lot_number, sl.manufacturer_party_id, GROUP BY p.id, p.name, p.sku, p.uom, sl.id, sl.lot_number, sl.manufacturer_party_id,
sl.manufacturer_lot_number, mp.name, sl.mfg_date, sl.expiry_date, sl.purchase_date, sl.manufacturer_lot_number, mp.name, sl.mfg_date, sl.expiry_date, sl.purchase_date,
sl.sellout_date, sl.sellout_forecast_status, v.qty_net, v.qty_in sl.sellout_date, sl.sellout_forecast_status, v.qty_net, v.qty_in,
next_sl.id, next_v.qty_net, next_sl.sellout_date,
next_sl.sellout_forecast_status, next_sl.expiry_date
ORDER BY p.name ASC, p.id ASC" ORDER BY p.name ASC, p.id ASC"
); );
@@ -143,6 +157,11 @@ function get_lager_product_overview(PDO $pdo): array
'purchase_date' => $row['purchase_date'], 'purchase_date' => $row['purchase_date'],
'sellout_date' => $row['sellout_date'], 'sellout_date' => $row['sellout_date'],
'sellout_forecast_status' => (string) ($row['sellout_forecast_status'] ?? 'no_demand'), 'sellout_forecast_status' => (string) ($row['sellout_forecast_status'] ?? 'no_demand'),
'next_lot_id' => $row['next_lot_id'] === null ? null : (int) $row['next_lot_id'],
'next_qty' => (float) ($row['next_qty'] ?? 0),
'next_sellout_date' => $row['next_sellout_date'],
'next_sellout_forecast_status' => (string) ($row['next_sellout_forecast_status'] ?? 'no_demand'),
'next_expiry_date' => $row['next_expiry_date'],
'virtual_shortage_qty' => (float) ($row['virtual_shortage_qty'] ?? 0), 'virtual_shortage_qty' => (float) ($row['virtual_shortage_qty'] ?? 0),
'virtual_lot_number' => $row['virtual_lot_number'], 'virtual_lot_number' => $row['virtual_lot_number'],
], ],
@@ -205,6 +224,7 @@ function get_lager_product_overview(PDO $pdo): array
sl.mfg_date, sl.mfg_date,
sl.expiry_date, sl.expiry_date,
sl.purchase_date, sl.purchase_date,
sl.next_active,
COALESCE(v.qty_in, 0) AS qty_in, COALESCE(v.qty_in, 0) AS qty_in,
COALESCE(v.qty_net, 0) AS qty_net COALESCE(v.qty_net, 0) AS qty_net
FROM stock_lot sl FROM stock_lot sl
@@ -223,9 +243,10 @@ function get_lager_product_overview(PDO $pdo): array
'manufacturer_name' => $openLot['manufacturer_name'], 'manufacturer_name' => $openLot['manufacturer_name'],
'mfg_date' => $openLot['mfg_date'], 'mfg_date' => $openLot['mfg_date'],
'expiry_date' => $openLot['expiry_date'], 'expiry_date' => $openLot['expiry_date'],
'purchase_date' => $openLot['purchase_date'], 'purchase_date' => $openLot['purchase_date'],
'qty_in' => (float) $openLot['qty_net'], 'qty_in' => (float) $openLot['qty_net'],
'qty_net' => (float) $openLot['qty_net'], 'qty_net' => (float) $openLot['qty_net'],
'next_active' => (bool) $openLot['next_active'],
]; ];
} }
@@ -235,32 +256,6 @@ function get_lager_product_overview(PDO $pdo): array
} }
unset($product); unset($product);
$lotIds = [];
foreach ($products as $product) {
if (!empty($product['lot_id'])) {
$lotIds[] = (int) $product['lot_id'];
}
foreach ($product['historical_lots'] as $lot) {
$lotIds[] = (int) ($lot['lot_id'] ?? 0);
}
foreach ($product['open_lots'] as $lot) {
$lotIds[] = (int) ($lot['lot_id'] ?? 0);
}
}
$documentsByLot = get_stock_lot_documents_by_lot_ids($pdo, $lotIds);
foreach ($products as &$product) {
$product['documents'] = $documentsByLot[(int) ($product['lot_id'] ?? 0)] ?? [];
foreach ($product['historical_lots'] as &$lot) {
$lot['documents'] = $documentsByLot[(int) ($lot['lot_id'] ?? 0)] ?? [];
}
unset($lot);
foreach ($product['open_lots'] as &$lot) {
$lot['documents'] = $documentsByLot[(int) ($lot['lot_id'] ?? 0)] ?? [];
}
unset($lot);
}
unset($product);
return $products; return $products;
} }
@@ -277,127 +272,32 @@ function get_lager_product_available_qty(PDO $pdo, int $productId): float
return (float) $stmt->fetchColumn(); return (float) $stmt->fetchColumn();
} }
function get_stock_lot_documents_by_lot_ids(PDO $pdo, array $lotIds): array
{
$lotIds = array_values(array_unique(array_filter(array_map('intval', $lotIds), static fn (int $id): bool => $id > 0)));
if ($lotIds === []) {
return [];
}
$placeholders = implode(',', array_fill(0, count($lotIds), '?'));
$stmt = $pdo->prepare(
"SELECT id, stock_lot_id, original_filename, mime_type, size_bytes, uploaded_at
FROM stock_lot_document
WHERE stock_lot_id IN ({$placeholders})
ORDER BY stock_lot_id, id"
);
$stmt->execute($lotIds);
$documents = [];
foreach ($stmt->fetchAll() as $row) {
$documents[(int) $row['stock_lot_id']][] = [
'id' => (int) $row['id'],
'original_filename' => (string) $row['original_filename'],
'mime_type' => (string) $row['mime_type'],
'size_bytes' => (int) $row['size_bytes'],
'uploaded_at' => (string) $row['uploaded_at'],
];
}
return $documents;
}
function get_stock_lot_document(PDO $pdo, int $documentId): array
{
$stmt = $pdo->prepare(
'SELECT id, stock_lot_id, original_filename, stored_filename, mime_type, size_bytes
FROM stock_lot_document WHERE id = :id'
);
$stmt->execute([':id' => $documentId]);
$document = $stmt->fetch();
if (!is_array($document)) {
throw new RuntimeException('Dokument nicht gefunden');
}
return $document;
}
function delete_stock_lot_document(PDO $pdo, int $documentId): void
{
$document = get_stock_lot_document($pdo, $documentId);
$statusStmt = $pdo->prepare('SELECT status FROM stock_lot WHERE id = :id');
$statusStmt->execute([':id' => (int) $document['stock_lot_id']]);
$status = $statusStmt->fetchColumn();
if ($status !== 'current' && $status !== 'open') {
throw new InvalidArgumentException('Dokumente vergangener Chargen können nicht gelöscht werden');
}
$stmt = $pdo->prepare('DELETE FROM stock_lot_document WHERE id = :id');
$stmt->execute([':id' => $documentId]);
$path = dirname(__DIR__, 3) . '/runtime/uploads/lager/' . basename((string) $document['stored_filename']);
if (is_file($path) && !unlink($path)) {
throw new RuntimeException('Dokumentdatei konnte nicht gelöscht werden');
}
}
function upload_stock_lot_document(PDO $pdo, int $lotId, array $file, string $uploadedBy): array
{
if ($lotId <= 0 || !is_uploaded_file((string) ($file['tmp_name'] ?? ''))) {
throw new InvalidArgumentException('Ungültige Dokumentdatei');
}
$error = (int) ($file['error'] ?? UPLOAD_ERR_NO_FILE);
if ($error !== UPLOAD_ERR_OK) {
throw new InvalidArgumentException('Dokument konnte nicht hochgeladen werden');
}
$size = (int) ($file['size'] ?? 0);
if ($size <= 0 || $size > 52428800) {
throw new InvalidArgumentException('Dokument darf maximal 50 MB groß sein');
}
$originalFilename = basename((string) ($file['name'] ?? ''));
if ($originalFilename === '') {
throw new InvalidArgumentException('Dateiname fehlt');
}
$lotStmt = $pdo->prepare("SELECT id FROM stock_lot WHERE id = :id AND status IN ('current', 'open')");
$lotStmt->execute([':id' => $lotId]);
if ($lotStmt->fetchColumn() === false) {
throw new InvalidArgumentException('Charge nicht gefunden');
}
$directory = dirname(__DIR__, 3) . '/runtime/uploads/lager';
if (!is_dir($directory) && !mkdir($directory, 0770, true) && !is_dir($directory)) {
throw new RuntimeException('Dokumentenverzeichnis konnte nicht erstellt werden');
}
$storedFilename = bin2hex(random_bytes(16));
$targetPath = $directory . '/' . $storedFilename;
if (!move_uploaded_file((string) $file['tmp_name'], $targetPath)) {
throw new RuntimeException('Dokument konnte nicht gespeichert werden');
}
$mimeType = (new finfo(FILEINFO_MIME_TYPE))->file($targetPath) ?: 'application/octet-stream';
try {
$stmt = $pdo->prepare(
'INSERT INTO stock_lot_document
(stock_lot_id, original_filename, stored_filename, mime_type, size_bytes, uploaded_by)
VALUES (:lot_id, :original_filename, :stored_filename, :mime_type, :size_bytes, :uploaded_by)
RETURNING id'
);
$stmt->execute([
':lot_id' => $lotId,
':original_filename' => $originalFilename,
':stored_filename' => $storedFilename,
':mime_type' => $mimeType,
':size_bytes' => $size,
':uploaded_by' => $uploadedBy,
]);
return get_stock_lot_document($pdo, (int) $stmt->fetchColumn());
} catch (Throwable $error) {
unlink($targetPath);
throw $error;
}
}
function get_lager_product_forecast(PDO $pdo, int $productId): array function get_lager_product_forecast(PDO $pdo, int $productId): array
{ {
$stmt = $pdo->prepare( $stmt = $pdo->prepare(
"SELECT sellout_date, sellout_forecast_status, expiry_date "SELECT stock_lot.sellout_date, stock_lot.sellout_forecast_status, stock_lot.expiry_date,
next_lot.sellout_date AS next_sellout_date,
next_lot.sellout_forecast_status AS next_sellout_forecast_status,
next_lot.expiry_date AS next_expiry_date
FROM stock_lot FROM stock_lot
WHERE product_id = :product_id LEFT JOIN LATERAL (
AND status = 'current' SELECT sl.sellout_date, sl.sellout_forecast_status, sl.expiry_date
FROM stock_lot sl
INNER JOIN v_stock_lot_balance v ON v.stock_lot_id = sl.id
WHERE sl.product_id = stock_lot.product_id
AND sl.status = 'open'
AND sl.next_active = TRUE
AND NULLIF(TRIM(sl.lot_number), '') IS NOT NULL
AND sl.manufacturer_party_id IS NOT NULL
AND NULLIF(TRIM(sl.manufacturer_lot_number), '') IS NOT NULL
AND sl.mfg_date IS NOT NULL
AND sl.expiry_date IS NOT NULL
AND sl.purchase_date IS NOT NULL
AND v.qty_net > 0
LIMIT 1
) next_lot ON TRUE
WHERE stock_lot.product_id = :product_id
AND stock_lot.status = 'current'
LIMIT 1" LIMIT 1"
); );
$stmt->execute([':product_id' => $productId]); $stmt->execute([':product_id' => $productId]);
@@ -409,6 +309,18 @@ function get_lager_product_forecast(PDO $pdo, int $productId): array
'date' => $forecast['sellout_date'], 'date' => $forecast['sellout_date'],
'status' => (string) $forecast['sellout_forecast_status'], 'status' => (string) $forecast['sellout_forecast_status'],
'expiryDate' => $forecast['expiry_date'], 'expiryDate' => $forecast['expiry_date'],
'current' => [
'date' => $forecast['sellout_date'],
'status' => (string) $forecast['sellout_forecast_status'],
'expiryDate' => $forecast['expiry_date'],
],
'next' => $forecast['next_sellout_date'] === null && $forecast['next_expiry_date'] === null
? null
: [
'date' => $forecast['next_sellout_date'],
'status' => (string) ($forecast['next_sellout_forecast_status'] ?? 'no_demand'),
'expiryDate' => $forecast['next_expiry_date'],
],
]; ];
} }
@@ -590,7 +502,7 @@ function update_stock_lot_traceability(
]); ]);
if ($receiptQty > 0 || $adjustmentQty !== 0.0 || $outQty > 0) { if ($receiptQty > 0 || $adjustmentQty !== 0.0 || $outQty > 0) {
book_stock_lot_movements($pdo, (int) $before['product_id'], $lotId, $receiptQty, $adjustmentQty, $outQty); book_stock_lot_movements($pdo, (int) $before['product_id'], $lotId, $receiptQty, $adjustmentQty, $outQty, $changedBy);
} }
if ($startedTransaction) { if ($startedTransaction) {
@@ -606,6 +518,506 @@ function update_stock_lot_traceability(
} }
} }
function is_qualified_open_lot(PDO $pdo, int $lotId): bool
{
$stmt = $pdo->prepare(
"SELECT EXISTS (
SELECT 1
FROM stock_lot sl
INNER JOIN v_stock_lot_balance v ON v.stock_lot_id = sl.id
WHERE sl.id = :lot_id
AND sl.status = 'open'
AND NULLIF(TRIM(sl.lot_number), '') IS NOT NULL
AND sl.manufacturer_party_id IS NOT NULL
AND NULLIF(TRIM(sl.manufacturer_lot_number), '') IS NOT NULL
AND sl.mfg_date IS NOT NULL
AND sl.expiry_date IS NOT NULL
AND sl.purchase_date IS NOT NULL
AND v.qty_net > 0
)"
);
$stmt->execute([':lot_id' => $lotId]);
return (bool) $stmt->fetchColumn();
}
function is_open_lot_selected_for_activation(PDO $pdo, int $lotId): bool
{
$stmt = $pdo->prepare(
"SELECT next_active
FROM stock_lot
WHERE id = :lot_id
AND status = 'open'"
);
$stmt->execute([':lot_id' => $lotId]);
return (bool) $stmt->fetchColumn();
}
function sync_open_lot_activation_selection(PDO $pdo, int $productId): void
{
$startedTransaction = !$pdo->inTransaction();
if ($startedTransaction) {
$pdo->beginTransaction();
}
try {
$lockStmt = $pdo->prepare(
"SELECT id
FROM stock_lot
WHERE product_id = :product_id
AND status = 'open'
FOR UPDATE"
);
$lockStmt->execute([':product_id' => $productId]);
$qualifiedStmt = $pdo->prepare(
"SELECT sl.id
FROM stock_lot sl
INNER JOIN v_stock_lot_balance v ON v.stock_lot_id = sl.id
WHERE sl.product_id = :product_id
AND sl.status = 'open'
AND NULLIF(TRIM(sl.lot_number), '') IS NOT NULL
AND sl.manufacturer_party_id IS NOT NULL
AND NULLIF(TRIM(sl.manufacturer_lot_number), '') IS NOT NULL
AND sl.mfg_date IS NOT NULL
AND sl.expiry_date IS NOT NULL
AND sl.purchase_date IS NOT NULL
AND v.qty_net > 0
ORDER BY sl.id"
);
$qualifiedStmt->execute([':product_id' => $productId]);
$qualifiedIds = array_map('intval', $qualifiedStmt->fetchAll(PDO::FETCH_COLUMN));
if (count($qualifiedIds) === 1) {
$clearStmt = $pdo->prepare(
"UPDATE stock_lot
SET next_active = FALSE, updated_at = NOW()
WHERE product_id = :product_id AND status = 'open'"
);
$clearStmt->execute([':product_id' => $productId]);
$selectStmt = $pdo->prepare(
"UPDATE stock_lot
SET next_active = TRUE, updated_at = NOW()
WHERE id = :lot_id"
);
$selectStmt->execute([':lot_id' => $qualifiedIds[0]]);
} elseif ($qualifiedIds === []) {
$clearStmt = $pdo->prepare(
"UPDATE stock_lot
SET next_active = FALSE, updated_at = NOW()
WHERE product_id = :product_id AND status = 'open'"
);
$clearStmt->execute([':product_id' => $productId]);
} else {
$selectedStmt = $pdo->prepare(
"SELECT id
FROM stock_lot
WHERE product_id = :product_id
AND status = 'open'
AND next_active = TRUE
LIMIT 1
FOR UPDATE"
);
$selectedStmt->execute([':product_id' => $productId]);
$selectedId = $selectedStmt->fetchColumn();
if ($selectedId === false || !in_array((int) $selectedId, $qualifiedIds, true)) {
$clearStmt = $pdo->prepare(
"UPDATE stock_lot
SET next_active = FALSE, updated_at = NOW()
WHERE product_id = :product_id AND status = 'open'"
);
$clearStmt->execute([':product_id' => $productId]);
}
}
if ($startedTransaction) {
$pdo->commit();
}
} catch (Throwable $error) {
if ($startedTransaction && $pdo->inTransaction()) {
$pdo->rollBack();
}
throw $error;
}
}
function promote_open_lot_when_virtual_shortage_is_current(PDO $pdo, int $productId, int $lotId, string $changedBy): bool
{
$startedTransaction = !$pdo->inTransaction();
if ($startedTransaction) {
$pdo->beginTransaction();
}
try {
$realCurrentStmt = $pdo->prepare(
"SELECT id
FROM stock_lot
WHERE product_id = :product_id
AND status = 'current'
LIMIT 1
FOR UPDATE"
);
$realCurrentStmt->execute([':product_id' => $productId]);
$realCurrent = $realCurrentStmt->fetch();
if (is_array($realCurrent)) {
$realCurrentBalanceStmt = $pdo->prepare(
"SELECT COALESCE(qty_net, 0)
FROM v_stock_lot_balance
WHERE stock_lot_id = :lot_id"
);
$realCurrentBalanceStmt->execute([':lot_id' => (int) $realCurrent['id']]);
$realCurrentQty = (float) ($realCurrentBalanceStmt->fetchColumn() ?: 0);
} else {
$realCurrentQty = 0.0;
}
if ($realCurrentQty > 0.0000001) {
if ($startedTransaction) {
$pdo->commit();
}
return false;
}
$currentStmt = $pdo->prepare(
"SELECT id
FROM stock_lot
WHERE product_id = :product_id
AND status = 'virtual_shortage'
ORDER BY id
LIMIT 1
FOR UPDATE"
);
$currentStmt->execute([':product_id' => $productId]);
$virtualShortageLotId = $currentStmt->fetchColumn();
if (!is_qualified_open_lot($pdo, $lotId)) {
if ($startedTransaction) {
$pdo->commit();
}
return false;
}
$lotStmt = $pdo->prepare(
"SELECT id
FROM stock_lot
WHERE id = :lot_id
AND product_id = :product_id
AND status = 'open'
FOR UPDATE"
);
$lotStmt->execute([':lot_id' => $lotId, ':product_id' => $productId]);
if ($lotStmt->fetchColumn() === false) {
if ($startedTransaction) {
$pdo->commit();
}
return false;
}
if (is_array($realCurrent)) {
$closeStmt = $pdo->prepare(
"UPDATE stock_lot
SET status = 'closed', updated_at = NOW()
WHERE id = :lot_id"
);
$closeStmt->execute([':lot_id' => (int) $realCurrent['id']]);
}
$promoteStmt = $pdo->prepare(
"UPDATE stock_lot
SET status = 'current', next_active = FALSE, updated_at = NOW()
WHERE id = :lot_id"
);
$promoteStmt->execute([':lot_id' => $lotId]);
if ($virtualShortageLotId !== false) {
reconcile_virtual_shortage_allocations(
$pdo,
(int) $virtualShortageLotId,
$lotId,
$changedBy,
'Automatische Verrechnung bei Aktivierung einer echten Charge'
);
}
if ($startedTransaction) {
$pdo->commit();
}
return true;
} catch (Throwable $error) {
if ($startedTransaction && $pdo->inTransaction()) {
$pdo->rollBack();
}
throw $error;
}
}
function reconcile_virtual_shortage_allocations(
PDO $pdo,
int $virtualLotId,
int $realLotId,
string $changedBy,
string $reason
): void {
$allocationStmt = $pdo->prepare(
"SELECT id, qty
FROM sales_order_line_lot_allocation
WHERE lot_id = :lot_id
AND allocation_status = 'reserved'
ORDER BY id
FOR UPDATE"
);
$allocationStmt->execute([':lot_id' => $virtualLotId]);
$allocations = $allocationStmt->fetchAll();
foreach ($allocations as $allocation) {
$balanceStmt = $pdo->prepare(
"SELECT COALESCE(qty_net, 0)
FROM v_stock_lot_balance
WHERE stock_lot_id = :lot_id"
);
$balanceStmt->execute([':lot_id' => $realLotId]);
$availableQty = (float) ($balanceStmt->fetchColumn() ?: 0);
if ($availableQty <= 0.0000001) {
break;
}
$replaceQty = min((float) $allocation['qty'], $availableQty);
replace_virtual_lot_allocation(
$pdo,
(int) $allocation['id'],
$realLotId,
$replaceQty,
$changedBy,
$reason
);
}
$remainingStmt = $pdo->prepare(
"SELECT COUNT(*)
FROM sales_order_line_lot_allocation
WHERE lot_id = :lot_id
AND allocation_status = 'reserved'"
);
$remainingStmt->execute([':lot_id' => $virtualLotId]);
if ((int) $remainingStmt->fetchColumn() !== 0) {
return;
}
$closeStmt = $pdo->prepare(
"UPDATE stock_lot
SET status = 'closed', next_active = FALSE, updated_at = NOW()
WHERE id = :lot_id
AND status = 'virtual_shortage'"
);
$closeStmt->execute([':lot_id' => $virtualLotId]);
if ($closeStmt->rowCount() !== 1) {
throw new RuntimeException('Virtuelle Fehlbestandscharge konnte nicht aufgeloest werden');
}
system_write_audit_log($pdo, 'stock_lot', (string) $virtualLotId, 'closed', $changedBy, [
'status' => 'virtual_shortage',
], [
'status' => 'closed',
], [
'module' => 'erp.lager',
'reason' => $reason,
]);
}
function ensure_current_lot_and_reconcile_virtual_shortage(
PDO $pdo,
int $productId,
string $changedBy,
string $reason
): void {
$startedTransaction = !$pdo->inTransaction();
if ($startedTransaction) {
$pdo->beginTransaction();
}
try {
$currentStmt = $pdo->prepare(
"SELECT id
FROM stock_lot
WHERE product_id = :product_id
AND status = 'current'
LIMIT 1
FOR UPDATE"
);
$currentStmt->execute([':product_id' => $productId]);
$currentLotId = $currentStmt->fetchColumn();
if ($currentLotId === false) {
throw new RuntimeException("Keine aktuelle Charge fuer Produkt {$productId} vorhanden");
}
$balanceStmt = $pdo->prepare(
"SELECT COALESCE(qty_net, 0)
FROM v_stock_lot_balance
WHERE stock_lot_id = :lot_id"
);
$balanceStmt->execute([':lot_id' => (int) $currentLotId]);
$currentQty = (float) ($balanceStmt->fetchColumn() ?: 0);
if ($currentQty <= 0.0000001) {
sync_open_lot_activation_selection($pdo, $productId);
$selectedStmt = $pdo->prepare(
"SELECT sl.id
FROM stock_lot sl
INNER JOIN v_stock_lot_balance v ON v.stock_lot_id = sl.id
WHERE sl.product_id = :product_id
AND sl.status = 'open'
AND sl.next_active = TRUE
AND NULLIF(TRIM(sl.lot_number), '') IS NOT NULL
AND sl.manufacturer_party_id IS NOT NULL
AND NULLIF(TRIM(sl.manufacturer_lot_number), '') IS NOT NULL
AND sl.mfg_date IS NOT NULL
AND sl.expiry_date IS NOT NULL
AND sl.purchase_date IS NOT NULL
AND v.qty_net > 0
LIMIT 1"
);
$selectedStmt->execute([':product_id' => $productId]);
$nextLotId = $selectedStmt->fetchColumn();
if ($nextLotId === false) {
$countStmt = $pdo->prepare(
"SELECT COUNT(*), MIN(sl.id)
FROM stock_lot sl
INNER JOIN v_stock_lot_balance v ON v.stock_lot_id = sl.id
WHERE sl.product_id = :product_id
AND sl.status = 'open'
AND NULLIF(TRIM(sl.lot_number), '') IS NOT NULL
AND sl.manufacturer_party_id IS NOT NULL
AND NULLIF(TRIM(sl.manufacturer_lot_number), '') IS NOT NULL
AND sl.mfg_date IS NOT NULL
AND sl.expiry_date IS NOT NULL
AND sl.purchase_date IS NOT NULL
AND v.qty_net > 0"
);
$countStmt->execute([':product_id' => $productId]);
$candidate = $countStmt->fetch(PDO::FETCH_NUM);
if (is_array($candidate) && (int) $candidate[0] === 1) {
$nextLotId = $candidate[1];
}
}
if ($nextLotId !== false && $nextLotId !== null) {
promote_open_lot_when_virtual_shortage_is_current(
$pdo,
$productId,
(int) $nextLotId,
$changedBy
);
}
}
for ($iteration = 0; $iteration < 100; $iteration++) {
$currentStmt->execute([':product_id' => $productId]);
$currentLotId = $currentStmt->fetchColumn();
if ($currentLotId === false) {
throw new RuntimeException("Keine aktuelle Charge fuer Produkt {$productId} vorhanden");
}
$balanceStmt->execute([':lot_id' => (int) $currentLotId]);
$currentQty = (float) ($balanceStmt->fetchColumn() ?: 0);
if ($currentQty <= 0.0000001) {
break;
}
$virtualStmt = $pdo->prepare(
"SELECT id
FROM stock_lot
WHERE product_id = :product_id
AND status = 'virtual_shortage'
ORDER BY id
LIMIT 1
FOR UPDATE"
);
$virtualStmt->execute([':product_id' => $productId]);
$virtualLotId = $virtualStmt->fetchColumn();
if ($virtualLotId === false) {
break;
}
reconcile_virtual_shortage_allocations(
$pdo,
(int) $virtualLotId,
(int) $currentLotId,
$changedBy,
$reason
);
$remainingStmt = $pdo->prepare(
"SELECT COUNT(*)
FROM sales_order_line_lot_allocation
WHERE lot_id = :lot_id
AND allocation_status = 'reserved'"
);
$remainingStmt->execute([':lot_id' => (int) $virtualLotId]);
if ((int) $remainingStmt->fetchColumn() === 0) {
break;
}
}
if ($iteration >= 100) {
throw new RuntimeException("Virtuelle Fehlbestandscharge fuer Produkt {$productId} konnte nicht vollstaendig verrechnet werden");
}
if ($startedTransaction) {
$pdo->commit();
}
} catch (Throwable $error) {
if ($startedTransaction && $pdo->inTransaction()) {
$pdo->rollBack();
}
throw $error;
}
}
function select_open_lot_for_activation(PDO $pdo, int $productId, int $lotId): void
{
if ($productId <= 0 || $lotId <= 0) {
throw new InvalidArgumentException('Nur eine qualifizierte offene Charge kann ausgewaehlt werden');
}
$startedTransaction = !$pdo->inTransaction();
if ($startedTransaction) {
$pdo->beginTransaction();
}
try {
$lotStmt = $pdo->prepare(
"SELECT product_id, status
FROM stock_lot
WHERE id = :lot_id
FOR UPDATE"
);
$lotStmt->execute([':lot_id' => $lotId]);
$lot = $lotStmt->fetch();
if (!is_array($lot) || (int) $lot['product_id'] !== $productId || (string) $lot['status'] !== 'open') {
throw new InvalidArgumentException('Offene Charge nicht gefunden');
}
if (!is_qualified_open_lot($pdo, $lotId)) {
throw new InvalidArgumentException('Nur eine qualifizierte offene Charge kann ausgewaehlt werden');
}
$clearStmt = $pdo->prepare(
"UPDATE stock_lot
SET next_active = FALSE, updated_at = NOW()
WHERE product_id = :product_id AND status = 'open'"
);
$clearStmt->execute([':product_id' => $productId]);
$selectStmt = $pdo->prepare(
"UPDATE stock_lot
SET next_active = TRUE, updated_at = NOW()
WHERE id = :lot_id"
);
$selectStmt->execute([':lot_id' => $lotId]);
if ($startedTransaction) {
$pdo->commit();
}
} catch (Throwable $error) {
if ($startedTransaction && $pdo->inTransaction()) {
$pdo->rollBack();
}
throw $error;
}
}
function get_current_lot_balance_for_update(PDO $pdo, int $productId): array function get_current_lot_balance_for_update(PDO $pdo, int $productId): array
{ {
$stmt = $pdo->prepare( $stmt = $pdo->prepare(
@@ -659,6 +1071,46 @@ function get_stock_lot_balance(PDO $pdo, int $lotId): array
]; ];
} }
function get_current_lot_snapshot(PDO $pdo, int $productId): array
{
$stmt = $pdo->prepare(
"SELECT sl.id, sl.product_id, sl.lot_number, sl.status, sl.next_active,
sl.manufacturer_party_id, sl.manufacturer_lot_number,
mp.name AS manufacturer_name, sl.mfg_date, sl.expiry_date, sl.purchase_date,
COALESCE(v.qty_net, 0) AS qty_net,
COALESCE(v.qty_in, 0) AS qty_in,
COALESCE(v.qty_out, 0) AS qty_out
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
WHERE sl.product_id = :product_id
AND sl.status = 'current'
LIMIT 1"
);
$stmt->execute([':product_id' => $productId]);
$row = $stmt->fetch();
if (!is_array($row)) {
throw new RuntimeException("Keine aktuelle Charge fuer Produkt {$productId} vorhanden");
}
return [
'id' => (int) $row['id'],
'product_id' => (int) $row['product_id'],
'lot_number' => $row['lot_number'],
'status' => (string) $row['status'],
'next_active' => (bool) $row['next_active'],
'manufacturer_party_id' => $row['manufacturer_party_id'] === null ? null : (int) $row['manufacturer_party_id'],
'manufacturer_lot_number' => $row['manufacturer_lot_number'],
'manufacturer_name' => $row['manufacturer_name'],
'mfg_date' => $row['mfg_date'],
'expiry_date' => $row['expiry_date'],
'purchase_date' => $row['purchase_date'],
'qty_net' => (float) $row['qty_net'],
'qty_in' => (float) $row['qty_in'],
'qty_out' => (float) $row['qty_out'],
];
}
function insert_stock_move_out( function insert_stock_move_out(
PDO $pdo, PDO $pdo,
int $productId, int $productId,
@@ -767,7 +1219,8 @@ function book_stock_lot_movements(
int $lotId, int $lotId,
float $receiptQty, float $receiptQty,
float $adjustmentQty, float $adjustmentQty,
float $outQty float $outQty,
string $changedBy
): void { ): void {
$locations = get_default_location_ids($pdo); $locations = get_default_location_ids($pdo);
$balanceStmt = $pdo->prepare( $balanceStmt = $pdo->prepare(
@@ -802,6 +1255,12 @@ function book_stock_lot_movements(
insert_stock_move_adjustment($pdo, $productId, $lotId, $correctionQty, $locations['storage'], null, 'Manuelle Bestandskorrektur via Chargenformular'); insert_stock_move_adjustment($pdo, $productId, $lotId, $correctionQty, $locations['storage'], null, 'Manuelle Bestandskorrektur via Chargenformular');
} }
$balance += $adjustmentQty; $balance += $adjustmentQty;
ensure_current_lot_and_reconcile_virtual_shortage(
$pdo,
$productId,
$changedBy,
'Verrechnung nach manueller Bestandskorrektur'
);
} }
if ($outQty > $balance) { if ($outQty > $balance) {
@@ -809,6 +1268,12 @@ function book_stock_lot_movements(
} }
if ($outQty > 0) { if ($outQty > 0) {
insert_stock_move_out($pdo, $productId, $lotId, $outQty, $locations['storage'], $locations['dispatch'], 'Manueller Lagerabgang via Chargenformular'); insert_stock_move_out($pdo, $productId, $lotId, $outQty, $locations['storage'], $locations['dispatch'], 'Manueller Lagerabgang via Chargenformular');
ensure_current_lot_and_reconcile_virtual_shortage(
$pdo,
$productId,
$changedBy,
'Verrechnung nach manuellem Lagerabgang'
);
} }
} }
@@ -960,23 +1425,8 @@ function get_or_create_virtual_shortage_lot(PDO $pdo, int $productId): int
return (int) $newLotId; return (int) $newLotId;
} }
function get_or_create_open_lot(PDO $pdo, int $productId): int function create_open_lot(PDO $pdo, int $productId): int
{ {
$select = $pdo->prepare(
"SELECT id
FROM stock_lot
WHERE product_id = :product_id
AND status = 'open'
ORDER BY id
LIMIT 1
FOR UPDATE"
);
$select->execute([':product_id' => $productId]);
$lotId = $select->fetchColumn();
if ($lotId !== false) {
return (int) $lotId;
}
$insert = $pdo->prepare( $insert = $pdo->prepare(
"INSERT INTO stock_lot (product_id, status, created_at, updated_at) "INSERT INTO stock_lot (product_id, status, created_at, updated_at)
VALUES (:product_id, 'open', NOW(), NOW()) VALUES (:product_id, 'open', NOW(), NOW())
@@ -1103,8 +1553,7 @@ function replace_virtual_lot_allocation(
$balanceStmt = $pdo->prepare( $balanceStmt = $pdo->prepare(
"SELECT COALESCE(v.qty_net, 0) "SELECT COALESCE(v.qty_net, 0)
FROM v_stock_lot_balance v FROM v_stock_lot_balance v
WHERE v.stock_lot_id = :lot_id WHERE v.stock_lot_id = :lot_id"
FOR UPDATE"
); );
$balanceStmt->execute([':lot_id' => $realLotId]); $balanceStmt->execute([':lot_id' => $realLotId]);
$available = (float) ($balanceStmt->fetchColumn() ?: 0); $available = (float) ($balanceStmt->fetchColumn() ?: 0);
@@ -1227,6 +1676,12 @@ function allocate_components_for_line(
throw new RuntimeException("Allokationsschutz ausgelost fuer Produkt {$productId}"); throw new RuntimeException("Allokationsschutz ausgelost fuer Produkt {$productId}");
} }
ensure_current_lot_and_reconcile_virtual_shortage(
$pdo,
$productId,
'system',
"Verrechnung vor Verkaufsallokation: order={$orderId}:line={$lineNo}"
);
$current = get_current_lot_balance_for_update($pdo, $productId); $current = get_current_lot_balance_for_update($pdo, $productId);
$lotId = $current['lot_id']; $lotId = $current['lot_id'];
$available = $current['qty_net']; $available = $current['qty_net'];
@@ -1248,6 +1703,12 @@ function allocate_components_for_line(
(int) $locations['dispatch'], (int) $locations['dispatch'],
"{$movementSource}:order={$orderId}:line={$lineNo}:product={$productId}" "{$movementSource}:order={$orderId}:line={$lineNo}:product={$productId}"
); );
ensure_current_lot_and_reconcile_virtual_shortage(
$pdo,
$productId,
'system',
"Verrechnung nach Verkaufsabgang: order={$orderId}:line={$lineNo}"
);
$allocationStatus = 'allocated'; $allocationStatus = 'allocated';
} }
+116 -118
View File
@@ -189,6 +189,30 @@ function auth_render_bestellungen_content(array $bestellungenTable): string
function auth_render_lager_table(string $title, array $rows, array $columns, string $tableKey): string function auth_render_lager_table(string $title, array $rows, array $columns, string $tableKey): string
{ {
$renderForecast = static function (?string $date, string $status, ?string $expiryDate, bool $available): array {
if (!$available) {
return ['text' => '-', 'warning' => false, 'mhdWarning' => false];
}
if ($date === null || $date === '') {
return ['text' => 'Keine Prognose', 'warning' => false, 'mhdWarning' => false];
}
$dateObject = new DateTimeImmutable($date);
$text = $dateObject->format('d.m.Y');
if ($status === 'depleted') {
return ['text' => $text . ' (BESTAND ENDE)', 'warning' => true, 'mhdWarning' => false];
}
$warning = false;
$mhdWarning = false;
if ($expiryDate !== null && $expiryDate !== '') {
$expiryObject = new DateTimeImmutable($expiryDate);
$warning = $dateObject >= $expiryObject->modify('-3 months') && $dateObject <= $expiryObject;
$mhdWarning = $dateObject > $expiryObject;
if ($warning || $mhdWarning) {
$text .= ' (MHD: ' . $expiryObject->format('d.m.Y') . ')';
}
}
return ['text' => $text, 'warning' => $warning, 'mhdWarning' => $mhdWarning];
};
$html = []; $html = [];
$html[] = '<article class="sg-card sg-large-table sg-large-table--lager" data-component="large-table" data-lager-table="true" data-lager-table-key="' . auth_escape_html($tableKey) . '" role="table" aria-label="' . auth_escape_html($title) . '">'; $html[] = '<article class="sg-card sg-large-table sg-large-table--lager" data-component="large-table" data-lager-table="true" data-lager-table-key="' . auth_escape_html($tableKey) . '" role="table" aria-label="' . auth_escape_html($title) . '">';
$html[] = '<div class="sg-card-segment sg-card-segment--header sg-card-segment--darkblue sg-large-table__title-segment" data-component-part="large-table-header"><div class="sg-strong">' . auth_escape_html($title) . '</div></div>'; $html[] = '<div class="sg-card-segment sg-card-segment--header sg-card-segment--darkblue sg-large-table__title-segment" data-component-part="large-table-header"><div class="sg-strong">' . auth_escape_html($title) . '</div></div>';
@@ -209,40 +233,31 @@ function auth_render_lager_table(string $title, array $rows, array $columns, str
$value = $tableKey === 'products' $value = $tableKey === 'products'
? (string) max(0, (int) round((float) ($row['available_qty'] ?? 0))) ? (string) max(0, (int) round((float) ($row['available_qty'] ?? 0)))
: (string) ($row['item_code'] ?? ''); : (string) ($row['item_code'] ?? '');
$forecastText = ''; $currentForecast = ['text' => '', 'warning' => false, 'mhdWarning' => false];
$forecastWarning = false; $nextForecast = ['text' => '-', 'warning' => false, 'mhdWarning' => false];
$forecastMhdWarning = false;
if ($tableKey === 'products') { if ($tableKey === 'products') {
$forecastStatus = (string) ($row['sellout_forecast_status'] ?? 'no_demand'); $currentForecast = $renderForecast(
$forecastDate = $row['sellout_date'] ?? null; $row['sellout_date'] ?? null,
if ($forecastDate !== null && $forecastDate !== '') { (string) ($row['sellout_forecast_status'] ?? 'no_demand'),
$forecastDateObject = new DateTimeImmutable((string) $forecastDate); $row['expiry_date'] ?? null,
$forecastText = $forecastDateObject->format('d.m.Y'); true
if ($forecastStatus === 'depleted') { );
$forecastWarning = true; $nextForecast = $renderForecast(
$forecastText .= ' (CHARGE ENDE)'; $row['next_sellout_date'] ?? null,
} elseif (!empty($row['expiry_date'])) { (string) ($row['next_sellout_forecast_status'] ?? 'no_demand'),
$expiryDate = new DateTimeImmutable((string) $row['expiry_date']); $row['next_expiry_date'] ?? null,
$forecastWarning = $forecastDateObject >= $expiryDate->modify('-3 months') $row['next_lot_id'] !== null
&& $forecastDateObject <= $expiryDate; );
$forecastMhdWarning = $forecastDateObject > $expiryDate;
if ($forecastWarning || $forecastMhdWarning) {
$forecastText .= ' (MHD: ' . $expiryDate->format('d.m.Y') . ')';
}
}
} else {
$forecastText = 'Keine Prognose';
}
} }
$nameData = ' data-lager-drawer-name="' . auth_escape_html($name) . '"'; $nameData = ' data-lager-drawer-name="' . auth_escape_html($name) . '"';
$valueData = ' data-lager-drawer-value="' . auth_escape_html($value) . '"'; $valueData = ' data-lager-drawer-value="' . auth_escape_html($value) . '"';
$typeData = ' data-lager-drawer-type="' . auth_escape_html($tableKey === 'products' ? 'Produkt' : 'Artikel') . '"'; $typeData = ' data-lager-drawer-type="' . auth_escape_html($tableKey === 'products' ? 'Produkt' : 'Artikel') . '"';
$detailData = ''; $detailData = '';
if ($tableKey === 'products') { if ($tableKey === 'products') {
$detailData = ' data-lager-drawer-forecast="' . auth_escape_html($forecastText) . '"' $detailData = ' data-lager-drawer-forecast="' . auth_escape_html($currentForecast['text']) . '"'
. ' data-lager-drawer-forecast-status="' . auth_escape_html($forecastStatus) . '"' . ' data-lager-drawer-forecast-status="' . auth_escape_html((string) ($row['sellout_forecast_status'] ?? 'no_demand')) . '"'
. ' data-lager-drawer-forecast-warning="' . ($forecastWarning ? 'true' : 'false') . '"' . ' data-lager-drawer-forecast-warning="' . ($currentForecast['warning'] ? 'true' : 'false') . '"'
. ' data-lager-drawer-forecast-mhd-warning="' . ($forecastMhdWarning ? 'true' : 'false') . '"' . ' data-lager-drawer-forecast-mhd-warning="' . ($currentForecast['mhdWarning'] ? 'true' : 'false') . '"'
. ' data-lager-drawer-current-qty="' . auth_escape_html(auth_format_lager_quantity((float) ($row['current_qty'] ?? 0))) . '"' . ' data-lager-drawer-current-qty="' . auth_escape_html(auth_format_lager_quantity((float) ($row['current_qty'] ?? 0))) . '"'
. ' data-lager-drawer-qty-in="' . auth_escape_html(auth_format_lager_quantity((float) ($row['qty_in'] ?? 0))) . '"' . ' data-lager-drawer-qty-in="' . auth_escape_html(auth_format_lager_quantity((float) ($row['qty_in'] ?? 0))) . '"'
. ' data-lager-drawer-virtual-shortage-qty="' . auth_escape_html(auth_format_lager_quantity((float) ($row['virtual_shortage_qty'] ?? 0))) . '"' . ' data-lager-drawer-virtual-shortage-qty="' . auth_escape_html(auth_format_lager_quantity((float) ($row['virtual_shortage_qty'] ?? 0))) . '"'
@@ -254,7 +269,6 @@ function auth_render_lager_table(string $title, array $rows, array $columns, str
. ' data-lager-drawer-mfg-date="' . auth_escape_html((string) ($row['mfg_date'] ?? '')) . '"' . ' data-lager-drawer-mfg-date="' . auth_escape_html((string) ($row['mfg_date'] ?? '')) . '"'
. ' data-lager-drawer-expiry-date="' . auth_escape_html((string) ($row['expiry_date'] ?? '')) . '"' . ' data-lager-drawer-expiry-date="' . auth_escape_html((string) ($row['expiry_date'] ?? '')) . '"'
. ' data-lager-drawer-purchase-date="' . auth_escape_html((string) ($row['purchase_date'] ?? '')) . '"' . ' data-lager-drawer-purchase-date="' . auth_escape_html((string) ($row['purchase_date'] ?? '')) . '"'
. ' data-lager-drawer-documents="' . auth_escape_html((string) json_encode($row['documents'] ?? [], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) . '"'
. ' data-lager-drawer-virtual-lot-number="' . auth_escape_html((string) ($row['virtual_lot_number'] ?? '')) . '"'; . ' data-lager-drawer-virtual-lot-number="' . auth_escape_html((string) ($row['virtual_lot_number'] ?? '')) . '"';
$openLot = $row['open_lots'][0] ?? []; $openLot = $row['open_lots'][0] ?? [];
$detailData .= ' data-lager-open-lot-id="' . auth_escape_html((string) ($openLot['lot_id'] ?? '')) . '"' $detailData .= ' data-lager-open-lot-id="' . auth_escape_html((string) ($openLot['lot_id'] ?? '')) . '"'
@@ -270,10 +284,15 @@ function auth_render_lager_table(string $title, array $rows, array $columns, str
$html[] = '<div class="sg-large-table__cell" role="cell"><a class="sg-hyperlink" href="#" aria-haspopup="dialog" aria-expanded="false" data-lager-drawer-open="true"' . $nameData . $valueData . $typeData . $detailData . '>' . auth_escape_html($name) . '</a></div>'; $html[] = '<div class="sg-large-table__cell" role="cell"><a class="sg-hyperlink" href="#" aria-haspopup="dialog" aria-expanded="false" data-lager-drawer-open="true"' . $nameData . $valueData . $typeData . $detailData . '>' . auth_escape_html($name) . '</a></div>';
$html[] = '<div class="sg-large-table__cell" role="cell"><a class="sg-hyperlink" href="#" aria-haspopup="dialog" aria-expanded="false" data-lager-drawer-open="true"' . $nameData . $valueData . $typeData . $detailData . '>' . auth_escape_html($value) . '</a></div>'; $html[] = '<div class="sg-large-table__cell" role="cell"><a class="sg-hyperlink" href="#" aria-haspopup="dialog" aria-expanded="false" data-lager-drawer-open="true"' . $nameData . $valueData . $typeData . $detailData . '>' . auth_escape_html($value) . '</a></div>';
if ($tableKey === 'products') { if ($tableKey === 'products') {
$forecastClass = $forecastWarning $forecastClass = $currentForecast['warning']
? ' sg-strong lager-forecast-warning' ? ' sg-strong lager-forecast-warning'
: ($forecastMhdWarning ? ' sg-strong lager-forecast-mhd-warning' : ''); : ($currentForecast['mhdWarning'] ? ' sg-strong lager-forecast-mhd-warning' : '');
$html[] = '<div class="sg-large-table__cell' . $forecastClass . '" role="cell">' . auth_escape_html($forecastText) . '</div>'; $nextForecastClass = $nextForecast['warning']
? ' sg-strong lager-forecast-warning'
: ($nextForecast['mhdWarning'] ? ' sg-strong lager-forecast-mhd-warning' : '');
$html[] = '<div class="sg-large-table__cell' . $forecastClass . '" role="cell">' . auth_escape_html($currentForecast['text']) . '</div>';
$html[] = '<div class="sg-large-table__cell" role="cell">' . auth_escape_html((string) max(0, (int) round((float) ($row['next_qty'] ?? 0)))) . '</div>';
$html[] = '<div class="sg-large-table__cell' . $nextForecastClass . '" role="cell">' . auth_escape_html($nextForecast['text']) . '</div>';
} }
$html[] = '</div>'; $html[] = '</div>';
} }
@@ -298,48 +317,12 @@ function auth_format_lager_quantity(float $value): string
: number_format($value, 2, ',', '.'); : number_format($value, 2, ',', '.');
} }
function auth_render_lager_documents(array $documents, bool $editable, string $scope): string
{
$segmentClass = $editable ? ' sg-card-segment--gray' : '';
$segmentState = $editable ? ' hidden data-lager-edit-documents="' . auth_escape_html($scope) . '"' : '';
$html = '<div class="sg-card-segment sg-card-segment--body' . $segmentClass . '" data-lager-documents="' . auth_escape_html($scope) . '"' . $segmentState . '>';
if ($editable) {
$html .= '<div class="sg-form-sections-card-wrapper" data-pattern="form-sections" aria-label="Dokumente"><section class="sg-form-sections-card"><div class="sg-form-sections-card__body"><section class="sg-form-sections-card__chapter"><h2 class="sg-strong sg-form-sections-card__chapter-title">Dokumente</h2>';
} else {
$html .= '<section class="sg-form-sections-card__chapter"><h2 class="sg-strong sg-form-sections-card__chapter-title">Dokumente</h2>';
}
if (!$editable) {
$html .= '<div data-lager-documents-read="' . auth_escape_html($scope) . '">';
if ($documents === []) {
$html .= '<p class="sg-body">Keine Dokumente vorhanden.</p>';
} else {
foreach ($documents as $document) {
$documentId = (int) ($document['id'] ?? 0);
$filename = (string) ($document['original_filename'] ?? 'Dokument');
$html .= '<p class="sg-body" data-lager-document-row="' . $documentId . '"><a class="sg-hyperlink" href="/api/lager-document.php?id=' . $documentId . '" data-component="hyperlink">' . auth_escape_html($filename) . '</a></p>';
}
}
$html .= '</div>';
}
if ($editable) {
$html .= '<div data-lager-documents-edit="' . auth_escape_html($scope) . '">';
foreach ($documents as $document) {
$documentId = (int) ($document['id'] ?? 0);
$filename = (string) ($document['original_filename'] ?? 'Dokument');
$html .= '<div class="sg-form-sections-card__field-group" data-lager-document-edit-row="' . $documentId . '"><span class="sg-body">' . auth_escape_html($filename) . ' <span class="sg-tab-button-group" role="tablist" aria-label="Dokument entfernen" data-component="tab-navigation" data-component-size="compact"><button class="sg-interaction-element sg-button sg-tab-button sg-tab-button--compact" type="button" role="tab" aria-selected="false" data-component-part="tab-button" aria-label="Dokument entfernen: ' . auth_escape_html($filename) . '" data-lager-document-delete="' . $documentId . '">×</button></span></span></div>';
}
$html .= '<div class="sg-file-upload" data-pattern="file-upload" data-lager-document-upload-form="' . auth_escape_html($scope) . '"><div class="sg-file-input" data-component="file-input"><div class="sg-file-upload__dropzone sg-labeled-input-row"><label class="sg-label" for="lager-document-file-' . auth_escape_html($scope) . '">Dateien auswählen</label><input class="sg-file-input__control" id="lager-document-file-' . auth_escape_html($scope) . '" type="file" multiple aria-describedby="lager-document-file-status-' . auth_escape_html($scope) . '" data-lager-document-file><button class="sg-interaction-element sg-button sg-button--active sg-file-input__trigger" type="button" data-lager-document-file-trigger>Dateien auswählen</button></div></div><ul class="sg-file-upload__files" data-lager-document-selected-files hidden></ul><p class="sg-body sg-file-upload__status" id="lager-document-file-status-' . auth_escape_html($scope) . '" data-lager-document-status aria-live="polite">Noch keine Dateien ausgewählt.</p></div>';
$html .= '</div>';
}
return $html . ($editable ? '</section></div></section></div></div>' : '</section></div>');
}
function auth_render_lager_historical_lots(array $lagerProducts): string function auth_render_lager_historical_lots(array $lagerProducts): string
{ {
$html = []; $html = [];
foreach ($lagerProducts as $product) { foreach ($lagerProducts as $product) {
$productId = (int) ($product['id'] ?? 0); $productId = (int) ($product['id'] ?? 0);
$html[] = '<div data-lager-historical-product="' . $productId . '" hidden>'; $html[] = '<div data-lager-historical-product="' . $productId . '" hidden><div class="sg-content-group-box" data-component="content-group-box">';
foreach (($product['historical_lots'] ?? []) as $lot) { foreach (($product['historical_lots'] ?? []) as $lot) {
$qtyNet = (float) ($lot['qty_net'] ?? 0); $qtyNet = (float) ($lot['qty_net'] ?? 0);
$emptyDate = $qtyNet <= 0 ? auth_format_lager_date($lot['last_out_at'] ?? null) : 'Nicht eindeutig'; $emptyDate = $qtyNet <= 0 ? auth_format_lager_date($lot['last_out_at'] ?? null) : 'Nicht eindeutig';
@@ -355,11 +338,9 @@ function auth_render_lager_historical_lots(array $lagerProducts): string
. '<strong>MHD:</strong> ' . auth_format_lager_date($lot['expiry_date'] ?? null) . '<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:</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>' . '<strong>Hersteller-Chargennummer:</strong> ' . auth_escape_html((string) ($lot['manufacturer_lot_number'] ?? 'Nicht hinterlegt')) . '</p>'
. '</div>' . '</div></article>';
. auth_render_lager_documents($lot['documents'] ?? [], false, 'historical-' . (int) ($lot['lot_id'] ?? 0))
. '</article>';
} }
$html[] = '</div>'; $html[] = '</div></div>';
} }
return implode('', $html); return implode('', $html);
@@ -370,12 +351,22 @@ function auth_render_lager_open_lots(array $lagerProducts, string $manufacturerO
$html = []; $html = [];
foreach ($lagerProducts as $product) { foreach ($lagerProducts as $product) {
$productId = (int) ($product['id'] ?? 0); $productId = (int) ($product['id'] ?? 0);
$html[] = '<div data-lager-open-product="' . $productId . '" hidden>'; $html[] = '<div data-lager-open-product="' . $productId . '" hidden><div class="sg-content-group-box" data-component="content-group-box" data-lager-open-lot-list>';
foreach (($product['open_lots'] ?? []) as $lot) { foreach (($product['open_lots'] ?? []) as $lot) {
$lotId = (int) ($lot['lot_id'] ?? 0); $lotId = (int) ($lot['lot_id'] ?? 0);
$html[] = '<div class="sg-delete-confirmation-pattern__stage sg-delete-confirmation-pattern__host" data-pattern="overlay-card" data-dialog-open="false" data-lager-open-card-wrapper="' . $lotId . '"><article class="sg-card sg-card--content-card sg-delete-confirmation-pattern__target" data-component="content-card" data-pattern="overlay-card" aria-label="Offene Charge">' $selectedClass = !empty($lot['next_active']) ? ' sg-lager-open-lot--next-active' : '';
. '<div class="sg-card-segment sg-card-segment--header sg-card-segment--darkgreen sg-object-card__header" data-component-part="card-header"><div class="sg-strong sg-text-on-dark">Offene Charge</div><div class="sg-component-row"><button class="sg-interaction-element sg-button sg-button--warning" type="button" data-component="button" data-component-variant="warning" data-lager-open-delete>Löschen</button><button class="sg-interaction-element sg-button sg-button--active" type="button" data-lager-open-edit-open>Bearbeiten</button></div></div>' $selectedText = !empty($lot['next_active']) ? 'Als nächste aktive Charge ausgewählt' : 'Nicht ausgewählt';
. '<div class="sg-card-segment sg-card-segment--body" data-component-part="card-body" data-lager-open-card="' . (int) ($lot['lot_id'] ?? 0) . '"><p class="sg-body"><strong>Naurua-Chargennummer:</strong> <span data-lager-open-card-lot-number>' . auth_escape_html((string) ($lot['lot_number'] ?? 'Nicht hinterlegt')) . '</span><br>' $isQualified = trim((string) ($lot['lot_number'] ?? '')) !== ''
&& (int) ($lot['manufacturer_party_id'] ?? 0) > 0
&& trim((string) ($lot['manufacturer_lot_number'] ?? '')) !== ''
&& trim((string) ($lot['mfg_date'] ?? '')) !== ''
&& trim((string) ($lot['expiry_date'] ?? '')) !== ''
&& trim((string) ($lot['purchase_date'] ?? '')) !== ''
&& (float) ($lot['qty_net'] ?? 0) > 0;
$selectButton = '<button class="sg-interaction-element sg-button sg-button--active" type="button" data-lager-open-select data-lager-open-selectable="' . ($isQualified ? 'true' : 'false') . '"' . ($isQualified && empty($lot['next_active']) ? '' : ' disabled aria-disabled="true"') . '>Als nächste aktive auswählen</button>';
$html[] = '<div class="sg-delete-confirmation-pattern__stage sg-delete-confirmation-pattern__host' . $selectedClass . '" data-pattern="overlay-card" data-dialog-open="false" data-lager-open-card-wrapper="' . $lotId . '"><article class="sg-card sg-card--content-card sg-delete-confirmation-pattern__target" data-component="content-card" data-pattern="overlay-card" aria-label="Offene Charge">'
. '<div class="sg-card-segment sg-card-segment--header sg-card-segment--darkgreen sg-object-card__header" data-component-part="card-header"><div class="sg-strong sg-text-on-dark">Offene Charge</div><div class="sg-component-row">' . $selectButton . '<button class="sg-interaction-element sg-button sg-button--warning" type="button" data-component="button" data-component-variant="warning" data-lager-open-delete>Löschen</button><button class="sg-interaction-element sg-button sg-button--active" type="button" data-lager-open-edit-open>Bearbeiten</button></div></div>'
. '<div class="sg-card-segment sg-card-segment--body" data-component-part="card-body" data-lager-open-card="' . (int) ($lot['lot_id'] ?? 0) . '"><p class="sg-body"><strong>Auswahl:</strong> <span data-lager-open-card-next-active>' . auth_escape_html($selectedText) . '</span><br><strong>Naurua-Chargennummer:</strong> <span data-lager-open-card-lot-number>' . auth_escape_html((string) ($lot['lot_number'] ?? 'Nicht hinterlegt')) . '</span><br>'
. '<strong>Status:</strong> Vorbereitet<br>' . '<strong>Status:</strong> Vorbereitet<br>'
. '<strong>Bestand:</strong> <span data-lager-open-card-qty-net>' . auth_format_lager_quantity((float) ($lot['qty_net'] ?? 0)) . '</span><br>' . '<strong>Bestand:</strong> <span data-lager-open-card-qty-net>' . auth_format_lager_quantity((float) ($lot['qty_net'] ?? 0)) . '</span><br>'
. '<strong>Eingangsmenge:</strong> <span data-lager-open-card-qty-in>' . auth_format_lager_quantity((float) ($lot['qty_in'] ?? 0)) . '</span><br>' . '<strong>Eingangsmenge:</strong> <span data-lager-open-card-qty-in>' . auth_format_lager_quantity((float) ($lot['qty_in'] ?? 0)) . '</span><br>'
@@ -384,8 +375,7 @@ function auth_render_lager_open_lots(array $lagerProducts, string $manufacturerO
. '<strong>Herstellungsdatum:</strong> <span data-lager-open-card-mfg-date>' . auth_format_lager_date($lot['mfg_date'] ?? null) . '</span><br>' . '<strong>Herstellungsdatum:</strong> <span data-lager-open-card-mfg-date>' . auth_format_lager_date($lot['mfg_date'] ?? null) . '</span><br>'
. '<strong>MHD:</strong> <span data-lager-open-card-expiry-date>' . auth_format_lager_date($lot['expiry_date'] ?? null) . '</span><br>' . '<strong>MHD:</strong> <span data-lager-open-card-expiry-date>' . auth_format_lager_date($lot['expiry_date'] ?? null) . '</span><br>'
. '<strong>Einkaufsdatum:</strong> <span data-lager-open-card-purchase-date>' . auth_format_lager_date($lot['purchase_date'] ?? null) . '</span></p></div>' . '<strong>Einkaufsdatum:</strong> <span data-lager-open-card-purchase-date>' . auth_format_lager_date($lot['purchase_date'] ?? null) . '</span></p></div>'
. auth_render_lager_documents($lot['documents'] ?? [], false, 'open-read-' . $lotId) . '<div class="sg-card-segment sg-card-segment--body sg-card-segment--gray" data-lager-open-edit-form hidden><div class="sg-form-sections-card-wrapper" data-pattern="form-sections" aria-label="Offene Charge bearbeiten"><form class="sg-form-sections-card" data-lager-open-edit-form-element><div class="sg-form-sections-card__body" data-pattern-part="form-body"><h2 class="sg-strong sg-form-sections-card__title">Offene Charge bearbeiten</h2>'
. '<div class="sg-card-segment sg-card-segment--body sg-card-segment--gray" data-lager-open-edit-form hidden><div class="sg-form-sections-card-wrapper" data-pattern="form-sections" aria-label="Offene Charge bearbeiten"><form class="sg-form-sections-card" id="lager-open-edit-form-' . $lotId . '" data-lager-open-edit-form-element><div class="sg-form-sections-card__body" data-pattern-part="form-body"><h2 class="sg-strong sg-form-sections-card__title">Offene Charge bearbeiten</h2>'
. '<section class="sg-form-sections-card__chapter" aria-labelledby="lager-open-charge-data-' . $lotId . '"><h2 id="lager-open-charge-data-' . $lotId . '" class="sg-strong sg-form-sections-card__chapter-title">Chargendaten</h2>' . '<section class="sg-form-sections-card__chapter" aria-labelledby="lager-open-charge-data-' . $lotId . '"><h2 id="lager-open-charge-data-' . $lotId . '" class="sg-strong sg-form-sections-card__chapter-title">Chargendaten</h2>'
. '<div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Naurua-Chargennummer</span><input class="sg-interaction-element sg-input-single-line" type="text" data-lager-open-lot-number value="' . auth_escape_html((string) ($lot['lot_number'] ?? '')) . '"></label></div>' . '<div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Naurua-Chargennummer</span><input class="sg-interaction-element sg-input-single-line" type="text" data-lager-open-lot-number value="' . auth_escape_html((string) ($lot['lot_number'] ?? '')) . '"></label></div>'
. '<div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Hersteller</span><select class="sg-interaction-element sg-input-single-line" data-lager-open-manufacturer>' . str_replace('value="' . (int) ($lot['manufacturer_party_id'] ?? 0) . '"', 'value="' . (int) ($lot['manufacturer_party_id'] ?? 0) . '" selected', $manufacturerOptions) . '</select></label></div>' . '<div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Hersteller</span><select class="sg-interaction-element sg-input-single-line" data-lager-open-manufacturer>' . str_replace('value="' . (int) ($lot['manufacturer_party_id'] ?? 0) . '"', 'value="' . (int) ($lot['manufacturer_party_id'] ?? 0) . '" selected', $manufacturerOptions) . '</select></label></div>'
@@ -394,14 +384,11 @@ function auth_render_lager_open_lots(array $lagerProducts, string $manufacturerO
. '<div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">MHD</span><input class="sg-interaction-element sg-input-single-line" type="date" data-lager-open-expiry-date value="' . auth_escape_html((string) ($lot['expiry_date'] ?? '')) . '"></label></div>' . '<div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">MHD</span><input class="sg-interaction-element sg-input-single-line" type="date" data-lager-open-expiry-date value="' . auth_escape_html((string) ($lot['expiry_date'] ?? '')) . '"></label></div>'
. '<div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Einkaufsdatum</span><input class="sg-interaction-element sg-input-single-line" type="date" data-lager-open-purchase-date value="' . auth_escape_html((string) ($lot['purchase_date'] ?? '')) . '"></label></div>' . '<div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Einkaufsdatum</span><input class="sg-interaction-element sg-input-single-line" type="date" data-lager-open-purchase-date value="' . auth_escape_html((string) ($lot['purchase_date'] ?? '')) . '"></label></div>'
. '<div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Menge</span><input class="sg-interaction-element sg-input-single-line" type="number" min="0" step="1" inputmode="numeric" data-lager-open-target-qty value="' . auth_escape_html((string) ($lot['qty_net'] ?? 0)) . '"></label></div>' . '<div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Menge</span><input class="sg-interaction-element sg-input-single-line" type="number" min="0" step="1" inputmode="numeric" data-lager-open-target-qty value="' . auth_escape_html((string) ($lot['qty_net'] ?? 0)) . '"></label></div>'
. '</section></div></form></div></div>' . '</section><footer class="sg-form-sections-card__actions-segment" data-pattern-part="form-actions-segment"><div class="sg-form-sections-card__actions" data-pattern-part="form-actions"><button class="sg-interaction-element sg-button sg-button--active sg-form-sections-card__action" type="button" data-lager-open-edit-close>Abbrechen</button><button class="sg-interaction-element sg-button sg-button--process sg-form-sections-card__action" type="submit">Speichern</button></div></footer><p class="sg-body" data-lager-open-edit-status aria-live="polite"></p></div></form></div></div></article>'
. auth_render_lager_documents($lot['documents'] ?? [], true, 'open-edit-' . $lotId)
. '<footer class="sg-card-segment sg-card-segment--body sg-card-segment--gray" data-lager-open-edit-actions hidden><div class="sg-form-sections-card-wrapper" data-pattern="form-sections"><div class="sg-form-sections-card"><div class="sg-form-sections-card__actions" data-pattern-part="form-actions"><button class="sg-interaction-element sg-button sg-button--active sg-form-sections-card__action" type="button" data-lager-open-edit-close>Abbrechen</button><button class="sg-interaction-element sg-button sg-button--process sg-form-sections-card__action" type="submit" form="lager-open-edit-form-' . $lotId . '">Speichern</button></div><p class="sg-body" data-lager-open-edit-status aria-live="polite"></p></div></div></footer>'
. '</article>'
. '<article class="sg-card sg-card--overlay-host sg-delete-confirmation-pattern__floating-card" aria-label="Leere Felder bestätigen" role="dialog" aria-modal="true" aria-labelledby="lager-open-confirmation-title-' . $lotId . '" data-lager-open-confirmation-card hidden><div class="sg-card-segment sg-card-segment--body sg-delete-confirmation-pattern__body"><p class="sg-body sg-delete-confirmation-pattern__text" id="lager-open-confirmation-title-' . $lotId . '"><strong>Leere Felder speichern?</strong></p><p class="sg-body sg-delete-confirmation-pattern__text">Die folgenden Felder werden leer gespeichert:</p><div class="sg-delete-confirmation-pattern__list" data-lager-open-confirmation-empty-fields aria-label="Leer zu speichernde Felder"></div><div class="sg-delete-confirmation-pattern__actions"><button class="sg-interaction-element sg-button sg-button--active" type="button" data-lager-open-confirmation-cancel>Abbrechen</button><button class="sg-interaction-element sg-button sg-button--process" type="button" data-lager-open-confirmation-submit>Speichern</button></div></div></article>' . '<article class="sg-card sg-card--overlay-host sg-delete-confirmation-pattern__floating-card" aria-label="Leere Felder bestätigen" role="dialog" aria-modal="true" aria-labelledby="lager-open-confirmation-title-' . $lotId . '" data-lager-open-confirmation-card hidden><div class="sg-card-segment sg-card-segment--body sg-delete-confirmation-pattern__body"><p class="sg-body sg-delete-confirmation-pattern__text" id="lager-open-confirmation-title-' . $lotId . '"><strong>Leere Felder speichern?</strong></p><p class="sg-body sg-delete-confirmation-pattern__text">Die folgenden Felder werden leer gespeichert:</p><div class="sg-delete-confirmation-pattern__list" data-lager-open-confirmation-empty-fields aria-label="Leer zu speichernde Felder"></div><div class="sg-delete-confirmation-pattern__actions"><button class="sg-interaction-element sg-button sg-button--active" type="button" data-lager-open-confirmation-cancel>Abbrechen</button><button class="sg-interaction-element sg-button sg-button--process" type="button" data-lager-open-confirmation-submit>Speichern</button></div></div></article>'
. '<article class="sg-card sg-card--overlay-host sg-delete-confirmation-pattern__floating-card" aria-label="Löschbestätigung" role="dialog" aria-modal="true" aria-labelledby="lager-open-delete-title-' . $lotId . '" data-overlay-dialog="delete" data-lager-open-delete-confirmation hidden><div class="sg-card-segment sg-card-segment--body sg-delete-confirmation-pattern__body"><p class="sg-body sg-delete-confirmation-pattern__text" id="lager-open-delete-title-' . $lotId . '"><strong>Möchtest du diese offene Charge wirklich löschen?</strong></p><p class="sg-body sg-delete-confirmation-pattern__text">Du kannst das nicht rückgängig machen. Bestätige durch Eingabe von <span class="sg-delete-confirmation-pattern__code">DELETE</span>.</p><label class="sg-labeled-input-row sg-delete-confirmation-pattern__input-row"><span class="sg-label">Bestätigung</span><input class="sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable" type="text" placeholder="DELETE" aria-label="Löschbestätigung durch DELETE" data-lager-open-delete-confirmation-input></label><p class="sg-body" data-lager-open-delete-status aria-live="polite"></p><div class="sg-delete-confirmation-pattern__actions"><button class="sg-interaction-element sg-button sg-button--active" type="button" data-lager-open-delete-confirmation-cancel>Abbrechen</button><button class="sg-interaction-element sg-button sg-button--process sg-button--process-inactive" type="button" disabled aria-disabled="true" data-lager-open-delete-confirmation-submit>Löschen</button></div></div></article></div>'; . '<article class="sg-card sg-card--overlay-host sg-delete-confirmation-pattern__floating-card" aria-label="Löschbestätigung" role="dialog" aria-modal="true" aria-labelledby="lager-open-delete-title-' . $lotId . '" data-overlay-dialog="delete" data-lager-open-delete-confirmation hidden><div class="sg-card-segment sg-card-segment--body sg-delete-confirmation-pattern__body"><p class="sg-body sg-delete-confirmation-pattern__text" id="lager-open-delete-title-' . $lotId . '"><strong>Möchtest du diese offene Charge wirklich löschen?</strong></p><p class="sg-body sg-delete-confirmation-pattern__text">Du kannst das nicht rückgängig machen. Bestätige durch Eingabe von <span class="sg-delete-confirmation-pattern__code">DELETE</span>.</p><label class="sg-labeled-input-row sg-delete-confirmation-pattern__input-row"><span class="sg-label">Bestätigung</span><input class="sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable" type="text" placeholder="DELETE" aria-label="Löschbestätigung durch DELETE" data-lager-open-delete-confirmation-input></label><p class="sg-body" data-lager-open-delete-status aria-live="polite"></p><div class="sg-delete-confirmation-pattern__actions"><button class="sg-interaction-element sg-button sg-button--active" type="button" data-lager-open-delete-confirmation-cancel>Abbrechen</button><button class="sg-interaction-element sg-button sg-button--process sg-button--process-inactive" type="button" disabled aria-disabled="true" data-lager-open-delete-confirmation-submit>Löschen</button></div></div></article></div>';
} }
$html[] = '</div>'; $html[] = '</div></div>';
} }
return implode('', $html); return implode('', $html);
@@ -415,8 +402,24 @@ function auth_render_lager_content(array $lagerProducts, array $lagerArtikel, ar
} }
$tables = $sectionTitle === 'Lager' $tables = $sectionTitle === 'Lager'
? auth_render_lager_table('Produkte', $lagerProducts, ['Produktname', 'Anzahl im Lager', 'Verbraucht bis'], 'products') ? auth_render_lager_table('Produkte', $lagerProducts, ['Produktname', 'Lager aktuell', 'Verkauf bis aktuell', 'Lager neu', 'Verkauf bis neu'], 'products')
: auth_render_lager_table('Artikel', $lagerArtikel, ['Artikel-Name', 'Artikelnummer'], 'articles'); : auth_render_lager_table('Artikel', $lagerArtikel, ['Artikel-Name', 'Artikelnummer'], 'articles');
$openLotCardTemplate = auth_render_lager_open_lots([[
'id' => 0,
'open_lots' => [[
'lot_id' => 0,
'lot_number' => null,
'manufacturer_party_id' => null,
'manufacturer_name' => null,
'manufacturer_lot_number' => null,
'mfg_date' => null,
'expiry_date' => null,
'purchase_date' => null,
'qty_net' => 0,
'qty_in' => 0,
'next_active' => false,
]],
]], $manufacturerOptions);
return '<div data-lager-content="true" data-lager-csrf-token="' . auth_escape_html($csrfToken) . '">' return '<div data-lager-content="true" data-lager-csrf-token="' . auth_escape_html($csrfToken) . '">'
. '<section class="sg-card-list-page" aria-label="' . auth_escape_html($sectionTitle) . '">' . '<section class="sg-card-list-page" aria-label="' . auth_escape_html($sectionTitle) . '">'
@@ -439,7 +442,7 @@ function auth_render_lager_content(array $lagerProducts, array $lagerArtikel, ar
. '<div class="sg-card-segment sg-card-segment--body sg-card-segment--gray"><div class="sg-form-sections-card-wrapper" data-pattern="form-sections" aria-label="Neue Charge anlegen"><form class="sg-form-sections-card" data-lager-new-lot-form-element><div class="sg-form-sections-card__body" data-pattern-part="form-body"><h2 class="sg-strong sg-form-sections-card__title">Neue Charge anlegen</h2>' . '<div class="sg-card-segment sg-card-segment--body sg-card-segment--gray"><div class="sg-form-sections-card-wrapper" data-pattern="form-sections" aria-label="Neue Charge anlegen"><form class="sg-form-sections-card" data-lager-new-lot-form-element><div class="sg-form-sections-card__body" data-pattern-part="form-body"><h2 class="sg-strong sg-form-sections-card__title">Neue Charge anlegen</h2>'
. '<div class="sg-notifications-pattern" data-lager-new-lot-notification-wrapper hidden><article class="sg-card sg-notifications-pattern__card" data-pattern="notification-small" data-component="notification-card"><div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-green sg-notifications-pattern__text-segment sg-notifications-pattern__text-segment--small" data-component-part="card-header"><p class="sg-body" data-lager-new-lot-notification></p></div></article></div>' . '<div class="sg-notifications-pattern" data-lager-new-lot-notification-wrapper hidden><article class="sg-card sg-notifications-pattern__card" data-pattern="notification-small" data-component="notification-card"><div class="sg-card-segment sg-card-segment--header sg-card-segment--signal-green sg-notifications-pattern__text-segment sg-notifications-pattern__text-segment--small" data-component-part="card-header"><p class="sg-body" data-lager-new-lot-notification></p></div></article></div>'
. '<section class="sg-form-sections-card__chapter" aria-labelledby="lager-new-charge-data-title"><h2 id="lager-new-charge-data-title" class="sg-strong sg-form-sections-card__chapter-title">Chargendaten</h2>' . '<section class="sg-form-sections-card__chapter" aria-labelledby="lager-new-charge-data-title"><h2 id="lager-new-charge-data-title" class="sg-strong sg-form-sections-card__chapter-title">Chargendaten</h2>'
. '<div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Naurua-Chargennummer</span><input class="sg-interaction-element sg-input-single-line" type="text" data-lager-new-lot-number required></label></div>' . '<div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Naurua-Chargennummer</span><input class="sg-interaction-element sg-input-single-line" type="text" data-lager-new-lot-number></label></div>'
. '<div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Hersteller</span><select class="sg-interaction-element sg-input-single-line" data-lager-new-manufacturer>' . $manufacturerOptions . '</select></label></div>' . '<div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Hersteller</span><select class="sg-interaction-element sg-input-single-line" data-lager-new-manufacturer>' . $manufacturerOptions . '</select></label></div>'
. '<div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Hersteller-Chargennummer</span><input class="sg-interaction-element sg-input-single-line" type="text" data-lager-new-manufacturer-lot></label></div>' . '<div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Hersteller-Chargennummer</span><input class="sg-interaction-element sg-input-single-line" type="text" data-lager-new-manufacturer-lot></label></div>'
. '<div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Herstellungsdatum</span><input class="sg-interaction-element sg-input-single-line" type="date" data-lager-new-mfg-date></label></div>' . '<div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Herstellungsdatum</span><input class="sg-interaction-element sg-input-single-line" type="date" data-lager-new-mfg-date></label></div>'
@@ -447,13 +450,15 @@ function auth_render_lager_content(array $lagerProducts, array $lagerArtikel, ar
. '<div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Einkaufsdatum</span><input class="sg-interaction-element sg-input-single-line" type="date" data-lager-new-purchase-date></label></div>' . '<div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Einkaufsdatum</span><input class="sg-interaction-element sg-input-single-line" type="date" data-lager-new-purchase-date></label></div>'
. '<div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Eingangsmenge</span><input class="sg-interaction-element sg-input-single-line" type="number" min="0" step="1" inputmode="numeric" data-lager-new-receipt-qty placeholder="Menge"></label></div>' . '<div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Eingangsmenge</span><input class="sg-interaction-element sg-input-single-line" type="number" min="0" step="1" inputmode="numeric" data-lager-new-receipt-qty placeholder="Menge"></label></div>'
. '</section><footer class="sg-form-sections-card__actions-segment" data-pattern-part="form-actions-segment"><div class="sg-form-sections-card__actions" data-pattern-part="form-actions"><button class="sg-interaction-element sg-button sg-button--active sg-form-sections-card__action" type="button" data-lager-new-lot-close>Abbrechen</button><button class="sg-interaction-element sg-button sg-button--process sg-form-sections-card__action" type="submit">Speichern</button></div></footer><p class="sg-body" data-lager-new-lot-status aria-live="polite"></p></div></form></div></div>' . '</section><footer class="sg-form-sections-card__actions-segment" data-pattern-part="form-actions-segment"><div class="sg-form-sections-card__actions" data-pattern-part="form-actions"><button class="sg-interaction-element sg-button sg-button--active sg-form-sections-card__action" type="button" data-lager-new-lot-close>Abbrechen</button><button class="sg-interaction-element sg-button sg-button--process sg-form-sections-card__action" type="submit">Speichern</button></div></footer><p class="sg-body" data-lager-new-lot-status aria-live="polite"></p></div></form></div></div>'
. '<article class="sg-card sg-card--overlay-host sg-delete-confirmation-pattern__floating-card" aria-label="Leere Felder bestätigen" role="dialog" aria-modal="true" aria-labelledby="lager-new-confirmation-title" data-lager-new-confirmation-card hidden><div class="sg-card-segment sg-card-segment--body sg-delete-confirmation-pattern__body"><p class="sg-body sg-delete-confirmation-pattern__text" id="lager-new-confirmation-title"><strong>Leere Felder speichern?</strong></p><p class="sg-body sg-delete-confirmation-pattern__text">Die folgenden Felder werden leer gespeichert:</p><div class="sg-delete-confirmation-pattern__list" data-lager-new-confirmation-empty-fields aria-label="Leer zu speichernde Felder"></div><div class="sg-delete-confirmation-pattern__actions"><button class="sg-interaction-element sg-button sg-button--active" type="button" data-lager-new-confirmation-cancel>Abbrechen</button><button class="sg-interaction-element sg-button sg-button--process" type="button" data-lager-new-confirmation-submit>Speichern</button></div></div></article>'
. '</article>' . '</article>'
. '<template data-lager-open-lot-template>' . $openLotCardTemplate . '</template>'
. '<div data-lager-drawer-topic="open-lots" aria-label="Offene Chargen">' . auth_render_lager_open_lots($lagerProducts, $manufacturerOptions) . '</div>' . '<div data-lager-drawer-topic="open-lots" aria-label="Offene Chargen">' . auth_render_lager_open_lots($lagerProducts, $manufacturerOptions) . '</div>'
. '<article class="sg-card sg-card--content-card" data-component="content-card" data-lager-drawer-topic="lot" aria-label="Aktive Charge">' . '<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 sg-object-card__header" data-component-part="card-header"><div class="sg-strong sg-text-on-dark">Aktive Charge</div><button class="sg-interaction-element sg-button sg-button--active" type="button" data-lager-edit-open>Bearbeiten</button></div>' . '<div class="sg-card-segment sg-card-segment--header sg-card-segment--darkgreen sg-object-card__header" data-component-part="card-header"><div class="sg-strong sg-text-on-dark">Aktive Charge</div><button class="sg-interaction-element sg-button sg-button--active" type="button" data-lager-edit-open>Bearbeiten</button></div>'
. '<div class="sg-card-segment sg-card-segment--body" data-component-part="card-body" data-lager-read-view><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></div>' . auth_render_lager_documents([], false, 'current-read') . '<div class="sg-card-segment sg-card-segment--body" data-component-part="card-body" data-lager-read-view><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></div>'
. '<div class="sg-card-segment sg-card-segment--body sg-card-segment--gray" data-lager-edit-form hidden><div class="sg-form-sections-card-wrapper" data-pattern="form-sections" aria-label="Aktive Charge bearbeiten">' . '<div class="sg-card-segment sg-card-segment--body sg-card-segment--gray" data-lager-edit-form hidden><div class="sg-form-sections-card-wrapper" data-pattern="form-sections" aria-label="Aktive Charge bearbeiten">'
. '<form class="sg-form-sections-card" id="lager-edit-form" data-lager-edit-form-element><div class="sg-form-sections-card__body" data-pattern-part="form-body"><h2 class="sg-strong sg-form-sections-card__title">Aktive Charge bearbeiten</h2>' . '<form class="sg-form-sections-card" data-lager-edit-form-element><div class="sg-form-sections-card__body" data-pattern-part="form-body"><h2 class="sg-strong sg-form-sections-card__title">Aktive Charge bearbeiten</h2>'
. '<section class="sg-form-sections-card__chapter" aria-labelledby="lager-charge-traceability-title"><h2 id="lager-charge-traceability-title" class="sg-strong sg-form-sections-card__chapter-title">Rückverfolgbarkeit</h2>' . '<section class="sg-form-sections-card__chapter" aria-labelledby="lager-charge-traceability-title"><h2 id="lager-charge-traceability-title" class="sg-strong sg-form-sections-card__chapter-title">Rückverfolgbarkeit</h2>'
. '<div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Naurua-Chargennummer</span><input class="sg-interaction-element sg-input-single-line sg-form-inactive-selectable" type="text" data-lager-lot-number readonly></label></div>' . '<div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Naurua-Chargennummer</span><input class="sg-interaction-element sg-input-single-line sg-form-inactive-selectable" type="text" data-lager-lot-number readonly></label></div>'
. '<div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Hersteller</span><select class="sg-interaction-element sg-input-single-line" id="lager-manufacturer" data-lager-manufacturer>' . $manufacturerOptions . '</select></label></div>' . '<div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Hersteller</span><select class="sg-interaction-element sg-input-single-line" id="lager-manufacturer" data-lager-manufacturer>' . $manufacturerOptions . '</select></label></div>'
@@ -466,14 +471,13 @@ function auth_render_lager_content(array $lagerProducts, array $lagerArtikel, ar
. '<div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Zugang buchen</span><input class="sg-interaction-element sg-input-single-line" type="number" min="0" step="0.01" inputmode="decimal" data-lager-receipt-qty placeholder="Menge"></label></div>' . '<div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Zugang buchen</span><input class="sg-interaction-element sg-input-single-line" type="number" min="0" step="0.01" inputmode="decimal" data-lager-receipt-qty placeholder="Menge"></label></div>'
. '<div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Bestandskorrektur (+ / −)</span><input class="sg-interaction-element sg-input-single-line" type="number" step="1" inputmode="numeric" data-lager-adjustment-qty placeholder="+ / − Menge"></label></div>' . '<div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Bestandskorrektur (+ / −)</span><input class="sg-interaction-element sg-input-single-line" type="number" step="1" inputmode="numeric" data-lager-adjustment-qty placeholder="+ / − Menge"></label></div>'
. '<div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Abgang buchen</span><input class="sg-interaction-element sg-input-single-line" type="number" min="0" step="0.01" inputmode="decimal" data-lager-out-qty placeholder="Menge"></label></div>' . '<div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Abgang buchen</span><input class="sg-interaction-element sg-input-single-line" type="number" min="0" step="0.01" inputmode="decimal" data-lager-out-qty placeholder="Menge"></label></div>'
. '</section></div></form></div></div>' . '</section><footer class="sg-form-sections-card__actions-segment" data-pattern-part="form-actions-segment"><div class="sg-form-sections-card__actions" data-pattern-part="form-actions"><button class="sg-interaction-element sg-button sg-button--active sg-form-sections-card__action" type="button" data-lager-edit-close>Abbrechen</button><button class="sg-interaction-element sg-button sg-button--process sg-form-sections-card__action" type="submit">Speichern</button></div></footer><p class="sg-body" data-lager-edit-status aria-live="polite"></p></div></form>'
. auth_render_lager_documents([], true, 'current-edit') . '</div></div>'
. '<footer class="sg-card-segment sg-card-segment--body sg-card-segment--gray" data-lager-edit-actions hidden><div class="sg-form-sections-card-wrapper" data-pattern="form-sections"><div class="sg-form-sections-card"><div class="sg-form-sections-card__actions" data-pattern-part="form-actions"><button class="sg-interaction-element sg-button sg-button--active sg-form-sections-card__action" type="button" data-lager-edit-close>Abbrechen</button><button class="sg-interaction-element sg-button sg-button--process sg-form-sections-card__action" type="submit" form="lager-edit-form">Speichern</button></div><p class="sg-body" data-lager-edit-status aria-live="polite"></p></div></div></footer>'
. '</article>' . '</article>'
. '<div data-lager-drawer-topic="historical-lots" aria-label="Vergangene Chargen">' . auth_render_lager_historical_lots($lagerProducts) . '</div>' . '<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">' . '<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--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>' . '<div class="sg-card-segment sg-card-segment--body" data-component-part="card-body"><p class="sg-body"><span data-lager-drawer-virtual-lot-value></span><br><span data-lager-drawer-virtual-lot-qty></span></p></div>'
. '</article>' . '</article>'
. '</div>' . '</div>'
. '</aside>' . '</aside>'
@@ -846,55 +850,49 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
echo " restoreLagerMobileDetail();"; echo " restoreLagerMobileDetail();";
echo " };"; echo " };";
echo " const getDrawer = () => document.querySelector('[data-lager-mobile-drawer]') || contentRoot.querySelector('[data-lager-drawer]');"; echo " const getDrawer = () => document.querySelector('[data-lager-mobile-drawer]') || contentRoot.querySelector('[data-lager-drawer]');";
echo " document.addEventListener('click', (event) => { const trigger = event.target.closest('[data-lager-drawer-open][data-lager-drawer-type=\"Produkt\"]'); if (!trigger) { return; } const drawer = getDrawer(); const lotValue = drawer?.querySelector('[data-lager-drawer-virtual-lot-value]'); const qtyValue = drawer?.querySelector('[data-lager-drawer-virtual-lot-qty]'); if (lotValue) { lotValue.textContent = trigger.dataset.lagerDrawerVirtualLotNumber ? 'Virtuelle Charge: ' + trigger.dataset.lagerDrawerVirtualLotNumber : 'Keine virtuelle Fehlbestandscharge vorhanden.'; } if (qtyValue) { qtyValue.textContent = 'Bereits virtuell verkauft: ' + (trigger.dataset.lagerDrawerVirtualShortageQty || '0'); } });";
echo " const setDrawerState = (open) => {"; echo " const setDrawerState = (open) => {";
echo " const drawer = getDrawer(); const backdrop = contentRoot.querySelector('[data-lager-drawer-backdrop]');"; echo " const drawer = getDrawer(); const backdrop = contentRoot.querySelector('[data-lager-drawer-backdrop]');";
echo " if (!drawer || !backdrop) { return; }"; 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)); document.body.classList.toggle('sg-card-list-page-drawer-open', open);"; echo " drawer.dataset.open = String(open); drawer.setAttribute('aria-hidden', String(!open)); backdrop.dataset.open = String(open); backdrop.setAttribute('aria-hidden', String(!open)); document.body.classList.toggle('sg-card-list-page-drawer-open', open);";
echo " };"; echo " };";
echo " const getLagerWarningTitle = (trigger) => trigger.dataset.lagerDrawerForecastStatus === 'depleted' ? 'Warnung: Charge ist aufgebraucht' : (trigger.dataset.lagerDrawerForecastMhdWarning === 'true' ? 'Warnung: MHD wird vor Chargenende erreicht' : (trigger.dataset.lagerDrawerForecastWarning === 'true' ? 'Warnung: Charge wird innerhalb von drei Monaten aufgebraucht sein' : 'Keine Warnung'));"; echo " const getLagerWarningTitle = (trigger) => trigger.dataset.lagerDrawerForecastStatus === 'depleted' ? 'Warnung: Bestand ist aufgebraucht' : (trigger.dataset.lagerDrawerForecastMhdWarning === 'true' ? 'Warnung: MHD wird vor Bestandsende erreicht' : (trigger.dataset.lagerDrawerForecastWarning === 'true' ? 'Warnung: Bestand wird innerhalb von drei Monaten aufgebraucht sein' : 'Keine Warnung'));";
echo " const escapeLagerHtml = (value) => String(value ?? '').replace(/[&<>\"']/g, (character) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' }[character] || character));";
echo " const renderLagerDocumentRows = (documents, editable) => { if (!documents.length) { return '<p class=\"sg-body\">Keine Dokumente vorhanden.</p>'; } return documents.map((document) => { const id = Number(document.id || 0); const filename = escapeLagerHtml(document.original_filename || 'Dokument'); if (!editable) { return '<p class=\"sg-body\" data-lager-document-row=\"' + id + '\"><a class=\"sg-hyperlink\" href=\"/api/lager-document.php?id=' + id + '\" data-component=\"hyperlink\">' + filename + '</a></p>'; } return '<div class=\"sg-form-sections-card__field-group\" data-lager-document-edit-row=\"' + id + '\"><span class=\"sg-body\">' + filename + ' <span class=\"sg-tab-button-group\" role=\"tablist\" aria-label=\"Dokument entfernen\" data-component=\"tab-navigation\" data-component-size=\"compact\"><button class=\"sg-interaction-element sg-button sg-tab-button sg-tab-button--compact\" type=\"button\" role=\"tab\" aria-selected=\"false\" data-component-part=\"tab-button\" aria-label=\"Dokument entfernen: ' + filename + '\" data-lager-document-delete=\"' + id + '\">×</button></span></span></div>'; }).join(''); };";
echo " const syncLagerDocumentViews = (root, documents) => { const read = root?.querySelector('[data-lager-documents-read]'); const edit = root?.querySelector('[data-lager-documents-edit]'); const upload = root?.querySelector('[data-lager-document-upload-form]'); if (read) { read.innerHTML = renderLagerDocumentRows(documents, false); } if (edit) { edit.innerHTML = renderLagerDocumentRows(documents, true); if (upload) { edit.appendChild(upload); } } };";
echo " const syncLagerCurrentDocuments = (trigger) => { const drawer = getDrawer(); if (!drawer) { return; } let documents = []; try { documents = JSON.parse(trigger.dataset.lagerDrawerDocuments || '[]'); } catch (error) { documents = []; } syncLagerDocumentViews(drawer.querySelector('[data-lager-documents=\"current-read\"]'), documents); syncLagerDocumentViews(drawer.querySelector('[data-lager-documents=\"current-edit\"]'), documents); };";
echo " const renderLagerSelectedFiles = (scope) => { const input = scope?.querySelector('[data-lager-document-file]'); const list = scope?.querySelector('[data-lager-document-selected-files]'); const status = scope?.querySelector('[data-lager-document-status]'); if (!input || !list) { return; } const files = Array.from(input.files || []); list.replaceChildren(...files.map((file, index) => { const item = document.createElement('li'); item.className = 'sg-file-upload__file'; const name = document.createElement('span'); name.className = 'sg-file-upload__file-name sg-body'; name.append(document.createTextNode(file.name + ' ')); const navigation = document.createElement('span'); navigation.className = 'sg-tab-button-group'; navigation.setAttribute('role', 'tablist'); navigation.setAttribute('aria-label', 'Datei entfernen'); navigation.dataset.component = 'tab-navigation'; navigation.dataset.componentSize = 'compact'; const remove = document.createElement('button'); remove.className = 'sg-interaction-element sg-button sg-tab-button sg-tab-button--compact sg-file-upload__remove'; remove.type = 'button'; remove.setAttribute('role', 'tab'); remove.setAttribute('aria-selected', 'false'); remove.setAttribute('data-component-part', 'tab-button'); remove.setAttribute('aria-label', 'Datei entfernen: ' + file.name); remove.dataset.lagerDocumentRemoveIndex = String(index); remove.textContent = '×'; navigation.append(remove); name.append(navigation); item.append(name); return item; })); list.hidden = files.length === 0; if (status && !status.textContent.includes('Upload') && !status.textContent.includes('hochgeladen')) { status.textContent = files.length ? files.length + ' Datei(en) ausgewählt.' : 'Noch keine Dateien ausgewählt.'; } };";
echo " const closeDrawer = () => { setDrawerState(false); contentRoot.querySelectorAll('[data-lager-drawer-open]').forEach((trigger) => trigger.setAttribute('aria-expanded', 'false')); };"; echo " const closeDrawer = () => { setDrawerState(false); contentRoot.querySelectorAll('[data-lager-drawer-open]').forEach((trigger) => trigger.setAttribute('aria-expanded', 'false')); };";
echo " document.addEventListener('click', (event) => { const trigger = event.target.closest('[data-lager-drawer-open]'); if (!trigger || trigger.dataset.lagerDrawerType !== 'Produkt') { return; } const drawer = getDrawer(); const forecast = drawer?.querySelector('[data-lager-drawer-forecast-value]'); if (forecast) { forecast.textContent = trigger.dataset.lagerDrawerForecast || 'Keine Prognose'; } syncLagerCurrentDocuments(trigger); });"; echo " document.addEventListener('click', (event) => { const trigger = event.target.closest('[data-lager-drawer-open]'); if (!trigger || trigger.dataset.lagerDrawerType !== 'Produkt') { return; } const drawer = getDrawer(); const forecast = drawer?.querySelector('[data-lager-drawer-forecast-value]'); if (forecast) { forecast.textContent = trigger.dataset.lagerDrawerForecast || 'Keine Prognose'; } });";
echo " const getLagerForecastWarningText = (forecast, trigger, text) => { if (forecast?.status === 'depleted' || trigger?.dataset.lagerDrawerForecastStatus === 'depleted') { return 'Die Charge ist aufgebraucht. Voraussichtlich leer: ' + text + '.'; } if (trigger?.dataset.lagerDrawerForecastMhdWarning === 'true') { const expiry = trigger.dataset.lagerDrawerExpiryDate ? new Date(trigger.dataset.lagerDrawerExpiryDate + 'T00:00:00').toLocaleDateString('de-CH') : 'dem MHD'; return 'Das MHD wird am ' + expiry + ' erreicht, bevor das Lager leer ist. Voraussichtlich leer: ' + text + '.'; } if (trigger?.dataset.lagerDrawerForecastWarning === 'true') { return 'Voraussichtlich leer: ' + text + '.'; } return 'Keine akute Warnung.'; };"; echo " const getLagerForecastWarningText = (forecast, trigger, text) => { if (forecast?.status === 'depleted' || trigger?.dataset.lagerDrawerForecastStatus === 'depleted') { return 'Der Bestand ist aufgebraucht. Voraussichtlich leer: ' + text + '.'; } if (trigger?.dataset.lagerDrawerForecastMhdWarning === 'true') { const expiry = trigger.dataset.lagerDrawerExpiryDate ? new Date(trigger.dataset.lagerDrawerExpiryDate + 'T00:00:00').toLocaleDateString('de-CH') : 'dem MHD'; const forecastDate = forecast?.date ? new Date(forecast.date + 'T00:00:00').toLocaleDateString('de-CH') : text.replace(/\\s+\\(MHD:.*\\)$/, ''); return 'Das MHD wird am ' + expiry + ' erreicht, bevor der Bestand leer ist. Voraussichtlich leer: ' + forecastDate + '.'; } if (trigger?.dataset.lagerDrawerForecastWarning === 'true') { return 'Voraussichtlich leer: ' + text + '.'; } return 'Keine akute Warnung.'; };";
echo " document.addEventListener('click', (event) => { const trigger = event.target.closest('[data-lager-drawer-open]'); if (!trigger || trigger.dataset.lagerDrawerType !== 'Produkt') { return; } const drawer = getDrawer(); const warningValue = drawer?.querySelector('[data-lager-drawer-warning-value]'); if (warningValue) { warningValue.textContent = getLagerForecastWarningText(null, trigger, trigger.dataset.lagerDrawerForecast || 'Keine Prognose'); } });"; echo " document.addEventListener('click', (event) => { const trigger = event.target.closest('[data-lager-drawer-open]'); if (!trigger || trigger.dataset.lagerDrawerType !== 'Produkt') { return; } const drawer = getDrawer(); const warningValue = drawer?.querySelector('[data-lager-drawer-warning-value]'); if (warningValue) { warningValue.textContent = getLagerForecastWarningText(null, trigger, trigger.dataset.lagerDrawerForecast || 'Keine Prognose'); } });";
echo " const syncLagerForecast = (forecast, productId) => { const drawer = getDrawer(); const trigger = contentRoot.querySelector('[data-lager-drawer-open][aria-expanded=\"true\"]') || contentRoot.querySelector('[data-lager-row][data-lager-id=\"' + (productId || '') + '\"] [data-lager-drawer-open]'); if (!forecast || !drawer) { return; } const forecastDate = forecast.date ? new Date(forecast.date + 'T00:00:00') : null; const expiryDate = forecast.expiryDate ? new Date(forecast.expiryDate + 'T00:00:00') : null; const mhdWarning = Boolean(forecastDate && expiryDate && forecastDate > expiryDate); const nearExpiryWarning = Boolean(forecastDate && expiryDate && forecastDate >= new Date(new Date(expiryDate).setMonth(expiryDate.getMonth() - 3)) && forecastDate <= expiryDate); const depleted = forecast.status === 'depleted'; const forecastWarning = depleted || nearExpiryWarning; const date = forecastDate ? forecastDate.toLocaleDateString('de-CH') : 'Keine Prognose'; const text = depleted ? date + ' (CHARGE ENDE)' : (nearExpiryWarning || mhdWarning ? date + ' (MHD: ' + expiryDate.toLocaleDateString('de-CH') + ')' : date); const value = drawer.querySelector('[data-lager-drawer-forecast-value]'); if (value) { value.textContent = text; } const header = drawer.querySelector('[data-lager-drawer-warning-header]'); const title = drawer.querySelector('[data-lager-drawer-warning-title]'); const warningValue = drawer.querySelector('[data-lager-drawer-warning-value]'); if (trigger) { trigger.dataset.lagerDrawerForecast = text; trigger.dataset.lagerDrawerForecastStatus = forecast.status || 'no_demand'; trigger.dataset.lagerDrawerForecastWarning = forecastWarning ? 'true' : 'false'; trigger.dataset.lagerDrawerForecastMhdWarning = mhdWarning ? 'true' : 'false'; if (header) { 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); } if (title) { title.textContent = getLagerWarningTitle(trigger); } if (warningValue) { warningValue.textContent = forecastWarning || mhdWarning ? text : 'Keine akute Warnung.'; warningValue.classList.toggle('sg-strong', forecastWarning || mhdWarning); warningValue.classList.toggle('lager-forecast-warning', forecastWarning); warningValue.classList.toggle('lager-forecast-mhd-warning', mhdWarning); } const row = trigger.closest('[data-lager-row]'); const cells = row?.querySelectorAll('[role=\"cell\"]'); const forecastCell = cells?.[cells.length - 1]; if (forecastCell && cells.length === 3) { forecastCell.textContent = text; } } };"; echo " const syncLagerForecast = (forecast, productId) => { const drawer = getDrawer(); const trigger = contentRoot.querySelector('[data-lager-drawer-open][aria-expanded=\"true\"]') || contentRoot.querySelector('[data-lager-row][data-lager-id=\"' + (productId || '') + '\"] [data-lager-drawer-open]'); if (!forecast || !drawer) { return; } const forecastDate = forecast.date ? new Date(forecast.date + 'T00:00:00') : null; const expiryDate = forecast.expiryDate ? new Date(forecast.expiryDate + 'T00:00:00') : null; const mhdWarning = Boolean(forecastDate && expiryDate && forecastDate > expiryDate); const nearExpiryWarning = Boolean(forecastDate && expiryDate && forecastDate >= new Date(new Date(expiryDate).setMonth(expiryDate.getMonth() - 3)) && forecastDate <= expiryDate); const depleted = forecast.status === 'depleted'; const forecastWarning = depleted || nearExpiryWarning; const date = forecastDate ? forecastDate.toLocaleDateString('de-CH') : 'Keine Prognose'; const text = depleted ? date + ' (BESTAND ENDE)' : (nearExpiryWarning || mhdWarning ? date + ' (MHD: ' + expiryDate.toLocaleDateString('de-CH') + ')' : date); const value = drawer.querySelector('[data-lager-drawer-forecast-value]'); if (value) { value.textContent = text; } const header = drawer.querySelector('[data-lager-drawer-warning-header]'); const title = drawer.querySelector('[data-lager-drawer-warning-title]'); const warningValue = drawer.querySelector('[data-lager-drawer-warning-value]'); if (trigger) { trigger.dataset.lagerDrawerForecast = text; trigger.dataset.lagerDrawerForecastStatus = forecast.status || 'no_demand'; trigger.dataset.lagerDrawerForecastWarning = forecastWarning ? 'true' : 'false'; trigger.dataset.lagerDrawerForecastMhdWarning = mhdWarning ? 'true' : 'false'; if (header) { 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); } if (title) { title.textContent = getLagerWarningTitle(trigger); } if (warningValue) { warningValue.textContent = forecastWarning || mhdWarning ? text : 'Keine akute Warnung.'; warningValue.classList.toggle('sg-strong', forecastWarning || mhdWarning); warningValue.classList.toggle('lager-forecast-warning', forecastWarning); warningValue.classList.toggle('lager-forecast-mhd-warning', mhdWarning); } const row = trigger.closest('[data-lager-row]'); const cells = row?.querySelectorAll('[role=\"cell\"]'); const forecastCell = cells?.[cells.length - 1]; if (forecastCell && cells.length === 3) { forecastCell.textContent = text; } } };";
echo " document.addEventListener('lager-save-complete', (event) => { const forecast = event.detail?.forecast; const productId = event.detail?.productId || ''; const trigger = contentRoot.querySelector('[data-lager-drawer-open][aria-expanded=\"true\"]') || contentRoot.querySelector('[data-lager-row][data-lager-id=\"' + productId + '\"] [data-lager-drawer-open]'); const drawer = getDrawer(); if (!forecast || !drawer) { return; } const date = forecast.date ? new Date(forecast.date + 'T00:00:00').toLocaleDateString('de-CH') : 'Keine Prognose'; const text = forecast.status === 'depleted' ? date + ' (CHARGE ENDE)' : date; const value = drawer.querySelector('[data-lager-drawer-forecast-value]'); if (value) { value.textContent = text; } if (trigger) { trigger.dataset.lagerDrawerForecast = text; const row = trigger.closest('[data-lager-row]'); const cells = row?.querySelectorAll('[role=\"cell\"]'); const forecastCell = cells?.[cells.length - 1]; if (forecastCell && cells.length === 3) { forecastCell.textContent = text; } } });"; echo " document.addEventListener('lager-save-complete', (event) => { const forecast = event.detail?.forecast; const productId = event.detail?.productId || ''; const trigger = contentRoot.querySelector('[data-lager-drawer-open][aria-expanded=\"true\"]') || contentRoot.querySelector('[data-lager-row][data-lager-id=\"' + productId + '\"] [data-lager-drawer-open]'); const drawer = getDrawer(); if (!forecast || !drawer) { return; } const date = forecast.date ? new Date(forecast.date + 'T00:00:00').toLocaleDateString('de-CH') : 'Keine Prognose'; const text = forecast.status === 'depleted' ? date + ' (BESTAND ENDE)' : date; const value = drawer.querySelector('[data-lager-drawer-forecast-value]'); if (value) { value.textContent = text; } if (trigger) { trigger.dataset.lagerDrawerForecast = text; const row = trigger.closest('[data-lager-row]'); const cells = row?.querySelectorAll('[role=\"cell\"]'); const forecastCell = cells?.[cells.length - 1]; if (forecastCell && cells.length === 3) { forecastCell.textContent = text; } } });";
echo " document.addEventListener('click', (event) => {"; echo " document.addEventListener('click', (event) => {";
echo " const trigger = event.target.closest('[data-lager-drawer-open]');"; echo " const trigger = event.target.closest('[data-lager-drawer-open]');";
echo " if (trigger) { event.preventDefault(); if (window.matchMedia('(max-width: 767px)').matches) { openLagerMobileDetail(trigger); return; } 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 = false; } drawer.querySelector('[data-lager-drawer-value-label]').textContent = type === 'Produkt' ? 'Anzahl im Lager:' : 'Artikelnummer:'; drawer.querySelector('[data-lager-drawer-value-value]').textContent = value; drawer.querySelectorAll('[data-lager-drawer-topic]').forEach((card) => { card.hidden = type !== 'Produkt'; }); if (type === 'Produkt') { const forecast = trigger.dataset.lagerDrawerForecast || 'Keine Prognose'; const warning = trigger.dataset.lagerDrawerForecastWarning === 'true' || trigger.dataset.lagerDrawerForecastMhdWarning === 'true'; const warningValue = drawer.querySelector('[data-lager-drawer-warning-value]'); if (warningValue) { warningValue.textContent = warning ? forecast : 'Keine akute Warnung.'; warningValue.classList.toggle('sg-strong', warning); warningValue.classList.toggle('lager-forecast-warning', trigger.dataset.lagerDrawerForecastWarning === 'true'); warningValue.classList.toggle('lager-forecast-mhd-warning', trigger.dataset.lagerDrawerForecastMhdWarning === 'true'); } drawer.querySelector('[data-lager-drawer-lot-number]').textContent = trigger.dataset.lagerDrawerLotNumber || 'Nicht hinterlegt'; drawer.querySelector('[data-lager-drawer-manufacturer-name]').textContent = trigger.dataset.lagerDrawerManufacturerName || 'Nicht hinterlegt'; drawer.querySelector('[data-lager-drawer-manufacturer-lot-number]').textContent = trigger.dataset.lagerDrawerManufacturerLotNumber || 'Nicht hinterlegt'; drawer.querySelector('[data-lager-drawer-mfg-date]').textContent = trigger.dataset.lagerDrawerMfgDate || 'Nicht hinterlegt'; drawer.querySelector('[data-lager-drawer-expiry-date]').textContent = trigger.dataset.lagerDrawerExpiryDate || 'Nicht hinterlegt'; drawer.querySelector('[data-lager-drawer-purchase-date]').textContent = trigger.dataset.lagerDrawerPurchaseDate || 'Nicht hinterlegt'; const virtualLot = trigger.dataset.lagerDrawerVirtualLotNumber || ''; drawer.querySelector('[data-lager-drawer-virtual-lot-value]').textContent = virtualLot ? 'Virtuelle Charge: ' + virtualLot : 'Keine virtuelle Fehlbestandscharge vorhanden.'; const manufacturer = drawer.querySelector('[data-lager-manufacturer]'); if (manufacturer) { manufacturer.value = trigger.dataset.lagerDrawerManufacturerPartyId || ''; } const manufacturerLot = drawer.querySelector('[data-lager-manufacturer-lot]'); if (manufacturerLot) { manufacturerLot.value = trigger.dataset.lagerDrawerManufacturerLotNumber || ''; } const mfgDate = drawer.querySelector('[data-lager-mfg-date]'); if (mfgDate) { mfgDate.value = trigger.dataset.lagerDrawerMfgDate || ''; } const expiryDate = drawer.querySelector('[data-lager-expiry-date]'); if (expiryDate) { expiryDate.value = trigger.dataset.lagerDrawerExpiryDate || ''; } const purchaseDate = drawer.querySelector('[data-lager-purchase-date]'); if (purchaseDate) { purchaseDate.value = trigger.dataset.lagerDrawerPurchaseDate || ''; } const editForm = drawer.querySelector('[data-lager-edit-form]'); if (editForm) { editForm.hidden = true; } drawer.dataset.lagerActiveLotId = trigger.dataset.lagerDrawerLotId || ''; } setDrawerState(true); contentRoot.querySelectorAll('[data-lager-drawer-open]').forEach((button) => button.setAttribute('aria-expanded', String(button === trigger))); return; }"; echo " if (trigger) { event.preventDefault(); if (window.matchMedia('(max-width: 767px)').matches) { openLagerMobileDetail(trigger); return; } 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 = false; } drawer.querySelector('[data-lager-drawer-value-label]').textContent = type === 'Produkt' ? 'Anzahl im Lager:' : 'Artikelnummer:'; drawer.querySelector('[data-lager-drawer-value-value]').textContent = value; drawer.querySelectorAll('[data-lager-drawer-topic]').forEach((card) => { card.hidden = type !== 'Produkt'; }); if (type === 'Produkt') { const forecast = trigger.dataset.lagerDrawerForecast || 'Keine Prognose'; const warning = trigger.dataset.lagerDrawerForecastWarning === 'true' || trigger.dataset.lagerDrawerForecastMhdWarning === 'true'; const warningValue = drawer.querySelector('[data-lager-drawer-warning-value]'); if (warningValue) { warningValue.textContent = warning ? forecast : 'Keine akute Warnung.'; warningValue.classList.toggle('sg-strong', warning); warningValue.classList.toggle('lager-forecast-warning', trigger.dataset.lagerDrawerForecastWarning === 'true'); warningValue.classList.toggle('lager-forecast-mhd-warning', trigger.dataset.lagerDrawerForecastMhdWarning === 'true'); } drawer.querySelector('[data-lager-drawer-lot-number]').textContent = trigger.dataset.lagerDrawerLotNumber || 'Nicht hinterlegt'; drawer.querySelector('[data-lager-drawer-manufacturer-name]').textContent = trigger.dataset.lagerDrawerManufacturerName || 'Nicht hinterlegt'; drawer.querySelector('[data-lager-drawer-manufacturer-lot-number]').textContent = trigger.dataset.lagerDrawerManufacturerLotNumber || 'Nicht hinterlegt'; drawer.querySelector('[data-lager-drawer-mfg-date]').textContent = trigger.dataset.lagerDrawerMfgDate || 'Nicht hinterlegt'; drawer.querySelector('[data-lager-drawer-expiry-date]').textContent = trigger.dataset.lagerDrawerExpiryDate || 'Nicht hinterlegt'; drawer.querySelector('[data-lager-drawer-purchase-date]').textContent = trigger.dataset.lagerDrawerPurchaseDate || 'Nicht hinterlegt'; const virtualLot = trigger.dataset.lagerDrawerVirtualLotNumber || ''; drawer.querySelector('[data-lager-drawer-virtual-lot-value]').textContent = virtualLot ? 'Virtuelle Charge: ' + virtualLot : 'Keine virtuelle Fehlbestandscharge vorhanden.'; const manufacturer = drawer.querySelector('[data-lager-manufacturer]'); if (manufacturer) { manufacturer.value = trigger.dataset.lagerDrawerManufacturerPartyId || ''; } const manufacturerLot = drawer.querySelector('[data-lager-manufacturer-lot]'); if (manufacturerLot) { manufacturerLot.value = trigger.dataset.lagerDrawerManufacturerLotNumber || ''; } const mfgDate = drawer.querySelector('[data-lager-mfg-date]'); if (mfgDate) { mfgDate.value = trigger.dataset.lagerDrawerMfgDate || ''; } const expiryDate = drawer.querySelector('[data-lager-expiry-date]'); if (expiryDate) { expiryDate.value = trigger.dataset.lagerDrawerExpiryDate || ''; } const purchaseDate = drawer.querySelector('[data-lager-purchase-date]'); if (purchaseDate) { purchaseDate.value = trigger.dataset.lagerDrawerPurchaseDate || ''; } const editForm = drawer.querySelector('[data-lager-edit-form]'); if (editForm) { editForm.hidden = true; } drawer.dataset.lagerActiveLotId = trigger.dataset.lagerDrawerLotId || ''; } setDrawerState(true); contentRoot.querySelectorAll('[data-lager-drawer-open]').forEach((button) => button.setAttribute('aria-expanded', String(button === trigger))); return; }";
echo " document.addEventListener('click', (event) => { const trigger = event.target.closest('[data-lager-drawer-open]'); if (trigger?.dataset.lagerDrawerType === 'Produkt') { syncLagerCurrentDocuments(trigger); } });";
echo " if (event.target.closest('[data-lager-drawer-close]') || event.target.closest('[data-lager-drawer-backdrop]')) { closeDrawer(); }"; echo " if (event.target.closest('[data-lager-drawer-close]') || event.target.closest('[data-lager-drawer-backdrop]')) { closeDrawer(); }";
echo " });"; echo " });";
echo " document.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 forecastWarning = trigger.dataset.lagerDrawerForecastWarning === 'true'; const mhdWarning = trigger.dataset.lagerDrawerForecastMhdWarning === 'true'; title.textContent = getLagerWarningTitle(trigger); 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 " document.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 forecastWarning = trigger.dataset.lagerDrawerForecastWarning === 'true'; const mhdWarning = trigger.dataset.lagerDrawerForecastMhdWarning === 'true'; title.textContent = getLagerWarningTitle(trigger); 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 " document.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; } setLagerEditMode(false); const drawer = getDrawer(); const currentQty = drawer?.querySelector('[data-lager-drawer-current-qty]'); const qtyIn = drawer?.querySelector('[data-lager-drawer-qty-in]'); const lotNumber = drawer?.querySelector('[data-lager-lot-number]'); if (currentQty) { currentQty.textContent = trigger.dataset.lagerDrawerCurrentQty || '0'; } if (qtyIn) { qtyIn.textContent = trigger.dataset.lagerDrawerQtyIn || '0'; } if (lotNumber) { lotNumber.value = trigger.dataset.lagerDrawerLotNumber || ''; } const productId = trigger.closest('[data-lager-row]')?.dataset.lagerId || ''; contentRoot.querySelectorAll('[data-lager-historical-product]').forEach((product) => { product.hidden = product.dataset.lagerHistoricalProduct !== productId; }); });"; echo " document.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; } setLagerEditMode(false); const drawer = getDrawer(); const currentQty = drawer?.querySelector('[data-lager-drawer-current-qty]'); const qtyIn = drawer?.querySelector('[data-lager-drawer-qty-in]'); const lotNumber = drawer?.querySelector('[data-lager-lot-number]'); if (currentQty) { currentQty.textContent = trigger.dataset.lagerDrawerCurrentQty || '0'; } if (qtyIn) { qtyIn.textContent = trigger.dataset.lagerDrawerQtyIn || '0'; } if (lotNumber) { lotNumber.value = trigger.dataset.lagerDrawerLotNumber || ''; } 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'); } syncLagerForecast(result.forecast, result.productId); const trigger = contentRoot.querySelector('[data-lager-drawer-open][aria-expanded=\"true\"]') || contentRoot.querySelector('[data-lager-row][data-lager-id=\"' + (result.productId || '') + '\"] [data-lager-drawer-open]'); const drawer = getDrawer(); const warningValue = drawer?.querySelector('[data-lager-drawer-warning-value]'); if (warningValue) { warningValue.textContent = getLagerForecastWarningText(result.forecast, trigger, trigger?.dataset.lagerDrawerForecast || 'Keine Prognose'); } return result; };"; 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'); } syncLagerForecast(result.forecast, result.productId); const trigger = contentRoot.querySelector('[data-lager-drawer-open][aria-expanded=\"true\"]') || contentRoot.querySelector('[data-lager-row][data-lager-id=\"' + (result.productId || '') + '\"] [data-lager-drawer-open]'); const drawer = getDrawer(); const warningValue = drawer?.querySelector('[data-lager-drawer-warning-value]'); if (warningValue) { warningValue.textContent = getLagerForecastWarningText(result.forecast, trigger, trigger?.dataset.lagerDrawerForecast || 'Keine Prognose'); } return result; };";
echo " document.addEventListener('click', (event) => { const remove = event.target.closest('[data-lager-document-remove-index]'); if (remove) { const scope = remove.closest('[data-lager-document-upload-form]'); const input = scope?.querySelector('[data-lager-document-file]'); const index = Number(remove.dataset.lagerDocumentRemoveIndex); if (input && Number.isInteger(index)) { const data = new DataTransfer(); Array.from(input.files || []).forEach((file, fileIndex) => { if (fileIndex !== index) { data.items.add(file); } }); input.files = data.files; input.dispatchEvent(new Event('change', { bubbles: true })); } return; } const trigger = event.target.closest('[data-lager-document-file-trigger]'); const input = trigger?.closest('[data-lager-document-upload-form]')?.querySelector('[data-lager-document-file]'); if (!input) { return; } event.preventDefault(); input.click(); });";
echo " document.addEventListener('dragover', (event) => { const dropzone = event.target.closest('[data-lager-document-upload-form] .sg-file-upload__dropzone'); if (!dropzone) { return; } event.preventDefault(); dropzone.dataset.dragOver = 'true'; });";
echo " document.addEventListener('dragleave', (event) => { const dropzone = event.target.closest('[data-lager-document-upload-form] .sg-file-upload__dropzone'); if (dropzone && !dropzone.contains(event.relatedTarget)) { delete dropzone.dataset.dragOver; } });";
echo " document.addEventListener('drop', (event) => { const dropzone = event.target.closest('[data-lager-document-upload-form] .sg-file-upload__dropzone'); if (!dropzone) { return; } event.preventDefault(); delete dropzone.dataset.dragOver; const input = dropzone.closest('[data-lager-document-upload-form]')?.querySelector('[data-lager-document-file]'); if (input && event.dataTransfer?.files) { input.files = event.dataTransfer.files; input.dispatchEvent(new Event('change', { bubbles: true })); } });";
echo " document.addEventListener('change', (event) => { const input = event.target.closest('[data-lager-document-file]'); if (!input) { return; } const form = input.closest('[data-lager-document-upload-form]'); renderLagerSelectedFiles(form); });";
echo " const uploadLagerDocuments = async (scope, lotId) => { const files = Array.from(scope?.querySelectorAll('[data-lager-document-file]') || []).flatMap((input) => Array.from(input.files || [])); if (!files.length) { return []; } const status = scope.querySelector('[data-lager-document-status]'); const uploaded = []; const errorMessage = (error) => error instanceof Error ? error.message : 'Upload fehlgeschlagen'; if (files.some((file) => file.size > 52428800)) { const error = new Error('Dokument darf maximal 50 MB groß sein.'); if (status) { status.textContent = error.message; } throw error; } if (status) { status.textContent = 'Upload läuft …'; } try { for (const file of files) { const data = new FormData(); data.append('action', 'upload_document'); data.append('csrfToken', contentRoot.querySelector('[data-lager-csrf-token]')?.dataset.lagerCsrfToken || ''); data.append('lotId', lotId || ''); data.append('document', file); const response = await fetch('/api/lager-charge.php', { method: 'POST', body: data }); const result = await response.json(); if (!response.ok || result.ok !== true) { throw new Error(result.error || 'Upload fehlgeschlagen'); } uploaded.push(result.document); } } catch (error) { if (status) { status.textContent = errorMessage(error); } throw error; } if (status) { status.textContent = files.length + ' Datei(en) hochgeladen.'; } return uploaded; };";
echo " document.addEventListener('click', async (event) => { const remove = event.target.closest('[data-lager-document-delete]'); if (!remove) { return; } remove.disabled = true; try { await postLagerChange({ action: 'delete_document', documentId: remove.dataset.lagerDocumentDelete }); window.location.reload(); } catch (error) { remove.disabled = false; remove.setCustomValidity(error instanceof Error ? error.message : 'Dokument konnte nicht entfernt werden'); remove.reportValidity(); } });";
echo " document.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 " document.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 " const setLagerEditMode = (editing) => { const drawer = getDrawer(); const readView = drawer?.querySelector('[data-lager-read-view]'); const readDocuments = drawer?.querySelector('[data-lager-documents=\"current-read\"]'); const editForm = drawer?.querySelector('[data-lager-edit-form]'); const documents = drawer?.querySelector('[data-lager-edit-documents=\"current-edit\"]'); const actions = drawer?.querySelector('[data-lager-edit-actions]'); if (readView) { readView.hidden = editing; } if (readDocuments) { readDocuments.hidden = editing; } if (editForm) { editForm.hidden = !editing; } if (documents) { documents.hidden = !editing; } if (actions) { actions.hidden = !editing; } };"; echo " const setLagerEditMode = (editing) => { const drawer = getDrawer(); const readView = drawer?.querySelector('[data-lager-read-view]'); const editForm = drawer?.querySelector('[data-lager-edit-form]'); if (readView) { readView.hidden = editing; } if (editForm) { editForm.hidden = !editing; } };";
echo " document.addEventListener('click', (event) => { if (event.target.closest('[data-lager-edit-open]')) { setLagerEditMode(true); } if (event.target.closest('[data-lager-edit-close]')) { clearLagerMovementInputs(); setLagerEditMode(false); } });"; echo " document.addEventListener('click', (event) => { if (event.target.closest('[data-lager-edit-open]')) { setLagerEditMode(true); } if (event.target.closest('[data-lager-edit-close]')) { clearLagerMovementInputs(); setLagerEditMode(false); } });";
echo " const syncLagerReadView = () => { const drawer = getDrawer(); if (!drawer) { return; } const manufacturer = drawer.querySelector('[data-lager-manufacturer]'); const values = { '[data-lager-drawer-manufacturer-name]': manufacturer?.selectedOptions?.[0]?.textContent || 'Nicht hinterlegt', '[data-lager-drawer-manufacturer-lot-number]': drawer.querySelector('[data-lager-manufacturer-lot]')?.value || 'Nicht hinterlegt', '[data-lager-drawer-mfg-date]': drawer.querySelector('[data-lager-mfg-date]')?.value || 'Nicht hinterlegt', '[data-lager-drawer-expiry-date]': drawer.querySelector('[data-lager-expiry-date]')?.value || 'Nicht hinterlegt', '[data-lager-drawer-purchase-date]': drawer.querySelector('[data-lager-purchase-date]')?.value || 'Nicht hinterlegt' }; Object.entries(values).forEach(([selector, value]) => { const target = drawer.querySelector(selector); if (target) { target.textContent = value; } }); const trigger = contentRoot.querySelector('[data-lager-drawer-open][aria-expanded=\"true\"]'); if (trigger) { trigger.dataset.lagerDrawerManufacturerName = values['[data-lager-drawer-manufacturer-name]']; trigger.dataset.lagerDrawerManufacturerLotNumber = values['[data-lager-drawer-manufacturer-lot-number]']; trigger.dataset.lagerDrawerMfgDate = values['[data-lager-drawer-mfg-date]']; trigger.dataset.lagerDrawerExpiryDate = values['[data-lager-drawer-expiry-date]']; trigger.dataset.lagerDrawerPurchaseDate = values['[data-lager-drawer-purchase-date]']; } };"; echo " const syncLagerReadView = () => { const drawer = getDrawer(); if (!drawer) { return; } const manufacturer = drawer.querySelector('[data-lager-manufacturer]'); const values = { '[data-lager-drawer-manufacturer-name]': manufacturer?.selectedOptions?.[0]?.textContent || 'Nicht hinterlegt', '[data-lager-drawer-manufacturer-lot-number]': drawer.querySelector('[data-lager-manufacturer-lot]')?.value || 'Nicht hinterlegt', '[data-lager-drawer-mfg-date]': drawer.querySelector('[data-lager-mfg-date]')?.value || 'Nicht hinterlegt', '[data-lager-drawer-expiry-date]': drawer.querySelector('[data-lager-expiry-date]')?.value || 'Nicht hinterlegt', '[data-lager-drawer-purchase-date]': drawer.querySelector('[data-lager-purchase-date]')?.value || 'Nicht hinterlegt' }; Object.entries(values).forEach(([selector, value]) => { const target = drawer.querySelector(selector); if (target) { target.textContent = value; } }); const trigger = contentRoot.querySelector('[data-lager-drawer-open][aria-expanded=\"true\"]'); if (trigger) { trigger.dataset.lagerDrawerManufacturerName = values['[data-lager-drawer-manufacturer-name]']; trigger.dataset.lagerDrawerManufacturerLotNumber = values['[data-lager-drawer-manufacturer-lot-number]']; trigger.dataset.lagerDrawerMfgDate = values['[data-lager-drawer-mfg-date]']; trigger.dataset.lagerDrawerExpiryDate = values['[data-lager-drawer-expiry-date]']; trigger.dataset.lagerDrawerPurchaseDate = values['[data-lager-drawer-purchase-date]']; } };";
echo " const syncLagerBalanceView = (balance) => { const drawer = getDrawer(); if (!drawer || !balance) { return; } const currentQty = drawer.querySelector('[data-lager-drawer-current-qty]'); const qtyIn = drawer.querySelector('[data-lager-drawer-qty-in]'); if (currentQty) { currentQty.textContent = String(balance.qty_net ?? 0); } if (qtyIn) { qtyIn.textContent = String(balance.qty_in ?? 0); } const trigger = contentRoot.querySelector('[data-lager-drawer-open][aria-expanded=\"true\"]'); if (trigger) { trigger.dataset.lagerDrawerCurrentQty = String(balance.qty_net ?? 0); trigger.dataset.lagerDrawerQtyIn = String(balance.qty_in ?? 0); } };"; echo " const syncLagerBalanceView = (balance) => { const drawer = getDrawer(); if (!drawer || !balance) { return; } const currentQty = drawer.querySelector('[data-lager-drawer-current-qty]'); const qtyIn = drawer.querySelector('[data-lager-drawer-qty-in]'); if (currentQty) { currentQty.textContent = String(balance.qty_net ?? 0); } if (qtyIn) { qtyIn.textContent = String(balance.qty_in ?? 0); } const trigger = contentRoot.querySelector('[data-lager-drawer-open][aria-expanded=\"true\"]'); if (trigger) { trigger.dataset.lagerDrawerCurrentQty = String(balance.qty_net ?? 0); trigger.dataset.lagerDrawerQtyIn = String(balance.qty_in ?? 0); } };";
echo " const syncLagerActiveLotView = (lot) => { const drawer = getDrawer(); const trigger = contentRoot.querySelector('[data-lager-drawer-open][aria-expanded=\"true\"]'); if (!drawer || !lot || !trigger) { return; } const manufacturer = drawer.querySelector('[data-lager-manufacturer]'); if (manufacturer) { manufacturer.value = lot.manufacturer_party_id ?? ''; } const manufacturerName = manufacturer?.selectedOptions?.[0]?.textContent || 'Nicht hinterlegt'; const values = { '[data-lager-drawer-lot-number]': lot.lot_number || 'Nicht hinterlegt', '[data-lager-drawer-manufacturer-name]': manufacturerName, '[data-lager-drawer-manufacturer-lot-number]': lot.manufacturer_lot_number || 'Nicht hinterlegt', '[data-lager-drawer-mfg-date]': lot.mfg_date || 'Nicht hinterlegt', '[data-lager-drawer-expiry-date]': lot.expiry_date || 'Nicht hinterlegt', '[data-lager-drawer-purchase-date]': lot.purchase_date || 'Nicht hinterlegt' }; Object.entries(values).forEach(([selector, value]) => { const target = drawer.querySelector(selector); if (target) { target.textContent = value; } }); const formValues = { '[data-lager-lot-number]': lot.lot_number || '', '[data-lager-manufacturer-lot]': lot.manufacturer_lot_number || '', '[data-lager-mfg-date]': lot.mfg_date || '', '[data-lager-expiry-date]': lot.expiry_date || '', '[data-lager-purchase-date]': lot.purchase_date || '' }; Object.entries(formValues).forEach(([selector, value]) => { const input = drawer.querySelector(selector); if (input) { input.value = value; } }); trigger.dataset.lagerDrawerLotId = String(lot.id || lot.lot_id || ''); trigger.dataset.lagerDrawerLotNumber = lot.lot_number || ''; trigger.dataset.lagerDrawerManufacturerPartyId = String(lot.manufacturer_party_id ?? ''); trigger.dataset.lagerDrawerManufacturerName = manufacturerName; trigger.dataset.lagerDrawerManufacturerLotNumber = lot.manufacturer_lot_number || ''; trigger.dataset.lagerDrawerMfgDate = lot.mfg_date || ''; trigger.dataset.lagerDrawerExpiryDate = lot.expiry_date || ''; trigger.dataset.lagerDrawerPurchaseDate = lot.purchase_date || ''; drawer.dataset.lagerActiveLotId = String(lot.id || lot.lot_id || ''); };";
echo " const clearLagerMovementInputs = () => { const drawer = getDrawer(); if (!drawer) { return; } drawer.querySelectorAll('[data-lager-receipt-qty], [data-lager-adjustment-qty], [data-lager-out-qty]').forEach((input) => { input.value = ''; }); };"; echo " const clearLagerMovementInputs = () => { const drawer = getDrawer(); if (!drawer) { return; } drawer.querySelectorAll('[data-lager-receipt-qty], [data-lager-adjustment-qty], [data-lager-out-qty]').forEach((input) => { input.value = ''; }); };";
echo " const syncLagerOpenLotCard = (lot, balance) => { const drawer = getDrawer(); const card = drawer?.querySelector('[data-lager-open-card=\"' + (lot?.id || '') + '\"]'); if (!card) { return; } const values = { '[data-lager-open-card-lot-number]': lot?.lot_number || 'Nicht hinterlegt', '[data-lager-open-card-qty-net]': String(balance?.qty_net ?? 0), '[data-lager-open-card-qty-in]': String(balance?.qty_net ?? 0), '[data-lager-open-card-manufacturer]': drawer.querySelector('[data-lager-new-manufacturer]')?.selectedOptions?.[0]?.textContent || 'Nicht hinterlegt', '[data-lager-open-card-manufacturer-lot]': lot?.manufacturer_lot_number || 'Nicht hinterlegt', '[data-lager-open-card-mfg-date]': lot?.mfg_date || 'Nicht hinterlegt', '[data-lager-open-card-expiry-date]': lot?.expiry_date || 'Nicht hinterlegt', '[data-lager-open-card-purchase-date]': lot?.purchase_date || 'Nicht hinterlegt' }; Object.entries(values).forEach(([selector, value]) => { const target = card.querySelector(selector); if (target) { target.textContent = value; } }); const wrapper = card.closest('[data-lager-open-card-wrapper]'); wrapper?.querySelector('[data-lager-edit-documents]')?.setAttribute('hidden', ''); wrapper?.querySelector('[data-lager-open-edit-actions]')?.setAttribute('hidden', ''); wrapper?.querySelector('[data-lager-documents^=\"open-read-\"]')?.removeAttribute('hidden'); };"; echo " const syncLagerOpenLotCard = (lot, balance, exactCard = null) => { const drawer = getDrawer(); const lotId = lot?.id || lot?.lot_id || ''; const card = exactCard || drawer?.querySelector('[data-lager-open-card=\"' + lotId + '\"]'); if (!card) { return; } const manufacturerSelect = card.querySelector('[data-lager-open-manufacturer]') || drawer?.querySelector('[data-lager-new-manufacturer]'); const qualified = Boolean(lot?.lot_number && lot?.manufacturer_party_id && lot?.manufacturer_lot_number && lot?.mfg_date && lot?.expiry_date && lot?.purchase_date && Number(balance?.qty_net || 0) > 0); const selected = lot?.next_active === true; const values = { '[data-lager-open-card-lot-number]': lot?.lot_number || 'Nicht hinterlegt', '[data-lager-open-card-qty-net]': String(balance?.qty_net ?? 0), '[data-lager-open-card-qty-in]': String(balance?.qty_in ?? 0), '[data-lager-open-card-manufacturer]': manufacturerSelect?.selectedOptions?.[0]?.textContent || 'Nicht hinterlegt', '[data-lager-open-card-manufacturer-lot]': lot?.manufacturer_lot_number || 'Nicht hinterlegt', '[data-lager-open-card-mfg-date]': lot?.mfg_date || 'Nicht hinterlegt', '[data-lager-open-card-expiry-date]': lot?.expiry_date || 'Nicht hinterlegt', '[data-lager-open-card-purchase-date]': lot?.purchase_date || 'Nicht hinterlegt' }; Object.entries(values).forEach(([selector, value]) => { const target = card.querySelector(selector); if (target) { target.textContent = value; } }); card.classList.toggle('sg-lager-open-lot--next-active', selected); const selectedLabel = card.querySelector('[data-lager-open-card-next-active]'); if (selectedLabel) { selectedLabel.textContent = selected ? 'Als nächste aktive Charge ausgewählt' : 'Nicht ausgewählt'; } const selectButton = card.querySelector('[data-lager-open-select]'); if (selectButton) { selectButton.dataset.lagerOpenSelectable = String(qualified); selectButton.disabled = !qualified || selected; selectButton.setAttribute('aria-disabled', String(!qualified || selected)); } };";
echo " document.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 drawer = getDrawer(); const productId = trigger.closest('[data-lager-row]')?.dataset.lagerId || ''; const openProduct = drawer?.querySelector('[data-lager-open-product=\"' + productId + '\"]'); drawer?.querySelectorAll('[data-lager-open-product]').forEach((product) => { product.hidden = product !== openProduct; product.querySelectorAll('[data-lager-open-edit-form]').forEach((form) => { form.hidden = true; }); product.querySelectorAll('[data-lager-open-edit-actions]').forEach((actions) => { actions.hidden = true; }); product.querySelectorAll('[data-lager-open-card]').forEach((readView) => { readView.hidden = false; }); product.querySelectorAll('[data-lager-documents^=\"open-read-\"]').forEach((readDocuments) => { readDocuments.hidden = false; }); product.querySelectorAll('[data-lager-edit-documents]').forEach((documents) => { documents.hidden = true; }); }); const newForm = drawer?.querySelector('[data-lager-drawer-topic=\"new-lot-form\"]'); const newAction = drawer?.querySelector('[data-lager-drawer-topic=\"new-lot-action\"]'); if (newForm) { newForm.hidden = true; } if (newAction) { newAction.hidden = false; } const notificationWrapper = drawer?.querySelector('[data-lager-new-lot-notification-wrapper]'); const notification = drawer?.querySelector('[data-lager-new-lot-notification]'); const shortageQty = trigger.dataset.lagerDrawerVirtualShortageQty || '0'; const hasShortage = shortageQty !== '0' && shortageQty !== '0,00'; if (notificationWrapper) { notificationWrapper.hidden = !hasShortage; } if (notification && hasShortage) { notification.textContent = 'Für dieses Produkt bestehen virtuelle Fehlbestände in Höhe von ' + shortageQty + '. Die Eingangsmenge wird nach Aktivierung der Charge zur Nachallokation verwendet.'; } const setValue = (selector, value) => { const field = drawer?.querySelector(selector); if (field) { field.value = value || ''; } }; setValue('[data-lager-new-lot-number]', trigger.dataset.lagerOpenLotNumber); setValue('[data-lager-new-manufacturer]', trigger.dataset.lagerOpenManufacturerPartyId); setValue('[data-lager-new-manufacturer-lot]', trigger.dataset.lagerOpenManufacturerLotNumber); setValue('[data-lager-new-mfg-date]', trigger.dataset.lagerOpenMfgDate); setValue('[data-lager-new-expiry-date]', trigger.dataset.lagerOpenExpiryDate); setValue('[data-lager-new-purchase-date]', trigger.dataset.lagerOpenPurchaseDate); if (drawer) { drawer.dataset.lagerOpenLotId = trigger.dataset.lagerOpenLotId || ''; drawer.dataset.lagerProductId = productId; } });"; echo " const appendLagerOpenLotCard = (lot, balance) => { if (lot?.status === 'current') { syncLagerActiveLotView(lot); syncLagerBalanceView(balance); return null; } const drawer = getDrawer(); const product = drawer?.querySelector('[data-lager-open-product=\"' + (drawer?.dataset.lagerProductId || '') + '\"]'); const list = product?.querySelector('[data-lager-open-lot-list]'); const templateProduct = drawer?.querySelector('[data-lager-open-lot-template]')?.content.querySelector('[data-lager-open-product]'); const card = templateProduct?.querySelector('[data-lager-open-card-wrapper]')?.cloneNode(true); const lotId = String(lot?.id || lot?.lot_id || ''); if (!list || !card || !lotId) { return null; } card.dataset.lagerOpenCardWrapper = lotId; const readView = card.querySelector('[data-lager-open-card]'); if (readView) { readView.dataset.lagerOpenCard = lotId; } const editTitle = card.querySelector('[id^=\"lager-open-charge-data-\"]'); if (editTitle) { editTitle.id = 'lager-open-charge-data-' + lotId; editTitle.closest('section')?.setAttribute('aria-labelledby', editTitle.id); } const confirmationTitle = card.querySelector('[id^=\"lager-open-confirmation-title-\"]'); if (confirmationTitle) { confirmationTitle.id = 'lager-open-confirmation-title-' + lotId; confirmationTitle.closest('[data-lager-open-confirmation-card]')?.setAttribute('aria-labelledby', confirmationTitle.id); } const deleteTitle = card.querySelector('[id^=\"lager-open-delete-title-\"]'); if (deleteTitle) { deleteTitle.id = 'lager-open-delete-title-' + lotId; deleteTitle.closest('[data-lager-open-delete-confirmation]')?.setAttribute('aria-labelledby', deleteTitle.id); } const setValue = (selector, value) => { const input = card.querySelector(selector); if (input) { input.value = value ?? ''; } }; setValue('[data-lager-open-lot-number]', lot.lot_number); setValue('[data-lager-open-manufacturer]', lot.manufacturer_party_id); setValue('[data-lager-open-manufacturer-lot]', lot.manufacturer_lot_number); setValue('[data-lager-open-mfg-date]', lot.mfg_date); setValue('[data-lager-open-expiry-date]', lot.expiry_date); setValue('[data-lager-open-purchase-date]', lot.purchase_date); setValue('[data-lager-open-target-qty]', balance?.qty_net ?? 0); const qualified = Boolean(lot.lot_number && lot.manufacturer_party_id && lot.manufacturer_lot_number && lot.mfg_date && lot.expiry_date && lot.purchase_date && Number(balance?.qty_net || 0) > 0); const selected = lot.next_active === true; card.classList.toggle('sg-lager-open-lot--next-active', selected); const selectedLabel = card.querySelector('[data-lager-open-card-next-active]'); if (selectedLabel) { selectedLabel.textContent = selected ? 'Als nächste aktive Charge ausgewählt' : 'Nicht ausgewählt'; } const selectButton = card.querySelector('[data-lager-open-select]'); if (selectButton) { selectButton.dataset.lagerOpenSelectable = String(qualified); selectButton.disabled = !qualified || selected; selectButton.setAttribute('aria-disabled', String(!qualified || selected)); } list.append(card); syncLagerOpenLotCard(lot, balance, card); return card; };";
echo " document.addEventListener('click', (event) => { const openButton = event.target.closest('[data-lager-new-lot-open]'); if (openButton) { const drawer = getDrawer(); const form = drawer?.querySelector('[data-lager-drawer-topic=\"new-lot-form\"]'); const action = drawer?.querySelector('[data-lager-drawer-topic=\"new-lot-action\"]'); if (form) { form.hidden = false; } if (action) { action.hidden = true; } return; } if (event.target.closest('[data-lager-new-lot-close]')) { const drawer = getDrawer(); const form = drawer?.querySelector('[data-lager-drawer-topic=\"new-lot-form\"]'); const action = drawer?.querySelector('[data-lager-drawer-topic=\"new-lot-action\"]'); if (form) { form.hidden = true; } if (action) { action.hidden = false; } } });"; echo " const resetLagerNewLotConfirmation = () => { const drawer = getDrawer(); const confirmation = drawer?.querySelector('[data-lager-new-confirmation-card]'); const list = drawer?.querySelector('[data-lager-new-confirmation-empty-fields]'); const form = drawer?.querySelector('[data-lager-new-lot-form-element]'); if (confirmation) { confirmation.hidden = true; } if (list) { list.replaceChildren(); } if (form) { form.dataset.confirmationAccepted = 'false'; } if (drawer) { drawer.dataset.dialogOpen = 'false'; } };";
echo " document.addEventListener('click', (event) => { const editButton = event.target.closest('[data-lager-open-edit-open]'); if (editButton) { const card = editButton.closest('[data-lager-open-card-wrapper]'); const form = card?.querySelector('[data-lager-open-edit-form]'); const readView = card?.querySelector('[data-lager-open-card]'); const readDocuments = card?.querySelector('[data-lager-documents^=\"open-read-\"]'); const documents = card?.querySelector('[data-lager-edit-documents]'); const actions = card?.querySelector('[data-lager-open-edit-actions]'); if (form) { form.hidden = false; } if (readView) { readView.hidden = true; } if (readDocuments) { readDocuments.hidden = true; } if (documents) { documents.hidden = false; } if (actions) { actions.hidden = false; } return; } const closeButton = event.target.closest('[data-lager-open-edit-close]'); if (closeButton) { const card = closeButton.closest('[data-lager-open-card-wrapper]'); const form = card?.querySelector('[data-lager-open-edit-form]'); const readView = card?.querySelector('[data-lager-open-card]'); const readDocuments = card?.querySelector('[data-lager-documents^=\"open-read-\"]'); const documents = card?.querySelector('[data-lager-edit-documents]'); const actions = card?.querySelector('[data-lager-open-edit-actions]'); if (form) { form.hidden = true; } if (readView) { readView.hidden = false; } if (readDocuments) { readDocuments.hidden = false; } if (documents) { documents.hidden = true; } if (actions) { actions.hidden = true; } } });"; echo " const resetLagerOpenLotConfirmations = () => { const drawer = getDrawer(); drawer?.querySelectorAll('[data-lager-open-card-wrapper]').forEach((card) => { const confirmation = card.querySelector('[data-lager-open-confirmation-card]'); const list = card.querySelector('[data-lager-open-confirmation-empty-fields]'); const form = card.querySelector('[data-lager-open-edit-form-element]'); if (confirmation) { confirmation.hidden = true; } if (list) { list.replaceChildren(); } if (form) { form.dataset.confirmationAccepted = 'false'; } card.dataset.dialogOpen = 'false'; }); };";
echo " document.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 drawer = getDrawer(); const productId = trigger.closest('[data-lager-row]')?.dataset.lagerId || ''; const openProduct = drawer?.querySelector('[data-lager-open-product=\"' + productId + '\"]'); drawer?.querySelectorAll('[data-lager-open-product]').forEach((product) => { product.hidden = product !== openProduct; product.querySelectorAll('[data-lager-open-edit-form]').forEach((form) => { form.hidden = true; }); product.querySelectorAll('[data-lager-open-card]').forEach((readView) => { readView.hidden = false; }); }); resetLagerNewLotConfirmation(); resetLagerOpenLotConfirmations(); const newForm = drawer?.querySelector('[data-lager-drawer-topic=\"new-lot-form\"]'); const newAction = drawer?.querySelector('[data-lager-drawer-topic=\"new-lot-action\"]'); if (newForm) { newForm.hidden = true; } if (newAction) { newAction.hidden = false; } const notificationWrapper = drawer?.querySelector('[data-lager-new-lot-notification-wrapper]'); const notification = drawer?.querySelector('[data-lager-new-lot-notification]'); const shortageQty = trigger.dataset.lagerDrawerVirtualShortageQty || '0'; const hasShortage = shortageQty !== '0' && shortageQty !== '0,00'; if (notificationWrapper) { notificationWrapper.hidden = !hasShortage; } if (notification && hasShortage) { notification.textContent = 'Für dieses Produkt bestehen virtuelle Fehlbestände in Höhe von ' + shortageQty + '. Die Eingangsmenge wird nach Aktivierung der Charge zur Nachallokation verwendet.'; } const setValue = (selector, value) => { const field = drawer?.querySelector(selector); if (field) { field.value = value || ''; } }; setValue('[data-lager-new-lot-number]', ''); setValue('[data-lager-new-manufacturer]', ''); setValue('[data-lager-new-manufacturer-lot]', ''); setValue('[data-lager-new-mfg-date]', ''); setValue('[data-lager-new-expiry-date]', ''); setValue('[data-lager-new-purchase-date]', ''); setValue('[data-lager-new-receipt-qty]', ''); if (drawer) { drawer.dataset.lagerOpenLotId = ''; drawer.dataset.lagerProductId = productId; } });";
echo " document.addEventListener('click', (event) => { const openButton = event.target.closest('[data-lager-new-lot-open]'); if (openButton) { const drawer = getDrawer(); const form = drawer?.querySelector('[data-lager-drawer-topic=\"new-lot-form\"]'); const action = drawer?.querySelector('[data-lager-drawer-topic=\"new-lot-action\"]'); resetLagerNewLotConfirmation(); if (form) { form.hidden = false; } if (action) { action.hidden = true; } return; } if (event.target.closest('[data-lager-new-lot-close]')) { const drawer = getDrawer(); const form = drawer?.querySelector('[data-lager-drawer-topic=\"new-lot-form\"]'); const action = drawer?.querySelector('[data-lager-drawer-topic=\"new-lot-action\"]'); resetLagerNewLotConfirmation(); if (form) { form.hidden = true; } if (action) { action.hidden = false; } } });";
echo " document.addEventListener('click', (event) => { const editButton = event.target.closest('[data-lager-open-edit-open]'); if (editButton) { const card = editButton.closest('[data-lager-open-card-wrapper]'); const form = card?.querySelector('[data-lager-open-edit-form]'); const readView = card?.querySelector('[data-lager-open-card]'); resetLagerOpenLotConfirmations(); if (form) { form.hidden = false; } if (readView) { readView.hidden = true; } return; } const closeButton = event.target.closest('[data-lager-open-edit-close]'); if (closeButton) { const card = closeButton.closest('[data-lager-open-card-wrapper]'); const form = card?.querySelector('[data-lager-open-edit-form]'); const readView = card?.querySelector('[data-lager-open-card]'); resetLagerOpenLotConfirmations(); if (form) { form.hidden = true; } if (readView) { readView.hidden = false; } } });";
echo " document.addEventListener('input', (event) => { const input = event.target.closest('[data-lager-open-delete-confirmation-input]'); if (!input) { return; } const card = input.closest('[data-lager-open-card-wrapper]'); const submit = card?.querySelector('[data-lager-open-delete-confirmation-submit]'); const valid = input.value === 'DELETE'; if (submit) { submit.disabled = !valid; submit.setAttribute('aria-disabled', String(!valid)); submit.classList.toggle('sg-button--process-inactive', !valid); } });"; echo " document.addEventListener('input', (event) => { const input = event.target.closest('[data-lager-open-delete-confirmation-input]'); if (!input) { return; } const card = input.closest('[data-lager-open-card-wrapper]'); const submit = card?.querySelector('[data-lager-open-delete-confirmation-submit]'); const valid = input.value === 'DELETE'; if (submit) { submit.disabled = !valid; submit.setAttribute('aria-disabled', String(!valid)); submit.classList.toggle('sg-button--process-inactive', !valid); } });";
echo " document.addEventListener('click', async (event) => { const deleteButton = event.target.closest('[data-lager-open-delete]'); if (deleteButton) { const card = deleteButton.closest('[data-lager-open-card-wrapper]'); const confirmation = card?.querySelector('[data-lager-open-delete-confirmation]'); const input = card?.querySelector('[data-lager-open-delete-confirmation-input]'); const submit = card?.querySelector('[data-lager-open-delete-confirmation-submit]'); if (input) { input.value = ''; } if (submit) { submit.disabled = true; submit.setAttribute('aria-disabled', 'true'); submit.classList.add('sg-button--process-inactive'); } if (confirmation) { confirmation.hidden = false; } if (card) { card.dataset.dialogOpen = 'true'; } return; } const cancel = event.target.closest('[data-lager-open-delete-confirmation-cancel]'); if (cancel) { const card = cancel.closest('[data-lager-open-card-wrapper]'); const confirmation = card?.querySelector('[data-lager-open-delete-confirmation]'); if (confirmation) { confirmation.hidden = true; } if (card) { card.dataset.dialogOpen = 'false'; } return; } const submit = event.target.closest('[data-lager-open-delete-confirmation-submit]'); if (!submit || submit.disabled) { return; } const card = submit.closest('[data-lager-open-card-wrapper]'); const confirmation = card?.querySelector('[data-lager-open-delete-confirmation]'); const status = card?.querySelector('[data-lager-open-delete-status]'); if (status) { status.textContent = 'Löschen läuft …'; } submit.disabled = true; try { await postLagerChange({ action: 'delete_open_lot', lotId: card?.dataset.lagerOpenCardWrapper || '' }); card?.remove(); } catch (error) { if (status) { status.textContent = error instanceof Error ? error.message : 'Löschen fehlgeschlagen'; } submit.disabled = false; } });"; echo " document.addEventListener('click', async (event) => { const selectButton = event.target.closest('[data-lager-open-select]'); if (!selectButton) { return; } const card = selectButton.closest('[data-lager-open-card-wrapper]'); const product = card?.closest('[data-lager-open-product]'); const drawer = getDrawer(); const productId = product?.dataset.lagerOpenProduct || drawer?.dataset.lagerProductId || ''; const lotId = card?.dataset.lagerOpenCardWrapper || ''; if (!productId || !lotId) { return; } selectButton.disabled = true; try { const result = await postLagerChange({ action: 'select_open_lot', productId, lotId }); if (result.currentLot?.id && String(result.currentLot.id) === String(lotId)) { card?.remove(); syncLagerActiveLotView(result.currentLot); syncLagerBalanceView({ qty_net: result.currentLot.qty_net, qty_in: result.currentLot.qty_in, qty_out: result.currentLot.qty_out }); return; } product.querySelectorAll('[data-lager-open-card-wrapper]').forEach((candidate) => { const selected = candidate === card; candidate.classList.toggle('sg-lager-open-lot--next-active', selected); const label = candidate.querySelector('[data-lager-open-card-next-active]'); if (label) { label.textContent = selected ? 'Als nächste aktive Charge ausgewählt' : 'Nicht ausgewählt'; } const button = candidate.querySelector('[data-lager-open-select]'); if (button) { button.disabled = selected || button.dataset.lagerOpenSelectable !== 'true'; button.setAttribute('aria-disabled', String(button.disabled)); } }); } catch (error) { const status = card?.querySelector('[data-lager-open-edit-status]'); if (status) { status.textContent = error instanceof Error ? error.message : 'Auswahl fehlgeschlagen'; } selectButton.disabled = false; selectButton.setAttribute('aria-disabled', 'false'); } });";
echo " document.addEventListener('click', async (event) => { const deleteButton = event.target.closest('[data-lager-open-delete]'); if (deleteButton) { const card = deleteButton.closest('[data-lager-open-card-wrapper]'); const confirmation = card?.querySelector('[data-lager-open-delete-confirmation]'); const input = card?.querySelector('[data-lager-open-delete-confirmation-input]'); const submit = card?.querySelector('[data-lager-open-delete-confirmation-submit]'); if (input) { input.value = ''; } if (submit) { submit.disabled = true; submit.setAttribute('aria-disabled', 'true'); submit.classList.add('sg-button--process-inactive'); } if (confirmation) { confirmation.hidden = false; } if (card) { card.dataset.dialogOpen = 'true'; } return; } const cancel = event.target.closest('[data-lager-open-delete-confirmation-cancel]'); if (cancel) { const card = cancel.closest('[data-lager-open-card-wrapper]'); const confirmation = card?.querySelector('[data-lager-open-delete-confirmation]'); if (confirmation) { confirmation.hidden = true; } if (card) { card.dataset.dialogOpen = 'false'; } return; } const submit = event.target.closest('[data-lager-open-delete-confirmation-submit]'); if (!submit || submit.disabled) { return; } const card = submit.closest('[data-lager-open-card-wrapper]'); const confirmation = card?.querySelector('[data-lager-open-delete-confirmation]'); const status = card?.querySelector('[data-lager-open-delete-status]'); if (status) { status.textContent = 'Löschen läuft …'; } submit.disabled = true; try { const result = await postLagerChange({ action: 'delete_open_lot', lotId: card?.dataset.lagerOpenCardWrapper || '' }); if (result.currentLot) { syncLagerActiveLotView(result.currentLot); syncLagerBalanceView({ qty_net: result.currentLot.qty_net, qty_in: result.currentLot.qty_in, qty_out: result.currentLot.qty_out }); } const productId = card?.closest('[data-lager-open-product]')?.dataset.lagerOpenProduct || ''; contentRoot.querySelectorAll('[data-lager-drawer-open]').forEach((trigger) => { if (trigger.closest('[data-lager-row]')?.dataset.lagerId === productId) { trigger.dataset.lagerOpenLotId = ''; } }); const drawer = getDrawer(); if (drawer) { drawer.dataset.lagerOpenLotId = ''; } card?.remove(); } catch (error) { if (status) { status.textContent = error instanceof Error ? error.message : 'Löschen fehlgeschlagen'; } submit.disabled = false; } });";
echo " document.addEventListener('click', (event) => { const cancel = event.target.closest('[data-lager-open-confirmation-cancel]'); if (cancel) { const card = cancel.closest('[data-lager-open-card-wrapper]'); const confirmation = card?.querySelector('[data-lager-open-confirmation-card]'); if (confirmation) { confirmation.hidden = true; } if (card) { card.dataset.dialogOpen = 'false'; } const form = card?.querySelector('[data-lager-open-edit-form-element]'); if (form) { form.dataset.confirmationAccepted = 'false'; } return; } const confirm = event.target.closest('[data-lager-open-confirmation-submit]'); if (confirm) { const card = confirm.closest('[data-lager-open-card-wrapper]'); const confirmation = card?.querySelector('[data-lager-open-confirmation-card]'); const form = card?.querySelector('[data-lager-open-edit-form-element]'); if (confirmation) { confirmation.hidden = true; } if (card) { card.dataset.dialogOpen = 'false'; } if (form) { form.dataset.confirmationAccepted = 'true'; form.requestSubmit(); } } });"; echo " document.addEventListener('click', (event) => { const cancel = event.target.closest('[data-lager-open-confirmation-cancel]'); if (cancel) { const card = cancel.closest('[data-lager-open-card-wrapper]'); const confirmation = card?.querySelector('[data-lager-open-confirmation-card]'); if (confirmation) { confirmation.hidden = true; } if (card) { card.dataset.dialogOpen = 'false'; } const form = card?.querySelector('[data-lager-open-edit-form-element]'); if (form) { form.dataset.confirmationAccepted = 'false'; } return; } const confirm = event.target.closest('[data-lager-open-confirmation-submit]'); if (confirm) { const card = confirm.closest('[data-lager-open-card-wrapper]'); const confirmation = card?.querySelector('[data-lager-open-confirmation-card]'); const form = card?.querySelector('[data-lager-open-edit-form-element]'); if (confirmation) { confirmation.hidden = true; } if (card) { card.dataset.dialogOpen = 'false'; } if (form) { form.dataset.confirmationAccepted = 'true'; form.requestSubmit(); } } });";
echo " document.addEventListener('submit', async (event) => { const openLotForm = event.target.closest('[data-lager-open-edit-form-element]'); if (!openLotForm) { return; } event.preventDefault(); const drawer = getDrawer(); const card = openLotForm.closest('[data-lager-open-card-wrapper]'); const editSection = card?.querySelector('[data-lager-open-edit-form]'); const fields = [{ label: 'Naurua-Chargennummer', input: openLotForm.querySelector('[data-lager-open-lot-number]') }, { label: 'Hersteller', input: openLotForm.querySelector('[data-lager-open-manufacturer]') }, { label: 'Hersteller-Chargennummer', input: openLotForm.querySelector('[data-lager-open-manufacturer-lot]') }, { label: 'Herstellungsdatum', input: openLotForm.querySelector('[data-lager-open-mfg-date]') }, { label: 'MHD', input: openLotForm.querySelector('[data-lager-open-expiry-date]') }, { label: 'Einkaufsdatum', input: openLotForm.querySelector('[data-lager-open-purchase-date]') }, { label: 'Menge', input: openLotForm.querySelector('[data-lager-open-target-qty]') }]; const emptyFields = fields.filter((field) => (field.input?.value || '').trim() === ''); if (emptyFields.length > 0 && openLotForm.dataset.confirmationAccepted !== 'true') { const confirmation = card?.querySelector('[data-lager-open-confirmation-card]'); const emptyFieldsList = card?.querySelector('[data-lager-open-confirmation-empty-fields]'); if (emptyFieldsList) { emptyFieldsList.replaceChildren(...emptyFields.map((field) => { const item = document.createElement('div'); item.className = 'sg-body'; item.textContent = field.label; return item; })); } if (confirmation) { confirmation.hidden = false; } if (card) { card.dataset.dialogOpen = 'true'; } return; } openLotForm.dataset.confirmationAccepted = 'false'; const status = openLotForm.closest('[data-lager-open-edit-form]')?.querySelector('[data-lager-open-edit-status]'); if (status) { status.textContent = 'Speichern läuft …'; } try { const lotId = card?.dataset.lagerOpenCardWrapper || ''; const uploadedDocuments = await uploadLagerDocuments(card?.querySelector('[data-lager-documents=open-edit-' + lotId + ']'), lotId); const existingDocuments = Array.from(card?.querySelectorAll('[data-lager-documents^=\"open-read-\"] [data-lager-document-row]') || []).map((row) => ({ id: Number(row.dataset.lagerDocumentRow || 0), original_filename: row.querySelector('a')?.textContent || 'Dokument' })); const targetQtyInput = openLotForm.querySelector('[data-lager-open-target-qty]'); const result = await postLagerChange({ action: 'prepare_lot', productId: drawer?.dataset.lagerProductId || '', lotId, lotNumber: openLotForm.querySelector('[data-lager-open-lot-number]')?.value || '', manufacturerPartyId: openLotForm.querySelector('[data-lager-open-manufacturer]')?.value || null, manufacturerLotNumber: openLotForm.querySelector('[data-lager-open-manufacturer-lot]')?.value || '', mfgDate: openLotForm.querySelector('[data-lager-open-mfg-date]')?.value || '', expiryDate: openLotForm.querySelector('[data-lager-open-expiry-date]')?.value || '', purchaseDate: openLotForm.querySelector('[data-lager-open-purchase-date]')?.value || '', targetQty: targetQtyInput?.value.trim() === '' ? '0' : targetQtyInput?.value || '' }); syncLagerDocumentViews(card, [...existingDocuments, ...uploadedDocuments]); syncLagerOpenLotCard(result.lot, result.balance); if (editSection) { editSection.hidden = true; } card?.querySelector('[data-lager-open-card]')?.removeAttribute('hidden'); if (status) { status.textContent = ''; } } catch (error) { if (status) { status.textContent = error instanceof Error ? error.message : 'Speichern fehlgeschlagen'; } } });"; echo " document.addEventListener('submit', async (event) => { const openLotForm = event.target.closest('[data-lager-open-edit-form-element]'); if (!openLotForm) { return; } event.preventDefault(); const drawer = getDrawer(); const card = openLotForm.closest('[data-lager-open-card-wrapper]'); const editSection = card?.querySelector('[data-lager-open-edit-form]'); const fields = [{ label: 'Naurua-Chargennummer', input: openLotForm.querySelector('[data-lager-open-lot-number]') }, { label: 'Hersteller', input: openLotForm.querySelector('[data-lager-open-manufacturer]') }, { label: 'Hersteller-Chargennummer', input: openLotForm.querySelector('[data-lager-open-manufacturer-lot]') }, { label: 'Herstellungsdatum', input: openLotForm.querySelector('[data-lager-open-mfg-date]') }, { label: 'MHD', input: openLotForm.querySelector('[data-lager-open-expiry-date]') }, { label: 'Einkaufsdatum', input: openLotForm.querySelector('[data-lager-open-purchase-date]') }, { label: 'Menge', input: openLotForm.querySelector('[data-lager-open-target-qty]') }]; const emptyFields = fields.filter((field) => (field.input?.value || '').trim() === ''); if (emptyFields.length > 0 && openLotForm.dataset.confirmationAccepted !== 'true') { const confirmation = card?.querySelector('[data-lager-open-confirmation-card]'); const emptyFieldsList = card?.querySelector('[data-lager-open-confirmation-empty-fields]'); if (emptyFieldsList) { emptyFieldsList.replaceChildren(...emptyFields.map((field) => { const item = document.createElement('div'); item.className = 'sg-body'; item.textContent = field.label; return item; })); } if (confirmation) { confirmation.hidden = false; } if (card) { card.dataset.dialogOpen = 'true'; } return; } openLotForm.dataset.confirmationAccepted = 'false'; const status = openLotForm.querySelector('[data-lager-open-edit-status]'); if (status) { status.textContent = 'Speichern läuft …'; } try { const targetQtyInput = openLotForm.querySelector('[data-lager-open-target-qty]'); const result = await postLagerChange({ action: 'prepare_lot', productId: drawer?.dataset.lagerProductId || '', lotId: card?.dataset.lagerOpenCardWrapper || '', lotNumber: openLotForm.querySelector('[data-lager-open-lot-number]')?.value || '', manufacturerPartyId: openLotForm.querySelector('[data-lager-open-manufacturer]')?.value || null, manufacturerLotNumber: openLotForm.querySelector('[data-lager-open-manufacturer-lot]')?.value || '', mfgDate: openLotForm.querySelector('[data-lager-open-mfg-date]')?.value || '', purchaseDate: openLotForm.querySelector('[data-lager-open-purchase-date]')?.value || '', expiryDate: openLotForm.querySelector('[data-lager-open-expiry-date]')?.value || '', targetQty: targetQtyInput?.value.trim() === '' ? '0' : targetQtyInput?.value || '' }); if (result.lot?.status === 'current') { card?.remove(); syncLagerActiveLotView(result.lot); syncLagerBalanceView(result.balance); } else { syncLagerOpenLotCard(result.lot, result.balance); } if (editSection) { editSection.hidden = true; } card?.querySelector('[data-lager-open-card]')?.removeAttribute('hidden'); if (status) { status.textContent = ''; } } catch (error) { if (status) { status.textContent = error instanceof Error ? error.message : 'Speichern fehlgeschlagen'; } } });";
echo " document.addEventListener('submit', async (event) => { const newLotForm = event.target.closest('[data-lager-new-lot-form-element]'); if (!newLotForm) { return; } event.preventDefault(); const drawer = getDrawer(); const status = drawer?.querySelector('[data-lager-new-lot-status]'); if (status) { status.textContent = 'Speichern läuft …'; } try { const result = await postLagerChange({ action: 'prepare_lot', productId: drawer?.dataset.lagerProductId || '', lotId: drawer?.dataset.lagerOpenLotId || '', lotNumber: drawer?.querySelector('[data-lager-new-lot-number]')?.value || '', manufacturerPartyId: drawer?.querySelector('[data-lager-new-manufacturer]')?.value || null, manufacturerLotNumber: drawer?.querySelector('[data-lager-new-manufacturer-lot]')?.value || '', mfgDate: drawer?.querySelector('[data-lager-new-mfg-date]')?.value || '', expiryDate: drawer?.querySelector('[data-lager-new-expiry-date]')?.value || '', purchaseDate: drawer?.querySelector('[data-lager-new-purchase-date]')?.value || '', receiptQty: drawer?.querySelector('[data-lager-new-receipt-qty]')?.value || '' }); syncLagerOpenLotCard(result.lot, result.balance); const form = drawer?.querySelector('[data-lager-drawer-topic=\"new-lot-form\"]'); const action = drawer?.querySelector('[data-lager-drawer-topic=\"new-lot-action\"]'); if (form) { form.hidden = true; } if (action) { action.hidden = false; } if (status) { status.textContent = ''; } } catch (error) { if (status) { status.textContent = error instanceof Error ? error.message : 'Speichern fehlgeschlagen'; } } });"; echo " document.addEventListener('click', (event) => { const cancel = event.target.closest('[data-lager-new-confirmation-cancel]'); if (cancel) { const drawer = getDrawer(); const confirmation = drawer?.querySelector('[data-lager-new-confirmation-card]'); if (confirmation) { confirmation.hidden = true; } if (drawer) { drawer.dataset.dialogOpen = 'false'; } const form = drawer?.querySelector('[data-lager-new-lot-form-element]'); if (form) { form.dataset.confirmationAccepted = 'false'; } return; } const confirm = event.target.closest('[data-lager-new-confirmation-submit]'); if (confirm) { const drawer = getDrawer(); const confirmation = drawer?.querySelector('[data-lager-new-confirmation-card]'); const form = drawer?.querySelector('[data-lager-new-lot-form-element]'); if (confirmation) { confirmation.hidden = true; } if (drawer) { drawer.dataset.dialogOpen = 'false'; } if (form) { form.dataset.confirmationAccepted = 'true'; form.requestSubmit(); } } });";
echo " document.addEventListener('submit', async (event) => { const editForm = event.target.closest('[data-lager-edit-form-element]'); if (!editForm) { return; } event.preventDefault(); const drawer = getDrawer(); const status = drawer?.querySelector('[data-lager-edit-status]'); if (status) { status.textContent = 'Speichern läuft …'; } try { const lotId = drawer?.dataset.lagerActiveLotId || ''; const uploadedDocuments = await uploadLagerDocuments(drawer?.querySelector('[data-lager-documents=\"current-edit\"]'), lotId); const result = await postLagerChange({ action: 'update_lot', lotId, 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 || '', receiptQty: drawer?.querySelector('[data-lager-receipt-qty]')?.value || '', adjustmentQty: drawer?.querySelector('[data-lager-adjustment-qty]')?.value || '', outQty: drawer?.querySelector('[data-lager-out-qty]')?.value || '' }); const trigger = contentRoot.querySelector('[data-lager-drawer-open][aria-expanded=\"true\"]'); if (trigger && uploadedDocuments.length) { let existingDocuments = []; try { existingDocuments = JSON.parse(trigger.dataset.lagerDrawerDocuments || '[]'); } catch (error) { existingDocuments = []; } trigger.dataset.lagerDrawerDocuments = JSON.stringify([...existingDocuments, ...uploadedDocuments]); syncLagerCurrentDocuments(trigger); } syncLagerReadView(); syncLagerBalanceView(result.balance); clearLagerMovementInputs(); setLagerEditMode(false); if (status) { status.textContent = ''; } } catch (error) { if (status) { status.textContent = error instanceof Error ? error.message : 'Speichern fehlgeschlagen'; } } });"; echo " document.addEventListener('submit', async (event) => { const newLotForm = event.target.closest('[data-lager-new-lot-form-element]'); if (!newLotForm) { return; } event.preventDefault(); const drawer = getDrawer(); const fields = [{ label: 'Naurua-Chargennummer', input: drawer?.querySelector('[data-lager-new-lot-number]') }, { label: 'Hersteller', input: drawer?.querySelector('[data-lager-new-manufacturer]') }, { label: 'Hersteller-Chargennummer', input: drawer?.querySelector('[data-lager-new-manufacturer-lot]') }, { label: 'Herstellungsdatum', input: drawer?.querySelector('[data-lager-new-mfg-date]') }, { label: 'MHD', input: drawer?.querySelector('[data-lager-new-expiry-date]') }, { label: 'Einkaufsdatum', input: drawer?.querySelector('[data-lager-new-purchase-date]') }, { label: 'Eingangsmenge', input: drawer?.querySelector('[data-lager-new-receipt-qty]') }]; const emptyFields = fields.filter((field) => (field.input?.value || '').trim() === ''); if (emptyFields.length > 0 && newLotForm.dataset.confirmationAccepted !== 'true') { const confirmation = drawer?.querySelector('[data-lager-new-confirmation-card]'); const emptyFieldsList = drawer?.querySelector('[data-lager-new-confirmation-empty-fields]'); if (emptyFieldsList) { emptyFieldsList.replaceChildren(...emptyFields.map((field) => { const item = document.createElement('div'); item.className = 'sg-body'; item.textContent = field.label; return item; })); } if (confirmation) { confirmation.hidden = false; } if (drawer) { drawer.dataset.dialogOpen = 'true'; } return; } newLotForm.dataset.confirmationAccepted = 'false'; const status = drawer?.querySelector('[data-lager-new-lot-status]'); if (status) { status.textContent = 'Speichern läuft …'; } try { const result = await postLagerChange({ action: 'prepare_lot', productId: drawer?.dataset.lagerProductId || '', lotId: '', lotNumber: drawer?.querySelector('[data-lager-new-lot-number]')?.value || '', manufacturerPartyId: drawer?.querySelector('[data-lager-new-manufacturer]')?.value || null, manufacturerLotNumber: drawer?.querySelector('[data-lager-new-manufacturer-lot]')?.value || '', mfgDate: drawer?.querySelector('[data-lager-new-mfg-date]')?.value || '', expiryDate: drawer?.querySelector('[data-lager-new-expiry-date]')?.value || '', purchaseDate: drawer?.querySelector('[data-lager-new-purchase-date]')?.value || '', receiptQty: drawer?.querySelector('[data-lager-new-receipt-qty]')?.value || '' }); appendLagerOpenLotCard(result.lot, result.balance); resetLagerNewLotConfirmation(); const form = drawer?.querySelector('[data-lager-drawer-topic=\"new-lot-form\"]'); const action = drawer?.querySelector('[data-lager-drawer-topic=\"new-lot-action\"]'); if (form) { form.hidden = true; } if (action) { action.hidden = false; } if (status) { status.textContent = ''; } } catch (error) { if (status) { status.textContent = error instanceof Error ? error.message : 'Speichern fehlgeschlagen'; } } });";
echo " document.addEventListener('submit', async (event) => { const editForm = event.target.closest('[data-lager-edit-form-element]'); if (!editForm) { return; } event.preventDefault(); const drawer = getDrawer(); const status = drawer?.querySelector('[data-lager-edit-status]'); if (status) { status.textContent = 'Speichern läuft …'; } try { const result = 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 || '', receiptQty: drawer?.querySelector('[data-lager-receipt-qty]')?.value || '', adjustmentQty: drawer?.querySelector('[data-lager-adjustment-qty]')?.value || '', outQty: drawer?.querySelector('[data-lager-out-qty]')?.value || '' }); syncLagerActiveLotView(result.lot); syncLagerBalanceView(result.balance); clearLagerMovementInputs(); setLagerEditMode(false); if (status) { status.textContent = ''; } } catch (error) { if (status) { status.textContent = error instanceof Error ? error.message : 'Speichern fehlgeschlagen'; } } });";
echo " document.addEventListener('click', (event) => { if (event.target.closest('[data-lager-detail-back]')) { event.preventDefault(); closeLagerMobileDetail(); } });"; echo " document.addEventListener('click', (event) => { if (event.target.closest('[data-lager-detail-back]')) { event.preventDefault(); closeLagerMobileDetail(); } });";
echo " document.addEventListener('keydown', (event) => { if (event.key === 'Escape') { closeDrawer(); closeLagerMobileDetail(); } });"; echo " document.addEventListener('keydown', (event) => { if (event.key === 'Escape') { closeDrawer(); closeLagerMobileDetail(); } });";
echo " window.addEventListener('popstate', (event) => { if (lagerMobileDetailOpen && (!event.state || event.state.lagerView !== 'detail')) { restoreLagerMobileDetail(); } });"; echo " window.addEventListener('popstate', (event) => { if (lagerMobileDetailOpen && (!event.state || event.state.lagerView !== 'detail')) { restoreLagerMobileDetail(); } });";
-30
View File
@@ -1,30 +0,0 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../modules/shared/db.php';
require_once __DIR__ . '/../../modules/shared/auth/service.php';
require_once __DIR__ . '/../../modules/erp/lager/service.php';
$env = expand_env_values(parse_env_file(__DIR__ . '/../../.env'));
try {
$pdo = connect_database($env);
auth_bootstrap_session();
if (auth_current_user($pdo) === null) {
http_response_code(401);
exit('Unauthorized');
}
$document = get_stock_lot_document($pdo, (int) ($_GET['id'] ?? 0));
$path = dirname(__DIR__, 2) . '/runtime/uploads/lager/' . basename((string) $document['stored_filename']);
if (!is_file($path)) {
http_response_code(404);
exit('Not Found');
}
header('Content-Type: application/octet-stream');
header('Content-Length: ' . (string) filesize($path));
header("Content-Disposition: attachment; filename*=UTF-8''" . rawurlencode((string) $document['original_filename']));
readfile($path);
} catch (Throwable) {
http_response_code(404);
exit('Not Found');
}
+2 -13
View File
@@ -247,7 +247,7 @@ body.sg-lager-mobile-detail-open > :not([data-lager-mobile-detail-host]) {
} }
.sg-large-table--lager[data-lager-table-key="products"] .sg-large-table__row { .sg-large-table--lager[data-lager-table-key="products"] .sg-large-table__row {
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(5, minmax(0, 1fr));
} }
.sg-large-table--lager[data-lager-table-key="products"] .sg-large-table__cell:last-child { .sg-large-table--lager[data-lager-table-key="products"] .sg-large-table__cell:last-child {
@@ -275,21 +275,10 @@ body.sg-lager-mobile-detail-open > :not([data-lager-mobile-detail-host]) {
.sg-card-list-page-drawer [data-lager-drawer-topic="new-lot-form"][hidden], .sg-card-list-page-drawer [data-lager-drawer-topic="new-lot-form"][hidden],
.sg-card-list-page-drawer [data-lager-new-lot-notification-wrapper][hidden], .sg-card-list-page-drawer [data-lager-new-lot-notification-wrapper][hidden],
.sg-card-list-page-drawer [data-lager-open-edit-form][hidden], .sg-card-list-page-drawer [data-lager-open-edit-form][hidden],
.sg-card-list-page-drawer [data-lager-open-edit-actions][hidden], .sg-card-list-page-drawer [data-lager-open-card][hidden] {
.sg-card-list-page-drawer [data-lager-open-card][hidden],
.sg-card-list-page-drawer [data-lager-edit-documents][hidden],
.sg-card-list-page-drawer [data-lager-edit-actions][hidden] {
display: none; display: none;
} }
.sg-card-list-page-drawer .sg-file-upload {
max-width: none;
}
.sg-card-list-page-drawer .sg-file-upload__dropzone {
box-sizing: border-box;
}
.sg-card-list-page-drawer [data-lager-drawer-topic="new-lot-action"] { .sg-card-list-page-drawer [data-lager-drawer-topic="new-lot-action"] {
display: flex; display: flex;
align-items: center; align-items: center;