changes to the mahnungssystem in generell and especialy for the overview
Release Inventarsystem / release-docker (push) Successful in 2m16s
Release Inventarsystem / release-docker (push) Successful in 2m16s
This commit is contained in:
+330
-343
@@ -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},<br><br>"
|
||||
f"bitte beachte, dass die Ausleihe für den Artikel <b>{item_name}</b> "
|
||||
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},<br><br>"
|
||||
f"deine Ausleihe für den Artikel <b>{item_name}</b> ist nun seit {days_overdue} Tagen überfällig. "
|
||||
f"Dies ist ein automatischer Bericht über deinen Status: <b>Dein Konto wurde soeben für neue Reservierungen gesperrt.</b><br><br>"
|
||||
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},<br><br>"
|
||||
f"dies ist eine Erinnerung bezüglich der Ausleihe für den Artikel <b>{item_name}</b>.<br>"
|
||||
f"Aktuelle Mahnstufe: <b>{mahnstufe}</b>.<br><br>"
|
||||
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={
|
||||
|
||||
@@ -129,97 +129,182 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="mahnungen-shell">
|
||||
<div class="mahnungen-head">
|
||||
<h1>Mahnungsübersicht</h1>
|
||||
<div class="head-actions">
|
||||
<a class="btn btn-outline-secondary" href="{{ url_for('library_loans_admin') }}">Zur Ausleihenverwaltung</a>
|
||||
<a class="btn btn-secondary" href="{{ url_for('library_view') }}">Bibliothek öffnen</a>
|
||||
<div class="container-fluid py-4">
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0"><i class="bi bi-exclamation-triangle-fill me-2"></i>Mahnungs- und Ausleihverwaltung</h5>
|
||||
<span class="badge bg-light text-dark">Übersicht überfälliger Medien</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if overdue_items and overdue_items|length > 0 %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Schüler / Ausweis</th>
|
||||
<th>Klasse</th>
|
||||
<th>Medium</th>
|
||||
<th>Fälligkeitsdatum</th>
|
||||
<th>Überfällig seit</th>
|
||||
<th>Mahnstufe</th>
|
||||
<th class="text-end">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in overdue_items %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{{ item.schueler_name }}</strong><br>
|
||||
<small class="text-muted">{{ item.ausweis_id }}</small>
|
||||
</td>
|
||||
<td>{{ item.klasse }}</td>
|
||||
<td>
|
||||
{{ item.item_name }}<br>
|
||||
<small class="text-muted">Code: {{ item.item_code or '—' }}</small>
|
||||
</td>
|
||||
<td>{{ item.due_date }}</td>
|
||||
<td>
|
||||
<span class="badge bg-danger">{{ item.days_overdue }} Tage</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if item.mahnstufe == 2 %}
|
||||
<span class="badge bg-dark text-danger">Stufe 2 (Gesperrt)</span>
|
||||
{% elif item.mahnstufe == 1 %}
|
||||
<span class="badge bg-warning text-dark">Stufe 1</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Stufe 0</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary"
|
||||
onclick="openEmailModal('{{ item.id }}', '{{ item.schueler_name }}', '{{ item.email }}')">
|
||||
<i class="bi bi-envelope"></i> E-Mail
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-warning text-dark"
|
||||
onclick="resetMahnung('{{ item.id }}', '{{ item.schueler_name }}')">
|
||||
<i class="bi bi-arrow-counterclockwise"></i> Zurücksetzen
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-check-circle-fill text-success fs-1"></i>
|
||||
<p class="text-muted mt-2">Aktuell gibt es keine überfälligen Mahnungen.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mahnungen-card">
|
||||
{% if overdue_list %}
|
||||
<div class="search-box-wrapper">
|
||||
<input type="text" id="searchInput" class="search-box" placeholder="🔍 Suche nach Nutzer, Klasse oder Gegenstand..." onkeyup="filterTable()">
|
||||
</div>
|
||||
|
||||
<table class="mahnungen-table" id="mahnungTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nutzer</th>
|
||||
<th>Klasse</th>
|
||||
<th>Gegenstand</th>
|
||||
<th>Fällig am</th>
|
||||
<th>Tage drüber</th>
|
||||
<th>Mahnstufe</th>
|
||||
<th>Konto-Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in overdue_list %}
|
||||
<tr>
|
||||
<td><strong>{{ item.user or '—' }}</strong></td>
|
||||
<td>{{ item.klasse or '—' }}</td>
|
||||
<td>{{ item.item_name or '—' }}</td>
|
||||
<td><span class="mono">{{ item.due_date or '—' }}</span></td>
|
||||
<td class="text-danger">{{ item.days_overdue }} Tage</td>
|
||||
<td>
|
||||
{% if item.mahnstufe == 2 %}
|
||||
<span class="badge-pill badge-open">Stufe 2 (Gesperrt)</span>
|
||||
{% elif item.mahnstufe == 1 %}
|
||||
<span class="badge-pill badge-warning">Stufe 1 (Gewarnt)</span>
|
||||
{% else %}
|
||||
<span class="badge-pill badge-info">Stufe 0 (Neu)</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if item.is_blocked %}
|
||||
<span class="badge-pill badge-open">Gesperrt</span>
|
||||
{% else %}
|
||||
<span class="badge-pill badge-paid">Aktiv</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<strong>Hervorragend!</strong> Aktuell gibt es keine überfälligen Ausleihen im System.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<!-- E-Mail Modal -->
|
||||
<div class="modal fade" id="emailModal" tabindex="-1" aria-labelledby="emailModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="emailModalLabel">Mahnungs-E-Mail senden</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Schließen"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="emailForm">
|
||||
<input type="hidden" id="modalLoanId">
|
||||
<div class="mb-3">
|
||||
<label for="modalStudentName" class="form-label">Empfänger</label>
|
||||
<input type="text" class="form-control" id="modalStudentName" readonly>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="modalEmail" class="form-label">E-Mail-Adresse</label>
|
||||
<input type="email" class="form-control" id="modalEmail" placeholder="E-Mail-Adresse eingeben...">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="modalMessage" class="form-label">Nachricht</label>
|
||||
<textarea class="form-control" id="modalMessage" rows="4" placeholder="Optionaler Text..."></textarea>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||
<button type="button" class="btn btn-primary" onclick="submitEmailMahnung()">E-Mail senden</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function filterTable() {
|
||||
const input = document.getElementById("searchInput");
|
||||
const filter = input.value.toLowerCase();
|
||||
const table = document.getElementById("mahnungTable");
|
||||
const tr = table.getElementsByTagName("tbody")[0].getElementsByTagName("tr");
|
||||
function openEmailModal(loanId, studentName, studentEmail) {
|
||||
document.getElementById('modalLoanId').value = loanId;
|
||||
document.getElementById('modalStudentName').value = studentName;
|
||||
document.getElementById('modalEmail').value = studentEmail || '';
|
||||
document.getElementById('modalMessage').value = '';
|
||||
|
||||
for (let i = 0; i < tr.length; i++) {
|
||||
const tdUser = tr[i].getElementsByTagName("td")[0];
|
||||
const tdClass = tr[i].getElementsByTagName("td")[1];
|
||||
const tdItem = tr[i].getElementsByTagName("td")[2];
|
||||
var myModal = new bootstrap.Modal(document.getElementById('emailModal'));
|
||||
myModal.show();
|
||||
}
|
||||
|
||||
if (tdUser || tdClass || tdItem) {
|
||||
const textUser = tdUser.textContent || tdUser.innerText;
|
||||
const textClass = tdClass.textContent || tdClass.innerText;
|
||||
const textItem = tdItem.textContent || tdItem.innerText;
|
||||
function submitEmailMahnung() {
|
||||
const loanId = document.getElementById('modalLoanId').value;
|
||||
const email = document.getElementById('modalEmail').value;
|
||||
const message = document.getElementById('modalMessage').value;
|
||||
|
||||
if (
|
||||
textUser.toLowerCase().indexOf(filter) > -1 ||
|
||||
textClass.toLowerCase().indexOf(filter) > -1 ||
|
||||
textItem.toLowerCase().indexOf(filter) > -1
|
||||
) {
|
||||
tr[i].style.display = "";
|
||||
} else {
|
||||
tr[i].style.display = "none";
|
||||
}
|
||||
}
|
||||
if (!email) {
|
||||
alert('Bitte geben Sie eine gültige E-Mail-Adresse ein.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
fetch('/mahnungen_send_email', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ loan_id: loanId, email: email, message: message })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert('E-Mail erfolgreich gesendet!');
|
||||
var modalEl = document.getElementById('emailModal');
|
||||
var modal = bootstrap.Modal.getInstance(modalEl);
|
||||
modal.hide();
|
||||
} else {
|
||||
alert('Fehler: ' + data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert('Fehler beim Senden der Anfrage.');
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
function resetMahnung(loanId, studentName) {
|
||||
if (!confirm('Möchten Sie die Mahnung für "' + studentName + '" wirklich zurücksetzen?\n\nDadurch wird die Mahnstufe auf 0 gesetzt, die Frist um 14 Tage verlängert und der Schülerausweis entsperrt.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('/mahnungen_reset', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ loan_id: loanId })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert(data.message);
|
||||
location.reload(); // Seite neu laden, um die aktualisierte Ansicht zu sehen
|
||||
} else {
|
||||
alert('Fehler: ' + data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert('Fehler beim Senden der Anfrage.');
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user