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 @@
  • {% if current_permissions.pages.get('library_loans_admin', False) %}
  • Alle Ausleihen/Alle Defekten Items
  • +
  • Mahnungen
  • {% endif %} {% if student_cards_module_enabled %} {% if current_permissions.actions.get('can_manage_users', False) %} diff --git a/Web/templates/mahnungen_admin.html b/Web/templates/mahnungen_admin.html new file mode 100644 index 0000000..cf4b224 --- /dev/null +++ b/Web/templates/mahnungen_admin.html @@ -0,0 +1,256 @@ +{% extends "base.html" %} + +{% block title %}Mahnungen & Überfällige Ausleihen{% endblock %} + +{% block content %} + + + + + + + + + + + +
    + +
    +

    🚨 Mahnungsübersicht

    + Zurück zur Bibliothek +
    + +
    + {% if overdue_list %} + + + + + + + + + + + + + + + + + + {% for item in overdue_list %} + + + + + + + + + + + + {% endfor %} + +
    NutzerKlasseGegenstandFällig amTage drüberMahnstufeKonto-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 %} +
    + {% else %} +
    + Hervorragend! Aktuell gibt es keine überfälligen Ausleihen im System. +
    + {% endif %} +
    +
    + + + + + + +{% endblock %} \ No newline at end of file