Compare commits

...

9 Commits

Author SHA1 Message Date
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
4 changed files with 380 additions and 122 deletions
+238 -68
View File
@@ -3312,13 +3312,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'):
@@ -3337,7 +3341,6 @@ def library_loans_admin():
return _format_money_value(value)
def safe_decrypt(val):
"""Helper to decrypt strings that start with enc:: or standard encrypted strings."""
if not val or not isinstance(val, str):
return val or ''
try:
@@ -3345,19 +3348,48 @@ def library_loans_admin():
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']
users_col = db['users']
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())
@@ -3370,42 +3402,29 @@ def library_loans_admin():
'Klasse': 1, 'klasse': 1, 'Class': 1, 'school_class': 1}
).sort('Start', -1))
# class_map aufbauen (wie zuvor besprochen)
class_map = {}
student_cards = list(student_cards_col.find({}, {'SchülerName': 1, 'Klasse': 1, 'AusweisId': 1}))
for sc in student_cards:
dec_cls = safe_decrypt(sc.get('Klasse'))
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
dec_name = safe_decrypt(sc.get('SchülerName'))
raw_name = sc.get('SchülerName')
ausweis_id = sc.get('AusweisId')
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
class_map[dec_name] = dec_cls
if raw_name:
class_map[raw_name] = dec_cls
if ausweis_id:
class_map[str(ausweis_id).strip().lower()] = dec_cls
users = list(users_col.find({}, {'Username': 1, 'Name': 1, 'User': 1, 'Klasse': 1, 'klasse': 1, 'Class': 1,
'school_class': 1}))
for u in users:
raw_cls = u.get('Klasse') or u.get('klasse') or u.get('Class') or u.get('school_class') or ''
dec_cls = safe_decrypt(raw_cls)
if not dec_cls:
continue
for key in [u.get('Username'), u.get('Name'), u.get('User')]:
if key:
dec_key = safe_decrypt(key)
class_map[key] = dec_cls
class_map[dec_key] = dec_cls
class_map[dec_key.strip().lower()] = dec_cls
active_item_ids = set()
loan_entries = []
for record in active_records:
item_id = str(record.get('Item') or '')
if item_id and record.get('Status') == 'active':
@@ -3417,26 +3436,24 @@ def library_loans_admin():
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 = safe_decrypt(raw_user)
direct_class = (
user_class = (
safe_decrypt(record.get('Klasse'))
or safe_decrypt(record.get('klasse'))
or safe_decrypt(record.get('Class'))
or safe_decrypt(record.get('school_class'))
)
user_class = (
direct_class
or class_map.get(decrypted_user)
or class_map.get(decrypted_user.strip().lower())
or class_map.get(raw_user)
or ''
or class_map.get(raw_user, '')
or class_map.get(decrypted_user, '')
or class_map.get(decrypted_user.strip().lower(), '')
or ''
)
loan_entries.append({
@@ -3447,7 +3464,7 @@ 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')),
@@ -3455,24 +3472,32 @@ def library_loans_admin():
'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', ''),
@@ -3480,11 +3505,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(
@@ -9214,7 +9241,7 @@ def resolve_repaired_item_funct(item_id, action, new_code_4="", current_user="ad
"""
Verarbeitet Items aus der Reparatur und aktualisiert die Serien-Counts
sowie Parent/Child-Abhängigkeiten beim Löschen korrekt.
Entfernt Schadens-Flags komplett aus Items und zugehörigen Ausleihen.
Verschiebt gemeldete Schäden in die Historie (DamageRepairs) bei Reparatur/Austausch.
"""
if not ObjectId.is_valid(item_id):
return False, "Ungültige Item-ID."
@@ -9231,19 +9258,24 @@ 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()
damage_unset = {
'HasDamage': "",
legacy_damage_unset = {
'has_damage': "",
'DamageCount': "",
'damage_count': "",
'DamageText': "",
'damage_text': "",
'is_damaged': "",
'IsDamaged': "",
'IsDamaged': ""
}
borrowings_unset = legacy_damage_unset.copy()
borrowings_unset.update({
'HasDamage': "",
'Condition': "",
'DamageReports': ""
}
})
# --- OPTION 1: DELETE ---
if action == 'delete':
@@ -9285,24 +9317,36 @@ def resolve_repaired_item_funct(item_id, action, new_code_4="", current_user="ad
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)},
{
'$push': {'DamageRepairs': repair_entry},
'$set': {
'Code_4': str(new_code_4).strip(),
'Verfuegbar': True,
'LastUpdated': datetime.datetime.now()
'HasDamage': False,
'Condition': 'gut',
'DamageReports': [],
'LastUpdated': now
},
'$unset': damage_unset
'$unset': legacy_damage_unset
}
)
if borrowings is not None:
borrowings.update_many(
{'$or': [{'item_id': str(item_id)}, {'item_id': ObjectId(item_id)}]},
{'$unset': damage_unset}
{'$unset': borrowings_unset}
)
return True, f"Item wurde ersetzt. Neuer Code 4 zugewiesen: {new_code_4}."
@@ -9312,18 +9356,22 @@ def resolve_repaired_item_funct(item_id, action, new_code_4="", current_user="ad
items.update_one(
{'_id': ObjectId(item_id)},
{
'$push': {'DamageRepairs': repair_entry},
'$set': {
'Verfuegbar': True,
'LastUpdated': datetime.datetime.now()
'HasDamage': False,
'Condition': 'gut',
'DamageReports': [],
'LastUpdated': now
},
'$unset': damage_unset
'$unset': legacy_damage_unset
}
)
if borrowings is not None:
borrowings.update_many(
{'$or': [{'item_id': str(item_id)}, {'item_id': ObjectId(item_id)}]},
{'$unset': damage_unset}
{'$unset': borrowings_unset}
)
return True, "Item wurde repariert und ist wieder regulär verfügbar."
@@ -9335,9 +9383,10 @@ def resolve_repaired_item_funct(item_id, action, new_code_4="", current_user="ad
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!',
@@ -9373,6 +9422,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]},
@@ -9403,7 +9455,9 @@ def library_item_invoices(item_id):
entries.append({
'borrow_id': str(borrow_doc.get('_id')),
'borrow_status': borrow_doc.get('Status', ''),
'borrow_user': decrypt_text(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'),
@@ -9419,6 +9473,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={
@@ -9429,14 +9598,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:
+5 -5
View File
@@ -299,7 +299,7 @@
<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>
@@ -322,9 +322,9 @@
{% 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>
@@ -363,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?');">
+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 %}
+77 -44
View File
@@ -1523,30 +1523,48 @@
document.getElementById('new-location-input').value = '';
}
// Function to fetch book information from ISBN
function fetchBookInfo(formType) {
if (!libraryModuleEnabled) {
// 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) {
alert('Bibliotheks-Modul ist deaktiviert.');
return;
}
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);
// Fallback, falls normalizeIsbnClient nicht geladen wurde
const isbn = typeof normalizeIsbnClient === 'function' ? normalizeIsbnClient(isbnField.value) : isbnField.value.trim();
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>';
// Make API request to fetch book data
fetch(`/fetch_book_info/${isbn}`)
.then(response => {
@@ -1562,63 +1580,66 @@
.then(data => {
if (data.error) {
infoContainer.innerHTML = `<div class="error-message">${data.error}</div>`;
currentFetchedIsbn = null; // Bei Fehler zurücksetzen
return;
}
// Store book data globally for import functionality
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="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="window.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
function importBookInfo(formType) {
// Function to import book information into form (an window gebunden)
window.importBookInfo = function(formType) {
if (!currentBookData) {
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
const priceField = document.getElementById('anschaffungskosten');
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 +1660,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
@@ -1652,24 +1673,36 @@
}
// Download and import book cover image if available
// Sicherstellen, dass die Funktion nicht crasht, falls sie auf der Seite fehlt
if (currentBookData.thumbnail) {
downloadBookCover(currentBookData.thumbnail);
if (typeof downloadBookCover === 'function') {
try {
downloadBookCover(currentBookData.thumbnail);
} catch(e) {
console.warn('Cover konnte nicht geladen werden:', e);
}
}
}
// 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!';
infoContainer.appendChild(successMessage);
// Remove success message after 3 seconds
setTimeout(() => {
if (successMessage.parentNode) {
successMessage.parentNode.removeChild(successMessage);
}
}, 3000);
}
};
function downloadBookCover(imageUrl) {
if (!imageUrl) {