Compare commits

...

5 Commits

Author SHA1 Message Date
Aiirondev_dev 2cd95ef808 Removal again
Release Inventarsystem / release-docker (push) Successful in 3m6s
2026-08-22 15:14:26 +02:00
Aiirondev_dev 17b58ff7ca Removal again
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-22 00:08:38 +02:00
Aiirondev_dev c67f64a45a Try again
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-22 00:02:47 +02:00
Aiirondev_dev 7727bfd94a Revert of changes
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-21 23:44:26 +02:00
Aiirondev_dev 55f34e0462 changes to the info importing for the library item
Release Inventarsystem / release-docker (push) Successful in 2m27s
2026-08-21 23:28:52 +02:00
4 changed files with 527 additions and 17 deletions
+255 -2
View File
@@ -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():
"""
+1
View File
@@ -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) %}
+256
View File
@@ -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 %}
+15 -15
View File
@@ -1531,22 +1531,22 @@
}
const isbnField = document.getElementById('isbn');
const infoContainer = document.getElementById('book-info-container');
if (!isbnField || !infoContainer) {
alert('Fehler: Erforderliche Formularelemente nicht gefunden.');
return;
}
const isbn = normalizeIsbnClient(isbnField.value);
if (!isbn) {
infoContainer.innerHTML = '<div class="error-message">Bitte geben Sie eine ISBN oder einen Barcode ein.</div>';
return;
}
// Show loading indicator
infoContainer.innerHTML = '<div class="loading-spinner">Informationen werden abgerufen...</div>';
// Make API request to fetch book data
fetch(`/fetch_book_info/${isbn}`)
.then(response => {
@@ -1564,10 +1564,10 @@
infoContainer.innerHTML = `<div class="error-message">${data.error}</div>`;
return;
}
// Store book data globally for import functionality
currentBookData = data;
// Display book information with import button
infoContainer.innerHTML = `
<div class="book-info">
@@ -1602,23 +1602,23 @@
alert('Keine Buchdaten verfügbar zum Import.');
return;
}
// Get form fields
const nameField = document.getElementById('name');
const descriptionField = document.getElementById('beschreibung');
const priceField = document.getElementById('anschaffungskosten'); // <-- NEU
if (!nameField || !descriptionField) {
alert('Fehler: Formularfelder nicht gefunden.');
return;
}
// Generate book title
let bookTitle = currentBookData.title || '';
if (currentBookData.authors && currentBookData.authors !== 'Unknown Author') {
bookTitle += ` - ${currentBookData.authors}`;
}
// Generate description
let description = '';
if (currentBookData.description && currentBookData.description !== 'No description available') {
@@ -1639,11 +1639,11 @@
description += `\nSeiten: ${currentBookData.pageCount}`;
}
}
// Import the data into form fields
nameField.value = bookTitle;
descriptionField.value = description;
// Preis in das Formularfeld eintragen
if (priceField && currentBookData.price !== null && currentBookData.price !== undefined) {
// Wandelt den Float (z.B. 12.25) in einen String mit Komma (12,25) um
@@ -1655,14 +1655,14 @@
if (currentBookData.thumbnail) {
downloadBookCover(currentBookData.thumbnail);
}
// Show success message
const infoContainer = document.getElementById('book-info-container');
const successMessage = document.createElement('div');
successMessage.className = 'import-success-message';
successMessage.textContent = 'Buchdaten erfolgreich in das Formular übernommen!';
infoContainer.appendChild(successMessage);
// Remove success message after 3 seconds
setTimeout(() => {
if (successMessage.parentNode) {