Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| acfb633cda | |||
| 23dfb7d719 | |||
| 6b9cf8a024 | |||
| 69bb02b7dc | |||
| 6a3a5d6373 | |||
| 09efedad69 | |||
| b05d238a60 |
+75
-28
@@ -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
|
||||
@@ -1381,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 ---
|
||||
@@ -1483,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
|
||||
|
||||
@@ -1566,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
|
||||
@@ -1597,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:
|
||||
@@ -2176,7 +2188,7 @@ def _upload_student_cards_excel():
|
||||
|
||||
current_permissions = us.get_effective_permissions(session['username'])
|
||||
|
||||
if not current_permissions['actions'].get('can_manage_user', False):
|
||||
if not current_permissions['actions'].get('can_manage_users', False):
|
||||
flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error')
|
||||
return redirect(url_for('library_view'))
|
||||
|
||||
@@ -3660,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 = []
|
||||
@@ -3705,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
|
||||
@@ -4261,6 +4276,7 @@ def api_library_scan_action():
|
||||
- scan student card id
|
||||
- scan media code (ISBN/Code_4)
|
||||
- borrow if available, otherwise return (toggle behavior)
|
||||
- action='borrow' can be used by scan modes that must never return an item
|
||||
"""
|
||||
if 'username' not in session:
|
||||
return jsonify({'ok': False, 'message': 'Nicht angemeldet.'}), 401
|
||||
@@ -4270,6 +4286,7 @@ def api_library_scan_action():
|
||||
return jsonify({'ok': False, 'message': 'Schülerausweis-Modul ist deaktiviert.'}), 403
|
||||
|
||||
payload = request.get_json(silent=True) or {}
|
||||
requested_action = str(payload.get('action') or 'toggle').strip().lower()
|
||||
student_card_id = us.normalize_student_card_id(payload.get('student_card_id') or payload.get('card_id'))
|
||||
item_code_raw = str(payload.get('item_code') or payload.get('code') or '').strip()
|
||||
duration_raw = str(payload.get('borrow_duration_days') or '').strip()
|
||||
@@ -4342,7 +4359,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',
|
||||
@@ -4367,6 +4390,12 @@ def api_library_scan_action():
|
||||
'message': f"{item_doc.get('Name', 'Medium')} wurde ausgeliehen."
|
||||
}), 200
|
||||
|
||||
if requested_action == 'borrow':
|
||||
return jsonify({
|
||||
'ok': False,
|
||||
'message': f"Medium ist bereits ausgeliehen und wurde nicht verändert.",
|
||||
}), 409
|
||||
|
||||
# Toggle back: item is currently borrowed -> return
|
||||
current_borrower = str(item_doc.get('User') or '').strip()
|
||||
current_permissions = us.get_effective_permissions(session['username'])
|
||||
@@ -5140,7 +5169,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'))
|
||||
@@ -5334,7 +5362,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'):
|
||||
@@ -7541,7 +7569,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',
|
||||
@@ -7632,7 +7666,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={
|
||||
@@ -7677,10 +7717,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
|
||||
|
||||
|
||||
@@ -1376,7 +1376,7 @@
|
||||
<li><a class="dropdown-item" href="{{ url_for('mahnungen_admin') }}">Mahnungen</a></li>
|
||||
{% endif %}
|
||||
{% if student_cards_module_enabled %}
|
||||
{% if current_permissions.actions.get('can_manage_users', False) %}
|
||||
{% if current_permissions.actions.get('can_manage_settings', False) %}
|
||||
<li><a class="dropdown-item" href="{{ url_for('student_cards_admin') }}">Bibliotheksausweis</a></li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
@@ -1383,7 +1383,7 @@
|
||||
updatePhysicalScannerModal('Ausweis erkannt. Jetzt Mediencodes scannen.', 'ok');
|
||||
|
||||
setTimeout(() => {
|
||||
if (document.getElementById('scanModeSelect').value === 'continuous') {
|
||||
if (!physicalScannerModalOpen && document.getElementById('scanModeSelect').value === 'continuous') {
|
||||
startScanner();
|
||||
}
|
||||
}, 1000);
|
||||
@@ -1396,7 +1396,7 @@
|
||||
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);
|
||||
@@ -1414,7 +1414,8 @@
|
||||
},
|
||||
body: JSON.stringify({
|
||||
student_card_id: activeStudentCardId,
|
||||
item_code: scannedCode
|
||||
item_code: scannedCode,
|
||||
action: 'borrow'
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1425,7 +1426,10 @@
|
||||
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');
|
||||
@@ -1449,7 +1453,7 @@
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (document.getElementById('scanModeSelect').value === 'continuous') {
|
||||
if (!physicalScannerModalOpen && document.getElementById('scanModeSelect').value === 'continuous') {
|
||||
startScanner();
|
||||
}
|
||||
}, 1500);
|
||||
@@ -1491,7 +1495,10 @@
|
||||
|
||||
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');
|
||||
@@ -1517,7 +1524,11 @@
|
||||
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();
|
||||
@@ -1653,21 +1664,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');
|
||||
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user