Compare commits

...

10 Commits

Author SHA1 Message Date
Aiirondev_dev 6b9cf8a024 changes to th the authentification for the page
Release Inventarsystem / release-docker (push) Successful in 2m25s
2026-09-14 09:13:43 +02:00
Aiirondev_dev 69bb02b7dc changes to the fuplicate import
Release Inventarsystem / release-docker (push) Successful in 13m24s
2026-09-14 09:03:02 +02:00
Aiirondev_dev 6a3a5d6373 Changes to the library scanning function
Release Inventarsystem / release-docker (push) Successful in 3m11s
2026-09-10 19:42:32 +02:00
Aiirondev_dev 09efedad69 changes to the Mahnungssystem
Release Inventarsystem / release-docker (push) Successful in 9m58s
2026-09-09 20:36:08 +02:00
Aiirondev_dev b05d238a60 changes to the Schüler id getting
Release Inventarsystem / release-docker (push) Successful in 2m18s
2026-09-09 20:09:44 +02:00
Aiirondev_dev 63a7b64150 change to the Hinweis that is shown
Release Inventarsystem / release-docker (push) Successful in 2m19s
2026-09-09 19:02:19 +02:00
Aiirondev_dev d5f4558b70 fixes to the physical scanner functionality
Release Inventarsystem / release-docker (push) Successful in 3m15s
2026-09-09 18:50:14 +02:00
Aiirondev_dev 0e85337fc5 changes to the scanning functrionality
Release Inventarsystem / release-docker (push) Successful in 2m22s
2026-09-08 18:36:34 +02:00
Aiirondev_dev 0114fec928 changes to the physical scanner processing
Release Inventarsystem / release-docker (push) Successful in 12m48s
2026-09-08 18:19:40 +02:00
Aiirondev_dev b16ad56b98 New password option fo the tenant adding and heatlth check
Release Inventarsystem / release-docker (push) Successful in 3m16s
2026-08-27 11:31:07 +02:00
5 changed files with 316 additions and 77 deletions
+71 -29
View File
@@ -17,7 +17,6 @@ Features:
- Booking and reservation of items
"""
from random import random
from flask import Flask, render_template, request, redirect, url_for, session, flash, send_from_directory, get_flashed_messages, jsonify, Response, make_response, send_file, abort
from werkzeug.utils import secure_filename
from werkzeug.middleware.proxy_fix import ProxyFix
@@ -542,7 +541,8 @@ def _enforce_module_access():
msg = {
'library': "Bibliotheks-Modul ist deaktiviert.",
'inventory': "Inventar-Modul ist deaktiviert.",
'student_cards': "Schülerausweis-Modul ist deaktiviert."
'student_cards': "Schülerausweis-Modul ist deaktiviert.",
'terminplaner': "Termin-Modul ist deaktiviert."
}.get(name, f"{name.capitalize()}-Modul ist deaktiviert.")
if request.path.startswith('/api/') or request.is_json:
@@ -554,6 +554,8 @@ def _enforce_module_access():
return redirect(url_for('home_admin'))
elif name != 'library' and cfg.MODULES.is_enabled('library'):
return redirect('/library')
elif name != 'terminplaner' and cfg.MODULES.is_enabled('terminplaner'):
return redirect('/terminplaner')
return redirect(url_for('my_borrowed_items'))
@@ -706,7 +708,7 @@ def _action_access_allowed(permissions, action_key):
def _permission_denied_fallback_endpoint(permissions, current_endpoint=None):
username = session.get('username')
for candidate in ('home_admin', 'my_borrowed_items', 'tutorial_page', 'notifications_view', 'impressum'):
for candidate in ('home_admin', 'library_view', 'terminplaner', 'my_borrowed_items', 'tutorial_page', 'notifications_view', 'impressum'):
if current_endpoint and candidate == current_endpoint:
continue
if _page_access_allowed(permissions, candidate):
@@ -1378,11 +1380,31 @@ def update_appointment_statuses():
activation_user = str(appointment.get('User') or '').strip()
activation_item_name = str(appointment.get('Item') or 'Termin')
# is_library_item expects an item document, not the stored item id.
item_doc_for_status = None
item_id_for_status = appointment.get('Item')
if item_id_for_status:
item_lookup = [{'_id': item_id_for_status}]
try:
item_lookup.insert(0, {'_id': ObjectId(str(item_id_for_status))})
except (InvalidId, TypeError):
pass
try:
item_doc_for_status = items_col.find_one(
{'$or': item_lookup},
{'ItemType': 1, 'is_library': 1}
)
except Exception as item_lookup_error:
app.logger.warning(
f"Could not resolve item type for appointment {appointment.get('_id')}: {item_lookup_error}"
)
item_is_library = it.is_library_item(item_doc_for_status)
# Aktuellen Status bestimmen
new_status = au.get_current_status(appointment, log_changes=True, user='scheduler')
# Wenn sich der Status geändert hat, aktualisiere in der Datenbank
if new_status != old_status and not it.is_library_item(appointment.get('Item')):
if new_status != old_status and not item_is_library:
extra_fields = {}
# --- Conflict resolver: planned → active transition ---
@@ -1480,15 +1502,22 @@ def update_appointment_statuses():
# -----------------------------------------------------------------
# Mahnlauf für Bibliotheksartikel (Prüfung auf Überfälligkeit)
# -----------------------------------------------------------------
elif it.is_library_item(appointment.get('Item')):
elif item_is_library:
appt = appointment # Verwende das aktuelle Dokument aus der Schleife
if appt.get('Status') != 'active':
continue
due_date_obj = appt.get('DueDate')
due_date_obj = appt.get('DueDate') or appt.get('End')
if not due_date_obj:
continue
# Backfill the deadline for loans created before DueDate was stored.
if not appt.get('DueDate') and appt.get('End'):
ausleihungen.update_one(
{'_id': appt['_id'], 'DueDate': {'$exists': False}},
{'$set': {'DueDate': due_date_obj}}
)
due_date_naive = due_date_obj.replace(tzinfo=None) if due_date_obj.tzinfo else due_date_obj
days_overdue = (current_time_naive - due_date_naive).days
@@ -1563,13 +1592,6 @@ def update_appointment_statuses():
except Exception as n_err:
app.logger.warning(f"Fehler beim Erstellen der Admin-Notif (Stufe 2): {n_err}")
# 2. Web-Push Notification für Admins
if 'create_return_reminders' in globals():
try:
create_return_reminders(title=title, body=body, url=target_url)
except Exception as p_err:
app.logger.error(f"Fehler beim Senden der Admin-Push (Stufe 2): {p_err}")
app.logger.warning(f"Mahnstufe 2 & Ausweis-Sperre für Schülerausweis '{student_name}' ({target_ausweis_id}) gesetzt.")
# STUFE 1: >= 14 Tage überfällig -> Stufe 1 setzen & Admins benachrichtigen
@@ -1594,13 +1616,6 @@ def update_appointment_statuses():
except Exception as n_err:
app.logger.warning(f"Fehler beim Erstellen der Admin-Notif (Stufe 1): {n_err}")
# 2. Web-Push Notification für Admins
if 'create_return_reminders' in globals():
try:
create_return_reminders(title=title, body=body, url=target_url)
except Exception as p_err:
app.logger.error(f"Fehler beim Senden der Admin-Push (Stufe 1): {p_err}")
app.logger.info(f"Mahnstufe 1 für Schülerausweis '{student_name}' ({target_ausweis_id}) gesetzt.")
if updated_count > 0:
@@ -3657,7 +3672,10 @@ def mahnungen_admin():
overdue_records = list(ausleihungen_col.find({
'Status': 'active',
'DueDate': {'$lt': current_time}
'$or': [
{'DueDate': {'$lt': current_time}},
{'DueDate': {'$exists': False}, 'End': {'$lt': current_time}},
]
}).sort('DueDate', 1))
overdue_list = []
@@ -3702,7 +3720,7 @@ def mahnungen_admin():
student_email = student_card.get('email') or student_card.get('Email', '')
is_blocked = student_card.get('is_blocked', False)
due_date_obj = record.get('DueDate')
due_date_obj = record.get('DueDate') or record.get('End')
if due_date_obj:
due_date_naive = due_date_obj.replace(tzinfo=None) if due_date_obj.tzinfo else due_date_obj
days_overdue = (current_time_naive - due_date_naive).days
@@ -4339,7 +4357,13 @@ def api_library_scan_action():
due_date = now + datetime.timedelta(days=borrow_duration_days)
it.update_item_status(item_id, False, borrower_name)
au.add_ausleihung(item_id, borrower_name, now, due_date)
au.add_ausleihung(
item_id,
borrower_name,
now,
end_date=due_date,
due_date=due_date,
)
_append_audit_event_standalone(
event_type='ausleihung_borrowed',
@@ -5137,7 +5161,6 @@ def student_card_class_barcode_download():
"""
Download PDF with student card barcodes filtered by a specific class from dropdown.
"""
from flask import request, session, redirect, url_for, send_file, flash
if 'username' not in session:
return redirect(url_for('login'))
@@ -5331,7 +5354,7 @@ def student_card_single_barcode_download(card_id):
return redirect(url_for('login'))
current_permissions = us.get_effective_permissions(session['username'])
if not current_permissions['actions'].get('can_manage_users', False):
if not current_permissions['actions'].get('can_manage_settings', False):
flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error')
return redirect(url_for('library_view'))
if not cfg.MODULES.is_enabled('student_cards'):
@@ -7538,7 +7561,13 @@ def ausleihen(id):
for unit in selected_units:
unit_id = str(unit.get('_id'))
it.update_item_status(unit_id, False, effective_borrower)
au.add_ausleihung(unit_id, effective_borrower, start_date, end_date=end_date)
au.add_ausleihung(
unit_id,
effective_borrower,
start_date,
end_date=end_date,
due_date=end_date if is_library_item else None,
)
_append_audit_event_standalone(
event_type='ausleihung_returned',
@@ -7629,7 +7658,13 @@ def ausleihen(id):
if total_exemplare <= 1:
it.update_item_status(id, False, effective_borrower)
start_date = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
au.add_ausleihung(id, effective_borrower, start_date, end_date=end_date)
au.add_ausleihung(
id,
effective_borrower,
start_date,
end_date=end_date,
due_date=end_date if is_library_item else None,
)
_append_audit_event_standalone(
event_type='ausleihung_returned',
payload={
@@ -7674,10 +7709,17 @@ def ausleihen(id):
start_date = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
for exemplar in new_borrowed_exemplars:
exemplar_id = f"{id}_{exemplar['number']}"
au.add_ausleihung(exemplar_id, effective_borrower, start_date, end_date=end_date, exemplar_data={
au.add_ausleihung(
exemplar_id,
effective_borrower,
start_date,
end_date=end_date,
due_date=end_date if is_library_item else None,
exemplar_data={
'parent_id': id,
'exemplar_number': exemplar['number']
})
}
)
_append_audit_event_standalone(
event_type='ausleihung_returned',
+1 -1
View File
@@ -685,7 +685,7 @@ def student_card_exists(student_card_id):
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client)
users = db['student_cards']
exists = users.find_one({'SchülerName': normalized}) is not None
exists = users.find_one({'AusweisId': normalized}) is not None
client.close()
return exists
+221 -39
View File
@@ -451,6 +451,27 @@
height: 100%;
}
.physical-scanner-modal {
z-index: 1100;
}
.physical-scanner-modal .modal-content {
max-width: 520px;
text-align: center;
}
.physical-scanner-code {
min-height: 28px;
margin: 18px 0 8px;
padding: 12px;
border: 1px dashed #cbd5e1;
border-radius: 6px;
color: var(--ui-text);
font-family: monospace;
font-size: 1.2em;
letter-spacing: 2px;
}
.detail-gallery-container {
display: flex;
@@ -518,10 +539,9 @@
<input type="text" id="manualItemCode" placeholder="Manueller Mediencode (optional)" style="min-width:180px;">
<button id="resetCardBtn" class="button" type="button">Feld zurücksetzen</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>
<button id="physicalScannerBtn" class="button" type="button" style="margin-left:6px;">
Physischer Scanner starten
</button>
<button id="manualActionBtn" class="button" type="button" style="margin-left:6px; background:#4f46e5; color:white;">Code verarbeiten</button>
</div>
<div class="library-scan-reader-wrap" id="scanReaderWrap" style="display: none; margin-top: 15px;">
@@ -612,6 +632,16 @@
<div id="detailContent"></div>
</div>
</div>
<div id="physicalScannerModal" class="modal physical-scanner-modal" style="display: none;" role="dialog" aria-modal="true" aria-labelledby="physicalScannerTitle">
<div class="modal-content">
<span class="close" id="closePhysicalScannerBtn" role="button" tabindex="0" aria-label="Physischen Scanner schließen">&times;</span>
<h2 id="physicalScannerTitle">Physischer Scanner</h2>
<p id="physicalScannerInstruction">Scanner ist bereit. Bitte den angezeigten Code scannen.</p>
<div id="physicalScannerCode" class="physical-scanner-code" tabindex="0" aria-label="Eingehender Scan" aria-live="polite"></div>
<p id="physicalScannerStatus" class="library-scan-status" aria-live="polite">Warte auf Scan...</p>
<button id="stopPhysicalScannerBtn" class="button" type="button">Scanner schließen</button>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2/dist/quagga.js"></script>
<script>
@@ -842,7 +872,10 @@
let keyboardScannerEnabled = false;
let keyboardScanBuffer = '';
let keyboardLastKeyAt = 0;
const KEYBOARD_SCAN_INTERCHAR_MS = 100; // max time between keystrokes to consider them one scan
let physicalScannerModalOpen = false;
const KEYBOARD_SCAN_INTERCHAR_MS = 500; // max time between slower scanner keystrokes
const physicalScanQueue = [];
let keyboardScanProcessing = false;
let editLibraryState = {
itemId: '',
seriesGroupId: '',
@@ -1184,29 +1217,86 @@
// =========================================================================
// Keyboard scanner handling (physical scanners that send chars then Enter)
// =========================================================================
function getPhysicalScannerInstruction() {
const mode = document.getElementById('scanModeSelect')?.value || 'card_only';
if (mode === 'return_only') return 'Hinweis: Benötigt wird ein Mediencode zur Rückgabe.';
if (mode === 'card_only') return 'Hinweis: Benötigt wird ein Bibliotheksausweis.';
if (mode === 'quick_toggle' && !activeStudentCardId) {
return 'Hinweis: Zuerst wird ein Bibliotheksausweis benötigt.';
}
if (mode === 'quick_toggle') return 'Hinweis: Jetzt wird ein Mediencode benötigt.';
if (mode === 'continuous' && !activeStudentCardId) {
return 'Hinweis: Zuerst wird ein Bibliotheksausweis benötigt.';
}
return 'Hinweis: Jetzt wird der nächste Mediencode benötigt.';
}
function updatePhysicalScannerModal(message, kind = '') {
const instruction = document.getElementById('physicalScannerInstruction');
const status = document.getElementById('physicalScannerStatus');
if (instruction) instruction.textContent = getPhysicalScannerInstruction();
if (status) {
status.textContent = message;
status.classList.remove('ok', 'warn', 'error');
if (kind) status.classList.add(kind);
}
}
function openPhysicalScannerModal() {
const modal = document.getElementById('physicalScannerModal');
if (!modal) return;
physicalScannerModalOpen = true;
keyboardScannerEnabled = true;
keyboardScanBuffer = '';
keyboardLastKeyAt = 0;
modal.style.display = 'flex';
updatePhysicalScannerModal('Warte auf Scan...', 'warn');
document.addEventListener('keydown', keyboardScanKeydownHandler);
document.getElementById('physicalScannerCode')?.focus();
}
function closePhysicalScannerModal() {
physicalScannerModalOpen = false;
keyboardScannerEnabled = false;
keyboardScanBuffer = '';
keyboardLastKeyAt = 0;
document.removeEventListener('keydown', keyboardScanKeydownHandler);
const modal = document.getElementById('physicalScannerModal');
if (modal) modal.style.display = 'none';
const code = document.getElementById('physicalScannerCode');
if (code) code.textContent = '';
}
function keyboardScanKeydownHandler(e) {
// Only active when explicitly enabled
if (!keyboardScannerEnabled) return;
if (!keyboardScannerEnabled || !physicalScannerModalOpen) return;
// Ignore if focus is in an input/textarea/contenteditable to avoid interfering with typing
const active = document.activeElement;
if (active && (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA' || active.isContentEditable)) return;
if (e.key === 'Escape') {
e.preventDefault();
closePhysicalScannerModal();
return;
}
const now = Date.now();
// If Enter/Return pressed -> finalize buffer
if (e.key === 'Enter') {
e.preventDefault();
const code = keyboardScanBuffer.trim();
keyboardScanBuffer = '';
keyboardLastKeyAt = 0;
if (!code) return;
// Process exactly like a camera scan - centralized logic handles the rest!
handleScanSuccess(code);
// Scanner keystrokes must not be submitted into the focused form field.
const displayCode = document.getElementById('physicalScannerCode');
if (displayCode) displayCode.textContent = code;
updatePhysicalScannerModal('Enter erkannt. Code wird verarbeitet...', 'warn');
processPhysicalScan(code);
return;
}
// Only accept common printable characters; ignore modifier keys
// Accept all printable scanner characters, including letters and punctuation.
if (e.key.length === 1) {
// If time gap too big, start new buffer
if (keyboardLastKeyAt && (now - keyboardLastKeyAt) > KEYBOARD_SCAN_INTERCHAR_MS) {
@@ -1214,12 +1304,32 @@
}
keyboardScanBuffer += e.key;
keyboardLastKeyAt = now;
// Prevent default so scanner input doesn't accidentally move focus or trigger shortcuts
const displayCode = document.getElementById('physicalScannerCode');
if (displayCode) displayCode.textContent = keyboardScanBuffer;
// Prevent default so scanner input is captured even while an input has focus.
e.preventDefault();
}
}
function handleScanSuccess(decodedText) {
async function processPhysicalScan(code) {
physicalScanQueue.push(code);
if (keyboardScanProcessing) return;
keyboardScanProcessing = true;
try {
while (physicalScanQueue.length > 0) {
await handleScanSuccess(physicalScanQueue.shift());
if (physicalScannerModalOpen) {
const displayCode = document.getElementById('physicalScannerCode');
if (displayCode) displayCode.textContent = '';
}
}
} finally {
keyboardScanProcessing = false;
}
}
async function handleScanSuccess(decodedText) {
const scannedCode = typeof normalizeScannedCode === "function" ? normalizeScannedCode(decodedText) : decodedText;
if (!scannedCode) return;
@@ -1235,26 +1345,28 @@
// 1. Modus: Nur Rückgabe
if (mode === 'return_only') {
returnByCode(scannedCode);
await returnByCode(scannedCode);
return;
}
// 2. Modus: Nur Ausweis
if (mode === 'card_only') {
setActiveStudentCard(scannedCode);
setScanStatus(`Ausweis gesetzt: ${activeStudentCardId}`, 'ok');
setScanStatus(`Ausweis gesetzt: ${activeStudentCardId}. Bitte den nächsten Ausweis scannen.`, 'ok');
showSmallConfirm(`Ausweis erkannt: ${activeStudentCardId}. Sie können den nächsten scannen.`, 'ok');
updatePhysicalScannerModal(`Ausweis erkannt. Bereit für den nächsten Scan.`, 'ok');
return;
}
// 3. Modus: Schnellmodus (1x Ausweis, 1x Buch)
if (mode === 'quick_toggle') {
processQuickToggleScan(scannedCode);
await processQuickToggleScan(scannedCode);
return;
}
// 4. Modus: Dauermodus (1x Ausweis, Nx Bücher)
if (mode === 'continuous') {
processContinuousScan(scannedCode);
await processContinuousScan(scannedCode);
return;
}
}
@@ -1268,9 +1380,10 @@
setActiveStudentCard(scannedCode);
setScanStatus(`Neuer Ausweis gesetzt: ${activeStudentCardId}. Bitte Medien scannen.`, 'ok');
showSmallConfirm(`Benutzer gewechselt zu: ${activeStudentCardId}`, 'ok');
updatePhysicalScannerModal('Ausweis erkannt. Jetzt Mediencodes scannen.', 'ok');
setTimeout(() => {
if (document.getElementById('scanModeSelect').value === 'continuous') {
if (!physicalScannerModalOpen && document.getElementById('scanModeSelect').value === 'continuous') {
startScanner();
}
}, 1000);
@@ -1280,9 +1393,10 @@
if (!activeStudentCardId) {
setScanStatus('Kein Ausweis aktiv! Bitte zuerst einen Schülerausweis scannen.', 'error');
showSmallConfirm('Bitte zuerst Ausweis scannen', 'error');
updatePhysicalScannerModal('Kein Ausweis aktiv. Bitte zuerst den Ausweis scannen.', 'error');
setTimeout(() => {
if (document.getElementById('scanModeSelect').value === 'continuous') {
if (!physicalScannerModalOpen && document.getElementById('scanModeSelect').value === 'continuous') {
startScanner();
}
}, 2000);
@@ -1308,15 +1422,22 @@
if (!response.ok || !result.ok) {
setScanStatus(result.message || 'Scan-Aktion fehlgeschlagen.', 'error');
showSmallConfirm(result.message || 'Aktion fehlgeschlagen.', 'error');
updatePhysicalScannerModal(result.message || 'Scan-Aktion fehlgeschlagen.', 'error');
} else if (result.action === 'borrowed') {
setScanStatus(`Ausgeliehen: ${result.item_name}`, 'ok');
showSmallConfirm(`Ausgeliehen: ${result.item_name}`, 'ok');
showSmallConfirm(`Ausgeliehen: ${result.item_name}`, 'ok', {
actionLabel: 'Mit nächstem Ausweis starten',
onAction: startNextStudentCardScan
});
updatePhysicalScannerModal('Ausleihe erfolgreich. Bereit für den nächsten Mediencode.', 'ok');
} else if (result.action === 'returned') {
setScanStatus(`Zurückgegeben: ${result.item_name}`, 'ok');
showSmallConfirm(`Zurückgegeben: ${result.item_name}`, 'ok');
updatePhysicalScannerModal('Rückgabe erfolgreich. Bereit für den nächsten Mediencode.', 'ok');
} else {
setScanStatus(result.message || 'Aktion durchgeführt.', 'ok');
showSmallConfirm(result.message || 'Erfolgreich', 'ok');
updatePhysicalScannerModal('Aktion erfolgreich. Bereit für den nächsten Mediencode.', 'ok');
}
// Tabellen-Ansicht aktualisieren
@@ -1327,10 +1448,11 @@
console.error('Continuous scan action failed:', err);
setScanStatus('Fehler beim Verarbeiten des Scans.', 'error');
showSmallConfirm('Fehler im Netzwerk/System', 'error');
updatePhysicalScannerModal('Fehler beim Verarbeiten. Bitte erneut scannen.', 'error');
}
setTimeout(() => {
if (document.getElementById('scanModeSelect').value === 'continuous') {
if (!physicalScannerModalOpen && document.getElementById('scanModeSelect').value === 'continuous') {
startScanner();
}
}, 1500);
@@ -1341,7 +1463,9 @@
// 2. Wenn kein Ausweis gesetzt ist, wird der Code als Ausweis interpretiert
if (!activeStudentCardId) {
setActiveStudentCard(scannedCode);
setScanStatus(`Ausweis gesetzt: ${activeStudentCardId}`, 'ok');
setScanStatus(`Ausweis gesetzt: ${activeStudentCardId}. Bitte den nächsten Mediencode scannen.`, 'ok');
showSmallConfirm(`Ausweis erkannt: ${activeStudentCardId}. Bitte jetzt den nächsten Mediencode scannen.`, 'ok');
updatePhysicalScannerModal('Ausweis erkannt. Jetzt den Mediencode scannen.', 'ok');
return;
}
@@ -1350,7 +1474,11 @@
setScanStatus('Verarbeite Mediencode...', 'warn');
const response = await fetch('/api/library_scan_action', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token }}',
'X-CSRF-Token': '{{ csrf_token }}'
},
body: JSON.stringify({
student_card_id: activeStudentCardId,
item_code: scannedCode
@@ -1360,24 +1488,32 @@
const result = await response.json();
if (!response.ok || !result.ok) {
setScanStatus(result.message || 'Scan-Aktion fehlgeschlagen.', 'error');
updatePhysicalScannerModal(result.message || 'Scan-Aktion fehlgeschlagen.', 'error');
return;
}
if (result.action === 'borrowed') {
setScanStatus(`Ausgeliehen: ${result.item_name}`, 'ok');
showSmallConfirm(`Ausgeliehen: ${result.item_name}`, 'ok');
showSmallConfirm(`Ausgeliehen: ${result.item_name}`, 'ok', {
actionLabel: 'Mit nächstem Ausweis starten',
onAction: startNextStudentCardScan
});
updatePhysicalScannerModal('Ausleihe erfolgreich. Bereit für den nächsten Mediencode.', 'ok');
} else if (result.action === 'returned') {
setScanStatus(`Zurückgegeben: ${result.item_name}`, 'ok');
showSmallConfirm(`Zurückgegeben: ${result.item_name}`, 'ok');
updatePhysicalScannerModal('Rückgabe erfolgreich. Bereit für den nächsten Mediencode.', 'ok');
} else {
setScanStatus(result.message || 'Aktion durchgeführt.', 'ok');
showSmallConfirm(result.message || 'Aktion durchgeführt.', 'ok');
updatePhysicalScannerModal('Aktion erfolgreich. Bereit für den nächsten Mediencode.', 'ok');
}
await loadLibraryItems();
} catch (err) {
console.error('Quick scan action failed:', err);
setScanStatus('Fehler beim Verarbeiten des Scans.', 'error');
updatePhysicalScannerModal('Fehler beim Verarbeiten. Bitte erneut scannen.', 'error');
}
}
@@ -1387,23 +1523,30 @@
try {
const resp = await fetch('/api/library_return_by_code', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token }}',
'X-CSRF-Token': '{{ csrf_token }}'
},
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');
updatePhysicalScannerModal(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');
updatePhysicalScannerModal('Rückgabe erfolgreich. Bereit für den nächsten Mediencode.', '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');
updatePhysicalScannerModal('Fehler bei Rückgabe. Bitte erneut scannen.', 'error');
return false;
}
}
@@ -1520,21 +1663,51 @@
if (kind) el.classList.add(kind);
}
function showSmallConfirm(message, kind='ok') {
function showSmallConfirm(message, kind='ok', action = null) {
// 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">&times;</div>`;
el.innerHTML = `<div>${escapeHtml(String(message || ''))}</div><div style="opacity:0.9; margin-left:8px; font-size:0.85em;">${helper}</div>`;
if (action && typeof action.onAction === 'function') {
const actionButton = document.createElement('button');
actionButton.type = 'button';
actionButton.textContent = action.actionLabel || 'Weiter';
actionButton.style.marginLeft = '8px';
actionButton.style.padding = '6px 10px';
actionButton.style.cursor = 'pointer';
actionButton.addEventListener('click', () => {
if (el.parentNode) el.parentNode.removeChild(el);
action.onAction();
});
el.appendChild(actionButton);
}
const closeButton = document.createElement('div');
closeButton.className = 'close-x';
closeButton.innerHTML = '&times;';
el.appendChild(closeButton);
document.body.appendChild(el);
// close handler
el.querySelector('.close-x').addEventListener('click', () => {
closeButton.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);
}
async function startNextStudentCardScan() {
setActiveStudentCard('');
setScanStatus('Bereit für den nächsten Ausweis.', 'warn');
updatePhysicalScannerModal('Bereit für den nächsten Ausweis.', 'warn');
const mode = document.getElementById('scanModeSelect')?.value;
if (!physicalScannerModalOpen && (mode === 'quick_toggle' || mode === 'continuous') && !scannerRunning) {
await startScanner();
}
}
function setActiveStudentCard(cardId) {
activeStudentCardId = (cardId || '').trim().toUpperCase();
const input = document.getElementById('activeStudentCard');
@@ -1649,7 +1822,7 @@
const toggleBtn = document.getElementById('toggleScannerBtn');
const resetBtn = document.getElementById('resetCardBtn');
const modeSelect = document.getElementById('scanModeSelect');
const keyboardToggle = document.getElementById('keyboardScannerToggle');
const physicalScannerBtn = document.getElementById('physicalScannerBtn');
if (toggleBtn) {
toggleBtn.addEventListener('click', async () => {
@@ -1670,6 +1843,9 @@
if (modeSelect) {
modeSelect.addEventListener('change', () => {
if (physicalScannerModalOpen) {
updatePhysicalScannerModal('Warte auf Scan...', 'warn');
}
if (modeSelect.value === 'quick_toggle' && !activeStudentCardId) {
setScanStatus('Schnellmodus: zuerst Bibliotheksausweis scannen.', 'warn');
} else if (modeSelect.value === 'card_only') {
@@ -1678,18 +1854,24 @@
});
}
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');
if (physicalScannerBtn) {
physicalScannerBtn.addEventListener('click', openPhysicalScannerModal);
}
const closePhysicalScannerBtn = document.getElementById('closePhysicalScannerBtn');
const stopPhysicalScannerBtn = document.getElementById('stopPhysicalScannerBtn');
if (closePhysicalScannerBtn) {
closePhysicalScannerBtn.addEventListener('click', closePhysicalScannerModal);
closePhysicalScannerBtn.addEventListener('keydown', (event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
closePhysicalScannerModal();
}
});
}
if (stopPhysicalScannerBtn) {
stopPhysicalScannerBtn.addEventListener('click', closePhysicalScannerModal);
}
}
// Run when DOM structure is entirely ready
+5 -2
View File
@@ -256,6 +256,7 @@ function submitEmailMahnung() {
const loanId = document.getElementById('modalLoanId').value;
const email = document.getElementById('modalEmail').value;
const message = document.getElementById('modalMessage').value;
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '';
if (!email) {
alert('Bitte geben Sie eine gültige E-Mail-Adresse ein.');
@@ -264,7 +265,7 @@ function submitEmailMahnung() {
fetch('/mahnungen_send_email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken },
body: JSON.stringify({ loan_id: loanId, email: email, message: message })
})
.then(response => response.json())
@@ -289,9 +290,11 @@ function resetMahnung(loanId, studentName) {
return;
}
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '';
fetch('/mahnungen_reset', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken },
body: JSON.stringify({ loan_id: loanId })
})
.then(response => response.json())
+18 -6
View File
@@ -821,14 +821,26 @@ case "$COMMAND" in
exit 1
fi
PORT_ARG="${3:-}"
PASSWORD_ARG="${4:-admin123}" # Optional 4th parameter; defaults to 'admin123' if omitted
ARG3="${3:-}"
ARG4="${4:-}"
PORT_ARG=""
PASSWORD_ARG="admin123"
# Check if the 3rd argument is numeric (Port) or text (Password)
if [ -n "$ARG3" ]; then
if printf '%s\n' "$ARG3" | grep -qE '^[0-9]+$'; then
PORT_ARG="$ARG3"
if [ -n "$ARG4" ]; then
PASSWORD_ARG="$ARG4"
fi
else
# If ARG3 is not numeric, treat it as the password without setting a port
PASSWORD_ARG="$ARG3"
fi
fi
if [ -n "$PORT_ARG" ]; then
if ! printf '%s\n' "$PORT_ARG" | grep -qE '^[0-9]+$'; then
echo "Error: Port must be a numeric value."
exit 1
fi
register_tenant_port "$TENANT_ID" "$PORT_ARG"
update_runtime_ports "$PORT_ARG"
sync_tenant_port_map