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 @@
-
-