Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c1e840c97 | ||
|
|
764b6fa886 | ||
|
|
3ae9951b13 | ||
|
|
959586ecc9 | ||
|
|
77f6fee5ac | ||
|
|
8f7fb9875a | ||
|
|
400b352522 | ||
|
|
4a8936761b | ||
|
|
0e8df45548 | ||
|
|
5059bd741a | ||
|
|
8846a57b43 | ||
|
|
17efaee53f | ||
|
|
e69eda9953 | ||
|
|
801695ea89 | ||
|
|
ba65b05c08 | ||
|
|
15b391f1d6 | ||
|
|
f8f011f96c | ||
|
|
a9fa9955d2 | ||
|
|
f50cf944fe | ||
|
|
59c953d42f | ||
|
|
b92c92f5b5 | ||
|
|
7daf8b7096 | ||
|
|
51a5e630a8 | ||
|
|
3f15756596 | ||
|
|
9eb9cd6e0c | ||
|
|
34967b10cd | ||
|
|
f38b48dc53 | ||
|
|
d640f3656d | ||
|
|
f7dcbdf786 | ||
|
|
5903425f78 | ||
|
|
a422137997 | ||
|
|
f83195c9b3 | ||
|
|
b9a6d6d8cd | ||
|
|
520b68a433 | ||
|
|
51571f6224 | ||
|
|
6a2b163a51 | ||
|
|
bd219ac463 | ||
|
|
0b39acbf30 | ||
|
|
61fc46df6e | ||
|
|
59db50eccd | ||
|
|
c83741fca0 | ||
|
|
2b540b91ed | ||
|
|
241e4e961c | ||
|
|
f4b31af7b8 | ||
|
|
4ca6f2241e | ||
|
|
879129cb13 | ||
|
|
df3f2fa46c | ||
|
|
7d865e335a | ||
|
|
2c408f2706 | ||
|
|
6d8508bf3e | ||
|
|
bb5cd7d039 | ||
|
|
ae311e4612 | ||
|
|
485882536f | ||
|
|
f1631f5755 | ||
|
|
99ec2667da | ||
|
|
b3c84fae4c | ||
|
|
fdcea56c26 | ||
|
|
608d91f530 | ||
|
|
1b752c0830 | ||
|
|
a1f6e2edee | ||
|
|
0867ffa0fa | ||
|
|
8356b545c4 | ||
|
|
4dda727f03 | ||
|
|
7491e0b7c5 | ||
|
|
3a3de9da22 | ||
|
|
f130092d40 | ||
|
|
e9903eec19 | ||
|
|
0826560373 | ||
|
|
c75e308f45 | ||
|
|
0d03de8ff2 | ||
|
|
683327c240 | ||
|
|
382d9fe9df | ||
|
|
b8136764eb | ||
|
|
a31057f0c0 | ||
|
|
5b0a623f4b | ||
|
|
b3f52f2810 | ||
|
|
2bec934732 | ||
|
|
e47990dfe0 | ||
|
|
9d55a6d213 | ||
|
|
7c7c6da344 | ||
|
|
6837ebfa6b | ||
|
|
939c96f62d | ||
|
|
82162d5dff | ||
|
|
e98c5e6aa4 | ||
|
|
6bd2bf1b3f | ||
|
|
e62c7e11c2 | ||
|
|
9382647a87 | ||
|
|
668ebbc64a | ||
|
|
44faf75e28 | ||
|
|
49ecbfa17c | ||
|
|
aeab22b48a | ||
|
|
c076ec9889 | ||
|
|
de2df1a13b | ||
|
|
c8d9f03d7d | ||
|
|
28fa29f294 | ||
|
|
584d941c03 | ||
|
|
3a56a24b00 | ||
|
|
e6d4276445 | ||
|
|
19ec3d66d9 | ||
|
|
42f02d9441 | ||
|
|
16569a2357 | ||
|
|
1b783b6417 | ||
|
|
f6ff1d0f78 | ||
|
|
36c66eb0cc | ||
|
|
4fbd944ece | ||
|
|
20670b4666 | ||
|
|
2ee52b3672 | ||
|
|
2e78162e93 | ||
|
|
ca5cf5a03f | ||
|
|
e656841de2 | ||
|
|
7b25f4da0a | ||
|
|
501f32c35e | ||
|
|
af58b813ca | ||
|
|
a5ef8a4f43 | ||
|
|
89057c0c26 | ||
|
|
f21bf84e80 | ||
|
|
b1fdf99e91 | ||
|
|
88dbb008d2 | ||
|
|
1cc35e9b5a | ||
|
|
1362e94948 | ||
|
|
a526cb9f60 | ||
|
|
ba796f0a3f | ||
|
|
f739106ab9 | ||
|
|
307affbb67 | ||
|
|
85617b5b34 | ||
|
|
bd2ef4e799 |
No files matched your search
@@ -15,6 +15,11 @@
|
||||
11. `db/migrations/0015_sales_order_payments.sql`
|
||||
12. `db/migrations/0016_stock_lot_forecast_history.sql`
|
||||
13. `db/migrations/0017_manufacturer_lot_traceability.sql`
|
||||
14. `db/migrations/0018_shopify_delta_sync_state.sql`
|
||||
15. `db/migrations/0019_allow_multiple_open_stock_lots.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
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
BEGIN;
|
||||
|
||||
DROP INDEX IF EXISTS uq_stock_lot_one_open_per_product;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,22 @@
|
||||
BEGIN;
|
||||
|
||||
CREATE OR REPLACE VIEW v_stock_lot_balance AS
|
||||
SELECT
|
||||
sl.id AS stock_lot_id,
|
||||
sl.product_id,
|
||||
COALESCE(SUM(CASE WHEN sm.move_type = 'in' THEN sm.qty ELSE 0 END), 0) AS qty_in,
|
||||
COALESCE(SUM(CASE WHEN sm.move_type = 'out' THEN sm.qty ELSE 0 END), 0) AS qty_out,
|
||||
COALESCE(SUM(
|
||||
CASE
|
||||
WHEN sm.move_type = 'in' THEN sm.qty
|
||||
WHEN sm.move_type = 'out' THEN -sm.qty
|
||||
WHEN sm.move_type = 'adjustment' AND sm.to_location_id IS NOT NULL AND sm.from_location_id IS NULL THEN sm.qty
|
||||
WHEN sm.move_type = 'adjustment' AND sm.from_location_id IS NOT NULL AND sm.to_location_id IS NULL THEN -sm.qty
|
||||
ELSE 0
|
||||
END
|
||||
), 0) AS qty_net
|
||||
FROM stock_lot sl
|
||||
LEFT JOIN stock_move sm ON sm.lot_id = sl.id
|
||||
GROUP BY sl.id, sl.product_id;
|
||||
|
||||
COMMIT;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
+2
-2
@@ -20,7 +20,7 @@ Es werden keine vorbereiteten Shopify-Geschäftsdaten an nachgelagerte Module ü
|
||||
|
||||
## 4. Exakter Prozessablauf
|
||||
|
||||
1. `cutoff_timestamp` und `dry_run` validieren; beim Cron den kleineren Wert aus Wasserstand und ältestem offenen Webhook-Eingang verwenden.
|
||||
1. `cutoff_timestamp` und `dry_run` validieren; beim Cron den kleineren Wert aus Wasserstand und ältestem offenen Webhook-Eingang verwenden und offene Webhook-Order-GIDs zusätzlich direkt zur Recovery aufnehmen.
|
||||
2. Read-only Shopify-Bestellungen mit `createdAt` nach dem Stichtag in stabiler Reihenfolge laden.
|
||||
3. Stornierte oder vollständig erstattete Bestellungen als nicht lagerwirksam klassifizieren und nur technisch protokollieren.
|
||||
4. Für jede qualifizierte Bestellung die Shopify Order-GID idempotent gegen `sales_order.shopify_order_gid` prüfen.
|
||||
@@ -43,7 +43,7 @@ Write targets:
|
||||
|
||||
## 5. Batch, Betriebsmodell und Einbettung
|
||||
|
||||
Der Initiallauf verarbeitet den festen Zeitraum nach dem bestehenden ERP-Stichtag seriell. Spätere tägliche Reconciliation-Läufe nutzen den persistierten Wasserstand und ziehen offene oder fehlgeschlagene Webhook-Eingänge wieder in den Prüfbereich. Der Wasserstand wird nur nach einem vollständig erfolgreichen Lauf fortgeschrieben. Keine Parallelisierung.
|
||||
Der Initiallauf verarbeitet den festen Zeitraum nach dem bestehenden ERP-Stichtag seriell. Spätere tägliche Reconciliation-Läufe nutzen den persistierten Wasserstand und ziehen offene oder fehlgeschlagene Webhook-Eingänge sowohl über den Prüfbereich als auch über ihre stabile Order-GID wieder in die Verarbeitung. Ein technisches Event wird erst nach erfolgreicher Projektion als verarbeitet markiert. Der Wasserstand wird nur nach einem vollständig erfolgreichen Lauf fortgeschrieben. Keine Parallelisierung.
|
||||
|
||||
## 6. Output
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ Status: Verbindliche Prozess-Spezifikation
|
||||
|
||||
## 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
|
||||
|
||||
@@ -13,18 +13,18 @@ Direkt ausgeführter Lager-Prozess mit zwei Triggern: synchron nach jeder erfolg
|
||||
## 3. Input
|
||||
|
||||
- optional eine einzelne `product_id` bei synchronem Verkaufstrigger
|
||||
- aktive Produkte, aktuelle Chargen, 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
|
||||
|
||||
1. Bei einem Verkaufstrigger die betroffenen Produkt-IDs aus der erfolgreichen Allokation übernehmen; beim Cron-Trigger alle aktiven Produkte bestimmen.
|
||||
2. Aktive Produkte und ihre aktuelle Charge aus `product`, `stock_lot` und `v_stock_lot_balance` laden.
|
||||
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.
|
||||
4. `allocated`- und `reserved`-Mengen nach Bestell- beziehungsweise OTC-Verkaufsdatum je Produkt und Kalendertag aggregieren.
|
||||
5. Stornierte Auftrags- und Positionsmengen aus der Nachfrage herausrechnen.
|
||||
6. Den gewichteten Tagesabsatz mit einer Halbwertszeit von 60 Tagen berechnen.
|
||||
7. Für jede aktuelle Charge das Leer-Datum aus physischem Bestand und gewichteter Nachfrage berechnen.
|
||||
8. Jede erfolgreiche Berechnung in `stock_lot_forecast_history` speichern und den aktuellen Snapshot in `stock_lot` aktualisieren.
|
||||
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 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.
|
||||
|
||||
## 5. Batch, Betriebsmodell und Einbettung
|
||||
@@ -37,7 +37,8 @@ Ein normalisiertes JSON-Ergebnis mit Gesamtzahl, erfolgreich verarbeiteten Produ
|
||||
|
||||
## 7. Erfolgskriterien
|
||||
|
||||
- Jede aktuelle Charge erhält täglich einen aktuellen Prognosesnapshot.
|
||||
- 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.
|
||||
- Die vollständige Berechnungshistorie bleibt erhalten.
|
||||
- Ein einzelner Produktfehler verhindert keine weiteren Berechnungen.
|
||||
|
||||
@@ -26,6 +26,10 @@
|
||||
<button class="sg-interaction-element sg-button sg-button--active" type="button" data-component="button" data-component-state="active">
|
||||
Link-Button aktiv
|
||||
</button>
|
||||
|
||||
<button class="sg-interaction-element sg-button sg-button--warning" type="button" data-component="button" data-component-variant="warning">
|
||||
Link Button Warning
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -843,7 +847,28 @@
|
||||
</section>
|
||||
|
||||
<script src="../scripts/help-icon-overlays.js"></script>
|
||||
<section id="component-file-input">
|
||||
<p class="sg-preview-label">Component: File Input</p>
|
||||
<div class="sg-form-preview-area">
|
||||
<div class="sg-file-input" data-component="file-input">
|
||||
<div class="sg-file-input__row sg-labeled-input-row">
|
||||
<label class="sg-label" for="component-file-input-control">Datei auswählen</label>
|
||||
<input class="sg-file-input__control" id="component-file-input-control" type="file" multiple>
|
||||
<button class="sg-interaction-element sg-button sg-button--active sg-file-input__trigger" type="button">Dateien auswählen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
document.querySelectorAll('[data-component="file-input"]').forEach((fileInput) => {
|
||||
const input = fileInput.querySelector('.sg-file-input__control');
|
||||
const trigger = fileInput.querySelector('.sg-file-input__trigger');
|
||||
if (input && trigger) {
|
||||
trigger.addEventListener('click', () => input.click());
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('.sg-mode-toggle:not(.sg-activation-mode-toggle)').forEach((toggle) => {
|
||||
toggle.addEventListener('click', () => {
|
||||
const nextState = toggle.dataset.active === 'relative' ? 'absolute' : 'relative';
|
||||
|
||||
@@ -158,6 +158,8 @@
|
||||
<tr><td>input-field-max-width</td><td>600px</td><td>Maximale Breite von ein- und mehrzeiligen Eingabefeldern (ohne Suchfeld).</td></tr>
|
||||
<tr><td>email-body-max-width</td><td>600px</td><td>Maximale Breite des Inhaltsbereichs von E-Mail-Templates innerhalb des E-Mail-Viewports.</td></tr>
|
||||
<tr><td>object-card-height</td><td>600px</td><td>Fixe Höhe der Object Card im Desktop-Layout (u. a. für Company Card).</td></tr>
|
||||
<tr><td>scrollable-card-width</td><td>600px</td><td>Standardbreite der eigenständigen Scrollable Card; kann im konsumierenden Portal über den semantischen Layout-Token angepasst werden.</td></tr>
|
||||
<tr><td>scrollable-card-height</td><td>600px</td><td>Standardhöhe der eigenständigen Scrollable Card; kann im konsumierenden Portal über den semantischen Layout-Token angepasst werden.</td></tr>
|
||||
<tr><td>content-card-margin-top-desktop</td><td>100px</td><td>Oberer Außenabstand der Content Card auf Desktop.</td></tr>
|
||||
<tr><td>content-card-margin-top-mobile</td><td>1rem</td><td>Oberer Außenabstand der Content Card auf Mobile; entspricht spacing-large.</td></tr>
|
||||
<tr><td>notification-card-width</td><td>50%</td><td>Breite der Notification Card relativ zum übergeordneten Layout.</td></tr>
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
<li><a href="./patterns/portal-header.html">Portal Header</a></li>
|
||||
<li><a href="./patterns/options-row.html">Options Row</a></li>
|
||||
<li><a href="./patterns/object-card.html">Object Card</a></li>
|
||||
<li><a href="./patterns/scrollable-card.html">Scrollable Card</a></li>
|
||||
<li><a href="./patterns/object-group-card.html">Object Group Card</a></li>
|
||||
<li><a href="./patterns/navigation-card.html">Navigation Card</a></li>
|
||||
<li><a href="./patterns/left-navigation.html">Left Navigation</a></li>
|
||||
@@ -50,6 +51,7 @@
|
||||
<li><a href="./patterns/content-cards-group.html">Content Cards Group</a></li>
|
||||
<li><a href="./patterns/card-gruppe-mit-tastennavigation.html">Card Gruppe mit Tastennavigation</a></li>
|
||||
<li><a href="./patterns/formular-mit-abschnitten.html">Formular mit Abschnitten</a></li>
|
||||
<li><a href="./patterns/file-upload.html">Datei-Upload</a></li>
|
||||
<li><a href="./patterns/multiselektions-pulldown.html">Multiselektions-Pulldown</a></li>
|
||||
<li><a href="./patterns/text-layouts.html">Text Layouts</a></li>
|
||||
<li><a href="./patterns/delete-confirmation-overlay.html">Overlay Card</a></li>
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Styleguide – Pattern Datei-Upload</title>
|
||||
<link rel="stylesheet" href="../styleguide.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1 class="sg-main-heading">Pattern – Datei-Upload</h1>
|
||||
<section id="pattern-file-upload">
|
||||
<p class="sg-preview-label">Pattern: Datei-Upload</p>
|
||||
<form class="sg-file-upload" data-pattern="file-upload" action="#" method="post">
|
||||
<div class="sg-file-upload__dropzone sg-labeled-input-row" data-drag-over="false">
|
||||
<label class="sg-label" for="file-upload-input">Datei auswählen</label>
|
||||
<input class="sg-file-input__control" id="file-upload-input" name="documents" type="file" multiple aria-describedby="file-upload-status">
|
||||
<button class="sg-interaction-element sg-button sg-button--active sg-file-upload__trigger" type="button">Dateien auswählen</button>
|
||||
</div>
|
||||
<p class="sg-body sg-file-upload__status" id="file-upload-status" aria-live="polite">Noch keine Dateien ausgewählt.</p>
|
||||
<ul class="sg-file-upload__files" aria-label="Ausgewählte Dateien"></ul>
|
||||
<button class="sg-interaction-element sg-button sg-button--process sg-button--process-inactive sg-file-upload__submit" type="submit" disabled aria-disabled="true">Hochladen</button>
|
||||
</form>
|
||||
</section>
|
||||
<script>
|
||||
(() => {
|
||||
const upload = document.querySelector('[data-pattern="file-upload"]');
|
||||
if (!upload) return;
|
||||
const input = upload.querySelector('#file-upload-input');
|
||||
const dropzone = upload.querySelector('.sg-file-upload__dropzone');
|
||||
const trigger = upload.querySelector('.sg-file-upload__trigger');
|
||||
const filesList = upload.querySelector('.sg-file-upload__files');
|
||||
const status = upload.querySelector('.sg-file-upload__status');
|
||||
const submit = upload.querySelector('[type="submit"]');
|
||||
let selectedFiles = [];
|
||||
const formatSize = (bytes) => bytes < 1024 * 1024 ? `${Math.max(1, Math.round(bytes / 1024))} KB` : `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
const render = () => {
|
||||
filesList.innerHTML = '';
|
||||
selectedFiles.forEach((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';
|
||||
name.textContent = file.name;
|
||||
const meta = document.createElement('span');
|
||||
meta.className = 'sg-file-upload__file-meta';
|
||||
meta.textContent = ` · ${formatSize(file.size)}`;
|
||||
name.append(meta);
|
||||
const remove = document.createElement('button');
|
||||
remove.className = 'sg-interaction-element sg-button sg-button--active sg-file-upload__remove';
|
||||
remove.type = 'button';
|
||||
remove.textContent = 'Entfernen';
|
||||
remove.addEventListener('click', () => { selectedFiles.splice(index, 1); render(); });
|
||||
item.append(name, remove);
|
||||
filesList.append(item);
|
||||
});
|
||||
const count = selectedFiles.length;
|
||||
status.textContent = count === 0 ? 'Noch keine Dateien ausgewählt.' : `${count} Datei${count === 1 ? '' : 'en'} ausgewählt.`;
|
||||
submit.disabled = count === 0;
|
||||
submit.setAttribute('aria-disabled', String(count === 0));
|
||||
submit.classList.toggle('sg-button--process-inactive', count === 0);
|
||||
};
|
||||
trigger.addEventListener('click', () => input.click());
|
||||
input.addEventListener('change', () => { selectedFiles = [...selectedFiles, ...Array.from(input.files)]; input.value = ''; render(); });
|
||||
['dragenter', 'dragover'].forEach((eventName) => dropzone.addEventListener(eventName, (event) => { event.preventDefault(); dropzone.dataset.dragOver = 'true'; }));
|
||||
['dragleave', 'drop'].forEach((eventName) => dropzone.addEventListener(eventName, (event) => { event.preventDefault(); dropzone.dataset.dragOver = 'false'; }));
|
||||
dropzone.addEventListener('drop', (event) => { selectedFiles = [...selectedFiles, ...Array.from(event.dataTransfer.files)]; render(); });
|
||||
render();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,52 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Styleguide – Pattern Scrollable Card</title>
|
||||
<link rel="stylesheet" href="../styleguide.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1 class="sg-main-heading">Pattern – Scrollable Card</h1>
|
||||
|
||||
<section id="pattern-scrollable-card">
|
||||
<p class="sg-preview-label">Pattern: Scrollable Card</p>
|
||||
<p class="sg-body">Die Scrollable Card besitzt eine fixe Breite und Höhe. Header und Aktionsbereich bleiben sichtbar; ausschließlich der Inhaltsbereich ist scrollbar. Das Pattern ist unabhängig von Overlay-Patterns und kann bei Bedarf von diesen verwendet werden.</p>
|
||||
|
||||
<article class="sg-card sg-scrollable-card" data-pattern="scrollable-card" aria-label="Scrollable Card">
|
||||
<header class="sg-card-segment sg-card-segment--header sg-card-segment--darkblue sg-scrollable-card__header" data-pattern-part="scrollable-card-header">
|
||||
<div class="sg-strong">Scrollable Card</div>
|
||||
</header>
|
||||
|
||||
<div class="sg-card-segment sg-card-segment--body sg-scrollable-card__content" data-pattern-part="scrollable-card-content" tabindex="0" aria-label="Scrollbarer Karteninhalt">
|
||||
<p class="sg-body">Der Karteninhalt bleibt innerhalb der definierten Kartenhöhe. Bei längeren Inhalten entsteht hier ein eigener vertikaler Scrollbereich, während Titel und Aktionen sichtbar bleiben.</p>
|
||||
<p class="sg-body">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer luctus, turpis vel porttitor cursus, nibh justo feugiat sem, sit amet egestas lorem arcu sed augue.</p>
|
||||
<p class="sg-body">Donec sed lorem in urna sagittis aliquet. Suspendisse potenti. Curabitur blandit tempus porttitor. Maecenas faucibus mollis interdum.</p>
|
||||
<p class="sg-body">Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Aenean lacinia bibendum nulla sed consectetur.</p>
|
||||
<p class="sg-body">Zusätzlicher Inhalt zur Demonstration des unabhängigen Scrollbereichs innerhalb der Card.</p>
|
||||
<p class="sg-body">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed posuere consectetur est at lobortis. Cras mattis consectetur purus sit amet fermentum.</p>
|
||||
<p class="sg-body">Maecenas sed diam eget risus varius blandit sit amet non magna. Donec ullamcorper nulla non metus auctor fringilla.</p>
|
||||
<p class="sg-body">Nullam quis risus eget urna mollis ornare vel eu leo. Vestibulum id ligula porta felis euismod semper.</p>
|
||||
<p class="sg-body">Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum. Etiam porta sem malesuada magna mollis euismod.</p>
|
||||
<p class="sg-body">Integer posuere erat a ante venenatis dapibus posuere velit aliquet. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.</p>
|
||||
<p class="sg-body">Curabitur blandit tempus porttitor. Donec id elit non mi porta gravida at eget metus. Vestibulum ligula porta felis euismod semper.</p>
|
||||
<p class="sg-body">Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus.</p>
|
||||
<p class="sg-body">Cras justo odio, dapibus ac facilisis in, egestas eget quam. Etiam porta sem malesuada magna mollis euismod.</p>
|
||||
<p class="sg-body">Morbi risus porta ac consectetur ac, vestibulum at eros. Sed posuere consectetur est at lobortis.</p>
|
||||
<p class="sg-body">Donec sed odio dui. Aenean lacinia bibendum nulla sed consectetur. Nulla vitae elit libero, a pharetra augue.</p>
|
||||
<p class="sg-body">Vestibulum id ligula porta felis euismod semper. Donec ullamcorper nulla non metus auctor fringilla.</p>
|
||||
<p class="sg-body">Der untere Aktionsbereich bleibt auch am Ende des Scrollvorgangs an seiner Position sichtbar.</p>
|
||||
</div>
|
||||
|
||||
<footer class="sg-card-segment sg-card-segment--body sg-scrollable-card__actions-segment" data-pattern-part="scrollable-card-actions">
|
||||
<div class="sg-scrollable-card__actions">
|
||||
<button class="sg-interaction-element sg-button sg-button--active" type="button">Abbrechen</button>
|
||||
<button class="sg-interaction-element sg-button sg-button--active" type="button">Speichern</button>
|
||||
</div>
|
||||
</footer>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -63,6 +63,12 @@
|
||||
<tr><td>surface-object-card-lower-segment</td><td>color-white</td><td>Hintergrundfläche der unteren zwei Segmente der Object Card.</td></tr>
|
||||
</tbody></table>
|
||||
|
||||
<h3 class="sg-sub-heading sg-section-h3">Scrollable Card</h3>
|
||||
<table class="sg-foundation-table sg-table-label"><thead><tr><th>Semantischer Token</th><th>Verwendeter Foundation-Token</th><th>Beschreibung Verwendungszweck im Pattern</th></tr></thead><tbody>
|
||||
<tr><td>layout-scrollable-card-width</td><td>dimension-scrollable-card-width</td><td>Fixe Breite der Scrollable Card; kann im konsumierenden Portal individuell gesetzt werden.</td></tr>
|
||||
<tr><td>layout-scrollable-card-height</td><td>dimension-scrollable-card-height</td><td>Fixe Höhe der Scrollable Card; kann im konsumierenden Portal individuell gesetzt werden.</td></tr>
|
||||
</tbody></table>
|
||||
|
||||
<h3 class="sg-sub-heading sg-section-h3">Object Group Card</h3>
|
||||
<table class="sg-foundation-table sg-table-label"><thead><tr><th>Semantischer Token</th><th>Verwendeter Foundation-Token</th><th>Beschreibung Verwendungszweck im Pattern</th></tr></thead><tbody>
|
||||
<tr><td>layout-object-group-card-min-width</td><td>dimension-object-group-card-min-width</td><td>Mindestbreite der einzelnen Karten im Pattern Object Group Card.</td></tr>
|
||||
@@ -142,6 +148,14 @@
|
||||
<tr><td>layout-delete-confirmation-target-dim-opacity</td><td>opacity-delete-confirmation-target-dim</td><td>Deckkraft der grauen Überlagerung auf dem zu löschenden Objekt.</td></tr>
|
||||
<tr><td>layer-delete-confirmation-overlay</td><td>dimension-layer-delete-confirmation-overlay</td><td>Z-Index-Layer des schwebenden Bestätigungsfensters.</td></tr>
|
||||
</tbody></table>
|
||||
|
||||
<h3 class="sg-sub-heading sg-section-h3">Datei-Upload</h3>
|
||||
<table class="sg-foundation-table sg-table-label"><thead><tr><th>Semantischer Token</th><th>Verwendeter Foundation-Token</th><th>Beschreibung Verwendungszweck im Pattern</th></tr></thead><tbody>
|
||||
<tr><td>surface-file-upload-dropzone</td><td>surface-form-preview</td><td>Hintergrundfläche des auswählbaren Upload-Bereichs.</td></tr>
|
||||
<tr><td>surface-file-upload-file-item</td><td>surface-control-default</td><td>Hintergrundfläche eines Eintrags in der Dateiliste.</td></tr>
|
||||
<tr><td>text-file-upload-meta</td><td>text-control-disabled</td><td>Textfarbe für Dateigröße und ergänzende Dateimetadaten.</td></tr>
|
||||
<tr><td>text-file-upload-error</td><td>color-signal-red</td><td>Textfarbe für Validierungs- und Upload-Fehlermeldungen.</td></tr>
|
||||
</tbody></table>
|
||||
</section>
|
||||
|
||||
</body>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
@import "./styles/02-base.css";
|
||||
@import "./styles/03-typography-helpers.css";
|
||||
@import "./styles/10-components-interactive-elements.css";
|
||||
@import "./styles/47-patterns-file-upload.css";
|
||||
@import "./styles/20-patterns-portal-header.css";
|
||||
@import "./styles/21-patterns-options-row.css";
|
||||
@import "./styles/22-patterns-object-card.css";
|
||||
@@ -26,3 +27,4 @@
|
||||
@import "./styles/43-components-typography.css";
|
||||
@import "./styles/44-components-public-page-components.css";
|
||||
@import "./styles/45-patterns-public-page-patterns.css";
|
||||
@import "./styles/46-patterns-scrollable-card.css";
|
||||
@@ -175,6 +175,8 @@
|
||||
--layout-object-card-mobile-width: var(--dimension-object-card-mobile-width);
|
||||
--layout-object-card-mobile-height: var(--dimension-object-card-mobile-height);
|
||||
--layout-object-card-desktop-breakpoint: var(--dimension-object-card-desktop-breakpoint);
|
||||
--layout-scrollable-card-width: var(--dimension-scrollable-card-width);
|
||||
--layout-scrollable-card-height: var(--dimension-scrollable-card-height);
|
||||
--layout-content-card-margin-top-desktop: var(--dimension-content-card-margin-top-desktop);
|
||||
--layout-content-card-margin-top-mobile: var(--dimension-content-card-margin-top-mobile);
|
||||
--layout-notification-card-width: var(--dimension-notification-card-width);
|
||||
@@ -287,6 +289,8 @@
|
||||
--dimension-object-card-mobile-width: 100%;
|
||||
--dimension-object-card-mobile-height: auto;
|
||||
--dimension-object-card-desktop-breakpoint: 768px;
|
||||
--dimension-scrollable-card-width: 600px;
|
||||
--dimension-scrollable-card-height: 600px;
|
||||
--dimension-content-card-margin-top-desktop: 100px;
|
||||
--dimension-content-card-margin-top-mobile: 1rem;
|
||||
--dimension-object-group-card-min-width: 450px;
|
||||
|
||||
@@ -139,6 +139,13 @@
|
||||
color: var(--text-control-default);
|
||||
}
|
||||
|
||||
.sg-button--warning {
|
||||
--surface-button-warning: var(--color-signal-red);
|
||||
--text-button-warning: var(--color-font-light);
|
||||
background: var(--surface-button-warning);
|
||||
color: var(--text-button-warning);
|
||||
}
|
||||
|
||||
.sg-button--process {
|
||||
background: var(--surface-button-process);
|
||||
color: var(--text-button-process);
|
||||
@@ -1578,3 +1585,52 @@
|
||||
color: var(--text-hyperlink);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Component: File Input */
|
||||
.sg-file-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sg-file-input__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-large);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sg-file-input__control {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.sg-file-input:focus-within .sg-file-input__trigger {
|
||||
outline: 2px solid var(--text-hyperlink);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.sg-file-input__trigger {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
@media (max-width: 48rem) {
|
||||
.sg-file-input__row {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.sg-file-input--disabled {
|
||||
opacity: var(--disabled-opacity);
|
||||
}
|
||||
|
||||
.sg-file-input--disabled .sg-file-input__trigger {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/* ========================================================= */
|
||||
/* Patterns: Scrollable Card */
|
||||
/* ========================================================= */
|
||||
|
||||
.sg-scrollable-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: var(--layout-scrollable-card-width);
|
||||
max-width: 100%;
|
||||
height: var(--layout-scrollable-card-height);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sg-scrollable-card__header,
|
||||
.sg-scrollable-card__actions-segment {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.sg-scrollable-card__content {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sg-scrollable-card__actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-small);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sg-scrollable-card__actions > .sg-button {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/* ========================================================= */
|
||||
/* Patterns: Datei-Upload */
|
||||
/* ========================================================= */
|
||||
|
||||
.sg-file-upload {
|
||||
--surface-file-upload-dropzone: var(--surface-form-preview);
|
||||
--surface-file-upload-file-item: var(--surface-control-default);
|
||||
--text-file-upload-meta: var(--text-control-disabled);
|
||||
--text-file-upload-error: var(--color-signal-red);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-large);
|
||||
width: 100%;
|
||||
max-width: var(--layout-input-field-max-width);
|
||||
}
|
||||
|
||||
.sg-file-upload__status,
|
||||
.sg-file-upload__file-meta {
|
||||
margin: var(--spacing-small) 0 0;
|
||||
}
|
||||
|
||||
.sg-file-upload__dropzone {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-large);
|
||||
width: 100%;
|
||||
padding: var(--spacing-large);
|
||||
background: var(--surface-file-upload-dropzone);
|
||||
border: 1px dashed var(--surface-button-active);
|
||||
border-radius: var(--radius-interaction);
|
||||
}
|
||||
|
||||
.sg-file-upload__dropzone[data-drag-over="true"] {
|
||||
outline: 2px solid var(--text-hyperlink);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.sg-file-upload__files {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-small);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.sg-file-upload__file {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-large);
|
||||
padding: var(--spacing-small) var(--spacing-large);
|
||||
color: var(--text-control-default);
|
||||
background: var(--surface-file-upload-file-item);
|
||||
border-radius: var(--radius-interaction);
|
||||
}
|
||||
|
||||
.sg-file-upload__file-name {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.sg-file-upload__file-meta {
|
||||
color: var(--text-file-upload-meta);
|
||||
}
|
||||
|
||||
.sg-file-upload__remove {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.sg-file-upload__submit {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.sg-file-upload__error {
|
||||
color: var(--text-file-upload-error);
|
||||
}
|
||||
|
||||
@media (max-width: 48rem) {
|
||||
.sg-file-upload__dropzone {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sg-file-upload__file {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-small);
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,32 @@ function shopify_delta_reconciliation_cutoff(PDO $pdo): string
|
||||
return ($pendingDate < $stateDate ? $pendingDate : $stateDate)->format(DATE_ATOM);
|
||||
}
|
||||
|
||||
function shopify_delta_load_pending_order_gids(PDO $pdo): array
|
||||
{
|
||||
$stmt = $pdo->query(
|
||||
"SELECT DISTINCT shopify_order_gid
|
||||
FROM shopify_webhook_event
|
||||
WHERE status IN ('received', 'processing', 'failed')
|
||||
AND shopify_order_gid IS NOT NULL
|
||||
ORDER BY shopify_order_gid"
|
||||
);
|
||||
return array_values(array_filter(array_map(
|
||||
static fn (mixed $value): string => trim((string) $value),
|
||||
$stmt->fetchAll(PDO::FETCH_COLUMN)
|
||||
), static fn (string $value): bool => $value !== ''));
|
||||
}
|
||||
|
||||
function shopify_delta_mark_order_events_processed(PDO $pdo, string $orderGid): void
|
||||
{
|
||||
$stmt = $pdo->prepare(
|
||||
"UPDATE shopify_webhook_event
|
||||
SET status = 'processed', processed_at = NOW(), error_code = NULL, error_message = NULL
|
||||
WHERE shopify_order_gid = :shopify_order_gid
|
||||
AND status IN ('received', 'processing', 'failed')"
|
||||
);
|
||||
$stmt->execute([':shopify_order_gid' => $orderGid]);
|
||||
}
|
||||
|
||||
function shopify_delta_store_watermark(PDO $pdo, string $cutoff): void
|
||||
{
|
||||
$stmt = $pdo->prepare(
|
||||
@@ -120,7 +146,7 @@ GQL;
|
||||
return $orders;
|
||||
}
|
||||
|
||||
function run_shopify_delta(array $env, PDO $pdo, string $cutoff, bool $dryRun): array
|
||||
function run_shopify_delta(array $env, PDO $pdo, string $cutoff, bool $dryRun, array $recoveryOrderGids = []): array
|
||||
{
|
||||
$cutoffDate = new DateTimeImmutable($cutoff);
|
||||
$searchQuery = 'created_at:>' . $cutoffDate->format('Y-m-d');
|
||||
@@ -135,14 +161,31 @@ function run_shopify_delta(array $env, PDO $pdo, string $cutoff, bool $dryRun):
|
||||
'clarification_order_gids' => [],
|
||||
];
|
||||
|
||||
$ordersByGid = [];
|
||||
foreach (shopify_delta_load_orders($env, $searchQuery) as $summaryOrder) {
|
||||
$orderGid = trim((string) ($summaryOrder['id'] ?? ''));
|
||||
if ($orderGid !== '') {
|
||||
$ordersByGid[$orderGid] = $summaryOrder;
|
||||
}
|
||||
}
|
||||
foreach ($recoveryOrderGids as $recoveryOrderGid) {
|
||||
if (!isset($ordersByGid[$recoveryOrderGid])) {
|
||||
$ordersByGid[$recoveryOrderGid] = shopify_load_order_by_gid($env, $recoveryOrderGid);
|
||||
}
|
||||
}
|
||||
|
||||
$recoverySet = array_fill_keys($recoveryOrderGids, true);
|
||||
foreach ($ordersByGid as $orderGid => $summaryOrder) {
|
||||
$createdAt = new DateTimeImmutable((string) ($summaryOrder['createdAt'] ?? ''));
|
||||
if ($createdAt <= $cutoffDate) {
|
||||
if (!isset($recoverySet[$orderGid]) && $createdAt <= $cutoffDate) {
|
||||
continue;
|
||||
}
|
||||
$counters['checked_orders']++;
|
||||
if (($summaryOrder['cancelledAt'] ?? null) !== null || strtoupper((string) ($summaryOrder['displayFinancialStatus'] ?? '')) === 'REFUNDED') {
|
||||
$counters['skipped_cancelled_or_refunded']++;
|
||||
if (!$dryRun) {
|
||||
shopify_delta_mark_order_events_processed($pdo, (string) $summaryOrder['id']);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -166,8 +209,14 @@ function run_shopify_delta(array $env, PDO $pdo, string $cutoff, bool $dryRun):
|
||||
}
|
||||
if (($result['status'] ?? '') === 'already_imported') {
|
||||
$counters['already_imported']++;
|
||||
if (!$dryRun && isset($recoverySet[(string) $summaryOrder['id']])) {
|
||||
shopify_delta_mark_order_events_processed($pdo, (string) $summaryOrder['id']);
|
||||
}
|
||||
} else {
|
||||
$counters['handed_off']++;
|
||||
if (!$dryRun) {
|
||||
shopify_delta_mark_order_events_processed($pdo, (string) $summaryOrder['id']);
|
||||
}
|
||||
}
|
||||
} catch (Throwable $error) {
|
||||
$counters['status'] = 'partial_success';
|
||||
|
||||
@@ -132,19 +132,21 @@ function shopify_webhook_worker_run(PDO $pdo, array $env, int $limit): array
|
||||
}
|
||||
}
|
||||
|
||||
$argvLimit = 20;
|
||||
foreach ($argv as $argument) {
|
||||
if (str_starts_with($argument, '--limit=')) {
|
||||
$argvLimit = max(1, min(100, (int) substr($argument, 8)));
|
||||
if (PHP_SAPI === 'cli' && realpath($_SERVER['SCRIPT_FILENAME'] ?? '') === __FILE__) {
|
||||
$argvLimit = 20;
|
||||
foreach ($argv as $argument) {
|
||||
if (str_starts_with($argument, '--limit=')) {
|
||||
$argvLimit = max(1, min(100, (int) substr($argument, 8)));
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$env = expand_env_values(parse_env_file(__DIR__ . '/../../../.env'));
|
||||
$pdo = connect_database($env);
|
||||
echo json_encode(shopify_webhook_worker_run($pdo, $env, $argvLimit), JSON_UNESCAPED_SLASHES) . PHP_EOL;
|
||||
exit(0);
|
||||
} catch (Throwable $error) {
|
||||
fwrite(STDERR, $error->getMessage() . PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$env = expand_env_values(parse_env_file(__DIR__ . '/../../../.env'));
|
||||
$pdo = connect_database($env);
|
||||
echo json_encode(shopify_webhook_worker_run($pdo, $env, $argvLimit), JSON_UNESCAPED_SLASHES) . PHP_EOL;
|
||||
exit(0);
|
||||
} catch (Throwable $error) {
|
||||
fwrite(STDERR, $error->getMessage() . PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
@@ -62,16 +62,57 @@ try {
|
||||
$adjustmentQty,
|
||||
$outQty
|
||||
);
|
||||
json_response(200, ['ok' => true, 'lot' => $updatedLot, 'balance' => get_stock_lot_balance($pdo, (int) ($data['lotId'] ?? 0))]);
|
||||
$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);
|
||||
$forecast = get_lager_product_forecast($pdo, $productId);
|
||||
$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') {
|
||||
$productId = delete_open_stock_lot(
|
||||
$pdo,
|
||||
(int) ($data['lotId'] ?? 0),
|
||||
(string) ($currentUser['username'] ?? $currentUser['email'] ?? 'unknown')
|
||||
);
|
||||
sync_open_lot_activation_selection($pdo, $productId);
|
||||
ensure_current_lot_and_reconcile_virtual_shortage(
|
||||
$pdo,
|
||||
$productId,
|
||||
(string) ($currentUser['username'] ?? $currentUser['email'] ?? 'unknown'),
|
||||
'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') {
|
||||
$productId = (int) ($data['productId'] ?? 0);
|
||||
$lotNumber = trim((string) ($data['lotNumber'] ?? ''));
|
||||
if ($lotNumber === '') {
|
||||
throw new InvalidArgumentException('Naurua-Chargennummer fehlt');
|
||||
}
|
||||
$lotId = (int) ($data['lotId'] ?? 0);
|
||||
$manufacturerPartyId = $data['manufacturerPartyId'] ?? null;
|
||||
$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) {
|
||||
$lotStmt = $pdo->prepare(
|
||||
"SELECT id
|
||||
@@ -85,26 +126,50 @@ try {
|
||||
throw new InvalidArgumentException('Offene Charge nicht gefunden');
|
||||
}
|
||||
} 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;
|
||||
$preparedLot = update_stock_lot_traceability(
|
||||
$pdo,
|
||||
$lotId,
|
||||
$manufacturerPartyId,
|
||||
isset($data['manufacturerLotNumber']) ? (string) $data['manufacturerLotNumber'] : null,
|
||||
isset($data['mfgDate']) ? (string) $data['mfgDate'] : null,
|
||||
isset($data['expiryDate']) ? (string) $data['expiryDate'] : null,
|
||||
isset($data['purchaseDate']) ? (string) $data['purchaseDate'] : null,
|
||||
$manufacturerLotNumber,
|
||||
$mfgDate,
|
||||
$expiryDate,
|
||||
$purchaseDate,
|
||||
(string) ($currentUser['username'] ?? $currentUser['email'] ?? 'unknown'),
|
||||
$receiptQty,
|
||||
0.0,
|
||||
0.0,
|
||||
$lotNumber
|
||||
$lotNumber,
|
||||
$targetQty,
|
||||
$lotNumber === ''
|
||||
);
|
||||
json_response(200, ['ok' => true, 'lot' => $preparedLot, 'balance' => get_stock_lot_balance($pdo, $lotId)]);
|
||||
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);
|
||||
$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]);
|
||||
}
|
||||
|
||||
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']);
|
||||
@@ -113,5 +178,19 @@ try {
|
||||
} catch (InvalidArgumentException $error) {
|
||||
json_response(422, ['ok' => false, 'error' => $error->getMessage()]);
|
||||
} catch (Throwable $error) {
|
||||
json_response(500, ['ok' => false, 'error' => 'Internal server error: ' . $error->getMessage()]);
|
||||
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']);
|
||||
}
|
||||
@@ -35,10 +35,25 @@ function lager_forecast_load_current_lots(PDO $pdo): array
|
||||
{
|
||||
$stmt = $pdo->query(
|
||||
"SELECT p.id AS product_id, p.sku, sl.id AS lot_id,
|
||||
COALESCE(v.qty_net, 0) AS physical_qty
|
||||
COALESCE(v.qty_net, 0) AS physical_qty,
|
||||
next_sl.id AS next_lot_id,
|
||||
COALESCE(next_v.qty_net, 0) AS next_physical_qty
|
||||
FROM product p
|
||||
LEFT JOIN stock_lot sl ON sl.product_id = p.id AND sl.status = 'current'
|
||||
LEFT JOIN v_stock_lot_balance v ON v.stock_lot_id = sl.id
|
||||
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'
|
||||
ORDER BY p.id"
|
||||
);
|
||||
@@ -49,10 +64,25 @@ function lager_forecast_load_current_lot(PDO $pdo, int $productId): ?array
|
||||
{
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT p.id AS product_id, p.sku, sl.id AS lot_id,
|
||||
COALESCE(v.qty_net, 0) AS physical_qty
|
||||
COALESCE(v.qty_net, 0) AS physical_qty,
|
||||
next_sl.id AS next_lot_id,
|
||||
COALESCE(next_v.qty_net, 0) AS next_physical_qty
|
||||
FROM product p
|
||||
LEFT JOIN stock_lot sl ON sl.product_id = p.id AND sl.status = 'current'
|
||||
LEFT JOIN v_stock_lot_balance v ON v.stock_lot_id = sl.id
|
||||
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
|
||||
LIMIT 1"
|
||||
);
|
||||
@@ -94,7 +124,12 @@ function lager_forecast_load_daily_sales(PDO $pdo, DateTimeImmutable $asOfDate,
|
||||
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;
|
||||
$halfLifeDays = 60;
|
||||
@@ -146,8 +181,9 @@ function lager_forecast_calculate(DateTimeImmutable $asOfDate, float $physicalQt
|
||||
? 'high'
|
||||
: ($salesDays >= 10 ? 'medium' : 'low');
|
||||
$daysUntilDepleted = (int) ceil($physicalQty / $dailyDemand);
|
||||
$forecastStartDate ??= $asOfDate;
|
||||
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,
|
||||
'sales_units' => $salesUnits, 'sales_days' => $salesDays, 'observed_span_days' => $observedSpanDays,
|
||||
'lookback_days' => $lookbackDays, 'half_life_days' => $halfLifeDays,
|
||||
@@ -225,6 +261,18 @@ function refresh_lager_stock_lot_forecast_for_product(PDO $pdo, int $productId):
|
||||
$dailySales[$productId] ?? []
|
||||
);
|
||||
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
|
||||
@@ -250,6 +298,18 @@ function run_lager_stock_lot_forecast(PDO $pdo): array
|
||||
try {
|
||||
$forecast = lager_forecast_calculate($asOfDate, (float) $lot['physical_qty'], $dailySales[$productId] ?? []);
|
||||
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']++;
|
||||
} catch (Throwable $error) {
|
||||
$result['failed_products']++;
|
||||
|
||||
+768
-33
@@ -75,7 +75,8 @@ function get_lager_product_overview(PDO $pdo): array
|
||||
p.name,
|
||||
p.sku,
|
||||
p.uom,
|
||||
COALESCE(SUM(v.qty_net), 0) AS available_qty,
|
||||
COALESCE(v.qty_net, 0) AS available_qty,
|
||||
COALESCE(v.qty_net, 0) AS current_qty,
|
||||
COALESCE(v.qty_in, 0) AS qty_in,
|
||||
sl.lot_number,
|
||||
sl.id AS lot_id,
|
||||
@@ -87,6 +88,11 @@ function get_lager_product_overview(PDO $pdo): array
|
||||
sl.purchase_date,
|
||||
sl.sellout_date,
|
||||
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((
|
||||
SELECT SUM(a.qty)
|
||||
FROM sales_order_line_lot_allocation a
|
||||
@@ -110,10 +116,25 @@ function get_lager_product_overview(PDO $pdo): array
|
||||
LEFT JOIN party mp ON mp.id = sl.manufacturer_party_id
|
||||
LEFT JOIN v_stock_lot_balance v
|
||||
ON v.stock_lot_id = sl.id
|
||||
LEFT JOIN stock_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'
|
||||
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.sellout_date, sl.sellout_forecast_status, 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"
|
||||
);
|
||||
|
||||
@@ -124,6 +145,7 @@ function get_lager_product_overview(PDO $pdo): array
|
||||
'sku' => (string) $row['sku'],
|
||||
'uom' => (string) $row['uom'],
|
||||
'available_qty' => (float) $row['available_qty'],
|
||||
'current_qty' => (float) $row['current_qty'],
|
||||
'qty_in' => (float) $row['qty_in'],
|
||||
'lot_id' => $row['lot_id'] === null ? null : (int) $row['lot_id'],
|
||||
'lot_number' => $row['lot_number'],
|
||||
@@ -135,6 +157,11 @@ function get_lager_product_overview(PDO $pdo): array
|
||||
'purchase_date' => $row['purchase_date'],
|
||||
'sellout_date' => $row['sellout_date'],
|
||||
'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_lot_number' => $row['virtual_lot_number'],
|
||||
],
|
||||
@@ -197,6 +224,7 @@ function get_lager_product_overview(PDO $pdo): array
|
||||
sl.mfg_date,
|
||||
sl.expiry_date,
|
||||
sl.purchase_date,
|
||||
sl.next_active,
|
||||
COALESCE(v.qty_in, 0) AS qty_in,
|
||||
COALESCE(v.qty_net, 0) AS qty_net
|
||||
FROM stock_lot sl
|
||||
@@ -215,9 +243,10 @@ function get_lager_product_overview(PDO $pdo): array
|
||||
'manufacturer_name' => $openLot['manufacturer_name'],
|
||||
'mfg_date' => $openLot['mfg_date'],
|
||||
'expiry_date' => $openLot['expiry_date'],
|
||||
'purchase_date' => $openLot['purchase_date'],
|
||||
'qty_in' => (float) $openLot['qty_in'],
|
||||
'qty_net' => (float) $openLot['qty_net'],
|
||||
'purchase_date' => $openLot['purchase_date'],
|
||||
'qty_in' => (float) $openLot['qty_net'],
|
||||
'qty_net' => (float) $openLot['qty_net'],
|
||||
'next_active' => (bool) $openLot['next_active'],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -230,6 +259,71 @@ function get_lager_product_overview(PDO $pdo): array
|
||||
return $products;
|
||||
}
|
||||
|
||||
function get_lager_product_available_qty(PDO $pdo, int $productId): float
|
||||
{
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT COALESCE(SUM(v.qty_net), 0)
|
||||
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 IN ('current', 'open')"
|
||||
);
|
||||
$stmt->execute([':product_id' => $productId]);
|
||||
return (float) $stmt->fetchColumn();
|
||||
}
|
||||
|
||||
function get_lager_product_forecast(PDO $pdo, int $productId): array
|
||||
{
|
||||
$stmt = $pdo->prepare(
|
||||
"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
|
||||
LEFT JOIN LATERAL (
|
||||
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"
|
||||
);
|
||||
$stmt->execute([':product_id' => $productId]);
|
||||
$forecast = $stmt->fetch();
|
||||
if (!is_array($forecast)) {
|
||||
throw new RuntimeException('Keine aktuelle Charge fuer Forecast vorhanden');
|
||||
}
|
||||
return [
|
||||
'date' => $forecast['sellout_date'],
|
||||
'status' => (string) $forecast['sellout_forecast_status'],
|
||||
'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'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
function update_stock_lot_traceability(
|
||||
PDO $pdo,
|
||||
int $lotId,
|
||||
@@ -242,7 +336,9 @@ function update_stock_lot_traceability(
|
||||
float $receiptQty = 0.0,
|
||||
float $adjustmentQty = 0.0,
|
||||
float $outQty = 0.0,
|
||||
?string $lotNumber = null
|
||||
?string $lotNumber = null,
|
||||
?float $targetQty = null,
|
||||
bool $clearLotNumber = false
|
||||
): array {
|
||||
if ($lotId <= 0) {
|
||||
throw new InvalidArgumentException('Chargen-ID fehlt');
|
||||
@@ -253,13 +349,19 @@ function update_stock_lot_traceability(
|
||||
$manufacturerLotNumber = null;
|
||||
}
|
||||
$lotNumber = $lotNumber !== null ? trim($lotNumber) : null;
|
||||
if ($lotNumber === '') {
|
||||
if ($lotNumber === '' && !$clearLotNumber) {
|
||||
throw new InvalidArgumentException('Naurua-Chargennummer fehlt');
|
||||
}
|
||||
if ($lotNumber === '') {
|
||||
$lotNumber = null;
|
||||
}
|
||||
|
||||
if (!is_finite($receiptQty) || !is_finite($adjustmentQty) || !is_finite($outQty)) {
|
||||
throw new InvalidArgumentException('Ungueltige Bewegungsmenge');
|
||||
}
|
||||
if ($targetQty !== null && (!is_finite($targetQty) || $targetQty < 0)) {
|
||||
throw new InvalidArgumentException('Ungueltige Chargenmenge');
|
||||
}
|
||||
if ($receiptQty < 0 || $outQty < 0) {
|
||||
throw new InvalidArgumentException('Zugang und Abgang duerfen nicht negativ sein');
|
||||
}
|
||||
@@ -307,6 +409,9 @@ function update_stock_lot_traceability(
|
||||
if ((string) $before['status'] === 'virtual_shortage') {
|
||||
throw new InvalidArgumentException('Virtuelle Fehlbestandschargen koennen nicht gepflegt werden');
|
||||
}
|
||||
if ($targetQty !== null && (string) $before['status'] !== 'open') {
|
||||
throw new InvalidArgumentException('Nur offene Chargen koennen mit einer Zielmenge bearbeitet werden');
|
||||
}
|
||||
if ($lotNumber !== null && (string) $before['status'] !== 'open') {
|
||||
throw new InvalidArgumentException('Nur offene Chargen koennen neu angelegt werden');
|
||||
}
|
||||
@@ -331,13 +436,35 @@ function update_stock_lot_traceability(
|
||||
if ($manufacturerPartyId !== null && $manufacturerLotNumber === null && (string) $before['status'] !== 'open') {
|
||||
throw new InvalidArgumentException('Hersteller-Chargennummer fehlt');
|
||||
}
|
||||
if (($adjustmentQty !== 0.0 || $outQty > 0) && (string) $before['status'] !== 'current') {
|
||||
if ($targetQty !== null) {
|
||||
$balanceStmt = $pdo->prepare(
|
||||
"SELECT COALESCE(SUM(CASE
|
||||
WHEN move_type = 'in' THEN qty
|
||||
WHEN move_type = 'out' THEN -qty
|
||||
WHEN move_type = 'adjustment' AND to_location_id IS NOT NULL AND from_location_id IS NULL THEN qty
|
||||
WHEN move_type = 'adjustment' AND from_location_id IS NOT NULL AND to_location_id IS NULL THEN -qty
|
||||
ELSE 0
|
||||
END), 0)
|
||||
FROM stock_move
|
||||
WHERE lot_id = :lot_id"
|
||||
);
|
||||
$balanceStmt->execute([':lot_id' => $lotId]);
|
||||
$quantityDelta = $targetQty - (float) $balanceStmt->fetchColumn();
|
||||
$receiptQty = $quantityDelta > 0 ? $quantityDelta : 0.0;
|
||||
$outQty = 0.0;
|
||||
$adjustmentQty = $quantityDelta < 0 ? $quantityDelta : 0.0;
|
||||
}
|
||||
|
||||
if (($adjustmentQty !== 0.0 || $outQty > 0) && (string) $before['status'] !== 'current' && $targetQty === null) {
|
||||
throw new InvalidArgumentException('Korrekturen und Abgaenge koennen nur auf der aktiven Charge gebucht werden');
|
||||
}
|
||||
|
||||
$lotNumberAssignment = $clearLotNumber
|
||||
? 'NULL'
|
||||
: ($lotNumber === null ? 'lot_number' : 'COALESCE(:lot_number, lot_number)');
|
||||
$updateStmt = $pdo->prepare(
|
||||
"UPDATE stock_lot
|
||||
SET lot_number = COALESCE(:lot_number, lot_number),
|
||||
SET lot_number = {$lotNumberAssignment},
|
||||
manufacturer_party_id = :manufacturer_party_id,
|
||||
manufacturer_lot_number = :manufacturer_lot_number,
|
||||
mfg_date = :mfg_date,
|
||||
@@ -346,18 +473,23 @@ function update_stock_lot_traceability(
|
||||
updated_at = NOW()
|
||||
WHERE id = :id"
|
||||
);
|
||||
$updateStmt->execute([
|
||||
':lot_number' => $lotNumber,
|
||||
$updateParams = [
|
||||
':manufacturer_party_id' => $manufacturerPartyId,
|
||||
':manufacturer_lot_number' => $manufacturerLotNumber,
|
||||
':mfg_date' => $dates['mfg_date'],
|
||||
':expiry_date' => $dates['expiry_date'],
|
||||
':purchase_date' => $dates['purchase_date'],
|
||||
':id' => $lotId,
|
||||
]);
|
||||
];
|
||||
if (!$clearLotNumber && $lotNumber !== null) {
|
||||
$updateParams[':lot_number'] = $lotNumber;
|
||||
}
|
||||
$updateStmt->execute($updateParams);
|
||||
|
||||
$after = $before;
|
||||
if ($lotNumber !== null) {
|
||||
if ($clearLotNumber) {
|
||||
$after['lot_number'] = null;
|
||||
} elseif ($lotNumber !== null) {
|
||||
$after['lot_number'] = $lotNumber;
|
||||
}
|
||||
$after['manufacturer_party_id'] = $manufacturerPartyId;
|
||||
@@ -370,7 +502,7 @@ function update_stock_lot_traceability(
|
||||
]);
|
||||
|
||||
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) {
|
||||
@@ -386,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
|
||||
{
|
||||
$stmt = $pdo->prepare(
|
||||
@@ -439,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(
|
||||
PDO $pdo,
|
||||
int $productId,
|
||||
@@ -547,7 +1219,8 @@ function book_stock_lot_movements(
|
||||
int $lotId,
|
||||
float $receiptQty,
|
||||
float $adjustmentQty,
|
||||
float $outQty
|
||||
float $outQty,
|
||||
string $changedBy
|
||||
): void {
|
||||
$locations = get_default_location_ids($pdo);
|
||||
$balanceStmt = $pdo->prepare(
|
||||
@@ -582,6 +1255,12 @@ function book_stock_lot_movements(
|
||||
insert_stock_move_adjustment($pdo, $productId, $lotId, $correctionQty, $locations['storage'], null, 'Manuelle Bestandskorrektur via Chargenformular');
|
||||
}
|
||||
$balance += $adjustmentQty;
|
||||
ensure_current_lot_and_reconcile_virtual_shortage(
|
||||
$pdo,
|
||||
$productId,
|
||||
$changedBy,
|
||||
'Verrechnung nach manueller Bestandskorrektur'
|
||||
);
|
||||
}
|
||||
|
||||
if ($outQty > $balance) {
|
||||
@@ -589,6 +1268,12 @@ function book_stock_lot_movements(
|
||||
}
|
||||
if ($outQty > 0) {
|
||||
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'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -740,23 +1425,8 @@ function get_or_create_virtual_shortage_lot(PDO $pdo, int $productId): int
|
||||
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 INTO stock_lot (product_id, status, created_at, updated_at)
|
||||
VALUES (:product_id, 'open', NOW(), NOW())
|
||||
@@ -771,6 +1441,60 @@ function get_or_create_open_lot(PDO $pdo, int $productId): int
|
||||
return (int) $newLotId;
|
||||
}
|
||||
|
||||
function delete_open_stock_lot(PDO $pdo, int $lotId, string $changedBy): int
|
||||
{
|
||||
if ($lotId <= 0 || trim($changedBy) === '') {
|
||||
throw new InvalidArgumentException('Offene Charge und Bediener sind erforderlich');
|
||||
}
|
||||
|
||||
$startedTransaction = !$pdo->inTransaction();
|
||||
if ($startedTransaction) {
|
||||
$pdo->beginTransaction();
|
||||
}
|
||||
|
||||
try {
|
||||
$lotStmt = $pdo->prepare(
|
||||
"SELECT id, product_id, lot_number, manufacturer_party_id, manufacturer_lot_number,
|
||||
mfg_date, expiry_date, purchase_date, status
|
||||
FROM stock_lot
|
||||
WHERE id = :id
|
||||
FOR UPDATE"
|
||||
);
|
||||
$lotStmt->execute([':id' => $lotId]);
|
||||
$lot = $lotStmt->fetch();
|
||||
if (!is_array($lot)) {
|
||||
throw new InvalidArgumentException('Charge nicht gefunden');
|
||||
}
|
||||
if ((string) $lot['status'] !== 'open') {
|
||||
throw new InvalidArgumentException('Nur offene Chargen koennen geloescht werden');
|
||||
}
|
||||
|
||||
system_write_audit_log($pdo, 'stock_lot', (string) $lotId, 'deleted', $changedBy, $lot, [], [
|
||||
'module' => 'erp.lager',
|
||||
]);
|
||||
|
||||
$moveStmt = $pdo->prepare('DELETE FROM stock_move WHERE lot_id = :lot_id');
|
||||
$moveStmt->execute([':lot_id' => $lotId]);
|
||||
|
||||
$deleteStmt = $pdo->prepare('DELETE FROM stock_lot WHERE id = :id AND status = \'open\'');
|
||||
$deleteStmt->execute([':id' => $lotId]);
|
||||
if ($deleteStmt->rowCount() !== 1) {
|
||||
throw new RuntimeException('Offene Charge konnte nicht geloescht werden');
|
||||
}
|
||||
|
||||
$productId = (int) $lot['product_id'];
|
||||
if ($startedTransaction) {
|
||||
$pdo->commit();
|
||||
}
|
||||
return $productId;
|
||||
} catch (Throwable $error) {
|
||||
if ($startedTransaction && $pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
throw $error;
|
||||
}
|
||||
}
|
||||
|
||||
function replace_virtual_lot_allocation(
|
||||
PDO $pdo,
|
||||
int $allocationId,
|
||||
@@ -829,8 +1553,7 @@ function replace_virtual_lot_allocation(
|
||||
$balanceStmt = $pdo->prepare(
|
||||
"SELECT COALESCE(v.qty_net, 0)
|
||||
FROM v_stock_lot_balance v
|
||||
WHERE v.stock_lot_id = :lot_id
|
||||
FOR UPDATE"
|
||||
WHERE v.stock_lot_id = :lot_id"
|
||||
);
|
||||
$balanceStmt->execute([':lot_id' => $realLotId]);
|
||||
$available = (float) ($balanceStmt->fetchColumn() ?: 0);
|
||||
@@ -953,6 +1676,12 @@ function allocate_components_for_line(
|
||||
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);
|
||||
$lotId = $current['lot_id'];
|
||||
$available = $current['qty_net'];
|
||||
@@ -974,6 +1703,12 @@ function allocate_components_for_line(
|
||||
(int) $locations['dispatch'],
|
||||
"{$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';
|
||||
}
|
||||
|
||||
|
||||
+187
-180
@@ -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
|
||||
{
|
||||
$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[] = '<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>';
|
||||
@@ -209,41 +233,32 @@ function auth_render_lager_table(string $title, array $rows, array $columns, str
|
||||
$value = $tableKey === 'products'
|
||||
? (string) max(0, (int) round((float) ($row['available_qty'] ?? 0)))
|
||||
: (string) ($row['item_code'] ?? '');
|
||||
$forecastText = '';
|
||||
$forecastWarning = false;
|
||||
$forecastMhdWarning = false;
|
||||
$currentForecast = ['text' => '', 'warning' => false, 'mhdWarning' => false];
|
||||
$nextForecast = ['text' => '-', 'warning' => false, 'mhdWarning' => false];
|
||||
if ($tableKey === 'products') {
|
||||
$forecastStatus = (string) ($row['sellout_forecast_status'] ?? 'no_demand');
|
||||
$forecastDate = $row['sellout_date'] ?? null;
|
||||
if ($forecastDate !== null && $forecastDate !== '') {
|
||||
$forecastDateObject = new DateTimeImmutable((string) $forecastDate);
|
||||
$forecastText = $forecastDateObject->format('d.m.Y');
|
||||
if ($forecastStatus === 'depleted') {
|
||||
$forecastWarning = true;
|
||||
$forecastText .= ' (CHARGE ENDE)';
|
||||
} elseif (!empty($row['expiry_date'])) {
|
||||
$expiryDate = new DateTimeImmutable((string) $row['expiry_date']);
|
||||
$forecastWarning = $forecastDateObject >= $expiryDate->modify('-3 months')
|
||||
&& $forecastDateObject <= $expiryDate;
|
||||
$forecastMhdWarning = $forecastDateObject > $expiryDate;
|
||||
if ($forecastWarning || $forecastMhdWarning) {
|
||||
$forecastText .= ' (MHD: ' . $expiryDate->format('d.m.Y') . ')';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$forecastText = 'Keine Prognose';
|
||||
}
|
||||
$currentForecast = $renderForecast(
|
||||
$row['sellout_date'] ?? null,
|
||||
(string) ($row['sellout_forecast_status'] ?? 'no_demand'),
|
||||
$row['expiry_date'] ?? null,
|
||||
true
|
||||
);
|
||||
$nextForecast = $renderForecast(
|
||||
$row['next_sellout_date'] ?? null,
|
||||
(string) ($row['next_sellout_forecast_status'] ?? 'no_demand'),
|
||||
$row['next_expiry_date'] ?? null,
|
||||
$row['next_lot_id'] !== null
|
||||
);
|
||||
}
|
||||
$nameData = ' data-lager-drawer-name="' . auth_escape_html($name) . '"';
|
||||
$valueData = ' data-lager-drawer-value="' . auth_escape_html($value) . '"';
|
||||
$typeData = ' data-lager-drawer-type="' . auth_escape_html($tableKey === 'products' ? 'Produkt' : 'Artikel') . '"';
|
||||
$detailData = '';
|
||||
if ($tableKey === 'products') {
|
||||
$detailData = ' data-lager-drawer-forecast="' . auth_escape_html($forecastText) . '"'
|
||||
. ' data-lager-drawer-forecast-status="' . auth_escape_html($forecastStatus) . '"'
|
||||
. ' data-lager-drawer-forecast-warning="' . ($forecastWarning ? 'true' : 'false') . '"'
|
||||
. ' data-lager-drawer-forecast-mhd-warning="' . ($forecastMhdWarning ? 'true' : 'false') . '"'
|
||||
. ' data-lager-drawer-current-qty="' . auth_escape_html(auth_format_lager_quantity((float) ($row['available_qty'] ?? 0))) . '"'
|
||||
$detailData = ' data-lager-drawer-forecast="' . auth_escape_html($currentForecast['text']) . '"'
|
||||
. ' data-lager-drawer-forecast-status="' . auth_escape_html((string) ($row['sellout_forecast_status'] ?? 'no_demand')) . '"'
|
||||
. ' data-lager-drawer-forecast-warning="' . ($currentForecast['warning'] ? '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-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-lot-id="' . auth_escape_html((string) ($row['lot_id'] ?? '')) . '"'
|
||||
@@ -269,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($value) . '</a></div>';
|
||||
if ($tableKey === 'products') {
|
||||
$forecastClass = $forecastWarning
|
||||
$forecastClass = $currentForecast['warning']
|
||||
? ' sg-strong lager-forecast-warning'
|
||||
: ($forecastMhdWarning ? ' sg-strong lager-forecast-mhd-warning' : '');
|
||||
$html[] = '<div class="sg-large-table__cell' . $forecastClass . '" role="cell">' . auth_escape_html($forecastText) . '</div>';
|
||||
: ($currentForecast['mhdWarning'] ? ' sg-strong lager-forecast-mhd-warning' : '');
|
||||
$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>';
|
||||
}
|
||||
@@ -302,7 +322,7 @@ function auth_render_lager_historical_lots(array $lagerProducts): string
|
||||
$html = [];
|
||||
foreach ($lagerProducts as $product) {
|
||||
$productId = (int) ($product['id'] ?? 0);
|
||||
$html[] = '<div data-lager-historical-product="' . $productId . '" hidden>';
|
||||
$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) {
|
||||
$qtyNet = (float) ($lot['qty_net'] ?? 0);
|
||||
$emptyDate = $qtyNet <= 0 ? auth_format_lager_date($lot['last_out_at'] ?? null) : 'Nicht eindeutig';
|
||||
@@ -320,7 +340,7 @@ function auth_render_lager_historical_lots(array $lagerProducts): string
|
||||
. '<strong>Hersteller-Chargennummer:</strong> ' . auth_escape_html((string) ($lot['manufacturer_lot_number'] ?? 'Nicht hinterlegt')) . '</p>'
|
||||
. '</div></article>';
|
||||
}
|
||||
$html[] = '</div>';
|
||||
$html[] = '</div></div>';
|
||||
}
|
||||
|
||||
return implode('', $html);
|
||||
@@ -331,12 +351,22 @@ function auth_render_lager_open_lots(array $lagerProducts, string $manufacturerO
|
||||
$html = [];
|
||||
foreach ($lagerProducts as $product) {
|
||||
$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) {
|
||||
$lotId = (int) ($lot['lot_id'] ?? 0);
|
||||
$html[] = '<article class="sg-card sg-card--content-card" data-component="content-card" data-lager-open-card-wrapper="' . $lotId . '" 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><button class="sg-interaction-element sg-button sg-button--active" type="button" data-lager-open-edit-open>Bearbeiten</button></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>Naurua-Chargennummer:</strong> <span data-lager-open-card-lot-number>' . auth_escape_html((string) ($lot['lot_number'] ?? 'Nicht hinterlegt')) . '</span><br>'
|
||||
$selectedClass = !empty($lot['next_active']) ? ' sg-lager-open-lot--next-active' : '';
|
||||
$selectedText = !empty($lot['next_active']) ? 'Als nächste aktive Charge ausgewählt' : 'Nicht ausgewählt';
|
||||
$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>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>'
|
||||
@@ -347,16 +377,18 @@ function auth_render_lager_open_lots(array $lagerProducts, string $manufacturerO
|
||||
. '<strong>Einkaufsdatum:</strong> <span data-lager-open-card-purchase-date>' . auth_format_lager_date($lot['purchase_date'] ?? null) . '</span></p></div>'
|
||||
. '<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>'
|
||||
. '<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 required 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-Chargennummer</span><input class="sg-interaction-element sg-input-single-line" type="text" data-lager-open-manufacturer-lot value="' . auth_escape_html((string) ($lot['manufacturer_lot_number'] ?? '')) . '"></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-open-mfg-date value="' . auth_escape_html((string) ($lot['mfg_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">Zugang buchen</span><input class="sg-interaction-element sg-input-single-line" type="number" min="0" step="1" inputmode="numeric" data-lager-open-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-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>';
|
||||
. '<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><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>'
|
||||
. '<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>';
|
||||
}
|
||||
$html[] = '</div>';
|
||||
$html[] = '</div></div>';
|
||||
}
|
||||
|
||||
return implode('', $html);
|
||||
@@ -370,8 +402,24 @@ function auth_render_lager_content(array $lagerProducts, array $lagerArtikel, ar
|
||||
}
|
||||
|
||||
$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');
|
||||
$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) . '">'
|
||||
. '<section class="sg-card-list-page" aria-label="' . auth_escape_html($sectionTitle) . '">'
|
||||
@@ -379,11 +427,11 @@ function auth_render_lager_content(array $lagerProducts, array $lagerArtikel, ar
|
||||
. '</section>'
|
||||
. '<div class="sg-card-list-page-drawer-backdrop" aria-hidden="true" data-lager-drawer-backdrop="true" data-open="false"></div>'
|
||||
. '<aside class="sg-card-list-page-drawer" aria-label="Lager-Details" aria-hidden="true" data-lager-drawer="true" data-open="false">'
|
||||
. '<div class="sg-card-list-page-drawer__header"><h2 class="sg-heading-h2 sg-card-list-page-drawer__title" data-lager-drawer-title>Lager-Details</h2><button class="sg-interaction-element sg-hyperlink" type="button" data-lager-drawer-close>schliessen</button></div>'
|
||||
. '<div class="sg-card-list-page-drawer__header"><h2 class="sg-heading-h2 sg-card-list-page-drawer__title" data-lager-drawer-title>Lager-Details</h2></div>'
|
||||
. '<div class="sg-card-list-page-drawer__content">'
|
||||
. '<article class="sg-card sg-vsf-drawer-card" data-component="card" data-pattern="card" aria-label="Lager-Details">'
|
||||
. '<header class="sg-card-segment sg-card-segment--header sg-card-segment--darkblue sg-vsf-drawer-card__header"><div class="sg-strong">Lager-Details</div></header>'
|
||||
. '<div class="sg-card-segment sg-card-segment--body sg-vsf-drawer-card__content"><p class="sg-body"><strong>Name:</strong> <span data-lager-drawer-name-value></span></p><p class="sg-body" data-lager-drawer-value-row><strong data-lager-drawer-value-label>Wert:</strong> <span data-lager-drawer-value-value></span></p></div>'
|
||||
. '<div class="sg-card-segment sg-card-segment--body sg-vsf-drawer-card__content"><p class="sg-body"><strong>Name:</strong> <span data-lager-drawer-name-value></span></p><p class="sg-body" data-lager-drawer-value-row><strong data-lager-drawer-value-label>Wert:</strong> <span data-lager-drawer-value-value></span><br><strong>Voraussichtlich leer:</strong> <span data-lager-drawer-forecast-value>Keine Prognose</span></p></div>'
|
||||
. '</article>'
|
||||
. '<article class="sg-card sg-card--content-card" data-component="content-card" data-lager-drawer-topic="warning" aria-label="Warnungen">'
|
||||
. '<div class="sg-card-segment sg-card-segment--header sg-card-segment--darkgreen" data-component-part="card-header" data-lager-drawer-warning-header><div class="sg-strong sg-text-on-dark" data-lager-drawer-warning-title>Keine Warnung</div></div>'
|
||||
@@ -394,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-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>'
|
||||
. '<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-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>'
|
||||
@@ -402,7 +450,9 @@ 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">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>'
|
||||
. '<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>'
|
||||
. '<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>'
|
||||
. '<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>'
|
||||
@@ -427,7 +477,7 @@ function auth_render_lager_content(array $lagerProducts, array $lagerArtikel, ar
|
||||
. '<div data-lager-drawer-topic="historical-lots" aria-label="Vergangene Chargen">' . auth_render_lager_historical_lots($lagerProducts) . '</div>'
|
||||
. '<article class="sg-card sg-card--content-card" data-component="content-card" data-lager-drawer-topic="virtual-lot" aria-label="Virtuelle Charge">'
|
||||
. '<div class="sg-card-segment sg-card-segment--header sg-card-segment--darkgreen" data-component-part="card-header"><div class="sg-strong sg-text-on-dark">Virtuelle Charge</div></div>'
|
||||
. '<div class="sg-card-segment sg-card-segment--body" data-component-part="card-body"><p class="sg-body" data-lager-drawer-virtual-lot-value></p></div>'
|
||||
. '<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>'
|
||||
. '</div>'
|
||||
. '</aside>'
|
||||
@@ -448,9 +498,9 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
$disabledAttribute = $hasSellableMapping ? '' : ' disabled aria-disabled="true"';
|
||||
|
||||
$otcProductRows .= '<div class="sg-form-sections-card__field-group">';
|
||||
$otcProductRows .= '<label class="sg-label" for="' . $inputId . '">' . $productName . ($productSku !== '' ? ' (' . $productSku . ')' : '') . $mappingNote . '</label>';
|
||||
$otcProductRows .= '<input class="sg-interaction-element sg-input-single-line" type="number" id="' . $inputId . '" min="0" step="1" data-otc-order-product data-product-id="' . $productId . '" data-title="' . $productName . '" data-available-qty="' . $availableQty . '"' . $disabledAttribute . '>';
|
||||
$otcProductRows .= '<p class="sg-body sg-form-sections-card__sentence">Physischer Bestand: ' . $availableQty . '</p>';
|
||||
$otcProductRows .= '<label class="sg-labeled-input-row sg-otc-order-product-row"><span class="sg-label">' . $productName . ($productSku !== '' ? ' (' . $productSku . ')' : '') . $mappingNote . '</span>';
|
||||
$otcProductRows .= '<input class="sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable" type="number" id="' . $inputId . '" min="0" step="1" data-otc-order-product data-product-id="' . $productId . '" data-title="' . $productName . '" data-available-qty="' . $availableQty . '"' . $disabledAttribute . '>';
|
||||
$otcProductRows .= '</label>';
|
||||
$otcProductRows .= '</div>';
|
||||
}
|
||||
|
||||
@@ -555,14 +605,14 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo '</section>';
|
||||
echo '<section class="sg-otc-order-overlay" data-otc-order-overlay data-open="false" aria-hidden="true">';
|
||||
echo '<div class="sg-delete-confirmation-pattern__stage sg-delete-confirmation-pattern__host sg-object-card" data-pattern="overlay-card" data-dialog-open="false" data-otc-confirmation-stage>';
|
||||
echo '<article class="sg-card sg-object-card sg-object-card--variable-height sg-otc-order-overlay__panel sg-delete-confirmation-pattern__target" data-pattern="object-card" aria-labelledby="otc-order-modal-title" role="dialog" aria-modal="true">';
|
||||
echo '<header class="sg-card-segment sg-card-segment--header sg-card-segment--darkblue sg-object-card__header">';
|
||||
echo '<article class="sg-card sg-scrollable-card sg-delete-confirmation-pattern__target sg-otc-order-overlay__panel" data-pattern="scrollable-card" aria-labelledby="otc-order-modal-title" role="dialog" aria-modal="true">';
|
||||
echo '<header class="sg-card-segment sg-card-segment--header sg-card-segment--darkblue sg-scrollable-card__header sg-object-card__header" data-pattern-part="scrollable-card-header">';
|
||||
echo '<h2 id="otc-order-modal-title" class="sg-strong sg-otc-order-overlay__title">Neuen OTC-Verkauf erfassen</h2>';
|
||||
echo '<button class="sg-interaction-element sg-button sg-button--active sg-otc-order-overlay__close" type="button" data-otc-order-close>Schliessen</button>';
|
||||
echo '</header>';
|
||||
echo '<div class="sg-card-segment sg-card-segment--gray sg-object-card__content">';
|
||||
echo '<div class="sg-card-segment sg-card-segment--gray sg-scrollable-card__content sg-object-card__content" data-pattern-part="scrollable-card-content" tabindex="0" aria-label="Scrollbarer Formularinhalt">';
|
||||
echo '<div class="sg-form-sections-card-wrapper" data-pattern="form-sections" aria-label="Formular mit Abschnitten">';
|
||||
echo '<form class="sg-form-sections-card" action="#" method="post" data-otc-order-form aria-label="Neuen OTC-Verkauf erfassen">';
|
||||
echo '<form id="otc-order-form" class="sg-form-sections-card" action="#" method="post" data-otc-order-form aria-label="Neuen OTC-Verkauf erfassen">';
|
||||
echo '<div class="sg-form-sections-card__body" data-pattern-part="form-body">';
|
||||
echo '<section class="sg-form-sections-card__chapter" aria-labelledby="otc-products-title">';
|
||||
echo '<h2 id="otc-products-title" class="sg-strong sg-form-sections-card__chapter-title">PRODUKTE</h2>';
|
||||
@@ -571,20 +621,23 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo '<section class="sg-form-sections-card__chapter" aria-labelledby="otc-order-date-title">';
|
||||
echo '<h2 id="otc-order-date-title" class="sg-strong sg-form-sections-card__chapter-title">Datum</h2>';
|
||||
echo '<div class="sg-form-sections-card__field-group">';
|
||||
echo '<label class="sg-label" for="otc-order-date">Bestelldatum</label>';
|
||||
echo '<input class="sg-interaction-element sg-input-single-line" type="date" id="otc-order-date" value="' . htmlspecialchars(date('Y-m-d'), ENT_QUOTES, 'UTF-8') . '" data-otc-order-date>';
|
||||
echo '<label class="sg-labeled-input-row"><span class="sg-label">Bestelldatum</span>';
|
||||
echo '<input class="sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable" type="date" id="otc-order-date" value="' . htmlspecialchars(date('Y-m-d'), ENT_QUOTES, 'UTF-8') . '" data-otc-order-date>';
|
||||
echo '</label>';
|
||||
echo '</div>';
|
||||
echo '</section>';
|
||||
echo '<section class="sg-form-sections-card__chapter" aria-labelledby="otc-payment-title">';
|
||||
echo '<h2 id="otc-payment-title" class="sg-strong sg-form-sections-card__chapter-title">Bezahlung</h2>';
|
||||
echo '<div data-otc-payment-rows>';
|
||||
echo '<div class="sg-form-sections-card__field-group" data-otc-payment-row>';
|
||||
echo '<label class="sg-label" for="otc-payment-method-1">Bezahlart</label>';
|
||||
echo '<select class="sg-interaction-element sg-input-single-line" id="otc-payment-method-1" data-otc-payment-method>';
|
||||
echo '<label class="sg-labeled-input-row"><span class="sg-label">Bezahlart</span>';
|
||||
echo '<select class="sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable sg-pulldown" id="otc-payment-method-1" data-otc-payment-method>';
|
||||
echo '<option value="twint">Twint</option><option value="card">Kreditkarte</option><option value="cash">Barzahlung</option><option value="paypal">PayPal</option><option value="bank_transfer">Überweisung</option>';
|
||||
echo '</select>';
|
||||
echo '<label class="sg-label" for="otc-payment-amount-1">Betrag (CHF)</label>';
|
||||
echo '<input class="sg-interaction-element sg-input-single-line" type="number" id="otc-payment-amount-1" min="0.01" step="0.01" value="0.00" data-otc-payment-amount>';
|
||||
echo '</label>';
|
||||
echo '<label class="sg-labeled-input-row"><span class="sg-label">Betrag (CHF)</span>';
|
||||
echo '<input class="sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable" type="number" id="otc-payment-amount-1" min="0.01" step="0.01" value="0.00" data-otc-payment-amount>';
|
||||
echo '</label>';
|
||||
echo '</div>';
|
||||
echo '</div>';
|
||||
echo '<div class="sg-form-sections-card__actions-segment">';
|
||||
@@ -594,43 +647,49 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo '<section class="sg-form-sections-card__chapter" aria-labelledby="otc-billing-title">';
|
||||
echo '<h2 id="otc-billing-title" class="sg-strong sg-form-sections-card__chapter-title">Rechnungsadresse</h2>';
|
||||
echo '<div class="sg-form-sections-card__field-group">';
|
||||
echo '<label class="sg-label" for="otc-first-name">Vorname</label>';
|
||||
echo '<input class="sg-interaction-element sg-input-single-line" type="text" id="otc-first-name" value="Fabienne" data-otc-order-first-name>';
|
||||
echo '<label class="sg-labeled-input-row"><span class="sg-label">Vorname</span>';
|
||||
echo '<input class="sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable" type="text" id="otc-first-name" value="Fabienne" data-otc-order-first-name>';
|
||||
echo '</label>';
|
||||
echo '</div>';
|
||||
echo '<div class="sg-form-sections-card__field-group">';
|
||||
echo '<label class="sg-label" for="otc-last-name">Nachname</label>';
|
||||
echo '<input class="sg-interaction-element sg-input-single-line" type="text" id="otc-last-name" value="Föhn" data-otc-order-last-name>';
|
||||
echo '<label class="sg-labeled-input-row"><span class="sg-label">Nachname</span>';
|
||||
echo '<input class="sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable" type="text" id="otc-last-name" value="Föhn" data-otc-order-last-name>';
|
||||
echo '</label>';
|
||||
echo '</div>';
|
||||
echo '<div class="sg-form-sections-card__field-group">';
|
||||
echo '<label class="sg-label" for="otc-street">Strasse</label>';
|
||||
echo '<input class="sg-interaction-element sg-input-single-line" type="text" id="otc-street" value="Im Hochrain" data-otc-order-street>';
|
||||
echo '<label class="sg-labeled-input-row"><span class="sg-label">Strasse</span>';
|
||||
echo '<input class="sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable" type="text" id="otc-street" value="Im Hochrain" data-otc-order-street>';
|
||||
echo '</label>';
|
||||
echo '</div>';
|
||||
echo '<div class="sg-form-sections-card__field-group">';
|
||||
echo '<label class="sg-label" for="otc-house-number">Hausnummer</label>';
|
||||
echo '<input class="sg-interaction-element sg-input-single-line" type="text" id="otc-house-number" value="2" data-otc-order-house-number>';
|
||||
echo '<label class="sg-labeled-input-row"><span class="sg-label">Hausnummer</span>';
|
||||
echo '<input class="sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable" type="text" id="otc-house-number" value="2" data-otc-order-house-number>';
|
||||
echo '</label>';
|
||||
echo '</div>';
|
||||
echo '<div class="sg-form-sections-card__field-group">';
|
||||
echo '<label class="sg-label" for="otc-zip">PLZ</label>';
|
||||
echo '<input class="sg-interaction-element sg-input-single-line" type="text" id="otc-zip" value="8102" data-otc-order-zip>';
|
||||
echo '<label class="sg-labeled-input-row"><span class="sg-label">PLZ</span>';
|
||||
echo '<input class="sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable" type="text" id="otc-zip" value="8102" data-otc-order-zip>';
|
||||
echo '</label>';
|
||||
echo '</div>';
|
||||
echo '<div class="sg-form-sections-card__field-group">';
|
||||
echo '<label class="sg-label" for="otc-city">Ort</label>';
|
||||
echo '<input class="sg-interaction-element sg-input-single-line" type="text" id="otc-city" value="Oberengstringen" data-otc-order-city>';
|
||||
echo '<label class="sg-labeled-input-row"><span class="sg-label">Ort</span>';
|
||||
echo '<input class="sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable" type="text" id="otc-city" value="Oberengstringen" data-otc-order-city>';
|
||||
echo '</label>';
|
||||
echo '</div>';
|
||||
echo '<div class="sg-form-sections-card__field-group">';
|
||||
echo '<div class="sg-body sg-otc-order-form__status hidden" data-otc-order-success>Bestellung erfolgreich erfasst! Die Bestellnummer wird automatisch generiert.</div>';
|
||||
echo '<div class="sg-body sg-otc-order-form__status sg-otc-order-form__status--error hidden" data-otc-order-error></div>';
|
||||
echo '</div>';
|
||||
echo '</div>';
|
||||
echo '<footer class="sg-form-sections-card__actions-segment" data-pattern-part="form-actions-segment">';
|
||||
echo '<div class="sg-form-sections-card__actions" data-pattern-part="form-actions">';
|
||||
echo '<button class="sg-interaction-element sg-button sg-button--active sg-form-sections-card__action" type="button" data-otc-order-close>Abbrechen</button>';
|
||||
echo '<button class="sg-interaction-element sg-button sg-button--process sg-button--process-inactive sg-form-sections-card__action" type="submit" disabled aria-disabled="true" data-otc-order-submit>Weiter</button>';
|
||||
echo '</div>';
|
||||
echo '</footer>';
|
||||
echo '</form>';
|
||||
echo '</div>';
|
||||
echo '</div>';
|
||||
echo '<footer class="sg-card-segment sg-card-segment--body sg-scrollable-card__actions-segment" data-pattern-part="scrollable-card-actions">';
|
||||
echo '<div class="sg-scrollable-card__actions">';
|
||||
echo '<button class="sg-interaction-element sg-button sg-button--active" type="button" form="otc-order-form" data-otc-order-close>Abbrechen</button>';
|
||||
echo '<button class="sg-interaction-element sg-button sg-button--process sg-button--process-inactive" type="submit" form="otc-order-form" disabled aria-disabled="true" data-otc-order-submit>Weiter</button>';
|
||||
echo '</div>';
|
||||
echo '</footer>';
|
||||
echo '</article>';
|
||||
echo '<article class="sg-card sg-card--overlay-host sg-delete-confirmation-pattern__floating-card" aria-label="Verkaufsbestätigung" role="dialog" aria-modal="true" aria-labelledby="otc-confirmation-title" data-otc-confirmation-card hidden>';
|
||||
echo '<div class="sg-card-segment sg-card-segment--body sg-delete-confirmation-pattern__body">';
|
||||
@@ -775,30 +834,13 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " lagerBindingsInstalled = true;";
|
||||
echo " let lagerMobileDetailHost = null;";
|
||||
echo " let lagerMobileDetailOpen = false;";
|
||||
echo " const escapeLagerHtml = (value) => String(value ?? '').replace(/[&<>\"']/g, (character) => ({ '&': '&', '<': '<', '>': '>', '\"': '"', \"'\": ''' }[character] || character));";
|
||||
echo " const renderLagerMobileDetail = (trigger) => {";
|
||||
echo " const type = trigger.dataset.lagerDrawerType || 'Lager';";
|
||||
echo " const name = trigger.dataset.lagerDrawerName || '';";
|
||||
echo " const value = trigger.dataset.lagerDrawerValue || '';";
|
||||
echo " const isProduct = type === 'Produkt';";
|
||||
echo " const forecast = trigger.dataset.lagerDrawerForecast || 'Keine Prognose';";
|
||||
echo " const warning = trigger.dataset.lagerDrawerForecastWarning === 'true' || trigger.dataset.lagerDrawerForecastMhdWarning === 'true';";
|
||||
echo " const warningText = warning ? forecast : 'Keine akute Warnung.';";
|
||||
echo " const lotNumber = trigger.dataset.lagerDrawerLotNumber || 'Nicht hinterlegt';";
|
||||
echo " const manufacturerName = trigger.dataset.lagerDrawerManufacturerName || 'Nicht hinterlegt';";
|
||||
echo " const manufacturerLotNumber = trigger.dataset.lagerDrawerManufacturerLotNumber || 'Nicht hinterlegt';";
|
||||
echo " const mfgDate = trigger.dataset.lagerDrawerMfgDate || 'Nicht hinterlegt';";
|
||||
echo " const expiryDate = trigger.dataset.lagerDrawerExpiryDate || 'Nicht hinterlegt';";
|
||||
echo " const purchaseDate = trigger.dataset.lagerDrawerPurchaseDate || 'Nicht hinterlegt';";
|
||||
echo " const virtualLot = trigger.dataset.lagerDrawerVirtualLotNumber || '';";
|
||||
echo " return ['<div class=\"sg-lager-mobile-detail-host\" data-lager-mobile-detail-host=\"true\">', '<div class=\"sg-navigation-card-layout sg-mobile-detail-back-card\">', '<div class=\"sg-navigation-card-block\">', '<article class=\"sg-card\" data-component=\"card\" data-pattern=\"navigation-card\" aria-label=\"Navigation Detailansicht zurück oben\">', '<div class=\"sg-card-segment sg-card-segment--body\" data-component-part=\"card-body\" data-pattern-part=\"navigation-card-segment\">', '<div class=\"sg-navigation-card-center\">', '<button class=\"sg-interaction-element sg-hyperlink\" type=\"button\" data-lager-detail-back>zurück</button>', '</div></div></article></div></div>', '<article class=\"sg-card sg-vsf-drawer-card\" data-component=\"card\" data-pattern=\"card\" aria-label=\"Lager-Details\">', '<header class=\"sg-card-segment sg-card-segment--header sg-card-segment--darkblue\"><div class=\"sg-strong\">' + escapeLagerHtml(type) + ': ' + escapeLagerHtml(name) + '</div></header>', '<div class=\"sg-card-segment sg-card-segment--body sg-vsf-drawer-card__content\"><p class=\"sg-body\"><strong>Name:</strong> ' + escapeLagerHtml(name) + '</p><p class=\"sg-body\"><strong>' + (isProduct ? 'Anzahl im Lager:' : 'Artikelnummer:') + '</strong> ' + escapeLagerHtml(value) + '</p>' + (isProduct ? '<p class=\"sg-body\"><strong>Warnungen:</strong> ' + escapeLagerHtml(warningText) + '</p><p class=\"sg-body\"><strong>Naurua-Chargennummer:</strong> ' + escapeLagerHtml(lotNumber) + '<br><strong>Hersteller:</strong> ' + escapeLagerHtml(manufacturerName) + '<br><strong>Hersteller-Chargennummer:</strong> ' + escapeLagerHtml(manufacturerLotNumber) + '<br><strong>Herstellungsdatum:</strong> ' + escapeLagerHtml(mfgDate) + '<br><strong>MHD:</strong> ' + escapeLagerHtml(expiryDate) + '<br><strong>Einkaufsdatum:</strong> ' + escapeLagerHtml(purchaseDate) + '</p><p class=\"sg-body\"><strong>Virtuelle Charge:</strong> ' + escapeLagerHtml(virtualLot || 'Keine virtuelle Fehlbestandscharge vorhanden.') + '</p>' : '') + '</div></article></div>'].join('');";
|
||||
echo " };";
|
||||
echo " const renderLagerMobileDetail = (sourceDrawer, trigger) => { const type = trigger.dataset.lagerDrawerType || 'Lager'; const name = trigger.dataset.lagerDrawerName || ''; const value = trigger.dataset.lagerDrawerValue || ''; sourceDrawer.querySelector('[data-lager-drawer-title]').textContent = type + ': ' + name; sourceDrawer.querySelector('[data-lager-drawer-name-value]').textContent = name; sourceDrawer.querySelector('[data-lager-drawer-value-label]').textContent = type === 'Produkt' ? 'Anzahl im Lager:' : 'Artikelnummer:'; sourceDrawer.querySelector('[data-lager-drawer-value-value]').textContent = value; sourceDrawer.querySelectorAll('[data-lager-drawer-topic]').forEach((card) => { card.hidden = type !== 'Produkt'; }); const newLotForm = sourceDrawer.querySelector('[data-lager-drawer-topic=\"new-lot-form\"]'); if (newLotForm) { newLotForm.hidden = true; } const newLotAction = sourceDrawer.querySelector('[data-lager-drawer-topic=\"new-lot-action\"]'); if (newLotAction) { newLotAction.hidden = false; } if (type === 'Produkt') { const warning = trigger.dataset.lagerDrawerForecastWarning === 'true' || trigger.dataset.lagerDrawerForecastMhdWarning === 'true'; const warningValue = sourceDrawer.querySelector('[data-lager-drawer-warning-value]'); if (warningValue) { warningValue.textContent = warning ? (trigger.dataset.lagerDrawerForecast || 'Keine Prognose') : 'Keine akute Warnung.'; } const warningTitle = sourceDrawer.querySelector('[data-lager-drawer-warning-title]'); if (warningTitle) { warningTitle.textContent = getLagerWarningTitle(trigger); } sourceDrawer.querySelector('[data-lager-drawer-lot-number]').textContent = trigger.dataset.lagerDrawerLotNumber || 'Nicht hinterlegt'; sourceDrawer.querySelector('[data-lager-drawer-current-qty]').textContent = trigger.dataset.lagerDrawerCurrentQty || '0'; sourceDrawer.querySelector('[data-lager-drawer-qty-in]').textContent = trigger.dataset.lagerDrawerQtyIn || '0'; sourceDrawer.querySelector('[data-lager-drawer-manufacturer-name]').textContent = trigger.dataset.lagerDrawerManufacturerName || 'Nicht hinterlegt'; sourceDrawer.querySelector('[data-lager-drawer-manufacturer-lot-number]').textContent = trigger.dataset.lagerDrawerManufacturerLotNumber || 'Nicht hinterlegt'; sourceDrawer.querySelector('[data-lager-drawer-mfg-date]').textContent = trigger.dataset.lagerDrawerMfgDate || 'Nicht hinterlegt'; sourceDrawer.querySelector('[data-lager-drawer-expiry-date]').textContent = trigger.dataset.lagerDrawerExpiryDate || 'Nicht hinterlegt'; sourceDrawer.querySelector('[data-lager-drawer-purchase-date]').textContent = trigger.dataset.lagerDrawerPurchaseDate || 'Nicht hinterlegt'; const productId = trigger.closest('[data-lager-row]')?.dataset.lagerId || ''; sourceDrawer.querySelectorAll('[data-lager-historical-product]').forEach((product) => { product.hidden = product.dataset.lagerHistoricalProduct !== productId; }); } const host = document.createElement('div'); host.className = 'sg-lager-mobile-detail-host'; host.dataset.lagerMobileDetailHost = 'true'; host.innerHTML = '<div class=\"sg-navigation-card-layout sg-mobile-detail-back-card\"><div class=\"sg-navigation-card-block\"><article class=\"sg-card\" data-component=\"card\" data-pattern=\"navigation-card\" aria-label=\"Navigation Detailansicht zurück oben\"><div class=\"sg-card-segment sg-card-segment--body\" data-component-part=\"card-body\" data-pattern-part=\"navigation-card-segment\"><div class=\"sg-navigation-card-center\"><button class=\"sg-interaction-element sg-hyperlink\" type=\"button\" data-lager-detail-back>zurück</button></div></div></article></div></div>'; const mobileDrawer = sourceDrawer; mobileDrawer.dataset.lagerMobileDrawer = 'true'; mobileDrawer.dataset.open = 'true'; mobileDrawer.setAttribute('aria-hidden', 'false'); host.appendChild(sourceDrawer); return host; };";
|
||||
echo " const restoreLagerMobileDetail = () => {";
|
||||
echo " if (lagerMobileDetailHost && lagerMobileDetailHost.parentNode) { lagerMobileDetailHost.parentNode.removeChild(lagerMobileDetailHost); }";
|
||||
echo " if (lagerMobileDetailHost && lagerMobileDetailHost.parentNode) { const mobileDrawer = lagerMobileDetailHost.querySelector('[data-lager-mobile-drawer]'); if (mobileDrawer) { contentRoot.appendChild(mobileDrawer); mobileDrawer.removeAttribute('data-lager-mobile-drawer'); mobileDrawer.dataset.open = 'false'; mobileDrawer.setAttribute('aria-hidden', 'true'); } lagerMobileDetailHost.parentNode.removeChild(lagerMobileDetailHost); }";
|
||||
echo " lagerMobileDetailHost = null; lagerMobileDetailOpen = false; document.body.classList.remove('sg-lager-mobile-detail-open');";
|
||||
echo " };";
|
||||
echo " const openLagerMobileDetail = (trigger) => {";
|
||||
echo " restoreLagerMobileDetail(); const host = document.createElement('div'); host.innerHTML = renderLagerMobileDetail(trigger); lagerMobileDetailHost = host.firstElementChild;";
|
||||
echo " restoreLagerMobileDetail(); const sourceDrawer = contentRoot.querySelector('[data-lager-drawer]'); lagerMobileDetailHost = sourceDrawer ? renderLagerMobileDetail(sourceDrawer, trigger) : null; const mobileDrawer = lagerMobileDetailHost?.querySelector('[data-lager-mobile-drawer]'); if (mobileDrawer) { const virtualLot = trigger.dataset.lagerDrawerVirtualLotNumber || ''; const virtualLotValue = mobileDrawer.querySelector('[data-lager-drawer-virtual-lot-value]'); if (virtualLotValue) { virtualLotValue.textContent = virtualLot ? 'Virtuelle Charge: ' + virtualLot : 'Keine virtuelle Fehlbestandscharge vorhanden.'; } }";
|
||||
echo " if (lagerMobileDetailHost) { document.body.appendChild(lagerMobileDetailHost); document.body.classList.add('sg-lager-mobile-detail-open'); }";
|
||||
echo " lagerMobileDetailOpen = true; window.history.pushState({ ...(window.history.state || {}), lagerView: 'detail' }, '', window.location.href);";
|
||||
echo " };";
|
||||
@@ -807,38 +849,56 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " if (window.history.state && window.history.state.lagerView === 'detail') { window.history.back(); return; }";
|
||||
echo " restoreLagerMobileDetail();";
|
||||
echo " };";
|
||||
echo " const getDrawer = () => 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 drawer = getDrawer(); const backdrop = contentRoot.querySelector('[data-lager-drawer-backdrop]');";
|
||||
echo " if (!drawer || !backdrop) { return; }";
|
||||
echo " drawer.dataset.open = String(open); drawer.setAttribute('aria-hidden', String(!open)); backdrop.dataset.open = String(open); backdrop.setAttribute('aria-hidden', String(!open)); document.body.classList.toggle('sg-card-list-page-drawer-open', open);";
|
||||
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 closeDrawer = () => { setDrawerState(false); contentRoot.querySelectorAll('[data-lager-drawer-open]').forEach((trigger) => trigger.setAttribute('aria-expanded', 'false')); };";
|
||||
echo " contentRoot.addEventListener('click', (event) => {";
|
||||
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 '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 " 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 + ' (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 " 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 (event.target.closest('[data-lager-drawer-close]') || event.target.closest('[data-lager-drawer-backdrop]')) { closeDrawer(); }";
|
||||
echo " });";
|
||||
echo " contentRoot.addEventListener('click', (event) => { const trigger = event.target.closest('[data-lager-drawer-open]'); if (!trigger || trigger.dataset.lagerDrawerType !== 'Produkt' || window.matchMedia('(max-width: 767px)').matches) { return; } const header = contentRoot.querySelector('[data-lager-drawer-warning-header]'); const title = contentRoot.querySelector('[data-lager-drawer-warning-title]'); if (!header || !title) { return; } const 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 " contentRoot.addEventListener('click', (event) => { const trigger = event.target.closest('[data-lager-drawer-open]'); if (!trigger || trigger.dataset.lagerDrawerType !== 'Produkt' || window.matchMedia('(max-width: 767px)').matches) { return; } 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'); } return result; };";
|
||||
echo " contentRoot.addEventListener('click', (event) => { const editButton = event.target.closest('[data-lager-edit-open]'); if (editButton) { const drawer = getDrawer(); const editForm = drawer?.querySelector('[data-lager-edit-form]'); if (editForm) { editForm.hidden = false; } return; } const closeButton = event.target.closest('[data-lager-edit-close]'); if (closeButton) { const drawer = getDrawer(); const editForm = drawer?.querySelector('[data-lager-edit-form]'); if (editForm) { editForm.hidden = true; } } });";
|
||||
echo " 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 " 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 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 editForm = drawer?.querySelector('[data-lager-edit-form]'); if (readView) { readView.hidden = editing; } if (editForm) { editForm.hidden = !editing; } };";
|
||||
echo " contentRoot.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 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 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_in ?? 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; } }); };";
|
||||
echo " contentRoot.addEventListener('click', (event) => { const trigger = event.target.closest('[data-lager-drawer-open]'); if (!trigger || trigger.dataset.lagerDrawerType !== 'Produkt' || window.matchMedia('(max-width: 767px)').matches) { return; } const 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; }); }); 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 " contentRoot.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 " contentRoot.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]'); 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]'); if (form) { form.hidden = true; } if (readView) { readView.hidden = false; } } });";
|
||||
echo " contentRoot.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 status = openLotForm.querySelector('[data-lager-open-edit-status]'); if (status) { status.textContent = 'Speichern läuft …'; } try { 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 || '', expiryDate: openLotForm.querySelector('[data-lager-open-expiry-date]')?.value || '', purchaseDate: openLotForm.querySelector('[data-lager-open-purchase-date]')?.value || '', receiptQty: openLotForm.querySelector('[data-lager-open-receipt-qty]')?.value || '' }); syncLagerOpenLotCard(result.lot, result.balance); openLotForm.querySelector('[data-lager-open-receipt-qty]').value = ''; openLotForm.setAttribute('hidden', 'hidden'); 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 " contentRoot.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 " contentRoot.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 || '' }); 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 " 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 " 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 " 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 " 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('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('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('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 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('keydown', (event) => { if (event.key === 'Escape') { closeDrawer(); closeLagerMobileDetail(); } });";
|
||||
echo " window.addEventListener('popstate', (event) => { if (lagerMobileDetailOpen && (!event.state || event.state.lagerView !== 'detail')) { restoreLagerMobileDetail(); } });";
|
||||
echo " const syncLagerDisplayedTotal = () => { const drawer = getDrawer(); const trigger = contentRoot.querySelector('[data-lager-drawer-open][aria-expanded=\"true\"]'); if (!drawer || !trigger || trigger.dataset.lagerDrawerType !== 'Produkt') { return; } const active = Number((drawer.querySelector('[data-lager-drawer-current-qty]')?.textContent || '0').replace(',', '.')) || 0; const productId = trigger.closest('[data-lager-row]')?.dataset.lagerId || ''; const openProduct = drawer.querySelector('[data-lager-open-product=\"' + productId + '\"]'); const open = Array.from(openProduct?.querySelectorAll('[data-lager-open-card-qty-net]') || []).reduce((sum, node) => sum + (Number((node.textContent || '0').replace(',', '.')) || 0), 0); const total = active + open; const formatted = Number.isInteger(total) ? String(total) : total.toFixed(2).replace(/\.?0+$/, ''); const value = drawer.querySelector('[data-lager-drawer-value-value]'); if (value && value.textContent !== formatted) { value.textContent = formatted; } trigger.dataset.lagerDrawerValue = formatted; };";
|
||||
echo " const lagerTotalObserver = new MutationObserver(syncLagerDisplayedTotal);";
|
||||
echo " lagerTotalObserver.observe(contentRoot, { childList: true, subtree: true, characterData: true });";
|
||||
echo "}";
|
||||
echo "function initBestellungenBindings() {";
|
||||
echo " const contentRoot = document.querySelector('[data-left-navigation-content-body]');";
|
||||
@@ -878,7 +938,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " const isMobileDetailMode = () => window.matchMedia('(max-width: 767px)').matches;";
|
||||
echo " const escapeHtml = (value) => String(value ?? '').replace(/[&<>\"']/g, (character) => ({ '&': '&', '<': '<', '>': '>', '\"': '"', \"'\": ''' }[character] || character));";
|
||||
echo " let bestellungenMobileDetailHost = null;";
|
||||
echo " let bestellungenDetailOpen = false;";
|
||||
echo " let bestellungenDetailOpen = false; let bestellungenOpeningMobileDetail = false;";
|
||||
echo " const syncSearchState = () => {";
|
||||
echo " const input = getSearchInput();";
|
||||
echo " const wrap = getSearchWrap();";
|
||||
@@ -887,47 +947,10 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " wrap.setAttribute('data-has-value', String(hasValue));";
|
||||
echo " wrap.dataset.componentState = hasValue ? 'active' : 'inactive-selectable';";
|
||||
echo " };";
|
||||
echo " const renderMobileDetail = (trigger) => {";
|
||||
echo " const orderNumber = trigger.dataset.orderNumber || '';";
|
||||
echo " const orderDate = trigger.dataset.orderDate || '';";
|
||||
echo " const total = trigger.dataset.orderTotal || '';";
|
||||
echo " const street = trigger.dataset.orderStreet || '';";
|
||||
echo " const houseNumber = trigger.dataset.orderHouseNumber || '';";
|
||||
echo " const zip = trigger.dataset.orderZip || '';";
|
||||
echo " const city = trigger.dataset.orderCity || '';";
|
||||
echo " const country = trigger.dataset.orderCountry || '';";
|
||||
echo " const formatDate = (value) => value ? new Date(value).toLocaleString('de-CH', { dateStyle: 'short', timeStyle: 'short' }) : 'Nicht vorhanden';";
|
||||
echo " const streetLine = [street, houseNumber].filter((part) => part && part.length > 0).join(' ').trim();";
|
||||
echo " const cityLine = [zip, city].filter((part) => part && part.length > 0).join(' ').trim();";
|
||||
echo " return [";
|
||||
echo " '<div class=\"sg-bestellungen-mobile-detail-host\" data-bestellungen-mobile-detail-host=\"true\">',";
|
||||
echo " '<div class=\"sg-navigation-card-layout sg-mobile-detail-back-card\">',";
|
||||
echo " '<div class=\"sg-navigation-card-block\">',";
|
||||
echo " '<article class=\"sg-card\" data-component=\"card\" data-pattern=\"navigation-card\" aria-label=\"Navigation Detailansicht zurück oben\">',";
|
||||
echo " '<div class=\"sg-card-segment sg-card-segment--body\" data-component-part=\"card-body\" data-pattern-part=\"navigation-card-segment\">',";
|
||||
echo " '<div class=\"sg-navigation-card-center\">',";
|
||||
echo " '<button class=\"sg-interaction-element sg-hyperlink\" type=\"button\" data-bestellungen-detail-back>zurück</button>',";
|
||||
echo " '</div>',";
|
||||
echo " '</div>',";
|
||||
echo " '</article>',";
|
||||
echo " '</div>',";
|
||||
echo " '</div>',";
|
||||
echo " '<div data-bestellungen-mobile-cards><article class=\"sg-card sg-card--content-card\" data-component=\"content-card\" aria-label=\"Bestell-Details\">',";
|
||||
echo " '<div class=\"sg-card-segment sg-card-segment--body sg-vsf-drawer-card__content\">',";
|
||||
echo " '<p class=\"sg-body\"><strong>Bestellnummer:</strong> <span>' + escapeHtml(orderNumber) + '</span></p>',";
|
||||
echo " '<p class=\"sg-body\"><strong>Bestelldatum:</strong> <span>' + escapeHtml(orderDate) + '</span></p>',";
|
||||
echo " '<p class=\"sg-body\"><strong>Lieferadresse:</strong></p>',";
|
||||
echo " '<p class=\"sg-body\">' + escapeHtml(streetLine) + '</p>',";
|
||||
echo " '<p class=\"sg-body\">' + escapeHtml(cityLine) + '</p>',";
|
||||
echo " '<p class=\"sg-body\">' + escapeHtml(country) + '</p>',";
|
||||
echo " '<p class=\"sg-body\"><strong>Gesamtsumme:</strong> <span>' + escapeHtml(total) + '</span></p>',";
|
||||
echo " '<p class=\"sg-body\">Detailinhalt wird noch definiert.</p>',";
|
||||
echo " '</div>',";
|
||||
echo " '</article></div>',";
|
||||
echo " '</div>'";
|
||||
echo " ].join('');";
|
||||
echo " };";
|
||||
echo " const getDrawer = () => contentRoot.querySelector('[data-bestellungen-drawer]');";
|
||||
echo " const renderMobileDetail = () => {";
|
||||
echo " return '<div class=\"sg-bestellungen-mobile-detail-host\" data-bestellungen-mobile-detail-host=\"true\"><div class=\"sg-navigation-card-layout sg-mobile-detail-back-card\"><div class=\"sg-navigation-card-block\"><article class=\"sg-card\" data-component=\"card\" data-pattern=\"navigation-card\" aria-label=\"Navigation Detailansicht zurück oben\"><div class=\"sg-card-segment sg-card-segment--body\" data-component-part=\"card-body\" data-pattern-part=\"navigation-card-segment\"><div class=\"sg-navigation-card-center\"><button class=\"sg-interaction-element sg-hyperlink\" type=\"button\" data-bestellungen-detail-back>zurück</button></div></div></article></div></div></div>';";
|
||||
echo " };";
|
||||
echo " const getDrawer = () => document.querySelector('[data-bestellungen-mobile-drawer]') || contentRoot.querySelector('[data-bestellungen-drawer]');";
|
||||
echo " const getDrawerBackdrop = () => contentRoot.querySelector('[data-bestellungen-drawer-backdrop]');";
|
||||
echo " const setDrawerState = (open) => {";
|
||||
echo " const drawer = getDrawer();";
|
||||
@@ -948,7 +971,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " const openDrawer = (trigger) => {";
|
||||
echo " const drawer = getDrawer();";
|
||||
echo " if (!drawer) { return; }";
|
||||
echo " if (isMobileDetailMode()) {";
|
||||
echo " if (isMobileDetailMode() && !bestellungenOpeningMobileDetail) {";
|
||||
echo " openMobileDetail(trigger);";
|
||||
echo " return;";
|
||||
echo " }";
|
||||
@@ -973,7 +996,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " });";
|
||||
echo " };";
|
||||
echo " const restoreBestellungenList = () => {";
|
||||
echo " if (bestellungenMobileDetailHost && bestellungenMobileDetailHost.parentNode) {";
|
||||
echo " if (bestellungenMobileDetailHost && bestellungenMobileDetailHost.parentNode) { const mobileDrawer = bestellungenMobileDetailHost.querySelector('[data-bestellungen-mobile-drawer]'); if (mobileDrawer) { contentRoot.appendChild(mobileDrawer); mobileDrawer.removeAttribute('data-bestellungen-mobile-drawer'); mobileDrawer.dataset.open = 'false'; mobileDrawer.setAttribute('aria-hidden', 'true'); } document.body.classList.remove('sg-card-list-page-drawer-open');";
|
||||
echo " bestellungenMobileDetailHost.parentNode.removeChild(bestellungenMobileDetailHost);";
|
||||
echo " }";
|
||||
echo " bestellungenMobileDetailHost = null;";
|
||||
@@ -983,14 +1006,13 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " };";
|
||||
echo " const openMobileDetail = (trigger) => {";
|
||||
echo " restoreBestellungenList();";
|
||||
echo " const sourceDrawer = contentRoot.querySelector('[data-bestellungen-drawer]');";
|
||||
echo " if (!sourceDrawer) { return; }";
|
||||
echo " const host = document.createElement('div');";
|
||||
echo " host.innerHTML = renderMobileDetail(trigger);";
|
||||
echo " host.innerHTML = renderMobileDetail();";
|
||||
echo " bestellungenMobileDetailHost = host.firstElementChild;";
|
||||
echo " if (bestellungenMobileDetailHost) {";
|
||||
echo " document.body.appendChild(bestellungenMobileDetailHost);";
|
||||
echo " document.body.classList.add('sg-bestellungen-mobile-detail-open');";
|
||||
echo " }";
|
||||
echo " fetch('/api/bestellungen/detail.php?id=' + encodeURIComponent(trigger.dataset.orderId || ''), { credentials: 'same-origin' }).then((response) => response.ok ? response.json() : Promise.reject(new Error('Bestelldetails konnten nicht geladen werden.'))).then((payload) => { if (!payload.ok || !bestellungenMobileDetailHost) { throw new Error('Bestelldetails konnten nicht geladen werden.'); } const order = payload.order; const cards = bestellungenMobileDetailHost.querySelector('[data-bestellungen-mobile-cards]'); const lines = (order.lines || []).map((line) => '<p class=\"sg-body\"><strong>' + escapeHtml(line.item_name) + '</strong> · ' + escapeHtml(line.item_code) + ' · Menge: ' + escapeHtml(String(line.qty)) + ' · Status: ' + escapeHtml(line.line_status) + '</p>').join(''); const allocations = (order.lines || []).flatMap((line) => (line.product_allocations || []).map((item) => '<p class=\"sg-body\"><strong>' + escapeHtml(item.product_name) + ':</strong> ' + escapeHtml(item.lot_number || 'Nicht zugeordnet') + ' · ' + escapeHtml(item.allocation_status || 'Nicht vorhanden') + '</p>')).join(''); if (cards) { cards.innerHTML = '<article class=\"sg-card sg-card--content-card\"><header class=\"sg-card-segment sg-card-segment--header sg-card-segment--darkgreen\"><div class=\"sg-strong sg-text-on-dark\">Bestellübersicht</div></header><div class=\"sg-card-segment sg-card-segment--body\"><p class=\"sg-body\"><strong>Bestellnummer:</strong> ' + escapeHtml(order.external_ref) + '</p><p class=\"sg-body\"><strong>Bestelldatum:</strong> ' + escapeHtml(formatDate(order.order_date)) + '</p><p class=\"sg-body\"><strong>Verkaufskanal:</strong> ' + escapeHtml(order.order_source) + '</p><p class=\"sg-body\"><strong>Status:</strong> ' + escapeHtml(order.order_status) + '</p></div></article><article class=\"sg-card sg-card--content-card\"><header class=\"sg-card-segment sg-card-segment--header sg-card-segment--darkgreen\"><div class=\"sg-strong sg-text-on-dark\">Bestellte Artikel</div></header><div class=\"sg-card-segment sg-card-segment--body\">' + (lines || '<p class=\"sg-body\">Keine Artikel vorhanden.</p>') + '</div></article><article class=\"sg-card sg-card--content-card\"><header class=\"sg-card-segment sg-card-segment--header sg-card-segment--darkgreen\"><div class=\"sg-strong sg-text-on-dark\">Chargenzuordnung</div></header><div class=\"sg-card-segment sg-card-segment--body\">' + (allocations || '<p class=\"sg-body\">Keine Chargenzuordnung vorhanden.</p>') + '</div></article>'; } }).catch((error) => { const cards = bestellungenMobileDetailHost?.querySelector('[data-bestellungen-mobile-cards]'); if (cards) { cards.innerHTML = '<p class=\"sg-body\">' + escapeHtml(error.message) + '</p>'; } });";
|
||||
echo " if (bestellungenMobileDetailHost) { const mobileDrawer = sourceDrawer; mobileDrawer.dataset.bestellungenMobileDrawer = 'true'; mobileDrawer.dataset.open = 'true'; mobileDrawer.setAttribute('aria-hidden', 'false'); bestellungenMobileDetailHost.appendChild(mobileDrawer); document.body.appendChild(bestellungenMobileDetailHost); document.body.classList.add('sg-bestellungen-mobile-detail-open'); }";
|
||||
echo " bestellungenOpeningMobileDetail = true; openDrawer(trigger); bestellungenOpeningMobileDetail = false;";
|
||||
echo " bestellungenDetailOpen = true;";
|
||||
echo " window.history.pushState({ bestellungenView: 'detail', orderId: trigger.dataset.orderId || '' }, '', window.location.href);";
|
||||
echo " };";
|
||||
@@ -1144,7 +1166,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " closeMobileDetail();";
|
||||
echo " }";
|
||||
echo " });";
|
||||
echo " contentRoot.addEventListener('click', (event) => {";
|
||||
echo " document.addEventListener('click', (event) => {";
|
||||
echo " const openTrigger = event.target.closest('[data-order-drawer-open]');";
|
||||
echo " if (openTrigger) {";
|
||||
echo " event.preventDefault();";
|
||||
@@ -1218,7 +1240,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " const houseNumberInput = form.querySelector('[data-otc-order-house-number]');";
|
||||
echo " const zipInput = form.querySelector('[data-otc-order-zip]');";
|
||||
echo " const cityInput = form.querySelector('[data-otc-order-city]');";
|
||||
echo " const submitBtn = form.querySelector('[data-otc-order-submit]');";
|
||||
echo " const submitBtn = overlay.querySelector('[data-otc-order-submit]');";
|
||||
echo " const errorEl = form.querySelector('[data-otc-order-error]');";
|
||||
echo " const successEl = form.querySelector('[data-otc-order-success]');";
|
||||
echo " const successDefaultText = successEl ? successEl.textContent : '';";
|
||||
@@ -1239,7 +1261,6 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " const clearSuccess = () => {";
|
||||
echo " if (successEl) { successEl.textContent = successDefaultText; successEl.classList.add('hidden'); }";
|
||||
echo " };";
|
||||
echo " let backdropPointerDown = false;";
|
||||
echo " const getTodayValue = () => {";
|
||||
echo " const now = new Date();";
|
||||
echo " const year = now.getFullYear();";
|
||||
@@ -1330,9 +1351,6 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " confirmationCard.hidden = false;";
|
||||
echo " requestAnimationFrame(() => confirmationSubmit.focus());";
|
||||
echo " };";
|
||||
echo " overlay.addEventListener('pointerdown', (event) => {";
|
||||
echo " backdropPointerDown = event.target === overlay;";
|
||||
echo " });";
|
||||
echo " document.addEventListener('click', (event) => {";
|
||||
echo " const openTrigger = event.target.closest('[data-otc-order-open]');";
|
||||
echo " if (openTrigger) {";
|
||||
@@ -1346,17 +1364,6 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " toggleOverlay(false);";
|
||||
echo " }";
|
||||
echo " });";
|
||||
echo " overlay.addEventListener('click', (event) => {";
|
||||
echo " if (event.target === overlay && backdropPointerDown) {";
|
||||
echo " toggleOverlay(false);";
|
||||
echo " }";
|
||||
echo " backdropPointerDown = false;";
|
||||
echo " });";
|
||||
echo " document.addEventListener('keydown', (event) => {";
|
||||
echo " if (event.key === 'Escape' && overlay.dataset.open === 'true') {";
|
||||
echo " toggleOverlay(false);";
|
||||
echo " }";
|
||||
echo " });";
|
||||
echo " [orderDateInput, firstNameInput, lastNameInput, streetInput, houseNumberInput, zipInput, cityInput].forEach((input) => {";
|
||||
echo " if (!input) { return; }";
|
||||
echo " input.addEventListener('input', updateFormState);";
|
||||
@@ -1377,7 +1384,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
|
||||
echo " const row = document.createElement('div');";
|
||||
echo " row.className = 'sg-form-sections-card__field-group';";
|
||||
echo " row.dataset.otcPaymentRow = '';";
|
||||
echo " row.innerHTML = '<label class=\"sg-label\" for=\"otc-payment-method-' + number + '\">Bezahlart</label><select class=\"sg-interaction-element sg-input-single-line\" id=\"otc-payment-method-' + number + '\" data-otc-payment-method>' + paymentOptions + '</select><label class=\"sg-label\" for=\"otc-payment-amount-' + number + '\">Betrag (CHF)</label><input class=\"sg-interaction-element sg-input-single-line\" type=\"number\" id=\"otc-payment-amount-' + number + '\" min=\"0.01\" step=\"0.01\" value=\"0.00\" data-otc-payment-amount>';";
|
||||
echo " row.innerHTML = '<label class=\"sg-labeled-input-row\"><span class=\"sg-label\">Bezahlart</span><select class=\"sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable sg-pulldown\" id=\"otc-payment-method-' + number + '\" data-otc-payment-method>' + paymentOptions + '</select></label><label class=\"sg-labeled-input-row\"><span class=\"sg-label\">Betrag (CHF)</span><input class=\"sg-interaction-element sg-input-single-line sg-input-single-line--inactive-selectable sg-form-inactive-selectable\" type=\"number\" id=\"otc-payment-amount-' + number + '\" min=\"0.01\" step=\"0.01\" value=\"0.00\" data-otc-payment-amount></label>' ;";
|
||||
echo " row.querySelector('[data-otc-payment-method]').value = nextPaymentMethod;";
|
||||
echo " paymentRows.appendChild(row);";
|
||||
echo " bindPaymentRow(row);";
|
||||
|
||||
@@ -26,9 +26,10 @@ try {
|
||||
'erp.import-integration.shopify_webhook_worker' => static fn (): array => shopify_webhook_worker_run($pdo, $env, 100),
|
||||
'erp.import-integration.shopify_delta_order_import' => static function () use ($pdo, $env, $reconciliationStartedAt): array {
|
||||
$cutoff = shopify_delta_reconciliation_cutoff($pdo);
|
||||
$recoveryOrderGids = shopify_delta_load_pending_order_gids($pdo);
|
||||
$runId = shopify_delta_start_run($pdo, $cutoff, false);
|
||||
try {
|
||||
$result = run_shopify_delta($env, $pdo, $cutoff, false);
|
||||
$result = run_shopify_delta($env, $pdo, $cutoff, false, $recoveryOrderGids);
|
||||
$result['technical_run_id'] = $runId;
|
||||
if (($result['status'] ?? '') === 'done') {
|
||||
shopify_delta_store_watermark($pdo, $reconciliationStartedAt->format(DATE_ATOM));
|
||||
|
||||
@@ -175,6 +175,8 @@
|
||||
--layout-object-card-mobile-width: var(--dimension-object-card-mobile-width);
|
||||
--layout-object-card-mobile-height: var(--dimension-object-card-mobile-height);
|
||||
--layout-object-card-desktop-breakpoint: var(--dimension-object-card-desktop-breakpoint);
|
||||
--layout-scrollable-card-width: var(--dimension-scrollable-card-width);
|
||||
--layout-scrollable-card-height: var(--dimension-scrollable-card-height);
|
||||
--layout-content-card-margin-top-desktop: var(--dimension-content-card-margin-top-desktop);
|
||||
--layout-content-card-margin-top-mobile: var(--dimension-content-card-margin-top-mobile);
|
||||
--layout-notification-card-width: var(--dimension-notification-card-width);
|
||||
@@ -287,6 +289,8 @@
|
||||
--dimension-object-card-mobile-width: 100%;
|
||||
--dimension-object-card-mobile-height: auto;
|
||||
--dimension-object-card-desktop-breakpoint: 768px;
|
||||
--dimension-scrollable-card-width: 600px;
|
||||
--dimension-scrollable-card-height: 600px;
|
||||
--dimension-content-card-margin-top-desktop: 100px;
|
||||
--dimension-content-card-margin-top-mobile: 1rem;
|
||||
--dimension-object-group-card-min-width: 450px;
|
||||
@@ -656,6 +660,13 @@ section + section {
|
||||
color: var(--text-control-default);
|
||||
}
|
||||
|
||||
.sg-button--warning {
|
||||
--surface-button-warning: var(--color-signal-red);
|
||||
--text-button-warning: var(--color-font-light);
|
||||
background: var(--surface-button-warning);
|
||||
color: var(--text-button-warning);
|
||||
}
|
||||
|
||||
.sg-button--process {
|
||||
background: var(--surface-button-process);
|
||||
color: var(--text-button-process);
|
||||
@@ -2096,6 +2107,147 @@ section + section {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Component: File Input */
|
||||
.sg-file-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sg-file-input__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-large);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sg-file-input__control {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.sg-file-input:focus-within .sg-file-input__trigger {
|
||||
outline: 2px solid var(--text-hyperlink);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.sg-file-input__trigger {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
@media (max-width: 48rem) {
|
||||
.sg-file-input__row {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.sg-file-input--disabled {
|
||||
opacity: var(--disabled-opacity);
|
||||
}
|
||||
|
||||
.sg-file-input--disabled .sg-file-input__trigger {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ========================================================= */
|
||||
/* Patterns: Datei-Upload */
|
||||
/* ========================================================= */
|
||||
|
||||
.sg-file-upload {
|
||||
--surface-file-upload-dropzone: var(--surface-form-preview);
|
||||
--surface-file-upload-file-item: var(--surface-control-default);
|
||||
--text-file-upload-meta: var(--text-control-disabled);
|
||||
--text-file-upload-error: var(--color-signal-red);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-large);
|
||||
width: 100%;
|
||||
max-width: var(--layout-input-field-max-width);
|
||||
}
|
||||
|
||||
.sg-file-upload__status,
|
||||
.sg-file-upload__file-meta {
|
||||
margin: var(--spacing-small) 0 0;
|
||||
}
|
||||
|
||||
.sg-file-upload__dropzone {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-large);
|
||||
width: 100%;
|
||||
padding: var(--spacing-large);
|
||||
background: var(--surface-file-upload-dropzone);
|
||||
border: 1px dashed var(--surface-button-active);
|
||||
border-radius: var(--radius-interaction);
|
||||
}
|
||||
|
||||
.sg-file-upload__dropzone[data-drag-over="true"] {
|
||||
outline: 2px solid var(--text-hyperlink);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.sg-file-upload__files {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-small);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.sg-file-upload__file {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-large);
|
||||
padding: var(--spacing-small) var(--spacing-large);
|
||||
color: var(--text-control-default);
|
||||
background: var(--surface-file-upload-file-item);
|
||||
border-radius: var(--radius-interaction);
|
||||
}
|
||||
|
||||
.sg-file-upload__file-name {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.sg-file-upload__file-meta {
|
||||
color: var(--text-file-upload-meta);
|
||||
}
|
||||
|
||||
.sg-file-upload__remove {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.sg-file-upload__submit {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.sg-file-upload__error {
|
||||
color: var(--text-file-upload-error);
|
||||
}
|
||||
|
||||
@media (max-width: 48rem) {
|
||||
.sg-file-upload__dropzone {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sg-file-upload__file {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-small);
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================================= */
|
||||
/* Patterns: Portal Header */
|
||||
/* ========================================================= */
|
||||
@@ -5031,3 +5183,38 @@ main {
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================================= */
|
||||
/* Patterns: Scrollable Card */
|
||||
/* ========================================================= */
|
||||
|
||||
.sg-scrollable-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: var(--layout-scrollable-card-width);
|
||||
max-width: 100%;
|
||||
height: var(--layout-scrollable-card-height);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sg-scrollable-card__header,
|
||||
.sg-scrollable-card__actions-segment {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.sg-scrollable-card__content {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sg-scrollable-card__actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-small);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sg-scrollable-card__actions > .sg-button {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"styleguideVersion": "2026.05.18.1",
|
||||
"styleguideCommit": "3f4ec38",
|
||||
"syncedAtUtc": "2026-09-03T15:57:49Z",
|
||||
"styleguideCommit": "71734d6",
|
||||
"syncedAtUtc": "2026-09-08T09:49:27Z",
|
||||
"sourceRepo": "/Users/mathias/Documents/Dokumente Chouchou/Codebases/Styleguide",
|
||||
"mirroredDocsPath": "docs/styleguide"
|
||||
}
|
||||
@@ -175,6 +175,8 @@
|
||||
--layout-object-card-mobile-width: var(--dimension-object-card-mobile-width);
|
||||
--layout-object-card-mobile-height: var(--dimension-object-card-mobile-height);
|
||||
--layout-object-card-desktop-breakpoint: var(--dimension-object-card-desktop-breakpoint);
|
||||
--layout-scrollable-card-width: var(--dimension-scrollable-card-width);
|
||||
--layout-scrollable-card-height: var(--dimension-scrollable-card-height);
|
||||
--layout-content-card-margin-top-desktop: var(--dimension-content-card-margin-top-desktop);
|
||||
--layout-content-card-margin-top-mobile: var(--dimension-content-card-margin-top-mobile);
|
||||
--layout-notification-card-width: var(--dimension-notification-card-width);
|
||||
@@ -287,6 +289,8 @@
|
||||
--dimension-object-card-mobile-width: 100%;
|
||||
--dimension-object-card-mobile-height: auto;
|
||||
--dimension-object-card-desktop-breakpoint: 768px;
|
||||
--dimension-scrollable-card-width: 600px;
|
||||
--dimension-scrollable-card-height: 600px;
|
||||
--dimension-content-card-margin-top-desktop: 100px;
|
||||
--dimension-content-card-margin-top-mobile: 1rem;
|
||||
--dimension-object-group-card-min-width: 450px;
|
||||
@@ -656,6 +660,13 @@ section + section {
|
||||
color: var(--text-control-default);
|
||||
}
|
||||
|
||||
.sg-button--warning {
|
||||
--surface-button-warning: var(--color-signal-red);
|
||||
--text-button-warning: var(--color-font-light);
|
||||
background: var(--surface-button-warning);
|
||||
color: var(--text-button-warning);
|
||||
}
|
||||
|
||||
.sg-button--process {
|
||||
background: var(--surface-button-process);
|
||||
color: var(--text-button-process);
|
||||
@@ -2096,6 +2107,147 @@ section + section {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Component: File Input */
|
||||
.sg-file-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sg-file-input__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-large);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sg-file-input__control {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.sg-file-input:focus-within .sg-file-input__trigger {
|
||||
outline: 2px solid var(--text-hyperlink);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.sg-file-input__trigger {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
@media (max-width: 48rem) {
|
||||
.sg-file-input__row {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.sg-file-input--disabled {
|
||||
opacity: var(--disabled-opacity);
|
||||
}
|
||||
|
||||
.sg-file-input--disabled .sg-file-input__trigger {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ========================================================= */
|
||||
/* Patterns: Datei-Upload */
|
||||
/* ========================================================= */
|
||||
|
||||
.sg-file-upload {
|
||||
--surface-file-upload-dropzone: var(--surface-form-preview);
|
||||
--surface-file-upload-file-item: var(--surface-control-default);
|
||||
--text-file-upload-meta: var(--text-control-disabled);
|
||||
--text-file-upload-error: var(--color-signal-red);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-large);
|
||||
width: 100%;
|
||||
max-width: var(--layout-input-field-max-width);
|
||||
}
|
||||
|
||||
.sg-file-upload__status,
|
||||
.sg-file-upload__file-meta {
|
||||
margin: var(--spacing-small) 0 0;
|
||||
}
|
||||
|
||||
.sg-file-upload__dropzone {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-large);
|
||||
width: 100%;
|
||||
padding: var(--spacing-large);
|
||||
background: var(--surface-file-upload-dropzone);
|
||||
border: 1px dashed var(--surface-button-active);
|
||||
border-radius: var(--radius-interaction);
|
||||
}
|
||||
|
||||
.sg-file-upload__dropzone[data-drag-over="true"] {
|
||||
outline: 2px solid var(--text-hyperlink);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.sg-file-upload__files {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-small);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.sg-file-upload__file {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-large);
|
||||
padding: var(--spacing-small) var(--spacing-large);
|
||||
color: var(--text-control-default);
|
||||
background: var(--surface-file-upload-file-item);
|
||||
border-radius: var(--radius-interaction);
|
||||
}
|
||||
|
||||
.sg-file-upload__file-name {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.sg-file-upload__file-meta {
|
||||
color: var(--text-file-upload-meta);
|
||||
}
|
||||
|
||||
.sg-file-upload__remove {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.sg-file-upload__submit {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.sg-file-upload__error {
|
||||
color: var(--text-file-upload-error);
|
||||
}
|
||||
|
||||
@media (max-width: 48rem) {
|
||||
.sg-file-upload__dropzone {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sg-file-upload__file {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-small);
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================================= */
|
||||
/* Patterns: Portal Header */
|
||||
/* ========================================================= */
|
||||
@@ -5031,3 +5183,38 @@ main {
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================================= */
|
||||
/* Patterns: Scrollable Card */
|
||||
/* ========================================================= */
|
||||
|
||||
.sg-scrollable-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: var(--layout-scrollable-card-width);
|
||||
max-width: 100%;
|
||||
height: var(--layout-scrollable-card-height);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sg-scrollable-card__header,
|
||||
.sg-scrollable-card__actions-segment {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.sg-scrollable-card__content {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sg-scrollable-card__actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-small);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sg-scrollable-card__actions > .sg-button {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@@ -96,6 +96,26 @@ body.sg-lager-mobile-detail-open > :not([data-lager-mobile-detail-host]) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sg-lager-mobile-detail-host .sg-card-list-page-drawer {
|
||||
position: static;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.sg-bestellungen-mobile-detail-host .sg-card-list-page-drawer {
|
||||
position: static;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.sg-mobile-detail-back-card,
|
||||
.sg-mobile-detail-back-card * {
|
||||
cursor: pointer;
|
||||
@@ -117,9 +137,8 @@ body.sg-lager-mobile-detail-open > :not([data-lager-mobile-detail-host]) {
|
||||
}
|
||||
|
||||
.sg-otc-order-overlay__panel {
|
||||
width: min(960px, 100%);
|
||||
max-height: calc(100vh - (2 * var(--spacing-large)));
|
||||
overflow: auto;
|
||||
--layout-scrollable-card-width: min(960px, 100%);
|
||||
--layout-scrollable-card-height: min(600px, calc(100vh - (2 * var(--spacing-large))));
|
||||
}
|
||||
|
||||
.sg-otc-order-overlay__title {
|
||||
@@ -138,6 +157,19 @@ body.sg-lager-mobile-detail-open > :not([data-lager-mobile-detail-host]) {
|
||||
color: var(--color-signal-red);
|
||||
}
|
||||
|
||||
.sg-otc-order-overlay [data-otc-order-product] {
|
||||
flex: 0 0 6rem;
|
||||
width: 6rem;
|
||||
max-width: 6rem;
|
||||
margin-left: auto;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.sg-otc-order-overlay .sg-otc-order-product-row .sg-label {
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.sg-otc-order-overlay .sg-form-sections-card__field-group {
|
||||
gap: var(--spacing-small);
|
||||
}
|
||||
@@ -154,6 +186,14 @@ body.sg-lager-mobile-detail-open > :not([data-lager-mobile-detail-host]) {
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.sg-otc-order-overlay [data-otc-order-product] {
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin-left: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.sg-large-table--bestellungen .sg-search-field-row {
|
||||
width: min(100%, calc(var(--layout-search-field-width) * 0.75));
|
||||
}
|
||||
@@ -207,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 {
|
||||
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 {
|
||||
|
||||
Reference in new issue
Block a user