From d3bfaa4580e5704b9cee93ee81aada5af825a7c1 Mon Sep 17 00:00:00 2001 From: AIIrondev Date: Sun, 9 Aug 2026 21:15:13 +0200 Subject: [PATCH 01/56] update of an outadet approach of the user adding in the current version --- .scheduler_lock | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 .scheduler_lock diff --git a/.scheduler_lock b/.scheduler_lock new file mode 100644 index 0000000..e69de29 From 0cacfb0871b50f2d94b76c460567109be231921a Mon Sep 17 00:00:00 2001 From: AIIrondev Date: Sun, 9 Aug 2026 23:39:21 +0200 Subject: [PATCH 02/56] Quik and more or less temporary fix for the Issue #43 further changes are not required --- Web/templates/manage_filters.html | 48 ------------------------------- 1 file changed, 48 deletions(-) diff --git a/Web/templates/manage_filters.html b/Web/templates/manage_filters.html index d87d475..b7349a5 100755 --- a/Web/templates/manage_filters.html +++ b/Web/templates/manage_filters.html @@ -110,54 +110,6 @@ - - -
-
-
-

{{ filter_names.get('3', 'Typ/Art') }} (Filter 3)

-
-
-
-
- - -
-
- -
Vorhandene Werte
- {% if filter3_values %} -
- {% for value in filter3_values %} -
-
- {{ value }} -
- -
- -
-
-
- -
- {% endfor %} -
- {% else %} -
Keine Werte definiert.
- {% endif %} -
-
-
From c07d4e0bddf3b2e37d934a629a3cdac1307bbf9a Mon Sep 17 00:00:00 2001 From: AIIrondev Date: Sun, 9 Aug 2026 23:59:13 +0200 Subject: [PATCH 03/56] Changes to match the designated endpoint of the user when uploading an Item to dont have the system Jump between the systems. --- Web/app.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Web/app.py b/Web/app.py index b5fa923..c6482e6 100755 --- a/Web/app.py +++ b/Web/app.py @@ -5177,17 +5177,13 @@ def upload_item(): fs = get_gridfs() - can_access_admin_home = _page_access_allowed(permissions, 'home_admin') and _action_access_allowed(permissions, 'can_manage_settings') - if can_access_admin_home: - success_redirect_endpoint = 'home_admin' - elif cfg.MODULES.is_enabled('library') and _page_access_allowed(permissions, 'home_library'): - success_redirect_endpoint = 'home_library' + if cfg.MODULES.is_enabled('library') and sanitize_form_value(request.form.get('item_type_input', '')) != "other": + success_redirect_endpoint = 'library' else: success_redirect_endpoint = 'home_admin' # Detect if request is from mobile device is_mobile = 'Mobile' in request.headers.get('User-Agent', '') - is_ios = 'iPhone' in request.headers.get('User-Agent', '') or 'iPad' in request.headers.get('User-Agent', '') # Log mobile request for debugging if is_mobile: From 25f52eeeb53b1adbb0239cb7cedbc9e369e40d8f Mon Sep 17 00:00:00 2001 From: AIIrondev Date: Mon, 10 Aug 2026 00:24:05 +0200 Subject: [PATCH 04/56] Change sfor the issue #16 with some of the described changes as addtion of a arrow pointig down or the rearange ment done --- Web/templates/upload_admin.html | 107 ++++++++++++++++---------------- 1 file changed, 54 insertions(+), 53 deletions(-) diff --git a/Web/templates/upload_admin.html b/Web/templates/upload_admin.html index 441f8b8..37056ad 100755 --- a/Web/templates/upload_admin.html +++ b/Web/templates/upload_admin.html @@ -761,6 +761,21 @@

{{ page_title|default('Artikel hochladen') }}

+ + {% if show_library_features %} +
+ +
+ + + +
+ + Scannen oder manuell eingeben. Gültige ISBNs helfen beim Abruf von Buchdaten, andere Codes werden trotzdem akzeptiert. +
+
+ {% endif %} +
@@ -784,7 +799,44 @@
- + +
+ + +
+ + + +
+ +
+ + +
+ + +
+ +
+ + + Bei Anzahl > 1 können hier individuelle Codes pro Item gesetzt werden. Der Scanner setzt immer zuerst den Basis-Code im Feld oben; weitere Einzelcodes werden hier angehängt. +
+ {% if show_library_features %}
@@ -902,42 +954,6 @@
-
- - -
- - - -
- -
- - -
- - -
- -
- - - Bei Anzahl > 1 können hier individuelle Codes pro Item gesetzt werden. Der Scanner setzt immer zuerst den Basis-Code im Feld oben; weitere Einzelcodes werden hier angehängt. -
@@ -946,27 +962,12 @@
- - - {% if show_library_features %} -
- -
- - - -
- - Scannen oder manuell eingeben. Gültige ISBNs helfen beim Abruf von Buchdaten, andere Codes werden trotzdem akzeptiert. -
-
- +
Wenn deaktiviert, kann der Artikel nicht im Voraus reserviert werden (Sofort-Ausleihe bleibt möglich).
- {% endif %}
From a6b246a92bd4e074a5627e833d72be0ee9f7c0d2 Mon Sep 17 00:00:00 2001 From: AIIrondev Date: Mon, 10 Aug 2026 14:10:08 +0200 Subject: [PATCH 05/56] Refactor upload form fields for clarity and enhance focus navigation --- .scheduler_lock | 0 Web/templates/upload_admin.html | 70 ++++++++++++++++++++++++++++++--- 2 files changed, 65 insertions(+), 5 deletions(-) delete mode 100644 .scheduler_lock diff --git a/.scheduler_lock b/.scheduler_lock deleted file mode 100644 index e69de29..0000000 diff --git a/Web/templates/upload_admin.html b/Web/templates/upload_admin.html index 37056ad..f69ed54 100755 --- a/Web/templates/upload_admin.html +++ b/Web/templates/upload_admin.html @@ -812,12 +812,12 @@ @@ -852,9 +852,9 @@ Wählen Sie einen Medientyp aus zur Klassifizierung.
-

Kategorie/Typ:

+

Kategorie/Typ/Fach:

- + Geben Sie hier eine beliebige Kategorie ein zur freien Klassifizierung.
@@ -1024,6 +1024,66 @@ -

- Alle aufgeführten Codes gehören zu diesem Datensatz: +

+ Änderungen an Titel, Ort und Beschreibung werden auf alle Exemplare der Gruppe übertragen.

