From 0b39acbf30fa680a2729479a272fc5f12e6942cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20Gla=CC=88ser?= Date: Tue, 8 Sep 2026 13:49:20 +0200 Subject: [PATCH] Revert "Remove lot document comments" This reverts commit 3a3de9da221e00f4fb5f89f7c73fdac14cbd7ca2. --- modules/erp/lager/api/lot.php | 7 +++++++ modules/erp/lager/service.php | 28 +++++++++++++++++++++++----- modules/shared/auth/ui/home.php | 17 ++++++++++++----- public/assets/styles.portal.css | 6 ++++++ 4 files changed, 48 insertions(+), 10 deletions(-) diff --git a/modules/erp/lager/api/lot.php b/modules/erp/lager/api/lot.php index 861a230..91da6e7 100644 --- a/modules/erp/lager/api/lot.php +++ b/modules/erp/lager/api/lot.php @@ -36,11 +36,13 @@ try { $pdo, (int) ($_POST['lotId'] ?? 0), $file, + (string) ($_POST['comment'] ?? ''), (string) ($currentUser['username'] ?? $currentUser['email'] ?? 'unknown') ); json_response(201, ['ok' => true, 'document' => [ 'id' => (int) $document['id'], 'original_filename' => (string) $document['original_filename'], + 'comment' => $document['comment'] === null ? '' : (string) $document['comment'], ]]); } @@ -101,6 +103,11 @@ try { json_response(200, ['ok' => true, 'productId' => $productId, 'availableQty' => get_lager_product_available_qty($pdo, $productId)]); } + if ($action === 'update_document_comment') { + update_stock_lot_document_comment($pdo, (int) ($data['documentId'] ?? 0), (string) ($data['comment'] ?? '')); + json_response(200, ['ok' => true]); + } + if ($action === 'delete_document') { delete_stock_lot_document($pdo, (int) ($data['documentId'] ?? 0)); json_response(200, ['ok' => true]); diff --git a/modules/erp/lager/service.php b/modules/erp/lager/service.php index 5d485d2..b728f47 100644 --- a/modules/erp/lager/service.php +++ b/modules/erp/lager/service.php @@ -286,7 +286,7 @@ function get_stock_lot_documents_by_lot_ids(PDO $pdo, array $lotIds): array $placeholders = implode(',', array_fill(0, count($lotIds), '?')); $stmt = $pdo->prepare( - "SELECT id, stock_lot_id, original_filename, mime_type, size_bytes, uploaded_at + "SELECT id, stock_lot_id, original_filename, mime_type, size_bytes, comment, uploaded_at FROM stock_lot_document WHERE stock_lot_id IN ({$placeholders}) ORDER BY stock_lot_id, id" @@ -299,6 +299,7 @@ function get_stock_lot_documents_by_lot_ids(PDO $pdo, array $lotIds): array 'original_filename' => (string) $row['original_filename'], 'mime_type' => (string) $row['mime_type'], 'size_bytes' => (int) $row['size_bytes'], + 'comment' => $row['comment'] === null ? '' : (string) $row['comment'], 'uploaded_at' => (string) $row['uploaded_at'], ]; } @@ -308,7 +309,7 @@ function get_stock_lot_documents_by_lot_ids(PDO $pdo, array $lotIds): array 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 + 'SELECT id, stock_lot_id, original_filename, stored_filename, mime_type, size_bytes, comment FROM stock_lot_document WHERE id = :id' ); $stmt->execute([':id' => $documentId]); @@ -319,6 +320,22 @@ function get_stock_lot_document(PDO $pdo, int $documentId): array return $document; } +function update_stock_lot_document_comment(PDO $pdo, int $documentId, string $comment): 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 bearbeitet werden'); + } + $stmt = $pdo->prepare('UPDATE stock_lot_document SET comment = :comment WHERE id = :id'); + $stmt->execute([':comment' => trim($comment) === '' ? null : trim($comment), ':id' => $documentId]); + if ($stmt->rowCount() !== 1) { + throw new RuntimeException('Dokument nicht gefunden'); + } +} + function delete_stock_lot_document(PDO $pdo, int $documentId): void { $document = get_stock_lot_document($pdo, $documentId); @@ -336,7 +353,7 @@ function delete_stock_lot_document(PDO $pdo, int $documentId): void } } -function upload_stock_lot_document(PDO $pdo, int $lotId, array $file, string $uploadedBy): array +function upload_stock_lot_document(PDO $pdo, int $lotId, array $file, string $comment, string $uploadedBy): array { if ($lotId <= 0 || !is_uploaded_file((string) ($file['tmp_name'] ?? ''))) { throw new InvalidArgumentException('Ungültige Dokumentdatei'); @@ -372,8 +389,8 @@ function upload_stock_lot_document(PDO $pdo, int $lotId, array $file, string $up 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) + (stock_lot_id, original_filename, stored_filename, mime_type, size_bytes, comment, uploaded_by) + VALUES (:lot_id, :original_filename, :stored_filename, :mime_type, :size_bytes, :comment, :uploaded_by) RETURNING id' ); $stmt->execute([ @@ -382,6 +399,7 @@ function upload_stock_lot_document(PDO $pdo, int $lotId, array $file, string $up ':stored_filename' => $storedFilename, ':mime_type' => $mimeType, ':size_bytes' => $size, + ':comment' => trim($comment) === '' ? null : trim($comment), ':uploaded_by' => $uploadedBy, ]); return get_stock_lot_document($pdo, (int) $stmt->fetchColumn()); diff --git a/modules/shared/auth/ui/home.php b/modules/shared/auth/ui/home.php index 34d3129..e1ae080 100644 --- a/modules/shared/auth/ui/home.php +++ b/modules/shared/auth/ui/home.php @@ -311,7 +311,12 @@ function auth_render_lager_documents(array $documents, bool $editable, string $s foreach ($documents as $document) { $documentId = (int) ($document['id'] ?? 0); $filename = (string) ($document['original_filename'] ?? 'Dokument'); - $html .= '

' . auth_escape_html($filename) . '

'; + $comment = trim((string) ($document['comment'] ?? '')); + $html .= '

' . auth_escape_html($filename) . ''; + if ($comment !== '') { + $html .= '
Kommentar: ' . auth_escape_html($comment) . ''; + } + $html .= '

'; } } $html .= ''; @@ -321,9 +326,10 @@ function auth_render_lager_documents(array $documents, bool $editable, string $s foreach ($documents as $document) { $documentId = (int) ($document['id'] ?? 0); $filename = (string) ($document['original_filename'] ?? 'Dokument'); - $html .= '
' . auth_escape_html($filename) . '
'; + $comment = (string) ($document['comment'] ?? ''); + $html .= '
'; } - $html .= '

Noch keine Dateien ausgewählt.

'; + $html .= '

Noch keine Dateien ausgewählt.

'; $html .= ''; } return $html . ''; @@ -847,7 +853,7 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best 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) => ({ '&': '&', '<': '<', '>': '>', '\"': '"', \"'\": ''' }[character] || character));"; - echo " const renderLagerDocumentRows = (documents, editable) => { if (!documents.length) { return '

Keine Dokumente vorhanden.

'; } return documents.map((document) => { const id = Number(document.id || 0); const filename = escapeLagerHtml(document.original_filename || 'Dokument'); if (!editable) { return '

' + filename + '

'; } return '
' + filename + '
'; }).join(''); };"; + echo " const renderLagerDocumentRows = (documents, editable) => { if (!documents.length) { return '

Keine Dokumente vorhanden.

'; } return documents.map((document) => { const id = Number(document.id || 0); const filename = escapeLagerHtml(document.original_filename || 'Dokument'); const comment = String(document.comment || '').trim(); if (!editable) { return '

' + filename + '' + (comment ? '
Kommentar: ' + escapeLagerHtml(comment) + '' : '') + '

'; } return '
'; }).join(''); };"; echo " const syncLagerCurrentDocuments = (trigger) => { const drawer = getDrawer(); if (!drawer) { return; } let documents = []; try { documents = JSON.parse(trigger.dataset.lagerDrawerDocuments || '[]'); } catch (error) { documents = []; } const read = drawer.querySelector('[data-lager-documents-read=\"current-read\"]'); const edit = drawer.querySelector('[data-lager-documents-edit=\"current-edit\"]'); if (read) { read.innerHTML = renderLagerDocumentRows(documents, false); } if (edit) { edit.innerHTML = renderLagerDocumentRows(documents, true) + edit.querySelector('[data-lager-document-upload-form]')?.outerHTML || ''; } };"; 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); });"; @@ -867,7 +873,8 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $best echo " const getLagerDocumentLotId = (form) => form.closest('[data-lager-open-card-wrapper]')?.dataset.lagerOpenCardWrapper || getDrawer()?.dataset.lagerActiveLotId || '';"; echo " contentRoot.querySelectorAll('[data-lager-document-upload-form]').forEach((form) => { const input = form.querySelector('[data-lager-document-file]'); const trigger = form.querySelector('.sg-file-input__trigger'); if (input && trigger) { trigger.addEventListener('click', () => input.click()); } });"; 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]'); const submit = form?.querySelector('[type=\"submit\"]'); const status = form?.querySelector('[data-lager-document-status]'); const count = input.files?.length || 0; if (status) { status.textContent = count ? count + ' Datei(en) ausgewählt.' : 'Noch keine Dateien ausgewählt.'; } if (submit) { submit.disabled = count === 0; submit.setAttribute('aria-disabled', String(count === 0)); submit.classList.toggle('sg-button--process-inactive', count === 0); } });"; - echo " document.addEventListener('submit', async (event) => { const form = event.target.closest('[data-lager-document-upload-form]'); if (!form) { return; } event.preventDefault(); const input = form.querySelector('[data-lager-document-file]'); const submit = form.querySelector('[type=\"submit\"]'); const status = form.querySelector('[data-lager-document-status]'); const files = Array.from(input?.files || []); if (!files.length) { return; } if (files.some((file) => file.size > 52428800)) { if (status) { status.textContent = 'Dokument darf maximal 50 MB groß sein.'; } return; } if (submit) { submit.disabled = true; } 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', getLagerDocumentLotId(form)); 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'); } } window.location.reload(); } catch (error) { if (status) { status.textContent = error instanceof Error ? error.message : 'Upload fehlgeschlagen'; } if (submit) { submit.disabled = false; } } });"; + echo " document.addEventListener('submit', async (event) => { const form = event.target.closest('[data-lager-document-upload-form]'); if (!form) { return; } event.preventDefault(); const input = form.querySelector('[data-lager-document-file]'); const submit = form.querySelector('[type=\"submit\"]'); const status = form.querySelector('[data-lager-document-status]'); const files = Array.from(input?.files || []); if (!files.length) { return; } if (files.some((file) => file.size > 52428800)) { if (status) { status.textContent = 'Dokument darf maximal 50 MB groß sein.'; } return; } if (submit) { submit.disabled = true; } 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', getLagerDocumentLotId(form)); data.append('comment', form.querySelector('[data-lager-document-comment]')?.value || ''); 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'); } } window.location.reload(); } catch (error) { if (status) { status.textContent = error instanceof Error ? error.message : 'Upload fehlgeschlagen'; } if (submit) { submit.disabled = false; } } });"; + echo " document.addEventListener('blur', async (event) => { const input = event.target.closest('[data-lager-document-comment-input]'); if (!input) { return; } try { await postLagerChange({ action: 'update_document_comment', documentId: input.dataset.lagerDocumentCommentInput, comment: input.value }); } catch (error) { input.setCustomValidity(error instanceof Error ? error.message : 'Kommentar konnte nicht gespeichert werden'); input.reportValidity(); } }, true);"; echo " document.addEventListener('click', (event) => { const open = event.target.closest('[data-lager-document-delete]'); if (!open) { return; } const row = open.closest('[data-lager-document-edit-row]'); const id = open.dataset.lagerDocumentDelete; if (row?.querySelector('[data-lager-document-confirmation]')) { return; } const confirmation = document.createElement('article'); confirmation.className = 'sg-card sg-card--overlay-host sg-delete-confirmation-pattern__floating-card'; confirmation.setAttribute('role', 'dialog'); confirmation.setAttribute('aria-modal', 'true'); confirmation.dataset.lagerDocumentConfirmation = id; confirmation.innerHTML = '

