Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4e47ef0c88 | |||
| 0911b362fd |
+88
-5
@@ -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
|
||||
|
||||
@@ -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 @@
|
||||
<option value="quick_toggle">Schnellmodus: Ausweis + Mediencode</option>
|
||||
</select>
|
||||
<input type="text" id="activeStudentCard" placeholder="Aktiver Ausweis (gescannt)" readonly>
|
||||
<input type="text" id="manualItemCode" placeholder="Manueller Mediencode (optional)" style="min-width:180px;">
|
||||
<button id="resetCardBtn" class="button" type="button">Ausweis löschen</button>
|
||||
<button id="toggleScannerBtn" class="button" type="button">Scanner starten</button>
|
||||
<label style="display:flex; align-items:center; gap:8px; margin-left:6px;">
|
||||
<input type="checkbox" id="keyboardScannerToggle">
|
||||
<span style="font-size:0.9em;">Physischer Scanner</span>
|
||||
</label>
|
||||
<label style="display:flex; align-items:center; gap:8px; margin-left:6px;">
|
||||
<input type="checkbox" id="returnOnlyToggle">
|
||||
<span style="font-size:0.9em;">Nur Rückgabe (nur Mediencode)</span>
|
||||
</label>
|
||||
<button id="manualReturnBtn" class="button" type="button" style="margin-left:6px; background:#10b981;color:white;">Rückgabe per Code</button>
|
||||
</div>
|
||||
<div id="scanStatus" class="library-scan-status">
|
||||
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 = `<div>${escapeHtml(String(message || ''))}</div><div style="opacity:0.9; margin-left:8px; font-size:0.85em;">${helper}</div><div class="close-x">×</div>`;
|
||||
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) {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<div class="col-md-4">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title h5 mb-0">{{ filter_names.get('1', 'Fach/Kategorie') }} (Filter 1)</h2>
|
||||
<h2 class="card-title h5 mb-0">{{ filter_names.get('1', 'Jahrgang') }} (Filter 1)</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ url_for('add_filter_value', filter_num=1) }}" class="mb-4">
|
||||
@@ -67,7 +67,7 @@
|
||||
<div class="col-md-4">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title h5 mb-0">{{ filter_names.get('2', 'System/Bereich') }} (Filter 2)</h2>
|
||||
<h2 class="card-title h5 mb-0">{{ filter_names.get('2', 'Fach') }} (Filter 2)</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ url_for('add_filter_value', filter_num=2) }}" class="mb-4">
|
||||
|
||||
@@ -23,16 +23,6 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ url_for('admin_anonymize_names') }}" class="mb-3">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-outline-danger"
|
||||
onclick="return confirm('Sollen alle gespeicherten Klarnamen dauerhaft in Synonym-Kuerzel umgewandelt werden?')"
|
||||
>
|
||||
Gespeicherte Namen anonymisieren
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="filter-bar mb-3">
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-md-3">
|
||||
|
||||
Reference in New Issue
Block a user