Compare commits

..
Author SHA1 Message Date
gitea_admin c11cb39dc4 Sync styleguide 2026.05.18.1 2026-09-06 15:32:07 +02:00
25 changed files with 196 additions and 1265 deletions

No files matched your search

-3
View File
@@ -15,9 +15,6 @@
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`
## Beispielausfuehrung
@@ -1,5 +0,0 @@
BEGIN;
DROP INDEX IF EXISTS uq_stock_lot_one_open_per_product;
COMMIT;
@@ -1,22 +0,0 @@
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;
@@ -1,18 +0,0 @@
BEGIN;
CREATE TABLE stock_lot_document (
id BIGSERIAL PRIMARY KEY,
stock_lot_id BIGINT NOT NULL REFERENCES stock_lot(id) ON DELETE CASCADE,
original_filename TEXT NOT NULL,
stored_filename TEXT NOT NULL,
mime_type TEXT NOT NULL,
size_bytes BIGINT NOT NULL,
comment TEXT,
uploaded_by TEXT NOT NULL,
uploaded_at TIMESTAMP NOT NULL DEFAULT NOW(),
CONSTRAINT chk_stock_lot_document_size CHECK (size_bytes > 0 AND size_bytes <= 52428800)
);
CREATE INDEX idx_stock_lot_document_lot ON stock_lot_document(stock_lot_id, id);
COMMIT;
@@ -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 und offene Webhook-Order-GIDs zusätzlich direkt zur Recovery aufnehmen.
1. `cutoff_timestamp` und `dry_run` validieren; beim Cron den kleineren Wert aus Wasserstand und ältestem offenen Webhook-Eingang verwenden.
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 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.
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.
## 6. Output
@@ -13,17 +13,17 @@ 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, eingetragene Mengen aller offenen Folgechargen, physischer Chargenbestand und gültige Verkaufsallokationen aus dem eigenen Datenbestand
- aktive Produkte, aktuelle Chargen, physischer Chargenbestand 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, ihre aktuelle Charge und die Nettobestände aller bereits angelegten offenen Folgechargen aus `product`, `stock_lot` und `v_stock_lot_balance` laden. Je offene Folgecharge wird nur eine positive Menge berücksichtigt.
2. Aktive Produkte und ihre aktuelle Charge aus `product`, `stock_lot` und `v_stock_lot_balance` laden.
3. Verkaufsallokationen der letzten 365 Tage aus `sales_order_line_lot_allocation`, `sales_order_line` und `sales_order` laden.
4. `allocated`- und `reserved`-Mengen nach Bestell- beziehungsweise OTC-Verkaufsdatum je Produkt und Kalendertag aggregieren.
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 ihrem physischen Bestand zuzüglich der eingetragenen Mengen aller offenen Folgechargen und der gewichteten Nachfrage 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.
9. Im Cron Fehler einzelner Produkte sammeln und mit den übrigen Produkten fortfahren.
@@ -37,7 +37,7 @@ Ein normalisiertes JSON-Ergebnis mit Gesamtzahl, erfolgreich verarbeiteten Produ
## 7. Erfolgskriterien
- Jede aktuelle Charge erhält täglich einen aktuellen Prognosesnapshot, der eingetragene Mengen aller offenen Folgechargen berücksichtigt.
- Jede aktuelle Charge erhält täglich einen aktuellen Prognosesnapshot.
- 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,10 +26,6 @@
<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>
@@ -847,28 +843,7 @@
</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';
-1
View File
@@ -51,7 +51,6 @@
<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>
-71
View File
@@ -1,71 +0,0 @@
<!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>
@@ -148,14 +148,6 @@
<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>
-1
View File
@@ -2,7 +2,6 @@
@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";
@@ -139,13 +139,6 @@
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);
@@ -1585,52 +1578,3 @@
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;
}
@@ -1,91 +0,0 @@
/* ========================================================= */
/* 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,32 +62,6 @@ 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(
@@ -146,7 +120,7 @@ GQL;
return $orders;
}
function run_shopify_delta(array $env, PDO $pdo, string $cutoff, bool $dryRun, array $recoveryOrderGids = []): array
function run_shopify_delta(array $env, PDO $pdo, string $cutoff, bool $dryRun): array
{
$cutoffDate = new DateTimeImmutable($cutoff);
$searchQuery = 'created_at:>' . $cutoffDate->format('Y-m-d');
@@ -161,31 +135,14 @@ function run_shopify_delta(array $env, PDO $pdo, string $cutoff, bool $dryRun, a
'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 (!isset($recoverySet[$orderGid]) && $createdAt <= $cutoffDate) {
if ($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;
}
@@ -209,14 +166,8 @@ function run_shopify_delta(array $env, PDO $pdo, string $cutoff, bool $dryRun, a
}
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,21 +132,19 @@ function shopify_webhook_worker_run(PDO $pdo, array $env, int $limit): array
}
}
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);
$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);
}
+6 -52
View File
@@ -20,30 +20,6 @@ try {
json_response(405, ['ok' => false, 'error' => 'Method Not Allowed']);
}
$contentType = (string) ($_SERVER['CONTENT_TYPE'] ?? '');
if (str_starts_with($contentType, 'multipart/form-data')) {
if (!auth_validate_csrf_token($_POST['csrfToken'] ?? null)) {
json_response(403, ['ok' => false, 'error' => 'Invalid security token']);
}
if (($_POST['action'] ?? '') !== 'upload_document') {
json_response(422, ['ok' => false, 'error' => 'Unsupported action']);
}
$file = $_FILES['document'] ?? null;
if (!is_array($file)) {
throw new InvalidArgumentException('Dokumentdatei fehlt');
}
$document = upload_stock_lot_document(
$pdo,
(int) ($_POST['lotId'] ?? 0),
$file,
(string) ($currentUser['username'] ?? $currentUser['email'] ?? 'unknown')
);
json_response(201, ['ok' => true, 'document' => [
'id' => (int) $document['id'],
'original_filename' => (string) $document['original_filename'],
]]);
}
$jsonInput = file_get_contents('php://input');
if ($jsonInput === false || trim($jsonInput) === '') {
json_response(400, ['ok' => false, 'error' => 'Empty payload']);
@@ -86,33 +62,16 @@ try {
$adjustmentQty,
$outQty
);
$productId = (int) $updatedLot['product_id'];
refresh_lager_stock_lot_forecast_for_product($pdo, $productId);
$forecast = get_lager_product_forecast($pdo, $productId);
json_response(200, ['ok' => true, 'productId' => $productId, 'lot' => $updatedLot, 'balance' => get_stock_lot_balance($pdo, (int) ($data['lotId'] ?? 0)), 'forecast' => $forecast]);
}
if ($action === 'delete_open_lot') {
$productId = delete_open_stock_lot(
$pdo,
(int) ($data['lotId'] ?? 0),
(string) ($currentUser['username'] ?? $currentUser['email'] ?? 'unknown')
);
json_response(200, ['ok' => true, 'productId' => $productId, 'availableQty' => get_lager_product_available_qty($pdo, $productId)]);
}
if ($action === 'delete_document') {
delete_stock_lot_document($pdo, (int) ($data['documentId'] ?? 0));
json_response(200, ['ok' => true]);
json_response(200, ['ok' => true, 'lot' => $updatedLot, 'balance' => get_stock_lot_balance($pdo, (int) ($data['lotId'] ?? 0))]);
}
if ($action === 'prepare_lot') {
$productId = (int) ($data['productId'] ?? 0);
$lotNumber = trim((string) ($data['lotNumber'] ?? ''));
$lotId = (int) ($data['lotId'] ?? 0);
if ($lotNumber === '' && $lotId <= 0) {
if ($lotNumber === '') {
throw new InvalidArgumentException('Naurua-Chargennummer fehlt');
}
$lotId = (int) ($data['lotId'] ?? 0);
if ($lotId > 0) {
$lotStmt = $pdo->prepare(
"SELECT id
@@ -131,7 +90,6 @@ try {
$manufacturerPartyId = $data['manufacturerPartyId'] ?? null;
$manufacturerPartyId = $manufacturerPartyId === null || $manufacturerPartyId === '' ? null : (int) $manufacturerPartyId;
$receiptQty = isset($data['receiptQty']) && $data['receiptQty'] !== '' ? (float) $data['receiptQty'] : 0.0;
$targetQty = isset($data['targetQty']) && $data['targetQty'] !== '' ? (float) $data['targetQty'] : null;
$preparedLot = update_stock_lot_traceability(
$pdo,
$lotId,
@@ -144,13 +102,9 @@ try {
$receiptQty,
0.0,
0.0,
$lotNumber,
$targetQty,
$lotId > 0 && $lotNumber === ''
$lotNumber
);
refresh_lager_stock_lot_forecast_for_product($pdo, $productId);
$forecast = get_lager_product_forecast($pdo, $productId);
json_response(200, ['ok' => true, 'productId' => $productId, 'lot' => $preparedLot, 'balance' => get_stock_lot_balance($pdo, $lotId), 'availableQty' => get_lager_product_available_qty($pdo, $productId), 'forecast' => $forecast]);
json_response(200, ['ok' => true, 'lot' => $preparedLot, 'balance' => get_stock_lot_balance($pdo, $lotId)]);
}
json_response(422, ['ok' => false, 'error' => 'Unsupported action']);
@@ -159,5 +113,5 @@ try {
} catch (InvalidArgumentException $error) {
json_response(422, ['ok' => false, 'error' => $error->getMessage()]);
} catch (Throwable $error) {
json_response(500, ['ok' => false, 'error' => 'Interner Serverfehler']);
json_response(500, ['ok' => false, 'error' => 'Internal server error: ' . $error->getMessage()]);
}
+2 -12
View File
@@ -35,12 +35,7 @@ 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) + COALESCE((
SELECT SUM(GREATEST(COALESCE(open_v.qty_net, 0), 0))
FROM stock_lot open_lot
LEFT JOIN v_stock_lot_balance open_v ON open_v.stock_lot_id = open_lot.id
WHERE open_lot.product_id = p.id AND open_lot.status = 'open'
), 0) AS physical_qty
COALESCE(v.qty_net, 0) AS 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
@@ -54,12 +49,7 @@ 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) + COALESCE((
SELECT SUM(GREATEST(COALESCE(open_v.qty_net, 0), 0))
FROM stock_lot open_lot
LEFT JOIN v_stock_lot_balance open_v ON open_v.stock_lot_id = open_lot.id
WHERE open_lot.product_id = p.id AND open_lot.status = 'open'
), 0) AS physical_qty
COALESCE(v.qty_net, 0) AS 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
+11 -285
View File
@@ -75,14 +75,7 @@ function get_lager_product_overview(PDO $pdo): array
p.name,
p.sku,
p.uom,
COALESCE(SUM(v.qty_net), 0) + COALESCE((
SELECT SUM(open_v.qty_net)
FROM stock_lot open_sl
INNER JOIN v_stock_lot_balance open_v ON open_v.stock_lot_id = open_sl.id
WHERE open_sl.product_id = p.id
AND open_sl.status = 'open'
), 0) AS available_qty,
COALESCE(v.qty_net, 0) AS current_qty,
COALESCE(SUM(v.qty_net), 0) AS available_qty,
COALESCE(v.qty_in, 0) AS qty_in,
sl.lot_number,
sl.id AS lot_id,
@@ -120,7 +113,7 @@ function get_lager_product_overview(PDO $pdo): array
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_net, v.qty_in
sl.sellout_date, sl.sellout_forecast_status, v.qty_in
ORDER BY p.name ASC, p.id ASC"
);
@@ -131,7 +124,6 @@ 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'],
@@ -224,7 +216,7 @@ function get_lager_product_overview(PDO $pdo): array
'mfg_date' => $openLot['mfg_date'],
'expiry_date' => $openLot['expiry_date'],
'purchase_date' => $openLot['purchase_date'],
'qty_in' => (float) $openLot['qty_net'],
'qty_in' => (float) $openLot['qty_in'],
'qty_net' => (float) $openLot['qty_net'],
];
}
@@ -235,183 +227,9 @@ function get_lager_product_overview(PDO $pdo): array
}
unset($product);
$lotIds = [];
foreach ($products as $product) {
if (!empty($product['lot_id'])) {
$lotIds[] = (int) $product['lot_id'];
}
foreach ($product['historical_lots'] as $lot) {
$lotIds[] = (int) ($lot['lot_id'] ?? 0);
}
foreach ($product['open_lots'] as $lot) {
$lotIds[] = (int) ($lot['lot_id'] ?? 0);
}
}
$documentsByLot = get_stock_lot_documents_by_lot_ids($pdo, $lotIds);
foreach ($products as &$product) {
$product['documents'] = $documentsByLot[(int) ($product['lot_id'] ?? 0)] ?? [];
foreach ($product['historical_lots'] as &$lot) {
$lot['documents'] = $documentsByLot[(int) ($lot['lot_id'] ?? 0)] ?? [];
}
unset($lot);
foreach ($product['open_lots'] as &$lot) {
$lot['documents'] = $documentsByLot[(int) ($lot['lot_id'] ?? 0)] ?? [];
}
unset($lot);
}
unset($product);
return $products;
}
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_stock_lot_documents_by_lot_ids(PDO $pdo, array $lotIds): array
{
$lotIds = array_values(array_unique(array_filter(array_map('intval', $lotIds), static fn (int $id): bool => $id > 0)));
if ($lotIds === []) {
return [];
}
$placeholders = implode(',', array_fill(0, count($lotIds), '?'));
$stmt = $pdo->prepare(
"SELECT id, stock_lot_id, original_filename, mime_type, size_bytes, uploaded_at
FROM stock_lot_document
WHERE stock_lot_id IN ({$placeholders})
ORDER BY stock_lot_id, id"
);
$stmt->execute($lotIds);
$documents = [];
foreach ($stmt->fetchAll() as $row) {
$documents[(int) $row['stock_lot_id']][] = [
'id' => (int) $row['id'],
'original_filename' => (string) $row['original_filename'],
'mime_type' => (string) $row['mime_type'],
'size_bytes' => (int) $row['size_bytes'],
'uploaded_at' => (string) $row['uploaded_at'],
];
}
return $documents;
}
function get_stock_lot_document(PDO $pdo, int $documentId): array
{
$stmt = $pdo->prepare(
'SELECT id, stock_lot_id, original_filename, stored_filename, mime_type, size_bytes
FROM stock_lot_document WHERE id = :id'
);
$stmt->execute([':id' => $documentId]);
$document = $stmt->fetch();
if (!is_array($document)) {
throw new RuntimeException('Dokument nicht gefunden');
}
return $document;
}
function delete_stock_lot_document(PDO $pdo, int $documentId): void
{
$document = get_stock_lot_document($pdo, $documentId);
$statusStmt = $pdo->prepare('SELECT status FROM stock_lot WHERE id = :id');
$statusStmt->execute([':id' => (int) $document['stock_lot_id']]);
$status = $statusStmt->fetchColumn();
if ($status !== 'current' && $status !== 'open') {
throw new InvalidArgumentException('Dokumente vergangener Chargen können nicht gelöscht werden');
}
$stmt = $pdo->prepare('DELETE FROM stock_lot_document WHERE id = :id');
$stmt->execute([':id' => $documentId]);
$path = dirname(__DIR__, 3) . '/runtime/uploads/lager/' . basename((string) $document['stored_filename']);
if (is_file($path) && !unlink($path)) {
throw new RuntimeException('Dokumentdatei konnte nicht gelöscht werden');
}
}
function upload_stock_lot_document(PDO $pdo, int $lotId, array $file, string $uploadedBy): array
{
if ($lotId <= 0 || !is_uploaded_file((string) ($file['tmp_name'] ?? ''))) {
throw new InvalidArgumentException('Ungültige Dokumentdatei');
}
$error = (int) ($file['error'] ?? UPLOAD_ERR_NO_FILE);
if ($error !== UPLOAD_ERR_OK) {
throw new InvalidArgumentException('Dokument konnte nicht hochgeladen werden');
}
$size = (int) ($file['size'] ?? 0);
if ($size <= 0 || $size > 52428800) {
throw new InvalidArgumentException('Dokument darf maximal 50 MB groß sein');
}
$originalFilename = basename((string) ($file['name'] ?? ''));
if ($originalFilename === '') {
throw new InvalidArgumentException('Dateiname fehlt');
}
$lotStmt = $pdo->prepare("SELECT id FROM stock_lot WHERE id = :id AND status IN ('current', 'open')");
$lotStmt->execute([':id' => $lotId]);
if ($lotStmt->fetchColumn() === false) {
throw new InvalidArgumentException('Charge nicht gefunden');
}
$directory = dirname(__DIR__, 3) . '/runtime/uploads/lager';
if (!is_dir($directory) && !mkdir($directory, 0770, true) && !is_dir($directory)) {
throw new RuntimeException('Dokumentenverzeichnis konnte nicht erstellt werden');
}
$storedFilename = bin2hex(random_bytes(16));
$targetPath = $directory . '/' . $storedFilename;
if (!move_uploaded_file((string) $file['tmp_name'], $targetPath)) {
throw new RuntimeException('Dokument konnte nicht gespeichert werden');
}
$mimeType = (new finfo(FILEINFO_MIME_TYPE))->file($targetPath) ?: 'application/octet-stream';
try {
$stmt = $pdo->prepare(
'INSERT INTO stock_lot_document
(stock_lot_id, original_filename, stored_filename, mime_type, size_bytes, uploaded_by)
VALUES (:lot_id, :original_filename, :stored_filename, :mime_type, :size_bytes, :uploaded_by)
RETURNING id'
);
$stmt->execute([
':lot_id' => $lotId,
':original_filename' => $originalFilename,
':stored_filename' => $storedFilename,
':mime_type' => $mimeType,
':size_bytes' => $size,
':uploaded_by' => $uploadedBy,
]);
return get_stock_lot_document($pdo, (int) $stmt->fetchColumn());
} catch (Throwable $error) {
unlink($targetPath);
throw $error;
}
}
function get_lager_product_forecast(PDO $pdo, int $productId): array
{
$stmt = $pdo->prepare(
"SELECT sellout_date, sellout_forecast_status, expiry_date
FROM stock_lot
WHERE product_id = :product_id
AND 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'],
];
}
function update_stock_lot_traceability(
PDO $pdo,
int $lotId,
@@ -424,9 +242,7 @@ function update_stock_lot_traceability(
float $receiptQty = 0.0,
float $adjustmentQty = 0.0,
float $outQty = 0.0,
?string $lotNumber = null,
?float $targetQty = null,
bool $clearLotNumber = false
?string $lotNumber = null
): array {
if ($lotId <= 0) {
throw new InvalidArgumentException('Chargen-ID fehlt');
@@ -437,19 +253,13 @@ function update_stock_lot_traceability(
$manufacturerLotNumber = null;
}
$lotNumber = $lotNumber !== null ? trim($lotNumber) : null;
if ($lotNumber === '' && !$clearLotNumber) {
throw new InvalidArgumentException('Naurua-Chargennummer fehlt');
}
if ($lotNumber === '') {
$lotNumber = null;
throw new InvalidArgumentException('Naurua-Chargennummer fehlt');
}
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');
}
@@ -497,9 +307,6 @@ 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');
}
@@ -524,35 +331,13 @@ function update_stock_lot_traceability(
if ($manufacturerPartyId !== null && $manufacturerLotNumber === null && (string) $before['status'] !== 'open') {
throw new InvalidArgumentException('Hersteller-Chargennummer fehlt');
}
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) {
if (($adjustmentQty !== 0.0 || $outQty > 0) && (string) $before['status'] !== 'current') {
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 = {$lotNumberAssignment},
SET lot_number = COALESCE(:lot_number, lot_number),
manufacturer_party_id = :manufacturer_party_id,
manufacturer_lot_number = :manufacturer_lot_number,
mfg_date = :mfg_date,
@@ -561,23 +346,18 @@ function update_stock_lot_traceability(
updated_at = NOW()
WHERE id = :id"
);
$updateParams = [
$updateStmt->execute([
':lot_number' => $lotNumber,
':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 ($clearLotNumber) {
$after['lot_number'] = null;
} elseif ($lotNumber !== null) {
if ($lotNumber !== null) {
$after['lot_number'] = $lotNumber;
}
$after['manufacturer_party_id'] = $manufacturerPartyId;
@@ -991,60 +771,6 @@ 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,
+148 -157
View File
@@ -243,7 +243,7 @@ function auth_render_lager_table(string $title, array $rows, array $columns, str
. ' 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['current_qty'] ?? 0))) . '"'
. ' data-lager-drawer-current-qty="' . auth_escape_html(auth_format_lager_quantity((float) ($row['available_qty'] ?? 0))) . '"'
. ' data-lager-drawer-qty-in="' . auth_escape_html(auth_format_lager_quantity((float) ($row['qty_in'] ?? 0))) . '"'
. ' data-lager-drawer-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'] ?? '')) . '"'
@@ -254,7 +254,6 @@ function auth_render_lager_table(string $title, array $rows, array $columns, str
. ' data-lager-drawer-mfg-date="' . auth_escape_html((string) ($row['mfg_date'] ?? '')) . '"'
. ' data-lager-drawer-expiry-date="' . auth_escape_html((string) ($row['expiry_date'] ?? '')) . '"'
. ' data-lager-drawer-purchase-date="' . auth_escape_html((string) ($row['purchase_date'] ?? '')) . '"'
. ' data-lager-drawer-documents="' . auth_escape_html((string) json_encode($row['documents'] ?? [], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) . '"'
. ' data-lager-drawer-virtual-lot-number="' . auth_escape_html((string) ($row['virtual_lot_number'] ?? '')) . '"';
$openLot = $row['open_lots'][0] ?? [];
$detailData .= ' data-lager-open-lot-id="' . auth_escape_html((string) ($openLot['lot_id'] ?? '')) . '"'
@@ -298,42 +297,6 @@ function auth_format_lager_quantity(float $value): string
: number_format($value, 2, ',', '.');
}
function auth_render_lager_documents(array $documents, bool $editable, string $scope): string
{
$segmentClass = $editable ? ' sg-card-segment--gray' : '';
$segmentState = $editable ? ' hidden data-lager-edit-documents="' . auth_escape_html($scope) . '"' : '';
$html = '<div class="sg-card-segment sg-card-segment--body' . $segmentClass . '" data-lager-documents="' . auth_escape_html($scope) . '"' . $segmentState . '>';
if ($editable) {
$html .= '<div class="sg-form-sections-card-wrapper" data-pattern="form-sections" aria-label="Dokumente"><section class="sg-form-sections-card"><div class="sg-form-sections-card__body"><section class="sg-form-sections-card__chapter"><h2 class="sg-strong sg-form-sections-card__chapter-title">Dokumente</h2>';
} else {
$html .= '<section class="sg-form-sections-card__chapter"><h2 class="sg-strong sg-form-sections-card__chapter-title">Dokumente</h2>';
}
if (!$editable) {
$html .= '<div data-lager-documents-read="' . auth_escape_html($scope) . '">';
if ($documents === []) {
$html .= '<p class="sg-body">Keine Dokumente vorhanden.</p>';
} else {
foreach ($documents as $document) {
$documentId = (int) ($document['id'] ?? 0);
$filename = (string) ($document['original_filename'] ?? 'Dokument');
$html .= '<p class="sg-body" data-lager-document-row="' . $documentId . '"><a class="sg-hyperlink" href="/api/lager-document.php?id=' . $documentId . '" data-component="hyperlink">' . auth_escape_html($filename) . '</a></p>';
}
}
$html .= '</div>';
}
if ($editable) {
$html .= '<div data-lager-documents-edit="' . auth_escape_html($scope) . '">';
foreach ($documents as $document) {
$documentId = (int) ($document['id'] ?? 0);
$filename = (string) ($document['original_filename'] ?? 'Dokument');
$html .= '<div class="sg-form-sections-card__field-group" data-lager-document-edit-row="' . $documentId . '"><span class="sg-body">' . auth_escape_html($filename) . ' <span class="sg-tab-button-group" role="tablist" aria-label="Dokument entfernen" data-component="tab-navigation" data-component-size="compact"><button class="sg-interaction-element sg-button sg-tab-button sg-tab-button--compact" type="button" role="tab" aria-selected="false" data-component-part="tab-button" aria-label="Dokument entfernen: ' . auth_escape_html($filename) . '" data-lager-document-delete="' . $documentId . '">×</button></span></span></div>';
}
$html .= '<div class="sg-file-upload" data-pattern="file-upload" data-lager-document-upload-form="' . auth_escape_html($scope) . '"><div class="sg-file-input" data-component="file-input"><div class="sg-file-upload__dropzone sg-labeled-input-row"><label class="sg-label" for="lager-document-file-' . auth_escape_html($scope) . '">Dateien auswählen</label><input class="sg-file-input__control" id="lager-document-file-' . auth_escape_html($scope) . '" type="file" multiple aria-describedby="lager-document-file-status-' . auth_escape_html($scope) . '" data-lager-document-file><button class="sg-interaction-element sg-button sg-button--active sg-file-input__trigger" type="button" data-lager-document-file-trigger>Dateien auswählen</button></div></div><ul class="sg-file-upload__files" data-lager-document-selected-files hidden></ul><p class="sg-body sg-file-upload__status" id="lager-document-file-status-' . auth_escape_html($scope) . '" data-lager-document-status aria-live="polite">Noch keine Dateien ausgewählt.</p></div>';
$html .= '</div>';
}
return $html . ($editable ? '</section></div></section></div></div>' : '</section></div>');
}
function auth_render_lager_historical_lots(array $lagerProducts): string
{
$html = [];
@@ -355,9 +318,7 @@ function auth_render_lager_historical_lots(array $lagerProducts): string
. '<strong>MHD:</strong> ' . auth_format_lager_date($lot['expiry_date'] ?? null) . '<br>'
. '<strong>Hersteller:</strong> ' . auth_escape_html((string) ($lot['manufacturer_name'] ?? 'Nicht hinterlegt')) . '<br>'
. '<strong>Hersteller-Chargennummer:</strong> ' . auth_escape_html((string) ($lot['manufacturer_lot_number'] ?? 'Nicht hinterlegt')) . '</p>'
. '</div>'
. auth_render_lager_documents($lot['documents'] ?? [], false, 'historical-' . (int) ($lot['lot_id'] ?? 0))
. '</article>';
. '</div></article>';
}
$html[] = '</div>';
}
@@ -373,8 +334,8 @@ function auth_render_lager_open_lots(array $lagerProducts, string $manufacturerO
$html[] = '<div data-lager-open-product="' . $productId . '" hidden>';
foreach (($product['open_lots'] ?? []) as $lot) {
$lotId = (int) ($lot['lot_id'] ?? 0);
$html[] = '<div class="sg-delete-confirmation-pattern__stage sg-delete-confirmation-pattern__host" data-pattern="overlay-card" data-dialog-open="false" data-lager-open-card-wrapper="' . $lotId . '"><article class="sg-card sg-card--content-card sg-delete-confirmation-pattern__target" data-component="content-card" data-pattern="overlay-card" aria-label="Offene Charge">'
. '<div class="sg-card-segment sg-card-segment--header sg-card-segment--darkgreen sg-object-card__header" data-component-part="card-header"><div class="sg-strong sg-text-on-dark">Offene Charge</div><div class="sg-component-row"><button class="sg-interaction-element sg-button sg-button--warning" type="button" data-component="button" data-component-variant="warning" data-lager-open-delete>Löschen</button><button class="sg-interaction-element sg-button sg-button--active" type="button" data-lager-open-edit-open>Bearbeiten</button></div></div>'
$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>'
. '<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>'
@@ -384,22 +345,16 @@ function auth_render_lager_open_lots(array $lagerProducts, string $manufacturerO
. '<strong>Herstellungsdatum:</strong> <span data-lager-open-card-mfg-date>' . auth_format_lager_date($lot['mfg_date'] ?? null) . '</span><br>'
. '<strong>MHD:</strong> <span data-lager-open-card-expiry-date>' . auth_format_lager_date($lot['expiry_date'] ?? null) . '</span><br>'
. '<strong>Einkaufsdatum:</strong> <span data-lager-open-card-purchase-date>' . auth_format_lager_date($lot['purchase_date'] ?? null) . '</span></p></div>'
. auth_render_lager_documents($lot['documents'] ?? [], false, 'open-read-' . $lotId)
. '<div class="sg-card-segment sg-card-segment--body sg-card-segment--gray" data-lager-open-edit-form hidden><div class="sg-form-sections-card-wrapper" data-pattern="form-sections" aria-label="Offene Charge bearbeiten"><form class="sg-form-sections-card" id="lager-open-edit-form-' . $lotId . '" data-lager-open-edit-form-element><div class="sg-form-sections-card__body" data-pattern-part="form-body"><h2 class="sg-strong sg-form-sections-card__title">Offene Charge bearbeiten</h2>'
. '<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 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 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">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">Menge</span><input class="sg-interaction-element sg-input-single-line" type="number" min="0" step="1" inputmode="numeric" data-lager-open-target-qty value="' . auth_escape_html((string) ($lot['qty_net'] ?? 0)) . '"></label></div>'
. '</section></div></form></div></div>'
. auth_render_lager_documents($lot['documents'] ?? [], true, 'open-edit-' . $lotId)
. '<footer class="sg-card-segment sg-card-segment--body sg-card-segment--gray" data-lager-open-edit-actions hidden><div class="sg-form-sections-card-wrapper" data-pattern="form-sections"><div class="sg-form-sections-card"><div class="sg-form-sections-card__actions" data-pattern-part="form-actions"><button class="sg-interaction-element sg-button sg-button--active sg-form-sections-card__action" type="button" data-lager-open-edit-close>Abbrechen</button><button class="sg-interaction-element sg-button sg-button--process sg-form-sections-card__action" type="submit" form="lager-open-edit-form-' . $lotId . '">Speichern</button></div><p class="sg-body" data-lager-open-edit-status aria-live="polite"></p></div></div></footer>'
. '</article>'
. '<article class="sg-card sg-card--overlay-host sg-delete-confirmation-pattern__floating-card" aria-label="Leere Felder bestätigen" role="dialog" aria-modal="true" aria-labelledby="lager-open-confirmation-title-' . $lotId . '" data-lager-open-confirmation-card hidden><div class="sg-card-segment sg-card-segment--body sg-delete-confirmation-pattern__body"><p class="sg-body sg-delete-confirmation-pattern__text" id="lager-open-confirmation-title-' . $lotId . '"><strong>Leere Felder speichern?</strong></p><p class="sg-body sg-delete-confirmation-pattern__text">Die folgenden Felder werden leer gespeichert:</p><div class="sg-delete-confirmation-pattern__list" data-lager-open-confirmation-empty-fields aria-label="Leer zu speichernde Felder"></div><div class="sg-delete-confirmation-pattern__actions"><button class="sg-interaction-element sg-button sg-button--active" type="button" data-lager-open-confirmation-cancel>Abbrechen</button><button class="sg-interaction-element sg-button sg-button--process" type="button" data-lager-open-confirmation-submit>Speichern</button></div></div></article>'
. '<article class="sg-card sg-card--overlay-host sg-delete-confirmation-pattern__floating-card" aria-label="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>';
. '<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>';
}
$html[] = '</div>';
}
@@ -424,11 +379,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></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><button class="sg-interaction-element sg-hyperlink" type="button" data-lager-drawer-close>schliessen</button></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><br><strong>Voraussichtlich leer:</strong> <span data-lager-drawer-forecast-value>Keine Prognose</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></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>'
@@ -451,9 +406,9 @@ function auth_render_lager_content(array $lagerProducts, array $lagerArtikel, ar
. '<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>'
. '<div class="sg-card-segment sg-card-segment--body" data-component-part="card-body" data-lager-read-view><p class="sg-body"><strong>Naurua-Chargennummer:</strong> <span data-lager-drawer-lot-number></span><br><strong>Aktueller Bestand:</strong> <span data-lager-drawer-current-qty></span><br><strong>Eingangsmenge:</strong> <span data-lager-drawer-qty-in></span><br><strong>Hersteller:</strong> <span data-lager-drawer-manufacturer-name></span><br><strong>Hersteller-Chargennummer:</strong> <span data-lager-drawer-manufacturer-lot-number></span><br><strong>Herstellungsdatum:</strong> <span data-lager-drawer-mfg-date></span><br><strong>MHD:</strong> <span data-lager-drawer-expiry-date></span><br><strong>Einkaufsdatum:</strong> <span data-lager-drawer-purchase-date></span></p></div>' . auth_render_lager_documents([], false, 'current-read')
. '<div class="sg-card-segment sg-card-segment--body" data-component-part="card-body" data-lager-read-view><p class="sg-body"><strong>Naurua-Chargennummer:</strong> <span data-lager-drawer-lot-number></span><br><strong>Aktueller Bestand:</strong> <span data-lager-drawer-current-qty></span><br><strong>Eingangsmenge:</strong> <span data-lager-drawer-qty-in></span><br><strong>Hersteller:</strong> <span data-lager-drawer-manufacturer-name></span><br><strong>Hersteller-Chargennummer:</strong> <span data-lager-drawer-manufacturer-lot-number></span><br><strong>Herstellungsdatum:</strong> <span data-lager-drawer-mfg-date></span><br><strong>MHD:</strong> <span data-lager-drawer-expiry-date></span><br><strong>Einkaufsdatum:</strong> <span data-lager-drawer-purchase-date></span></p></div>'
. '<div class="sg-card-segment sg-card-segment--body sg-card-segment--gray" data-lager-edit-form hidden><div class="sg-form-sections-card-wrapper" data-pattern="form-sections" aria-label="Aktive Charge bearbeiten">'
. '<form class="sg-form-sections-card" id="lager-edit-form" data-lager-edit-form-element><div class="sg-form-sections-card__body" data-pattern-part="form-body"><h2 class="sg-strong sg-form-sections-card__title">Aktive Charge bearbeiten</h2>'
. '<form class="sg-form-sections-card" data-lager-edit-form-element><div class="sg-form-sections-card__body" data-pattern-part="form-body"><h2 class="sg-strong sg-form-sections-card__title">Aktive Charge bearbeiten</h2>'
. '<section class="sg-form-sections-card__chapter" aria-labelledby="lager-charge-traceability-title"><h2 id="lager-charge-traceability-title" class="sg-strong sg-form-sections-card__chapter-title">Rückverfolgbarkeit</h2>'
. '<div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Naurua-Chargennummer</span><input class="sg-interaction-element sg-input-single-line sg-form-inactive-selectable" type="text" data-lager-lot-number readonly></label></div>'
. '<div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Hersteller</span><select class="sg-interaction-element sg-input-single-line" id="lager-manufacturer" data-lager-manufacturer>' . $manufacturerOptions . '</select></label></div>'
@@ -466,9 +421,8 @@ function auth_render_lager_content(array $lagerProducts, array $lagerArtikel, ar
. '<div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Zugang buchen</span><input class="sg-interaction-element sg-input-single-line" type="number" min="0" step="0.01" inputmode="decimal" data-lager-receipt-qty placeholder="Menge"></label></div>'
. '<div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Bestandskorrektur (+ / −)</span><input class="sg-interaction-element sg-input-single-line" type="number" step="1" inputmode="numeric" data-lager-adjustment-qty placeholder="+ / − Menge"></label></div>'
. '<div class="sg-form-sections-card__field-group"><label class="sg-labeled-input-row"><span class="sg-label">Abgang buchen</span><input class="sg-interaction-element sg-input-single-line" type="number" min="0" step="0.01" inputmode="decimal" data-lager-out-qty placeholder="Menge"></label></div>'
. '</section></div></form></div></div>'
. auth_render_lager_documents([], true, 'current-edit')
. '<footer class="sg-card-segment sg-card-segment--body sg-card-segment--gray" data-lager-edit-actions hidden><div class="sg-form-sections-card-wrapper" data-pattern="form-sections"><div class="sg-form-sections-card"><div class="sg-form-sections-card__actions" data-pattern-part="form-actions"><button class="sg-interaction-element sg-button sg-button--active sg-form-sections-card__action" type="button" data-lager-edit-close>Abbrechen</button><button class="sg-interaction-element sg-button sg-button--process sg-form-sections-card__action" type="submit" form="lager-edit-form">Speichern</button></div><p class="sg-body" data-lager-edit-status aria-live="polite"></p></div></div></footer>'
. '</section><footer class="sg-form-sections-card__actions-segment" data-pattern-part="form-actions-segment"><div class="sg-form-sections-card__actions" data-pattern-part="form-actions"><button class="sg-interaction-element sg-button sg-button--active sg-form-sections-card__action" type="button" data-lager-edit-close>Abbrechen</button><button class="sg-interaction-element sg-button sg-button--process sg-form-sections-card__action" type="submit">Speichern</button></div></footer><p class="sg-body" data-lager-edit-status aria-live="polite"></p></div></form>'
. '</div></div>'
. '</article>'
. '<div data-lager-drawer-topic="historical-lots" aria-label="Vergangene Chargen">' . auth_render_lager_historical_lots($lagerProducts) . '</div>'
. '<article class="sg-card sg-card--content-card" data-component="content-card" data-lager-drawer-topic="virtual-lot" aria-label="Virtuelle Charge">'
@@ -494,9 +448,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-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 .= '<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 .= '</div>';
}
@@ -601,14 +555,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-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 '<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 '<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-scrollable-card__content sg-object-card__content" data-pattern-part="scrollable-card-content" tabindex="0" aria-label="Scrollbarer Formularinhalt">';
echo '<div class="sg-card-segment sg-card-segment--gray sg-object-card__content">';
echo '<div class="sg-form-sections-card-wrapper" data-pattern="form-sections" aria-label="Formular mit Abschnitten">';
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 '<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>';
@@ -617,23 +571,20 @@ 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-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 '<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 '</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-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 '<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 '<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>';
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 '<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 '</div>';
echo '</div>';
echo '<div class="sg-form-sections-card__actions-segment">';
@@ -643,49 +594,43 @@ 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-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 '<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 '</div>';
echo '<div class="sg-form-sections-card__field-group">';
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 '<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 '</div>';
echo '<div class="sg-form-sections-card__field-group">';
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 '<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 '</div>';
echo '<div class="sg-form-sections-card__field-group">';
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 '<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 '</div>';
echo '<div class="sg-form-sections-card__field-group">';
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 '<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 '</div>';
echo '<div class="sg-form-sections-card__field-group">';
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 '<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 '</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">';
@@ -830,13 +775,30 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
echo " lagerBindingsInstalled = true;";
echo " let lagerMobileDetailHost = null;";
echo " let lagerMobileDetailOpen = false;";
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 escapeLagerHtml = (value) => String(value ?? '').replace(/[&<>\"']/g, (character) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' }[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 restoreLagerMobileDetail = () => {";
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 " if (lagerMobileDetailHost && lagerMobileDetailHost.parentNode) { 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 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 " restoreLagerMobileDetail(); const host = document.createElement('div'); host.innerHTML = renderLagerMobileDetail(trigger); lagerMobileDetailHost = host.firstElementChild;";
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 " };";
@@ -845,62 +807,38 @@ 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 = () => document.querySelector('[data-lager-mobile-drawer]') || contentRoot.querySelector('[data-lager-drawer]');";
echo " const getDrawer = () => contentRoot.querySelector('[data-lager-drawer]');";
echo " const setDrawerState = (open) => {";
echo " const drawer = getDrawer(); const backdrop = contentRoot.querySelector('[data-lager-drawer-backdrop]');";
echo " if (!drawer || !backdrop) { return; }";
echo " drawer.dataset.open = String(open); drawer.setAttribute('aria-hidden', String(!open)); backdrop.dataset.open = String(open); backdrop.setAttribute('aria-hidden', String(!open)); 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 escapeLagerHtml = (value) => String(value ?? '').replace(/[&<>\"']/g, (character) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' }[character] || character));";
echo " const renderLagerDocumentRows = (documents, editable) => { if (!documents.length) { return '<p class=\"sg-body\">Keine Dokumente vorhanden.</p>'; } return documents.map((document) => { const id = Number(document.id || 0); const filename = escapeLagerHtml(document.original_filename || 'Dokument'); if (!editable) { return '<p class=\"sg-body\" data-lager-document-row=\"' + id + '\"><a class=\"sg-hyperlink\" href=\"/api/lager-document.php?id=' + id + '\" data-component=\"hyperlink\">' + filename + '</a></p>'; } return '<div class=\"sg-form-sections-card__field-group\" data-lager-document-edit-row=\"' + id + '\"><span class=\"sg-body\">' + filename + ' <span class=\"sg-tab-button-group\" role=\"tablist\" aria-label=\"Dokument entfernen\" data-component=\"tab-navigation\" data-component-size=\"compact\"><button class=\"sg-interaction-element sg-button sg-tab-button sg-tab-button--compact\" type=\"button\" role=\"tab\" aria-selected=\"false\" data-component-part=\"tab-button\" aria-label=\"Dokument entfernen: ' + filename + '\" data-lager-document-delete=\"' + id + '\">×</button></span></span></div>'; }).join(''); };";
echo " const syncLagerDocumentViews = (root, documents) => { const read = root?.querySelector('[data-lager-documents-read]'); const edit = root?.querySelector('[data-lager-documents-edit]'); const upload = root?.querySelector('[data-lager-document-upload-form]'); if (read) { read.innerHTML = renderLagerDocumentRows(documents, false); } if (edit) { edit.innerHTML = renderLagerDocumentRows(documents, true); if (upload) { edit.appendChild(upload); } } };";
echo " const syncLagerCurrentDocuments = (trigger) => { const drawer = getDrawer(); if (!drawer) { return; } let documents = []; try { documents = JSON.parse(trigger.dataset.lagerDrawerDocuments || '[]'); } catch (error) { documents = []; } syncLagerDocumentViews(drawer.querySelector('[data-lager-documents=\"current-read\"]'), documents); syncLagerDocumentViews(drawer.querySelector('[data-lager-documents=\"current-edit\"]'), documents); };";
echo " const renderLagerSelectedFiles = (scope) => { const input = scope?.querySelector('[data-lager-document-file]'); const list = scope?.querySelector('[data-lager-document-selected-files]'); const status = scope?.querySelector('[data-lager-document-status]'); if (!input || !list) { return; } const files = Array.from(input.files || []); list.replaceChildren(...files.map((file, index) => { const item = document.createElement('li'); item.className = 'sg-file-upload__file'; const name = document.createElement('span'); name.className = 'sg-file-upload__file-name sg-body'; name.append(document.createTextNode(file.name + ' ')); const navigation = document.createElement('span'); navigation.className = 'sg-tab-button-group'; navigation.setAttribute('role', 'tablist'); navigation.setAttribute('aria-label', 'Datei entfernen'); navigation.dataset.component = 'tab-navigation'; navigation.dataset.componentSize = 'compact'; const remove = document.createElement('button'); remove.className = 'sg-interaction-element sg-button sg-tab-button sg-tab-button--compact sg-file-upload__remove'; remove.type = 'button'; remove.setAttribute('role', 'tab'); remove.setAttribute('aria-selected', 'false'); remove.setAttribute('data-component-part', 'tab-button'); remove.setAttribute('aria-label', 'Datei entfernen: ' + file.name); remove.dataset.lagerDocumentRemoveIndex = String(index); remove.textContent = '×'; navigation.append(remove); name.append(navigation); item.append(name); return item; })); list.hidden = files.length === 0; if (status && !status.textContent.includes('Upload') && !status.textContent.includes('hochgeladen')) { status.textContent = files.length ? files.length + ' Datei(en) ausgewählt.' : 'Noch keine Dateien ausgewählt.'; } };";
echo " const closeDrawer = () => { setDrawerState(false); contentRoot.querySelectorAll('[data-lager-drawer-open]').forEach((trigger) => trigger.setAttribute('aria-expanded', 'false')); };";
echo " document.addEventListener('click', (event) => { const trigger = event.target.closest('[data-lager-drawer-open]'); if (!trigger || trigger.dataset.lagerDrawerType !== 'Produkt') { return; } const drawer = getDrawer(); const forecast = drawer?.querySelector('[data-lager-drawer-forecast-value]'); if (forecast) { forecast.textContent = trigger.dataset.lagerDrawerForecast || 'Keine Prognose'; } syncLagerCurrentDocuments(trigger); });";
echo " const getLagerForecastWarningText = (forecast, trigger, text) => { if (forecast?.status === 'depleted' || trigger?.dataset.lagerDrawerForecastStatus === 'depleted') { return 'Die Charge ist aufgebraucht. Voraussichtlich leer: ' + text + '.'; } if (trigger?.dataset.lagerDrawerForecastMhdWarning === 'true') { const expiry = trigger.dataset.lagerDrawerExpiryDate ? new Date(trigger.dataset.lagerDrawerExpiryDate + 'T00:00:00').toLocaleDateString('de-CH') : 'dem MHD'; return 'Das MHD wird am ' + expiry + ' erreicht, bevor das Lager leer ist. Voraussichtlich leer: ' + text + '.'; } if (trigger?.dataset.lagerDrawerForecastWarning === 'true') { return 'Voraussichtlich leer: ' + text + '.'; } return 'Keine akute Warnung.'; };";
echo " document.addEventListener('click', (event) => { const trigger = event.target.closest('[data-lager-drawer-open]'); if (!trigger || trigger.dataset.lagerDrawerType !== 'Produkt') { return; } const drawer = getDrawer(); const warningValue = drawer?.querySelector('[data-lager-drawer-warning-value]'); if (warningValue) { warningValue.textContent = getLagerForecastWarningText(null, trigger, trigger.dataset.lagerDrawerForecast || 'Keine Prognose'); } });";
echo " const syncLagerForecast = (forecast, productId) => { const drawer = getDrawer(); const trigger = contentRoot.querySelector('[data-lager-drawer-open][aria-expanded=\"true\"]') || contentRoot.querySelector('[data-lager-row][data-lager-id=\"' + (productId || '') + '\"] [data-lager-drawer-open]'); if (!forecast || !drawer) { return; } const forecastDate = forecast.date ? new Date(forecast.date + 'T00:00:00') : null; const expiryDate = forecast.expiryDate ? new Date(forecast.expiryDate + 'T00:00:00') : null; const mhdWarning = Boolean(forecastDate && expiryDate && forecastDate > expiryDate); const nearExpiryWarning = Boolean(forecastDate && expiryDate && forecastDate >= new Date(new Date(expiryDate).setMonth(expiryDate.getMonth() - 3)) && forecastDate <= expiryDate); const depleted = forecast.status === 'depleted'; const forecastWarning = depleted || nearExpiryWarning; const date = forecastDate ? forecastDate.toLocaleDateString('de-CH') : 'Keine Prognose'; const text = depleted ? date + ' (CHARGE ENDE)' : (nearExpiryWarning || mhdWarning ? date + ' (MHD: ' + expiryDate.toLocaleDateString('de-CH') + ')' : date); const value = drawer.querySelector('[data-lager-drawer-forecast-value]'); if (value) { value.textContent = text; } const header = drawer.querySelector('[data-lager-drawer-warning-header]'); const title = drawer.querySelector('[data-lager-drawer-warning-title]'); const warningValue = drawer.querySelector('[data-lager-drawer-warning-value]'); if (trigger) { trigger.dataset.lagerDrawerForecast = text; trigger.dataset.lagerDrawerForecastStatus = forecast.status || 'no_demand'; trigger.dataset.lagerDrawerForecastWarning = forecastWarning ? 'true' : 'false'; trigger.dataset.lagerDrawerForecastMhdWarning = mhdWarning ? 'true' : 'false'; if (header) { header.classList.toggle('sg-card-segment--darkgreen', !forecastWarning && !mhdWarning); header.classList.toggle('sg-card-segment--signal-red', forecastWarning); header.classList.toggle('sg-card-segment--signal-yellow', mhdWarning); } if (title) { title.textContent = getLagerWarningTitle(trigger); } if (warningValue) { warningValue.textContent = forecastWarning || mhdWarning ? text : 'Keine akute Warnung.'; warningValue.classList.toggle('sg-strong', forecastWarning || mhdWarning); warningValue.classList.toggle('lager-forecast-warning', forecastWarning); warningValue.classList.toggle('lager-forecast-mhd-warning', mhdWarning); } const row = trigger.closest('[data-lager-row]'); const cells = row?.querySelectorAll('[role=\"cell\"]'); const forecastCell = cells?.[cells.length - 1]; if (forecastCell && cells.length === 3) { forecastCell.textContent = text; } } };";
echo " document.addEventListener('lager-save-complete', (event) => { const forecast = event.detail?.forecast; const productId = event.detail?.productId || ''; const trigger = contentRoot.querySelector('[data-lager-drawer-open][aria-expanded=\"true\"]') || contentRoot.querySelector('[data-lager-row][data-lager-id=\"' + productId + '\"] [data-lager-drawer-open]'); const drawer = getDrawer(); if (!forecast || !drawer) { return; } const date = forecast.date ? new Date(forecast.date + 'T00:00:00').toLocaleDateString('de-CH') : 'Keine Prognose'; const text = forecast.status === 'depleted' ? date + ' (CHARGE ENDE)' : date; const value = drawer.querySelector('[data-lager-drawer-forecast-value]'); if (value) { value.textContent = text; } if (trigger) { trigger.dataset.lagerDrawerForecast = text; const row = trigger.closest('[data-lager-row]'); const cells = row?.querySelectorAll('[role=\"cell\"]'); const forecastCell = cells?.[cells.length - 1]; if (forecastCell && cells.length === 3) { forecastCell.textContent = text; } } });";
echo " document.addEventListener('click', (event) => {";
echo " contentRoot.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 " document.addEventListener('click', (event) => { const trigger = event.target.closest('[data-lager-drawer-open]'); if (trigger?.dataset.lagerDrawerType === 'Produkt') { syncLagerCurrentDocuments(trigger); } });";
echo " if (event.target.closest('[data-lager-drawer-close]') || event.target.closest('[data-lager-drawer-backdrop]')) { closeDrawer(); }";
echo " });";
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 remove = event.target.closest('[data-lager-document-remove-index]'); if (remove) { const scope = remove.closest('[data-lager-document-upload-form]'); const input = scope?.querySelector('[data-lager-document-file]'); const index = Number(remove.dataset.lagerDocumentRemoveIndex); if (input && Number.isInteger(index)) { const data = new DataTransfer(); Array.from(input.files || []).forEach((file, fileIndex) => { if (fileIndex !== index) { data.items.add(file); } }); input.files = data.files; input.dispatchEvent(new Event('change', { bubbles: true })); } return; } const trigger = event.target.closest('[data-lager-document-file-trigger]'); const input = trigger?.closest('[data-lager-document-upload-form]')?.querySelector('[data-lager-document-file]'); if (!input) { return; } event.preventDefault(); input.click(); });";
echo " document.addEventListener('dragover', (event) => { const dropzone = event.target.closest('[data-lager-document-upload-form] .sg-file-upload__dropzone'); if (!dropzone) { return; } event.preventDefault(); dropzone.dataset.dragOver = 'true'; });";
echo " document.addEventListener('dragleave', (event) => { const dropzone = event.target.closest('[data-lager-document-upload-form] .sg-file-upload__dropzone'); if (dropzone && !dropzone.contains(event.relatedTarget)) { delete dropzone.dataset.dragOver; } });";
echo " document.addEventListener('drop', (event) => { const dropzone = event.target.closest('[data-lager-document-upload-form] .sg-file-upload__dropzone'); if (!dropzone) { return; } event.preventDefault(); delete dropzone.dataset.dragOver; const input = dropzone.closest('[data-lager-document-upload-form]')?.querySelector('[data-lager-document-file]'); if (input && event.dataTransfer?.files) { input.files = event.dataTransfer.files; input.dispatchEvent(new Event('change', { bubbles: true })); } });";
echo " document.addEventListener('change', (event) => { const input = event.target.closest('[data-lager-document-file]'); if (!input) { return; } const form = input.closest('[data-lager-document-upload-form]'); renderLagerSelectedFiles(form); });";
echo " const uploadLagerDocuments = async (scope, lotId) => { const files = Array.from(scope?.querySelectorAll('[data-lager-document-file]') || []).flatMap((input) => Array.from(input.files || [])); if (!files.length) { return []; } const status = scope.querySelector('[data-lager-document-status]'); const uploaded = []; const errorMessage = (error) => error instanceof Error ? error.message : 'Upload fehlgeschlagen'; if (files.some((file) => file.size > 52428800)) { const error = new Error('Dokument darf maximal 50 MB groß sein.'); if (status) { status.textContent = error.message; } throw error; } if (status) { status.textContent = 'Upload läuft …'; } try { for (const file of files) { const data = new FormData(); data.append('action', 'upload_document'); data.append('csrfToken', contentRoot.querySelector('[data-lager-csrf-token]')?.dataset.lagerCsrfToken || ''); data.append('lotId', lotId || ''); data.append('document', file); const response = await fetch('/api/lager-charge.php', { method: 'POST', body: data }); const result = await response.json(); if (!response.ok || result.ok !== true) { throw new Error(result.error || 'Upload fehlgeschlagen'); } uploaded.push(result.document); } } catch (error) { if (status) { status.textContent = errorMessage(error); } throw error; } if (status) { status.textContent = files.length + ' Datei(en) hochgeladen.'; } return uploaded; };";
echo " document.addEventListener('click', async (event) => { const remove = event.target.closest('[data-lager-document-delete]'); if (!remove) { return; } remove.disabled = true; try { await postLagerChange({ action: 'delete_document', documentId: remove.dataset.lagerDocumentDelete }); window.location.reload(); } catch (error) { remove.disabled = false; remove.setCustomValidity(error instanceof Error ? error.message : 'Dokument konnte nicht entfernt werden'); remove.reportValidity(); } });";
echo " document.addEventListener('click', (event) => { const editButton = event.target.closest('[data-lager-edit-open]'); if (editButton) { const drawer = getDrawer(); const editForm = drawer?.querySelector('[data-lager-edit-form]'); if (editForm) { editForm.hidden = false; } return; } const closeButton = event.target.closest('[data-lager-edit-close]'); if (closeButton) { const drawer = getDrawer(); const editForm = drawer?.querySelector('[data-lager-edit-form]'); if (editForm) { editForm.hidden = true; } } });";
echo " const setLagerEditMode = (editing) => { const drawer = getDrawer(); const readView = drawer?.querySelector('[data-lager-read-view]'); const readDocuments = drawer?.querySelector('[data-lager-documents=\"current-read\"]'); const editForm = drawer?.querySelector('[data-lager-edit-form]'); const documents = drawer?.querySelector('[data-lager-edit-documents=\"current-edit\"]'); const actions = drawer?.querySelector('[data-lager-edit-actions]'); if (readView) { readView.hidden = editing; } if (readDocuments) { readDocuments.hidden = editing; } if (editForm) { editForm.hidden = !editing; } if (documents) { documents.hidden = !editing; } if (actions) { actions.hidden = !editing; } };";
echo " 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 " 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 " 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 " 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 clearLagerMovementInputs = () => { const drawer = getDrawer(); if (!drawer) { return; } drawer.querySelectorAll('[data-lager-receipt-qty], [data-lager-adjustment-qty], [data-lager-out-qty]').forEach((input) => { input.value = ''; }); };";
echo " const syncLagerOpenLotCard = (lot, balance) => { const drawer = getDrawer(); const card = drawer?.querySelector('[data-lager-open-card=\"' + (lot?.id || '') + '\"]'); if (!card) { return; } const values = { '[data-lager-open-card-lot-number]': lot?.lot_number || 'Nicht hinterlegt', '[data-lager-open-card-qty-net]': String(balance?.qty_net ?? 0), '[data-lager-open-card-qty-in]': String(balance?.qty_net ?? 0), '[data-lager-open-card-manufacturer]': drawer.querySelector('[data-lager-new-manufacturer]')?.selectedOptions?.[0]?.textContent || 'Nicht hinterlegt', '[data-lager-open-card-manufacturer-lot]': lot?.manufacturer_lot_number || 'Nicht hinterlegt', '[data-lager-open-card-mfg-date]': lot?.mfg_date || 'Nicht hinterlegt', '[data-lager-open-card-expiry-date]': lot?.expiry_date || 'Nicht hinterlegt', '[data-lager-open-card-purchase-date]': lot?.purchase_date || 'Nicht hinterlegt' }; Object.entries(values).forEach(([selector, value]) => { const target = card.querySelector(selector); if (target) { target.textContent = value; } }); const wrapper = card.closest('[data-lager-open-card-wrapper]'); wrapper?.querySelector('[data-lager-edit-documents]')?.setAttribute('hidden', ''); wrapper?.querySelector('[data-lager-open-edit-actions]')?.setAttribute('hidden', ''); wrapper?.querySelector('[data-lager-documents^=\"open-read-\"]')?.removeAttribute('hidden'); };";
echo " document.addEventListener('click', (event) => { const trigger = event.target.closest('[data-lager-drawer-open]'); if (!trigger || trigger.dataset.lagerDrawerType !== 'Produkt' || window.matchMedia('(max-width: 767px)').matches) { return; } const drawer = getDrawer(); const productId = trigger.closest('[data-lager-row]')?.dataset.lagerId || ''; const openProduct = drawer?.querySelector('[data-lager-open-product=\"' + productId + '\"]'); drawer?.querySelectorAll('[data-lager-open-product]').forEach((product) => { product.hidden = product !== openProduct; product.querySelectorAll('[data-lager-open-edit-form]').forEach((form) => { form.hidden = true; }); product.querySelectorAll('[data-lager-open-edit-actions]').forEach((actions) => { actions.hidden = true; }); product.querySelectorAll('[data-lager-open-card]').forEach((readView) => { readView.hidden = false; }); product.querySelectorAll('[data-lager-documents^=\"open-read-\"]').forEach((readDocuments) => { readDocuments.hidden = false; }); product.querySelectorAll('[data-lager-edit-documents]').forEach((documents) => { documents.hidden = true; }); }); const newForm = drawer?.querySelector('[data-lager-drawer-topic=\"new-lot-form\"]'); const newAction = drawer?.querySelector('[data-lager-drawer-topic=\"new-lot-action\"]'); if (newForm) { newForm.hidden = true; } if (newAction) { newAction.hidden = false; } const notificationWrapper = drawer?.querySelector('[data-lager-new-lot-notification-wrapper]'); const notification = drawer?.querySelector('[data-lager-new-lot-notification]'); const shortageQty = trigger.dataset.lagerDrawerVirtualShortageQty || '0'; const hasShortage = shortageQty !== '0' && shortageQty !== '0,00'; if (notificationWrapper) { notificationWrapper.hidden = !hasShortage; } if (notification && hasShortage) { notification.textContent = 'Für dieses Produkt bestehen virtuelle Fehlbestände in Höhe von ' + shortageQty + '. Die Eingangsmenge wird nach Aktivierung der Charge zur Nachallokation verwendet.'; } const setValue = (selector, value) => { const field = drawer?.querySelector(selector); if (field) { field.value = value || ''; } }; setValue('[data-lager-new-lot-number]', trigger.dataset.lagerOpenLotNumber); setValue('[data-lager-new-manufacturer]', trigger.dataset.lagerOpenManufacturerPartyId); setValue('[data-lager-new-manufacturer-lot]', trigger.dataset.lagerOpenManufacturerLotNumber); setValue('[data-lager-new-mfg-date]', trigger.dataset.lagerOpenMfgDate); setValue('[data-lager-new-expiry-date]', trigger.dataset.lagerOpenExpiryDate); setValue('[data-lager-new-purchase-date]', trigger.dataset.lagerOpenPurchaseDate); if (drawer) { drawer.dataset.lagerOpenLotId = trigger.dataset.lagerOpenLotId || ''; drawer.dataset.lagerProductId = productId; } });";
echo " document.addEventListener('click', (event) => { const openButton = event.target.closest('[data-lager-new-lot-open]'); if (openButton) { const drawer = getDrawer(); const form = drawer?.querySelector('[data-lager-drawer-topic=\"new-lot-form\"]'); const action = drawer?.querySelector('[data-lager-drawer-topic=\"new-lot-action\"]'); if (form) { form.hidden = false; } if (action) { action.hidden = true; } return; } if (event.target.closest('[data-lager-new-lot-close]')) { const drawer = getDrawer(); const form = drawer?.querySelector('[data-lager-drawer-topic=\"new-lot-form\"]'); const action = drawer?.querySelector('[data-lager-drawer-topic=\"new-lot-action\"]'); if (form) { form.hidden = true; } if (action) { action.hidden = false; } } });";
echo " document.addEventListener('click', (event) => { const editButton = event.target.closest('[data-lager-open-edit-open]'); if (editButton) { const card = editButton.closest('[data-lager-open-card-wrapper]'); const form = card?.querySelector('[data-lager-open-edit-form]'); const readView = card?.querySelector('[data-lager-open-card]'); const readDocuments = card?.querySelector('[data-lager-documents^=\"open-read-\"]'); const documents = card?.querySelector('[data-lager-edit-documents]'); const actions = card?.querySelector('[data-lager-open-edit-actions]'); if (form) { form.hidden = false; } if (readView) { readView.hidden = true; } if (readDocuments) { readDocuments.hidden = true; } if (documents) { documents.hidden = false; } if (actions) { actions.hidden = false; } return; } const closeButton = event.target.closest('[data-lager-open-edit-close]'); if (closeButton) { const card = closeButton.closest('[data-lager-open-card-wrapper]'); const form = card?.querySelector('[data-lager-open-edit-form]'); const readView = card?.querySelector('[data-lager-open-card]'); const readDocuments = card?.querySelector('[data-lager-documents^=\"open-read-\"]'); const documents = card?.querySelector('[data-lager-edit-documents]'); const actions = card?.querySelector('[data-lager-open-edit-actions]'); if (form) { form.hidden = true; } if (readView) { readView.hidden = false; } if (readDocuments) { readDocuments.hidden = false; } if (documents) { documents.hidden = true; } if (actions) { actions.hidden = true; } } });";
echo " document.addEventListener('input', (event) => { const input = event.target.closest('[data-lager-open-delete-confirmation-input]'); if (!input) { return; } const card = input.closest('[data-lager-open-card-wrapper]'); const submit = card?.querySelector('[data-lager-open-delete-confirmation-submit]'); const valid = input.value === 'DELETE'; if (submit) { submit.disabled = !valid; submit.setAttribute('aria-disabled', String(!valid)); submit.classList.toggle('sg-button--process-inactive', !valid); } });";
echo " document.addEventListener('click', async (event) => { const deleteButton = event.target.closest('[data-lager-open-delete]'); if (deleteButton) { const card = deleteButton.closest('[data-lager-open-card-wrapper]'); const confirmation = card?.querySelector('[data-lager-open-delete-confirmation]'); const input = card?.querySelector('[data-lager-open-delete-confirmation-input]'); const submit = card?.querySelector('[data-lager-open-delete-confirmation-submit]'); if (input) { input.value = ''; } if (submit) { submit.disabled = true; submit.setAttribute('aria-disabled', 'true'); submit.classList.add('sg-button--process-inactive'); } if (confirmation) { confirmation.hidden = false; } if (card) { card.dataset.dialogOpen = 'true'; } return; } const cancel = event.target.closest('[data-lager-open-delete-confirmation-cancel]'); if (cancel) { const card = cancel.closest('[data-lager-open-card-wrapper]'); const confirmation = card?.querySelector('[data-lager-open-delete-confirmation]'); if (confirmation) { confirmation.hidden = true; } if (card) { card.dataset.dialogOpen = 'false'; } return; } const submit = event.target.closest('[data-lager-open-delete-confirmation-submit]'); if (!submit || submit.disabled) { return; } const card = submit.closest('[data-lager-open-card-wrapper]'); const confirmation = card?.querySelector('[data-lager-open-delete-confirmation]'); const status = card?.querySelector('[data-lager-open-delete-status]'); if (status) { status.textContent = 'Löschen läuft …'; } submit.disabled = true; try { await postLagerChange({ action: 'delete_open_lot', lotId: card?.dataset.lagerOpenCardWrapper || '' }); card?.remove(); } catch (error) { if (status) { status.textContent = error instanceof Error ? error.message : 'Löschen fehlgeschlagen'; } submit.disabled = false; } });";
echo " document.addEventListener('click', (event) => { const cancel = event.target.closest('[data-lager-open-confirmation-cancel]'); if (cancel) { const card = cancel.closest('[data-lager-open-card-wrapper]'); const confirmation = card?.querySelector('[data-lager-open-confirmation-card]'); if (confirmation) { confirmation.hidden = true; } if (card) { card.dataset.dialogOpen = 'false'; } const form = card?.querySelector('[data-lager-open-edit-form-element]'); if (form) { form.dataset.confirmationAccepted = 'false'; } return; } const confirm = event.target.closest('[data-lager-open-confirmation-submit]'); if (confirm) { const card = confirm.closest('[data-lager-open-card-wrapper]'); const confirmation = card?.querySelector('[data-lager-open-confirmation-card]'); const form = card?.querySelector('[data-lager-open-edit-form-element]'); if (confirmation) { confirmation.hidden = true; } if (card) { card.dataset.dialogOpen = 'false'; } if (form) { form.dataset.confirmationAccepted = 'true'; form.requestSubmit(); } } });";
echo " document.addEventListener('submit', async (event) => { const openLotForm = event.target.closest('[data-lager-open-edit-form-element]'); if (!openLotForm) { return; } event.preventDefault(); const drawer = getDrawer(); const card = openLotForm.closest('[data-lager-open-card-wrapper]'); const editSection = card?.querySelector('[data-lager-open-edit-form]'); const fields = [{ label: 'Naurua-Chargennummer', input: openLotForm.querySelector('[data-lager-open-lot-number]') }, { label: 'Hersteller', input: openLotForm.querySelector('[data-lager-open-manufacturer]') }, { label: 'Hersteller-Chargennummer', input: openLotForm.querySelector('[data-lager-open-manufacturer-lot]') }, { label: 'Herstellungsdatum', input: openLotForm.querySelector('[data-lager-open-mfg-date]') }, { label: 'MHD', input: openLotForm.querySelector('[data-lager-open-expiry-date]') }, { label: 'Einkaufsdatum', input: openLotForm.querySelector('[data-lager-open-purchase-date]') }, { label: 'Menge', input: openLotForm.querySelector('[data-lager-open-target-qty]') }]; const emptyFields = fields.filter((field) => (field.input?.value || '').trim() === ''); if (emptyFields.length > 0 && openLotForm.dataset.confirmationAccepted !== 'true') { const confirmation = card?.querySelector('[data-lager-open-confirmation-card]'); const emptyFieldsList = card?.querySelector('[data-lager-open-confirmation-empty-fields]'); if (emptyFieldsList) { emptyFieldsList.replaceChildren(...emptyFields.map((field) => { const item = document.createElement('div'); item.className = 'sg-body'; item.textContent = field.label; return item; })); } if (confirmation) { confirmation.hidden = false; } if (card) { card.dataset.dialogOpen = 'true'; } return; } openLotForm.dataset.confirmationAccepted = 'false'; const status = openLotForm.closest('[data-lager-open-edit-form]')?.querySelector('[data-lager-open-edit-status]'); if (status) { status.textContent = 'Speichern läuft …'; } try { const lotId = card?.dataset.lagerOpenCardWrapper || ''; const uploadedDocuments = await uploadLagerDocuments(card?.querySelector('[data-lager-documents=open-edit-' + lotId + ']'), lotId); const existingDocuments = Array.from(card?.querySelectorAll('[data-lager-documents^=\"open-read-\"] [data-lager-document-row]') || []).map((row) => ({ id: Number(row.dataset.lagerDocumentRow || 0), original_filename: row.querySelector('a')?.textContent || 'Dokument' })); const targetQtyInput = openLotForm.querySelector('[data-lager-open-target-qty]'); const result = await postLagerChange({ action: 'prepare_lot', productId: drawer?.dataset.lagerProductId || '', lotId, lotNumber: openLotForm.querySelector('[data-lager-open-lot-number]')?.value || '', manufacturerPartyId: openLotForm.querySelector('[data-lager-open-manufacturer]')?.value || null, manufacturerLotNumber: openLotForm.querySelector('[data-lager-open-manufacturer-lot]')?.value || '', mfgDate: openLotForm.querySelector('[data-lager-open-mfg-date]')?.value || '', expiryDate: openLotForm.querySelector('[data-lager-open-expiry-date]')?.value || '', purchaseDate: openLotForm.querySelector('[data-lager-open-purchase-date]')?.value || '', targetQty: targetQtyInput?.value.trim() === '' ? '0' : targetQtyInput?.value || '' }); syncLagerDocumentViews(card, [...existingDocuments, ...uploadedDocuments]); syncLagerOpenLotCard(result.lot, result.balance); if (editSection) { editSection.hidden = true; } card?.querySelector('[data-lager-open-card]')?.removeAttribute('hidden'); if (status) { status.textContent = ''; } } catch (error) { if (status) { status.textContent = error instanceof Error ? error.message : 'Speichern fehlgeschlagen'; } } });";
echo " document.addEventListener('submit', async (event) => { const newLotForm = event.target.closest('[data-lager-new-lot-form-element]'); if (!newLotForm) { return; } event.preventDefault(); const drawer = getDrawer(); const status = drawer?.querySelector('[data-lager-new-lot-status]'); if (status) { status.textContent = 'Speichern läuft …'; } try { const result = await postLagerChange({ action: 'prepare_lot', productId: drawer?.dataset.lagerProductId || '', lotId: drawer?.dataset.lagerOpenLotId || '', lotNumber: drawer?.querySelector('[data-lager-new-lot-number]')?.value || '', manufacturerPartyId: drawer?.querySelector('[data-lager-new-manufacturer]')?.value || null, manufacturerLotNumber: drawer?.querySelector('[data-lager-new-manufacturer-lot]')?.value || '', mfgDate: drawer?.querySelector('[data-lager-new-mfg-date]')?.value || '', expiryDate: drawer?.querySelector('[data-lager-new-expiry-date]')?.value || '', purchaseDate: drawer?.querySelector('[data-lager-new-purchase-date]')?.value || '', receiptQty: drawer?.querySelector('[data-lager-new-receipt-qty]')?.value || '' }); syncLagerOpenLotCard(result.lot, result.balance); const form = drawer?.querySelector('[data-lager-drawer-topic=\"new-lot-form\"]'); const action = drawer?.querySelector('[data-lager-drawer-topic=\"new-lot-action\"]'); if (form) { form.hidden = true; } if (action) { action.hidden = false; } if (status) { status.textContent = ''; } } catch (error) { if (status) { status.textContent = error instanceof Error ? error.message : 'Speichern fehlgeschlagen'; } } });";
echo " document.addEventListener('submit', async (event) => { const editForm = event.target.closest('[data-lager-edit-form-element]'); if (!editForm) { return; } event.preventDefault(); const drawer = getDrawer(); const status = drawer?.querySelector('[data-lager-edit-status]'); if (status) { status.textContent = 'Speichern läuft …'; } try { const lotId = drawer?.dataset.lagerActiveLotId || ''; const uploadedDocuments = await uploadLagerDocuments(drawer?.querySelector('[data-lager-documents=\"current-edit\"]'), lotId); const result = await postLagerChange({ action: 'update_lot', lotId, manufacturerPartyId: drawer?.querySelector('[data-lager-manufacturer]')?.value || null, manufacturerLotNumber: drawer?.querySelector('[data-lager-manufacturer-lot]')?.value || '', mfgDate: drawer?.querySelector('[data-lager-mfg-date]')?.value || '', expiryDate: drawer?.querySelector('[data-lager-expiry-date]')?.value || '', purchaseDate: drawer?.querySelector('[data-lager-purchase-date]')?.value || '', receiptQty: drawer?.querySelector('[data-lager-receipt-qty]')?.value || '', adjustmentQty: drawer?.querySelector('[data-lager-adjustment-qty]')?.value || '', outQty: drawer?.querySelector('[data-lager-out-qty]')?.value || '' }); const trigger = contentRoot.querySelector('[data-lager-drawer-open][aria-expanded=\"true\"]'); if (trigger && uploadedDocuments.length) { let existingDocuments = []; try { existingDocuments = JSON.parse(trigger.dataset.lagerDrawerDocuments || '[]'); } catch (error) { existingDocuments = []; } trigger.dataset.lagerDrawerDocuments = JSON.stringify([...existingDocuments, ...uploadedDocuments]); syncLagerCurrentDocuments(trigger); } syncLagerReadView(); syncLagerBalanceView(result.balance); clearLagerMovementInputs(); setLagerEditMode(false); if (status) { status.textContent = ''; } } catch (error) { if (status) { status.textContent = error instanceof Error ? error.message : 'Speichern fehlgeschlagen'; } } });";
echo " 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 " 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]');";
@@ -940,7 +878,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) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' }[character] || character));";
echo " let bestellungenMobileDetailHost = null;";
echo " let bestellungenDetailOpen = false; let bestellungenOpeningMobileDetail = false;";
echo " let bestellungenDetailOpen = false;";
echo " const syncSearchState = () => {";
echo " const input = getSearchInput();";
echo " const wrap = getSearchWrap();";
@@ -949,10 +887,47 @@ 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 = () => {";
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 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 getDrawerBackdrop = () => contentRoot.querySelector('[data-bestellungen-drawer-backdrop]');";
echo " const setDrawerState = (open) => {";
echo " const drawer = getDrawer();";
@@ -973,7 +948,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() && !bestellungenOpeningMobileDetail) {";
echo " if (isMobileDetailMode()) {";
echo " openMobileDetail(trigger);";
echo " return;";
echo " }";
@@ -998,7 +973,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
echo " });";
echo " };";
echo " const restoreBestellungenList = () => {";
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 " if (bestellungenMobileDetailHost && bestellungenMobileDetailHost.parentNode) {";
echo " bestellungenMobileDetailHost.parentNode.removeChild(bestellungenMobileDetailHost);";
echo " }";
echo " bestellungenMobileDetailHost = null;";
@@ -1008,13 +983,14 @@ 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();";
echo " host.innerHTML = renderMobileDetail(trigger);";
echo " bestellungenMobileDetailHost = host.firstElementChild;";
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 " 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 " bestellungenDetailOpen = true;";
echo " window.history.pushState({ bestellungenView: 'detail', orderId: trigger.dataset.orderId || '' }, '', window.location.href);";
echo " };";
@@ -1168,7 +1144,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best
echo " closeMobileDetail();";
echo " }";
echo " });";
echo " document.addEventListener('click', (event) => {";
echo " contentRoot.addEventListener('click', (event) => {";
echo " const openTrigger = event.target.closest('[data-order-drawer-open]');";
echo " if (openTrigger) {";
echo " event.preventDefault();";
@@ -1242,7 +1218,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 = overlay.querySelector('[data-otc-order-submit]');";
echo " const submitBtn = form.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 : '';";
@@ -1263,6 +1239,7 @@ 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();";
@@ -1353,6 +1330,9 @@ 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) {";
@@ -1366,6 +1346,17 @@ 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);";
@@ -1386,7 +1377,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-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.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.querySelector('[data-otc-payment-method]').value = nextPaymentMethod;";
echo " paymentRows.appendChild(row);";
echo " bindPaymentRow(row);";
+1 -2
View File
@@ -26,10 +26,9 @@ 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, $recoveryOrderGids);
$result = run_shopify_delta($env, $pdo, $cutoff, false);
$result['technical_run_id'] = $runId;
if (($result['status'] ?? '') === 'done') {
shopify_delta_store_watermark($pdo, $reconciliationStartedAt->format(DATE_ATOM));
-30
View File
@@ -1,30 +0,0 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../modules/shared/db.php';
require_once __DIR__ . '/../../modules/shared/auth/service.php';
require_once __DIR__ . '/../../modules/erp/lager/service.php';
$env = expand_env_values(parse_env_file(__DIR__ . '/../../.env'));
try {
$pdo = connect_database($env);
auth_bootstrap_session();
if (auth_current_user($pdo) === null) {
http_response_code(401);
exit('Unauthorized');
}
$document = get_stock_lot_document($pdo, (int) ($_GET['id'] ?? 0));
$path = dirname(__DIR__, 2) . '/runtime/uploads/lager/' . basename((string) $document['stored_filename']);
if (!is_file($path)) {
http_response_code(404);
exit('Not Found');
}
header('Content-Type: application/octet-stream');
header('Content-Length: ' . (string) filesize($path));
header("Content-Disposition: attachment; filename*=UTF-8''" . rawurlencode((string) $document['original_filename']));
readfile($path);
} catch (Throwable) {
http_response_code(404);
exit('Not Found');
}
-148
View File
@@ -660,13 +660,6 @@ 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);
@@ -2107,147 +2100,6 @@ 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 */
/* ========================================================= */
+2 -2
View File
@@ -1,7 +1,7 @@
{
"styleguideVersion": "2026.05.18.1",
"styleguideCommit": "71734d6",
"syncedAtUtc": "2026-09-08T09:49:27Z",
"styleguideCommit": "fd73842",
"syncedAtUtc": "2026-09-06T13:32:07Z",
"sourceRepo": "/Users/mathias/Documents/Dokumente Chouchou/Codebases/Styleguide",
"mirroredDocsPath": "docs/styleguide"
}
-148
View File
@@ -660,13 +660,6 @@ 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);
@@ -2107,147 +2100,6 @@ 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 */
/* ========================================================= */
+4 -55
View File
@@ -96,26 +96,6 @@ 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;
@@ -137,8 +117,9 @@ body.sg-lager-mobile-detail-open > :not([data-lager-mobile-detail-host]) {
}
.sg-otc-order-overlay__panel {
--layout-scrollable-card-width: min(960px, 100%);
--layout-scrollable-card-height: min(600px, calc(100vh - (2 * var(--spacing-large))));
width: min(960px, 100%);
max-height: calc(100vh - (2 * var(--spacing-large)));
overflow: auto;
}
.sg-otc-order-overlay__title {
@@ -157,19 +138,6 @@ 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);
}
@@ -186,14 +154,6 @@ 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));
}
@@ -275,21 +235,10 @@ body.sg-lager-mobile-detail-open > :not([data-lager-mobile-detail-host]) {
.sg-card-list-page-drawer [data-lager-drawer-topic="new-lot-form"][hidden],
.sg-card-list-page-drawer [data-lager-new-lot-notification-wrapper][hidden],
.sg-card-list-page-drawer [data-lager-open-edit-form][hidden],
.sg-card-list-page-drawer [data-lager-open-edit-actions][hidden],
.sg-card-list-page-drawer [data-lager-open-card][hidden],
.sg-card-list-page-drawer [data-lager-edit-documents][hidden],
.sg-card-list-page-drawer [data-lager-edit-actions][hidden] {
.sg-card-list-page-drawer [data-lager-open-card][hidden] {
display: none;
}
.sg-card-list-page-drawer .sg-file-upload {
max-width: none;
}
.sg-card-list-page-drawer .sg-file-upload__dropzone {
box-sizing: border-box;
}
.sg-card-list-page-drawer [data-lager-drawer-topic="new-lot-action"] {
display: flex;
align-items: center;