Compare commits

...

9 Commits

Author SHA1 Message Date
Aiirondev_dev 0f0f0ebefd impolementation of standardised time zone
Release Inventarsystem / release-docker (push) Successful in 2m17s
2026-08-23 11:29:42 +02:00
Aiirondev_dev 65fadf7f5f impolementation of standardised time zone
Release Inventarsystem / release-docker (push) Successful in 2m17s
2026-08-23 11:28:53 +02:00
Aiirondev_dev 6f9994c4ce Addtion of missing import
Release Inventarsystem / release-docker (push) Successful in 3m9s
2026-08-23 10:27:36 +02:00
Aiirondev_dev 6496716572 Slight changes to the Design
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-22 16:13:45 +02:00
Aiirondev_dev f750739b51 Testing of the mahnugnssystem
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-22 16:02:39 +02:00
Aiirondev_dev 4c3545f4ba Design changes
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-22 15:44:29 +02:00
Aiirondev_dev c0bf0c0b8e Mahnungswesen implementierung
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-22 15:38:36 +02:00
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
5 changed files with 834 additions and 119 deletions
+565 -44
View File
@@ -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
+14 -13
View File
@@ -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
+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) %}
+225
View File
@@ -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 %}
+29 -62
View File
@@ -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) {