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 @@ } -
-
-

Mahnungsübersicht

-
- Zur Ausleihenverwaltung - Bibliothek öffnen +
+
+
+
+
+
Mahnungs- und Ausleihverwaltung
+ Übersicht überfälliger Medien +
+
+ {% if overdue_items and overdue_items|length > 0 %} +
+ + + + + + + + + + + + + + {% for item in overdue_items %} + + + + + + + + + + {% endfor %} + +
Schüler / AusweisKlasseMediumFälligkeitsdatumÜberfällig seitMahnstufeAktionen
+ {{ 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 %} + +
+ + +
+
+
+ {% else %} +
+ +

Aktuell gibt es keine überfälligen Mahnungen.

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