diff --git a/Web/app.py b/Web/app.py
index 662b58f..56bc73a 100755
--- a/Web/app.py
+++ b/Web/app.py
@@ -1588,8 +1588,7 @@ def update_appointment_statuses():
f"Failed to create activation notification for {appointment.get('_id')}: {notif_err}"
)
elif it.is_library_item(appointment.get('Item')):
- # Introduction of an messaging system and a Mahnstufen implementation for the library Book bookings after the designatet time.
- pass
+ create_return_reminders()
client.close()
@@ -1713,6 +1712,111 @@ def _shutdown_scheduler():
atexit.register(_shutdown_scheduler)
+
+def create_return_reminders():
+ """
+ Prüft aktive Ausleihungen auf Überschreitung der Rückgabefrist,
+ erhöht Mahnstufen, versendet E-Mails/Benachrichtigungen und sperrt Nutzer.
+ """
+ current_time = datetime.datetime.now(datetime.timezone.utc)
+
+ try:
+ client = MongoClient(MONGODB_HOST, MONGODB_PORT)
+ db = client[MONGODB_DB]
+ ausleihungen = db['ausleihungen']
+ users_col = db['users']
+ items_col = db['items']
+
+ # Finde alle aktiven Ausleihungen, die in der Vergangenheit fällig waren
+ overdue_appointments = list(ausleihungen.find({
+ 'Status': 'active',
+ 'DueDate': {'$lt': current_time}
+ }))
+
+ 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')
+
+ # 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
+
+ # 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
+
+ user_email = user_doc.get('email')
+
+ # 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}}
+ )
+
+ _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'
+ )
+
+ 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.")
+
+ 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}}
+ )
+
+ # 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.'
+ }}
+ )
+
+ _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'
+ )
+
+ 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.")
+
+ client.close()
+
+ except Exception as e:
+ app.logger.error(f"Fehler bei der automatischen Mahnlauf-Prüfung: {e}")
+
"""-------------------------------------------------------------File Upload Validation----------------------------------------------------------------------------- """
def allowed_file(filename, file_content=None, max_size_mb=cfg.MAX_UPLOAD_MB):
@@ -3529,6 +3633,155 @@ def library_loans_admin():
if client:
client.close()
+
+@app.route('/mahnungen_admin')
+def mahnungen_admin():
+ """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(datetime.timezone.utc)
+ 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 '—'
+ )
+
+ due_date_obj = record.get('DueDate')
+ days_overdue = (current_time - due_date_obj).days if due_date_obj else 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,
+ 'due_date': fmt_dt(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()
+
@app.route('/api/library_items')
def api_library_items():
"""
diff --git a/Web/templates/base.html b/Web/templates/base.html
index 9877196..cd3aaa5 100755
--- a/Web/templates/base.html
+++ b/Web/templates/base.html
@@ -1373,6 +1373,7 @@
| Nutzer | +Klasse | +Gegenstand | +Fällig am | +Tage drüber | +Mahnstufe | +Konto-Status | +
|---|---|---|---|---|---|---|
| {{ item.user }} | +{{ item.klasse }} | +{{ item.item_name }} | +{{ item.due_date }} | +{{ 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 %} + | +