feat(inventory): add repair resolution workflow and invoice correction logic
Release Inventarsystem / release-docker (push) Successful in 2m14s

- Add repair resolution mechanism (`resolve_repaired_item`) allowing users to:
  * Just repair: Return item to normal state (`Verfuegbar: True`)
  * Replace: Update `Code_4` (inventory/barcode) and restore availability
  * Delete: Remove item and update series counts (`SeriesCount`) and parent/child references
- Support `Code_4` updates in `update_item` for specific physical items
- Implement invoice correction route (`admin_add_invoice_correction`) with audit logging
- Add modal UI in library admin view to handle repair actions and code replacements
This commit is contained in:
2026-08-19 22:05:32 +02:00
parent 78c215b234
commit cd03bbd2e5
2 changed files with 279 additions and 122 deletions
+189 -71
View File
@@ -6743,7 +6743,6 @@ def mark_damage_repaired(id):
if result.matched_count == 0: if result.matched_count == 0:
return jsonify({'success': False, 'message': 'Objekt nicht gefunden.'}), 404 return jsonify({'success': False, 'message': 'Objekt nicht gefunden.'}), 404
# Best-effort system log entry for repair action
try: try:
logs_collection = db['system_logs'] logs_collection = db['system_logs']
logs_collection.insert_one({ logs_collection.insert_one({
@@ -8840,132 +8839,145 @@ def admin_mark_invoice_paid(borrow_id):
client.close() client.close()
@app.route('/admin/borrowings/<borrow_id>/invoice/finalize', methods=['POST']) @app.route('/admin/borrowings/<borrow_id>/invoice/pay', methods=['POST'])
def admin_finalize_invoice_and_repair(borrow_id): def admin_mark_invoice_paid(borrow_id):
"""Mark invoice as paid and item as repaired in one action.""" """Mark invoice as paid."""
if 'username' not in session: 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, versuchen sie es erneut nach Anmeldung!', 'error')
return redirect(url_for('login')) return redirect(url_for('login'))
current_permissions = us.get_effective_permissions(session['username']) current_permissions = us.get_effective_permissions(session['username'])
if not current_permissions['pages'].get('admin_borrowings', False): if not current_permissions['pages'].get('admin_borrowings', False):
flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error') flash('Ihnen fehlen die nötigen Berechtigungen.', 'error')
return redirect(url_for('home_admin')) return redirect(request.referrer or url_for('home_admin'))
client = None client = None
try: try:
client = MongoClient(MONGODB_HOST, MONGODB_PORT) client = MongoClient(MONGODB_HOST, MONGODB_PORT)
db = client[MONGODB_DB] db = client[MONGODB_DB]
ausleihungen = db['ausleihungen'] ausleihungen = db['ausleihungen']
items_col = db['items']
borrow_doc = ausleihungen.find_one({'_id': ObjectId(borrow_id)}) borrow_doc = ausleihungen.find_one({'_id': ObjectId(borrow_id)})
if not borrow_doc: if not borrow_doc:
flash('Ausleihung nicht gefunden.', 'error') flash('Ausleihung nicht gefunden.', 'error')
return redirect(url_for('admin_borrowings')) return redirect(request.referrer or url_for('admin_borrowings'))
invoice_data = borrow_doc.get('InvoiceData') or {} invoice_data = borrow_doc.get('InvoiceData') or {}
if not invoice_data: if not invoice_data:
flash('Für diese Ausleihung existiert keine Rechnung.', 'warning') flash('Für diese Ausleihung existiert keine Rechnung.', 'warning')
return redirect(url_for('admin_borrowings')) return redirect(request.referrer or url_for('admin_borrowings'))
item_doc = None
item_id = borrow_doc.get('Item')
if item_id:
try:
item_doc = items_col.find_one({'_id': ObjectId(item_id)})
except Exception:
item_doc = items_col.find_one({'_id': item_id})
now = datetime.datetime.now() now = datetime.datetime.now()
update_fields = {'LastUpdated': now}
update_fields = { # Rechnung auf bezahlt setzen
'LastUpdated': now,
}
if invoice_data.get('paid') is not True: if invoice_data.get('paid') is not True:
update_fields.update({ update_fields.update({
'InvoiceData.paid': True, 'InvoiceData.paid': True,
'InvoiceData.paid_at': now, 'InvoiceData.paid_at': now,
'InvoiceData.paid_by': session.get('username', ''), 'InvoiceData.paid_by': session.get('username', ''),
}) })
# Ausleihe abschließen, falls noch aktiv
if borrow_doc.get('Status') == 'active': if borrow_doc.get('Status') == 'active':
update_fields['Status'] = 'completed' update_fields['Status'] = 'completed'
update_fields['End'] = now update_fields['End'] = now
ausleihungen.update_one({'_id': borrow_doc['_id']}, {'$set': update_fields}) ausleihungen.update_one({'_id': borrow_doc['_id']}, {'$set': update_fields})
repaired = False # Logging
resolved_count = 0
if item_doc:
open_reports = item_doc.get('DamageReports', []) or []
resolved_count = len(open_reports)
item_update = {
'$set': {
'DamageReports': [],
'HasDamage': False,
'Verfuegbar': True,
'LastUpdated': now,
},
'$unset': {
'User': '',
'Condition': '',
},
}
if open_reports:
repair_entry = {
'repaired_by': session['username'],
'repaired_at': now,
'resolved_reports': open_reports,
}
item_update['$push'] = {'DamageRepairs': {'$each': [repair_entry], '$position': 0}}
item_result = items_col.update_one({'_id': item_doc['_id']}, item_update)
repaired = item_result.matched_count > 0
try: try:
logs_collection = db['system_logs'] db['system_logs'].insert_one({
logs_collection.insert_one({ 'type': 'invoice_paid',
'type': 'damage_invoice_finalize',
'timestamp': now.isoformat(), 'timestamp': now.isoformat(),
'user': session.get('username'), 'user': session.get('username'),
'borrow_id': borrow_id, 'borrow_id': borrow_id,
'item_id': str(item_doc.get('_id')) if item_doc else '',
'invoice_number': invoice_data.get('invoice_number', ''), 'invoice_number': invoice_data.get('invoice_number', ''),
'amount': invoice_data.get('amount'), 'amount': invoice_data.get('amount'),
'repaired': repaired,
'resolved_damage_reports': resolved_count,
'ip': request.remote_addr, 'ip': request.remote_addr,
}) })
except Exception as log_err: except Exception as log_err:
app.logger.warning(f"Damage invoice finalize log write failed for borrow {borrow_id}: {log_err}") app.logger.warning(f"Log failed: {log_err}")
_append_audit_event_standalone( _append_audit_event_standalone(
event_type='invoice_finalized_and_repaired', event_type='invoice_paid',
payload={ payload={
'borrow_id': borrow_id, 'borrow_id': borrow_id,
'item_id': str(item_doc.get('_id')) if item_doc else '',
'invoice_number': invoice_data.get('invoice_number', ''), 'invoice_number': invoice_data.get('invoice_number', ''),
'amount': invoice_data.get('amount'), 'amount': invoice_data.get('amount'),
'repaired': repaired,
'resolved_damage_reports': resolved_count,
} }
) )
if repaired: flash('Rechnung erfolgreich als bezahlt markiert.', 'success')
flash('Rechnung als bezahlt markiert und Element als repariert abgeschlossen.', 'success') return redirect(request.referrer or url_for('library_loans_admin'))
else:
flash('Rechnung als bezahlt markiert. Element konnte nicht repariert werden (nicht gefunden).', 'warning')
return redirect(url_for('admin_borrowings'))
except Exception as e: except Exception as e:
app.logger.error(f"Error finalizing invoice/repair for borrow {borrow_id}: {e}") app.logger.error(f"Error marking invoice paid for borrow {borrow_id}: {e}")
flash('Fehler beim Kombinieren von bezahlt und repariert.', 'error') flash('Fehler beim Markieren als bezahlt.', 'error')
return redirect(url_for('admin_borrowings')) return redirect(request.referrer or url_for('library_loans_admin'))
finally: finally:
if client: if client:
client.close() client.close()
@app.route('/admin/items/<item_id>/repair', methods=['POST'])
def mark_damage_repaired(item_id):
"""Mark item as repaired and make it available again."""
if 'username' not in session:
return redirect(url_for('login'))
client = None
try:
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
db = client[MONGODB_DB]
items_col = db['items']
try:
obj_id = ObjectId(item_id)
except Exception:
obj_id = item_id
item_doc = items_col.find_one({'_id': obj_id})
if not item_doc:
flash('Element nicht gefunden.', 'error')
return redirect(request.referrer or url_for('library_loans_admin'))
now = datetime.datetime.now()
open_reports = item_doc.get('DamageReports', []) or []
item_update = {
'$set': {
'DamageReports': [],
'HasDamage': False,
'Verfuegbar': True,
'LastUpdated': now,
},
'$unset': {
'User': '',
'Condition': '',
},
}
# Historie der Reparaturen speichern
if open_reports:
repair_entry = {
'repaired_by': session['username'],
'repaired_at': now,
'resolved_reports': open_reports,
}
item_update['$push'] = {'DamageRepairs': {'$each': [repair_entry], '$position': 0}}
items_col.update_one({'_id': item_doc['_id']}, item_update)
flash('Element wurde erfolgreich als repariert markiert und ist wieder verfügbar.', 'success')
return redirect(request.referrer or url_for('library_loans_admin'))
except Exception as e:
app.logger.error(f"Error repairing item {item_id}: {e}")
flash('Fehler bei der Reparatur.', 'error')
return redirect(request.referrer or url_for('library_loans_admin'))
finally:
if client:
client.close()
@app.route('/admin/borrowings/<borrow_id>/invoice/pdf', methods=['GET']) @app.route('/admin/borrowings/<borrow_id>/invoice/pdf', methods=['GET'])
def admin_view_invoice_pdf(borrow_id): def admin_view_invoice_pdf(borrow_id):
"""View a previously created invoice PDF for a borrowing.""" """View a previously created invoice PDF for a borrowing."""
@@ -9026,15 +9038,19 @@ def admin_view_invoice_pdf(borrow_id):
def admin_add_invoice_correction(borrow_id): def admin_add_invoice_correction(borrow_id):
"""Append an invoice correction entry without mutating the original invoice body.""" """Append an invoice correction entry without mutating the original invoice body."""
if 'username' not in session: 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('Bitte melden Sie sich an, um auf diese Seite zuzugreifen.', 'error')
return redirect(url_for('login')) return redirect(url_for('login'))
current_permissions = us.get_effective_permissions(session['username']) current_permissions = us.get_effective_permissions(session['username'])
if not current_permissions['pages'].get('admin_borrowings', False): if not current_permissions['pages'].get('admin_borrowings', False):
flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error') flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error')
return redirect(url_for('home_admin')) return redirect(url_for('home_admin'))
if not ObjectId.is_valid(borrow_id):
flash('Ungültige Ausleih-ID.', 'error')
return redirect(url_for('admin_borrowings'))
client = None client = None
try: try:
client = MongoClient(MONGODB_HOST, MONGODB_PORT) client = MongoClient(MONGODB_HOST, MONGODB_PORT)
@@ -9064,6 +9080,7 @@ def admin_add_invoice_correction(borrow_id):
now = datetime.datetime.now() now = datetime.datetime.now()
correction_number = f"CORR-{now.strftime('%Y%m%d-%H%M%S')}-{str(borrow_doc.get('_id'))[-6:].upper()}" correction_number = f"CORR-{now.strftime('%Y%m%d-%H%M%S')}-{str(borrow_doc.get('_id'))[-6:].upper()}"
correction_entry = { correction_entry = {
'correction_number': correction_number, 'correction_number': correction_number,
'reason': correction_reason, 'reason': correction_reason,
@@ -9095,6 +9112,7 @@ def admin_add_invoice_correction(borrow_id):
flash('Korrekturbuchung wurde revisionssicher ergänzt.', 'success') flash('Korrekturbuchung wurde revisionssicher ergänzt.', 'success')
return redirect(url_for('admin_borrowings')) return redirect(url_for('admin_borrowings'))
except Exception as e: except Exception as e:
app.logger.error(f"Error creating invoice correction for borrow {borrow_id}: {e}") app.logger.error(f"Error creating invoice correction for borrow {borrow_id}: {e}")
flash('Fehler beim Anlegen der Korrekturbuchung.', 'error') flash('Fehler beim Anlegen der Korrekturbuchung.', 'error')
@@ -9104,6 +9122,106 @@ def admin_add_invoice_correction(borrow_id):
client.close() client.close()
def resolve_repaired_item(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.
"""
if not ObjectId.is_valid(item_id):
return False, "Ungültige Item-ID."
try:
with MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) as client:
db = client[cfg.MONGODB_DB]
items = db['items']
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')
# --- OPTION 1: DELETE ---
if action == 'delete':
is_parent = not item.get('IsGroupedSubItem')
if is_parent and series_group_id:
sibling = items.find_one({
'SeriesGroupId': series_group_id,
'_id': {'$ne': ObjectId(item_id)},
'Deleted': {'$ne': True}
})
if sibling:
new_parent_id_str = str(sibling['_id'])
# 1. Mache das Geschwister-Item zum neuen Parent
items.update_one(
{'_id': sibling['_id']},
{'$set': {
'IsGroupedSubItem': False,
'ParentItemId': None
}}
)
# 2. Leite alle anderen verbleibenden Sub-Items auf den neuen Parent um
items.update_many(
{
'SeriesGroupId': series_group_id,
'_id': {'$nin': [ObjectId(item_id), sibling['_id']]}
},
{'$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},
{'$inc': {'SeriesCount': -1}}
)
return True, "Item wurde gelöscht und der Bestand aller Serien-Items angepasst."
# --- OPTION 2: REPLACE ---
elif action == 'replace':
items.update_one(
{'_id': ObjectId(item_id)},
{'$set': {
'Code_4': str(new_code_4).strip(),
'Verfuegbar': True,
'LastUpdated': datetime.datetime.now()
}}
)
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()
}}
)
return True, "Item wurde repariert und ist wieder regulär verfügbar."
else:
return False, "Ungültige Aktion ausgewählt."
except Exception as e:
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']) @app.route('/admin/library/items/<item_id>/invoices', methods=['GET'])
def library_item_invoices(item_id): def library_item_invoices(item_id):
"""Show all stored invoices for one specific library item.""" """Show all stored invoices for one specific library item."""
+90 -51
View File
@@ -154,6 +154,7 @@
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 8px; gap: 8px;
align-items: center;
} }
.row-actions form { .row-actions form {
@@ -196,7 +197,7 @@
<div class="library-admin-hero"> <div class="library-admin-hero">
<div> <div>
<h1>Bibliotheks-Ausleihen</h1> <h1>Bibliotheks-Ausleihen</h1>
<p>Nur Bibliotheksmedien. Hier kannst du offene Ausleihen abschließen, Rechnungen bezahlen und defekte Medien direkt zurücksetzen.</p> <p>Nur Bibliotheksmedien. Hier kannst du offene Ausleihen abschließen, Rechnungen bezahlen und defekte Medien verwalten.</p>
</div> </div>
<div class="hero-actions"> <div class="hero-actions">
<a class="btn btn-secondary" href="{{ url_for('library_view') }}">Bibliothek öffnen</a> <a class="btn btn-secondary" href="{{ url_for('library_view') }}">Bibliothek öffnen</a>
@@ -215,7 +216,7 @@
<div class="value">{{ damaged_items|length }}</div> <div class="value">{{ damaged_items|length }}</div>
</div> </div>
<div class="summary-card"> <div class="summary-card">
<span class="label">Direkt reparierbar</span> <span class="label">Direkt bearbeitbar</span>
<div class="value">{{ damaged_items|length }}</div> <div class="value">{{ damaged_items|length }}</div>
</div> </div>
</div> </div>
@@ -311,35 +312,29 @@
{% if e.status == 'active' %} {% if e.status == 'active' %}
<button type="button" class="btn btn-outline-danger btn-sm" onclick="openDamageReportPrompt(this)">Schaden melden</button> <button type="button" class="btn btn-outline-danger btn-sm" onclick="openDamageReportPrompt(this)">Schaden melden</button>
{% endif %} {% endif %}
{% if e.invoice_number and ((not e.invoice_paid) or e.has_damage) %}
<form method="post" action="{{ url_for('admin_finalize_invoice_and_repair', borrow_id=e.id) }}" onsubmit="return confirm('Rechnung als bezahlt und Element als repariert markieren?');"> {% if e.invoice_number and not e.invoice_paid %}
<button type="submit" class="btn btn-success btn-sm"> <form method="post" action="{{ url_for('admin_mark_invoice_paid', borrow_id=e.id) }}" onsubmit="return confirm('Rechnung als bezahlt markieren?');">
{% if not e.invoice_paid and e.has_damage %} <button type="submit" class="btn btn-success btn-sm">Als bezahlt markieren</button>
Bezahlt + repariert
{% elif not e.invoice_paid %}
Als bezahlt markieren
{% else %}
Als repariert markieren
{% endif %}
</button>
</form>
{% elif e.has_damage %}
<form method="post" action="{{ url_for('mark_damage_repaired', id=e.item_id) }}" onsubmit="return confirm('Medium als repariert markieren?');">
<button type="submit" class="btn btn-success btn-sm">Als repariert markieren</button>
</form> </form>
{% endif %} {% endif %}
{% if e.has_damage %}
<!-- Aufruf des Reparatur-Auswahl-Modals -->
<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?');"> <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" placeholder="Korrekturgrund" required style="padding:6px; border:1px solid #ddd; border-radius:6px; min-width:140px;"> <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="Delta optional" style="padding:6px; border:1px solid #ddd; border-radius:6px; width:120px;"> <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> <button type="submit" class="btn btn-outline-danger btn-sm">Korrektur</button>
</form> </form>
{% endif %} {% endif %}
{% if e.status in ['active', 'planned'] %} {% 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?');"> <form method="post" action="{{ url_for('admin_reset_borrowing', borrow_id=e.id) }}" onsubmit="return confirm('Ausleihe zurücksetzen?');">
<button type="submit" class="btn btn-warning btn-sm">Zurücksetzen</button> <button type="submit" class="btn btn-secondary btn-sm">Zurücksetzen</button>
</form> </form>
{% endif %} {% endif %}
</div> </div>
@@ -392,9 +387,8 @@
<div class="row-actions" style="margin-bottom:8px;"> <div class="row-actions" style="margin-bottom:8px;">
<a class="btn btn-outline-secondary btn-sm" href="{{ url_for('library_item_invoices', item_id=item.id) }}">Rechnungen</a> <a class="btn btn-outline-secondary btn-sm" href="{{ url_for('library_item_invoices', item_id=item.id) }}">Rechnungen</a>
</div> </div>
<form method="post" action="{{ url_for('mark_damage_repaired', id=item.id) }}" onsubmit="return confirm('Dieses Medium als repariert und wieder verfügbar markieren?');"> <!-- Aufruf des Reparatur-Auswahl-Modals -->
<button type="submit" class="btn btn-success btn-sm">Als repariert markieren</button> <button type="button" class="btn btn-warning btn-sm" onclick="openRepairModal('{{ item.id }}', '{{ item.code }}')">Reparieren / Ersetzen</button>
</form>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
@@ -407,6 +401,7 @@
</div> </div>
</div> </div>
<!-- Modal: Rechnung für Schaden erstellen -->
<div id="damage-invoice-modal" role="dialog" aria-modal="true" aria-labelledby="modal-title" style="display:none; position:fixed; inset:0; background:rgba(15,23,42,0.72); z-index:9999; padding:20px; overflow:auto;"> <div id="damage-invoice-modal" role="dialog" aria-modal="true" aria-labelledby="modal-title" style="display:none; position:fixed; inset:0; background:rgba(15,23,42,0.72); z-index:9999; padding:20px; overflow:auto;">
<div style="max-width:760px; margin:40px auto; background: var(--ui-surface); border-radius:12px; padding:24px; box-shadow:0 20px 60px rgba(0,0,0,0.3);"> <div style="max-width:760px; margin:40px auto; background: var(--ui-surface); border-radius:12px; padding:24px; box-shadow:0 20px 60px rgba(0,0,0,0.3);">
<div style="display:flex; justify-content:space-between; align-items:center; gap:12px; margin-bottom:18px;"> <div style="display:flex; justify-content:space-between; align-items:center; gap:12px; margin-bottom:18px;">
@@ -464,6 +459,33 @@
</div> </div>
</div> </div>
<!-- Neues Modal: Reparatur-Optionen (Reparieren, Ersetzen mit neuem Code_4, Löschen) -->
<div id="repair-action-modal" role="dialog" aria-modal="true" aria-labelledby="repair-modal-title" style="display:none; position:fixed; inset:0; background:rgba(15,23,42,0.72); z-index:9999; padding:20px; overflow:auto;">
<div style="max-width:540px; margin:60px auto; background: var(--ui-surface); border-radius:12px; padding:24px; box-shadow:0 20px 60px rgba(0,0,0,0.3);">
<div style="display:flex; justify-content:space-between; align-items:center; gap:12px; margin-bottom:16px;">
<h2 id="repair-modal-title" style="margin:0; font-size: 1.3rem;">Medium-Bearbeitung nach Reparatur</h2>
<button type="button" class="btn btn-secondary btn-sm" onclick="closeRepairModal()">Schließen</button>
</div>
<p style="color: #666; margin-bottom: 18px; font-size: 0.95rem;">Wähle aus, wie mit diesem beschädigten Medium weiter verfahren werden soll:</p>
<form id="repair-action-form" method="post" action="">
<input type="hidden" name="action" id="repair-action-input" value="repair">
<div style="margin-bottom: 16px; display: none;" id="new-code-container">
<label for="modal-new-code-4" style="display:block; font-weight:700; margin-bottom:6px;">Neuer Code 4 (Inventarnummer / Barcode)</label>
<input type="text" id="modal-new-code-4" name="new_code_4" placeholder="Neuen Code 4 eingeben..." style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px;">
</div>
<div style="display: flex; flex-direction: column; gap: 10px;">
<button type="button" class="btn btn-success" onclick="submitRepairAction('repair')">Nur Reparieren (Wieder verfügbar machen)</button>
<button type="button" class="btn btn-primary" onclick="toggleReplaceMode()">Ersetzen (Neuen Code 4 zuweisen)</button>
<button type="submit" id="submit-replace-btn" class="btn btn-primary" style="display:none;" onclick="document.getElementById('repair-action-input').value='replace'; return confirm('Medium wirklich mit neuem Code aktualisieren?');">Änderung speichern</button>
<button type="submit" class="btn btn-danger" onclick="document.getElementById('repair-action-input').value='delete'; return confirm('Achtung: Soll dieses Medium wirklich gelöscht und der Bestand aktualisiert werden?');">Löschen & Bestand anpassen</button>
</div>
</form>
</div>
</div>
<script> <script>
(function() { (function() {
const searchInput = document.getElementById('library-search'); const searchInput = document.getElementById('library-search');
@@ -473,6 +495,7 @@
const damagedRows = Array.from(document.querySelectorAll('.damaged-row')); const damagedRows = Array.from(document.querySelectorAll('.damaged-row'));
const loansEmpty = document.getElementById('loans-empty'); const loansEmpty = document.getElementById('loans-empty');
const damagedEmpty = document.getElementById('damaged-empty'); const damagedEmpty = document.getElementById('damaged-empty');
const damageInvoiceModal = document.getElementById('damage-invoice-modal'); const damageInvoiceModal = document.getElementById('damage-invoice-modal');
const damageInvoiceForm = document.getElementById('damage-invoice-form'); const damageInvoiceForm = document.getElementById('damage-invoice-form');
const damageInvoiceItem = document.getElementById('damage-invoice-item'); const damageInvoiceItem = document.getElementById('damage-invoice-item');
@@ -482,23 +505,56 @@
const damageInvoiceReason = document.getElementById('damage-invoice-reason'); const damageInvoiceReason = document.getElementById('damage-invoice-reason');
const damageInvoiceReplaceBtn = document.getElementById('damage-invoice-replace-btn'); const damageInvoiceReplaceBtn = document.getElementById('damage-invoice-replace-btn');
// Funktionen für das Reparatur-Modal
window.openRepairModal = function(itemId, currentCode) {
const modal = document.getElementById('repair-action-modal');
const form = document.getElementById('repair-action-form');
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';
document.getElementById('modal-new-code-4').value = currentCode || '';
codeContainer.style.display = 'none';
replaceBtn.style.display = 'none';
modal.style.display = 'block';
};
window.closeRepairModal = function() {
document.getElementById('repair-action-modal').style.display = 'none';
};
window.toggleReplaceMode = function() {
const codeContainer = document.getElementById('new-code-container');
const replaceBtn = document.getElementById('submit-replace-btn');
document.getElementById('repair-action-input').value = 'replace';
codeContainer.style.display = 'block';
replaceBtn.style.display = 'block';
document.getElementById('modal-new-code-4').focus();
};
window.submitRepairAction = function(actionType) {
const form = document.getElementById('repair-action-form');
document.getElementById('repair-action-input').value = actionType;
if (confirm('Medium als regulär repariert markieren?')) {
form.submit();
}
};
function openDamageReportPrompt(button) { function openDamageReportPrompt(button) {
const row = button.closest('.loan-row'); const row = button.closest('.loan-row');
if (!row) { if (!row) return;
return;
}
const itemId = row.dataset.itemId || ''; const itemId = row.dataset.itemId || '';
const itemName = row.dataset.itemName || 'Bibliotheksmedium';
const noteInput = prompt('Schadensmeldung für dieses Bibliotheksmedium:\nNotiz zum Schaden (optional):', ''); const noteInput = prompt('Schadensmeldung für dieses Bibliotheksmedium:\nNotiz zum Schaden (optional):', '');
if (noteInput === null) { if (noteInput === null) return;
return;
}
const description = noteInput.trim() || 'Schaden erneut gemeldet'; const description = noteInput.trim() || 'Schaden erneut gemeldet';
// Visuelles Feedback: Button deaktivieren und Text ändern
const originalText = button.textContent; const originalText = button.textContent;
button.disabled = true; button.disabled = true;
button.textContent = 'Speichere...'; button.textContent = 'Speichere...';
@@ -509,7 +565,6 @@
body: JSON.stringify({ description }) body: JSON.stringify({ description })
}) })
.then(async response => { .then(async response => {
// Robustes JSON-Parsing (verhindert Absturz, falls der Server kein JSON zurückgibt)
const data = await response.json().catch(() => ({})); const data = await response.json().catch(() => ({}));
if (!response.ok || !data.success) { if (!response.ok || !data.success) {
throw new Error(data.message || 'Fehler beim Speichern der Schadensmeldung.'); throw new Error(data.message || 'Fehler beim Speichern der Schadensmeldung.');
@@ -519,18 +574,14 @@
.then(data => { .then(data => {
if (confirm('Schaden gespeichert. Soll direkt eine Rechnung erstellt werden?')) { if (confirm('Schaden gespeichert. Soll direkt eine Rechnung erstellt werden?')) {
openDamageInvoiceModal(row, description); openDamageInvoiceModal(row, description);
// Button wieder zurücksetzen, da die Seite nicht neu geladen wird
button.disabled = false; button.disabled = false;
button.textContent = originalText; button.textContent = originalText;
return; return;
} }
// Bei "Abbrechen" im Confirm -> Neuladen der Tabelle
window.location.reload(); window.location.reload();
}) })
.catch(error => { .catch(error => {
alert(error.message || 'Ein unbekannter Fehler ist aufgetreten.'); alert(error.message || 'Ein unbekannter Fehler ist aufgetreten.');
// Fehlerbehandlung: Button wieder aktiv schalten
button.disabled = false; button.disabled = false;
button.textContent = originalText; button.textContent = originalText;
}); });
@@ -539,17 +590,12 @@
window.openDamageReportPrompt = openDamageReportPrompt; window.openDamageReportPrompt = openDamageReportPrompt;
function openDamageInvoiceModal(row, description) { function openDamageInvoiceModal(row, description) {
if (!damageInvoiceModal || !damageInvoiceForm) { if (!damageInvoiceModal || !damageInvoiceForm) return;
console.error("Modal oder Formular nicht gefunden.");
return;
}
const borrowId = row.dataset.borrowId || ''; const borrowId = row.dataset.borrowId || '';
const itemName = row.dataset.itemName || ''; const itemName = row.dataset.itemName || '';
const borrower = row.dataset.userName || ''; const borrower = row.dataset.userName || '';
const itemCode = row.dataset.itemCode || ''; const itemCode = row.dataset.itemCode || '';
// KORREKTUR: Jetzt greifen wir auf das richtige dataset-Attribut zu
const itemCost = row.dataset.itemCost || ''; const itemCost = row.dataset.itemCost || '';
damageInvoiceForm.action = "{{ url_for('admin_create_invoice', borrow_id='__BORROW_ID__') }}".replace('__BORROW_ID__', borrowId); damageInvoiceForm.action = "{{ url_for('admin_create_invoice', borrow_id='__BORROW_ID__') }}".replace('__BORROW_ID__', borrowId);
@@ -557,20 +603,14 @@
damageInvoiceItem.value = itemName; damageInvoiceItem.value = itemName;
damageInvoiceBorrower.value = borrower; damageInvoiceBorrower.value = borrower;
damageInvoiceCode.value = itemCode; damageInvoiceCode.value = itemCode;
// Feld zunächst leeren, damit der Ersetzen-Button genutzt werden kann
damageInvoiceAmount.value = ''; damageInvoiceAmount.value = '';
// Original-Preis im Button als data-Attribut hinterlegen
if (damageInvoiceReplaceBtn) { if (damageInvoiceReplaceBtn) {
damageInvoiceReplaceBtn.dataset.acquisition_costs = String(itemCost).replace(' EUR', '').trim(); damageInvoiceReplaceBtn.dataset.acquisition_costs = String(itemCost).replace(' EUR', '').trim();
} }
damageInvoiceReason.value = description || `Schaden gemeldet für ${itemName}`; damageInvoiceReason.value = description || `Schaden gemeldet für ${itemName}`;
damageInvoiceModal.style.display = 'block'; damageInvoiceModal.style.display = 'block';
// Accessibility: Fokus ins erste aktivierbare Feld setzen
damageInvoiceAmount.focus(); damageInvoiceAmount.focus();
} }
@@ -580,7 +620,6 @@
} }
} }
// Event-Listener für den Ersetzen-Button
if (damageInvoiceReplaceBtn) { if (damageInvoiceReplaceBtn) {
damageInvoiceReplaceBtn.addEventListener('click', function() { damageInvoiceReplaceBtn.addEventListener('click', function() {
if (this.dataset.acquisition_costs) { if (this.dataset.acquisition_costs) {