From 4e47ef0c88ca85e1a178483ca63bd984dc7af3b7 Mon Sep 17 00:00:00 2001 From: AIIrondev Date: Mon, 10 Aug 2026 22:17:52 +0200 Subject: [PATCH] Implement library item return by code API and enhance UI for manual returns --- Web/app.py | 93 +++++++++++++++++++++++-- Web/templates/library_table.html | 116 +++++++++++++++++++++++++++++-- Web/templates/user_del.html | 10 --- 3 files changed, 200 insertions(+), 19 deletions(-) diff --git a/Web/app.py b/Web/app.py index c6482e6..979f35b 100755 --- a/Web/app.py +++ b/Web/app.py @@ -225,12 +225,95 @@ def rollover_student_card_classes(dry_run=False, *, max_class=None, graduate_lab if client: client.close() - summary = {'examined': examined, 'updated': updated, 'failures': failures, 'dry_run': bool(dry_run)} + +@app.route('/api/library_return_by_code', methods=['POST']) +def api_library_return_by_code(): + """ + Return a library item by scanning its code only (no student card required). + This marks active ausleihungen for the item as completed and updates item status. + """ + if 'username' not in session: + return jsonify({'ok': False, 'message': 'Nicht angemeldet.'}), 401 + if not cfg.MODULES.is_enabled('library'): + return jsonify({'ok': False, 'message': 'Bibliotheks-Modul ist deaktiviert.'}), 403 + + payload = request.get_json(silent=True) or {} + item_code_raw = str(payload.get('item_code') or payload.get('code') or '').strip() + if not item_code_raw: + return jsonify({'ok': False, 'message': 'Mediencode fehlt.'}), 400 + + normalized_isbn = normalize_and_validate_isbn(item_code_raw) + normalized_code = item_code_raw.upper() + + client = None try: - _append_audit_event_standalone('student_cards_rollover', summary) - except Exception: - app.logger.warning('Audit write failed for student_cards_rollover') - return summary + client = MongoClient(MONGODB_HOST, MONGODB_PORT) + db = client[MONGODB_DB] + items_col = db['items'] + ausleihungen_col = db['ausleihungen'] + + query_or = [ + {'Code_4': item_code_raw}, + {'Code_4': normalized_code}, + ] + if normalized_isbn: + query_or.append({'ISBN': normalized_isbn}) + + item_doc = items_col.find_one({ + 'ItemType': {'$in': LIBRARY_ITEM_TYPES}, + '$or': query_or + }) + + if not item_doc: + return jsonify({'ok': False, 'message': 'Kein Bibliotheksmedium für diesen Code gefunden.'}), 404 + + item_id = str(item_doc['_id']) + now = datetime.datetime.now() + + # If item already available -> nothing to return + if item_doc.get('Verfuegbar', True): + return jsonify({'ok': False, 'message': 'Dieses Medium ist nicht als ausgeliehen markiert.'}), 409 + + # Mark active ausleihungen as completed + update_result = ausleihungen_col.update_many( + {'Item': item_id, 'Status': 'active'}, + {'$set': { + 'Status': 'completed', + 'End': now, + 'LastUpdated': now + }} + ) + + # Update item status to available + borrower_name = str(item_doc.get('User') or '').strip() or '' + it.update_item_status(item_id, True, borrower_name) + + _append_audit_event_standalone( + event_type='ausleihung_returned_by_code', + payload={ + 'channel': 'library_return_code', + 'item_id': item_id, + 'item_name': item_doc.get('Name', ''), + 'completed_records': update_result.modified_count, + 'performed_by': session.get('username') + } + ) + + return jsonify({ + 'ok': True, + 'action': 'returned', + 'item_id': item_id, + 'item_name': item_doc.get('Name', ''), + 'completed_records': update_result.modified_count, + 'message': f"{item_doc.get('Name', 'Medium')} wurde zurückgegeben." + }), 200 + except Exception as e: + app.logger.error(f"Error in library return by code: {e}") + return jsonify({'ok': False, 'message': 'Fehler beim Verarbeiten der Rückgabe.'}), 500 + finally: + if client: + client.close() + # Admin route to trigger rollover manually diff --git a/Web/templates/library_table.html b/Web/templates/library_table.html index d4665f3..e2b2a1c 100644 --- a/Web/templates/library_table.html +++ b/Web/templates/library_table.html @@ -355,6 +355,28 @@ color: #6b7280; } + /* Small confirmation popup (toast) */ + .small-popup { + position: fixed; + bottom: 24px; + left: 50%; + transform: translateX(-50%); + background: rgba(17, 24, 39, 0.96); + color: #fff; + padding: 12px 16px; + border-radius: 8px; + box-shadow: 0 6px 24px rgba(2,6,23,0.6); + z-index: 2000; + display: flex; + gap: 10px; + align-items: center; + max-width: 90%; + font-size: 0.95em; + } + .small-popup.ok { background: rgba(16,185,129,0.95); color: #032; } + .small-popup.error { background: rgba(239,68,68,0.95); color: #210; } + .small-popup .close-x { margin-left: 8px; cursor: pointer; font-weight: 700; } + /* Modal styles */ .modal { display: none; @@ -503,12 +525,18 @@ + + +
Hinweis: Im Schnellmodus zuerst den Schülerausweis scannen, danach den Buch-/Mediencode. @@ -914,6 +942,13 @@ const currentCallback = activeScannerCallback; stopScanner(); + const returnOnly = (document.getElementById('returnOnlyToggle') || {}).checked; + if (returnOnly) { + // direct return flow + returnByCode(barcode); + return; + } + if (typeof currentCallback === "function") { currentCallback(barcode); } else { @@ -940,6 +975,13 @@ keyboardScanBuffer = ''; keyboardLastKeyAt = 0; if (!code) return; + // If return-only mode is active, attempt direct return + const returnOnly = (document.getElementById('returnOnlyToggle') || {}).checked; + if (returnOnly) { + returnByCode(code); + return; + } + // Process exactly like a camera scan handleScanSuccess(code); return; @@ -981,11 +1023,46 @@ async function processQuickToggleScan(scannedCode) { if (!activeStudentCardId) { - setActiveStudentCard(scannedCode); - setScanStatus(`Ausweis gesetzt: ${activeStudentCardId}`, 'ok'); - return; - } + // 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; + } + } try { setScanStatus('Verarbeite Mediencode...', 'warn'); const response = await fetch('/api/library_scan_action', { @@ -1005,10 +1082,13 @@ 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(); @@ -1121,6 +1201,21 @@ 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(); @@ -1286,6 +1381,19 @@ }); } + 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); + }); + } + // Edit Modal Form processing const editForm = document.getElementById('editLibraryForm'); if (editForm) { diff --git a/Web/templates/user_del.html b/Web/templates/user_del.html index 80a68bf..1a86909 100755 --- a/Web/templates/user_del.html +++ b/Web/templates/user_del.html @@ -23,16 +23,6 @@
-
- -
-