This commit is contained in:
+255
-2
@@ -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},<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.")
|
||||
|
||||
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},<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.")
|
||||
|
||||
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():
|
||||
"""
|
||||
|
||||
@@ -1373,6 +1373,7 @@
|
||||
<li><h6 class="dropdown-header">Bibliotheks-Verwaltung</h6></li>
|
||||
{% if current_permissions.pages.get('library_loans_admin', False) %}
|
||||
<li><a class="dropdown-item" href="{{ url_for('library_loans_admin') }}">Alle Ausleihen/Alle Defekten Items</a></li>
|
||||
<li><a class="dropdown-item" href="{{ url_for('mahnungen_admin') }}">Mahnungen</a></li>
|
||||
{% endif %}
|
||||
{% if student_cards_module_enabled %}
|
||||
{% if current_permissions.actions.get('can_manage_users', False) %}
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Mahnungen & Überfällige Ausleihen{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<!-- ==========================================
|
||||
CSS STYLES
|
||||
========================================== -->
|
||||
<style>
|
||||
:root {
|
||||
--bg-color: #f4f7f6;
|
||||
--text-color: #333;
|
||||
--primary: #4a90e2;
|
||||
--danger: #dc3545;
|
||||
--warning: #ffc107;
|
||||
--info: #0dcaf0;
|
||||
--success: #198754;
|
||||
--border-color: #dee2e6;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header-bar h2 {
|
||||
margin: 0;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
/* Suchfeld Styling */
|
||||
.search-box {
|
||||
padding: 10px;
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Card & Table Styling */
|
||||
.card {
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
|
||||
padding: 20px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.table th, .table td {
|
||||
padding: 12px 15px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.table th {
|
||||
background-color: #f8f9fa;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.table tbody tr:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
/* Badges */
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 5px 10px;
|
||||
font-size: 0.85em;
|
||||
font-weight: bold;
|
||||
border-radius: 20px;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bg-danger { background-color: var(--danger); }
|
||||
.bg-warning { background-color: var(--warning); color: #000; }
|
||||
.bg-info { background-color: var(--info); color: #000; }
|
||||
.bg-success { background-color: var(--success); }
|
||||
|
||||
.text-danger {
|
||||
color: var(--danger);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
padding: 10px 15px;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
background-color: #6c757d;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.btn:hover { background-color: #5a6268; }
|
||||
|
||||
/* Alert */
|
||||
.alert-success {
|
||||
background-color: #d1e7dd;
|
||||
color: #0f5132;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
border: 1px solid #badbcc;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ==========================================
|
||||
HTML CONTENT (JINJA2)
|
||||
========================================== -->
|
||||
<div class="container">
|
||||
|
||||
<div class="header-bar">
|
||||
<h2>🚨 Mahnungsübersicht</h2>
|
||||
<a href="{{ url_for('library_loans_admin') }}" class="btn">Zurück zur Bibliothek</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
{% if overdue_list %}
|
||||
|
||||
<!-- Suchfeld für das JavaScript -->
|
||||
<input type="text" id="searchInput" class="search-box" placeholder="Suche nach Nutzer, Klasse oder Gegenstand..." onkeyup="filterTable()">
|
||||
|
||||
<table class="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 }}</strong></td>
|
||||
<td>{{ item.klasse }}</td>
|
||||
<td>{{ item.item_name }}</td>
|
||||
<td>{{ item.due_date }}</td>
|
||||
<td class="text-danger">{{ item.days_overdue }} Tage</td>
|
||||
|
||||
<td>
|
||||
{% if item.mahnstufe == 2 %}
|
||||
<span class="badge bg-danger">Stufe 2 (Gesperrt)</span>
|
||||
{% elif item.mahnstufe == 1 %}
|
||||
<span class="badge bg-warning">Stufe 1 (Gewarnt)</span>
|
||||
{% else %}
|
||||
<span class="badge bg-info">Stufe 0 (Neu)</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{% if item.is_blocked %}
|
||||
<span class="badge bg-danger">Gesperrt</span>
|
||||
{% else %}
|
||||
<span class="badge bg-success">Aktiv</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="alert-success">
|
||||
<strong>Hervorragend!</strong> Aktuell gibt es keine überfälligen Ausleihen im System.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ==========================================
|
||||
JAVASCRIPT
|
||||
========================================== -->
|
||||
<script>
|
||||
/**
|
||||
* Funktion: filterTable()
|
||||
* Liest die Eingabe aus dem Suchfeld und filtert die Tabellenzeilen live.
|
||||
* Durchsucht Nutzer, Klassen und Gegenstände.
|
||||
*/
|
||||
function filterTable() {
|
||||
// Hole den Wert aus dem Eingabefeld und wandle ihn in Kleinbuchstaben um
|
||||
let input = document.getElementById("searchInput");
|
||||
let filter = input.value.toLowerCase();
|
||||
|
||||
// Hole die Tabelle und alle Zeilen (tr) aus dem tbody
|
||||
let table = document.getElementById("mahnungTable");
|
||||
let tr = table.getElementsByTagName("tbody")[0].getElementsByTagName("tr");
|
||||
|
||||
// Durchlaufe alle Zeilen
|
||||
for (let i = 0; i < tr.length; i++) {
|
||||
// Hole die Zellen für Nutzer (0), Klasse (1) und Gegenstand (2)
|
||||
let tdUser = tr[i].getElementsByTagName("td")[0];
|
||||
let tdClass = tr[i].getElementsByTagName("td")[1];
|
||||
let tdItem = tr[i].getElementsByTagName("td")[2];
|
||||
|
||||
if (tdUser || tdClass || tdItem) {
|
||||
// Extrahiere den Text
|
||||
let textUser = tdUser.textContent || tdUser.innerText;
|
||||
let textClass = tdClass.textContent || tdClass.innerText;
|
||||
let textItem = tdItem.textContent || tdItem.innerText;
|
||||
|
||||
// Prüfe, ob der Suchbegriff in einer der drei Spalten vorkommt
|
||||
if (
|
||||
textUser.toLowerCase().indexOf(filter) > -1 ||
|
||||
textClass.toLowerCase().indexOf(filter) > -1 ||
|
||||
textItem.toLowerCase().indexOf(filter) > -1
|
||||
) {
|
||||
tr[i].style.display = ""; // Zeile anzeigen
|
||||
} else {
|
||||
tr[i].style.display = "none"; // Zeile ausblenden
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user