- - -
- +
- Hinweis: Änderungen an Titel/Ort/Beschreibung werden auf alle Exemplare der Range übertragen. + Hinweis: Jeder Mediencode wird einzeln gespeichert, damit alle Exemplare der Gruppe korrekt bleiben.
@@ -1597,8 +1636,8 @@
- - + +
@@ -1616,20 +1655,4 @@
- {% endblock %} \ No newline at end of file From a11bce17c53a3f21841ac7ac75c82f3dbb06b53a Mon Sep 17 00:00:00 2001 From: AIIrondev Date: Tue, 11 Aug 2026 00:31:57 +0200 Subject: [PATCH 12/56] Fixy of the api_library_items() --- Web/app.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Web/app.py b/Web/app.py index e29fa2c..1869a10 100755 --- a/Web/app.py +++ b/Web/app.py @@ -3314,7 +3314,6 @@ def api_library_items(): query = { 'ItemType': {'$in': ['book', 'cd', 'dvd', 'schoolbook', 'schulbuch', 'Buch', 'Schulbuch']}, - 'IsGroupedSubItem': {'$ne': True}, 'Deleted': {'$ne': True} } From e9006f5a0761a23929876da552dc7673dcd19323 Mon Sep 17 00:00:00 2001 From: AIIrondev Date: Tue, 11 Aug 2026 00:38:48 +0200 Subject: [PATCH 13/56] Fix of an missing parethasys --- Web/templates/library_table.html | 1059 +++++++++++++++--------------- 1 file changed, 533 insertions(+), 526 deletions(-) diff --git a/Web/templates/library_table.html b/Web/templates/library_table.html index 3a9c40b..0651bf9 100644 --- a/Web/templates/library_table.html +++ b/Web/templates/library_table.html @@ -1040,557 +1040,564 @@ setScanStatus(`Ausweis gesetzt: ${activeStudentCardId}`, 'ok'); return; } - - async function returnByCode(code) { - if (!code) return; - setScanStatus('Verarbeite Rückgabe...', 'warn'); - try { - const resp = await fetch('/api/library_return_by_code', { - method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({ item_code: code }) - }); - const result = await resp.json(); - if (!resp.ok || !result.ok) { - setScanStatus(result.message || 'Rückgabe fehlgeschlagen.', 'error'); - showSmallConfirm(result.message || 'Rückgabe fehlgeschlagen.', 'error'); - return false; - } - setScanStatus(result.message || `Zurückgegeben: ${result.item_name || ''}`, 'ok'); - showSmallConfirm(result.message || `Zurückgegeben: ${result.item_name || ''}`, 'ok'); - await loadLibraryItems(); - return true; - } catch (err) { - console.error('Return by code failed:', err); - setScanStatus('Fehler bei Rückgabe.', 'error'); - showSmallConfirm('Fehler bei Rückgabe.', 'error'); - return false; - } - } - try { - setScanStatus('Verarbeite Mediencode...', 'warn'); - const response = await fetch('/api/library_scan_action', { - method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({ - student_card_id: activeStudentCardId, - item_code: scannedCode - }) - }); - - const result = await response.json(); - if (!response.ok || !result.ok) { - setScanStatus(result.message || 'Scan-Aktion fehlgeschlagen.', 'error'); - return; - } - - if (result.action === 'borrowed') { - setScanStatus(`Ausgeliehen: ${result.item_name}`, 'ok'); - showSmallConfirm(`Ausgeliehen: ${result.item_name}`, 'ok'); - } else if (result.action === 'returned') { - setScanStatus(`Zurückgegeben: ${result.item_name}`, 'ok'); - showSmallConfirm(`Zurückgegeben: ${result.item_name}`, 'ok'); - } else { - setScanStatus(result.message || 'Aktion durchgeführt.', 'ok'); - showSmallConfirm(result.message || 'Aktion durchgeführt.', 'ok'); - } - - await loadLibraryItems(); - } catch (err) { - console.error('Quick scan action failed:', err); - setScanStatus('Fehler beim Verarbeiten des Scans.', 'error'); - } - } - function scanIntoEditCode() { - const scanReaderWrap = document.getElementById('scanReaderWrap'); - const editCodeInput = document.getElementById('edit-code4'); - const scanEditBtn = document.getElementById('scan-edit-code-btn'); - if (!scanReaderWrap || !editCodeInput || !scanEditBtn) { - return; - } - - if (scannerRunning && scanReaderWrap.style.display !== 'none') { - stopScanner(); - scanEditBtn.textContent = 'Barcode scannen'; - return; - } - - scanEditBtn.textContent = 'Scanner schließen'; - - startScanner(function(decodedText) { - editCodeInput.value = decodedText; - if(typeof validateCodeField === "function") { - validateCodeField(editCodeInput, document.getElementById('edit-item-id')?.value || null); - } - scanEditBtn.textContent = 'Barcode scannen'; - }); - } - - // ========================================================================= - // 4. UI INTERACTIONS & UTILITIES - // ========================================================================= - function borrowItem(itemId) { - const selectedItem = (libraryItems || []).find(item => item._id === itemId); - if (selectedItem && selectedItem.LibraryDisplayStatus === 'damaged') { - alert('Dieses Medium ist als defekt/zerstört markiert und kann nicht ausgeliehen werden.'); - return; - } - - const defaultCardId = activeStudentCardId || ''; - const cardId = (window.prompt('Bitte Schülerausweis-ID eingeben:', defaultCardId) || '').trim().toUpperCase(); - if (!cardId) { - alert('Ausleihe abgebrochen: Für Bibliotheksmedien ist eine gültige Schülerausweis-ID erforderlich.'); - return; - } - - setActiveStudentCard(cardId); - - const durationInput = (window.prompt('Ausleihdauer in Tagen (optional):') || '').trim(); - const maxAvailable = Math.max(1, parseInt(selectedItem?.AvailableGroupedCount || selectedItem?.Quantity || 1, 10) || 1); - const countPrompt = (window.prompt(`Anzahl ausleihen? (Standard: 1, verfügbar: ${maxAvailable})`, '1') || '').trim(); - let borrowCount = parseInt(countPrompt || '1', 10); - if (!Number.isFinite(borrowCount) || borrowCount < 1) { - borrowCount = 1; - } - if (borrowCount > maxAvailable) { - alert(`Es sind nur ${maxAvailable} Exemplar(e) verfügbar.`); - return; - } - - const form = document.createElement('form'); - form.method = 'POST'; - form.action = `/ausleihen/${itemId}`; - - const csrfField = document.createElement('input'); - csrfField.type = 'hidden'; - csrfField.name = 'csrf_token'; - csrfField.value = '{{ csrf_token }}'; - form.appendChild(csrfField); - - const cardField = document.createElement('input'); - cardField.type = 'hidden'; - cardField.name = 'borrower_card_id'; - cardField.value = cardId; - form.appendChild(cardField); - - const returnTargetField = document.createElement('input'); - returnTargetField.type = 'hidden'; - returnTargetField.name = 'return_to'; - returnTargetField.value = 'library'; - form.appendChild(returnTargetField); - - if (durationInput) { - const durationField = document.createElement('input'); - durationField.type = 'hidden'; - durationField.name = 'borrow_duration_days'; - durationField.value = durationInput; - form.appendChild(durationField); - } - - const countField = document.createElement('input'); - countField.type = 'hidden'; - countField.name = 'exemplare_count'; - countField.value = String(borrowCount || 1); - form.appendChild(countField); - - document.body.appendChild(form); - form.submit(); - } - - function setScanStatus(message, kind = '') { - const el = document.getElementById('scanStatus'); - if (!el) return; - el.textContent = message; - el.classList.remove('ok', 'warn', 'error'); - if (kind) el.classList.add(kind); - } - - function showSmallConfirm(message, kind='ok') { - // Append the small helper text in German - const helper = 'Sie können fortfahren. Dies ist nur eine kleine Benachrichtigung.'; - const el = document.createElement('div'); - el.className = `small-popup ${kind === 'error' ? 'error' : 'ok'}`; - el.innerHTML = `
${escapeHtml(String(message || ''))}
${helper}
×
`; - document.body.appendChild(el); - // close handler - el.querySelector('.close-x').addEventListener('click', () => { - if (el && el.parentNode) el.parentNode.removeChild(el); - }); - // auto remove after 3 seconds - setTimeout(() => { try { if (el && el.parentNode) el.parentNode.removeChild(el); } catch(e){} }, 3000); - } - - function setActiveStudentCard(cardId) { - activeStudentCardId = (cardId || '').trim().toUpperCase(); - const input = document.getElementById('activeStudentCard'); - if (input) input.value = activeStudentCardId; - } - - function normalizeScannedCode(code) { - return (code || '').trim(); - } - - function getItemTypeLabel(type) { - const labels = { 'book': 'Buch', 'cd': 'CD', 'dvd': 'DVD', 'other': 'Sonstige', 'schoolbook': 'Schulbuch' }; - return labels[type] || type; - } - - function escapeHtml(text) { - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; - } - - // Opens the modal and fetches the data - function showItemDetail(itemId) { - const detailContent = document.getElementById('detailContent'); - const detailModal = document.getElementById('detailModal'); - - // 1. Show the loading state immediately - detailContent.innerHTML = '

Loading details...

'; - detailModal.style.display = 'flex'; - - // 2. Fetch the data - fetch(`/api/item_detail/${itemId}`) - .then(response => { - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); + async function returnByCode(code) { + if (!code) return; + setScanStatus('Verarbeite Rückgabe...', 'warn'); + try { + const resp = await fetch('/api/library_return_by_code', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({item_code: code}) + }); + const result = await resp.json(); + if (!resp.ok || !result.ok) { + setScanStatus(result.message || 'Rückgabe fehlgeschlagen.', 'error'); + showSmallConfirm(result.message || 'Rückgabe fehlgeschlagen.', 'error'); + return false; + } + setScanStatus(result.message || `Zurückgegeben: ${result.item_name || ''}`, 'ok'); + showSmallConfirm(result.message || `Zurückgegeben: ${result.item_name || ''}`, 'ok'); + await loadLibraryItems(); + return true; + } catch (err) { + console.error('Return by code failed:', err); + setScanStatus('Fehler bei Rückgabe.', 'error'); + showSmallConfirm('Fehler bei Rückgabe.', 'error'); + return false; } - return response.text(); - }) - .then(html => { - // 3. Clean the HTML and display it - detailContent.innerHTML = DOMPurify.sanitize(html); - }) - .catch(err => { - console.error('Error loading detail:', err); - detailContent.innerHTML = '

Sorry, we could not load the item details. Please try again later.

'; - }); - } + } - // Closes the modal via the 'x' button - function closeDetailModal() { - document.getElementById('detailModal').style.display = 'none'; - } + try { + setScanStatus('Verarbeite Mediencode...', 'warn'); + const response = await fetch('/api/library_scan_action', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + student_card_id: activeStudentCardId, + item_code: scannedCode + }) + }); - // Closes the modal if the user clicks the dark background overlay - window.onclick = function(event) { - const modal = document.getElementById('detailModal'); - // If the click happened directly on the dark overlay (not the white box) - if (event.target === modal) { - closeDetailModal(); - } - }; - - // ========================================================================= - // 5. EVENT LISTENERS INITIALIZATION & ON-LOAD INITIALIZER - // ========================================================================= - function wireScannerUi() { - const toggleBtn = document.getElementById('toggleScannerBtn'); - const resetBtn = document.getElementById('resetCardBtn'); - const modeSelect = document.getElementById('scanModeSelect'); - const keyboardToggle = document.getElementById('keyboardScannerToggle'); - - if (toggleBtn) { - toggleBtn.addEventListener('click', async () => { - if (scannerRunning) { - await stopScanner(); - } else { - await startScanner(null); - } - }); - } - - if (resetBtn) { - resetBtn.addEventListener('click', () => { - setActiveStudentCard(''); - setScanStatus('Ausweis zurückgesetzt. Bitte neu scannen.', 'warn'); - }); - } - - if (modeSelect) { - modeSelect.addEventListener('change', () => { - if (modeSelect.value === 'quick_toggle' && !activeStudentCardId) { - setScanStatus('Schnellmodus: zuerst Schülerausweis scannen.', 'warn'); - } else if (modeSelect.value === 'card_only') { - setScanStatus('Nur Ausweis-Modus aktiv.', 'warn'); - } - }); - } - - if (keyboardToggle) { - keyboardToggle.addEventListener('change', () => { - keyboardScannerEnabled = !!keyboardToggle.checked; - if (keyboardScannerEnabled) { - document.addEventListener('keydown', keyboardScanKeydownHandler); - setScanStatus('Physischer Scanner aktiv (Schnellmodus empfohlen).', 'ok'); - } else { - document.removeEventListener('keydown', keyboardScanKeydownHandler); - setScanStatus('Physischer Scanner deaktiviert.', 'warn'); - } - }); - } - } - - // Run when DOM structure is entirely ready - document.addEventListener('DOMContentLoaded', () => { - wireScannerUi(); // Setup scanner control buttons - loadLibraryItems(); // Fetch your database items right away! - - // Safely connect standard Filters and Search inputs inside DOMContentLoaded - const filterToggleBtn = document.getElementById('filterToggleBtn'); - if (filterToggleBtn) { - filterToggleBtn.addEventListener('click', () => { - filterPanelOpen = !filterPanelOpen; - document.getElementById('filterPanel').classList.toggle('open'); - filterToggleBtn.classList.toggle('active'); - }); - } - - const applyFilterBtn = document.getElementById('applyFilterBtn'); - if (applyFilterBtn) { - applyFilterBtn.addEventListener('click', () => { - activeFilters.isbn = document.getElementById('filterISBN').value.toLowerCase(); - activeFilters.type = document.getElementById('filterType').value; - activeFilters.status = document.getElementById('filterStatus').value; - applyFiltersAndSearch(true); - }); - } - - const clearFilterBtn = document.getElementById('clearFilterBtn'); - if (clearFilterBtn) { - clearFilterBtn.addEventListener('click', () => { - document.getElementById('filterISBN').value = ''; - document.getElementById('filterType').value = ''; - document.getElementById('filterStatus').value = ''; - activeFilters = { isbn: '', type: '', status: '' }; - applyFiltersAndSearch(true); - }); - } - - const librarySearch = document.getElementById('librarySearch'); - if (librarySearch) { - librarySearch.addEventListener('input', (e) => { - currentSearchTerm = (e.target.value || '').toLowerCase(); - applyFiltersAndSearch(true); - }); - } - - const loadMoreBtn = document.getElementById('loadMoreBtn'); - if (loadMoreBtn) { - loadMoreBtn.addEventListener('click', () => { - renderedCount = Math.min(renderedCount + RENDER_BATCH_COUNT, visibleItems.length); - renderItems(); - }); - } - - const manualReturnBtn = document.getElementById('manualReturnBtn'); - const manualItemCode = document.getElementById('manualItemCode'); - if (manualReturnBtn && manualItemCode) { - manualReturnBtn.addEventListener('click', async () => { - const code = (manualItemCode.value || '').trim(); - if (!code) { - alert('Bitte einen Mediencode eingeben.'); + const result = await response.json(); + if (!response.ok || !result.ok) { + setScanStatus(result.message || 'Scan-Aktion fehlgeschlagen.', 'error'); return; } - await returnByCode(code); - }); - } - const editForm = document.getElementById('editLibraryForm'); - if (editForm) { - editForm.addEventListener('submit', async function(e) { - e.preventDefault(); - - const itemId = document.getElementById('editLibraryItemId').value; - const currentItem = libraryItems.find(i => i._id === itemId); - if (!currentItem) return; - - const sharedPayload = { - name: document.getElementById('editLibraryName').value, - item_type: document.getElementById('editLibraryType').value, - isbn: document.getElementById('editLibraryIsbn').value, - ort: document.getElementById('editLibraryLocation').value, - beschreibung: document.getElementById('editLibraryDescription').value, - ansch_jahr: currentItem.Anschaffungsjahr || '', - ansch_kost: currentItem.Anschaffungskosten || '', - reservierbar: currentItem.Reservierbar !== false, - }; - - const codeInputs = Array.from(document.querySelectorAll('#editLibraryCodesContainer input[data-item-id]')); - const codeByItemId = new Map(codeInputs.map(input => [input.dataset.itemId, (input.value || '').trim()])); - const groupMembers = editLibraryState.groupMembers.length > 0 ? editLibraryState.groupMembers : [currentItem]; - const isGroupedEdit = Boolean(currentItem.SeriesGroupId) && groupMembers.length > 1; - - try { - if (isGroupedEdit) { - const payload = { - series_group_id: currentItem.SeriesGroupId, - ...sharedPayload, - items: groupMembers.map(member => ({ - id: member._id, - code_4: codeByItemId.get(member._id) || '' - })) - }; - - const response = await fetch('/update_group', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': '{{ csrf_token }}', - 'X-CSRF-Token': '{{ csrf_token }}' - }, - body: JSON.stringify(payload) - }); - - const result = await response.json(); - if (response.ok && result.success) { - alert(result.message || 'Gruppe erfolgreich aktualisiert!'); - closeEditLibraryModal(); - pagingState.loading = false; - await loadLibraryItems(); - } else { - alert(result.message || 'Fehler beim Speichern der Gruppenänderungen.'); - } - } else { - const primaryCodeInput = codeInputs[0]; - const payload = { - name: sharedPayload.name, - item_type: sharedPayload.item_type, - isbn: sharedPayload.isbn, - code_4: primaryCodeInput ? primaryCodeInput.value.trim() : (currentItem.Code_4 || currentItem.Code4 || '').trim(), - ort: sharedPayload.ort, - beschreibung: sharedPayload.beschreibung - }; - - const response = await fetch(`/api/library_item/${itemId}/update`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': '{{ csrf_token }}', - 'X-CSRF-Token': '{{ csrf_token }}' - }, - body: JSON.stringify(payload) - }); - - const result = await response.json(); - if (response.ok && result.ok) { - alert(result.message || 'Medium erfolgreich aktualisiert!'); - closeEditLibraryModal(); - pagingState.loading = false; - await loadLibraryItems(); - } else { - alert(result.message || 'Fehler beim Speichern der Änderungen.'); - } - } - } catch (error) { - console.error('Update failed:', error); - alert('Netzwerkfehler beim Aktualisieren des Mediums.'); + if (result.action === 'borrowed') { + setScanStatus(`Ausgeliehen: ${result.item_name}`, 'ok'); + showSmallConfirm(`Ausgeliehen: ${result.item_name}`, 'ok'); + } else if (result.action === 'returned') { + setScanStatus(`Zurückgegeben: ${result.item_name}`, 'ok'); + showSmallConfirm(`Zurückgegeben: ${result.item_name}`, 'ok'); + } else { + setScanStatus(result.message || 'Aktion durchgeführt.', 'ok'); + showSmallConfirm(result.message || 'Aktion durchgeführt.', 'ok'); } + + await loadLibraryItems(); + } catch (err) { + console.error('Quick scan action failed:', err); + setScanStatus('Fehler beim Verarbeiten des Scans.', 'error'); + } + } + + function scanIntoEditCode() { + const scanReaderWrap = document.getElementById('scanReaderWrap'); + const editCodeInput = document.getElementById('edit-code4'); + const scanEditBtn = document.getElementById('scan-edit-code-btn'); + if (!scanReaderWrap || !editCodeInput || !scanEditBtn) { + return; + } + + if (scannerRunning && scanReaderWrap.style.display !== 'none') { + stopScanner(); + scanEditBtn.textContent = 'Barcode scannen'; + return; + } + + scanEditBtn.textContent = 'Scanner schließen'; + + startScanner(function (decodedText) { + editCodeInput.value = decodedText; + if (typeof validateCodeField === "function") { + validateCodeField(editCodeInput, document.getElementById('edit-item-id')?.value || null); + } + scanEditBtn.textContent = 'Barcode scannen'; }); } - }); - async function fetchLibraryGroupMembers(seriesGroupId) { - if (!seriesGroupId) return []; - - try { - const response = await fetch(`/api/library_group/${encodeURIComponent(seriesGroupId)}`); - if (!response.ok) { - throw new Error(`HTTP ${response.status}`); + // ========================================================================= + // 4. UI INTERACTIONS & UTILITIES + // ========================================================================= + function borrowItem(itemId) { + const selectedItem = (libraryItems || []).find(item => item._id === itemId); + if (selectedItem && selectedItem.LibraryDisplayStatus === 'damaged') { + alert('Dieses Medium ist als defekt/zerstört markiert und kann nicht ausgeliehen werden.'); + return; } - const payload = await response.json(); - return Array.isArray(payload.items) ? payload.items : []; - } catch (error) { - console.warn('Falling back to loaded library items for group editing:', error); - return (libraryItems || []).filter(item => item.SeriesGroupId === seriesGroupId); - } - } - - function renderLibraryGroupCodeFields(groupMembers, currentItemId) { - const codesContainer = document.getElementById('editLibraryCodesContainer'); - const groupWarning = document.getElementById('editLibraryGroupWarning'); - const groupCount = document.getElementById('editLibraryGroupCount'); - const groupHint = document.getElementById('editLibraryGroupHint'); - - if (!codesContainer) return; - - const items = Array.isArray(groupMembers) ? groupMembers.slice() : []; - items.sort((a, b) => (a.SeriesPosition || 0) - (b.SeriesPosition || 0) || String(a.Name || '').localeCompare(String(b.Name || ''))); - - editLibraryState.groupMembers = items; - - if (groupWarning) { - groupWarning.style.display = items.length > 1 ? 'block' : 'none'; - } - if (groupCount) { - const totalCount = items.length || 1; - const declaredCount = items[0]?.SeriesCount || totalCount; - groupCount.textContent = `${totalCount} / ${declaredCount}`; - } - if (groupHint) { - groupHint.textContent = items.length > 1 - ? 'Jeder Code gehört zu einem eigenen Exemplar. Änderungen werden für alle Codes gespeichert.' - : 'Einzelnes Exemplar. Der Code wird direkt gespeichert.'; - } - - if (!items.length) { - codesContainer.innerHTML = '
Keine Codes geladen.
'; - return; - } - - codesContainer.innerHTML = items.map((member, index) => { - const codeValue = member.Code_4 || member.Code4 || ''; - const labelParts = []; - if (member.SeriesPosition !== undefined && member.SeriesPosition !== null) { - labelParts.push(`Exemplar ${member.SeriesPosition}`); - } else { - labelParts.push(`Exemplar ${index + 1}`); + const defaultCardId = activeStudentCardId || ''; + const cardId = (window.prompt('Bitte Schülerausweis-ID eingeben:', defaultCardId) || '').trim().toUpperCase(); + if (!cardId) { + alert('Ausleihe abgebrochen: Für Bibliotheksmedien ist eine gültige Schülerausweis-ID erforderlich.'); + return; } - if (member._id === currentItemId) { - labelParts.push('aktuelles Medium'); + + setActiveStudentCard(cardId); + + const durationInput = (window.prompt('Ausleihdauer in Tagen (optional):') || '').trim(); + const maxAvailable = Math.max(1, parseInt(selectedItem?.AvailableGroupedCount || selectedItem?.Quantity || 1, 10) || 1); + const countPrompt = (window.prompt(`Anzahl ausleihen? (Standard: 1, verfügbar: ${maxAvailable})`, '1') || '').trim(); + let borrowCount = parseInt(countPrompt || '1', 10); + if (!Number.isFinite(borrowCount) || borrowCount < 1) { + borrowCount = 1; } - return ` + if (borrowCount > maxAvailable) { + alert(`Es sind nur ${maxAvailable} Exemplar(e) verfügbar.`); + return; + } + + const form = document.createElement('form'); + form.method = 'POST'; + form.action = `/ausleihen/${itemId}`; + + const csrfField = document.createElement('input'); + csrfField.type = 'hidden'; + csrfField.name = 'csrf_token'; + csrfField.value = '{{ csrf_token }}'; + form.appendChild(csrfField); + + const cardField = document.createElement('input'); + cardField.type = 'hidden'; + cardField.name = 'borrower_card_id'; + cardField.value = cardId; + form.appendChild(cardField); + + const returnTargetField = document.createElement('input'); + returnTargetField.type = 'hidden'; + returnTargetField.name = 'return_to'; + returnTargetField.value = 'library'; + form.appendChild(returnTargetField); + + if (durationInput) { + const durationField = document.createElement('input'); + durationField.type = 'hidden'; + durationField.name = 'borrow_duration_days'; + durationField.value = durationInput; + form.appendChild(durationField); + } + + const countField = document.createElement('input'); + countField.type = 'hidden'; + countField.name = 'exemplare_count'; + countField.value = String(borrowCount || 1); + form.appendChild(countField); + + document.body.appendChild(form); + form.submit(); + } + + function setScanStatus(message, kind = '') { + const el = document.getElementById('scanStatus'); + if (!el) return; + el.textContent = message; + el.classList.remove('ok', 'warn', 'error'); + if (kind) el.classList.add(kind); + } + + function showSmallConfirm(message, kind = 'ok') { + // Append the small helper text in German + const helper = 'Sie können fortfahren. Dies ist nur eine kleine Benachrichtigung.'; + const el = document.createElement('div'); + el.className = `small-popup ${kind === 'error' ? 'error' : 'ok'}`; + el.innerHTML = `
${escapeHtml(String(message || ''))}
${helper}
×
`; + document.body.appendChild(el); + // close handler + el.querySelector('.close-x').addEventListener('click', () => { + if (el && el.parentNode) el.parentNode.removeChild(el); + }); + // auto remove after 3 seconds + setTimeout(() => { + try { + if (el && el.parentNode) el.parentNode.removeChild(el); + } catch (e) { + } + }, 3000); + } + + function setActiveStudentCard(cardId) { + activeStudentCardId = (cardId || '').trim().toUpperCase(); + const input = document.getElementById('activeStudentCard'); + if (input) input.value = activeStudentCardId; + } + + function normalizeScannedCode(code) { + return (code || '').trim(); + } + + function getItemTypeLabel(type) { + const labels = {'book': 'Buch', 'cd': 'CD', 'dvd': 'DVD', 'other': 'Sonstige', 'schoolbook': 'Schulbuch'}; + return labels[type] || type; + } + + function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } + + // Opens the modal and fetches the data + function showItemDetail(itemId) { + const detailContent = document.getElementById('detailContent'); + const detailModal = document.getElementById('detailModal'); + + // 1. Show the loading state immediately + detailContent.innerHTML = '

Loading details...

'; + detailModal.style.display = 'flex'; + + // 2. Fetch the data + fetch(`/api/item_detail/${itemId}`) + .then(response => { + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return response.text(); + }) + .then(html => { + // 3. Clean the HTML and display it + detailContent.innerHTML = DOMPurify.sanitize(html); + }) + .catch(err => { + console.error('Error loading detail:', err); + detailContent.innerHTML = '

Sorry, we could not load the item details. Please try again later.

'; + }); + } + + // Closes the modal via the 'x' button + function closeDetailModal() { + document.getElementById('detailModal').style.display = 'none'; + } + + // Closes the modal if the user clicks the dark background overlay + window.onclick = function (event) { + const modal = document.getElementById('detailModal'); + // If the click happened directly on the dark overlay (not the white box) + if (event.target === modal) { + closeDetailModal(); + } + }; + + // ========================================================================= + // 5. EVENT LISTENERS INITIALIZATION & ON-LOAD INITIALIZER + // ========================================================================= + function wireScannerUi() { + const toggleBtn = document.getElementById('toggleScannerBtn'); + const resetBtn = document.getElementById('resetCardBtn'); + const modeSelect = document.getElementById('scanModeSelect'); + const keyboardToggle = document.getElementById('keyboardScannerToggle'); + + if (toggleBtn) { + toggleBtn.addEventListener('click', async () => { + if (scannerRunning) { + await stopScanner(); + } else { + await startScanner(null); + } + }); + } + + if (resetBtn) { + resetBtn.addEventListener('click', () => { + setActiveStudentCard(''); + setScanStatus('Ausweis zurückgesetzt. Bitte neu scannen.', 'warn'); + }); + } + + if (modeSelect) { + modeSelect.addEventListener('change', () => { + if (modeSelect.value === 'quick_toggle' && !activeStudentCardId) { + setScanStatus('Schnellmodus: zuerst Schülerausweis scannen.', 'warn'); + } else if (modeSelect.value === 'card_only') { + setScanStatus('Nur Ausweis-Modus aktiv.', 'warn'); + } + }); + } + + if (keyboardToggle) { + keyboardToggle.addEventListener('change', () => { + keyboardScannerEnabled = !!keyboardToggle.checked; + if (keyboardScannerEnabled) { + document.addEventListener('keydown', keyboardScanKeydownHandler); + setScanStatus('Physischer Scanner aktiv (Schnellmodus empfohlen).', 'ok'); + } else { + document.removeEventListener('keydown', keyboardScanKeydownHandler); + setScanStatus('Physischer Scanner deaktiviert.', 'warn'); + } + }); + } + } + + // Run when DOM structure is entirely ready + document.addEventListener('DOMContentLoaded', () => { + wireScannerUi(); // Setup scanner control buttons + loadLibraryItems(); // Fetch your database items right away! + + // Safely connect standard Filters and Search inputs inside DOMContentLoaded + const filterToggleBtn = document.getElementById('filterToggleBtn'); + if (filterToggleBtn) { + filterToggleBtn.addEventListener('click', () => { + filterPanelOpen = !filterPanelOpen; + document.getElementById('filterPanel').classList.toggle('open'); + filterToggleBtn.classList.toggle('active'); + }); + } + + const applyFilterBtn = document.getElementById('applyFilterBtn'); + if (applyFilterBtn) { + applyFilterBtn.addEventListener('click', () => { + activeFilters.isbn = document.getElementById('filterISBN').value.toLowerCase(); + activeFilters.type = document.getElementById('filterType').value; + activeFilters.status = document.getElementById('filterStatus').value; + applyFiltersAndSearch(true); + }); + } + + const clearFilterBtn = document.getElementById('clearFilterBtn'); + if (clearFilterBtn) { + clearFilterBtn.addEventListener('click', () => { + document.getElementById('filterISBN').value = ''; + document.getElementById('filterType').value = ''; + document.getElementById('filterStatus').value = ''; + activeFilters = {isbn: '', type: '', status: ''}; + applyFiltersAndSearch(true); + }); + } + + const librarySearch = document.getElementById('librarySearch'); + if (librarySearch) { + librarySearch.addEventListener('input', (e) => { + currentSearchTerm = (e.target.value || '').toLowerCase(); + applyFiltersAndSearch(true); + }); + } + + const loadMoreBtn = document.getElementById('loadMoreBtn'); + if (loadMoreBtn) { + loadMoreBtn.addEventListener('click', () => { + renderedCount = Math.min(renderedCount + RENDER_BATCH_COUNT, visibleItems.length); + renderItems(); + }); + } + + const manualReturnBtn = document.getElementById('manualReturnBtn'); + const manualItemCode = document.getElementById('manualItemCode'); + if (manualReturnBtn && manualItemCode) { + manualReturnBtn.addEventListener('click', async () => { + const code = (manualItemCode.value || '').trim(); + if (!code) { + alert('Bitte einen Mediencode eingeben.'); + return; + } + await returnByCode(code); + }); + } + + const editForm = document.getElementById('editLibraryForm'); + if (editForm) { + editForm.addEventListener('submit', async function (e) { + e.preventDefault(); + + const itemId = document.getElementById('editLibraryItemId').value; + const currentItem = libraryItems.find(i => i._id === itemId); + if (!currentItem) return; + + const sharedPayload = { + name: document.getElementById('editLibraryName').value, + item_type: document.getElementById('editLibraryType').value, + isbn: document.getElementById('editLibraryIsbn').value, + ort: document.getElementById('editLibraryLocation').value, + beschreibung: document.getElementById('editLibraryDescription').value, + ansch_jahr: currentItem.Anschaffungsjahr || '', + ansch_kost: currentItem.Anschaffungskosten || '', + reservierbar: currentItem.Reservierbar !== false, + }; + + const codeInputs = Array.from(document.querySelectorAll('#editLibraryCodesContainer input[data-item-id]')); + const codeByItemId = new Map(codeInputs.map(input => [input.dataset.itemId, (input.value || '').trim()])); + const groupMembers = editLibraryState.groupMembers.length > 0 ? editLibraryState.groupMembers : [currentItem]; + const isGroupedEdit = Boolean(currentItem.SeriesGroupId) && groupMembers.length > 1; + + try { + if (isGroupedEdit) { + const payload = { + series_group_id: currentItem.SeriesGroupId, + ...sharedPayload, + items: groupMembers.map(member => ({ + id: member._id, + code_4: codeByItemId.get(member._id) || '' + })) + }; + + const response = await fetch('/update_group', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': '{{ csrf_token }}', + 'X-CSRF-Token': '{{ csrf_token }}' + }, + body: JSON.stringify(payload) + }); + + const result = await response.json(); + if (response.ok && result.success) { + alert(result.message || 'Gruppe erfolgreich aktualisiert!'); + closeEditLibraryModal(); + pagingState.loading = false; + await loadLibraryItems(); + } else { + alert(result.message || 'Fehler beim Speichern der Gruppenänderungen.'); + } + } else { + const primaryCodeInput = codeInputs[0]; + const payload = { + name: sharedPayload.name, + item_type: sharedPayload.item_type, + isbn: sharedPayload.isbn, + code_4: primaryCodeInput ? primaryCodeInput.value.trim() : (currentItem.Code_4 || currentItem.Code4 || '').trim(), + ort: sharedPayload.ort, + beschreibung: sharedPayload.beschreibung + }; + + const response = await fetch(`/api/library_item/${itemId}/update`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': '{{ csrf_token }}', + 'X-CSRF-Token': '{{ csrf_token }}' + }, + body: JSON.stringify(payload) + }); + + const result = await response.json(); + if (response.ok && result.ok) { + alert(result.message || 'Medium erfolgreich aktualisiert!'); + closeEditLibraryModal(); + pagingState.loading = false; + await loadLibraryItems(); + } else { + alert(result.message || 'Fehler beim Speichern der Änderungen.'); + } + } + } catch (error) { + console.error('Update failed:', error); + alert('Netzwerkfehler beim Aktualisieren des Mediums.'); + } + }); + } + }); + + async function fetchLibraryGroupMembers(seriesGroupId) { + if (!seriesGroupId) return []; + + try { + const response = await fetch(`/api/library_group/${encodeURIComponent(seriesGroupId)}`); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + + const payload = await response.json(); + return Array.isArray(payload.items) ? payload.items : []; + } catch (error) { + console.warn('Falling back to loaded library items for group editing:', error); + return (libraryItems || []).filter(item => item.SeriesGroupId === seriesGroupId); + } + } + + function renderLibraryGroupCodeFields(groupMembers, currentItemId) { + const codesContainer = document.getElementById('editLibraryCodesContainer'); + const groupWarning = document.getElementById('editLibraryGroupWarning'); + const groupCount = document.getElementById('editLibraryGroupCount'); + const groupHint = document.getElementById('editLibraryGroupHint'); + + if (!codesContainer) return; + + const items = Array.isArray(groupMembers) ? groupMembers.slice() : []; + items.sort((a, b) => (a.SeriesPosition || 0) - (b.SeriesPosition || 0) || String(a.Name || '').localeCompare(String(b.Name || ''))); + + editLibraryState.groupMembers = items; + + if (groupWarning) { + groupWarning.style.display = items.length > 1 ? 'block' : 'none'; + } + if (groupCount) { + const totalCount = items.length || 1; + const declaredCount = items[0]?.SeriesCount || totalCount; + groupCount.textContent = `${totalCount} / ${declaredCount}`; + } + if (groupHint) { + groupHint.textContent = items.length > 1 + ? 'Jeder Code gehört zu einem eigenen Exemplar. Änderungen werden für alle Codes gespeichert.' + : 'Einzelnes Exemplar. Der Code wird direkt gespeichert.'; + } + + if (!items.length) { + codesContainer.innerHTML = '
Keine Codes geladen.
'; + return; + } + + codesContainer.innerHTML = items.map((member, index) => { + const codeValue = member.Code_4 || member.Code4 || ''; + const labelParts = []; + if (member.SeriesPosition !== undefined && member.SeriesPosition !== null) { + labelParts.push(`Exemplar ${member.SeriesPosition}`); + } else { + labelParts.push(`Exemplar ${index + 1}`); + } + if (member._id === currentItemId) { + labelParts.push('aktuelles Medium'); + } + return `
`; - }).join(''); - } - - window.openEditLibraryItem = async function(itemId) { - const item = libraryItems.find(i => i._id === itemId); - if (!item) return; - - editLibraryState.itemId = item._id; - editLibraryState.seriesGroupId = item.SeriesGroupId || ''; - editLibraryState.groupMembers = []; - - // 1. Felder befüllen - document.getElementById('editLibraryItemId').value = item._id; - document.getElementById('editLibraryName').value = item.Name; - document.getElementById('editLibraryType').value = item.ItemType; - document.getElementById('editLibraryIsbn').value = item.ISBN || ''; - document.getElementById('editLibraryLocation').value = item.Ort; - document.getElementById('editLibraryDescription').value = item.Beschreibung; - - const codesContainer = document.getElementById('editLibraryCodesContainer'); - if (codesContainer) { - codesContainer.innerHTML = '
Lade Codes...
'; + }).join(''); } - const groupMembers = item.SeriesGroupId ? await fetchLibraryGroupMembers(item.SeriesGroupId) : [item]; - renderLibraryGroupCodeFields(groupMembers.length > 0 ? groupMembers : [item], item._id); + window.openEditLibraryItem = async function (itemId) { + const item = libraryItems.find(i => i._id === itemId); + if (!item) return; - document.getElementById('editLibraryModal').style.display = 'flex'; - }; + editLibraryState.itemId = item._id; + editLibraryState.seriesGroupId = item.SeriesGroupId || ''; + editLibraryState.groupMembers = []; - function closeEditLibraryModal() { - document.getElementById('editLibraryModal').style.display = 'none'; + // 1. Felder befüllen + document.getElementById('editLibraryItemId').value = item._id; + document.getElementById('editLibraryName').value = item.Name; + document.getElementById('editLibraryType').value = item.ItemType; + document.getElementById('editLibraryIsbn').value = item.ISBN || ''; + document.getElementById('editLibraryLocation').value = item.Ort; + document.getElementById('editLibraryDescription').value = item.Beschreibung; + + const codesContainer = document.getElementById('editLibraryCodesContainer'); + if (codesContainer) { + codesContainer.innerHTML = '
Lade Codes...
'; + } + + const groupMembers = item.SeriesGroupId ? await fetchLibraryGroupMembers(item.SeriesGroupId) : [item]; + renderLibraryGroupCodeFields(groupMembers.length > 0 ? groupMembers : [item], item._id); + + document.getElementById('editLibraryModal').style.display = 'flex'; + }; + + function closeEditLibraryModal() { + document.getElementById('editLibraryModal').style.display = 'none'; + } } From 9f3799a77fc308b464eba1e18fb7200e422d1f01 Mon Sep 17 00:00:00 2001 From: AIIrondev Date: Tue, 11 Aug 2026 00:57:51 +0200 Subject: [PATCH 14/56] Fix of an missing parethasys --- Web/templates/library_table.html | 893 +++++++++++++++---------------- 1 file changed, 444 insertions(+), 449 deletions(-) diff --git a/Web/templates/library_table.html b/Web/templates/library_table.html index 0651bf9..bbefe13 100644 --- a/Web/templates/library_table.html +++ b/Web/templates/library_table.html @@ -1042,465 +1042,460 @@ } - async function returnByCode(code) { - if (!code) return; - setScanStatus('Verarbeite Rückgabe...', 'warn'); - try { - const resp = await fetch('/api/library_return_by_code', { - method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({item_code: code}) - }); - const result = await resp.json(); - if (!resp.ok || !result.ok) { - setScanStatus(result.message || 'Rückgabe fehlgeschlagen.', 'error'); - showSmallConfirm(result.message || 'Rückgabe fehlgeschlagen.', 'error'); - return false; - } - setScanStatus(result.message || `Zurückgegeben: ${result.item_name || ''}`, 'ok'); - showSmallConfirm(result.message || `Zurückgegeben: ${result.item_name || ''}`, 'ok'); - await loadLibraryItems(); - return true; - } catch (err) { - console.error('Return by code failed:', err); - setScanStatus('Fehler bei Rückgabe.', 'error'); - showSmallConfirm('Fehler bei Rückgabe.', 'error'); - return false; - } + async function returnByCode(code) { + if (!code) return; + setScanStatus('Verarbeite Rückgabe...', 'warn'); + try { + const resp = await fetch('/api/library_return_by_code', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ item_code: code }) + }); + const result = await resp.json(); + if (!resp.ok || !result.ok) { + setScanStatus(result.message || 'Rückgabe fehlgeschlagen.', 'error'); + showSmallConfirm(result.message || 'Rückgabe fehlgeschlagen.', 'error'); + return false; + } + setScanStatus(result.message || `Zurückgegeben: ${result.item_name || ''}`, 'ok'); + showSmallConfirm(result.message || `Zurückgegeben: ${result.item_name || ''}`, 'ok'); + await loadLibraryItems(); + return true; + } catch (err) { + console.error('Return by code failed:', err); + setScanStatus('Fehler bei Rückgabe.', 'error'); + showSmallConfirm('Fehler bei Rückgabe.', 'error'); + return false; + } + } + try { + setScanStatus('Verarbeite Mediencode...', 'warn'); + const response = await fetch('/api/library_scan_action', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + student_card_id: activeStudentCardId, + item_code: scannedCode + }) + }); + + const result = await response.json(); + if (!response.ok || !result.ok) { + setScanStatus(result.message || 'Scan-Aktion fehlgeschlagen.', 'error'); + return; } - try { - setScanStatus('Verarbeite Mediencode...', 'warn'); - const response = await fetch('/api/library_scan_action', { - method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({ - student_card_id: activeStudentCardId, - item_code: scannedCode - }) - }); + if (result.action === 'borrowed') { + setScanStatus(`Ausgeliehen: ${result.item_name}`, 'ok'); + showSmallConfirm(`Ausgeliehen: ${result.item_name}`, 'ok'); + } else if (result.action === 'returned') { + setScanStatus(`Zurückgegeben: ${result.item_name}`, 'ok'); + showSmallConfirm(`Zurückgegeben: ${result.item_name}`, 'ok'); + } else { + setScanStatus(result.message || 'Aktion durchgeführt.', 'ok'); + showSmallConfirm(result.message || 'Aktion durchgeführt.', 'ok'); + } - const result = await response.json(); - if (!response.ok || !result.ok) { - setScanStatus(result.message || 'Scan-Aktion fehlgeschlagen.', 'error'); + await loadLibraryItems(); + } catch (err) { + console.error('Quick scan action failed:', err); + setScanStatus('Fehler beim Verarbeiten des Scans.', 'error'); + } + } + + function scanIntoEditCode() { + const scanReaderWrap = document.getElementById('scanReaderWrap'); + const editCodeInput = document.getElementById('edit-code4'); + const scanEditBtn = document.getElementById('scan-edit-code-btn'); + if (!scanReaderWrap || !editCodeInput || !scanEditBtn) { + return; + } + + if (scannerRunning && scanReaderWrap.style.display !== 'none') { + stopScanner(); + scanEditBtn.textContent = 'Barcode scannen'; + return; + } + + scanEditBtn.textContent = 'Scanner schließen'; + + startScanner(function(decodedText) { + editCodeInput.value = decodedText; + if(typeof validateCodeField === "function") { + validateCodeField(editCodeInput, document.getElementById('edit-item-id')?.value || null); + } + scanEditBtn.textContent = 'Barcode scannen'; + }); + } + + // ========================================================================= + // 4. UI INTERACTIONS & UTILITIES + // ========================================================================= + function borrowItem(itemId) { + const selectedItem = (libraryItems || []).find(item => item._id === itemId); + if (selectedItem && selectedItem.LibraryDisplayStatus === 'damaged') { + alert('Dieses Medium ist als defekt/zerstört markiert und kann nicht ausgeliehen werden.'); + return; + } + + const defaultCardId = activeStudentCardId || ''; + const cardId = (window.prompt('Bitte Schülerausweis-ID eingeben:', defaultCardId) || '').trim().toUpperCase(); + if (!cardId) { + alert('Ausleihe abgebrochen: Für Bibliotheksmedien ist eine gültige Schülerausweis-ID erforderlich.'); + return; + } + + setActiveStudentCard(cardId); + + const durationInput = (window.prompt('Ausleihdauer in Tagen (optional):') || '').trim(); + const maxAvailable = Math.max(1, parseInt(selectedItem?.AvailableGroupedCount || selectedItem?.Quantity || 1, 10) || 1); + const countPrompt = (window.prompt(`Anzahl ausleihen? (Standard: 1, verfügbar: ${maxAvailable})`, '1') || '').trim(); + let borrowCount = parseInt(countPrompt || '1', 10); + if (!Number.isFinite(borrowCount) || borrowCount < 1) { + borrowCount = 1; + } + if (borrowCount > maxAvailable) { + alert(`Es sind nur ${maxAvailable} Exemplar(e) verfügbar.`); + return; + } + + const form = document.createElement('form'); + form.method = 'POST'; + form.action = `/ausleihen/${itemId}`; + + const csrfField = document.createElement('input'); + csrfField.type = 'hidden'; + csrfField.name = 'csrf_token'; + csrfField.value = '{{ csrf_token }}'; + form.appendChild(csrfField); + + const cardField = document.createElement('input'); + cardField.type = 'hidden'; + cardField.name = 'borrower_card_id'; + cardField.value = cardId; + form.appendChild(cardField); + + const returnTargetField = document.createElement('input'); + returnTargetField.type = 'hidden'; + returnTargetField.name = 'return_to'; + returnTargetField.value = 'library'; + form.appendChild(returnTargetField); + + if (durationInput) { + const durationField = document.createElement('input'); + durationField.type = 'hidden'; + durationField.name = 'borrow_duration_days'; + durationField.value = durationInput; + form.appendChild(durationField); + } + + const countField = document.createElement('input'); + countField.type = 'hidden'; + countField.name = 'exemplare_count'; + countField.value = String(borrowCount || 1); + form.appendChild(countField); + + document.body.appendChild(form); + form.submit(); + } + + function setScanStatus(message, kind = '') { + const el = document.getElementById('scanStatus'); + if (!el) return; + el.textContent = message; + el.classList.remove('ok', 'warn', 'error'); + if (kind) el.classList.add(kind); + } + + function showSmallConfirm(message, kind='ok') { + // Append the small helper text in German + const helper = 'Sie können fortfahren. Dies ist nur eine kleine Benachrichtigung.'; + const el = document.createElement('div'); + el.className = `small-popup ${kind === 'error' ? 'error' : 'ok'}`; + el.innerHTML = `
${escapeHtml(String(message || ''))}
${helper}
×
`; + document.body.appendChild(el); + // close handler + el.querySelector('.close-x').addEventListener('click', () => { + if (el && el.parentNode) el.parentNode.removeChild(el); + }); + // auto remove after 3 seconds + setTimeout(() => { try { if (el && el.parentNode) el.parentNode.removeChild(el); } catch(e){} }, 3000); + } + + function setActiveStudentCard(cardId) { + activeStudentCardId = (cardId || '').trim().toUpperCase(); + const input = document.getElementById('activeStudentCard'); + if (input) input.value = activeStudentCardId; + } + + function normalizeScannedCode(code) { + return (code || '').trim(); + } + + function getItemTypeLabel(type) { + const labels = { 'book': 'Buch', 'cd': 'CD', 'dvd': 'DVD', 'other': 'Sonstige', 'schoolbook': 'Schulbuch' }; + return labels[type] || type; + } + + function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } + + // Opens the modal and fetches the data + function showItemDetail(itemId) { + const detailContent = document.getElementById('detailContent'); + const detailModal = document.getElementById('detailModal'); + + // 1. Show the loading state immediately + detailContent.innerHTML = '

Loading details...

'; + detailModal.style.display = 'flex'; + + // 2. Fetch the data + fetch(`/api/item_detail/${itemId}`) + .then(response => { + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return response.text(); + }) + .then(html => { + // 3. Clean the HTML and display it + detailContent.innerHTML = DOMPurify.sanitize(html); + }) + .catch(err => { + console.error('Error loading detail:', err); + detailContent.innerHTML = '

Sorry, we could not load the item details. Please try again later.

'; + }); + } + + // Closes the modal via the 'x' button + function closeDetailModal() { + document.getElementById('detailModal').style.display = 'none'; + } + + // Closes the modal if the user clicks the dark background overlay + window.onclick = function(event) { + const modal = document.getElementById('detailModal'); + // If the click happened directly on the dark overlay (not the white box) + if (event.target === modal) { + closeDetailModal(); + } + }; + + // ========================================================================= + // 5. EVENT LISTENERS INITIALIZATION & ON-LOAD INITIALIZER + // ========================================================================= + function wireScannerUi() { + const toggleBtn = document.getElementById('toggleScannerBtn'); + const resetBtn = document.getElementById('resetCardBtn'); + const modeSelect = document.getElementById('scanModeSelect'); + const keyboardToggle = document.getElementById('keyboardScannerToggle'); + + if (toggleBtn) { + toggleBtn.addEventListener('click', async () => { + if (scannerRunning) { + await stopScanner(); + } else { + await startScanner(null); + } + }); + } + + if (resetBtn) { + resetBtn.addEventListener('click', () => { + setActiveStudentCard(''); + setScanStatus('Ausweis zurückgesetzt. Bitte neu scannen.', 'warn'); + }); + } + + if (modeSelect) { + modeSelect.addEventListener('change', () => { + if (modeSelect.value === 'quick_toggle' && !activeStudentCardId) { + setScanStatus('Schnellmodus: zuerst Schülerausweis scannen.', 'warn'); + } else if (modeSelect.value === 'card_only') { + setScanStatus('Nur Ausweis-Modus aktiv.', 'warn'); + } + }); + } + + if (keyboardToggle) { + keyboardToggle.addEventListener('change', () => { + keyboardScannerEnabled = !!keyboardToggle.checked; + if (keyboardScannerEnabled) { + document.addEventListener('keydown', keyboardScanKeydownHandler); + setScanStatus('Physischer Scanner aktiv (Schnellmodus empfohlen).', 'ok'); + } else { + document.removeEventListener('keydown', keyboardScanKeydownHandler); + setScanStatus('Physischer Scanner deaktiviert.', 'warn'); + } + }); + } + } + + // Run when DOM structure is entirely ready + document.addEventListener('DOMContentLoaded', async () => { + wireScannerUi(); // Setup scanner control buttons + loadLibraryItems(); // Fetch your database items right away! + + // Safely connect standard Filters and Search inputs inside DOMContentLoaded + const filterToggleBtn = document.getElementById('filterToggleBtn'); + if (filterToggleBtn) { + filterToggleBtn.addEventListener('click', () => { + filterPanelOpen = !filterPanelOpen; + document.getElementById('filterPanel').classList.toggle('open'); + filterToggleBtn.classList.toggle('active'); + }); + } + + const applyFilterBtn = document.getElementById('applyFilterBtn'); + if (applyFilterBtn) { + applyFilterBtn.addEventListener('click', () => { + activeFilters.isbn = document.getElementById('filterISBN').value.toLowerCase(); + activeFilters.type = document.getElementById('filterType').value; + activeFilters.status = document.getElementById('filterStatus').value; + applyFiltersAndSearch(true); + }); + } + + const clearFilterBtn = document.getElementById('clearFilterBtn'); + if (clearFilterBtn) { + clearFilterBtn.addEventListener('click', () => { + document.getElementById('filterISBN').value = ''; + document.getElementById('filterType').value = ''; + document.getElementById('filterStatus').value = ''; + activeFilters = {isbn: '', type: '', status: ''}; + applyFiltersAndSearch(true); + }); + } + + const librarySearch = document.getElementById('librarySearch'); + if (librarySearch) { + librarySearch.addEventListener('input', (e) => { + currentSearchTerm = (e.target.value || '').toLowerCase(); + applyFiltersAndSearch(true); + }); + } + + const loadMoreBtn = document.getElementById('loadMoreBtn'); + if (loadMoreBtn) { + loadMoreBtn.addEventListener('click', () => { + renderedCount = Math.min(renderedCount + RENDER_BATCH_COUNT, visibleItems.length); + renderItems(); + }); + } + + const manualReturnBtn = document.getElementById('manualReturnBtn'); + const manualItemCode = document.getElementById('manualItemCode'); + if (manualReturnBtn && manualItemCode) { + manualReturnBtn.addEventListener('click', async () => { + const code = (manualItemCode.value || '').trim(); + if (!code) { + alert('Bitte einen Mediencode eingeben.'); return; } - - if (result.action === 'borrowed') { - setScanStatus(`Ausgeliehen: ${result.item_name}`, 'ok'); - showSmallConfirm(`Ausgeliehen: ${result.item_name}`, 'ok'); - } else if (result.action === 'returned') { - setScanStatus(`Zurückgegeben: ${result.item_name}`, 'ok'); - showSmallConfirm(`Zurückgegeben: ${result.item_name}`, 'ok'); - } else { - setScanStatus(result.message || 'Aktion durchgeführt.', 'ok'); - showSmallConfirm(result.message || 'Aktion durchgeführt.', 'ok'); - } - - await loadLibraryItems(); - } catch (err) { - console.error('Quick scan action failed:', err); - setScanStatus('Fehler beim Verarbeiten des Scans.', 'error'); - } - } - - function scanIntoEditCode() { - const scanReaderWrap = document.getElementById('scanReaderWrap'); - const editCodeInput = document.getElementById('edit-code4'); - const scanEditBtn = document.getElementById('scan-edit-code-btn'); - if (!scanReaderWrap || !editCodeInput || !scanEditBtn) { - return; - } - - if (scannerRunning && scanReaderWrap.style.display !== 'none') { - stopScanner(); - scanEditBtn.textContent = 'Barcode scannen'; - return; - } - - scanEditBtn.textContent = 'Scanner schließen'; - - startScanner(function (decodedText) { - editCodeInput.value = decodedText; - if (typeof validateCodeField === "function") { - validateCodeField(editCodeInput, document.getElementById('edit-item-id')?.value || null); - } - scanEditBtn.textContent = 'Barcode scannen'; + await returnByCode(code); }); } - // ========================================================================= - // 4. UI INTERACTIONS & UTILITIES - // ========================================================================= - function borrowItem(itemId) { - const selectedItem = (libraryItems || []).find(item => item._id === itemId); - if (selectedItem && selectedItem.LibraryDisplayStatus === 'damaged') { - alert('Dieses Medium ist als defekt/zerstört markiert und kann nicht ausgeliehen werden.'); - return; - } - - const defaultCardId = activeStudentCardId || ''; - const cardId = (window.prompt('Bitte Schülerausweis-ID eingeben:', defaultCardId) || '').trim().toUpperCase(); - if (!cardId) { - alert('Ausleihe abgebrochen: Für Bibliotheksmedien ist eine gültige Schülerausweis-ID erforderlich.'); - return; - } - - setActiveStudentCard(cardId); - - const durationInput = (window.prompt('Ausleihdauer in Tagen (optional):') || '').trim(); - const maxAvailable = Math.max(1, parseInt(selectedItem?.AvailableGroupedCount || selectedItem?.Quantity || 1, 10) || 1); - const countPrompt = (window.prompt(`Anzahl ausleihen? (Standard: 1, verfügbar: ${maxAvailable})`, '1') || '').trim(); - let borrowCount = parseInt(countPrompt || '1', 10); - if (!Number.isFinite(borrowCount) || borrowCount < 1) { - borrowCount = 1; - } - if (borrowCount > maxAvailable) { - alert(`Es sind nur ${maxAvailable} Exemplar(e) verfügbar.`); - return; - } - - const form = document.createElement('form'); - form.method = 'POST'; - form.action = `/ausleihen/${itemId}`; - - const csrfField = document.createElement('input'); - csrfField.type = 'hidden'; - csrfField.name = 'csrf_token'; - csrfField.value = '{{ csrf_token }}'; - form.appendChild(csrfField); - - const cardField = document.createElement('input'); - cardField.type = 'hidden'; - cardField.name = 'borrower_card_id'; - cardField.value = cardId; - form.appendChild(cardField); - - const returnTargetField = document.createElement('input'); - returnTargetField.type = 'hidden'; - returnTargetField.name = 'return_to'; - returnTargetField.value = 'library'; - form.appendChild(returnTargetField); - - if (durationInput) { - const durationField = document.createElement('input'); - durationField.type = 'hidden'; - durationField.name = 'borrow_duration_days'; - durationField.value = durationInput; - form.appendChild(durationField); - } - - const countField = document.createElement('input'); - countField.type = 'hidden'; - countField.name = 'exemplare_count'; - countField.value = String(borrowCount || 1); - form.appendChild(countField); - - document.body.appendChild(form); - form.submit(); - } - - function setScanStatus(message, kind = '') { - const el = document.getElementById('scanStatus'); - if (!el) return; - el.textContent = message; - el.classList.remove('ok', 'warn', 'error'); - if (kind) el.classList.add(kind); - } - - function showSmallConfirm(message, kind = 'ok') { - // Append the small helper text in German - const helper = 'Sie können fortfahren. Dies ist nur eine kleine Benachrichtigung.'; - const el = document.createElement('div'); - el.className = `small-popup ${kind === 'error' ? 'error' : 'ok'}`; - el.innerHTML = `
${escapeHtml(String(message || ''))}
${helper}
×
`; - document.body.appendChild(el); - // close handler - el.querySelector('.close-x').addEventListener('click', () => { - if (el && el.parentNode) el.parentNode.removeChild(el); - }); - // auto remove after 3 seconds - setTimeout(() => { - try { - if (el && el.parentNode) el.parentNode.removeChild(el); - } catch (e) { - } - }, 3000); - } - - function setActiveStudentCard(cardId) { - activeStudentCardId = (cardId || '').trim().toUpperCase(); - const input = document.getElementById('activeStudentCard'); - if (input) input.value = activeStudentCardId; - } - - function normalizeScannedCode(code) { - return (code || '').trim(); - } - - function getItemTypeLabel(type) { - const labels = {'book': 'Buch', 'cd': 'CD', 'dvd': 'DVD', 'other': 'Sonstige', 'schoolbook': 'Schulbuch'}; - return labels[type] || type; - } - - function escapeHtml(text) { - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; - } - - // Opens the modal and fetches the data - function showItemDetail(itemId) { - const detailContent = document.getElementById('detailContent'); - const detailModal = document.getElementById('detailModal'); - - // 1. Show the loading state immediately - detailContent.innerHTML = '

Loading details...

'; - detailModal.style.display = 'flex'; - - // 2. Fetch the data - fetch(`/api/item_detail/${itemId}`) - .then(response => { - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - return response.text(); - }) - .then(html => { - // 3. Clean the HTML and display it - detailContent.innerHTML = DOMPurify.sanitize(html); - }) - .catch(err => { - console.error('Error loading detail:', err); - detailContent.innerHTML = '

Sorry, we could not load the item details. Please try again later.

'; - }); - } - - // Closes the modal via the 'x' button - function closeDetailModal() { - document.getElementById('detailModal').style.display = 'none'; - } - - // Closes the modal if the user clicks the dark background overlay - window.onclick = function (event) { - const modal = document.getElementById('detailModal'); - // If the click happened directly on the dark overlay (not the white box) - if (event.target === modal) { - closeDetailModal(); - } - }; - - // ========================================================================= - // 5. EVENT LISTENERS INITIALIZATION & ON-LOAD INITIALIZER - // ========================================================================= - function wireScannerUi() { - const toggleBtn = document.getElementById('toggleScannerBtn'); - const resetBtn = document.getElementById('resetCardBtn'); - const modeSelect = document.getElementById('scanModeSelect'); - const keyboardToggle = document.getElementById('keyboardScannerToggle'); - - if (toggleBtn) { - toggleBtn.addEventListener('click', async () => { - if (scannerRunning) { - await stopScanner(); - } else { - await startScanner(null); - } - }); - } - - if (resetBtn) { - resetBtn.addEventListener('click', () => { - setActiveStudentCard(''); - setScanStatus('Ausweis zurückgesetzt. Bitte neu scannen.', 'warn'); - }); - } - - if (modeSelect) { - modeSelect.addEventListener('change', () => { - if (modeSelect.value === 'quick_toggle' && !activeStudentCardId) { - setScanStatus('Schnellmodus: zuerst Schülerausweis scannen.', 'warn'); - } else if (modeSelect.value === 'card_only') { - setScanStatus('Nur Ausweis-Modus aktiv.', 'warn'); - } - }); - } - - if (keyboardToggle) { - keyboardToggle.addEventListener('change', () => { - keyboardScannerEnabled = !!keyboardToggle.checked; - if (keyboardScannerEnabled) { - document.addEventListener('keydown', keyboardScanKeydownHandler); - setScanStatus('Physischer Scanner aktiv (Schnellmodus empfohlen).', 'ok'); - } else { - document.removeEventListener('keydown', keyboardScanKeydownHandler); - setScanStatus('Physischer Scanner deaktiviert.', 'warn'); - } - }); - } - } - - // Run when DOM structure is entirely ready - document.addEventListener('DOMContentLoaded', () => { - wireScannerUi(); // Setup scanner control buttons - loadLibraryItems(); // Fetch your database items right away! - - // Safely connect standard Filters and Search inputs inside DOMContentLoaded - const filterToggleBtn = document.getElementById('filterToggleBtn'); - if (filterToggleBtn) { - filterToggleBtn.addEventListener('click', () => { - filterPanelOpen = !filterPanelOpen; - document.getElementById('filterPanel').classList.toggle('open'); - filterToggleBtn.classList.toggle('active'); - }); - } - - const applyFilterBtn = document.getElementById('applyFilterBtn'); - if (applyFilterBtn) { - applyFilterBtn.addEventListener('click', () => { - activeFilters.isbn = document.getElementById('filterISBN').value.toLowerCase(); - activeFilters.type = document.getElementById('filterType').value; - activeFilters.status = document.getElementById('filterStatus').value; - applyFiltersAndSearch(true); - }); - } - - const clearFilterBtn = document.getElementById('clearFilterBtn'); - if (clearFilterBtn) { - clearFilterBtn.addEventListener('click', () => { - document.getElementById('filterISBN').value = ''; - document.getElementById('filterType').value = ''; - document.getElementById('filterStatus').value = ''; - activeFilters = {isbn: '', type: '', status: ''}; - applyFiltersAndSearch(true); - }); - } - - const librarySearch = document.getElementById('librarySearch'); - if (librarySearch) { - librarySearch.addEventListener('input', (e) => { - currentSearchTerm = (e.target.value || '').toLowerCase(); - applyFiltersAndSearch(true); - }); - } - - const loadMoreBtn = document.getElementById('loadMoreBtn'); - if (loadMoreBtn) { - loadMoreBtn.addEventListener('click', () => { - renderedCount = Math.min(renderedCount + RENDER_BATCH_COUNT, visibleItems.length); - renderItems(); - }); - } - - const manualReturnBtn = document.getElementById('manualReturnBtn'); - const manualItemCode = document.getElementById('manualItemCode'); - if (manualReturnBtn && manualItemCode) { - manualReturnBtn.addEventListener('click', async () => { - const code = (manualItemCode.value || '').trim(); - if (!code) { - alert('Bitte einen Mediencode eingeben.'); - return; - } - await returnByCode(code); - }); - } - - const editForm = document.getElementById('editLibraryForm'); - if (editForm) { - editForm.addEventListener('submit', async function (e) { - e.preventDefault(); - - const itemId = document.getElementById('editLibraryItemId').value; - const currentItem = libraryItems.find(i => i._id === itemId); - if (!currentItem) return; - - const sharedPayload = { - name: document.getElementById('editLibraryName').value, - item_type: document.getElementById('editLibraryType').value, - isbn: document.getElementById('editLibraryIsbn').value, - ort: document.getElementById('editLibraryLocation').value, - beschreibung: document.getElementById('editLibraryDescription').value, - ansch_jahr: currentItem.Anschaffungsjahr || '', - ansch_kost: currentItem.Anschaffungskosten || '', - reservierbar: currentItem.Reservierbar !== false, + // Edit Modal Form processing + const editForm = document.getElementById('editLibraryForm'); + if (editForm) { + try { + if (isGroupedEdit) { + const payload = { + series_group_id: currentItem.SeriesGroupId, + ...sharedPayload, + items: groupMembers.map(member => ({ + id: member._id, + code_4: codeByItemId.get(member._id) || '' + })) }; - const codeInputs = Array.from(document.querySelectorAll('#editLibraryCodesContainer input[data-item-id]')); - const codeByItemId = new Map(codeInputs.map(input => [input.dataset.itemId, (input.value || '').trim()])); - const groupMembers = editLibraryState.groupMembers.length > 0 ? editLibraryState.groupMembers : [currentItem]; - const isGroupedEdit = Boolean(currentItem.SeriesGroupId) && groupMembers.length > 1; + const response = await fetch('/update_group', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': '{{ csrf_token }}', + 'X-CSRF-Token': '{{ csrf_token }}' + }, + body: JSON.stringify(payload) + }); - try { - if (isGroupedEdit) { - const payload = { - series_group_id: currentItem.SeriesGroupId, - ...sharedPayload, - items: groupMembers.map(member => ({ - id: member._id, - code_4: codeByItemId.get(member._id) || '' - })) - }; - - const response = await fetch('/update_group', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': '{{ csrf_token }}', - 'X-CSRF-Token': '{{ csrf_token }}' - }, - body: JSON.stringify(payload) - }); - - const result = await response.json(); - if (response.ok && result.success) { - alert(result.message || 'Gruppe erfolgreich aktualisiert!'); - closeEditLibraryModal(); - pagingState.loading = false; - await loadLibraryItems(); - } else { - alert(result.message || 'Fehler beim Speichern der Gruppenänderungen.'); - } - } else { - const primaryCodeInput = codeInputs[0]; - const payload = { - name: sharedPayload.name, - item_type: sharedPayload.item_type, - isbn: sharedPayload.isbn, - code_4: primaryCodeInput ? primaryCodeInput.value.trim() : (currentItem.Code_4 || currentItem.Code4 || '').trim(), - ort: sharedPayload.ort, - beschreibung: sharedPayload.beschreibung - }; - - const response = await fetch(`/api/library_item/${itemId}/update`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': '{{ csrf_token }}', - 'X-CSRF-Token': '{{ csrf_token }}' - }, - body: JSON.stringify(payload) - }); - - const result = await response.json(); - if (response.ok && result.ok) { - alert(result.message || 'Medium erfolgreich aktualisiert!'); - closeEditLibraryModal(); - pagingState.loading = false; - await loadLibraryItems(); - } else { - alert(result.message || 'Fehler beim Speichern der Änderungen.'); - } - } - } catch (error) { - console.error('Update failed:', error); - alert('Netzwerkfehler beim Aktualisieren des Mediums.'); + const result = await response.json(); + if (response.ok && result.success) { + alert(result.message || 'Gruppe erfolgreich aktualisiert!'); + closeEditLibraryModal(); + pagingState.loading = false; + await loadLibraryItems(); + } else { + alert(result.message || 'Fehler beim Speichern der Gruppenänderungen.'); } - }); - } - }); + } else { + const primaryCodeInput = codeInputs[0]; + const payload = { + name: sharedPayload.name, + item_type: sharedPayload.item_type, + isbn: sharedPayload.isbn, + code_4: primaryCodeInput ? primaryCodeInput.value.trim() : (currentItem.Code_4 || currentItem.Code4 || '').trim(), + ort: sharedPayload.ort, + beschreibung: sharedPayload.beschreibung + }; - async function fetchLibraryGroupMembers(seriesGroupId) { - if (!seriesGroupId) return []; + const response = await fetch(`/api/library_item/${itemId}/update`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': '{{ csrf_token }}', + 'X-CSRF-Token': '{{ csrf_token }}' + }, + body: JSON.stringify(payload) + }); + + const result = await response.json(); + if (response.ok && result.ok) { + alert(result.message || 'Medium erfolgreich aktualisiert!'); + closeEditLibraryModal(); + pagingState.loading = false; + await loadLibraryItems(); + } else { + alert(result.message || 'Fehler beim Speichern der Änderungen.'); + } + } + } catch (error) { + console.error('Update failed:', error); + alert('Netzwerkfehler beim Aktualisieren des Mediums.'); + } + editForm.addEventListener('submit', async function (e) { + e.preventDefault(); + + const itemId = document.getElementById('editLibraryItemId').value; + const currentItem = libraryItems.find(i => i._id === itemId); + if (!currentItem) return; + + const codeInputs = Array.from(document.querySelectorAll('#editLibraryCodesContainer input[data-item-id]')); + + const sharedPayload = { + name: document.getElementById('editLibraryName').value, + item_type: document.getElementById('editLibraryType').value, + isbn: document.getElementById('editLibraryIsbn').value, + ort: document.getElementById('editLibraryLocation').value, + beschreibung: document.getElementById('editLibraryDescription').value, + ansch_jahr: currentItem.Anschaffungsjahr || '', + ansch_kost: currentItem.Anschaffungskosten || '', + reservierbar: currentItem.Reservierbar !== false, + }; + const codeByItemId = new Map(codeInputs.map(input => [input.dataset.itemId, (input.value || '').trim()])); + const groupMembers = editLibraryState.groupMembers.length > 0 ? editLibraryState.groupMembers : [currentItem]; + const isGroupedEdit = Boolean(currentItem.SeriesGroupId) && groupMembers.length > 1; + + }); + } + }); + + async function fetchLibraryGroupMembers(seriesGroupId) { + if (!seriesGroupId) return []; try { const response = await fetch(`/api/library_group/${encodeURIComponent(seriesGroupId)}`); @@ -1595,9 +1590,9 @@ document.getElementById('editLibraryModal').style.display = 'flex'; }; - function closeEditLibraryModal() { - document.getElementById('editLibraryModal').style.display = 'none'; - } + function closeEditLibraryModal() { + document.getElementById('editLibraryModal').style.display = 'none'; + } } From ee9ef3df6f42cec6bd9bac1bd2f0f6f58f8e2591 Mon Sep 17 00:00:00 2001 From: AIIrondev Date: Tue, 11 Aug 2026 01:09:53 +0200 Subject: [PATCH 15/56] Fix of an missing parethasys --- Web/templates/library_table.html | 381 ++++++++++++++++--------------- 1 file changed, 191 insertions(+), 190 deletions(-) diff --git a/Web/templates/library_table.html b/Web/templates/library_table.html index bbefe13..8f77143 100644 --- a/Web/templates/library_table.html +++ b/Web/templates/library_table.html @@ -1027,47 +1027,21 @@ } async function processQuickToggleScan(scannedCode) { - if (!activeStudentCardId) { - // If return-only mode is active, always attempt to return by code - const returnOnly = (document.getElementById('returnOnlyToggle') || {}).checked; - if (returnOnly) { - await returnByCode(scannedCode); - return; - } - - if (!activeStudentCardId) { - setActiveStudentCard(scannedCode); - setScanStatus(`Ausweis gesetzt: ${activeStudentCardId}`, 'ok'); - return; - } - - - async function returnByCode(code) { - if (!code) return; - setScanStatus('Verarbeite Rückgabe...', 'warn'); - try { - const resp = await fetch('/api/library_return_by_code', { - method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({ item_code: code }) - }); - const result = await resp.json(); - if (!resp.ok || !result.ok) { - setScanStatus(result.message || 'Rückgabe fehlgeschlagen.', 'error'); - showSmallConfirm(result.message || 'Rückgabe fehlgeschlagen.', 'error'); - return false; - } - setScanStatus(result.message || `Zurückgegeben: ${result.item_name || ''}`, 'ok'); - showSmallConfirm(result.message || `Zurückgegeben: ${result.item_name || ''}`, 'ok'); - await loadLibraryItems(); - return true; - } catch (err) { - console.error('Return by code failed:', err); - setScanStatus('Fehler bei Rückgabe.', 'error'); - showSmallConfirm('Fehler bei Rückgabe.', 'error'); - return false; + // 1. Prüfen, ob "Nur Rückgabe"-Modus aktiv ist + const returnOnly = (document.getElementById('returnOnlyToggle') || {}).checked; + if (returnOnly) { + await returnByCode(scannedCode); + return; } - } + + // 2. Wenn kein Ausweis gesetzt ist, wird der Code als Ausweis interpretiert + if (!activeStudentCardId) { + setActiveStudentCard(scannedCode); + setScanStatus(`Ausweis gesetzt: ${activeStudentCardId}`, 'ok'); + return; + } + + // 3. Ausleihe/Rückgabe verarbeiten (wenn Ausweis vorhanden) try { setScanStatus('Verarbeite Mediencode...', 'warn'); const response = await fetch('/api/library_scan_action', { @@ -1103,6 +1077,33 @@ } } + async function returnByCode(code) { + if (!code) return; + setScanStatus('Verarbeite Rückgabe...', 'warn'); + try { + const resp = await fetch('/api/library_return_by_code', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ item_code: code }) + }); + const result = await resp.json(); + if (!resp.ok || !result.ok) { + setScanStatus(result.message || 'Rückgabe fehlgeschlagen.', 'error'); + showSmallConfirm(result.message || 'Rückgabe fehlgeschlagen.', 'error'); + return false; + } + setScanStatus(result.message || `Zurückgegeben: ${result.item_name || ''}`, 'ok'); + showSmallConfirm(result.message || `Zurückgegeben: ${result.item_name || ''}`, 'ok'); + await loadLibraryItems(); + return true; + } catch (err) { + console.error('Return by code failed:', err); + setScanStatus('Fehler bei Rückgabe.', 'error'); + showSmallConfirm('Fehler bei Rückgabe.', 'error'); + return false; + } + } + function scanIntoEditCode() { const scanReaderWrap = document.getElementById('scanReaderWrap'); const editCodeInput = document.getElementById('edit-code4'); @@ -1399,74 +1400,8 @@ }); } - // Edit Modal Form processing const editForm = document.getElementById('editLibraryForm'); if (editForm) { - try { - if (isGroupedEdit) { - const payload = { - series_group_id: currentItem.SeriesGroupId, - ...sharedPayload, - items: groupMembers.map(member => ({ - id: member._id, - code_4: codeByItemId.get(member._id) || '' - })) - }; - - const response = await fetch('/update_group', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': '{{ csrf_token }}', - 'X-CSRF-Token': '{{ csrf_token }}' - }, - body: JSON.stringify(payload) - }); - - const result = await response.json(); - if (response.ok && result.success) { - alert(result.message || 'Gruppe erfolgreich aktualisiert!'); - closeEditLibraryModal(); - pagingState.loading = false; - await loadLibraryItems(); - } else { - alert(result.message || 'Fehler beim Speichern der Gruppenänderungen.'); - } - } else { - const primaryCodeInput = codeInputs[0]; - const payload = { - name: sharedPayload.name, - item_type: sharedPayload.item_type, - isbn: sharedPayload.isbn, - code_4: primaryCodeInput ? primaryCodeInput.value.trim() : (currentItem.Code_4 || currentItem.Code4 || '').trim(), - ort: sharedPayload.ort, - beschreibung: sharedPayload.beschreibung - }; - - const response = await fetch(`/api/library_item/${itemId}/update`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': '{{ csrf_token }}', - 'X-CSRF-Token': '{{ csrf_token }}' - }, - body: JSON.stringify(payload) - }); - - const result = await response.json(); - if (response.ok && result.ok) { - alert(result.message || 'Medium erfolgreich aktualisiert!'); - closeEditLibraryModal(); - pagingState.loading = false; - await loadLibraryItems(); - } else { - alert(result.message || 'Fehler beim Speichern der Änderungen.'); - } - } - } catch (error) { - console.error('Update failed:', error); - alert('Netzwerkfehler beim Aktualisieren des Mediums.'); - } editForm.addEventListener('submit', async function (e) { e.preventDefault(); @@ -1476,6 +1411,7 @@ const codeInputs = Array.from(document.querySelectorAll('#editLibraryCodesContainer input[data-item-id]')); + // Daten aus dem Formular sammeln const sharedPayload = { name: document.getElementById('editLibraryName').value, item_type: document.getElementById('editLibraryType').value, @@ -1486,114 +1422,179 @@ ansch_kost: currentItem.Anschaffungskosten || '', reservierbar: currentItem.Reservierbar !== false, }; + const codeByItemId = new Map(codeInputs.map(input => [input.dataset.itemId, (input.value || '').trim()])); const groupMembers = editLibraryState.groupMembers.length > 0 ? editLibraryState.groupMembers : [currentItem]; const isGroupedEdit = Boolean(currentItem.SeriesGroupId) && groupMembers.length > 1; + // API-Aufruf + try { + if (isGroupedEdit) { + const payload = { + series_group_id: currentItem.SeriesGroupId, + ...sharedPayload, + items: groupMembers.map(member => ({ + id: member._id, + code_4: codeByItemId.get(member._id) || '' + })) + }; + + const response = await fetch('/update_group', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': '{{ csrf_token }}', + 'X-CSRF-Token': '{{ csrf_token }}' + }, + body: JSON.stringify(payload) + }); + + const result = await response.json(); + if (response.ok && result.success) { + alert(result.message || 'Gruppe erfolgreich aktualisiert!'); + closeEditLibraryModal(); + pagingState.loading = false; + await loadLibraryItems(); + } else { + alert(result.message || 'Fehler beim Speichern der Gruppenänderungen.'); + } + } else { + const primaryCodeInput = codeInputs[0]; + const payload = { + name: sharedPayload.name, + item_type: sharedPayload.item_type, + isbn: sharedPayload.isbn, + code_4: primaryCodeInput ? primaryCodeInput.value.trim() : (currentItem.Code_4 || currentItem.Code4 || '').trim(), + ort: sharedPayload.ort, + beschreibung: sharedPayload.beschreibung + }; + + const response = await fetch(`/api/library_item/${itemId}/update`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': '{{ csrf_token }}', + 'X-CSRF-Token': '{{ csrf_token }}' + }, + body: JSON.stringify(payload) + }); + + const result = await response.json(); + if (response.ok && result.ok) { + alert(result.message || 'Medium erfolgreich aktualisiert!'); + closeEditLibraryModal(); + pagingState.loading = false; + await loadLibraryItems(); + } else { + alert(result.message || 'Fehler beim Speichern der Änderungen.'); + } + } + } catch (error) { + console.error('Update failed:', error); + alert('Netzwerkfehler beim Aktualisieren des Mediums.'); + } }); } }); async function fetchLibraryGroupMembers(seriesGroupId) { if (!seriesGroupId) return []; - - try { - const response = await fetch(`/api/library_group/${encodeURIComponent(seriesGroupId)}`); - if (!response.ok) { - throw new Error(`HTTP ${response.status}`); - } - - const payload = await response.json(); - return Array.isArray(payload.items) ? payload.items : []; - } catch (error) { - console.warn('Falling back to loaded library items for group editing:', error); - return (libraryItems || []).filter(item => item.SeriesGroupId === seriesGroupId); + try { + const response = await fetch(`/api/library_group/${encodeURIComponent(seriesGroupId)}`); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); } + + const payload = await response.json(); + return Array.isArray(payload.items) ? payload.items : []; + } catch (error) { + console.warn('Falling back to loaded library items for group editing:', error); + return (libraryItems || []).filter(item => item.SeriesGroupId === seriesGroupId); + } + } + + function renderLibraryGroupCodeFields(groupMembers, currentItemId) { + const codesContainer = document.getElementById('editLibraryCodesContainer'); + const groupWarning = document.getElementById('editLibraryGroupWarning'); + const groupCount = document.getElementById('editLibraryGroupCount'); + const groupHint = document.getElementById('editLibraryGroupHint'); + + if (!codesContainer) return; + + const items = Array.isArray(groupMembers) ? groupMembers.slice() : []; + items.sort((a, b) => (a.SeriesPosition || 0) - (b.SeriesPosition || 0) || String(a.Name || '').localeCompare(String(b.Name || ''))); + + editLibraryState.groupMembers = items; + + if (groupWarning) { + groupWarning.style.display = items.length > 1 ? 'block' : 'none'; + } + if (groupCount) { + const totalCount = items.length || 1; + const declaredCount = items[0]?.SeriesCount || totalCount; + groupCount.textContent = `${totalCount} / ${declaredCount}`; + } + if (groupHint) { + groupHint.textContent = items.length > 1 + ? 'Jeder Code gehört zu einem eigenen Exemplar. Änderungen werden für alle Codes gespeichert.' + : 'Einzelnes Exemplar. Der Code wird direkt gespeichert.'; } - function renderLibraryGroupCodeFields(groupMembers, currentItemId) { - const codesContainer = document.getElementById('editLibraryCodesContainer'); - const groupWarning = document.getElementById('editLibraryGroupWarning'); - const groupCount = document.getElementById('editLibraryGroupCount'); - const groupHint = document.getElementById('editLibraryGroupHint'); + if (!items.length) { + codesContainer.innerHTML = '
Keine Codes geladen.
'; + return; + } - if (!codesContainer) return; - - const items = Array.isArray(groupMembers) ? groupMembers.slice() : []; - items.sort((a, b) => (a.SeriesPosition || 0) - (b.SeriesPosition || 0) || String(a.Name || '').localeCompare(String(b.Name || ''))); - - editLibraryState.groupMembers = items; - - if (groupWarning) { - groupWarning.style.display = items.length > 1 ? 'block' : 'none'; + codesContainer.innerHTML = items.map((member, index) => { + const codeValue = member.Code_4 || member.Code4 || ''; + const labelParts = []; + if (member.SeriesPosition !== undefined && member.SeriesPosition !== null) { + labelParts.push(`Exemplar ${member.SeriesPosition}`); + } else { + labelParts.push(`Exemplar ${index + 1}`); } - if (groupCount) { - const totalCount = items.length || 1; - const declaredCount = items[0]?.SeriesCount || totalCount; - groupCount.textContent = `${totalCount} / ${declaredCount}`; + if (member._id === currentItemId) { + labelParts.push('aktuelles Medium'); } - if (groupHint) { - groupHint.textContent = items.length > 1 - ? 'Jeder Code gehört zu einem eigenen Exemplar. Änderungen werden für alle Codes gespeichert.' - : 'Einzelnes Exemplar. Der Code wird direkt gespeichert.'; - } - - if (!items.length) { - codesContainer.innerHTML = '
Keine Codes geladen.
'; - return; - } - - codesContainer.innerHTML = items.map((member, index) => { - const codeValue = member.Code_4 || member.Code4 || ''; - const labelParts = []; - if (member.SeriesPosition !== undefined && member.SeriesPosition !== null) { - labelParts.push(`Exemplar ${member.SeriesPosition}`); - } else { - labelParts.push(`Exemplar ${index + 1}`); - } - if (member._id === currentItemId) { - labelParts.push('aktuelles Medium'); - } - return ` -
- - -
+ return ` +
+ + +
`; - }).join(''); + }).join(''); + } + + window.openEditLibraryItem = async function (itemId) { + const item = libraryItems.find(i => i._id === itemId); + if (!item) return; + + editLibraryState.itemId = item._id; + editLibraryState.seriesGroupId = item.SeriesGroupId || ''; + editLibraryState.groupMembers = []; + + // Felder befüllen + document.getElementById('editLibraryItemId').value = item._id; + document.getElementById('editLibraryName').value = item.Name; + document.getElementById('editLibraryType').value = item.ItemType; + document.getElementById('editLibraryIsbn').value = item.ISBN || ''; + document.getElementById('editLibraryLocation').value = item.Ort; + document.getElementById('editLibraryDescription').value = item.Beschreibung; + + const codesContainer = document.getElementById('editLibraryCodesContainer'); + if (codesContainer) { + codesContainer.innerHTML = '
Lade Codes...
'; } - window.openEditLibraryItem = async function (itemId) { - const item = libraryItems.find(i => i._id === itemId); - if (!item) return; + const groupMembers = item.SeriesGroupId ? await fetchLibraryGroupMembers(item.SeriesGroupId) : [item]; + renderLibraryGroupCodeFields(groupMembers.length > 0 ? groupMembers : [item], item._id); - editLibraryState.itemId = item._id; - editLibraryState.seriesGroupId = item.SeriesGroupId || ''; - editLibraryState.groupMembers = []; - - // 1. Felder befüllen - document.getElementById('editLibraryItemId').value = item._id; - document.getElementById('editLibraryName').value = item.Name; - document.getElementById('editLibraryType').value = item.ItemType; - document.getElementById('editLibraryIsbn').value = item.ISBN || ''; - document.getElementById('editLibraryLocation').value = item.Ort; - document.getElementById('editLibraryDescription').value = item.Beschreibung; - - const codesContainer = document.getElementById('editLibraryCodesContainer'); - if (codesContainer) { - codesContainer.innerHTML = '
Lade Codes...
'; - } - - const groupMembers = item.SeriesGroupId ? await fetchLibraryGroupMembers(item.SeriesGroupId) : [item]; - renderLibraryGroupCodeFields(groupMembers.length > 0 ? groupMembers : [item], item._id); - - document.getElementById('editLibraryModal').style.display = 'flex'; - }; + document.getElementById('editLibraryModal').style.display = 'flex'; + }; function closeEditLibraryModal() { document.getElementById('editLibraryModal').style.display = 'none'; } - } -
+ + +
- +
+ + +
+
+ +
From 6a94d50d28e65a24895cd5576003521e0bba2242 Mon Sep 17 00:00:00 2001 From: AIIrondev Date: Tue, 11 Aug 2026 21:16:37 +0200 Subject: [PATCH 22/56] Additiopn to filter out the iduvidual items and --- Web/app.py | 178 ++++- Web/templates/edit_library.hmtl.html | 1005 ++++++++++++++++++++++++++ Web/templates/library_table.html | 195 +---- 3 files changed, 1185 insertions(+), 193 deletions(-) create mode 100644 Web/templates/edit_library.hmtl.html diff --git a/Web/app.py b/Web/app.py index 03b9ece..13acc50 100755 --- a/Web/app.py +++ b/Web/app.py @@ -46,7 +46,7 @@ import Web.modules.inventarsystem.pdf_export as pdf_export import Web.modules.inventarsystem.excel_export as excel_export import datetime from apscheduler.schedulers.background import BackgroundScheduler -from bson.objectid import ObjectId +from bson.objectid import ObjectId, InvalidId from urllib.parse import urlparse, urlunparse import requests import csv @@ -3314,6 +3314,7 @@ def api_library_items(): query = { 'ItemType': {'$in': ['book', 'cd', 'dvd', 'schoolbook', 'schulbuch', 'Buch', 'Schulbuch']}, + 'IsGroupedSubItem': {'$ne': True}, 'Deleted': {'$ne': True} } @@ -6366,6 +6367,181 @@ def edit_item(id): return redirect(url_for('home_admin')) +@app.route('/item_edit/', methods=['GET', 'POST']) +def item_edit(id): + """ + Endpoint zum Laden und Aktualisieren eines Eintrags (item_edit). + """ + # 1. Rechte- & Auth-Check + if 'username' not in session: + if request.method == 'POST' and request.is_json: + return jsonify({'success': False, 'message': 'Nicht angemeldet.'}), 401 + flash('Bitte melden Sie sich an.', 'error') + return redirect(url_for('login')) + + current_permissions = us.get_effective_permissions(session['username']) + if not current_permissions['actions'].get('can_edit', False): + if request.method == 'POST' and request.is_json: + return jsonify({'success': False, 'message': 'Keine Berechtigung zum Bearbeiten.'}), 403 + flash('Keine Berechtigung zum Bearbeiten.', 'error') + return redirect(url_for('home_admin')) + + try: + obj_id = ObjectId(id) + except InvalidId: + flash('Ungültige Element-ID.', 'error') + return redirect(url_for('home_admin')) + + db = it.db + items_col = db['items'] + + # --- GET: Template anzeigen --- + if request.method == 'GET': + item = items_col.find_one({'_id': obj_id}) + if not item: + flash('Element nicht gefunden.', 'error') + return redirect(url_for('home_admin')) + + item['_id'] = str(item['_id']) + show_library = cfg.MODULES.is_enabled('library') + + return render_template( + 'item_edit.html', + username=session['username'], + item=item, + library_module_enabled=show_library, + show_library_features=show_library, + page_title=f"Bearbeiten: {item.get('Name', '')}" + ) + + # --- POST: Speichern --- + redirect_target = request.referrer or url_for('home_admin') + current_item = items_col.find_one({'_id': obj_id}) + + if not current_item: + flash('Element in der Datenbank nicht gefunden.', 'error') + return redirect(redirect_target) + + # Formulardaten + name = sanitize_form_value(request.form.get('name')) + ort = sanitize_form_value(request.form.get('ort')) + beschreibung = sanitize_form_value(request.form.get('beschreibung')) + code_4 = sanitize_form_value(request.form.get('code_4')) + isbn_raw = sanitize_form_value(request.form.get('isbn', '')) + + anschaffungs_jahr = sanitize_form_value(request.form.get('anschaffungsjahr')) + anschaffungs_kosten = sanitize_form_value(request.form.get('anschaffungskosten')) + reservierbar = 'reservierbar' in request.form + + item_type_input = sanitize_form_value(request.form.get('item_type_input')) + library_category = sanitize_form_value(request.form.get('library_category')) + + filter1 = expand_filter_selection(sanitize_form_value(request.form.getlist('filter')), 1) + filter2 = expand_filter_selection(sanitize_form_value(request.form.getlist('filter2')), 2) + filter3 = sanitize_form_value(request.form.getlist('filter3')) + + # Barcode Prüfen + if code_4 and not it.is_code_unique(code_4, exclude_id=str(id)): + flash(f'Der Code "{code_4}" wird bereits verwendet.', 'error') + return redirect(redirect_target) + + # ISBN + item_isbn = '' + item_type = item_type_input or current_item.get('ItemType', 'general') + if cfg.MODULES.is_enabled('library') and isbn_raw: + item_isbn = normalize_and_validate_isbn(isbn_raw) + if not item_isbn: + flash('Ungültiges ISBN-Format.', 'error') + return redirect(redirect_target) + + # Bilder verarbeiten + images_to_keep = request.form.getlist('existing_images') + original_images = current_item.get('Images', []) + images = [img for img in original_images if img in images_to_keep] + + new_files = request.files.getlist('images') + if new_files and new_files[0].filename != '': + fs = get_gridfs() + for file in new_files: + if file and file.filename: + is_allowed, error_msg = allowed_file(file.filename, file) + if not is_allowed: + flash(error_msg, 'error') + return redirect(redirect_target) + + try: + secure_name = secure_filename(file.filename) + file.seek(0) + image_bytes = file.read() + + if not image_bytes: + continue + + optimized_io = io.BytesIO() + with Image.open(io.BytesIO(image_bytes)) as img: + if img.mode not in ('RGB', 'RGBA'): + img = img.convert('RGBA') + + max_width = 800 + if img.width > max_width: + ratio = max_width / img.width + img = img.resize((max_width, int(img.height * ratio)), Image.Resampling.LANCZOS) + + img.save(optimized_io, format='WEBP', quality=85, optimize=True) + + optimized_io.seek(0) + new_filename = f"{uuid.uuid4().hex}_{int(time.time())}.webp" + + fs.put( + optimized_io, + filename=new_filename, + content_type='image/webp', + metadata={'original_filename': secure_name, 'item_id': str(id)} + ) + images.append(new_filename) + except Exception as e: + app.logger.error(f"Bild-Fehler bei Item {id}: {e}") + + if ort and ort not in it.get_predefined_locations(): + it.add_predefined_location(ort) + + # Datenstruktur für Update + shared_fields = { + 'Name': name, + 'Ort': ort, + 'Beschreibung': beschreibung, + 'Anschaffungsjahr': anschaffungs_jahr, + 'Anschaffungskosten': anschaffungs_kosten, + 'Reservierbar': reservierbar, + 'ISBN': item_isbn, + 'ItemType': item_type, + 'Kategorie': library_category, + 'LastUpdated': datetime.datetime.now() + } + + # Gruppen-Update + group_item_ids = it.get_group_item_ids(str(id)) + + if group_item_ids: + group_object_ids = [ObjectId(g_id) for g_id in group_item_ids] + items_col.update_many({'_id': {'$in': group_object_ids}}, {'$set': shared_fields}) + + # Individual-Update + individual_update = { + **shared_fields, + 'Code_4': code_4, + 'Images': images, + 'Filter1': filter1, + 'Filter2': filter2, + 'Filter3': filter3 + } + + items_col.update_one({'_id': obj_id}, {'$set': individual_update}) + + flash('Artikel erfolgreich aktualisiert.', 'success') + return redirect(redirect_target) + + @app.route('/update_group', methods=['POST']) def update_group(): diff --git a/Web/templates/edit_library.hmtl.html b/Web/templates/edit_library.hmtl.html new file mode 100644 index 0000000..233f951 --- /dev/null +++ b/Web/templates/edit_library.hmtl.html @@ -0,0 +1,1005 @@ + +{% extends "base.html" %} + +{% block title %}{{ page_title|default('Artikel bearbeiten') }} - Inventarsystem{% endblock %} + +{% block content %} + + +{% endif %} + + + +
+
+

{{ page_title|default('Artikel bearbeiten') }}

+ +
+ + + + + {% if show_library_features %} + +
+ +
+ + + +
+ + +
+
+ {% endif %} + + +
+ + +
+ + +
+ + + + +
+ + +
+ + +
+ + +
+ +
+ + +
+ + +
+ + {% if show_library_features %} + +
+

Medientyp

+
+ {% set current_type = item.ItemType if item.ItemType else '' %} + +
+ +

Kategorie / Fach:

+
+ +
+
+ {% else %} + +
+

Unterrichtsfach:

+
+ {% for i in range(1, 5) %} +
+ + +
+ {% endfor %} +
+ +

Jahrgangsstufe:

+
+ {% for i in range(1, 5) %} +
+ + +
+ {% endfor %} +
+ +

Schlagwörter:

+
+ {% for i in range(1, 5) %} +
+ + +
+ {% endfor %} +
+
+ {% endif %} + + +
+ + +
+ +
+ + +
+ + +
+ +
+ {% if item.Images %} + {% for img in item.Images %} +
+ + +
+ {% endfor %} + {% else %} + Keine Bilder vorhanden. + {% endif %} +
+ + + +
+
+ + +
+ + +
+ + +
+
+
+ + + \ No newline at end of file diff --git a/Web/templates/library_table.html b/Web/templates/library_table.html index bb7047f..fcd744b 100644 --- a/Web/templates/library_table.html +++ b/Web/templates/library_table.html @@ -800,7 +800,7 @@ return ` ${escapeHtml(item.Name || 'Untitled')} - ${escapeHtml(item.Code_4 || item.Code4 || '-')} + ${escapeHtml(Item.ISBN || '-')} ${getItemTypeLabel(item.ItemType || 'book')} ${item.Quantity || item.GroupedDisplayCount || 1} @@ -1565,197 +1565,8 @@ }).join(''); } - window.openEditLibraryItem = async function (itemId) { - const item = libraryItems.find(i => i._id === itemId); - if (!item) return; - - editLibraryState.itemId = item._id; - editLibraryState.seriesGroupId = item.SeriesGroupId || ''; - editLibraryState.groupMembers = []; - - // Felder befüllen - document.getElementById('editLibraryItemId').value = item._id; - document.getElementById('editLibraryName').value = item.Name; - document.getElementById('editLibraryType').value = item.ItemType; - document.getElementById('editLibraryIsbn').value = item.ISBN || ''; - document.getElementById('editLibraryDescription').value = item.Beschreibung; - - // Orte laden und bestehenden Ort setzen - await loadEditLocationOptions(item.Ort); - - const codesContainer = document.getElementById('editLibraryCodesContainer'); - if (codesContainer) { - codesContainer.innerHTML = '
Lade Codes...
'; - } - - const groupMembers = item.SeriesGroupId ? await fetchLibraryGroupMembers(item.SeriesGroupId) : [item]; - renderLibraryGroupCodeFields(groupMembers.length > 0 ? groupMembers : [item], item._id); - - document.getElementById('editLibraryModal').style.display = 'flex'; - }; - - function closeEditLibraryModal() { - document.getElementById('editLibraryModal').style.display = 'none'; - // Setzt das Feld für neuen Ort sicherheitshalber zurück, wenn geschlossen wird - cancelEditAddLocation(); - } - - // Lädt die Optionen in das Dropdown und wählt den aktuellen Ort des Mediums aus - async function loadEditLocationOptions(currentLocation) { - try { - const response = await fetch('/get_predefined_locations'); - const data = await response.json(); - const ortSelect = document.getElementById('editLibraryLocation'); - - if (ortSelect) { - // Bestehende Optionen löschen (außer die erste Platzhalter-Option) - while (ortSelect.children.length > 1) { - ortSelect.removeChild(ortSelect.lastChild); - } - - let locationFound = false; - - // Neue Optionen aus der Datenbank hinzufügen - data.locations.forEach(location => { - const option = document.createElement('option'); - option.value = location; - option.textContent = location; - ortSelect.appendChild(option); - - if (location === currentLocation) { - locationFound = true; - } - }); - - // WICHTIG: Falls der aktuell hinterlegte Ort nicht in der Datenbank-Liste ist, - // fügen wir ihn als temporäre Option hinzu, damit er nicht beim Speichern überschrieben wird. - if (currentLocation && !locationFound) { - const option = document.createElement('option'); - option.value = currentLocation; - option.textContent = currentLocation; - ortSelect.appendChild(option); - } - - // Zuletzt den aktuellen Ort anwählen - if (currentLocation) { - ortSelect.value = currentLocation; - } - } - } catch (error) { - console.error('Error loading location options:', error); - } - } - - // Funktion um einen neuen Ort ins Bearbeiten-Dropdown aufzunehmen - function addEditNewLocation() { - const newLocationInput = document.getElementById('edit-new-location-input'); - const newLocation = newLocationInput.value.trim(); - - if (!newLocation) { - alert('Bitte geben Sie einen Ort ein.'); - return; - } - - // Füge Option dem Dropdown hinzu - const ortSelect = document.getElementById('editLibraryLocation'); - const option = document.createElement('option'); - option.value = newLocation; - option.textContent = newLocation; - ortSelect.appendChild(option); - - // Wähle die neue Option direkt aus - ortSelect.value = newLocation; - - // Verstecke die Eingabemaske wieder - cancelEditAddLocation(); - } - - // Funktion zum Abbrechen der Orteingabe - function cancelEditAddLocation() { - const container = document.getElementById('edit-new-location-container'); - if(container) container.style.display = 'none'; - - const input = document.getElementById('edit-new-location-input'); - if(input) input.value = ''; + function openEditLibraryItem(itemId) { + window.location.href = `/item_edit/${itemId}`; } - - {% endblock %} \ No newline at end of file From a518adb054a22ca32325defc567e3fa2bce760e0 Mon Sep 17 00:00:00 2001 From: AIIrondev Date: Tue, 11 Aug 2026 21:27:39 +0200 Subject: [PATCH 23/56] Dubvug changes --- Web/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Web/app.py b/Web/app.py index 13acc50..1d4fa4b 100755 --- a/Web/app.py +++ b/Web/app.py @@ -3314,7 +3314,7 @@ def api_library_items(): query = { 'ItemType': {'$in': ['book', 'cd', 'dvd', 'schoolbook', 'schulbuch', 'Buch', 'Schulbuch']}, - 'IsGroupedSubItem': {'$ne': True}, + #'IsGroupedSubItem': {'$ne': True}, 'Deleted': {'$ne': True} } From 01999575451deae7257bfae54bf9084fe953c56f Mon Sep 17 00:00:00 2001 From: AIIrondev Date: Tue, 11 Aug 2026 21:35:08 +0200 Subject: [PATCH 24/56] Dubvug changes --- Web/app.py | 2 +- Web/templates/library_table.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Web/app.py b/Web/app.py index 1d4fa4b..13acc50 100755 --- a/Web/app.py +++ b/Web/app.py @@ -3314,7 +3314,7 @@ def api_library_items(): query = { 'ItemType': {'$in': ['book', 'cd', 'dvd', 'schoolbook', 'schulbuch', 'Buch', 'Schulbuch']}, - #'IsGroupedSubItem': {'$ne': True}, + 'IsGroupedSubItem': {'$ne': True}, 'Deleted': {'$ne': True} } diff --git a/Web/templates/library_table.html b/Web/templates/library_table.html index fcd744b..bcb2eab 100644 --- a/Web/templates/library_table.html +++ b/Web/templates/library_table.html @@ -800,7 +800,7 @@ return ` ${escapeHtml(item.Name || 'Untitled')} - ${escapeHtml(Item.ISBN || '-')} + ${escapeHtml(item.ISBN || '-')} ${getItemTypeLabel(item.ItemType || 'book')} ${item.Quantity || item.GroupedDisplayCount || 1} From beeb562ac4b23390d2a09a2c92d3e27469199e11 Mon Sep 17 00:00:00 2001 From: AIIrondev Date: Tue, 11 Aug 2026 22:21:28 +0200 Subject: [PATCH 25/56] Dubvug changes --- Web/app.py | 69 +++++++++++++++++++++++------------------------------- 1 file changed, 29 insertions(+), 40 deletions(-) diff --git a/Web/app.py b/Web/app.py index 13acc50..9a28061 100755 --- a/Web/app.py +++ b/Web/app.py @@ -6392,12 +6392,10 @@ def item_edit(id): flash('Ungültige Element-ID.', 'error') return redirect(url_for('home_admin')) - db = it.db - items_col = db['items'] # --- GET: Template anzeigen --- if request.method == 'GET': - item = items_col.find_one({'_id': obj_id}) + item = it.get_item(id) if not item: flash('Element nicht gefunden.', 'error') return redirect(url_for('home_admin')) @@ -6416,13 +6414,13 @@ def item_edit(id): # --- POST: Speichern --- redirect_target = request.referrer or url_for('home_admin') - current_item = items_col.find_one({'_id': obj_id}) + current_item = it.get_item(obj_id) if not current_item: flash('Element in der Datenbank nicht gefunden.', 'error') return redirect(redirect_target) - # Formulardaten + # Formulardaten auslesen und bereinigen name = sanitize_form_value(request.form.get('name')) ort = sanitize_form_value(request.form.get('ort')) beschreibung = sanitize_form_value(request.form.get('beschreibung')) @@ -6440,12 +6438,15 @@ def item_edit(id): filter2 = expand_filter_selection(sanitize_form_value(request.form.getlist('filter2')), 2) filter3 = sanitize_form_value(request.form.getlist('filter3')) + # WICHTIG: Den aktuellen Verfügbarkeitsstatus aus der Datenbank übernehmen + verfuegbar = current_item.get('Verfuegbar', True) + # Barcode Prüfen if code_4 and not it.is_code_unique(code_4, exclude_id=str(id)): flash(f'Der Code "{code_4}" wird bereits verwendet.', 'error') return redirect(redirect_target) - # ISBN + # ISBN und Medientyp verarbeiten item_isbn = '' item_type = item_type_input or current_item.get('ItemType', 'general') if cfg.MODULES.is_enabled('library') and isbn_raw: @@ -6454,7 +6455,7 @@ def item_edit(id): flash('Ungültiges ISBN-Format.', 'error') return redirect(redirect_target) - # Bilder verarbeiten + # Bilder verarbeiten (GridFS) images_to_keep = request.form.getlist('existing_images') original_images = current_item.get('Images', []) images = [img for img in original_images if img in images_to_keep] @@ -6505,43 +6506,31 @@ def item_edit(id): if ort and ort not in it.get_predefined_locations(): it.add_predefined_location(ort) - # Datenstruktur für Update - shared_fields = { - 'Name': name, - 'Ort': ort, - 'Beschreibung': beschreibung, - 'Anschaffungsjahr': anschaffungs_jahr, - 'Anschaffungskosten': anschaffungs_kosten, - 'Reservierbar': reservierbar, - 'ISBN': item_isbn, - 'ItemType': item_type, - 'Kategorie': library_category, - 'LastUpdated': datetime.datetime.now() - } + success = it.update_item( + id=str(id), + name=name, + ort=ort, + beschreibung=beschreibung, + images=images, + verfuegbar=verfuegbar, + filter1=filter1, + filter2=filter2, + filter3=filter3, + ansch_jahr=anschaffungs_jahr, + ansch_kost=anschaffungs_kosten, + code_4=code_4, + reservierbar=reservierbar, + isbn=item_isbn, + item_type=item_type + ) - # Gruppen-Update - group_item_ids = it.get_group_item_ids(str(id)) + if success: + flash('Artikel erfolgreich aktualisiert.', 'success') + else: + flash('Fehler beim Aktualisieren des Artikels.', 'error') - if group_item_ids: - group_object_ids = [ObjectId(g_id) for g_id in group_item_ids] - items_col.update_many({'_id': {'$in': group_object_ids}}, {'$set': shared_fields}) - - # Individual-Update - individual_update = { - **shared_fields, - 'Code_4': code_4, - 'Images': images, - 'Filter1': filter1, - 'Filter2': filter2, - 'Filter3': filter3 - } - - items_col.update_one({'_id': obj_id}, {'$set': individual_update}) - - flash('Artikel erfolgreich aktualisiert.', 'success') return redirect(redirect_target) - @app.route('/update_group', methods=['POST']) def update_group(): From faf270ff93be018abe30fcf7e69c970ed0fe9b29 Mon Sep 17 00:00:00 2001 From: AIIrondev Date: Tue, 11 Aug 2026 22:35:43 +0200 Subject: [PATCH 26/56] Debug changes --- Web/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Web/app.py b/Web/app.py index 9a28061..dad920a 100755 --- a/Web/app.py +++ b/Web/app.py @@ -6404,7 +6404,7 @@ def item_edit(id): show_library = cfg.MODULES.is_enabled('library') return render_template( - 'item_edit.html', + 'edit_library.html', username=session['username'], item=item, library_module_enabled=show_library, From a2f2dd5a9e3f85716b7fe977bb8c18edc878a108 Mon Sep 17 00:00:00 2001 From: AIIrondev Date: Tue, 11 Aug 2026 22:41:30 +0200 Subject: [PATCH 27/56] Debug changes --- Web/templates/{edit_library.hmtl.html => edit_library.html} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Web/templates/{edit_library.hmtl.html => edit_library.html} (100%) diff --git a/Web/templates/edit_library.hmtl.html b/Web/templates/edit_library.html similarity index 100% rename from Web/templates/edit_library.hmtl.html rename to Web/templates/edit_library.html From 4917c22ae3520ea8dfb7952d746c67f8b93cd89a Mon Sep 17 00:00:00 2001 From: AIIrondev Date: Tue, 11 Aug 2026 22:49:40 +0200 Subject: [PATCH 28/56] Debug changes --- Web/templates/edit_library.html | 1 - 1 file changed, 1 deletion(-) diff --git a/Web/templates/edit_library.html b/Web/templates/edit_library.html index 233f951..26174ef 100644 --- a/Web/templates/edit_library.html +++ b/Web/templates/edit_library.html @@ -48,7 +48,6 @@ console.log("Server-provided duplicate data:", serverDuplicateData); } -{% endif %} + +
+
+

{{ page_title|default('Artikel bearbeiten') }}

+ +
+ + + + + {% if show_library_features %} + +
+ +
+ + + +
+ + +
+
+ {% endif %} + + +
+ + +
+ + +
+ + + + +
+ + +
+ + +
+ + +
+ +
+ + +
+ + +
+ + +
+ + + Der Scanner füllt zuerst den Basis-Code; weitere gescannte Codes werden hier automatisch zeilenweise angehängt. +
+ + {% if show_library_features %} + +
+

Medientyp

+
+ {% set current_type = item.ItemType if item.ItemType else '' %} + +
+ +

Kategorie / Fach:

+
+ +
+
+ {% else %} + +
+

Unterrichtsfach:

+
+ {% for i in range(1, 5) %} +
+ + +
+ {% endfor %} +
+ +

Jahrgangsstufe:

+
+ {% for i in range(1, 5) %} +
+ + +
+ {% endfor %} +
+ +

Schlagwörter:

+
+ {% for i in range(1, 5) %} +
+ + +
+ {% endfor %} +
+
+ {% endif %} + + +
+ + +
+ +
+ + +
+ + +
+ +
+ {% if item.Images %} + {% for img in item.Images %} +
+ + +
+ {% endfor %} + {% else %} + Keine Bilder vorhanden. + {% endif %} +
+ + + +
+
+ + +
+ + +
+ + +
+
+
+ + + + +{% endblock %} \ No newline at end of file diff --git a/Web/templates/edit_library.html b/Web/templates/edit_library.html index 2e20b48..b652ab2 100644 --- a/Web/templates/edit_library.html +++ b/Web/templates/edit_library.html @@ -1,395 +1,14 @@ {% extends "base.html" %} {% block title %}{{ page_title|default('Artikel bearbeiten') }} - Inventarsystem{% endblock %} {% block content %} - - -

{{ page_title|default('Artikel bearbeiten') }}

- -
- + - {% if show_library_features %} - +
- -
- - - + +
+ + +
- -
+ +
{% endif %} - +
- +
- +
- + {% if item.Ort %} + + {% endif %} - - - +
- +
- - + +
- +
- - - Der Scanner füllt zuerst den Basis-Code; weitere gescannte Codes werden hier automatisch zeilenweise angehängt. + + + Der Basis-Code steht oben. Alle weiteren Gruppenmitglieder werden hier untereinander aufgeführt.
{% if show_library_features %} - +

Medientyp

- {% set current_type = item.ItemType if item.ItemType else '' %} - + + + + +
-

Kategorie / Fach:

- +
{% else %} - +
-

Unterrichtsfach:

+

Unterrichtsfach (Filter 1):

- {% for i in range(1, 5) %} + {% for idx in range(4) %}
- - +
{% endfor %}
-

Jahrgangsstufe:

+

Jahrgangsstufe (Filter 2):

- {% for i in range(1, 5) %} + {% for idx in range(4) %}
- - +
{% endfor %}
-

Schlagwörter:

+

Schlagwort (Filter 3):

- {% for i in range(1, 5) %} + {% for idx in range(4) %}
- - + +
{% endfor %}
{% endif %} - +
- +
-
- +
- + {% if not show_library_features %} +
- -
- {% if item.Images %} - {% for img in item.Images %} -
- - -
- {% endfor %} - {% else %} - Keine Bilder vorhanden. - {% endif %} + + {% if item.Images and item.Images|length > 0 %} +
+ {% for img in item.Images %} +
+ Bild + +
+ {% endfor %}
+ {% else %} +

Keine Bilder vorhanden.

+ {% endif %} - - -
+ +
+ {% endif %} - +
- +
- +
- {% endblock %} \ No newline at end of file From 96d45710ac0df06de6a4b3a7faf5cae2c8f635dc Mon Sep 17 00:00:00 2001 From: AIIrondev Date: Wed, 12 Aug 2026 11:45:48 +0200 Subject: [PATCH 39/56] fix of the template name --- Web/app.py | 92 +++++++++++++++++++-------------- Web/modules/database/items.py | 13 +++++ Web/templates/edit_library.html | 39 ++++++-------- 3 files changed, 83 insertions(+), 61 deletions(-) diff --git a/Web/app.py b/Web/app.py index 888027f..47b56ad 100755 --- a/Web/app.py +++ b/Web/app.py @@ -6366,11 +6366,25 @@ def edit_item(id): return redirect(url_for('home_admin')) +def is_library_item(item): + """ + Prüft, ob ein Artikel ein Bibliotheks-Item ist. + - 'other', None oder Leerstring -> Inventarsystem (False) + - Jeder andere Medientyp ('Buch', 'CD', etc.) -> Bibliothek (True) + """ + if not item: + return False + item_type = item.get('ItemType', 'other') + if not item_type: + return False + return item_type.strip().lower() != 'other' + @app.route('/item_edit/', methods=['GET', 'POST']) def item_edit(id): """ - Endpoint zum Bearbeiten von Artikeln (unterstützt Bibliotheks- und Inventar-Modus). + Complete endpoint for editing items. Automatically detects whether the item + is a Library item (ItemType != 'other') or an Inventory item (ItemType == 'other'). """ if 'username' not in session: if request.method == 'POST' and request.is_json: @@ -6391,22 +6405,27 @@ def item_edit(id): flash('Ungültige Element-ID.', 'error') return redirect(url_for('home_admin')) - show_library = cfg.MODULES.is_enabled('library') + current_item = it.get_item(obj_id) + if not current_item: + flash('Element in der Datenbank nicht gefunden.', 'error') + return redirect(url_for('home_admin')) - # --- GET: Bearbeitungsformular laden --- + # Determine item type classification + library_module_active = cfg.MODULES.is_enabled('library') + is_lib_item = it.is_library_item(current_item) + show_library_features = library_module_active and is_lib_item + + # ------------------------------------------------------------------- + # GET METHOD: Render Form + # ------------------------------------------------------------------- if request.method == 'GET': - item = it.get_item(id) - if not item: - flash('Element nicht gefunden.', 'error') - return redirect(url_for('home_admin')) + current_item['_id'] = str(current_item['_id']) - item['_id'] = str(item['_id']) - - # Gruppen-Codes aufteilen (Basis-Code vs. Einzelcodes für die Textarea) - base_code = item.get('Code_4', '') + # Format individual group codes for the textarea + base_code = current_item.get('Code_4', '') individual_codes = [] - if item.get('SeriesGroupId'): - group_ids = it.get_group_item_ids(str(item['_id'])) + if current_item.get('SeriesGroupId'): + group_ids = it.get_group_item_ids(str(current_item['_id'])) if group_ids: for gid in group_ids: g_item = it.get_item(gid) @@ -6414,26 +6433,23 @@ def item_edit(id): if c4 and c4 != base_code: individual_codes.append(c4) - item['IndividualCodes'] = '\n'.join(individual_codes) + current_item['IndividualCodes'] = '\n'.join(individual_codes) return render_template( - 'edit_library.html', + 'item_edit.html', username=session['username'], - item=item, - show_library_features=show_library, - library_module_enabled=show_library, - page_title=f"Bearbeiten: {item.get('Name', '')}" + item=current_item, + show_library_features=show_library_features, + library_module_enabled=library_module_active, + page_title=f"Bearbeiten: {current_item.get('Name', '')}" ) - # --- POST: Änderungen speichern --- + # ------------------------------------------------------------------- + # POST METHOD: Save Changes + # ------------------------------------------------------------------- redirect_target = request.referrer or url_for('home_admin') - current_item = it.get_item(obj_id) - if not current_item: - flash('Element in der Datenbank nicht gefunden.', 'error') - return redirect(redirect_target) - - # Gemeinsame Felder auslesen & bereinigen + # Common fields name = sanitize_form_value(request.form.get('name')) ort = sanitize_form_value(request.form.get('ort')) beschreibung = sanitize_form_value(request.form.get('beschreibung')) @@ -6441,7 +6457,7 @@ def item_edit(id): anschaffungs_kosten = sanitize_form_value(request.form.get('anschaffungskosten')) reservierbar = 'reservierbar' in request.form - # Barcode & Gruppen-Codes + # Barcodes code_4 = sanitize_form_value(request.form.get('code_4')) individual_codes_raw = request.form.get('individual_codes', '') @@ -6453,7 +6469,7 @@ def item_edit(id): all_codes_to_check = [code_4] + individual_codes - # Uniqueness-Check für alle übergebenen Barcodes + # Barcode uniqueness check current_group_id = current_item.get('SeriesGroupId') client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) db_instance = client[cfg.MONGODB_DB] @@ -6476,9 +6492,9 @@ def item_edit(id): if has_code_error: return redirect(redirect_target) - # --- Modi-spezifische Auswertung --- - if show_library: - # 1. Bibliotheks-Modus + # Type-specific processing + if show_library_features: + # --- LIBRARY ITEM --- isbn_raw = sanitize_form_value(request.form.get('isbn', '')) item_isbn = normalize_and_validate_isbn(isbn_raw) if isbn_raw else '' item_type = sanitize_form_value(request.form.get('item_type_input', 'Buch')) @@ -6489,16 +6505,16 @@ def item_edit(id): filter3 = current_item.get('Filter3', []) images = current_item.get('Images', []) else: - # 2. Inventar-Modus + # --- INVENTORY ITEM --- item_isbn = current_item.get('ISBN', '') - item_type = current_item.get('ItemType', 'other') + item_type = 'other' # Standard type for inventory items library_category = current_item.get('library_category', '') filter1 = expand_filter_selection(sanitize_form_value(request.form.getlist('filter')), 1) filter2 = expand_filter_selection(sanitize_form_value(request.form.getlist('filter2')), 2) filter3 = sanitize_form_value(request.form.getlist('filter3')) - # Bilderverarbeitung für den Inventar-Modus + # Manage images for Inventory Mode images_to_keep = request.form.getlist('existing_images') original_images = current_item.get('Images', []) images = [img for img in original_images if img in images_to_keep] @@ -6537,16 +6553,16 @@ def item_edit(id): ) images.append(new_filename) except Exception as e: - app.logger.error(f"Bild-Fehler bei Item {id}: {e}") + app.logger.error(f"Image error for item {id}: {e}") - # Standort bei Bedarf zu vordefinierten Orten hinzufügen + # Auto-add new location to predefined locations list if applicable if ort and ort not in it.get_predefined_locations(): it.add_predefined_location(ort) - # 1. Gruppen-Codes synchronisieren + # Sync series/group barcode IDs it.sync_group_codes(str(id), code_4, individual_codes) - # 2. Datenbank-Eintrag aktualisieren + # Update item in database success = it.update_item( id=str(id), name=name, diff --git a/Web/modules/database/items.py b/Web/modules/database/items.py index 938d421..aecf205 100755 --- a/Web/modules/database/items.py +++ b/Web/modules/database/items.py @@ -27,6 +27,19 @@ from Web.modules.database.settings import MongoClient import Web.modules.inventarsystem.data_protection as dp +def is_library_item(item): + """ + Determines if an item belongs to the library system. + Returns False for 'other', None, or empty ItemType (Inventory item). + Returns True for any specific library type ('Buch', 'CD', 'DVD', etc.). + """ + if not item: + return False + item_type = item.get('ItemType', 'other') + if not item_type: + return False + return str(item_type).strip().lower() != 'other' + def safe_decrypt_user(encrypted_user): """ Safely decrypt an encrypted username string. diff --git a/Web/templates/edit_library.html b/Web/templates/edit_library.html index b652ab2..9b4c1ec 100644 --- a/Web/templates/edit_library.html +++ b/Web/templates/edit_library.html @@ -103,7 +103,7 @@ background-color: #0056b3; } - /* Scanner Video Styles */ + /* Scanner Video Canvas */ #code4-scanner video, #code4-scanner canvas, #isbn-scanner video, #isbn-scanner canvas { width: 100%; @@ -124,7 +124,7 @@ overflow: hidden; } - /* Image management grid */ + /* Image Management */ .existing-images-grid { display: flex; flex-wrap: wrap; @@ -197,7 +197,7 @@ {% if show_library_features %} - +
@@ -211,13 +211,12 @@
{% endif %} - +
-
-
@@ -251,7 +248,6 @@
-
@@ -259,7 +255,7 @@
{% if show_library_features %} - +

Medientyp

@@ -268,7 +264,7 @@ - +

Kategorie / Fach:

@@ -277,7 +273,7 @@
{% else %} - +

Unterrichtsfach (Filter 1):

@@ -315,7 +311,7 @@
{% endif %} - +
@@ -326,7 +322,7 @@
{% if not show_library_features %} - +
{% if item.Images and item.Images|length > 0 %} @@ -350,7 +346,6 @@
{% endif %} -
@@ -369,7 +364,7 @@ let code4LastScanned = ''; let code4LastScannedAt = 0; - // Filter-Optionen vom Server laden & bestehende Werte vorauswählen + // Load filter dropdown options and pre-select current item values function loadAndSelectFilterValues(filterNumber) { fetch(`/get_predefined_filter_values/${filterNumber}`) .then(res => res.json()) @@ -380,7 +375,6 @@ const selectedValue = select.getAttribute('data-selected') || ''; - // Optionen befüllen data.values.forEach(val => { if (!val || String(val).trim() === '') return; const opt = document.createElement('option'); @@ -389,7 +383,7 @@ select.appendChild(opt); }); - // Falls der gewählte Wert nicht in den Vorgaben war, als Option ergänzen + // Add custom option if the item has a value not in predefined list if (selectedValue && !Array.from(select.options).some(o => o.value === selectedValue)) { const customOpt = document.createElement('option'); customOpt.value = selectedValue; @@ -400,10 +394,10 @@ select.value = selectedValue; } }) - .catch(err => console.error(`Fehler beim Laden von Filter ${filterNumber}:`, err)); + .catch(err => console.error(`Error loading Filter ${filterNumber}:`, err)); } - // Orte vom Server laden + // Load locations function loadLocationOptions() { fetch('/get_predefined_locations') .then(res => res.json()) @@ -423,10 +417,10 @@ select.value = currentVal; }) - .catch(err => console.error('Fehler beim Laden der Orte:', err)); + .catch(err => console.error('Error loading locations:', err)); } - // Ort hinzufügen UI + // New location handler function addNewLocation() { const input = document.getElementById('new-location-input'); const val = input.value.trim(); @@ -446,7 +440,7 @@ document.getElementById('new-location-input').value = ''; } - // Quagga Barcode Scanner Initialisierung + // Quagga Barcode Scanner Engine function runEngineInitialization(targetSelector, activeCallback, completionMsg, errorStatusSetter) { if (scannerRunning) { Quagga.stop(); scannerRunning = false; } activeScannerCallback = activeCallback; @@ -470,7 +464,6 @@ if (typeof activeScannerCallback === "function") activeScannerCallback(String(data.codeResult.code).trim()); }); - // Barcode Scanner für Basis-Code function startCode4Scanner() { const scannerBox = document.getElementById('code4-scanner'); const scanBtn = document.getElementById('scan-code4-btn'); From 6a3865ef24a6bc3001d9bba17645610bb47d1657 Mon Sep 17 00:00:00 2001 From: AIIrondev Date: Wed, 12 Aug 2026 11:46:59 +0200 Subject: [PATCH 40/56] fix of the template name --- Web/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Web/app.py b/Web/app.py index 47b56ad..2a67695 100755 --- a/Web/app.py +++ b/Web/app.py @@ -6436,7 +6436,7 @@ def item_edit(id): current_item['IndividualCodes'] = '\n'.join(individual_codes) return render_template( - 'item_edit.html', + 'edit_library.html', username=session['username'], item=current_item, show_library_features=show_library_features, From 9452743660276e4c0c07108e66b96b2aa79849b9 Mon Sep 17 00:00:00 2001 From: AIIrondev Date: Wed, 12 Aug 2026 11:59:57 +0200 Subject: [PATCH 41/56] Changes to reflekt the right inventar and bibliothek item fields --- Web/app.py | 35 ++++++++++--------- Web/templates/edit_library.html | 59 ++++++++++++++------------------- 2 files changed, 42 insertions(+), 52 deletions(-) diff --git a/Web/app.py b/Web/app.py index 2a67695..89a0f82 100755 --- a/Web/app.py +++ b/Web/app.py @@ -6383,8 +6383,8 @@ def is_library_item(item): @app.route('/item_edit/', methods=['GET', 'POST']) def item_edit(id): """ - Complete endpoint for editing items. Automatically detects whether the item - is a Library item (ItemType != 'other') or an Inventory item (ItemType == 'other'). + Complete endpoint for editing items. Processes Filters 1-3 for ALL items, + and applies Library-specific fields when the item is a Library item. """ if 'username' not in session: if request.method == 'POST' and request.is_json: @@ -6410,7 +6410,7 @@ def item_edit(id): flash('Element in der Datenbank nicht gefunden.', 'error') return redirect(url_for('home_admin')) - # Determine item type classification + # Determine item classification library_module_active = cfg.MODULES.is_enabled('library') is_lib_item = it.is_library_item(current_item) show_library_features = library_module_active and is_lib_item @@ -6457,7 +6457,12 @@ def item_edit(id): anschaffungs_kosten = sanitize_form_value(request.form.get('anschaffungskosten')) reservierbar = 'reservierbar' in request.form - # Barcodes + # System Filters 1, 2, and 3 (Processed for ALL items) + filter1 = expand_filter_selection(sanitize_form_value(request.form.getlist('filter')), 1) + filter2 = expand_filter_selection(sanitize_form_value(request.form.getlist('filter2')), 2) + filter3 = sanitize_form_value(request.form.getlist('filter3')) + + # Barcodes & Group Codes code_4 = sanitize_form_value(request.form.get('code_4')) individual_codes_raw = request.form.get('individual_codes', '') @@ -6494,26 +6499,18 @@ def item_edit(id): # Type-specific processing if show_library_features: - # --- LIBRARY ITEM --- + # --- LIBRARY ITEM SPECIFICS --- isbn_raw = sanitize_form_value(request.form.get('isbn', '')) item_isbn = normalize_and_validate_isbn(isbn_raw) if isbn_raw else '' item_type = sanitize_form_value(request.form.get('item_type_input', 'Buch')) library_category = sanitize_form_value(request.form.get('library_category', '')) - - filter1 = current_item.get('Filter', []) - filter2 = current_item.get('Filter2', []) - filter3 = current_item.get('Filter3', []) images = current_item.get('Images', []) else: - # --- INVENTORY ITEM --- + # --- INVENTORY ITEM SPECIFICS --- item_isbn = current_item.get('ISBN', '') - item_type = 'other' # Standard type for inventory items + item_type = 'other' library_category = current_item.get('library_category', '') - filter1 = expand_filter_selection(sanitize_form_value(request.form.getlist('filter')), 1) - filter2 = expand_filter_selection(sanitize_form_value(request.form.getlist('filter2')), 2) - filter3 = sanitize_form_value(request.form.getlist('filter3')) - # Manage images for Inventory Mode images_to_keep = request.form.getlist('existing_images') original_images = current_item.get('Images', []) @@ -6555,14 +6552,14 @@ def item_edit(id): except Exception as e: app.logger.error(f"Image error for item {id}: {e}") - # Auto-add new location to predefined locations list if applicable + # Auto-add location to predefined locations list if applicable if ort and ort not in it.get_predefined_locations(): it.add_predefined_location(ort) - # Sync series/group barcode IDs + # Sync group barcodes it.sync_group_codes(str(id), code_4, individual_codes) - # Update item in database + # Save to MongoDB success = it.update_item( id=str(id), name=name, @@ -6588,6 +6585,8 @@ def item_edit(id): flash('Fehler beim Aktualisieren des Artikels.', 'error') return redirect(redirect_target) + + @app.route('/update_group', methods=['POST']) def update_group(): diff --git a/Web/templates/edit_library.html b/Web/templates/edit_library.html index 9b4c1ec..1f1bb5d 100644 --- a/Web/templates/edit_library.html +++ b/Web/templates/edit_library.html @@ -103,7 +103,7 @@ background-color: #0056b3; } - /* Scanner Video Canvas */ + /* Scanner Elements */ #code4-scanner video, #code4-scanner canvas, #isbn-scanner video, #isbn-scanner canvas { width: 100%; @@ -197,7 +197,7 @@ {% if show_library_features %} - +
@@ -209,9 +209,26 @@
+ +
+

Medientyp

+
+ +
+

Bibliotheks-Kategorie:

+
+ +
+
{% endif %} - +
@@ -254,26 +271,7 @@ Der Basis-Code steht oben. Alle weiteren Gruppenmitglieder werden hier untereinander aufgeführt.
- {% if show_library_features %} - -
-

Medientyp

-
- -
-

Kategorie / Fach:

-
- -
-
- {% else %} - +

Unterrichtsfach (Filter 1):

@@ -309,7 +307,6 @@ {% endfor %}
- {% endif %}
@@ -358,13 +355,12 @@ diff --git a/Web/templates/main_admin.html b/Web/templates/main_admin.html index 205bbd4..534ac8a 100755 --- a/Web/templates/main_admin.html +++ b/Web/templates/main_admin.html @@ -1951,11 +1951,6 @@ display: none; } - /* Edit new location container */ - .edit-new-location-container { - display: none; - margin-top: 10px; - } /* Modal dialog styling */ .modal-dialog-white { @@ -1969,11 +1964,6 @@ margin-top: 10px; } - /* Element text colors for better visibility */ - .edit-button, .duplicate-button, .generate-qr-button { - color: var(--ui-title) !important; - } - /* Standardized button styles across the application */ .search-button, .scan-button, .filter-toggle, .clear-filter, .add-new-btn, .popup-close-button, .prev-image-button, .next-image-button, @@ -2474,187 +2464,6 @@ document.addEventListener('DOMContentLoaded', ()=>{
- - - {% if current_permissions.actions.get('can_edit', False) %} -
- -
- {% endif %}
@@ -2889,58 +2698,6 @@ document.addEventListener('DOMContentLoaded', ()=>{ } }); - function scanIntoEditCode() { - const qrReader = document.getElementById('qr-reader'); - const editCodeInput = document.getElementById('edit-code4'); - const scanEditBtn = document.getElementById('scan-edit-code-btn'); - if (!qrReader || !editCodeInput || !scanEditBtn) { - return; - } - - // Toggle close if it's already running - if (isScanning && qrReader.style.display !== 'none') { - stopScanner(); - scanEditBtn.textContent = 'Barcode scannen'; - return; - } - - scanEditBtn.textContent = 'Scanner schließen'; - - // Start scanner with custom logic mapping to the Code input field - startScanner(function(decodedText) { - editCodeInput.value = decodedText; - validateCodeField(editCodeInput, document.getElementById('edit-item-id')?.value || null); - scanEditBtn.textContent = 'Barcode scannen'; - }); - } - - function scanIntoEditIsbn() { - const qrReader = document.getElementById('qr-reader'); - const editIsbnInput = document.getElementById('edit-isbn'); - const scanIsbnBtn = document.getElementById('scan-edit-isbn-btn'); - if (!qrReader || !editIsbnInput || !scanIsbnBtn) { - return; - } - - // Toggle close if it's already running - if (isScanning && qrReader.style.display !== 'none') { - stopScanner(); - scanIsbnBtn.textContent = 'ISBN scannen'; - return; - } - - scanIsbnBtn.textContent = 'Scanner schließen'; - - // Start scanner with custom logic mapping to the ISBN input field - startScanner(function(decodedText) { - editIsbnInput.value = decodedText; - scanIsbnBtn.textContent = 'ISBN scannen'; - if (typeof fetchBookInfo === 'function') { - fetchBookInfo('edit'); - } - }); - } - function rebuildFilter3Options() { if (!allItems) return; @@ -3324,21 +3081,11 @@ document.addEventListener('DOMContentLoaded', ()=>{ loadPredefinedFilterValues(1); loadPredefinedFilterValues(2); loadPredefinedFilterValues(3); - - // Set up edit form submission - setupEditFormSubmission(); + // Set up schedule form submission setupScheduleFormSubmission(); - - // Set up add new location buttons - const editAddLocationBtn = document.getElementById('edit-add-new-location-btn'); - if (editAddLocationBtn) { - editAddLocationBtn.addEventListener('click', function() { - const container = document.getElementById('edit-new-location-container'); - container.style.display = container.style.display === 'none' ? 'block' : 'none'; - }); - } + // Find and attach event listener to all logout links const logoutLinks = document.querySelectorAll('a[href*="logout"]'); @@ -3391,35 +3138,11 @@ document.addEventListener('DOMContentLoaded', ()=>{ // Close modals when clicking outside window.onclick = function(event) { const itemModal = document.getElementById('item-modal'); - const editModal = document.getElementById('edit-modal'); if (event.target === itemModal) { itemModal.style.display = 'none'; } - if (event.target === editModal) { - editModal.style.display = 'none'; - } }; - - // Set up code validation for edit form - const editCodeField = document.getElementById('edit-code4'); - if (editCodeField) { - editCodeField.addEventListener('blur', function() { - const itemIdField = document.getElementById('edit-item-id'); - const excludeId = itemIdField ? itemIdField.value : null; - validateCodeField(this, excludeId); - }); - } - - const scanEditCodeBtn = document.getElementById('scan-edit-code-btn'); - if (scanEditCodeBtn) { - scanEditCodeBtn.addEventListener('click', scanIntoEditCode); - } - - const scanEditIsbnBtn = document.getElementById('scan-edit-isbn-btn'); - if (scanEditIsbnBtn) { - scanEditIsbnBtn.addEventListener('click', scanIntoEditIsbn); - } }); // Function to load items from server @@ -4171,163 +3894,6 @@ document.addEventListener('DOMContentLoaded', ()=>{ }); } - function openEditModalForSelectedUnit(defaultItemId, selectId) { - let targetItemId = defaultItemId; - - try { - const selectedUnit = selectId ? document.getElementById(selectId) : null; - if (selectedUnit && selectedUnit.value) { - targetItemId = selectedUnit.value; - } - } catch (e) { - // Keep default item id as fallback. - } - - openEditModalFromServer(targetItemId); - } - - function closeEditModal() { - const editModal = document.getElementById('edit-modal'); - if (editModal) { - editModal.style.display = 'none'; - } - } - - function openEditModalFromServer(itemId) { - const editModal = document.getElementById('edit-modal'); - if (!editModal) { - console.error('Edit modal nicht gefunden'); - return; - } - - console.log('DEBUG: openEditModal called with itemId:', itemId); - - // Fetch the item data from the backend - fetch(`/get_item/${itemId}`) - .then(response => { - console.log('DEBUG: Response status:', response.status); - if (!response.ok) throw new Error(`HTTP ${response.status}`); - return response.json(); - }) - .then(data => { - console.log('DEBUG: Fetched data:', data); - // Backend returns the item directly or wrapped in error/success - const item = data.error ? null : (data.item || data); - - console.log('DEBUG: Parsed item:', item); - - if (!item || !item._id) { - console.error('DEBUG: Item not found or invalid'); - alert('Item nicht gefunden'); - return; - } - - // Fill in the form fields with the item data - document.getElementById('edit-item-id').value = item._id || ''; - document.getElementById('edit-name').value = item.Name || ''; - document.getElementById('edit-location').value = item.Ort || ''; - document.getElementById('edit-description').value = item.Beschreibung || ''; - document.getElementById('edit-year').value = item.Anschaffungsjahr || ''; - document.getElementById('edit-cost').value = item.Anschaffungskosten || ''; - document.getElementById('edit-code4').value = item.Code_4 || ''; - document.getElementById('edit-isbn').value = item.ISBN || ''; - document.getElementById('edit-reservierbar').checked = item.Reservierbar !== false; - - // Fill in filter 1 (Unterrichtsfach) - const filter1Array = Array.isArray(item.Filter) ? item.Filter : (item.Filter ? [item.Filter] : []); - document.getElementById('edit-filter1-1').value = filter1Array[0] || ''; - document.getElementById('edit-filter1-2').value = filter1Array[1] || ''; - document.getElementById('edit-filter1-3').value = filter1Array[2] || ''; - document.getElementById('edit-filter1-4').value = filter1Array[3] || ''; - - // Fill in filter 2 (Jahrgangsstufe) - const filter2Array = Array.isArray(item.Filter2) ? item.Filter2 : (item.Filter2 ? [item.Filter2] : []); - document.getElementById('edit-filter2-1').value = filter2Array[0] || ''; - document.getElementById('edit-filter2-2').value = filter2Array[1] || ''; - document.getElementById('edit-filter2-3').value = filter2Array[2] || ''; - document.getElementById('edit-filter2-4').value = filter2Array[3] || ''; - - // Fill in filter 3 (Schlagwort) - const filter3Array = Array.isArray(item.Filter3) ? item.Filter3 : (item.Filter3 ? [item.Filter3] : []); - document.getElementById('edit-filter3-1').value = filter3Array[0] || ''; - document.getElementById('edit-filter3-2').value = filter3Array[1] || ''; - document.getElementById('edit-filter3-3').value = filter3Array[2] || ''; - document.getElementById('edit-filter3-4').value = filter3Array[3] || ''; - - // Display existing images - const existingImagesContainer = document.getElementById('edit-existing-images'); - const editForm = document.getElementById('edit-item-form'); - existingImagesContainer.innerHTML = ''; - if (editForm) { - editForm.querySelectorAll('input[name="existing_images"], input[name="removed_images"]').forEach(input => input.remove()); - } - if (item.Images && Array.isArray(item.Images)) { - item.Images.forEach((image, index) => { - const isVideo = isVideoFile(image); - const imageDiv = document.createElement('div'); - imageDiv.className = 'existing-image-item'; - imageDiv.style.marginBottom = '10px'; - - const thumbnailInfo = item.ThumbnailInfo && item.ThumbnailInfo[index]; - const imageSrc = thumbnailInfo && thumbnailInfo.has_preview ? - thumbnailInfo.preview_url : - (image.startsWith('/uploads/') || image.startsWith('http') ? - image : - `{{ url_for('uploaded_file', filename='') }}${image}`); - - const row = document.createElement('div'); - row.style.display = 'flex'; - row.style.gap = '8px'; - row.style.alignItems = 'center'; - - if (isVideo) { - const video = document.createElement('video'); - video.src = imageSrc; - video.style.maxWidth = '100px'; - video.style.maxHeight = '100px'; - video.style.objectFit = 'contain'; - video.controls = true; - row.appendChild(video); - } else { - const img = document.createElement('img'); - img.src = imageSrc; - img.style.maxWidth = '100px'; - img.style.maxHeight = '100px'; - img.style.objectFit = 'contain'; - img.alt = `Existierendes Bild ${index + 1}`; - row.appendChild(img); - } - - const deleteButton = document.createElement('button'); - deleteButton.type = 'button'; - deleteButton.className = 'delete-image-button'; - deleteButton.textContent = 'Löschen'; - deleteButton.addEventListener('click', () => removeExistingImage(image, deleteButton)); - row.appendChild(deleteButton); - - imageDiv.appendChild(row); - existingImagesContainer.appendChild(imageDiv); - - if (editForm) { - const hiddenInput = document.createElement('input'); - hiddenInput.type = 'hidden'; - hiddenInput.name = 'existing_images'; - hiddenInput.value = image; - editForm.appendChild(hiddenInput); - } - - }); - } - - // Display the modal - editModal.style.display = 'block'; - }) - .catch(error => { - console.error('Fehler beim Laden des Items:', error); - alert('Fehler beim Laden des Items'); - }); - } - function escapeHtml(value) { return String(value ?? '').replace(/[&<>'"]/g, (char) => { const map = { @@ -4687,7 +4253,9 @@ document.addEventListener('DOMContentLoaded', ()=>{
`; - + + + modal.style.display = 'block'; const closeButton = modal.querySelector('.close-modal'); @@ -4924,6 +4492,13 @@ document.addEventListener('DOMContentLoaded', ()=>{ }); } + function openEditModalForSelectedUnit(itemId, selectId) { + const select = document.getElementById(selectId); + const targetId = (select && select.value) ? select.value : itemId; + // Leitet auf die Bearbeiten-Seite weiter und übergibt die aktuelle URL für den Redirect nach dem Speichern + window.location.href = `/item_edit/${targetId}`; + } + function changeModalImage(direction) { const currentIndex = window.currentModalImageIndex; const total = window.totalModalImages; @@ -5350,8 +4925,6 @@ document.addEventListener('DOMContentLoaded', ()=>{ }); } - // Load location options for edit modal - // Edit-related functions moved to edit_item_functions.html // Schedule modal functions function openScheduleModal(itemId) { @@ -5471,50 +5044,6 @@ document.addEventListener('DOMContentLoaded', ()=>{ } } - // Setup edit form submission - function setupEditFormSubmission() { - const editForm = document.getElementById('edit-item-form'); - if (editForm) { - editForm.addEventListener('submit', function(e) { - e.preventDefault(); - - const itemId = document.getElementById('edit-item-id').value; - const formData = new FormData(this); - - fetch(`/edit_item/${itemId}`, { - method: 'POST', - body: formData - }) - .then(response => { - if (response.ok) { - closeEditModal(); - // Reload items to show updated information - loadItems(); - // Show success message - const successMsg = document.createElement('div'); - successMsg.className = 'alert alert-success'; - successMsg.textContent = 'Item wurde erfolgreich aktualisiert!'; - successMsg.style.position = 'fixed'; - successMsg.style.top = '20px'; - successMsg.style.right = '20px'; - successMsg.style.zIndex = '9999'; - document.body.appendChild(successMsg); - setTimeout(() => { - if (successMsg.parentNode) { - successMsg.parentNode.removeChild(successMsg); - } - }, 3000); - } else { - alert('Fehler beim Aktualisieren des Items'); - } - }) - .catch(error => { - console.error('Error updating item:', error); - alert('Fehler beim Aktualisieren des Items'); - }); - }); - } - } // Duplication function function duplicateItem(itemId) { @@ -5690,9 +5219,6 @@ document.addEventListener('DOMContentLoaded', ()=>{ } - -{% include "edit_item_functions.html" %} - {% include "reset_item_functions.html" %} @@ -5920,16 +5446,4 @@ document.addEventListener('DOMContentLoaded', ()=>{ } } - -{% if open_item %} - -{% endif %} {% endblock %} From 5ba0944faf03f8d8ca4d38647883fbf13ecbce92 Mon Sep 17 00:00:00 2001 From: AIIrondev Date: Wed, 12 Aug 2026 15:46:31 +0200 Subject: [PATCH 47/56] implementation of the user .csv importing --- Web/app.py | 200 +++++++++++++++++++ Web/templates/register.html | 373 ++++++++++++++++++++---------------- 2 files changed, 407 insertions(+), 166 deletions(-) diff --git a/Web/app.py b/Web/app.py index cb1bc23..209a970 100755 --- a/Web/app.py +++ b/Web/app.py @@ -27,6 +27,10 @@ import os import sys from bs4 import BeautifulSoup from gridfs import GridFS +import string +from reportlab.lib.pagesizes import A4 +from reportlab.pdfgen import canvas +from reportlab.lib import colors # Ensure imports work regardless of whether gunicorn starts in /app or /app/Web. _CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -7861,6 +7865,202 @@ def register(): permission_page_options=PERMISSION_PAGE_OPTIONS ) +def generate_compliant_password(length=16): + lowers = string.ascii_lowercase + uppers = string.ascii_uppercase + digits = string.digits + symbols = "!@#$%^&*()_+~|}{[]:;?><,.-=" + + # Ensure at least one character from each required category + pwd = [ + secrets.choice(lowers), + secrets.choice(uppers), + secrets.choice(digits), + secrets.choice(symbols) + ] + all_chars = lowers + uppers + digits + symbols + pwd += [secrets.choice(all_chars) for _ in range(length - 4)] + + # Shuffle so guaranteed types aren't always at the start + secrets.SystemRandom().shuffle(pwd) + return "".join(pwd) + +def generate_credentials_pdf(created_users): + """ + Creates a PDF in memory with 2 user credential cards per A4 page. + created_users: list of dicts [{'name': ..., 'last_name': ..., 'username': ..., 'password': ...}] + """ + buffer = io.BytesIO() + pdf = canvas.Canvas(buffer, pagesize=A4) + width, height = A4 # 595.27 x 841.89 points + card_height = height / 2.0 # Split page into 2 equal halves + + for i, user in enumerate(created_users): + page_slot = i % 2 # 0 = Top half, 1 = Bottom half + + # If starting a new page (except for the very first item) + if i > 0 and page_slot == 0: + pdf.showPage() + + # Calculate Y offset for card position + y_offset = height - (page_slot + 1) * card_height + + # Card Container Box + margin = 35 + box_x = margin + box_y = y_offset + margin + box_w = width - (2 * margin) + box_h = card_height - (2 * margin) + + # Outer Border + pdf.setStrokeColor(colors.HexColor('#CBD5E1')) + pdf.setLineWidth(1) + pdf.rect(box_x, box_y, box_w, box_h, fill=0) + + # Header Banner inside Card + pdf.setFillColor(colors.HexColor('#1E293B')) + pdf.rect(box_x, box_y + box_h - 45, box_w, 45, fill=1, stroke=0) + + # Header Title Text + pdf.setFillColor(colors.white) + pdf.setFont("Helvetica-Bold", 14) + pdf.drawString(box_x + 20, box_y + box_h - 28, "Zugangsdaten / Account Credentials") + + # User Info Details + content_y = box_y + box_h - 80 + + # Name + pdf.setFillColor(colors.HexColor('#0F172A')) + pdf.setFont("Helvetica-Bold", 12) + pdf.drawString(box_x + 25, content_y, f"Name: {user['name']} {user['last_name']}") + + # Username + content_y -= 35 + pdf.setFont("Helvetica", 11) + pdf.setFillColor(colors.HexColor('#475569')) + pdf.drawString(box_x + 25, content_y, "Benutzername:") + pdf.setFont("Helvetica-Bold", 13) + pdf.setFillColor(colors.HexColor('#0F172A')) + pdf.drawString(box_x + 160, content_y, user['username']) + + # Password + content_y -= 30 + pdf.setFont("Helvetica", 11) + pdf.setFillColor(colors.HexColor('#475569')) + pdf.drawString(box_x + 25, content_y, "Passwort:") + pdf.setFont("Courier-Bold", 13) + pdf.setFillColor(colors.HexColor('#0F172A')) + pdf.drawString(box_x + 160, content_y, user['password']) + + # Security Footer Note + content_y -= 45 + pdf.setFont("Helvetica-Oblique", 9) + pdf.setFillColor(colors.HexColor('#64748B')) + pdf.drawString(box_x + 25, content_y, "Hinweis: Bitte ändern Sie Ihr Passwort nach der ersten Anmeldung.") + + # Dashed Cut Line between top and bottom cards + if page_slot == 0 and i < len(created_users) - 1: + pdf.setDash(4, 4) + pdf.setStrokeColor(colors.HexColor('#94A3B8')) + pdf.line(0, card_height, width, card_height) + pdf.setDash() # Reset dash + + pdf.save() + buffer.seek(0) + return buffer + + +@app.route('/register/csv', methods=['POST']) +def register_csv(): + if 'username' not in session: + flash('Ihnen ist es nicht gestattet, diese Aktion auszuführen.', 'error') + return redirect(url_for('login')) + + file = request.files.get('csv_file') + if not file or not file.filename.endswith('.csv'): + flash('Bitte laden Sie eine gültige CSV-Datei hoch.', 'error') + return redirect(url_for('register')) + + permission_preset = (request.form.get('permission_preset') or 'standard_user').strip() + + created_users = [] + + try: + # Read file stream handling UTF-8 and BOM (Excel exports) + file_stream = io.StringIO(file.stream.read().decode("utf-8-sig"), newline=None) + + # Sniff delimiter (comma or semicolon) + sample = file_stream.read(2048) + file_stream.seek(0) + delimiter = ';' if ';' in sample else ',' + + reader = csv.reader(file_stream, delimiter=delimiter) + + for row_num, row in enumerate(reader, start=1): + if not row or all(field.strip() == '' for field in row): + continue # Skip empty rows + + # Clean entries + row = [field.strip() for field in row] + + # Header row detection (Skip row if it looks like "Name, Nachname") + if row_num == 1 and ('name' in row[0].lower() or 'vorname' in row[0].lower()): + continue + + if len(row) < 2: + continue + + name = row[0] + last_name = row[1] + + if not name or not last_name: + continue + + # 1. Generate Username via backend logic + username = us.build_unique_username_from_name(name, last_name) + + # 2. Generate Random Secure Password + password = generate_compliant_password(16) + + # 3. Add User to DB + success = us.add_user( + username=username, + password=password, + name=name, + last_name=last_name, + is_student=False, + student_card_id=None, + max_borrow_days=None, + permission_preset=permission_preset, + ) + + if success: + created_users.append({ + 'name': name, + 'last_name': last_name, + 'username': username, + 'password': password + }) + + except Exception as e: + flash(f'Fehler beim Verarbeiten der CSV-Datei: {str(e)}', 'error') + return redirect(url_for('register')) + + if not created_users: + flash('Keine gültigen Benutzer in der CSV-Datei gefunden.', 'error') + return redirect(url_for('register')) + + # Generate PDF + pdf_buffer = generate_credentials_pdf(created_users) + + # Return downloadable PDF file + return send_file( + pdf_buffer, + as_attachment=True, + download_name='benutzer_zugangsdaten.pdf', + mimetype='application/pdf' + ) + @app.route('/user_del', methods=['GET']) def user_del(): """ diff --git a/Web/templates/register.html b/Web/templates/register.html index dcaf06f..ab3ceb8 100755 --- a/Web/templates/register.html +++ b/Web/templates/register.html @@ -6,7 +6,7 @@

Neuen Benutzer registrieren

-

Erstellen Sie ein neues Benutzerkonto und legen Sie Zugriffsrechte fest

+

Erstellen Sie ein neues Benutzerkonto oder importieren Sie mehrere Benutzer per CSV

@@ -23,7 +23,43 @@
+ +
+
+

Massenregistrierung via CSV

+

+ Laden Sie eine CSV-Datei hoch (Format: Vorname, Nachname). + Benutzernamen und sichere Passwörter werden serverseitig generiert. Nach dem Upload erhalten Sie direkt ein PDF mit Zugangsdaten (2 pro Seite zum Ausschneiden). +

+
+ +
+
+ + + + +
+ 📄 + +
+
+ +
+ +
+
+
+ +
+

Einzelnen Benutzer registrieren

@@ -32,11 +68,13 @@ 👤
+
👤
+
👤 @@ -44,7 +82,7 @@

Klarnamen werden nur zur Erzeugung des Benutzernamens als Kürzel (z.B. SimFri) verwendet; bei Kollision wird automatisch ein Buchstabe mehr genommen.

- +
@@ -57,20 +95,21 @@
  • Mindestens ein Sonderzeichen
  • - +
    🔒 - - +
    -
    +
    @@ -120,6 +159,163 @@
    + + - {% endblock %} \ No newline at end of file From cdf7b9c45d05461e497efd7cd10ee7eb2f679080 Mon Sep 17 00:00:00 2001 From: AIIrondev Date: Wed, 12 Aug 2026 16:09:52 +0200 Subject: [PATCH 48/56] slight correction to also account for name and last_name --- Web/app.py | 171 +++++++++++++------- Web/templates/register.html | 315 +++++++++++++++++++++++------------- 2 files changed, 313 insertions(+), 173 deletions(-) diff --git a/Web/app.py b/Web/app.py index 209a970..9a021cb 100755 --- a/Web/app.py +++ b/Web/app.py @@ -7865,6 +7865,82 @@ def register(): permission_page_options=PERMISSION_PAGE_OPTIONS ) +def parse_csv_users(file_bytes): + """ + Liest CSV-Dateien extrem robust ein: + - Erkennt automatisch UTF-8, UTF-8-SIG und Latin-1 (für Excel-Umlaute). + - Erkennt automatisch das Trennzeichen (; oder , oder Tab). + - Erkennt Vor- und Nachname unabhängig von der Spaltenreihenfolge und Headernamen. + """ + # 1. Dekodierung mit Fallback für deutsche Excel-Dateien + try: + content = file_bytes.decode('utf-8-sig') + except UnicodeDecodeError: + content = file_bytes.decode('latin-1') + + lines = [line.strip() for line in content.splitlines() if line.strip()] + if not lines: + return [] + + # 2. Trennzeichen ermitteln (; , oder \t) + first_line = lines[0] + if first_line.count(';') >= first_line.count(','): + delimiter = ';' + elif first_line.count('\t') > first_line.count(','): + delimiter = '\t' + else: + delimiter = ',' + + stream = io.StringIO(content, newline=None) + reader = csv.DictReader(stream, delimiter=delimiter) + + parsed_users = [] + + # 3. Auswertung mit DictReader (wenn Header vorhanden sind) + if reader.fieldnames: + for row in reader: + # Keys normalisieren (kleingeschrieben, ohne Leerzeichen) + cleaned_row = {str(k).strip().lower(): str(v).strip() for k, v in row.items() if k and v} + + # Dynamisches Mapping für Vornamen + name = ( + cleaned_row.get('vorname') or + cleaned_row.get('first_name') or + cleaned_row.get('firstname') or + cleaned_row.get('name') or '' + ) + + # Dynamisches Mapping für Nachnamen + last_name = ( + cleaned_row.get('nachname') or + cleaned_row.get('last_name') or + cleaned_row.get('lastname') or + cleaned_row.get('surname') or + cleaned_row.get('familienname') or '' + ) + + # Falls Header nicht erkannt wurden, aber Werte vorhanden sind (z.B. CSV ohne Header) + if not name and not last_name: + vals = [str(v).strip() for v in row.values() if v] + if len(vals) >= 2: + name, last_name = vals[0], vals[1] + + if name or last_name: + parsed_users.append({'name': name, 'last_name': last_name}) + else: + # Fallback für dateien ohne Header + stream.seek(0) + raw_reader = csv.reader(stream, delimiter=delimiter) + for row in raw_reader: + clean_row = [str(cell).strip() for cell in row if str(cell).strip()] + if len(clean_row) >= 2: + # Header-Zeilen überspringen + if clean_row[0].lower() in ['vorname', 'first_name', 'name'] and clean_row[1].lower() in ['nachname', 'last_name']: + continue + parsed_users.append({'name': clean_row[0], 'last_name': clean_row[1]}) + + return parsed_users + def generate_compliant_password(length=16): lowers = string.ascii_lowercase uppers = string.ascii_uppercase @@ -7983,77 +8059,54 @@ def register_csv(): permission_preset = (request.form.get('permission_preset') or 'standard_user').strip() + # 1. CSV über die robuste Funktion einlesen + file_bytes = file.stream.read() + raw_users = parse_csv_users(file_bytes) + + if not raw_users: + flash('Keine gültigen Benutzer in der CSV-Datei gefunden. Bitte prüfen Sie das Format.', 'error') + return redirect(url_for('register')) + created_users = [] - try: - # Read file stream handling UTF-8 and BOM (Excel exports) - file_stream = io.StringIO(file.stream.read().decode("utf-8-sig"), newline=None) + for entry in raw_users: + name = entry['name'] + last_name = entry['last_name'] - # Sniff delimiter (comma or semicolon) - sample = file_stream.read(2048) - file_stream.seek(0) - delimiter = ';' if ';' in sample else ',' + if not name or not last_name: + continue - reader = csv.reader(file_stream, delimiter=delimiter) + # Benutzernamen & Passwort generieren + username = us.build_unique_username_from_name(name, last_name) + password = generate_compliant_password(16) - for row_num, row in enumerate(reader, start=1): - if not row or all(field.strip() == '' for field in row): - continue # Skip empty rows + # In DB speichern + success = us.add_user( + username=username, + password=password, + name=name, + last_name=last_name, + is_student=False, + student_card_id=None, + max_borrow_days=None, + permission_preset=permission_preset, + ) - # Clean entries - row = [field.strip() for field in row] - - # Header row detection (Skip row if it looks like "Name, Nachname") - if row_num == 1 and ('name' in row[0].lower() or 'vorname' in row[0].lower()): - continue - - if len(row) < 2: - continue - - name = row[0] - last_name = row[1] - - if not name or not last_name: - continue - - # 1. Generate Username via backend logic - username = us.build_unique_username_from_name(name, last_name) - - # 2. Generate Random Secure Password - password = generate_compliant_password(16) - - # 3. Add User to DB - success = us.add_user( - username=username, - password=password, - name=name, - last_name=last_name, - is_student=False, - student_card_id=None, - max_borrow_days=None, - permission_preset=permission_preset, - ) - - if success: - created_users.append({ - 'name': name, - 'last_name': last_name, - 'username': username, - 'password': password - }) - - except Exception as e: - flash(f'Fehler beim Verarbeiten der CSV-Datei: {str(e)}', 'error') - return redirect(url_for('register')) + if success: + created_users.append({ + 'name': name, + 'last_name': last_name, + 'username': username, + 'password': password + }) if not created_users: - flash('Keine gültigen Benutzer in der CSV-Datei gefunden.', 'error') + flash('Fehler beim Erstellen der Benutzer aus der CSV.', 'error') return redirect(url_for('register')) - # Generate PDF + # PDF mit Zugangsdaten generieren pdf_buffer = generate_credentials_pdf(created_users) - # Return downloadable PDF file return send_file( pdf_buffer, as_attachment=True, diff --git a/Web/templates/register.html b/Web/templates/register.html index ab3ceb8..5294c0d 100755 --- a/Web/templates/register.html +++ b/Web/templates/register.html @@ -321,11 +321,14 @@ document.addEventListener('DOMContentLoaded', function () { --primary-color: #3498db; --primary-dark: #2980b9; --success-color: #2ecc71; + --success-dark: #27ae60; + --csv-color: #059669; + --csv-dark: #047857; --error-color: #e74c3c; - --text-color: var(--ui-text); + --text-color: var(--ui-text, #1f2937); --light-bg: #f9f9f9; --border-radius: 8px; - --shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + --shadow: 0 4px 6px rgba(0, 0, 0, 0.05); --transition: all 0.3s ease; } @@ -358,37 +361,55 @@ body { } .subtitle { - font-size: 1.1rem; - color: #777; + font-size: 1.05rem; + color: #6b7280; margin-top: 0; } .content { - background-color: var(--ui-surface); + background-color: var(--ui-surface, #ffffff); padding: 2rem; border-radius: var(--border-radius); box-shadow: var(--shadow); margin-bottom: 2rem; } +/* Karten-Layout für Einzel- & Massenregistrierung */ .form-card { - max-width: 500px; - margin: 0 auto; + max-width: 620px; + margin: 0 auto 2.5rem auto; + padding: 1.5rem; + background: var(--ui-surface-soft, #f9fafb); + border: 1px solid #e5e7eb; + border-radius: var(--border-radius); +} + +.form-card:last-child { + margin-bottom: 0; +} + +.form-card h2 { + font-size: 1.4rem; + margin-top: 0; + margin-bottom: 0.5rem; + color: #111827; } .form-group { - margin-bottom: 1.5rem; + margin-bottom: 1.25rem; } .form-group label { display: block; - margin-bottom: 0.5rem; - font-weight: 500; - color: var(--primary-dark); + margin-bottom: 0.4rem; + font-weight: 600; + font-size: 0.95rem; + color: #374151; } .input-container { position: relative; + width: 100%; } .input-icon { @@ -396,24 +417,30 @@ body { left: 1rem; top: 50%; transform: translateY(-50%); - color: #aaa; - font-size: 1.2rem; + color: #9ca3af; + font-size: 1.1rem; + pointer-events: none; + z-index: 1; } +/* Eingabefelder & Selects */ input[type="text"], input[type="password"], +input[type="file"], .form-select { width: 100%; - padding: 0.8rem 1rem 0.8rem 3rem; - border: 1px solid #ddd; + padding: 0.75rem 1rem 0.75rem 2.8rem; + border: 1px solid #d1d5db; border-radius: var(--border-radius); - font-size: 1rem; + font-size: 0.95rem; + background-color: #ffffff; transition: var(--transition); box-sizing: border-box; } input[type="text"]:focus, input[type="password"]:focus, +input[type="file"]:focus, .form-select:focus { border-color: var(--primary-color); box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2); @@ -422,14 +449,47 @@ input[type="password"]:focus, .form-select { padding-left: 1rem; + cursor: pointer; +} + +/* Stylings für den CSV-Datei-Upload */ +input[type="file"] { + padding-left: 2.8rem; + cursor: pointer; +} + +input[type="file"]::file-selector-button { + padding: 0.35rem 0.75rem; + border-radius: 4px; + border: 1px solid #d1d5db; + background: #f3f4f6; + color: #374151; + cursor: pointer; + margin-right: 12px; + font-weight: 500; + transition: var(--transition); +} + +input[type="file"]::file-selector-button:hover { + background: #e5e7eb; } input::placeholder { - color: #aaa; + color: #9ca3af; } +code { + background: #e0f2fe; + color: #0369a1; + padding: 2px 6px; + border-radius: 4px; + font-family: monospace; + font-size: 0.9em; +} + +/* Buttons */ .form-actions { - margin-top: 2rem; + margin-top: 1.5rem; } .action-button { @@ -437,17 +497,21 @@ input::placeholder { color: white; border: none; padding: 0.8rem 1.5rem; - border-radius: 30px; + border-radius: var(--border-radius); cursor: pointer; font-weight: 600; transition: var(--transition); width: 100%; font-size: 1rem; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; } .action-button:hover { background-color: var(--primary-dark); - transform: translateY(-2px); + transform: translateY(-1px); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); } @@ -456,79 +520,48 @@ input::placeholder { } .register-button:hover { - background-color: #27ae60; + background-color: var(--success-dark); } -.flash-container { - margin-bottom: 1.5rem; +/* Toggle-Button für Passwort-Auge */ +.input-wrapper { + position: relative; + width: 100%; } -.flash { - display: flex; - align-items: center; - padding: 1rem; - margin-bottom: 1rem; - border-radius: var(--border-radius); - animation: fadeIn 0.3s ease-in-out; +.toggle-pw-btn { + position: absolute; + right: 0.75rem; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + cursor: pointer; + font-size: 1.1rem; + padding: 4px; + z-index: 2; + opacity: 0.7; + transition: opacity 0.2s; } -.flash-icon { - margin-right: 0.8rem; - font-size: 1.2rem; - display: flex; - align-items: center; - justify-content: center; - width: 24px; - height: 24px; - border-radius: 50%; - background-color: rgba(255, 255, 255, 0.3); -} - -.flash.success { - background-color: #d4edda; - color: #155724; - border-left: 4px solid var(--success-color); -} - -.flash.error { - background-color: #f8d7da; - color: #721c24; - border-left: 4px solid var(--error-color); -} - -@keyframes fadeIn { - from { opacity: 0; transform: translateY(-10px); } - to { opacity: 1; transform: translateY(0); } -} - -@media (max-width: 768px) { - .container { - padding: 0 1rem; - margin: 1rem auto; - } - - .content { - padding: 1.5rem; - } - - .header-section h1 { - font-size: 2rem; - } +.toggle-pw-btn:hover { + opacity: 1; } +/* Passwort-Regeln Box */ .password-rules { margin-bottom: 10px; padding: 10px 12px; border: 1px solid #e5e7eb; border-radius: 8px; - background: var(--ui-surface-soft); + background: #ffffff; } .password-rules-title { - margin: 0 0 8px; + margin: 0 0 6px; font-weight: 700; color: #1f2937; - font-size: 0.92rem; + font-size: 0.88rem; } .password-rules ul { @@ -539,10 +572,10 @@ input::placeholder { .pw-rule { position: relative; - padding-left: 22px; - margin: 5px 0; - color: #b91c1c; - font-size: 0.9rem; + padding-left: 20px; + margin: 4px 0; + color: #dc2626; + font-size: 0.85rem; } .pw-rule::before { @@ -561,15 +594,39 @@ input::placeholder { content: '✓'; } +.pw-actions { + display: flex; + gap: 8px; + margin-top: 8px; +} + +.btn-secondary { + padding: 6px 12px; + border: 1px solid #d1d5db; + background-color: #ffffff; + color: #374151; + border-radius: 6px; + cursor: pointer; + font-size: 0.88rem; + font-weight: 500; + transition: var(--transition); +} + +.btn-secondary:hover { + background-color: #f3f4f6; + border-color: #9ca3af; +} + +/* Hinweise & Rechte-Panels */ .anonymize-hint { - margin-top: 10px; + margin-top: 8px; margin-bottom: 0; background: #f0f9ff; border: 1px solid #bae6fd; - border-radius: 8px; - padding: 10px 12px; - color: #0c4a6e; - font-size: 0.92rem; + border-radius: 6px; + padding: 8px 12px; + color: #0369a1; + font-size: 0.88rem; } .permission-panels { @@ -581,13 +638,13 @@ input::placeholder { .permission-panel { border: 1px solid #d1d5db; border-radius: 8px; - padding: 10px; - background: var(--ui-surface-soft); + padding: 12px; + background: #ffffff; } .permission-panel h4 { margin: 0 0 8px; - font-size: 1rem; + font-size: 0.95rem; color: #1f2937; } @@ -595,42 +652,72 @@ input::placeholder { display: flex; align-items: center; gap: 8px; - margin: 4px 0; - color: #1f2937; -} - -/* Neue Styles für die Passwort-Erweiterungen */ -.pw-actions { - display: flex; - gap: 8px; - margin-top: 8px; -} - -.btn-secondary { - padding: 8px 12px; - border: 1px solid #d1d5db; - background-color: #f3f4f6; - border-radius: 4px; + margin: 6px 0; + color: #374151; + font-size: 0.9rem; cursor: pointer; - font-size: 0.9em; - transition: background-color 0.2s; } -.btn-secondary:hover { - background-color: #e5e7eb; +/* Flash Nachrichten */ +.flash-container { + margin-bottom: 1.5rem; } -.input-wrapper { +.flash { display: flex; align-items: center; - gap: 8px; - width: 100%; + padding: 0.9rem 1rem; + margin-bottom: 1rem; + border-radius: var(--border-radius); + animation: fadeIn 0.3s ease-in-out; } -.input-wrapper .input-container { - flex-grow: 1; - margin: 0; +.flash-icon { + margin-right: 0.8rem; + font-size: 1rem; + display: flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + border-radius: 50%; + background-color: rgba(255, 255, 255, 0.4); +} + +.flash.success { + background-color: #d1fae5; + color: #065f46; + border-left: 4px solid var(--csv-color); +} + +.flash.error { + background-color: #fee2e2; + color: #991b1b; + border-left: 4px solid var(--error-color); +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(-8px); } + to { opacity: 1; transform: translateY(0); } +} + +@media (max-width: 768px) { + .container { + padding: 0 0.75rem; + margin: 1rem auto; + } + + .content { + padding: 1rem; + } + + .form-card { + padding: 1rem; + } + + .header-section h1 { + font-size: 1.8rem; + } } - {% endblock %} \ No newline at end of file From edc7b72a8f7028231c2d1f538769681169c37d25 Mon Sep 17 00:00:00 2001 From: AIIrondev Date: Wed, 12 Aug 2026 16:26:08 +0200 Subject: [PATCH 49/56] Fix for the delete user function --- Web/app.py | 73 +++++++++++++++++++++++++++--------------------------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/Web/app.py b/Web/app.py index 9a021cb..db328c7 100755 --- a/Web/app.py +++ b/Web/app.py @@ -8114,35 +8114,33 @@ def register_csv(): mimetype='application/pdf' ) + @app.route('/user_del', methods=['GET']) def user_del(): """ User deletion interface. Displays a list of users that can be deleted by an administrator. Prevents self-deletion by hiding the current user from the list. - - Returns: - flask.Response: Rendered template with user list or redirect """ if 'username' not in session: - flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error') + flash( + 'Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adresse zu nutzen. Bitte melden Sie sich an!', + 'error') return redirect(url_for('login')) - + + # Abruf aller User (falls get_all_users tenant_id unterstützt, kann diese hier übergeben werden) all_users = us.get_all_users() users_list = [] for user in all_users: - username = None - for field in ['Username']: - if field in user: - username = user[field] - break - - if username and username != session['username']: + username = user.get('Username') + + if username and username != session.get('username'): try: permissions_payload = us.get_effective_permissions(username) except Exception: permissions_payload = us.build_default_permission_payload('standard_user') + try: name = us.get_name(username) last_name = us.get_last_name(username) @@ -8154,12 +8152,13 @@ def user_del(): fullname = last_name else: fullname = None - except: + except Exception: name = "" last_name = "" fullname = None + users_list.append({ - 'username': decrypt_text(username), + 'username': username, # Username ist plain in DB, kein decrypt_text() notwendig 'admin': user.get('Admin', False), 'fullname': fullname, 'name': name, @@ -8168,7 +8167,7 @@ def user_del(): 'action_permissions': permissions_payload.get('actions', {}), 'page_permissions': permissions_payload.get('pages', {}), }) - + return render_template( 'user_del.html', users=users_list, @@ -8183,61 +8182,63 @@ def delete_user(): Process user deletion request. Deletes a specified user from the system. Includes safety checks to prevent self-deletion. - - Returns: - flask.Response: Redirect to the user deletion interface with status """ if 'username' not in session: - flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error') + flash( + 'Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adresse zu nutzen. Bitte melden Sie sich an!', + 'error') return redirect(url_for('login')) - + username = request.form.get('username') if not username: flash('Kein Benutzer ausgewählt', 'error') return redirect(url_for('user_del')) - - # Prevent self-deletion - if username == session['username']: + + if username == session.get('username'): flash('Sie können Ihr eigenes Konto nicht löschen', 'error') return redirect(url_for('user_del')) - - # Reset this user's borrowings and free items before deleting the user + try: - client = MongoClient(MONGODB_HOST, MONGODB_PORT) - db = client[MONGODB_DB] + client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) + tenant_id = session.get('tenant_id') + db = _get_tenant_db(client, tenant_id) + ausleihungen = db['ausleihungen'] items_col = db['items'] now = datetime.datetime.now() - # Complete all active borrowings of this user + # Aktive Ausleihen abschließen ausleihungen.update_many( {'User': username, 'Status': 'active'}, {'$set': {'Status': 'completed', 'End': now, 'LastUpdated': now}} ) - # Cancel all planned borrowings of this user + # Geplante Ausleihen stornieren ausleihungen.update_many( {'User': username, 'Status': 'planned'}, {'$set': {'Status': 'cancelled', 'LastUpdated': now}} ) - # Free all items currently associated with this user + # Inventar wieder verfügbar machen items_col.update_many( {'User': username}, {'$set': {'Verfuegbar': True, 'LastUpdated': now}, '$unset': {'User': ""}} ) - - client.close() except Exception as e: - app.logger.error(f"Error resetting borrowings for user {encrypt_text(username)}: {e}") - flash(f'Warnung: Ausleihungen/Reservierungen für {username} konnten nicht vollständig zurückgesetzt werden', 'warning') + app.logger.error(f"Error resetting borrowings for user {username}: {e}") + flash(f'Warnung: Ausleihungen/Reservierungen für {username} konnten nicht vollständig zurückgesetzt werden', + 'warning') + finally: + # Garantiert, dass die Datenbankverbindung geschlossen wird + if 'client' in locals(): + client.close() - # Delete the user try: + # us.delete_user muss ggf. noch angepasst werden, falls es auch den tenant_id verlangt us.delete_user(username) flash(f'Benutzer {username} erfolgreich gelöscht', 'success') except Exception as e: - app.logger.error(f"Error deleting user {encrypt_text(username)}: {e}") + app.logger.error(f"Error deleting user {username}: {e}") flash('Fehler beim Löschen des Benutzers', 'error') return redirect(url_for('user_del')) From c3db2d9c6bf495e954df960349f9cfea46273be1 Mon Sep 17 00:00:00 2001 From: AIIrondev Date: Wed, 12 Aug 2026 16:31:17 +0200 Subject: [PATCH 50/56] Fix for the delete user function --- Web/app.py | 42 ++++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/Web/app.py b/Web/app.py index db328c7..062b8a4 100755 --- a/Web/app.py +++ b/Web/app.py @@ -8180,13 +8180,11 @@ def user_del(): def delete_user(): """ Process user deletion request. - Deletes a specified user from the system. + Deletes a specified user from the system directly via the tenant db. Includes safety checks to prevent self-deletion. """ if 'username' not in session: - flash( - 'Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adresse zu nutzen. Bitte melden Sie sich an!', - 'error') + flash('Ihnen ist es nicht gestattet, diese Aktion auszuführen. Bitte melden Sie sich an!', 'error') return redirect(url_for('login')) username = request.form.get('username') @@ -8205,45 +8203,49 @@ def delete_user(): ausleihungen = db['ausleihungen'] items_col = db['items'] + users_col = db['users'] # Direkter Zugriff auf die User-Collection + now = datetime.datetime.now() - # Aktive Ausleihen abschließen + # 1. Aktive Ausleihen abschließen ausleihungen.update_many( {'User': username, 'Status': 'active'}, {'$set': {'Status': 'completed', 'End': now, 'LastUpdated': now}} ) - # Geplante Ausleihen stornieren + # 2. Geplante Ausleihen stornieren ausleihungen.update_many( {'User': username, 'Status': 'planned'}, {'$set': {'Status': 'cancelled', 'LastUpdated': now}} ) - # Inventar wieder verfügbar machen + # 3. Inventar wieder verfügbar machen items_col.update_many( {'User': username}, {'$set': {'Verfuegbar': True, 'LastUpdated': now}, '$unset': {'User': ""}} ) + + # 4. Den Benutzer direkt in der überprüften DB-Verbindung löschen + # Achte auf die exakte Großschreibung 'Username' (so wie in add_user definiert) + delete_result = users_col.delete_one({'Username': username}) + + # 5. Explizite Erfolgskontrolle + if delete_result.deleted_count > 0: + flash(f'Benutzer {username} erfolgreich gelöscht', 'success') + else: + # Fallback, falls der Nutzer nicht gefunden wurde (Fehlervermeidung) + flash(f'Löschen fehlgeschlagen: Benutzer {username} wurde in der Datenbank nicht gefunden.', 'error') + except Exception as e: - app.logger.error(f"Error resetting borrowings for user {username}: {e}") - flash(f'Warnung: Ausleihungen/Reservierungen für {username} konnten nicht vollständig zurückgesetzt werden', - 'warning') + app.logger.error(f"Error resetting borrowings or deleting user {username}: {e}") + flash('Kritischer Fehler beim Löschen des Benutzers', 'error') finally: - # Garantiert, dass die Datenbankverbindung geschlossen wird + # Garantiert, dass die DB-Verbindung geschlossen wird if 'client' in locals(): client.close() - try: - # us.delete_user muss ggf. noch angepasst werden, falls es auch den tenant_id verlangt - us.delete_user(username) - flash(f'Benutzer {username} erfolgreich gelöscht', 'success') - except Exception as e: - app.logger.error(f"Error deleting user {username}: {e}") - flash('Fehler beim Löschen des Benutzers', 'error') - return redirect(url_for('user_del')) - @app.route('/admin/borrowings') def admin_borrowings(): """ From fb29fb91a9592fc80e23668b84a97fd010c3cac1 Mon Sep 17 00:00:00 2001 From: AIIrondev Date: Wed, 12 Aug 2026 16:34:01 +0200 Subject: [PATCH 51/56] Fix for the delete user function --- Web/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Web/app.py b/Web/app.py index 062b8a4..6153564 100755 --- a/Web/app.py +++ b/Web/app.py @@ -8199,7 +8199,7 @@ def delete_user(): try: client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) tenant_id = session.get('tenant_id') - db = _get_tenant_db(client, tenant_id) + db = us._get_tenant_db(client, tenant_id) ausleihungen = db['ausleihungen'] items_col = db['items'] From cfec33b362cf8a8b7f88c2447345727bfacb9c97 Mon Sep 17 00:00:00 2001 From: AIIrondev Date: Wed, 12 Aug 2026 22:29:45 +0200 Subject: [PATCH 52/56] Slight clean up of old functions that are not in use anymore, and inclution of Image uploading for the library uploading process --- Web/app.py | 168 -------------------------------- Web/templates/upload_admin.html | 4 +- 2 files changed, 3 insertions(+), 169 deletions(-) diff --git a/Web/app.py b/Web/app.py index 6153564..a687ca4 100755 --- a/Web/app.py +++ b/Web/app.py @@ -6216,174 +6216,6 @@ def bulk_delete_items(): if client: client.close() - -@app.route('/edit_item/', methods=['POST']) -def edit_item(id): - """ - Route for editing an existing inventory item. - - Args: - id (str): ID of the item to edit - - Returns: - flask.Response: Redirect to admin homepage with status message - """ - if 'username' not in session: - flash('Nicht angemeldet.', 'error') - return redirect(url_for('login')) - - current_permissions = us.get_effective_permissions(session['username']) - - if not current_permissions['actions'].get('can_edit', False): - flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error') - return redirect(url_for('home_admin')) - - if not cfg.MODULES.is_enabled('inventory'): - flash('Bibliotheks-Modul ist deaktiviert.', 'error') - return redirect(url_for('library_view')) - - fs = get_gridfs() - - name = sanitize_form_value(request.form.get('name')) - ort = sanitize_form_value(request.form.get('ort')) - beschreibung = sanitize_form_value(request.form.get('beschreibung')) - - filter1 = sanitize_form_value(request.form.getlist('filter')) - filter2 = sanitize_form_value(request.form.getlist('filter2')) - filter3 = sanitize_form_value(request.form.getlist('filter3')) - - # Expand special "all values" selections for predefined filters. - filter1 = expand_filter_selection(filter1, 1) - filter2 = expand_filter_selection(filter2, 2) - - anschaffungs_jahr = sanitize_form_value(request.form.get('anschaffungsjahr')) - anschaffungs_kosten = sanitize_form_value(request.form.get('anschaffungskosten')) - code_4 = sanitize_form_value(request.form.get('code_4')) - isbn_raw = sanitize_form_value(request.form.get('isbn', '')) - reservierbar = 'reservierbar' in request.form - - item_isbn = '' - item_type = 'general' - if cfg.MODULES.is_enabled('library'): - item_isbn = normalize_and_validate_isbn(isbn_raw) - if isbn_raw and not item_isbn: - flash('Ungültige ISBN. Bitte ISBN-10 oder ISBN-13 verwenden.', 'error') - return redirect(url_for('home_admin')) - if item_isbn: - item_type = 'book' - - if code_4 and not it.is_code_unique(code_4, exclude_id=id): - flash('Der Code wird bereits verwendet. Bitte wählen Sie einen anderen Code.', 'error') - return redirect(url_for('home_admin')) - - current_item = it.get_item(id) - if not current_item: - flash('Element nicht gefunden', 'error') - return redirect(url_for('home_admin')) - - verfuegbar = current_item.get('Verfuegbar', True) - - images_to_keep = request.form.getlist('existing_images') - - original_images = current_item.get('Images', []) - - images = [img for img in original_images if img in images_to_keep] - - new_images = request.files.getlist('new_images') - - for image in new_images: - if image and image.filename: - is_allowed, error_message = allowed_file(image.filename, image) - - if is_allowed: - try: - secure_name = secure_filename(image.filename) - - image.seek(0) - image_bytes = image.read() - - if not image_bytes: - app.logger.error(f"Failed to read image in edit_item (0 bytes) for {secure_name}") - continue - - optimized_io = io.BytesIO() - with Image.open(io.BytesIO(image_bytes)) as img: - if img.mode not in ('RGB', 'RGBA'): - img = img.convert('RGBA') - - max_width = 500 - if img.width > max_width: - ratio = max_width / img.width - new_size = (max_width, int(img.height * ratio)) - img = img.resize(new_size, Image.Resampling.LANCZOS) - - img.save(optimized_io, format='WEBP', quality=85, optimize=True) - - optimized_io.seek(0) - new_filename = f"{uuid.uuid4().hex}_{int(time.time())}.webp" - - fs.put( - optimized_io, - filename=new_filename, - content_type='image/webp', - metadata={ - 'original_filename': secure_name, - 'upload_context': 'edit_item', - 'item_id': id - } - ) - - images.append(new_filename) - - except Exception as e: - app.logger.error(f"Error processing new image in edit_item: {str(e)}") - else: - flash(error_message, 'error') - return redirect(url_for('home_admin')) - - predefined_locations = it.get_predefined_locations() - if ort and ort not in predefined_locations: - it.add_predefined_location(ort) - - result = it.update_item( - id=id, - name=name, - ort=ort, - beschreibung=beschreibung, - images=images, - verfuegbar=verfuegbar, - filter1=filter1, - filter2=filter2, - filter3=filter3, - ansch_jahr=anschaffungs_jahr, - ansch_kost=anschaffungs_kosten, - code_4=code_4, - reservierbar=reservierbar, - isbn=item_isbn, - item_type=item_type - ) - - if result: - flash('Element erfolgreich aktualisiert (und ggf. Gruppe synchronisiert)', 'success') - else: - flash('Fehler beim Aktualisieren des Elements', 'error') - - return redirect(url_for('home_admin')) - -def is_library_item(item): - """ - Prüft, ob ein Artikel ein Bibliotheks-Item ist. - - 'other', None oder Leerstring -> Inventarsystem (False) - - Jeder andere Medientyp ('Buch', 'CD', etc.) -> Bibliothek (True) - """ - if not item: - return False - item_type = item.get('ItemType', 'other') - if not item_type: - return False - return item_type.strip().lower() != 'other' - - @app.route('/item_edit/', methods=['GET', 'POST']) def item_edit(id): if 'username' not in session: diff --git a/Web/templates/upload_admin.html b/Web/templates/upload_admin.html index 23e0018..f9380a0 100755 --- a/Web/templates/upload_admin.html +++ b/Web/templates/upload_admin.html @@ -954,8 +954,10 @@
    + + -
    +
    Erlaubte Formate: JPG, JPEG, PNG, GIF, MP4, MOV, AVI, MKV, WEBM, FLV, M4V, 3GP
    From 057c51751577d23b9869e9dcd1d64f53b56768e4 Mon Sep 17 00:00:00 2001 From: AIIrondev Date: Wed, 12 Aug 2026 22:38:07 +0200 Subject: [PATCH 53/56] Debug removal of whitelist --- Web/app.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Web/app.py b/Web/app.py index a687ca4..b728eaf 100755 --- a/Web/app.py +++ b/Web/app.py @@ -474,7 +474,8 @@ ALLOWED_COVER_DOMAINS = { "covers.openlibrary.org", "openlibrary.org", "lobid.org", - "www.googleapis.com" + "www.googleapis.com", + "www.westermann.de" } SENSITIVE_AUDIT_FIELDS = ["email", "username", "full_name", "phone", "borrower", "ip"] @@ -10043,8 +10044,8 @@ def download_book_cover(): return jsonify({"error": "Only public HTTPS URLs are allowed"}), 400 # 2. SSRF Protection: Strict Allowlist Check - if parsed_url.netloc not in ALLOWED_COVER_DOMAINS: - return jsonify({"error": "Target host is not an allowed book cover provider"}), 403 + # if parsed_url.netloc not in ALLOWED_COVER_DOMAINS: # ---------------------------------------------- has to be implementet at a laterstage --------------------------------------------------------------- + # return jsonify({"error": "Target host is not an allowed book cover provider"}), 403 # Download the image (allow_redirects=False prevents redirecting to internal IPs) response = requests.get(image_url, stream=True, timeout=10, allow_redirects=False) From 99ec28f329e179a41218825c0689320d030f9237 Mon Sep 17 00:00:00 2001 From: AIIrondev Date: Wed, 12 Aug 2026 23:10:06 +0200 Subject: [PATCH 54/56] Introduction of the images to the detailed view to the library_table.html --- Web/app.py | 34 ++++++-- Web/templates/library_table.html | 136 +++++++++++++++++++++---------- 2 files changed, 119 insertions(+), 51 deletions(-) diff --git a/Web/app.py b/Web/app.py index b728eaf..9e015e1 100755 --- a/Web/app.py +++ b/Web/app.py @@ -466,16 +466,36 @@ PERMISSION_ACTION_ENDPOINTS = { } ALLOWED_COVER_DOMAINS = { + # --- Google / Open APIs --- "books.google.com", - "covers.openlibrary.org", - "images-na.ssl-images-amazon.com", - "m.media-amazon.com", - "www.isbn.de", + "www.googleapis.com", + + # --- Open Library / Internet Archive --- "covers.openlibrary.org", "openlibrary.org", + + # --- Amazon / Goodreads --- + "images-na.ssl-images-amazon.com", + "m.media-amazon.com", + "i.gr-assets.com", # Goodreads image CDN + + # --- Library / Catalog Services --- + "www.isbn.de", "lobid.org", - "www.googleapis.com", - "www.westermann.de" + "syndetics.com", # Standard cover provider for libraries + "pics.librarything.com", # LibraryThing covers + "portal.dnb.de", # Deutsche Nationalbibliothek + + # --- German Educational & International Publishers --- + "www.westermann.de", + "www.klett.de", # Ernst Klett Verlag + "medien.klett.de", # Klett media CDN + "www.cornelsen.de", # Cornelsen Verlag + "images.penguinrandomhouse.com", # Penguin Random House + + # --- Book Retailer CDNs (often used for cover fetching) --- + "images.thalia.media", # Thalia + "bilder.buecher.de" # buecher.de } SENSITIVE_AUDIT_FIELDS = ["email", "username", "full_name", "phone", "borrower", "ip"] @@ -10044,7 +10064,7 @@ def download_book_cover(): return jsonify({"error": "Only public HTTPS URLs are allowed"}), 400 # 2. SSRF Protection: Strict Allowlist Check - # if parsed_url.netloc not in ALLOWED_COVER_DOMAINS: # ---------------------------------------------- has to be implementet at a laterstage --------------------------------------------------------------- + # if parsed_url.netloc not in ALLOWED_COVER_DOMAINS: # return jsonify({"error": "Target host is not an allowed book cover provider"}), 403 # Download the image (allow_redirects=False prevents redirecting to internal IPs) diff --git a/Web/templates/library_table.html b/Web/templates/library_table.html index bcb2eab..e8e51d2 100644 --- a/Web/templates/library_table.html +++ b/Web/templates/library_table.html @@ -52,9 +52,10 @@ /* The Scrollable Content Area */ #detailContent { - overflow-y: auto; /* Adds scrollbar only if needed */ - padding-right: 10px; /* Prevents text from rubbing against the scrollbar */ + overflow-y: auto; + padding-right: 10px; } + /* Library table-only view styles */ .library-table-container { max-width: 1400px; @@ -276,6 +277,7 @@ border-bottom: 1px solid #eee; color: #555; font-size: 0.95em; + vertical-align: middle; } .library-items-table tbody tr:hover { @@ -500,14 +502,14 @@