diff --git a/Web/app.py b/Web/app.py
index 3585e3b..bcaf712 100755
--- a/Web/app.py
+++ b/Web/app.py
@@ -1339,14 +1339,18 @@ def update_appointment_statuses():
Diese Funktion wird jede Minute ausgeführt und überprüft:
- Geplante Termine, die aktiviert werden sollten
- Aktive Termine, die beendet werden sollten
+ - Überfällige Bibliotheksartikel (Mahnlauf & Admin-Benachrichtigungen)
"""
current_time = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
+ current_time_naive = current_time.replace(tzinfo=None)
+ client = None
try:
- # Hole alle Termine mit Status 'planned' oder 'active'
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
db = client[MONGODB_DB]
ausleihungen = db['ausleihungen']
+ items_col = db['items']
+ student_cards_col = db['student_cards']
# Finde alle Termine, die status updates benötigen
appointments_to_check = list(ausleihungen.find({
@@ -1370,9 +1374,7 @@ def update_appointment_statuses():
extra_fields = {}
# --- Conflict resolver: planned → active transition ---
- # Check if the physical item is already borrowed by someone else
if old_status == 'planned' and new_status == 'active':
- items_col = db['items']
item_id_str = appointment.get('Item')
conflict_detected = False
conflict_note = ''
@@ -1387,7 +1389,6 @@ def update_appointment_statuses():
item_name = item_doc.get('Name', item_id_str)
activation_item_name = item_name
total_exemplare = int(item_doc.get('Exemplare', 1))
- # Count how many active (non-planned) borrows currently hold this item
active_borrows = ausleihungen.count_documents({
'Item': item_id_str,
'Status': 'active',
@@ -1396,7 +1397,6 @@ def update_appointment_statuses():
if active_borrows >= total_exemplare or item_doc.get('Verfuegbar') is False:
conflict_detected = True
borrower = item_doc.get('User', 'unbekannter Benutzer')
- item_name = item_doc.get('Name', item_id_str)
conflict_note = (
f"Gegenstand '{item_name}' war beim Aktivieren von "
f"'{appointment.get('User', '?')}' bereits ausgeliehen "
@@ -1405,13 +1405,10 @@ def update_appointment_statuses():
extra_fields['ConflictDetected'] = True
extra_fields['ConflictNote'] = conflict_note
extra_fields['ConflictAt'] = current_time
- conflict_log = (
- f" [KONFLIKT] Termin {appointment['_id']}: "
- f"planned → active, aber {conflict_note}"
+ app.logger.warning(
+ f" [KONFLIKT] Termin {appointment['_id']}: planned → active, aber {conflict_note}"
)
- app.logger.warning(conflict_log)
else:
- # No conflict — clear any previously stored conflict flag
extra_fields['ConflictDetected'] = False
extra_fields['ConflictNote'] = ''
except Exception as conflict_err:
@@ -1432,7 +1429,6 @@ def update_appointment_statuses():
updated_count += 1
if new_status == 'active':
activated_count += 1
- # Make item unshareable if no conflict is detected
if old_status == 'planned' and appointment.get('Item') and not extra_fields.get('ConflictDetected', False):
try:
it.update_item_status(str(appointment.get('Item')), False, activation_user)
@@ -1441,14 +1437,12 @@ def update_appointment_statuses():
elif new_status == 'completed':
completed_count += 1
- # Make item available again
if appointment.get('Item'):
try:
it.update_item_status(str(appointment.get('Item')), True)
except Exception as e:
app.logger.warning(f"Could not update item status to True for {appointment['_id']}: {e}")
- # Create activation notification even if another worker already updated the status.
if old_status == 'planned' and new_status == 'active' and activation_user:
try:
_create_notification(
@@ -1456,9 +1450,7 @@ def update_appointment_statuses():
audience='user',
notif_type='appointment_activated',
title='Reservierung ist jetzt aktiv',
- message=(
- f"Deine geplante Ausleihe für {activation_item_name} startet jetzt."
- ),
+ message=f"Deine geplante Ausleihe für {activation_item_name} startet jetzt.",
target_user=activation_user,
reference={
'appointment_id': str(appointment.get('_id')),
@@ -1472,108 +1464,131 @@ def update_appointment_statuses():
app.logger.warning(
f"Failed to create activation notification for {appointment.get('_id')}: {notif_err}"
)
+
+ # -----------------------------------------------------------------
+ # Mahnlauf für Bibliotheksartikel (Prüfung auf Überfälligkeit)
+ # -----------------------------------------------------------------
elif it.is_library_item(appointment.get('Item')):
- current_time = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
+ appt = appointment # Verwende das aktuelle Dokument aus der Schleife
+ if appt.get('Status') != 'active':
+ continue
- try:
- client = MongoClient(MONGODB_HOST, MONGODB_PORT)
- db = client[MONGODB_DB]
- ausleihungen = db['ausleihungen']
- users_col = db['users']
- items_col = db['items']
+ due_date_obj = appt.get('DueDate')
+ if not due_date_obj:
+ continue
- # Finde alle aktiven Ausleihungen, die in der Vergangenheit fällig waren
- overdue_appointments = list(ausleihungen.find({
- 'Status': 'active',
- 'DueDate': {'$lt': current_time}
- }))
+ 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
- for appt in overdue_appointments:
- days_overdue = (current_time - appt.get('DueDate', current_time)).days
- mahnstufe = appt.get('Mahnstufe', 0)
- target_user = str(appt.get('User', '')).strip()
- item_id = appt.get('Item')
+ mahnstufe = appt.get('Mahnstufe', 0)
+ raw_user = str(appt.get('User', '')).strip()
- # 1. Objektdetails holen (für den E-Mail-Text)
- item_name = "Unbekannter Artikel"
- if item_id:
- try:
- item_doc = items_col.find_one({'_id': ObjectId(item_id)})
- if item_doc:
- item_name = item_doc.get('Name', str(item_id))
- except Exception:
- pass
+ # Entschlüsselung der AusweisId
+ decrypted_user = decrypt_text(raw_user) if 'decrypt_text' in globals() else raw_user
+ target_ausweis_id = decrypted_user if decrypted_user else raw_user
+ item_id = appt.get('Item')
- # 2. Nutzerdetails holen (für die E-Mail-Adresse)
- user_doc = users_col.find_one({'username': target_user}) # Feldnamen ggf. anpassen (z.B. '_id')
- if not user_doc:
- app.logger.warning(f"Nutzer '{target_user}' für Mahnung nicht gefunden.")
- continue
+ if not target_ausweis_id:
+ continue
- user_email = user_doc.get('email')
+ # In student_cards suchen
+ card = student_cards_col.find_one({'AusweisId': target_ausweis_id})
+ if not card and raw_user != target_ausweis_id:
+ card = student_cards_col.find_one({'AusweisId': raw_user})
- # 3. Eskalationsstufen prüfen
- if days_overdue >= 14 and days_overdue < 28 and mahnstufe == 0:
- # STUFE 1: Erste Mahnung
- ausleihungen.update_one(
- {'_id': appt['_id']},
- {'$set': {'Mahnstufe': 1, 'LastUpdated': current_time}}
- )
+ if not card:
+ app.logger.warning(f"Mahnlauf: Kein Schülerausweis für AusweisId '{target_ausweis_id}' gefunden. Überspringe.")
+ continue
+ student_card = _decrypt_student_card_doc(card) if '_decrypt_student_card_doc' in globals() else card
+ student_name = student_card.get('SchülerName', target_ausweis_id)
+ student_class = student_card.get('Klasse', '—')
+
+ # Gegenstandsdetails laden
+ item_name = "Unbekannter Artikel"
+ if item_id:
+ try:
+ item_doc = items_col.find_one({'_id': ObjectId(str(item_id))})
+ if item_doc:
+ item_name = item_doc.get('Name', str(item_id))
+ code_4 = item_doc.get('Code_4')
+ if code_4:
+ item_name = f"{item_name} ({code_4})"
+ except Exception:
+ pass
+
+ # STUFE 2: >= 28 Tage überfällig -> Ausweis sperren, Stufe 2 setzen & Admins benachrichtigen
+ if days_overdue >= 28 and mahnstufe < 2:
+ ausleihungen.update_one(
+ {'_id': appt['_id']},
+ {'$set': {'Mahnstufe': 2, 'LastUpdated': current_time}}
+ )
+
+ block_reason = f'System-Sperre: Ausleihe von "{item_name}" ist seit {days_overdue} Tagen überfällig.'
+
+ student_cards_col.update_one(
+ {'_id': card['_id']},
+ {'$set': {
+ 'is_blocked': True,
+ 'block_reason': block_reason,
+ 'Aktualisiert': current_time
+ }}
+ )
+
+ title = 'Ausweis gesperrt (2. Mahnung)'
+ body = f'Der Schülerausweis von {student_name} (Klasse {student_class}) wurde wegen "{item_name}" automatisch gesperrt.'
+ target_url = '/mahnungen_admin'
+
+ # 1. In-App Notification (Admin-Tab)
+ if '_create_notification' in globals():
+ try:
_create_notification(
- db, audience='user', notif_type='warning',
- title='Erinnerung: Ausleihe überfällig',
- message=f'Deine Ausleihe für "{item_name}" ist seit {days_overdue} Tagen überfällig.',
- target_user=target_user, severity='warning'
+ db, audience='admin', notif_type='error',
+ title=title, message=body, severity='critical',
+ unique_key=f"mahnlauf:st2:{appt['_id']}"
)
+ except Exception as n_err:
+ app.logger.warning(f"Fehler beim Erstellen der Admin-Notif (Stufe 2): {n_err}")
- if user_email:
- subject = "1. Mahnung: Rückgabe überfällig"
- note = (f"Hallo {target_user},
"
- f"bitte beachte, dass die Ausleihe für den Artikel {item_name} "
- f"seit {days_overdue} Tagen überfällig ist. Bitte bringe den Artikel zeitnah zurück.")
- send(email=user_email, subject=subject, note=note, sender="Bibliotheksverwaltung")
- app.logger.info(f"1. Mahnung an {user_email} gesendet.")
+ # 2. Web-Push Notification für Admins
+ if 'send_push_to_all_admins' in globals():
+ try:
+ send_push_to_all_admins(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}")
- elif days_overdue >= 28 and mahnstufe == 1:
- # STUFE 2: Letzte Mahnung, Sperrung und Report
- ausleihungen.update_one(
- {'_id': appt['_id']},
- {'$set': {'Mahnstufe': 2, 'LastUpdated': current_time}}
- )
+ app.logger.warning(f"Mahnstufe 2 & Ausweis-Sperre für Schülerausweis '{student_name}' ({target_ausweis_id}) gesetzt.")
- # Nutzerkonto für weitere Ausleihen sperren
- users_col.update_one(
- {'_id': user_doc['_id']},
- {'$set': {
- 'is_blocked': True,
- 'block_reason': f'System-Sperre: Ausleihe von "{item_name}" {days_overdue} Tage überfällig.'
- }}
- )
+ # STUFE 1: >= 14 Tage überfällig -> Stufe 1 setzen & Admins benachrichtigen
+ elif days_overdue >= 14 and mahnstufe == 0:
+ ausleihungen.update_one(
+ {'_id': appt['_id']},
+ {'$set': {'Mahnstufe': 1, 'LastUpdated': current_time}}
+ )
+ title = '1. Mahnung erreicht'
+ body = f'Ausleihe überfällig: {student_name} (Klasse {student_class}) hat "{item_name}" seit {days_overdue} Tagen nicht zurückgegeben.'
+ target_url = '/mahnungen_admin'
+
+ # 1. In-App Notification (Admin-Tab)
+ if '_create_notification' in globals():
+ try:
_create_notification(
- db, audience='user', notif_type='error',
- title='Konto gesperrt - 2. Mahnung',
- message=f'Aufgrund der starken Überfälligkeit von "{item_name}" wurde dein Konto vorübergehend gesperrt.',
- target_user=target_user, severity='critical'
+ db, audience='admin', notif_type='warning',
+ title=title, message=body, severity='warning',
+ unique_key=f"mahnlauf:st1:{appt['_id']}"
)
+ except Exception as n_err:
+ app.logger.warning(f"Fehler beim Erstellen der Admin-Notif (Stufe 1): {n_err}")
- if user_email:
- subject = "WICHTIG: Kontosperrung & 2. Mahnung"
- note = (f"Hallo {target_user},
"
- f"deine Ausleihe für den Artikel {item_name} ist nun seit {days_overdue} Tagen überfällig. "
- f"Dies ist ein automatischer Bericht über deinen Status: Dein Konto wurde soeben für neue Reservierungen gesperrt.
"
- f"Die Sperre wird erst aufgehoben, sobald der Artikel im System als zurückgegeben gemeldet wurde.")
- send(email=user_email, subject=subject, note=note, sender="Bibliotheksverwaltung")
- app.logger.warning(
- f"Nutzer {target_user} gesperrt. 2. Mahnung an {user_email} gesendet.")
+ # 2. Web-Push Notification für Admins
+ if 'send_push_to_all_admins' in globals():
+ try:
+ send_push_to_all_admins(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}")
- client.close()
-
- except Exception as e:
- app.logger.error(f"Fehler bei der automatischen Mahnlauf-Prüfung: {e}")
-
- client.close()
+ app.logger.info(f"Mahnstufe 1 für Schülerausweis '{student_name}' ({target_ausweis_id}) gesetzt.")
if updated_count > 0:
app.logger.warning(
@@ -1582,6 +1597,9 @@ def update_appointment_statuses():
except Exception as e:
app.logger.error(f"Automatic appointment status update failed: {e}")
+ finally:
+ if client:
+ client.close()
# Initialize scheduler instances
@@ -3606,205 +3624,18 @@ def test_mahnungen():
# Testdaten (Mock-Daten), um alle if/else Bedingungen im HTML zu testen
generate_test_ausleihen()
create_return_reminders()
- """Admin overview for overdue library items (Mahnungen)."""
- if 'username' not in session:
- flash(
- 'Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!',
- 'error')
- return redirect(url_for('login'))
-
- current_permissions = us.get_effective_permissions(session['username'])
-
- # Hier nutzen wir beispielhaft die library_loans_admin Berechtigung
- if not current_permissions['pages'].get('library_loans_admin', False):
- flash(
- 'Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!',
- 'error')
- return redirect(url_for('library_view'))
-
- if not cfg.MODULES.is_enabled('library'):
- flash('Bibliotheks-Modul ist deaktiviert.', 'error')
- return redirect(url_for('home_admin'))
-
- def fmt_dt(dt):
- try:
- return dt.strftime('%d.%m.%Y') if dt else 'Unbekannt'
- except Exception:
- return str(dt) if dt else 'Unbekannt'
-
- def safe_decrypt(val):
- if not val or not isinstance(val, str):
- return val or ''
- try:
- return decrypt_text(val)
- except Exception:
- return val
-
- current_time = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
- current_time_naive = current_time.replace(tzinfo=None)
- client = None
-
- try:
- client = MongoClient(MONGODB_HOST, MONGODB_PORT)
- db = client[MONGODB_DB]
- ausleihungen_col = db['ausleihungen']
- items_col = db['items']
- users_col = db['users']
- student_cards_col = db['student_cards']
-
- # Überfällige Ausleihen abrufen (Status 'active' und DueDate in der Vergangenheit)
- overdue_records = list(ausleihungen_col.find({
- 'Status': 'active',
- 'DueDate': {'$lt': current_time}
- }).sort('DueDate', 1))
-
- overdue_list = []
-
- if overdue_records:
- # Bulk-Lookups zur Vermeidung von N+1 Queries
- item_ids = []
- for r in overdue_records:
- i_id = r.get('Item')
- if i_id:
- try:
- item_ids.append(ObjectId(str(i_id)))
- except Exception:
- item_ids.append(str(i_id))
-
- items_cursor = items_col.find({
- '_id': {'$in': item_ids}
- }, {'Name': 1, 'Code_4': 1})
- item_map = {str(item['_id']): item for item in items_cursor}
-
- raw_users = [r.get('User') for r in overdue_records if r.get('User')]
- users_cursor = users_col.find({'username': {'$in': raw_users}}, {'username': 1, 'is_blocked': 1})
- user_block_map = {u.get('username'): u.get('is_blocked', False) for u in users_cursor}
-
- class_map = {}
- all_cards = list(student_cards_col.find({}, {'SchülerName': 1, 'Klasse': 1, 'AusweisId': 1}))
- for card in all_cards:
- raw_cls = card.get('Klasse')
- dec_cls = safe_decrypt(raw_cls)
- if not dec_cls:
- continue
-
- raw_name = card.get('SchülerName')
- dec_name = safe_decrypt(raw_name)
- ausweis_id = card.get('AusweisId')
-
- if dec_name:
- class_map[dec_name.strip().lower()] = dec_cls
- if raw_name:
- class_map[raw_name] = dec_cls
- if ausweis_id:
- class_map[str(ausweis_id).strip().lower()] = dec_cls
-
- # Datenaufbereitung für das Template
- for record in overdue_records:
- item_id = str(record.get('Item') or '')
- item_doc = item_map.get(item_id, {})
- item_name = item_doc.get('Name', item_id)
- item_code = item_doc.get('Code_4', '')
-
- if item_code:
- item_name = f"{item_name} ({item_code})"
-
- raw_user = record.get('User', '')
- decrypted_user = safe_decrypt(raw_user)
- display_user = decrypted_user if decrypted_user else (raw_user or 'Unbekannt')
-
- user_class = (
- safe_decrypt(record.get('Klasse'))
- or safe_decrypt(record.get('Class'))
- or safe_decrypt(record.get('school_class'))
- or class_map.get(raw_user, '')
- or class_map.get(decrypted_user, '')
- or class_map.get(display_user.strip().lower(), '')
- or '—'
- )
-
- # KORREKTUR: 'record' statt 'appt'
- due_date = record.get('DueDate', current_time)
- due_date_naive = due_date.replace(tzinfo=None) if due_date else current_time_naive
- days_overdue = (current_time_naive - due_date_naive).days
- is_blocked = user_block_map.get(raw_user, False)
-
- overdue_list.append({
- 'id': str(record.get('_id')),
- 'item_name': item_name,
- 'user': display_user,
- 'klasse': user_class,
- 'due_date': fmt_dt(due_date), # KORREKTUR: 'due_date' statt 'due_date_obj'
- 'days_overdue': days_overdue,
- 'mahnstufe': record.get('Mahnstufe', 0),
- 'is_blocked': is_blocked
- })
-
- return render_template(
- 'mahnungen_admin.html',
- overdue_list=overdue_list,
- library_module_enabled=cfg.MODULES.is_enabled('library'),
- student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
- )
-
- except Exception as e:
- app.logger.error(f"Error loading mahnungen admin view: {e}")
- flash('Fehler beim Laden der Mahnungsverwaltung.', 'error')
- return redirect(url_for('home_admin'))
- finally:
- if client:
- client.close()
-
- """ test_overdue_list = [
- {
- "user": "Max Mustermann",
- "klasse": "10A",
- "item_name": "Biologie heute 2",
- "due_date": "10.08.2026",
- "days_overdue": 12,
- "mahnstufe": 2,
- "is_blocked": True
- },
- {
- "user": "Anna Schmidt",
- "klasse": "9B",
- "item_name": "Taschenrechner TI-30",
- "due_date": "15.08.2026",
- "days_overdue": 7,
- "mahnstufe": 1,
- "is_blocked": False
- },
- {
- "user": "Lukas Weber",
- "klasse": "12",
- "item_name": "Faust - Der Tragödie erster Teil",
- "due_date": "21.08.2026",
- "days_overdue": 1,
- "mahnstufe": 0,
- "is_blocked": False
- }
- ]
-
- # Render das Template und übergebe die Testdaten
- return render_template('mahnungen_admin.html', overdue_list=test_overdue_list, APP_VERSION="1.0.0")"""
@app.route('/mahnungen_admin')
def mahnungen_admin():
- """Admin overview for overdue library items (Mahnungen)."""
+ """Admin-Übersicht für überfällige Bibliotheks-Ausleihen."""
if 'username' not in session:
- flash(
- 'Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!',
- 'error')
+ flash('Bitte melden Sie sich an.', 'error')
return redirect(url_for('login'))
current_permissions = us.get_effective_permissions(session['username'])
-
- # Hier nutzen wir beispielhaft die library_loans_admin Berechtigung
if not current_permissions['pages'].get('library_loans_admin', False):
- flash(
- 'Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!',
- 'error')
+ flash('Fehlende Berechtigung.', 'error')
return redirect(url_for('library_view'))
if not cfg.MODULES.is_enabled('library'):
@@ -3817,14 +3648,6 @@ def mahnungen_admin():
except Exception:
return str(dt) if dt else 'Unbekannt'
- def safe_decrypt(val):
- if not val or not isinstance(val, str):
- return val or ''
- try:
- return decrypt_text(val)
- except Exception:
- return val
-
current_time = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
current_time_naive = current_time.replace(tzinfo=None)
client = None
@@ -3834,7 +3657,6 @@ def mahnungen_admin():
db = client[MONGODB_DB]
ausleihungen_col = db['ausleihungen']
items_col = db['items']
- users_col = db['users']
student_cards_col = db['student_cards']
overdue_records = list(ausleihungen_col.find({
@@ -3845,6 +3667,7 @@ def mahnungen_admin():
overdue_list = []
if overdue_records:
+ # 1. Items laden
item_ids = []
for r in overdue_records:
i_id = r.get('Item')
@@ -3854,72 +3677,51 @@ def mahnungen_admin():
except Exception:
item_ids.append(str(i_id))
- items_cursor = items_col.find({
- '_id': {'$in': item_ids}
- }, {'Name': 1, 'Code_4': 1})
+ items_cursor = items_col.find({'_id': {'$in': item_ids}}, {'Name': 1, 'Code_4': 1})
item_map = {str(item['_id']): item for item in items_cursor}
- raw_users = [r.get('User') for r in overdue_records if r.get('User')]
- users_cursor = users_col.find({'username': {'$in': raw_users}}, {'username': 1, 'is_blocked': 1})
- user_block_map = {u.get('username'): u.get('is_blocked', False) for u in users_cursor}
-
- class_map = {}
- all_cards = list(student_cards_col.find({}, {'SchülerName': 1, 'Klasse': 1, 'AusweisId': 1}))
- for card in all_cards:
- raw_cls = card.get('Klasse')
- dec_cls = safe_decrypt(raw_cls)
- if not dec_cls:
- continue
-
- raw_name = card.get('SchülerName')
- dec_name = safe_decrypt(raw_name)
- ausweis_id = card.get('AusweisId')
-
- if dec_name:
- class_map[dec_name.strip().lower()] = dec_cls
- if raw_name:
- class_map[raw_name] = dec_cls
+ # 2. Alle Schülerausweise entschlüsseln und mappen
+ all_cards = list(student_cards_col.find())
+ card_map = {}
+ for c in all_cards:
+ dec_card = _decrypt_student_card_doc(c) if '_decrypt_student_card_doc' in globals() else c
+ ausweis_id = dec_card.get('AusweisId')
if ausweis_id:
- class_map[str(ausweis_id).strip().lower()] = dec_cls
+ card_map[str(ausweis_id).strip().upper()] = dec_card
+ # 3. Liste für das Frontend aufbauen
for record in overdue_records:
item_id = str(record.get('Item') or '')
item_doc = item_map.get(item_id, {})
item_name = item_doc.get('Name', item_id)
item_code = item_doc.get('Code_4', '')
-
if item_code:
item_name = f"{item_name} ({item_code})"
- raw_user = record.get('User', '')
- decrypted_user = safe_decrypt(raw_user)
- display_user = decrypted_user if decrypted_user else (raw_user or 'Unbekannt')
+ raw_user = str(record.get('User') or '')
+ decrypted_user = decrypt_text(raw_user)
+ ausweis_id = (decrypted_user if decrypted_user else raw_user).strip().upper()
- user_class = (
- safe_decrypt(record.get('Klasse'))
- or safe_decrypt(record.get('Class'))
- or safe_decrypt(record.get('school_class'))
- or class_map.get(raw_user, '')
- or class_map.get(decrypted_user, '')
- or class_map.get(display_user.strip().lower(), '')
- or '—'
- )
+ student_card = card_map.get(ausweis_id, {})
+ student_name = student_card.get('SchülerName', ausweis_id if ausweis_id else 'Unbekannt')
+ student_class = student_card.get('Klasse', '—')
+ 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')
-
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
else:
days_overdue = 0
- is_blocked = user_block_map.get(raw_user, False)
-
overdue_list.append({
'id': str(record.get('_id')),
'item_name': item_name,
- 'user': display_user,
- 'klasse': user_class,
+ 'ausweis_id': ausweis_id,
+ 'schueler_name': student_name,
+ 'klasse': student_class,
+ 'email': student_email,
'due_date': fmt_dt(due_date_obj),
'days_overdue': days_overdue,
'mahnstufe': record.get('Mahnstufe', 0),
@@ -3934,13 +3736,156 @@ def mahnungen_admin():
)
except Exception as e:
- app.logger.error(f"Error loading mahnungen admin view: {e}")
+ app.logger.error(f"Fehler beim Laden der Mahnungsverwaltung: {e}")
flash('Fehler beim Laden der Mahnungsverwaltung.', 'error')
return redirect(url_for('home_admin'))
finally:
if client:
client.close()
+@app.route('/mahnungen_reset', methods=['POST'])
+def mahnungen_reset():
+ """Setzt die Mahnstufe einer Ausleihe zurück, verlängert die Frist und entsperrt den Schülerausweis."""
+ if 'username' not in session:
+ return jsonify({'success': False, 'message': 'Nicht angemeldet.'}), 401
+
+ current_permissions = us.get_effective_permissions(session['username'])
+ if not current_permissions['pages'].get('library_loans_admin', False):
+ return jsonify({'success': False, 'message': 'Keine Berechtigung.'}), 403
+
+ data = request.get_json() or {}
+ loan_id = data.get('loan_id')
+
+ if not loan_id:
+ return jsonify({'success': False, 'message': 'Ausleih-ID fehlt.'}), 400
+
+ client = None
+ try:
+ client = MongoClient(MONGODB_HOST, MONGODB_PORT)
+ db = client[MONGODB_DB]
+ ausleihungen_col = db['ausleihungen']
+ student_cards_col = db['student_cards']
+
+ # 1. Ausleihe finden
+ record = ausleihungen_col.find_one({'_id': ObjectId(loan_id)})
+ if not record:
+ return jsonify({'success': False, 'message': 'Ausleihe nicht gefunden.'}), 404
+
+ current_time = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
+ new_due_date = current_time + datetime.timedelta(days=14) # Frist um 14 Tage verlängern
+
+ # 2. Mahnstufe zurücksetzen und Frist verlängern
+ ausleihungen_col.update_one(
+ {'_id': ObjectId(loan_id)},
+ {'$set': {
+ 'Mahnstufe': 0,
+ 'DueDate': new_due_date,
+ 'LastUpdated': current_time
+ }}
+ )
+
+ # 3. Schülerausweis ermitteln und entsperren
+ raw_user = str(record.get('User', ''))
+ decrypted_user = decrypt_text(raw_user)
+ ausweis_id = decrypted_user if decrypted_user else raw_user
+
+ card = student_cards_col.find_one({'AusweisId': ausweis_id})
+ if not card and raw_user != ausweis_id:
+ card = student_cards_col.find_one({'AusweisId': raw_user})
+
+ if card:
+ student_cards_col.update_one(
+ {'_id': card['_id']},
+ {'$set': {
+ 'is_blocked': False,
+ 'block_reason': '',
+ 'Aktualisiert': current_time
+ }}
+ )
+ app.logger.info(f"Schülerausweis '{ausweis_id}' manuell entsperrt und Mahnstufe zurückgesetzt.")
+
+ return jsonify({
+ 'success': True,
+ 'message': 'Mahnstufe zurückgesetzt, Frist um 14 Tage verlängert und Ausweis entsperrt.'
+ })
+
+ except Exception as e:
+ app.logger.error(f"Fehler beim Zurücksetzen der Mahnung: {e}")
+ return jsonify({'success': False, 'message': f'Fehler beim Zurücksetzen: {str(e)}'}), 500
+ finally:
+ if client:
+ client.close()
+
+
+@app.route('/mahnungen_send_email', methods=['POST'])
+def mahnungen_send_email():
+ """Verschickt manuell eine Mahnungs-E-Mail über die Eingabe aus dem Frontend-Modal."""
+ if 'username' not in session:
+ return jsonify({'success': False, 'message': 'Nicht angemeldet.'}), 401
+
+ current_permissions = us.get_effective_permissions(session['username'])
+ if not current_permissions['pages'].get('library_loans_admin', False):
+ return jsonify({'success': False, 'message': 'Keine Berechtigung.'}), 403
+
+ data = request.get_json() or {}
+ loan_id = data.get('loan_id')
+ recipient_email = data.get('email', '').strip()
+
+ if not loan_id or not recipient_email:
+ return jsonify({'success': False, 'message': 'Ausleih-ID und E-Mail-Adresse sind erforderlich.'}), 400
+
+ client = None
+ try:
+ client = MongoClient(MONGODB_HOST, MONGODB_PORT)
+ db = client[MONGODB_DB]
+ ausleihungen_col = db['ausleihungen']
+ items_col = db['items']
+ student_cards_col = db['student_cards']
+
+ record = ausleihungen_col.find_one({'_id': ObjectId(loan_id)})
+ if not record:
+ return jsonify({'success': False, 'message': 'Ausleihe nicht gefunden.'}), 404
+
+ # Gegenstand und Schülerausweis ermitteln
+ raw_user = str(record.get('User', ''))
+ decrypted_user = decrypt_text(raw_user)
+ ausweis_id = decrypted_user if decrypted_user else raw_user
+
+ card = student_cards_col.find_one({'AusweisId': ausweis_id})
+ student_card = _decrypt_student_card_doc(card) if card and '_decrypt_student_card_doc' in globals() else (
+ card or {})
+ student_name = student_card.get('SchülerName', ausweis_id)
+
+ item_doc = items_col.find_one({'_id': ObjectId(str(record.get('Item')))}) if record.get('Item') else {}
+ item_name = item_doc.get('Name', 'Artikel') if item_doc else 'Artikel'
+
+ mahnstufe = record.get('Mahnstufe', 0)
+
+ # E-Mail Betreff & Text generieren
+ subject = f"{mahnstufe}. Mahnung: Bibliotheksausleihe überfällig" if mahnstufe > 0 else "Erinnerung: Bibliotheksausleihe"
+ note = (
+ f"Hallo {student_name},
"
+ f"dies ist eine Erinnerung bezüglich der Ausleihe für den Artikel {item_name}.
"
+ f"Aktuelle Mahnstufe: {mahnstufe}.
"
+ f"Bitte bringe den Artikel so schnell wie möglich in der Bibliothek zurück."
+ )
+
+ # E-Mail über existierende Mail-Funktion senden
+ send(email=recipient_email, subject=subject, note=note, sender="Bibliotheksverwaltung")
+
+ # Optional: E-Mail auch direkt auf der Schülerausweis-Karte hinterlegen/aktualisieren
+ if card:
+ student_cards_col.update_one({'_id': card['_id']}, {'$set': {'email': recipient_email}})
+
+ return jsonify({'success': True, 'message': f'Mahnung erfolgreich an {recipient_email} gesendet.'})
+
+ except Exception as e:
+ app.logger.error(f"Fehler beim Senden der Mahn-E-Mail: {e}")
+ return jsonify({'success': False, 'message': f'Fehler beim Senden: {str(e)}'}), 500
+ finally:
+ if client:
+ client.close()
+
@app.route('/api/library_items')
def api_library_items():
"""
@@ -4181,11 +4126,13 @@ def api_library_group(series_group_id):
app.logger.error('Error loading library group %s: %s', series_group_id, exc)
return jsonify({'items': [], 'message': 'Gruppe konnte nicht geladen werden.'}), 500
+
@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.
+ Automatically unblocks the student card if no other overdue items exist.
"""
if 'username' not in session:
return jsonify({'ok': False, 'message': 'Nicht angemeldet.'}), 401
@@ -4206,6 +4153,7 @@ def api_library_return_by_code():
db = client[MONGODB_DB]
items_col = db['items']
ausleihungen_col = db['ausleihungen']
+ student_cards_col = db['student_cards'] # Hinzugefügt für die Entsperr-Logik
query_or = [
{'Code_4': item_code_raw},
@@ -4229,7 +4177,10 @@ def api_library_return_by_code():
if item_doc.get('Verfuegbar', True):
return jsonify({'ok': False, 'message': 'Dieses Medium ist nicht als ausgeliehen markiert.'}), 409
- # Mark active ausleihungen as completed
+ # 1. Aktive Ausleihungen VOR dem Update zwischenspeichern, um den Nutzer (AusweisId) zu identifizieren
+ active_loans = list(ausleihungen_col.find({'Item': item_id, 'Status': 'active'}))
+
+ # 2. Mark active ausleihungen as completed
update_result = ausleihungen_col.update_many(
{'Item': item_id, 'Status': 'active'},
{'$set': {
@@ -4239,10 +4190,46 @@ def api_library_return_by_code():
}}
)
- # Update item status to available
+ # 3. Update item status to available
borrower_name = str(item_doc.get('User') or '').strip() or ''
it.update_item_status(item_id, True, borrower_name)
+ # 4. Automatische Entsperr-Logik für die identifizierten Schülerausweise
+ for loan in active_loans:
+ raw_user = str(loan.get('User', ''))
+ if not raw_user:
+ continue
+
+ # AusweisId entschlüsseln
+ decrypted_user = decrypt_text(raw_user) if 'safe_decrypt' in globals() else raw_user
+ ausweis_id = decrypted_user if decrypted_user else raw_user
+
+ # Prüfen, ob der Schüler noch andere eskalierte Ausleihungen hat
+ # Da die aktuelle Ausleihe oben auf 'completed' gesetzt wurde, wird sie hier nicht mitgezählt!
+ weitere_sperren = ausleihungen_col.count_documents({
+ 'User': {'$in': [raw_user, encrypt_text(ausweis_id) if 'encrypt_text' in globals() else ausweis_id]},
+ 'Status': 'active',
+ 'Mahnstufe': {'$gte': 2}
+ })
+
+ # Wenn keine anderen Mahnstufe 2+ Ausleihungen aktiv sind -> Ausweis entsperren
+ if weitere_sperren == 0:
+ card = student_cards_col.find_one({'AusweisId': ausweis_id})
+ if not card and raw_user != ausweis_id:
+ card = student_cards_col.find_one({'AusweisId': raw_user})
+
+ if card and card.get('is_blocked'):
+ student_cards_col.update_one(
+ {'_id': card['_id']},
+ {'$set': {
+ 'is_blocked': False,
+ 'block_reason': '',
+ 'Aktualisiert': now
+ }}
+ )
+ app.logger.info(
+ f"Automatische Entsperrung: Schülerausweis '{ausweis_id}' wurde nach Code-Rückgabe entsperrt.")
+
_append_audit_event_standalone(
event_type='ausleihung_returned_by_code',
payload={
diff --git a/Web/templates/mahnungen_admin.html b/Web/templates/mahnungen_admin.html
index e029187..4fb7325 100644
--- a/Web/templates/mahnungen_admin.html
+++ b/Web/templates/mahnungen_admin.html
@@ -129,97 +129,182 @@
}
-
| Schüler / Ausweis | +Klasse | +Medium | +Fälligkeitsdatum | +Überfällig seit | +Mahnstufe | +Aktionen | +
|---|---|---|---|---|---|---|
|
+ {{ item.schueler_name }} + {{ item.ausweis_id }} + |
+ {{ item.klasse }} | +
+ {{ item.item_name }} + Code: {{ item.item_code or '—' }} + |
+ {{ item.due_date }} | ++ {{ item.days_overdue }} Tage + | ++ {% if item.mahnstufe == 2 %} + Stufe 2 (Gesperrt) + {% elif item.mahnstufe == 1 %} + Stufe 1 + {% else %} + Stufe 0 + {% endif %} + | +
+
+
+
+
+ |
+
Aktuell gibt es keine überfälligen Mahnungen.
+| Nutzer | -Klasse | -Gegenstand | -Fällig am | -Tage drüber | -Mahnstufe | -Konto-Status | -
|---|---|---|---|---|---|---|
| {{ item.user or '—' }} | -{{ item.klasse or '—' }} | -{{ item.item_name or '—' }} | -{{ item.due_date or '—' }} | -{{ item.days_overdue }} Tage | -- {% if item.mahnstufe == 2 %} - Stufe 2 (Gesperrt) - {% elif item.mahnstufe == 1 %} - Stufe 1 (Gewarnt) - {% else %} - Stufe 0 (Neu) - {% endif %} - | -- {% if item.is_blocked %} - Gesperrt - {% else %} - Aktiv - {% endif %} - | -