Compare commits

...

22 Commits

Author SHA1 Message Date
Aiirondev_dev 2cd95ef808 Removal again
Release Inventarsystem / release-docker (push) Successful in 3m6s
2026-08-22 15:14:26 +02:00
Aiirondev_dev 17b58ff7ca Removal again
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-22 00:08:38 +02:00
Aiirondev_dev c67f64a45a Try again
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-22 00:02:47 +02:00
Aiirondev_dev 7727bfd94a Revert of changes
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-21 23:44:26 +02:00
Aiirondev_dev 55f34e0462 changes to the info importing for the library item
Release Inventarsystem / release-docker (push) Successful in 2m27s
2026-08-21 23:28:52 +02:00
Aiirondev_dev 5a3a180b88 Removal of the Korrektion function to remove an unneedet administrative ioverhead, but an option to add it back in later
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-21 23:04:37 +02:00
Aiirondev_dev 07ad592bd9 Improvement of naming
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-21 22:22:37 +02:00
Aiirondev_dev fa299cde06 Redirect issue with the Bibiliothek öffnen Button in the library_item_invoices.html
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-21 21:53:08 +02:00
Aiirondev_dev bb2c3f52e8 Implementation for the Damage history for the library items
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-21 18:10:31 +02:00
Aiirondev_dev 58ea4e5716 Implementation for the Damage history for the library items
Release Inventarsystem / release-docker (push) Successful in 2m22s
2026-08-21 17:45:55 +02:00
Aiirondev_dev e46ee80cf2 feat(library): add damage history view alongside invoices for items
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-21 13:51:16 +02:00
Aiirondev_dev f0b112b34c Klass implementation
Release Inventarsystem / release-docker (push) Successful in 2m19s
2026-08-21 13:20:34 +02:00
Aiirondev_dev 01637c31ee feat(library): add class filtering and sorting to library loans admin view
Release Inventarsystem / release-docker (push) Successful in 2m17s
- Add class filter dropdown and class column to borrowings table
- Implement interactive client-side column sorting for tables
- Update backend route to resolve and map user class information
2026-08-21 12:32:53 +02:00
Aiirondev_dev 52b2628adb Miss of an damage_unset
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-21 12:08:21 +02:00
Aiirondev_dev a51c4c4cbe fix: resolve item repair logic and clear mongo damage flags
Release Inventarsystem / release-docker (push) Successful in 2m20s
- Fixed incorrect form field key (`action_type` -> `action`) in admin route.
- Switched to MongoDB `$unset` to completely remove damage fields (`HasDamage`, `DamageText`, etc.) from the items collection instead of just setting them to false.
- Added cleanup logic to `$unset` damage flags from related active loans in the `borrowings` collection.
- Ensures repaired/replaced items correctly disappear from the damaged items UI.
2026-08-21 11:59:50 +02:00
Aiirondev_dev e4b27c2d62 Fix of a naming error in the repair resolver
Release Inventarsystem / release-docker (push) Successful in 2m17s
2026-08-21 11:48:09 +02:00
Aiirondev_dev bf85d406ac Fix of a naming error
Release Inventarsystem / release-docker (push) Successful in 3m11s
2026-08-21 11:02:21 +02:00
Aiirondev_dev 06e0c6dfb7 Fix of an redirect
Release Inventarsystem / release-docker (push) Successful in 3m8s
2026-08-20 23:12:43 +02:00
Aiirondev_dev fc25c2cd90 smal fix
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-19 23:19:11 +02:00
Aiirondev_dev c672ffcb12 methode not allowed issue path
Release Inventarsystem / release-docker (push) Successful in 2m16s
2026-08-19 23:12:27 +02:00
Aiirondev_dev 2bafc55e00 Redirection issue solved
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-19 22:45:36 +02:00
Aiirondev_dev 7e20790bac Fix of an encryption issue
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-19 22:40:25 +02:00
7 changed files with 1049 additions and 101 deletions
+591 -54
View File
@@ -1588,8 +1588,7 @@ def update_appointment_statuses():
f"Failed to create activation notification for {appointment.get('_id')}: {notif_err}"
)
elif it.is_library_item(appointment.get('Item')):
# Introduction of an messaging system and a Mahnstufen implementation for the library Book bookings after the designatet time.
pass
create_return_reminders()
client.close()
@@ -1713,6 +1712,111 @@ def _shutdown_scheduler():
atexit.register(_shutdown_scheduler)
def create_return_reminders():
"""
Prüft aktive Ausleihungen auf Überschreitung der Rückgabefrist,
erhöht Mahnstufen, versendet E-Mails/Benachrichtigungen und sperrt Nutzer.
"""
current_time = datetime.datetime.now(datetime.timezone.utc)
try:
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
db = client[MONGODB_DB]
ausleihungen = db['ausleihungen']
users_col = db['users']
items_col = db['items']
# Finde alle aktiven Ausleihungen, die in der Vergangenheit fällig waren
overdue_appointments = list(ausleihungen.find({
'Status': 'active',
'DueDate': {'$lt': current_time}
}))
for appt in overdue_appointments:
days_overdue = (current_time - appt.get('DueDate', current_time)).days
mahnstufe = appt.get('Mahnstufe', 0)
target_user = str(appt.get('User', '')).strip()
item_id = appt.get('Item')
# 1. Objektdetails holen (für den E-Mail-Text)
item_name = "Unbekannter Artikel"
if item_id:
try:
item_doc = items_col.find_one({'_id': ObjectId(item_id)})
if item_doc:
item_name = item_doc.get('Name', str(item_id))
except Exception:
pass
# 2. Nutzerdetails holen (für die E-Mail-Adresse)
user_doc = users_col.find_one({'username': target_user}) # Feldnamen ggf. anpassen (z.B. '_id')
if not user_doc:
app.logger.warning(f"Nutzer '{target_user}' für Mahnung nicht gefunden.")
continue
user_email = user_doc.get('email')
# 3. Eskalationsstufen prüfen
if days_overdue >= 14 and days_overdue < 28 and mahnstufe == 0:
# STUFE 1: Erste Mahnung
ausleihungen.update_one(
{'_id': appt['_id']},
{'$set': {'Mahnstufe': 1, 'LastUpdated': current_time}}
)
_create_notification(
db, audience='user', notif_type='warning',
title='Erinnerung: Ausleihe überfällig',
message=f'Deine Ausleihe für "{item_name}" ist seit {days_overdue} Tagen überfällig.',
target_user=target_user, severity='warning'
)
if user_email:
subject = "1. Mahnung: Rückgabe überfällig"
note = (f"Hallo {target_user},<br><br>"
f"bitte beachte, dass die Ausleihe für den Artikel <b>{item_name}</b> "
f"seit {days_overdue} Tagen überfällig ist. Bitte bringe den Artikel zeitnah zurück.")
send(email=user_email, subject=subject, note=note, sender="Bibliotheksverwaltung")
app.logger.info(f"1. Mahnung an {user_email} gesendet.")
elif days_overdue >= 28 and mahnstufe == 1:
# STUFE 2: Letzte Mahnung, Sperrung und Report
ausleihungen.update_one(
{'_id': appt['_id']},
{'$set': {'Mahnstufe': 2, 'LastUpdated': current_time}}
)
# Nutzerkonto für weitere Ausleihen sperren
users_col.update_one(
{'_id': user_doc['_id']},
{'$set': {
'is_blocked': True,
'block_reason': f'System-Sperre: Ausleihe von "{item_name}" {days_overdue} Tage überfällig.'
}}
)
_create_notification(
db, audience='user', notif_type='error',
title='Konto gesperrt - 2. Mahnung',
message=f'Aufgrund der starken Überfälligkeit von "{item_name}" wurde dein Konto vorübergehend gesperrt.',
target_user=target_user, severity='critical'
)
if user_email:
subject = "WICHTIG: Kontosperrung & 2. Mahnung"
note = (f"Hallo {target_user},<br><br>"
f"deine Ausleihe für den Artikel <b>{item_name}</b> ist nun seit {days_overdue} Tagen überfällig. "
f"Dies ist ein automatischer Bericht über deinen Status: <b>Dein Konto wurde soeben für neue Reservierungen gesperrt.</b><br><br>"
f"Die Sperre wird erst aufgehoben, sobald der Artikel im System als zurückgegeben gemeldet wurde.")
send(email=user_email, subject=subject, note=note, sender="Bibliotheksverwaltung")
app.logger.warning(f"Nutzer {target_user} gesperrt. 2. Mahnung an {user_email} gesendet.")
client.close()
except Exception as e:
app.logger.error(f"Fehler bei der automatischen Mahnlauf-Prüfung: {e}")
"""-------------------------------------------------------------File Upload Validation----------------------------------------------------------------------------- """
def allowed_file(filename, file_content=None, max_size_mb=cfg.MAX_UPLOAD_MB):
@@ -3312,13 +3416,17 @@ def library_view():
def library_loans_admin():
"""Admin overview for library borrowings and damaged library items."""
if 'username' not in session:
flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error')
flash(
'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'])
if not current_permissions['pages'].get('library_loans_admin', False):
flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error')
flash(
'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'):
@@ -3336,16 +3444,56 @@ def library_loans_admin():
def fmt_money(value):
return _format_money_value(value)
def safe_decrypt(val):
if not val or not isinstance(val, str):
return val or ''
try:
return decrypt_text(val)
except Exception:
return val
# Hilfsfunktion zur Formatierung der Schadenshistorie
def format_damage_history(reports, repairs):
fmt_reports = []
for r in (reports or []):
fmt_reports.append({
'description': r.get('description', ''),
'reported_by': r.get('reported_by', ''),
'reported_at': fmt_dt(r.get('reported_at'))
})
fmt_repairs = []
for rep in (repairs or []):
res_reports = []
for res in rep.get('resolved_reports', []):
res_reports.append({
'description': res.get('description', ''),
'reported_by': res.get('reported_by', ''),
'reported_at': fmt_dt(res.get('reported_at'))
})
fmt_repairs.append({
'repaired_by': rep.get('repaired_by', ''),
'repaired_at': fmt_dt(rep.get('repaired_at')),
'resolved_reports': res_reports
})
return fmt_reports, fmt_repairs
client = None
try:
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
db = client[MONGODB_DB]
items_col = db['items']
ausleihungen_col = db['ausleihungen']
student_cards_col = db['student_cards']
# WICHTIG: 'DamageRepairs': 1 zur Projektion hinzugefügt!
library_items = list(items_col.find(
{'ItemType': {'$in': LIBRARY_ITEM_TYPES}, 'Deleted': {'$ne': True}},
{'Name': 1, 'Code_4': 1, 'Anschaffungskosten': 1, 'Condition': 1, 'HasDamage': 1, 'DamageReports': 1, 'Verfuegbar': 1, 'User': 1, 'ItemType': 1, 'Author': 1, 'ISBN': 1}
{
'Name': 1, 'Code_4': 1, 'Anschaffungskosten': 1, 'Condition': 1,
'HasDamage': 1, 'DamageReports': 1, 'DamageRepairs': 1,
'Verfuegbar': 1, 'User': 1, 'ItemType': 1, 'Author': 1, 'ISBN': 1
}
))
item_map = {str(item['_id']): item for item in library_items if item.get('_id')}
item_ids = list(item_map.keys())
@@ -3354,27 +3502,63 @@ def library_loans_admin():
if item_ids:
active_records = list(ausleihungen_col.find(
{'Item': {'$in': item_ids}, 'Status': {'$in': ['active', 'planned', 'completed']}},
{'User': 1, 'Item': 1, 'Status': 1, 'Start': 1, 'End': 1, 'Period': 1, 'Notes': 1, 'InvoiceData': 1}
{'User': 1, 'Item': 1, 'Status': 1, 'Start': 1, 'End': 1, 'Period': 1, 'Notes': 1, 'InvoiceData': 1,
'Klasse': 1, 'klasse': 1, 'Class': 1, 'school_class': 1}
).sort('Start', -1))
# class_map aufbauen (wie zuvor besprochen)
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
active_item_ids = set()
loan_entries = []
for record in active_records:
item_id = str(record.get('Item') or '')
item_doc = item_map.get(item_id)
if item_id and record.get('Status') == 'active':
active_item_ids.add(item_id)
item_doc = item_map.get(item_id)
if not item_doc:
continue
invoice_data = record.get('InvoiceData') or {}
condition_value = str(item_doc.get('Condition', '')).strip().lower()
item_has_damage = bool(item_doc.get('HasDamage')) or condition_value == 'destroyed' or bool(item_doc.get('DamageReports'))
damage_reports = item_doc.get('DamageReports', []) or []
raw_reports = item_doc.get('DamageReports', [])
raw_repairs = item_doc.get('DamageRepairs', [])
item_has_damage = bool(item_doc.get('HasDamage')) or condition_value == 'destroyed' or bool(raw_reports)
fmt_reports, fmt_repairs = format_damage_history(raw_reports, raw_repairs)
raw_user = record.get('User', '')
decrypted_user = decrypt_text(raw_user) if raw_user else ''
decrypted_user = safe_decrypt(raw_user)
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(decrypted_user.strip().lower(), '')
or ''
)
loan_entries.append({
'id': str(record.get('_id')),
@@ -3384,31 +3568,40 @@ def library_loans_admin():
'item_author': item_doc.get('Author', ''),
'item_isbn': item_doc.get('ISBN', ''),
'item_cost_raw': item_doc.get('Anschaffungskosten', ''),
'user': decrypted_user,
'user': decrypted_user if decrypted_user else raw_user,
'klasse': user_class,
'status': record.get('Status', ''),
'start': fmt_dt(record.get('Start')),
'end': fmt_dt(record.get('End')),
'period': record.get('Period', ''),
'notes': record.get('Notes', ''),
'invoice_number': invoice_data.get('invoice_number', ''),
'invoice_amount': fmt_money(invoice_data.get('amount')) if invoice_data.get('amount') is not None else fmt_money(item_doc.get('Anschaffungskosten')),
'invoice_amount': fmt_money(invoice_data.get('amount')) if invoice_data.get(
'amount') is not None else fmt_money(item_doc.get('Anschaffungskosten')),
'invoice_paid': bool(invoice_data.get('paid', False)),
'invoice_paid_at': fmt_dt(invoice_data.get('paid_at')) if isinstance(invoice_data.get('paid_at'), datetime.datetime) else '',
'invoice_corrections_count': len(record.get('InvoiceCorrections', []) or []),
'invoice_paid_at': fmt_dt(invoice_data.get('paid_at')) if isinstance(invoice_data.get('paid_at'),
datetime.datetime) else '',
'has_damage': item_has_damage,
'damage_count': len(damage_reports),
'damage_text': (damage_reports[0].get('description', '') if damage_reports else ''),
'damage_count': len(raw_reports),
'damage_text': fmt_reports[0]['description'] if fmt_reports else '',
'damage_reports': fmt_reports,
'damage_repairs': fmt_repairs
})
damaged_items = []
for item_doc in library_items:
item_id = str(item_doc.get('_id') or '')
condition_value = str(item_doc.get('Condition', '')).strip().lower()
damage_reports = item_doc.get('DamageReports', []) or []
item_has_damage = bool(item_doc.get('HasDamage')) or condition_value == 'destroyed' or bool(damage_reports)
raw_reports = item_doc.get('DamageReports', [])
raw_repairs = item_doc.get('DamageRepairs', [])
item_has_damage = bool(item_doc.get('HasDamage')) or condition_value == 'destroyed' or bool(raw_reports)
if not item_has_damage or item_id in active_item_ids:
continue
fmt_reports, fmt_repairs = format_damage_history(raw_reports, raw_repairs)
damaged_items.append({
'id': item_id,
'name': item_doc.get('Name', ''),
@@ -3416,11 +3609,13 @@ def library_loans_admin():
'author': item_doc.get('Author', ''),
'isbn': item_doc.get('ISBN', ''),
'condition': item_doc.get('Condition', ''),
'damage_count': len(damage_reports),
'damage_text': (damage_reports[0].get('description', '') if damage_reports else ''),
'available': bool(item_doc.get('Verfuegbar', False)),
'last_updated': fmt_dt(item_doc.get('LastUpdated')),
'acquisition_costs': item_doc.get('Anschaffungskosten', "")
'acquisition_costs': item_doc.get('Anschaffungskosten', ""),
'damage_count': len(raw_reports),
'damage_text': fmt_reports[0]['description'] if fmt_reports else '',
'damage_reports': fmt_reports,
'damage_repairs': fmt_repairs
})
return render_template(
@@ -3438,6 +3633,155 @@ def library_loans_admin():
if client:
client.close()
@app.route('/mahnungen_admin')
def mahnungen_admin():
"""Admin overview for overdue library items (Mahnungen)."""
if 'username' not in session:
flash(
'Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!',
'error')
return redirect(url_for('login'))
current_permissions = us.get_effective_permissions(session['username'])
# Hier nutzen wir beispielhaft die library_loans_admin Berechtigung
if not current_permissions['pages'].get('library_loans_admin', False):
flash(
'Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!',
'error')
return redirect(url_for('library_view'))
if not cfg.MODULES.is_enabled('library'):
flash('Bibliotheks-Modul ist deaktiviert.', 'error')
return redirect(url_for('home_admin'))
def fmt_dt(dt):
try:
return dt.strftime('%d.%m.%Y') if dt else 'Unbekannt'
except Exception:
return str(dt) if dt else 'Unbekannt'
def safe_decrypt(val):
if not val or not isinstance(val, str):
return val or ''
try:
return decrypt_text(val)
except Exception:
return val
current_time = datetime.datetime.now(datetime.timezone.utc)
client = None
try:
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
db = client[MONGODB_DB]
ausleihungen_col = db['ausleihungen']
items_col = db['items']
users_col = db['users']
student_cards_col = db['student_cards']
# Überfällige Ausleihen abrufen (Status 'active' und DueDate in der Vergangenheit)
overdue_records = list(ausleihungen_col.find({
'Status': 'active',
'DueDate': {'$lt': current_time}
}).sort('DueDate', 1))
overdue_list = []
if overdue_records:
# Bulk-Lookups zur Vermeidung von N+1 Queries
item_ids = []
for r in overdue_records:
i_id = r.get('Item')
if i_id:
try:
item_ids.append(ObjectId(str(i_id)))
except Exception:
item_ids.append(str(i_id))
items_cursor = items_col.find({
'_id': {'$in': item_ids}
}, {'Name': 1, 'Code_4': 1})
item_map = {str(item['_id']): item for item in items_cursor}
raw_users = [r.get('User') for r in overdue_records if r.get('User')]
users_cursor = users_col.find({'username': {'$in': raw_users}}, {'username': 1, 'is_blocked': 1})
user_block_map = {u.get('username'): u.get('is_blocked', False) for u in users_cursor}
class_map = {}
all_cards = list(student_cards_col.find({}, {'SchülerName': 1, 'Klasse': 1, 'AusweisId': 1}))
for card in all_cards:
raw_cls = card.get('Klasse')
dec_cls = safe_decrypt(raw_cls)
if not dec_cls:
continue
raw_name = card.get('SchülerName')
dec_name = safe_decrypt(raw_name)
ausweis_id = card.get('AusweisId')
if dec_name:
class_map[dec_name.strip().lower()] = dec_cls
if raw_name:
class_map[raw_name] = dec_cls
if ausweis_id:
class_map[str(ausweis_id).strip().lower()] = dec_cls
# Datenaufbereitung für das Template
for record in overdue_records:
item_id = str(record.get('Item') or '')
item_doc = item_map.get(item_id, {})
item_name = item_doc.get('Name', item_id)
item_code = item_doc.get('Code_4', '')
if item_code:
item_name = f"{item_name} ({item_code})"
raw_user = record.get('User', '')
decrypted_user = safe_decrypt(raw_user)
display_user = decrypted_user if decrypted_user else (raw_user or 'Unbekannt')
user_class = (
safe_decrypt(record.get('Klasse'))
or safe_decrypt(record.get('Class'))
or safe_decrypt(record.get('school_class'))
or class_map.get(raw_user, '')
or class_map.get(decrypted_user, '')
or class_map.get(display_user.strip().lower(), '')
or ''
)
due_date_obj = record.get('DueDate')
days_overdue = (current_time - due_date_obj).days if due_date_obj else 0
is_blocked = user_block_map.get(raw_user, False)
overdue_list.append({
'id': str(record.get('_id')),
'item_name': item_name,
'user': display_user,
'klasse': user_class,
'due_date': fmt_dt(due_date_obj),
'days_overdue': days_overdue,
'mahnstufe': record.get('Mahnstufe', 0),
'is_blocked': is_blocked
})
return render_template(
'mahnungen_admin.html',
overdue_list=overdue_list,
library_module_enabled=cfg.MODULES.is_enabled('library'),
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
)
except Exception as e:
app.logger.error(f"Error loading mahnungen admin view: {e}")
flash('Fehler beim Laden der Mahnungsverwaltung.', 'error')
return redirect(url_for('home_admin'))
finally:
if client:
client.close()
@app.route('/api/library_items')
def api_library_items():
"""
@@ -8832,25 +9176,25 @@ def admin_mark_invoice_paid(borrow_id):
)
flash('Rechnung wurde als bezahlt markiert.', 'success')
return redirect(url_for('admin_borrowings'))
return redirect(url_for('library_loans_admin'))
except Exception as e:
app.logger.error(f"Error marking invoice paid for borrow {borrow_id}: {e}")
flash('Fehler beim Markieren als bezahlt.', 'error')
return redirect(url_for('admin_borrowings'))
return redirect(url_for('library_loans_admin'))
finally:
if client:
client.close()
@app.route('/admin/borrowings/<borrow_id>/invoice/pay', methods=['POST'])
def admin_pay_invoice_extended(borrow_id): # <--- HIER GEÄNDERT (Eindeutiger Funktionsname)
def admin_pay_invoice_extended(borrow_id):
"""Mark invoice as paid and complete borrowing if active."""
if 'username' not in session:
flash('Ihnen ist es nicht gestattet, versuchen sie es erneut nach Anmeldung!', 'error')
return redirect(url_for('login'))
current_permissions = us.get_effective_permissions(session['username'])
if not current_permissions['pages'].get('admin_borrowings', False):
if not current_permissions['pages'].get('library_loans_admin', False):
flash('Ihnen fehlen die nötigen Berechtigungen.', 'error')
return redirect(request.referrer or url_for('home_admin'))
@@ -8863,12 +9207,12 @@ def admin_pay_invoice_extended(borrow_id): # <--- HIER GEÄNDERT (Eindeutiger F
borrow_doc = ausleihungen.find_one({'_id': ObjectId(borrow_id)})
if not borrow_doc:
flash('Ausleihung nicht gefunden.', 'error')
return redirect(request.referrer or url_for('admin_borrowings'))
return redirect(request.referrer or url_for('library_loans_admin'))
invoice_data = borrow_doc.get('InvoiceData') or {}
if not invoice_data:
flash('Für diese Ausleihung existiert keine Rechnung.', 'warning')
return redirect(request.referrer or url_for('admin_borrowings'))
return redirect(request.referrer or url_for('library_loans_admin'))
now = datetime.datetime.now()
update_fields = {'LastUpdated': now}
@@ -9120,11 +9464,37 @@ def admin_add_invoice_correction(borrow_id):
if client:
client.close()
@app.route('/admin/items/<item_id>/resolve_repair', methods=['POST'])
def admin_resolve_repair(item_id):
"""Flask-Route für die Reparatur-Aktionen (repair, replace, delete)."""
if 'username' not in session:
flash('Bitte melden Sie sich an, um fortzufahren.', 'error')
return redirect(url_for('login'))
def resolve_repaired_item(item_id, action, new_code_4="", current_user="admin"):
current_permissions = us.get_effective_permissions(session['username'])
if not current_permissions['pages'].get('library_loans_admin', False):
flash('Ihnen fehlen die nötigen Berechtigungen für diese Aktion.', 'error')
return redirect(url_for('home_admin'))
action = request.form.get('action', 'repair')
new_code_4 = request.form.get('new_code_4', '')
current_user = session.get('username', 'admin')
success, message = resolve_repaired_item_funct(item_id, action, new_code_4, current_user)
if success:
flash(message, 'success')
else:
flash(message, 'error')
return redirect(url_for('library_loans_admin'))
def resolve_repaired_item_funct(item_id, action, new_code_4="", current_user="admin"):
"""
Verarbeitet Items aus der Reparatur und aktualisiert die Serien-Counts
sowie Parent/Child-Abhängigkeiten beim Löschen korrekt.
Verschiebt gemeldete Schäden in die Historie (DamageRepairs) bei Reparatur/Austausch.
"""
if not ObjectId.is_valid(item_id):
return False, "Ungültige Item-ID."
@@ -9134,11 +9504,31 @@ def resolve_repaired_item(item_id, action, new_code_4="", current_user="admin"):
db = client[cfg.MONGODB_DB]
items = db['items']
borrowings = db['borrowings'] if 'borrowings' in db.list_collection_names() else None
item = items.find_one({'_id': ObjectId(item_id)})
if not item:
return False, "Item nicht in der Datenbank gefunden."
series_group_id = item.get('SeriesGroupId')
now = datetime.datetime.now()
legacy_damage_unset = {
'has_damage': "",
'DamageCount': "",
'damage_count': "",
'DamageText': "",
'damage_text': "",
'is_damaged': "",
'IsDamaged': ""
}
borrowings_unset = legacy_damage_unset.copy()
borrowings_unset.update({
'HasDamage': "",
'Condition': "",
'DamageReports': ""
})
# --- OPTION 1: DELETE ---
if action == 'delete':
@@ -9154,7 +9544,6 @@ def resolve_repaired_item(item_id, action, new_code_4="", current_user="admin"):
if sibling:
new_parent_id_str = str(sibling['_id'])
# 1. Mache das Geschwister-Item zum neuen Parent
items.update_one(
{'_id': sibling['_id']},
{'$set': {
@@ -9163,7 +9552,6 @@ def resolve_repaired_item(item_id, action, new_code_4="", current_user="admin"):
}}
)
# 2. Leite alle anderen verbleibenden Sub-Items auf den neuen Parent um
items.update_many(
{
'SeriesGroupId': series_group_id,
@@ -9172,17 +9560,8 @@ def resolve_repaired_item(item_id, action, new_code_4="", current_user="admin"):
{'$set': {'ParentItemId': new_parent_id_str}}
)
# Physisch aus der Datenbank löschen (Hard Delete)
items.delete_one({'_id': ObjectId(item_id)})
# ALTERNATIVE (Soft Delete have to see if it makes sense):
# now = datetime.datetime.now()
# items.update_one(
# {'_id': ObjectId(item_id)},
# {'$set': {'Deleted': True, 'DeletedAt': now, 'DeletedBy': current_user}}
# )
# SeriesCount bei ALLEN verbleibenden Items dieser Serie um 1 reduzieren
if series_group_id:
items.update_many(
{'SeriesGroupId': series_group_id},
@@ -9191,27 +9570,63 @@ def resolve_repaired_item(item_id, action, new_code_4="", current_user="admin"):
return True, "Item wurde gelöscht und der Bestand aller Serien-Items angepasst."
current_reports = item.get('DamageReports', [])
repair_entry = {
'repaired_by': current_user,
'repaired_at': now,
'action_taken': action,
'resolved_reports': current_reports
}
# --- OPTION 2: REPLACE ---
elif action == 'replace':
if action == 'replace':
items.update_one(
{'_id': ObjectId(item_id)},
{'$set': {
'Code_4': str(new_code_4).strip(),
'Verfuegbar': True,
'LastUpdated': datetime.datetime.now()
}}
{
'$push': {'DamageRepairs': repair_entry},
'$set': {
'Code_4': str(new_code_4).strip(),
'Verfuegbar': True,
'HasDamage': False,
'Condition': 'gut',
'DamageReports': [],
'LastUpdated': now
},
'$unset': legacy_damage_unset
}
)
if borrowings is not None:
borrowings.update_many(
{'$or': [{'item_id': str(item_id)}, {'item_id': ObjectId(item_id)}]},
{'$unset': borrowings_unset}
)
return True, f"Item wurde ersetzt. Neuer Code 4 zugewiesen: {new_code_4}."
# --- OPTION 3: JUST REPAIR ---
elif action == 'repair':
items.update_one(
{'_id': ObjectId(item_id)},
{'$set': {
'Verfuegbar': True,
'LastUpdated': datetime.datetime.now()
}}
{
'$push': {'DamageRepairs': repair_entry},
'$set': {
'Verfuegbar': True,
'HasDamage': False,
'Condition': 'gut',
'DamageReports': [],
'LastUpdated': now
},
'$unset': legacy_damage_unset
}
)
if borrowings is not None:
borrowings.update_many(
{'$or': [{'item_id': str(item_id)}, {'item_id': ObjectId(item_id)}]},
{'$unset': borrowings_unset}
)
return True, "Item wurde repariert und ist wieder regulär verfügbar."
else:
@@ -9221,9 +9636,10 @@ def resolve_repaired_item(item_id, action, new_code_4="", current_user="admin"):
print(f"Error resolving repair for item {item_id}: {e}")
return False, "Ein Datenbankfehler ist aufgetreten."
@app.route('/admin/library/items/<item_id>/invoices', methods=['GET'])
def library_item_invoices(item_id):
"""Show all stored invoices for one specific library item."""
"""Show all stored invoices and damages for one specific library item."""
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!',
@@ -9259,6 +9675,9 @@ def library_item_invoices(item_id):
item_id_obj = item_doc.get('_id')
item_id_str = str(item_id_obj)
# ==========================================
# 1. RECHNUNGSHISTORIE (Aus Ausleihen)
# ==========================================
borrow_docs = list(ausleihungen.find(
{
'Item': {'$in': [item_id_str, item_id_obj]},
@@ -9289,7 +9708,9 @@ def library_item_invoices(item_id):
entries.append({
'borrow_id': str(borrow_doc.get('_id')),
'borrow_status': borrow_doc.get('Status', ''),
'borrow_user': borrow_doc.get('User', ''),
'borrow_user': decrypt_text(borrow_doc.get('User', '')) if hasattr(borrow_doc,
'get') and borrow_doc.get(
'User') else '',
'borrow_start': borrow_doc.get('Start').strftime('%d.%m.%Y %H:%M') if isinstance(
borrow_doc.get('Start'), datetime.datetime) else '',
'borrow_end': borrow_doc.get('End').strftime('%d.%m.%Y %H:%M') if isinstance(borrow_doc.get('End'),
@@ -9305,6 +9726,121 @@ def library_item_invoices(item_id):
'invoice_paid_by': invoice_data.get('paid_by', ''),
})
# ==========================================
# 2. SCHADENSHISTORIE (Neues Format + Fallback)
# ==========================================
raw_damages = []
# A: Aktive / gemeldete Schäden aus 'DamageReports'
damage_reports = item_doc.get('DamageReports', [])
if not isinstance(damage_reports, list):
damage_reports = []
for d in damage_reports:
if not isinstance(d, dict): continue
# Rechnungs-Metadaten anhängen, falls vorhanden
extra_info = ""
if d.get('invoice_number'):
amount = d.get('invoice_amount', '')
extra_info = f"\n(Mit Rechnung: {d['invoice_number']} - {amount}€)"
raw_damages.append({
'raw_date': d.get('reported_at'),
'source': str(d.get('source', 'Meldung')).capitalize(),
'user': d.get('reported_by', 'System/Admin'),
'description': d.get('description', 'Keine Beschreibung') + extra_info,
'status': 'Offen',
'borrow_id': None
})
# B: Behobene Schäden aus 'DamageRepairs'
damage_repairs = item_doc.get('DamageRepairs', [])
if not isinstance(damage_repairs, list):
damage_repairs = []
for r in damage_repairs:
if not isinstance(r, dict): continue
resolved_list = r.get('resolved_reports', [])
repaired_at = r.get('repaired_at')
repaired_by = r.get('repaired_by', 'Admin')
action_taken = r.get('action_taken', 'repair')
# Status anhand der Aktion bestimmen
status_text = 'Repariert' if action_taken == 'repair' else 'Erledigt'
if resolved_list and isinstance(resolved_list, list):
for res in resolved_list:
# Rechnungs-Metadaten aus dem ehemals offenen Report
extra_info = ""
if res.get('invoice_number'):
amount = res.get('invoice_amount', '')
extra_info = f"\n(Mit Rechnung: {res['invoice_number']} - {amount}€)"
raw_damages.append({
'raw_date': res.get('reported_at') or repaired_at,
'source': f"{status_text} von {repaired_by}",
'user': res.get('reported_by', 'System/Admin'),
'description': res.get('description', 'Keine Beschreibung') + extra_info,
'status': status_text,
'borrow_id': None
})
else:
# Fallback, falls keine genauen Reports hinterlegt sind
raw_damages.append({
'raw_date': repaired_at,
'source': 'Reparatur',
'user': repaired_by,
'description': 'Schaden behoben',
'status': status_text,
'borrow_id': None
})
# C: Legacy-Fallback: Schäden, die evtl. nur in alten Ausleih-Dokumenten vermerkt sind
damage_borrow_docs = list(ausleihungen.find(
{
'Item': {'$in': [item_id_str, item_id_obj]},
'$or': [
{'Damage': {'$exists': True, '$ne': ''}},
{'DamageData': {'$exists': True, '$ne': {}}},
{'Condition': {'$exists': True, '$ne': ''}}
]
}
))
for b in damage_borrow_docs:
borrow_user = decrypt_text(b.get('User', '')) if b.get('User') else ''
d_data = b.get('DamageData') or {}
if not isinstance(d_data, dict): d_data = {}
desc = d_data.get('description') or b.get('Damage') or b.get('Condition')
if desc and str(desc).strip():
raw_damages.append({
'raw_date': d_data.get('reported_at') or b.get('End') or b.get('Start'),
'source': "Ausleihe",
'user': borrow_user,
'description': desc,
'status': d_data.get('status', 'Gemeldet'),
'borrow_id': str(b.get('_id'))
})
# Daten chronologisch (neueste zuerst) sortieren
def get_sort_key(entry):
d = entry['raw_date']
return d if isinstance(d, datetime.datetime) else datetime.datetime.min
raw_damages.sort(key=get_sort_key, reverse=True)
# Datums-Objekte für das Template formatieren
damage_entries = []
for entry in raw_damages:
d_date = entry.pop('raw_date')
entry['date'] = d_date.strftime('%d.%m.%Y %H:%M') if isinstance(d_date, datetime.datetime) else str(
d_date or 'Unbekannt')
damage_entries.append(entry)
return render_template(
'library_item_invoices.html',
item={
@@ -9315,14 +9851,15 @@ def library_item_invoices(item_id):
'isbn': item_doc.get('ISBN', ''),
},
invoices=entries,
damages=damage_entries,
library_module_enabled=cfg.MODULES.is_enabled('library'),
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
mail_module_enabled=cfg.MODULES.is_enabled('mail')
)
except Exception as e:
app.logger.error(f"Error loading invoice history for item {item_id}: {e}")
flash('Fehler beim Laden der Rechnungshistorie.', 'error')
app.logger.error(f"Error loading invoice/damage history for item {item_id}: {e}")
flash('Fehler beim Laden der Rechnungs- oder Schadenshistorie.', 'error')
return redirect(url_for('library_loans_admin'))
finally:
if client:
+2 -2
View File
@@ -730,11 +730,11 @@ def get_user_by_student_ident(student_ident):
return user_doc
except Exception as e:
app.logger.error(f"Entschlüsselungsfehler bei ID {user_doc.get('_id')}: {e}")
logger.error(f"Entschlüsselungsfehler bei ID {user_doc.get('_id')}: {e}")
continue
except Exception as exc:
app.logger.error(f"Datenbankfehler in get_user_by_student_ident: {exc}")
logger.error(f"Datenbankfehler in get_user_by_student_ident: {exc}")
finally:
client.close()
+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) %}
+124 -25
View File
@@ -72,9 +72,10 @@
color: #1f2937;
}
/* Angepasst für 4 Filter-Felder statt 3 */
.filter-bar {
display: grid;
grid-template-columns: 1fr 220px 220px;
grid-template-columns: 1fr 140px 180px 180px;
gap: 10px;
margin-bottom: 16px;
}
@@ -121,6 +122,7 @@
letter-spacing: 0.04em;
color: #64748b;
background: var(--ui-surface-soft);
user-select: none; /* Verhindert Textmarkierung beim Klicken */
}
.library-table tr:hover td {
@@ -149,6 +151,7 @@
.badge-open { background: #fee2e2; color: #991b1b; }
.badge-paid { background: #dcfce7; color: #166534; }
.badge-damaged { background: #fee2e2; color: #991b1b; }
.badge-class { background: #f1f5f9; color: #475569; border: 1px solid #cbd5e1; } /* Neues Badge für Klasse */
.row-actions {
display: flex;
@@ -222,15 +225,30 @@
</div>
<div class="filter-bar">
<input id="library-search" type="text" placeholder="Nach Element, Benutzer, Ausweis oder Rechnung suchen...">
<input id="library-search" type="text" placeholder="Nach Element, Benutzer, Klasse, Ausweis oder Rechnung suchen...">
<!-- NEU: Klassen-Filter -->
<select id="class-filter">
<option value="all">Alle Klassen</option>
{% set classes = [] %}
{% for e in loan_entries %}
{% if e.klasse and e.klasse not in classes %}
{% set _ = classes.append(e.klasse) %}
{% endif %}
{% endfor %}
{% for c in classes|sort %}
<option value="{{ c|lower }}">{{ c }}</option>
{% endfor %}
</select>
<select id="loan-status-filter">
<option value="">Alle Ausleihen</option>
<option value="">Alle Status</option>
<option value="active">Aktiv</option>
<option value="planned">Geplant</option>
<option value="completed">Abgeschlossen</option>
</select>
<select id="damage-filter">
<option value="all">Alle Einträge</option>
<option value="all">Alle Zustände</option>
<option value="damage">Nur defekt</option>
<option value="clean">Nur ohne Schaden</option>
</select>
@@ -242,18 +260,31 @@
<table class="library-table" id="loans-table">
<thead>
<tr>
<th>Status</th>
<th>Element</th>
<th>Benutzer</th>
<th>Zeit</th>
<th>Rechnung</th>
<th>Schaden</th>
<!-- NEU: onClick Handler zum Sortieren hinzugefügt -->
<th style="cursor: pointer;" onclick="sortTable('loans-table', 0)">Status ↕</th>
<th style="cursor: pointer;" onclick="sortTable('loans-table', 1)">Element ↕</th>
<th style="cursor: pointer;" onclick="sortTable('loans-table', 2)">Benutzer ↕</th>
<th style="cursor: pointer;" onclick="sortTable('loans-table', 3)">Klasse ↕</th>
<th style="cursor: pointer;" onclick="sortTable('loans-table', 4)">Zeit ↕</th>
<th style="cursor: pointer;" onclick="sortTable('loans-table', 5)">Rechnung ↕</th>
<th style="cursor: pointer;" onclick="sortTable('loans-table', 6)">Schaden ↕</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
<!-- NEU: data-klasse hinzugefügt -->
{% for e in loan_entries %}
<tr class="loan-row" data-borrow-id="{{ e.id }}" data-item-id="{{ e.item_id }}" data-item-name="{{ e.item_name }}" data-item-code="{{ e.item_code }}" data-item-cost="{{ e.item_cost_raw }}" data-user-name="{{ e.user }}" data-search="{{ (e.item_name ~ ' ' ~ e.item_code ~ ' ' ~ e.user ~ ' ' ~ e.invoice_number ~ ' ' ~ e.item_author ~ ' ' ~ e.item_isbn)|lower }}" data-status="{{ e.status }}" data-has-damage="{{ '1' if e.has_damage else '0' }}">
<tr class="loan-row"
data-borrow-id="{{ e.id }}"
data-item-id="{{ e.item_id }}"
data-item-name="{{ e.item_name }}"
data-item-code="{{ e.item_code }}"
data-item-cost="{{ e.item_cost_raw }}"
data-user-name="{{ e.user }}"
data-klasse="{{ (e.klasse|default(''))|lower }}"
data-search="{{ (e.item_name ~ ' ' ~ e.item_code ~ ' ' ~ e.user ~ ' ' ~ (e.klasse|default('')) ~ ' ' ~ e.invoice_number ~ ' ' ~ e.item_author ~ ' ' ~ e.item_isbn)|lower }}"
data-status="{{ e.status }}"
data-has-damage="{{ '1' if e.has_damage else '0' }}">
<td>
{% if e.status == 'active' %}
<span class="badge-pill badge-active">Aktiv</span>
@@ -268,24 +299,32 @@
<div class="muted">{{ e.item_author or '—' }}</div>
<div class="mono">{{ e.item_code or '—' }}</div>
<div style="margin-top:6px;">
<a class="btn btn-outline-secondary btn-sm" href="{{ url_for('library_item_invoices', item_id=e.item_id) }}">Rechnungen</a>
<a class="btn btn-outline-secondary btn-sm" href="{{ url_for('library_item_invoices', item_id=e.item_id) }}">Rechnungen / Historie</a>
</div>
</td>
<td>
<div>{{ e.user }}</div>
<div class="muted">{{ e.start }}{% if e.end %} bis {{ e.end }}{% endif %}</div>
<div><strong>{{ e.user }}</strong></div>
</td>
<!-- NEU: Klasse Spalte -->
<td>
{% if e.klasse %}
<span class="badge-pill badge-class">{{ e.klasse }}</span>
{% else %}
<span class="muted"></span>
{% endif %}
</td>
<td>
<div>{{ e.period or '—' }}</div>
<div class="muted">{{ e.start }}{% if e.end %} bis {{ e.end }}{% endif %}</div>
{% if e.notes %}<div class="muted">{{ e.notes }}</div>{% endif %}
</td>
<td>
{% if e.invoice_number %}
<div class="mono">{{ e.invoice_number }}</div>
<div class="muted">{{ e.invoice_amount }}</div>
{% if e.invoice_corrections_count %}
<!--{% if e.invoice_corrections_count %}
<div class="muted" style="color:#7c2d12;">{{ e.invoice_corrections_count }} Korrektur(en)</div>
{% endif %}
{% endif %} -->
<div style="margin-top:6px;">
<a class="btn btn-outline-primary btn-sm" href="{{ url_for('admin_view_invoice_pdf', borrow_id=e.id) }}" target="_blank" rel="noopener">PDF öffnen</a>
</div>
@@ -324,13 +363,13 @@
<button type="button" class="btn btn-warning btn-sm" onclick="openRepairModal('{{ e.item_id }}', '{{ e.item_code }}')">Reparieren / Ersetzen</button>
{% endif %}
{% if e.invoice_number %}
<!--{% if e.invoice_number %}
<form method="post" action="{{ url_for('admin_add_invoice_correction', borrow_id=e.id) }}" onsubmit="return confirm('Korrekturbuchung hinzufügen?');" style="display: flex; gap: 6px; align-items: center; flex-wrap: wrap;">
<input type="text" name="correction_reason" value="Korrektur zu {{ e.invoice_number }}" placeholder="Korrekturgrund" required style="padding:6px; border:1px solid #ddd; border-radius:6px; min-width:160px; max-width:180px;">
<input type="text" name="amount_delta" placeholder="z.B. -{{ e.invoice_amount }}" style="padding:6px; border:1px solid #ddd; border-radius:6px; width:120px;">
<button type="submit" class="btn btn-outline-danger btn-sm">Korrektur</button>
</form>
{% endif %}
{% endif %}-->
{% if e.status in ['active', 'planned'] %}
<form method="post" action="{{ url_for('admin_reset_borrowing', borrow_id=e.id) }}" onsubmit="return confirm('Ausleihe zurücksetzen?');">
@@ -355,10 +394,11 @@
<table class="library-table" id="damaged-table">
<thead>
<tr>
<th>Element</th>
<th>Code</th>
<th>Schaden</th>
<th>Status</th>
<!-- NEU: onClick Handler zum Sortieren hinzugefügt -->
<th style="cursor: pointer;" onclick="sortTable('damaged-table', 0)">Element ↕</th>
<th style="cursor: pointer;" onclick="sortTable('damaged-table', 1)">Code ↕</th>
<th style="cursor: pointer;" onclick="sortTable('damaged-table', 2)">Schaden ↕</th>
<th style="cursor: pointer;" onclick="sortTable('damaged-table', 3)">Status ↕</th>
<th>Aktion</th>
</tr>
</thead>
@@ -489,6 +529,7 @@
<script>
(function() {
const searchInput = document.getElementById('library-search');
const classFilter = document.getElementById('class-filter'); // NEU: Klassenfilter hinzugefügt
const statusFilter = document.getElementById('loan-status-filter');
const damageFilter = document.getElementById('damage-filter');
const loanRows = Array.from(document.querySelectorAll('.loan-row'));
@@ -505,6 +546,51 @@
const damageInvoiceReason = document.getElementById('damage-invoice-reason');
const damageInvoiceReplaceBtn = document.getElementById('damage-invoice-replace-btn');
// NEU: Globale Sortier-Richtungsobjekte
let sortDirections = {};
// NEU: Sortier-Funktion
window.sortTable = function(tableId, columnIndex) {
const table = document.getElementById(tableId);
const tbody = table.tBodies[0];
const rows = Array.from(tbody.querySelectorAll("tr"));
if (rows.length === 0) return;
const sortKey = tableId + "-" + columnIndex;
if (!(sortKey in sortDirections)) {
sortDirections[sortKey] = true;
} else {
sortDirections[sortKey] = !sortDirections[sortKey];
}
const isAscending = sortDirections[sortKey];
const multiplier = isAscending ? 1 : -1;
// Pfeil im Header aktualisieren
const headers = table.querySelectorAll("th");
headers.forEach(th => {
if(th.innerHTML.includes('↕') || th.innerHTML.includes('▲') || th.innerHTML.includes('▼')) {
th.innerHTML = th.innerHTML.replace(/[↕▲▼]/g, '↕');
}
});
const currentTh = headers[columnIndex];
if (currentTh) {
currentTh.innerHTML = currentTh.innerHTML.replace('↕', isAscending ? '▲' : '▼');
}
// Zeilen sortieren
rows.sort((a, b) => {
const cellA = a.cells[columnIndex].textContent.trim();
const cellB = b.cells[columnIndex].textContent.trim();
return cellA.localeCompare(cellB, 'de', { numeric: true, sensitivity: 'base' }) * multiplier;
});
// Neu einfügen
rows.forEach(row => tbody.appendChild(row));
};
// Funktionen für das Reparatur-Modal
window.openRepairModal = function(itemId, currentCode) {
const modal = document.getElementById('repair-action-modal');
@@ -512,7 +598,6 @@
const codeContainer = document.getElementById('new-code-container');
const replaceBtn = document.getElementById('submit-replace-btn');
// Setze die Route im Formular (passe hier den Endpunkt an deine Backend-Route an, z.B. /admin/items/ID/resolve_repair)
form.action = `/admin/items/${itemId}/resolve_repair`;
document.getElementById('repair-action-input').value = 'repair';
@@ -639,20 +724,26 @@
});
}
// NEU: Kombinierte Filter-Funktion (Suche + Status + Schaden + Klasse)
function applyFilters() {
const search = (searchInput.value || '').trim().toLowerCase();
const status = statusFilter.value;
const damage = damageFilter.value;
const klasse = classFilter.value; // NEU
let visibleLoans = 0;
loanRows.forEach(row => {
const haystack = row.dataset.search || '';
const rowStatus = row.dataset.status || '';
const rowKlasse = row.dataset.klasse || ''; // NEU
const hasDamage = row.dataset.hasDamage === '1';
const searchMatch = !search || haystack.includes(search);
const statusMatch = !status || rowStatus === status;
const classMatch = klasse === 'all' || rowKlasse === klasse; // NEU
const damageMatch = damage === 'all' || (damage === 'damage' && hasDamage) || (damage === 'clean' && !hasDamage);
const show = searchMatch && statusMatch && damageMatch;
const show = searchMatch && statusMatch && classMatch && damageMatch; // NEU
row.style.display = show ? '' : 'none';
if (show) visibleLoans++;
});
@@ -663,10 +754,17 @@
const haystack = row.dataset.search || '';
const rowStatus = row.dataset.status || '';
const hasDamage = row.dataset.hasDamage === '1';
const searchMatch = !search || haystack.includes(search);
const statusMatch = !status || rowStatus === status || status === '';
const damageMatch = damage === 'all' || (damage === 'damage' && hasDamage) || (damage === 'clean' && !hasDamage);
const show = searchMatch && statusMatch && damageMatch;
// Defekte-Medien-Tabelle hat keine verknüpfte "Klasse", deshalb blenden wir sie nur bei Klassen-Filter "all" ein,
// oder wenn gar nicht nach Klasse gefiltert wird, damit sie nicht verschwindet.
// Falls sie bei aktiver Klassensuche komplett verschwinden soll, passe die Bedingung an:
const classMatch = klasse === 'all';
const show = searchMatch && statusMatch && damageMatch && classMatch;
row.style.display = show ? '' : 'none';
if (show) visibleDamaged++;
});
@@ -674,6 +772,7 @@
}
searchInput.addEventListener('input', applyFilters);
classFilter.addEventListener('change', applyFilters); // NEU
statusFilter.addEventListener('change', applyFilters);
damageFilter.addEventListener('change', applyFilters);
applyFilters();
+60 -5
View File
@@ -1,6 +1,6 @@
{% extends "base.html" %}
{% block title %}Rechnungen Element - {{ APP_VERSION }}{% endblock %}
{% block title %}Rechnungs- & Schadenshistorie - {{ APP_VERSION }}{% endblock %}
{% block content %}
<style>
@@ -39,6 +39,14 @@
flex-wrap: wrap;
}
.section-header {
font-size: 1.25rem;
font-weight: 700;
color: #1e293b;
margin: 36px 0 16px 0;
padding-left: 4px;
}
.invoice-card {
background: var(--ui-surface);
border: 1px solid #e2e8f0;
@@ -115,7 +123,7 @@
<div class="invoice-history-shell">
<div class="invoice-history-head">
<h1>Rechnungshistorie pro Element</h1>
<h1>Rechnungs- & Schadenshistorie pro Element</h1>
<div class="head-meta">
<span><strong>Element:</strong> {{ item.name or '—' }}</span>
<span><strong>Code:</strong> {{ item.code or '—' }}</span>
@@ -124,10 +132,12 @@
</div>
<div class="head-actions">
<a class="btn btn-outline-secondary" href="{{ url_for('library_loans_admin') }}">Zur Bibliotheks-Ausleihenverwaltung</a>
<a class="btn btn-secondary" href="{{ url_for('library_admin') }}">Bibliothek öffnen</a>
<a class="btn btn-secondary" href="{{ url_for('library_view') }}">Bibliothek öffnen</a>
</div>
</div>
<!-- BEREICH: RECHNUNGEN -->
<h2 class="section-header">Ausgestellte Rechnungen</h2>
<div class="invoice-card">
{% if invoices %}
<table class="invoice-table">
@@ -137,7 +147,7 @@
<th>Betrag</th>
<th>Ausleihe</th>
<th>Status</th>
<th>Schaden</th>
<th>Schaden / Grund</th>
<th>Aktion</th>
</tr>
</thead>
@@ -184,5 +194,50 @@
<div class="empty-state">Für dieses Element wurden noch keine Rechnungen gespeichert.</div>
{% endif %}
</div>
<!-- BEREICH: SCHÄDEN -->
<h2 class="section-header">Gemeldete Schäden</h2>
<div class="invoice-card">
{% if damages %}
<table class="invoice-table">
<thead>
<tr>
<th>Datum</th>
<th>Quelle</th>
<th>Nutzer / Verursacher</th>
<th>Beschreibung</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{% for row in damages %}
<tr>
<td>
<div class="muted">{{ row.date }}</div>
</td>
<td>
<div class="mono">{{ row.source }}</div>
{% if row.borrow_id %}
<div class="muted" style="font-size: 0.8rem;">ID: <span title="{{ row.borrow_id }}">{{ row.borrow_id[:6] }}...</span></div>
{% endif %}
</td>
<td>{{ row.user }}</td>
<td>{{ row.description }}</td>
<td>
<!-- Einfache Logik, um bei reparierten Objekten einen grünen Badge zu zeigen -->
{% if row.status|lower in ['repariert', 'erledigt', 'geschlossen', 'bezahlt'] %}
<span class="badge-pill badge-completed">{{ row.status }}</span>
{% else %}
<span class="badge-pill badge-open">{{ row.status }}</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty-state">Für dieses Element wurden bislang keine Schäden erfasst.</div>
{% endif %}
</div>
</div>
{% endblock %}
{% endblock %}
+256
View File
@@ -0,0 +1,256 @@
{% extends "base.html" %}
{% block title %}Mahnungen & Überfällige Ausleihen{% endblock %}
{% block content %}
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- ==========================================
CSS STYLES
========================================== -->
<style>
:root {
--bg-color: #f4f7f6;
--text-color: #333;
--primary: #4a90e2;
--danger: #dc3545;
--warning: #ffc107;
--info: #0dcaf0;
--success: #198754;
--border-color: #dee2e6;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
margin: 0;
padding: 20px;
}
.container {
width: 100%;
max-width: 1200px;
margin: 0 auto;
}
.header-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.header-bar h2 {
margin: 0;
color: #2c3e50;
}
/* Suchfeld Styling */
.search-box {
padding: 10px;
width: 100%;
max-width: 300px;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 1rem;
margin-bottom: 20px;
}
/* Card & Table Styling */
.card {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
padding: 20px;
overflow-x: auto;
}
.table {
width: 100%;
border-collapse: collapse;
text-align: left;
}
.table th, .table td {
padding: 12px 15px;
border-bottom: 1px solid var(--border-color);
}
.table th {
background-color: #f8f9fa;
font-weight: 600;
color: #495057;
}
.table tbody tr:hover {
background-color: #f8f9fa;
}
/* Badges */
.badge {
display: inline-block;
padding: 5px 10px;
font-size: 0.85em;
font-weight: bold;
border-radius: 20px;
color: #fff;
text-align: center;
}
.bg-danger { background-color: var(--danger); }
.bg-warning { background-color: var(--warning); color: #000; }
.bg-info { background-color: var(--info); color: #000; }
.bg-success { background-color: var(--success); }
.text-danger {
color: var(--danger);
font-weight: bold;
}
/* Buttons */
.btn {
padding: 10px 15px;
border-radius: 4px;
text-decoration: none;
font-weight: bold;
color: #fff;
background-color: #6c757d;
border: none;
cursor: pointer;
transition: background 0.2s;
}
.btn:hover { background-color: #5a6268; }
/* Alert */
.alert-success {
background-color: #d1e7dd;
color: #0f5132;
padding: 15px;
border-radius: 4px;
text-align: center;
border: 1px solid #badbcc;
}
</style>
</head>
<body>
<!-- ==========================================
HTML CONTENT (JINJA2)
========================================== -->
<div class="container">
<div class="header-bar">
<h2>🚨 Mahnungsübersicht</h2>
<a href="{{ url_for('library_loans_admin') }}" class="btn">Zurück zur Bibliothek</a>
</div>
<div class="card">
{% if overdue_list %}
<!-- Suchfeld für das JavaScript -->
<input type="text" id="searchInput" class="search-box" placeholder="Suche nach Nutzer, Klasse oder Gegenstand..." onkeyup="filterTable()">
<table class="table" id="mahnungTable">
<thead>
<tr>
<th>Nutzer</th>
<th>Klasse</th>
<th>Gegenstand</th>
<th>Fällig am</th>
<th>Tage drüber</th>
<th>Mahnstufe</th>
<th>Konto-Status</th>
</tr>
</thead>
<tbody>
{% for item in overdue_list %}
<tr>
<td><strong>{{ item.user }}</strong></td>
<td>{{ item.klasse }}</td>
<td>{{ item.item_name }}</td>
<td>{{ item.due_date }}</td>
<td class="text-danger">{{ item.days_overdue }} Tage</td>
<td>
{% if item.mahnstufe == 2 %}
<span class="badge bg-danger">Stufe 2 (Gesperrt)</span>
{% elif item.mahnstufe == 1 %}
<span class="badge bg-warning">Stufe 1 (Gewarnt)</span>
{% else %}
<span class="badge bg-info">Stufe 0 (Neu)</span>
{% endif %}
</td>
<td>
{% if item.is_blocked %}
<span class="badge bg-danger">Gesperrt</span>
{% else %}
<span class="badge bg-success">Aktiv</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="alert-success">
<strong>Hervorragend!</strong> Aktuell gibt es keine überfälligen Ausleihen im System.
</div>
{% endif %}
</div>
</div>
<!-- ==========================================
JAVASCRIPT
========================================== -->
<script>
/**
* Funktion: filterTable()
* Liest die Eingabe aus dem Suchfeld und filtert die Tabellenzeilen live.
* Durchsucht Nutzer, Klassen und Gegenstände.
*/
function filterTable() {
// Hole den Wert aus dem Eingabefeld und wandle ihn in Kleinbuchstaben um
let input = document.getElementById("searchInput");
let filter = input.value.toLowerCase();
// Hole die Tabelle und alle Zeilen (tr) aus dem tbody
let table = document.getElementById("mahnungTable");
let tr = table.getElementsByTagName("tbody")[0].getElementsByTagName("tr");
// Durchlaufe alle Zeilen
for (let i = 0; i < tr.length; i++) {
// Hole die Zellen für Nutzer (0), Klasse (1) und Gegenstand (2)
let tdUser = tr[i].getElementsByTagName("td")[0];
let tdClass = tr[i].getElementsByTagName("td")[1];
let tdItem = tr[i].getElementsByTagName("td")[2];
if (tdUser || tdClass || tdItem) {
// Extrahiere den Text
let textUser = tdUser.textContent || tdUser.innerText;
let textClass = tdClass.textContent || tdClass.innerText;
let textItem = tdItem.textContent || tdItem.innerText;
// Prüfe, ob der Suchbegriff in einer der drei Spalten vorkommt
if (
textUser.toLowerCase().indexOf(filter) > -1 ||
textClass.toLowerCase().indexOf(filter) > -1 ||
textItem.toLowerCase().indexOf(filter) > -1
) {
tr[i].style.display = ""; // Zeile anzeigen
} else {
tr[i].style.display = "none"; // Zeile ausblenden
}
}
}
}
</script>
</body>
</html>
{% endblock %}
+15 -15
View File
@@ -1531,22 +1531,22 @@
}
const isbnField = document.getElementById('isbn');
const infoContainer = document.getElementById('book-info-container');
if (!isbnField || !infoContainer) {
alert('Fehler: Erforderliche Formularelemente nicht gefunden.');
return;
}
const isbn = normalizeIsbnClient(isbnField.value);
if (!isbn) {
infoContainer.innerHTML = '<div class="error-message">Bitte geben Sie eine ISBN oder einen Barcode ein.</div>';
return;
}
// Show loading indicator
infoContainer.innerHTML = '<div class="loading-spinner">Informationen werden abgerufen...</div>';
// Make API request to fetch book data
fetch(`/fetch_book_info/${isbn}`)
.then(response => {
@@ -1564,10 +1564,10 @@
infoContainer.innerHTML = `<div class="error-message">${data.error}</div>`;
return;
}
// Store book data globally for import functionality
currentBookData = data;
// Display book information with import button
infoContainer.innerHTML = `
<div class="book-info">
@@ -1602,23 +1602,23 @@
alert('Keine Buchdaten verfügbar zum Import.');
return;
}
// Get form fields
const nameField = document.getElementById('name');
const descriptionField = document.getElementById('beschreibung');
const priceField = document.getElementById('anschaffungskosten'); // <-- NEU
if (!nameField || !descriptionField) {
alert('Fehler: Formularfelder nicht gefunden.');
return;
}
// Generate book title
let bookTitle = currentBookData.title || '';
if (currentBookData.authors && currentBookData.authors !== 'Unknown Author') {
bookTitle += ` - ${currentBookData.authors}`;
}
// Generate description
let description = '';
if (currentBookData.description && currentBookData.description !== 'No description available') {
@@ -1639,11 +1639,11 @@
description += `\nSeiten: ${currentBookData.pageCount}`;
}
}
// Import the data into form fields
nameField.value = bookTitle;
descriptionField.value = description;
// Preis in das Formularfeld eintragen
if (priceField && currentBookData.price !== null && currentBookData.price !== undefined) {
// Wandelt den Float (z.B. 12.25) in einen String mit Komma (12,25) um
@@ -1655,14 +1655,14 @@
if (currentBookData.thumbnail) {
downloadBookCover(currentBookData.thumbnail);
}
// Show success message
const infoContainer = document.getElementById('book-info-container');
const successMessage = document.createElement('div');
successMessage.className = 'import-success-message';
successMessage.textContent = 'Buchdaten erfolgreich in das Formular übernommen!';
infoContainer.appendChild(successMessage);
// Remove success message after 3 seconds
setTimeout(() => {
if (successMessage.parentNode) {