Dokument löschen?

Bestätige durch Eingabe von DELETE.

'; row.appendChild(confirmation); });"; echo " document.addEventListener('input', (event) => { const input = event.target.closest('[data-lager-document-delete-input]'); if (!input) { return; } const button = input.closest('[data-lager-document-confirmation]')?.querySelector('[data-lager-document-delete-confirm]'); const valid = input.value === 'DELETE'; if (button) { button.disabled = !valid; button.setAttribute('aria-disabled', String(!valid)); button.classList.toggle('sg-button--process-inactive', !valid); } });"; echo " document.addEventListener('click', async (event) => { const open = event.target.closest('[data-lager-document-delete]'); if (open) { const id = open.dataset.lagerDocumentDelete; const confirmation = open.closest('[data-lager-document-edit-row]')?.querySelector('[data-lager-document-confirmation=\"' + id + '\"]'); if (confirmation) { confirmation.hidden = false; } return; } const cancel = event.target.closest('[data-lager-document-delete-cancel]'); if (cancel) { const confirmation = cancel.closest('[data-lager-document-confirmation]'); if (confirmation) { confirmation.hidden = true; } return; } const confirm = event.target.closest('[data-lager-document-delete-confirm]'); if (!confirm || confirm.disabled) { return; } const confirmation = confirm.closest('[data-lager-document-confirmation]'); const id = confirm.dataset.lagerDocumentDeleteConfirm; confirm.disabled = true; try { await postLagerChange({ action: 'delete_document', documentId: id }); window.location.reload(); } catch (error) { confirm.disabled = false; const input = confirmation?.querySelector('[data-lager-document-delete-input]'); if (input) { input.setCustomValidity(error instanceof Error ? error.message : 'Dokument konnte nicht gelöscht werden'); input.reportValidity(); } } });"; diff --git a/public/assets/styles.portal.css b/public/assets/styles.portal.css index e1c5ba4..6c9b662 100644 --- a/public/assets/styles.portal.css +++ b/public/assets/styles.portal.css @@ -288,6 +288,12 @@ body.sg-lager-mobile-detail-open > :not([data-lager-mobile-detail-host]) { box-sizing: border-box; } +.sg-card-list-page-drawer [data-lager-document-upload-form] .sg-labeled-input-row .sg-label { + min-width: var(--layout-form-sections-label-column-width); + flex: 0 0 var(--layout-form-sections-label-column-width); + margin-right: var(--spacing-large); +} + .sg-card-list-page-drawer [data-lager-drawer-topic="new-lot-action"] { display: flex; align-items: center;