Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0f0f0ebefd | |||
| 65fadf7f5f | |||
| 6f9994c4ce | |||
| 6496716572 | |||
| f750739b51 | |||
| 4c3545f4ba | |||
| c0bf0c0b8e | |||
| 2cd95ef808 | |||
| 17b58ff7ca |
+565
-44
@@ -49,6 +49,7 @@ import Web.modules.log.audit_log as al
|
||||
import push_notifications as pn
|
||||
import Web.modules.inventarsystem.pdf_export as pdf_export
|
||||
import Web.modules.inventarsystem.excel_export as excel_export
|
||||
from Web.modules.emailservice.email import send
|
||||
import datetime
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from bson.objectid import ObjectId, InvalidId
|
||||
@@ -94,6 +95,7 @@ import Web.modules.database.settings as cfg
|
||||
from Web.modules.database.settings import MongoClient
|
||||
from tenant import get_tenant_context, get_tenant_db, get_tenant_trial_status, purge_expired_trial_tenants
|
||||
from pymongo.errors import DuplicateKeyError
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
|
||||
app = Flask(__name__, static_folder='static') # Correctly set static folder
|
||||
@@ -198,7 +200,7 @@ def rollover_student_card_classes(dry_run=False, *, max_class=None, graduate_lab
|
||||
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
|
||||
db = client[MONGODB_DB]
|
||||
col = db['student_cards']
|
||||
now = datetime.datetime.now()
|
||||
now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
|
||||
cursor = list(col.find({}, {'Klasse': 1}))
|
||||
for doc in cursor:
|
||||
@@ -268,7 +270,7 @@ def api_library_return_by_code():
|
||||
return jsonify({'ok': False, 'message': 'Kein Bibliotheksmedium für diesen Code gefunden.'}), 404
|
||||
|
||||
item_id = str(item_doc['_id'])
|
||||
now = datetime.datetime.now()
|
||||
now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
|
||||
# If item already available -> nothing to return
|
||||
if item_doc.get('Verfuegbar', True):
|
||||
@@ -1024,7 +1026,7 @@ def _prepare_invoice_pdf_payload(invoice_data, borrow_doc=None, item_doc=None):
|
||||
item_id = str(item_doc.get('_id'))
|
||||
|
||||
return {
|
||||
'invoice_number': invoice_data.get('invoice_number') or _build_invoice_number(borrow_doc.get('_id', ''), datetime.datetime.now()),
|
||||
'invoice_number': invoice_data.get('invoice_number') or _build_invoice_number(borrow_doc.get('_id', ''), datetime.datetime.now(ZoneInfo("Europe/Berlin"))),
|
||||
'created_at': created_at_raw,
|
||||
'created_at_display': created_at_display,
|
||||
'borrower': invoice_data.get('borrower') or borrow_doc.get('User', '-'),
|
||||
@@ -1047,7 +1049,7 @@ def _create_notification(db, *, audience, notif_type, title, message, target_use
|
||||
if existing:
|
||||
return False
|
||||
|
||||
now = datetime.datetime.now()
|
||||
now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
payload = {
|
||||
'Audience': audience,
|
||||
'Type': notif_type,
|
||||
@@ -1277,7 +1279,7 @@ def _build_reminder_message(item_name, start_dt=None, end_dt=None):
|
||||
|
||||
def create_return_reminders():
|
||||
"""Create one-time reminders for day-1 and planned-end events."""
|
||||
now = datetime.datetime.now()
|
||||
now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
one_day_ago = now - datetime.timedelta(days=1)
|
||||
|
||||
client = None
|
||||
@@ -1455,7 +1457,7 @@ def update_appointment_statuses():
|
||||
- Geplante Termine, die aktiviert werden sollten
|
||||
- Aktive Termine, die beendet werden sollten
|
||||
"""
|
||||
current_time = datetime.datetime.now(datetime.timezone.utc)
|
||||
current_time = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
|
||||
try:
|
||||
# Hole alle Termine mit Status 'planned' oder 'active'
|
||||
@@ -1588,8 +1590,105 @@ 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
|
||||
current_time = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
|
||||
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}")
|
||||
|
||||
client.close()
|
||||
|
||||
@@ -1621,7 +1720,7 @@ def _initialize_scheduler():
|
||||
|
||||
# 1. Generate a unique ID for this specific worker process
|
||||
_scheduler_worker_id = str(uuid.uuid4())
|
||||
now = datetime.datetime.now(datetime.timezone.utc)
|
||||
now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
|
||||
# 2. Ensure the lock document exists (initialize if missing)
|
||||
try:
|
||||
@@ -1665,7 +1764,7 @@ def _initialize_scheduler():
|
||||
hb_db = hb_client[MONGODB_DB]
|
||||
hb_db['system_locks'].update_one(
|
||||
{'_id': 'scheduler_lock', 'worker_id': _scheduler_worker_id},
|
||||
{'$set': {'locked_at': datetime.datetime.now(datetime.timezone.utc)}}
|
||||
{'$set': {'locked_at': datetime.datetime.now(ZoneInfo("Europe/Berlin"))}}
|
||||
)
|
||||
hb_client.close()
|
||||
except Exception as e:
|
||||
@@ -1713,6 +1812,7 @@ def _shutdown_scheduler():
|
||||
|
||||
atexit.register(_shutdown_scheduler)
|
||||
|
||||
|
||||
"""-------------------------------------------------------------File Upload Validation----------------------------------------------------------------------------- """
|
||||
|
||||
def allowed_file(filename, file_content=None, max_size_mb=cfg.MAX_UPLOAD_MB):
|
||||
@@ -2396,7 +2496,7 @@ def _upload_student_cards_excel():
|
||||
student_cards_col.insert_one({
|
||||
'AusweisId': row['ausweis_id'],
|
||||
'StandardAusleihdauer': int(row['default_borrow_days']),
|
||||
'Erstellt': datetime.datetime.now(),
|
||||
'Erstellt': datetime.datetime.now(ZoneInfo("Europe/Berlin")),
|
||||
**encrypted_payload,
|
||||
})
|
||||
created_total += 1
|
||||
@@ -3529,6 +3629,428 @@ def library_loans_admin():
|
||||
if client:
|
||||
client.close()
|
||||
|
||||
|
||||
def generate_test_ausleihen():
|
||||
"""
|
||||
Generiert Testdaten für die Überprüfung der Mahnungs- und Sperrlogik.
|
||||
Erstellt Dummy-Nutzer, Dummy-Items und Ausleihungen mit manipulierten Daten (DueDate).
|
||||
"""
|
||||
current_time = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
|
||||
try:
|
||||
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
|
||||
db = client[MONGODB_DB]
|
||||
ausleihungen = db['ausleihungen']
|
||||
users_col = db['users']
|
||||
items_col = db['items']
|
||||
|
||||
print("Generiere Testdaten...")
|
||||
|
||||
# 1. Test-Nutzer erstellen oder aktualisieren (upsert)
|
||||
test_users = [
|
||||
{"username": "test_user_stufe1", "email": "stufe1@test.local", "is_blocked": False},
|
||||
{"username": "test_user_stufe2", "email": "stufe2@test.local", "is_blocked": False},
|
||||
{"username": "test_user_frisch", "email": "frisch@test.local", "is_blocked": False}
|
||||
]
|
||||
|
||||
for u in test_users:
|
||||
users_col.update_one(
|
||||
{'username': u['username']},
|
||||
{'$set': u},
|
||||
upsert=True
|
||||
)
|
||||
|
||||
# 2. Test-Items (Gegenstände) erstellen
|
||||
item1_res = items_col.insert_one({"Name": "Biologie Buch (Test Stufe 1)"})
|
||||
item2_res = items_col.insert_one({"Name": "iPad Pro (Test Stufe 2)"})
|
||||
item3_res = items_col.insert_one({"Name": "Taschenrechner (Test Frisch)"})
|
||||
|
||||
# 3. Test-Ausleihungen generieren
|
||||
# Fall 1: 20 Tage drüber, Mahnstufe 0 -> Sollte Stufe 1 triggern
|
||||
due_date_stufe1 = current_time - datetime.timedelta(days=20)
|
||||
|
||||
# Fall 2: 30 Tage drüber, Mahnstufe 1 -> Sollte Stufe 2 triggern & Nutzer sperren
|
||||
due_date_stufe2 = current_time - datetime.timedelta(days=30)
|
||||
|
||||
# Fall 3: 5 Tage drüber, Mahnstufe 0 -> Sollte NICHTS tun (da < 14 Tage)
|
||||
due_date_frisch = current_time - datetime.timedelta(days=5)
|
||||
|
||||
test_ausleihungen = [
|
||||
{
|
||||
"User": encrypt_text("test_user_stufe1"),
|
||||
"Item": str(item1_res.inserted_id),
|
||||
"Status": "active",
|
||||
"DueDate": due_date_stufe1,
|
||||
"Mahnstufe": 0,
|
||||
"LastUpdated": current_time
|
||||
},
|
||||
{
|
||||
"User": "test_user_stufe2",
|
||||
"Item": str(item2_res.inserted_id),
|
||||
"Status": "active",
|
||||
"DueDate": due_date_stufe2,
|
||||
"Mahnstufe": 1, # Hat bereits Stufe 1
|
||||
"LastUpdated": due_date_stufe2 # Wurde in der Vergangenheit gemahnt
|
||||
},
|
||||
{
|
||||
"User": "test_user_frisch",
|
||||
"Item": str(item3_res.inserted_id),
|
||||
"Status": "active",
|
||||
"DueDate": due_date_frisch,
|
||||
"Mahnstufe": 0,
|
||||
"LastUpdated": current_time
|
||||
}
|
||||
]
|
||||
|
||||
# In die Datenbank einfügen
|
||||
ausleihungen.insert_many(test_ausleihungen)
|
||||
|
||||
print("Erfolgreich! Folgende Testdaten wurden in die DB geschrieben:")
|
||||
print(
|
||||
f"- Fall 1 (Mahnstufe 1 Trigger): Nutzer 'test_user_stufe1', fällig war am {due_date_stufe1.strftime('%Y-%m-%d')}")
|
||||
print(
|
||||
f"- Fall 2 (Mahnstufe 2 & Sperre Trigger): Nutzer 'test_user_stufe2', fällig war am {due_date_stufe2.strftime('%Y-%m-%d')}")
|
||||
print(
|
||||
f"- Fall 3 (Ignorieren - zu früh): Nutzer 'test_user_frisch', fällig war am {due_date_frisch.strftime('%Y-%m-%d')}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Fehler beim Erstellen der Testdaten: {e}")
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
@app.route('/test_mahnungen')
|
||||
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"))
|
||||
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()
|
||||
|
||||
""" 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)."""
|
||||
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"))
|
||||
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():
|
||||
"""
|
||||
@@ -3828,7 +4350,7 @@ def api_library_scan_action():
|
||||
|
||||
item_id = str(item_doc['_id'])
|
||||
borrower_name = card_doc.get('SchülerName') or f"Ausweis {student_card_id}"
|
||||
now = datetime.datetime.now()
|
||||
now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
|
||||
if item_doc.get('Verfuegbar', True):
|
||||
borrow_duration_days = None
|
||||
@@ -3851,9 +4373,8 @@ def api_library_scan_action():
|
||||
card_default = cfg.STUDENT_DEFAULT_BORROW_DAYS
|
||||
borrow_duration_days = max(1, min(card_default, cfg.STUDENT_MAX_BORROW_DAYS))
|
||||
|
||||
end_date = now + datetime.timedelta(days=borrow_duration_days) if borrow_duration_days else None
|
||||
it.update_item_status(item_id, False, borrower_name)
|
||||
au.add_ausleihung(item_id, borrower_name, now, end_date=end_date)
|
||||
au.add_ausleihung(item_id, borrower_name, now)
|
||||
|
||||
_append_audit_event_standalone(
|
||||
event_type='ausleihung_borrowed',
|
||||
@@ -4115,7 +4636,7 @@ def api_library_item_update(item_id):
|
||||
'Autor': author,
|
||||
'Code_4': code_4,
|
||||
'ItemType': media_type,
|
||||
'LastUpdated': datetime.datetime.now()
|
||||
'LastUpdated': datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
}
|
||||
|
||||
if normalized_isbn:
|
||||
@@ -4309,7 +4830,7 @@ def student_cards_admin():
|
||||
{'$set': {
|
||||
'AusweisId': ausweis_id,
|
||||
'StandardAusleihdauer': int(default_borrow_days),
|
||||
'Aktualisiert': datetime.datetime.now(),
|
||||
'Aktualisiert': datetime.datetime.now(ZoneInfo("Europe/Berlin")),
|
||||
**encrypted_payload
|
||||
}}
|
||||
)
|
||||
@@ -4348,7 +4869,7 @@ def student_cards_admin():
|
||||
student_cards.insert_one({
|
||||
'AusweisId': ausweis_id,
|
||||
'StandardAusleihdauer': int(default_borrow_days),
|
||||
'Erstellt': datetime.datetime.now(),
|
||||
'Erstellt': datetime.datetime.now(ZoneInfo("Europe/Berlin")),
|
||||
**encrypted_payload,
|
||||
})
|
||||
flash('Neuer Ausweis wurde hinzugefügt.', 'success')
|
||||
@@ -4405,7 +4926,7 @@ def student_cards_print():
|
||||
return render_template(
|
||||
'student_cards_print.html',
|
||||
student_cards=all_cards,
|
||||
current_datetime=datetime.datetime.now()
|
||||
current_datetime=datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
)
|
||||
|
||||
|
||||
@@ -4439,7 +4960,7 @@ def student_card_barcode_print():
|
||||
return render_template(
|
||||
'student_card_barcode_print.html',
|
||||
student_cards=all_cards,
|
||||
current_datetime=datetime.datetime.now(),
|
||||
current_datetime=datetime.datetime.now(ZoneInfo("Europe/Berlin")),
|
||||
download_link=url_for('student_card_barcode_download')
|
||||
)
|
||||
|
||||
@@ -4635,7 +5156,7 @@ def student_card_barcode_download():
|
||||
pdf_buffer,
|
||||
mimetype='application/pdf',
|
||||
as_attachment=True,
|
||||
download_name=f'schuelerausweise_all_{datetime.datetime.now().strftime("%Y%m%d_%H%M%S")}.pdf'
|
||||
download_name=f'schuelerausweise_all_{datetime.datetime.now(ZoneInfo("Europe/Berlin")).strftime("%Y%m%d_%H%M%S")}.pdf'
|
||||
)
|
||||
except Exception as e:
|
||||
app.logger.error(f"Error occurred while generating PDF for card {card['AusweisId']}: {e}")
|
||||
@@ -4821,7 +5342,7 @@ def student_card_class_barcode_download():
|
||||
c.save()
|
||||
pdf_buffer.seek(0)
|
||||
|
||||
filename = f'ausweise_klasse_{class_name.replace(" ", "_")}_{datetime.datetime.now().strftime("%Y%m%d")}.pdf'
|
||||
filename = f'ausweise_klasse_{class_name.replace(" ", "_")}_{datetime.datetime.now(ZoneInfo("Europe/Berlin")).strftime("%Y%m%d")}.pdf'
|
||||
|
||||
return send_file(
|
||||
pdf_buffer,
|
||||
@@ -6104,7 +6625,7 @@ def upload_item():
|
||||
|
||||
def _soft_delete_item_groups(db, root_item_ids, username):
|
||||
"""Soft-delete one or more item groups and their borrow records."""
|
||||
now = datetime.datetime.now()
|
||||
now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
unique_group_item_ids = []
|
||||
seen_ids = set()
|
||||
|
||||
@@ -6625,7 +7146,7 @@ def update_group():
|
||||
|
||||
# 1. Shared Fields (Group Logic)
|
||||
# These apply to every item in the group
|
||||
shared_update = {'LastUpdated': datetime.datetime.now()}
|
||||
shared_update = {'LastUpdated': datetime.datetime.now(ZoneInfo("Europe/Berlin"))}
|
||||
for source_key, target_key in (
|
||||
('name', 'Name'),
|
||||
('ort', 'Ort'),
|
||||
@@ -6700,7 +7221,7 @@ def report_damage(id):
|
||||
if not item_doc:
|
||||
return jsonify({'success': False, 'message': 'Objekt nicht gefunden.'}), 404
|
||||
|
||||
now = datetime.datetime.now()
|
||||
now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
damage_entry = {
|
||||
'description': description,
|
||||
'reported_by': session['username'],
|
||||
@@ -6804,7 +7325,7 @@ def mark_damage_repaired(id):
|
||||
return jsonify({'success': False, 'message': 'Keine offenen Schäden vorhanden.'}), 400
|
||||
|
||||
active_borrow = ausleihungen_col.find_one({'Item': str(id), 'Status': 'active'}, {'_id': 1})
|
||||
now = datetime.datetime.now()
|
||||
now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
repair_entry = {
|
||||
'repaired_by': session['username'],
|
||||
'repaired_at': now,
|
||||
@@ -6981,7 +7502,7 @@ def ausleihen(id):
|
||||
card_default = cfg.STUDENT_DEFAULT_BORROW_DAYS
|
||||
borrow_duration_days = max(1, min(card_default, cfg.STUDENT_MAX_BORROW_DAYS))
|
||||
|
||||
start_date = datetime.datetime.now()
|
||||
start_date = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
end_date = None
|
||||
if borrow_duration_days:
|
||||
end_date = start_date + datetime.timedelta(days=borrow_duration_days)
|
||||
@@ -7070,7 +7591,7 @@ def ausleihen(id):
|
||||
|
||||
# Before borrowing, block if there's a conflicting planned booking
|
||||
try:
|
||||
now = datetime.datetime.now()
|
||||
now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
# Fetch planned bookings for this item from DB
|
||||
planned = au.get_planned_ausleihungen()
|
||||
# Count relevant upcoming planned bookings for today or ongoing
|
||||
@@ -7131,12 +7652,12 @@ def ausleihen(id):
|
||||
return redirect(url_for(redirect_target))
|
||||
|
||||
# If we reach here, we can borrow the requested number of exemplars
|
||||
current_date = datetime.datetime.now().strftime('%d.%m.%Y %H:%M')
|
||||
current_date = datetime.datetime.now(ZoneInfo("Europe/Berlin")).strftime('%d.%m.%Y %H:%M')
|
||||
|
||||
# If the item doesn't use exemplars (single item)
|
||||
if total_exemplare <= 1:
|
||||
it.update_item_status(id, False, effective_borrower)
|
||||
start_date = datetime.datetime.now()
|
||||
start_date = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
au.add_ausleihung(id, effective_borrower, start_date, end_date=end_date)
|
||||
_append_audit_event_standalone(
|
||||
event_type='ausleihung_returned',
|
||||
@@ -7179,7 +7700,7 @@ def ausleihen(id):
|
||||
it.update_item_status(id, False, username)
|
||||
|
||||
# Create ausleihung records for each borrowed exemplar
|
||||
start_date = datetime.datetime.now()
|
||||
start_date = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
for exemplar in new_borrowed_exemplars:
|
||||
exemplar_id = f"{id}_{exemplar['number']}"
|
||||
au.add_ausleihung(exemplar_id, effective_borrower, start_date, end_date=end_date, exemplar_data={
|
||||
@@ -7241,7 +7762,7 @@ def zurueckgeben(id):
|
||||
'Status': 'active'
|
||||
})
|
||||
|
||||
end_date = datetime.datetime.now()
|
||||
end_date = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
original_user = item.get('User', username)
|
||||
|
||||
updated_count = 0
|
||||
@@ -7253,7 +7774,7 @@ def zurueckgeben(id):
|
||||
{'$set': {
|
||||
'Status': 'completed',
|
||||
'End': end_date,
|
||||
'LastUpdated': datetime.datetime.now()
|
||||
'LastUpdated': datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
}}
|
||||
)
|
||||
|
||||
@@ -7480,7 +8001,7 @@ def check_availability():
|
||||
# Also include current availability if checking today and item is borrowed now
|
||||
item_doc = items_col.find_one({'_id': ObjectId(item_id)})
|
||||
if item_doc and not item_doc.get('Verfuegbar', True):
|
||||
now = datetime.datetime.now()
|
||||
now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
if req_start.date() == now.date():
|
||||
conflicts.append({'status': 'active', 'user': item_doc.get('User'), 'start': None, 'end': None, 'period': None, 'id': None})
|
||||
|
||||
@@ -8173,7 +8694,7 @@ def delete_user():
|
||||
items_col = db['items']
|
||||
users_col = db['users'] # Direkter Zugriff auf die User-Collection
|
||||
|
||||
now = datetime.datetime.now()
|
||||
now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
|
||||
# 1. Aktive Ausleihen abschließen
|
||||
ausleihungen.update_many(
|
||||
@@ -8615,7 +9136,7 @@ def admin_reset_borrowing(borrow_id):
|
||||
item_id = rec.get('Item')
|
||||
user = rec.get('User')
|
||||
|
||||
now = datetime.datetime.now()
|
||||
now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
if status == 'active':
|
||||
ausleihungen.update_one({'_id': rec['_id']}, {'$set': {'Status': 'completed', 'End': now, 'LastUpdated': now}})
|
||||
# Free the item
|
||||
@@ -8713,7 +9234,7 @@ def admin_create_invoice(borrow_id):
|
||||
|
||||
mark_destroyed = request.form.get('mark_destroyed') == 'on'
|
||||
close_borrowing = request.form.get('close_borrowing') == 'on'
|
||||
now = datetime.datetime.now()
|
||||
now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
|
||||
existing_invoice = borrow_doc.get('InvoiceData') or {}
|
||||
if existing_invoice.get('invoice_number'):
|
||||
@@ -8882,7 +9403,7 @@ def admin_mark_invoice_paid(borrow_id):
|
||||
flash('Rechnung ist bereits als bezahlt markiert.', 'info')
|
||||
return redirect(url_for('admin_borrowings'))
|
||||
|
||||
now = datetime.datetime.now()
|
||||
now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
result = ausleihungen.update_one(
|
||||
{'_id': borrow_doc['_id']},
|
||||
{
|
||||
@@ -8961,7 +9482,7 @@ def admin_pay_invoice_extended(borrow_id):
|
||||
flash('Für diese Ausleihung existiert keine Rechnung.', 'warning')
|
||||
return redirect(request.referrer or url_for('library_loans_admin'))
|
||||
|
||||
now = datetime.datetime.now()
|
||||
now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
update_fields = {'LastUpdated': now}
|
||||
|
||||
if invoice_data.get('paid') is not True:
|
||||
@@ -9031,7 +9552,7 @@ def resolve_repaired_item(item_id):
|
||||
flash('Element nicht gefunden.', 'error')
|
||||
return redirect(request.referrer or url_for('library_loans_admin'))
|
||||
|
||||
now = datetime.datetime.now()
|
||||
now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
open_reports = item_doc.get('DamageReports', []) or []
|
||||
|
||||
item_update = {
|
||||
@@ -9168,7 +9689,7 @@ def admin_add_invoice_correction(borrow_id):
|
||||
flash('Ungültiger Korrekturbetrag.', 'error')
|
||||
return redirect(url_for('admin_borrowings'))
|
||||
|
||||
now = datetime.datetime.now()
|
||||
now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
correction_number = f"CORR-{now.strftime('%Y%m%d-%H%M%S')}-{str(borrow_doc.get('_id'))[-6:].upper()}"
|
||||
|
||||
correction_entry = {
|
||||
@@ -9258,7 +9779,7 @@ def resolve_repaired_item_funct(item_id, action, new_code_4="", current_user="ad
|
||||
return False, "Item nicht in der Datenbank gefunden."
|
||||
|
||||
series_group_id = item.get('SeriesGroupId')
|
||||
now = datetime.datetime.now()
|
||||
now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
|
||||
legacy_damage_unset = {
|
||||
'has_damage': "",
|
||||
@@ -9769,7 +10290,7 @@ def admin_anonymize_names():
|
||||
|
||||
result = student_cards_col.update_one(
|
||||
{'_id': card_doc['_id']},
|
||||
{'$set': {'Aktualisiert': datetime.datetime.now(), **encrypted_payload}}
|
||||
{'$set': {'Aktualisiert': datetime.datetime.now(ZoneInfo("Europe/Berlin")), **encrypted_payload}}
|
||||
)
|
||||
if result.modified_count > 0:
|
||||
cards_updated += 1
|
||||
@@ -10883,7 +11404,7 @@ def mark_notification_read(notification_id):
|
||||
{'_id': ObjectId(notification_id)},
|
||||
{
|
||||
'$addToSet': {'ReadBy': username},
|
||||
'$set': {'UpdatedAt': datetime.datetime.now()}
|
||||
'$set': {'UpdatedAt': datetime.datetime.now(ZoneInfo("Europe/Berlin"))}
|
||||
}
|
||||
)
|
||||
if result.modified_count > 0:
|
||||
@@ -11523,7 +12044,7 @@ def schedule_appointment():
|
||||
return jsonify({'success': False, 'message': f'Fehler beim Prüfen der Verfügbarkeit'}), 500
|
||||
|
||||
# Check if the appointment should already be active
|
||||
now = datetime.datetime.now()
|
||||
now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
initial_status = 'active' if start_datetime <= now else 'planned'
|
||||
|
||||
# Create the appointment
|
||||
|
||||
@@ -33,6 +33,7 @@ import json
|
||||
import Web.modules.database.settings as cfg
|
||||
from Web.modules.database.settings import MongoClient
|
||||
import Web.modules.inventarsystem.data_protection as dp
|
||||
import Web.modules.database.user as us
|
||||
|
||||
|
||||
def _get_client():
|
||||
@@ -79,7 +80,7 @@ def get_current_status(ausleihung, log_changes=False, user=None):
|
||||
if original_status == 'completed':
|
||||
return 'completed'
|
||||
|
||||
current_time = datetime.datetime.now()
|
||||
current_time = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
start_time = ausleihung.get('Start')
|
||||
end_time = ausleihung.get('End')
|
||||
|
||||
@@ -137,7 +138,7 @@ def create_backup_database():
|
||||
os.makedirs(backup_dir)
|
||||
|
||||
# Aktuelles Datum für den Dateinamen
|
||||
current_date = datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
|
||||
current_date = datetime.datetime.now(ZoneInfo("Europe/Berlin")).strftime('%Y-%m-%d_%H-%M-%S')
|
||||
backup_file = os.path.join(backup_dir, f'ausleihungen_backup_{current_date}.json')
|
||||
|
||||
# Ausleihungen abrufen und als JSON speichern
|
||||
@@ -175,7 +176,7 @@ def create_backup_database():
|
||||
|
||||
log_file = os.path.join(log_dir, 'ausleihungen_error.log')
|
||||
with open(log_file, 'a', encoding='utf-8') as f:
|
||||
f.write(f"{datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}: Backup-Fehler: {str(e)}\n")
|
||||
f.write(f"{datetime.datetime.now(ZoneInfo("Europe/Berlin")).strftime('%Y-%m-%d_%H-%M-%S')}: Backup-Fehler: {str(e)}\n")
|
||||
|
||||
print(f"Fehler beim Erstellen des Backups: {e}")
|
||||
return False
|
||||
@@ -204,7 +205,7 @@ def add_ausleihung(item_id, user, start_date, end_date=None, notes="", status="a
|
||||
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||
db = client[cfg.MONGODB_DB]
|
||||
ausleihungen = db['ausleihungen']
|
||||
|
||||
|
||||
ausleihung = {
|
||||
'Item': item_id,
|
||||
'User': dp.encrypt_text(user),
|
||||
@@ -270,7 +271,7 @@ def update_ausleihung(id, item_id=None, user_id=None, start=None, end=None, note
|
||||
return True
|
||||
|
||||
# UTC Zeitstempel nutzen
|
||||
update_data['LastUpdated'] = datetime.datetime.now(datetime.timezone.utc)
|
||||
update_data['LastUpdated'] = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
|
||||
result = ausleihungen.update_one(
|
||||
{'_id': doc_id},
|
||||
@@ -300,7 +301,7 @@ def complete_ausleihung(id, end_time=None):
|
||||
"""
|
||||
try:
|
||||
if end_time is None:
|
||||
end_time = datetime.datetime.now()
|
||||
end_time = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
|
||||
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||
db = client[cfg.MONGODB_DB]
|
||||
@@ -312,7 +313,7 @@ def complete_ausleihung(id, end_time=None):
|
||||
{'$set': {
|
||||
'End': end_time,
|
||||
'Status': 'completed',
|
||||
'LastUpdated': datetime.datetime.now()
|
||||
'LastUpdated': datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
}}
|
||||
)
|
||||
|
||||
@@ -320,7 +321,7 @@ def complete_ausleihung(id, end_time=None):
|
||||
{'_id': ObjectId(id)},
|
||||
{'$set': {
|
||||
'Verfuegbar': True,
|
||||
'LastUpdated': datetime.datetime.now()
|
||||
'LastUpdated': datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
}}
|
||||
)
|
||||
|
||||
@@ -351,7 +352,7 @@ def cancel_ausleihung(id):
|
||||
{'_id': ObjectId(id)},
|
||||
{'$set': {
|
||||
'Status': 'cancelled',
|
||||
'LastUpdated': datetime.datetime.now()
|
||||
'LastUpdated': datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
}}
|
||||
)
|
||||
|
||||
@@ -376,7 +377,7 @@ def remove_ausleihung(id):
|
||||
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||
db = client[cfg.MONGODB_DB]
|
||||
ausleihungen = db['ausleihungen']
|
||||
now = datetime.datetime.now()
|
||||
now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
result = ausleihungen.update_one(
|
||||
{'_id': ObjectId(id), 'Status': {'$ne': 'deleted'}},
|
||||
{'$set': {
|
||||
@@ -770,7 +771,7 @@ def activate_ausleihung(id):
|
||||
{'_id': doc_id, 'Status': 'planned'},
|
||||
{'$set': {
|
||||
'Status': 'active',
|
||||
'LastUpdated': datetime.datetime.now(datetime.timezone.utc)
|
||||
'LastUpdated': datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
}}
|
||||
)
|
||||
return result.modified_count > 0
|
||||
@@ -794,7 +795,7 @@ def reset_item_completely(item_id):
|
||||
return {'success': False, 'message': 'Item nicht gefunden'}
|
||||
|
||||
item_name = item.get('Name', 'Unbekannt')
|
||||
now_utc = datetime.datetime.now(datetime.timezone.utc)
|
||||
now_utc = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
|
||||
# 1. Bulk update active borrowings
|
||||
update_res = ausleihungen_collection.update_many(
|
||||
@@ -862,7 +863,7 @@ def mark_booking_active(booking_id, ausleihung_id=None):
|
||||
doc_id = ObjectId(booking_id) if isinstance(booking_id, str) else booking_id
|
||||
update_data = {
|
||||
'Status': 'active',
|
||||
'LastUpdated': datetime.datetime.now(datetime.timezone.utc)
|
||||
'LastUpdated': datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
}
|
||||
if ausleihung_id:
|
||||
update_data['AusleihungId'] = ausleihung_id
|
||||
|
||||
@@ -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,225 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Mahnungsübersicht{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<style>
|
||||
.mahnungen-shell {
|
||||
max-width: 1180px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.mahnungen-head {
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f7f9fc 100%);
|
||||
border: 1px solid #dbe4ee;
|
||||
border-radius: 16px;
|
||||
padding: 18px 20px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 10px 26px rgba(15, 23, 42, 0.07);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.mahnungen-head h1 {
|
||||
margin: 0;
|
||||
font-size: 1.55rem;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.head-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-box-wrapper {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
width: 100%;
|
||||
max-width: 360px;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 8px;
|
||||
font-size: 0.95rem;
|
||||
outline: none;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.search-box:focus {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
|
||||
}
|
||||
|
||||
.mahnungen-card {
|
||||
background: var(--ui-surface, #ffffff);
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 16px;
|
||||
padding: 18px;
|
||||
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.mahnungen-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.mahnungen-table th,
|
||||
.mahnungen-table td {
|
||||
padding: 12px 10px;
|
||||
border-bottom: 1px solid #edf2f7;
|
||||
vertical-align: middle;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.mahnungen-table th {
|
||||
font-size: 0.83rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: #64748b;
|
||||
background: var(--ui-surface-soft, #f8fafc);
|
||||
}
|
||||
|
||||
.mahnungen-table tr:hover td {
|
||||
background: #fbfdff;
|
||||
}
|
||||
|
||||
.badge-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.badge-open { background: #fee2e2; color: #991b1b; }
|
||||
.badge-warning { background: #fef3c7; color: #b45309; }
|
||||
.badge-info { background: #e0f2fe; color: #0369a1; }
|
||||
.badge-paid { background: #dcfce7; color: #166534; }
|
||||
|
||||
.mono {
|
||||
font-family: monospace;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: #dc2626;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
color: #6b7280;
|
||||
padding: 36px 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.mahnungen-table {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
</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>
|
||||
</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>
|
||||
</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");
|
||||
|
||||
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];
|
||||
|
||||
if (tdUser || tdClass || tdItem) {
|
||||
const textUser = tdUser.textContent || tdUser.innerText;
|
||||
const textClass = tdClass.textContent || tdClass.innerText;
|
||||
const textItem = tdItem.textContent || tdItem.innerText;
|
||||
|
||||
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";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1523,18 +1523,12 @@
|
||||
document.getElementById('new-location-input').value = '';
|
||||
}
|
||||
|
||||
// Globale Variablen definieren
|
||||
let currentFetchedIsbn = null;
|
||||
let currentBookData = null;
|
||||
|
||||
// Function to fetch book information from ISBN (an window gebunden)
|
||||
window.fetchBookInfo = function(formType) {
|
||||
// Sicherheitsprüfung, falls die Variable in manchen Ladezuständen nicht existiert
|
||||
if (typeof libraryModuleEnabled !== 'undefined' && !libraryModuleEnabled) {
|
||||
// Function to fetch book information from ISBN
|
||||
function fetchBookInfo(formType) {
|
||||
if (!libraryModuleEnabled) {
|
||||
alert('Bibliotheks-Modul ist deaktiviert.');
|
||||
return;
|
||||
}
|
||||
|
||||
const isbnField = document.getElementById('isbn');
|
||||
const infoContainer = document.getElementById('book-info-container');
|
||||
|
||||
@@ -1543,25 +1537,13 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback, falls normalizeIsbnClient nicht geladen wurde
|
||||
const isbn = typeof normalizeIsbnClient === 'function' ? normalizeIsbnClient(isbnField.value) : isbnField.value.trim();
|
||||
const isbn = normalizeIsbnClient(isbnField.value);
|
||||
|
||||
if (!isbn) {
|
||||
infoContainer.innerHTML = '<div class="error-message">Bitte geben Sie eine ISBN oder einen Barcode ein.</div>';
|
||||
currentFetchedIsbn = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// FIX FÜR DAS DOPPELKLICK-PROBLEM:
|
||||
// Wenn die gleiche ISBN bereits geladen wurde, brechen wir hier ab,
|
||||
// um den Import-Button nicht durch das DOM-Refresh zu überschreiben.
|
||||
if (currentFetchedIsbn === isbn && currentBookData !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Setze aktuelle ISBN, um neue Anfragen während des Ladens zu blockieren
|
||||
currentFetchedIsbn = isbn;
|
||||
|
||||
// Show loading indicator
|
||||
infoContainer.innerHTML = '<div class="loading-spinner">Informationen werden abgerufen...</div>';
|
||||
|
||||
@@ -1580,7 +1562,6 @@
|
||||
.then(data => {
|
||||
if (data.error) {
|
||||
infoContainer.innerHTML = `<div class="error-message">${data.error}</div>`;
|
||||
currentFetchedIsbn = null; // Bei Fehler zurücksetzen
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1588,37 +1569,35 @@
|
||||
currentBookData = data;
|
||||
|
||||
// Display book information with import button
|
||||
// WICHTIG: onclick ruft jetzt explizit window.importBookInfo auf
|
||||
infoContainer.innerHTML = `
|
||||
<div class="book-info">
|
||||
<h4>${data.title}</h4>
|
||||
${data.authors ? `<p><strong>Autor(en):</strong> ${data.authors}</p>` : ''}
|
||||
${data.publisher ? `<p><strong>Verlag:</strong> ${data.publisher}</p>` : ''}
|
||||
${data.publishedDate ? `<p><strong>Erscheinungsdatum:</strong> ${data.publishedDate}</p>` : ''}
|
||||
${data.pageCount ? `<p><strong>Seitenanzahl:</strong> ${data.pageCount}</p>` : ''}
|
||||
${data.price ? `<p><strong>Preis:</strong> ${data.price}</p>` : ''}
|
||||
${data.thumbnail ? `<img src="${data.thumbnail}" alt="Buchcover" class="book-thumbnail">` : ''}
|
||||
${data.description ? `
|
||||
<div class="book-description">
|
||||
<h5>Beschreibung:</h5>
|
||||
<p>${data.description}</p>
|
||||
</div>
|
||||
` : ''}
|
||||
<button type="button" class="import-book-button" onclick="window.importBookInfo('${formType}')">
|
||||
Buchdaten in Formular übernehmen
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
<div class="book-info">
|
||||
<h4>${data.title}</h4>
|
||||
${data.authors ? `<p><strong>Autor(en):</strong> ${data.authors}</p>` : ''}
|
||||
${data.publisher ? `<p><strong>Verlag:</strong> ${data.publisher}</p>` : ''}
|
||||
${data.publishedDate ? `<p><strong>Erscheinungsdatum:</strong> ${data.publishedDate}</p>` : ''}
|
||||
${data.pageCount ? `<p><strong>Seitenanzahl:</strong> ${data.pageCount}</p>` : ''}
|
||||
${data.price ? `<p><strong>Preis:</strong> ${data.price}</p>` : ''}
|
||||
${data.thumbnail ? `<img src="${data.thumbnail}" alt="Buchcover" class="book-thumbnail">` : ''}
|
||||
${data.description ? `
|
||||
<div class="book-description">
|
||||
<h5>Beschreibung:</h5>
|
||||
<p>${data.description}</p>
|
||||
</div>
|
||||
` : ''}
|
||||
<button type="button" class="import-book-button" onclick="importBookInfo('${formType}')">
|
||||
Buchdaten in Formular übernehmen
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.catch(error => {
|
||||
infoContainer.innerHTML = `<div class="error-message">${error.message}</div>`;
|
||||
currentBookData = null;
|
||||
currentFetchedIsbn = null; // Bei Fehler zurücksetzen
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// Function to import book information into form (an window gebunden)
|
||||
window.importBookInfo = function(formType) {
|
||||
// Function to import book information into form
|
||||
function importBookInfo(formType) {
|
||||
if (!currentBookData) {
|
||||
alert('Keine Buchdaten verfügbar zum Import.');
|
||||
return;
|
||||
@@ -1627,7 +1606,7 @@
|
||||
// Get form fields
|
||||
const nameField = document.getElementById('name');
|
||||
const descriptionField = document.getElementById('beschreibung');
|
||||
const priceField = document.getElementById('anschaffungskosten');
|
||||
const priceField = document.getElementById('anschaffungskosten'); // <-- NEU
|
||||
|
||||
if (!nameField || !descriptionField) {
|
||||
alert('Fehler: Formularfelder nicht gefunden.');
|
||||
@@ -1673,24 +1652,12 @@
|
||||
}
|
||||
|
||||
// Download and import book cover image if available
|
||||
// Sicherstellen, dass die Funktion nicht crasht, falls sie auf der Seite fehlt
|
||||
if (currentBookData.thumbnail) {
|
||||
if (typeof downloadBookCover === 'function') {
|
||||
try {
|
||||
downloadBookCover(currentBookData.thumbnail);
|
||||
} catch(e) {
|
||||
console.warn('Cover konnte nicht geladen werden:', e);
|
||||
}
|
||||
}
|
||||
downloadBookCover(currentBookData.thumbnail);
|
||||
}
|
||||
|
||||
// Show success message
|
||||
const infoContainer = document.getElementById('book-info-container');
|
||||
|
||||
// Alte Erfolgsmeldung entfernen, um Doppelungen zu vermeiden
|
||||
const oldMessage = infoContainer.querySelector('.import-success-message');
|
||||
if (oldMessage) oldMessage.remove();
|
||||
|
||||
const successMessage = document.createElement('div');
|
||||
successMessage.className = 'import-success-message';
|
||||
successMessage.textContent = 'Buchdaten erfolgreich in das Formular übernommen!';
|
||||
@@ -1702,7 +1669,7 @@
|
||||
successMessage.parentNode.removeChild(successMessage);
|
||||
}
|
||||
}, 3000);
|
||||
};
|
||||
}
|
||||
|
||||
function downloadBookCover(imageUrl) {
|
||||
if (!imageUrl) {
|
||||
|
||||
Reference in New Issue
Block a user