Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6b9cf8a024 | |||
| 69bb02b7dc | |||
| 6a3a5d6373 | |||
| 09efedad69 | |||
| b05d238a60 | |||
| 63a7b64150 | |||
| d5f4558b70 | |||
| 0e85337fc5 | |||
| 0114fec928 | |||
| b16ad56b98 |
+71
-29
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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">×</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">×</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 = '×';
|
||||
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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user