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:
return jsonify({'success': False, 'message': 'Objekt nicht gefunden.'}), 404
# Best-effort system log entry for repair action
try:
logs_collection = db['system_logs']
logs_collection.insert_one({
@@ -8840,132 +8839,145 @@ def admin_mark_invoice_paid(borrow_id):
client.close()
@app.route('/admin/borrowings/<borrow_id>/invoice/finalize', methods=['POST'])
def admin_finalize_invoice_and_repair(borrow_id):
"""Mark invoice as paid and item as repaired in one action."""
@app.route('/admin/borrowings/<borrow_id>/invoice/pay', methods=['POST'])
def admin_mark_invoice_paid(borrow_id):
"""Mark invoice as paid."""
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'))
current_permissions = us.get_effective_permissions(session['username'])
if not current_permissions['pages'].get('admin_borrowings', False):
flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error')
return redirect(url_for('home_admin'))
flash('Ihnen fehlen die nötigen Berechtigungen.', 'error')
return redirect(request.referrer or url_for('home_admin'))
client = None
try:
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
db = client[MONGODB_DB]
ausleihungen = db['ausleihungen']
items_col = db['items']
borrow_doc = ausleihungen.find_one({'_id': ObjectId(borrow_id)})
if not borrow_doc:
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 {}
if not invoice_data:
flash('Für diese Ausleihung existiert keine Rechnung.', 'warning')
return redirect(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})
return redirect(request.referrer or url_for('admin_borrowings'))
now = datetime.datetime.now()
update_fields = {'LastUpdated': now}
update_fields = {
'LastUpdated': now,
}
# Rechnung auf bezahlt setzen
if invoice_data.get('paid') is not True:
update_fields.update({
'InvoiceData.paid': True,
'InvoiceData.paid_at': now,
'InvoiceData.paid_by': session.get('username', ''),
})
# Ausleihe abschließen, falls noch aktiv
if borrow_doc.get('Status') == 'active':
update_fields['Status'] = 'completed'
update_fields['End'] = now
ausleihungen.update_one({'_id': borrow_doc['_id']}, {'$set': update_fields})
repaired = False
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
# Logging
try:
logs_collection = db['system_logs']
logs_collection.insert_one({
'type': 'damage_invoice_finalize',
db['system_logs'].insert_one({
'type': 'invoice_paid',
'timestamp': now.isoformat(),
'user': session.get('username'),
'borrow_id': borrow_id,
'item_id': str(item_doc.get('_id')) if item_doc else '',
'invoice_number': invoice_data.get('invoice_number', ''),
'amount': invoice_data.get('amount'),
'repaired': repaired,
'resolved_damage_reports': resolved_count,
'ip': request.remote_addr,
})
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(
event_type='invoice_finalized_and_repaired',
event_type='invoice_paid',
payload={
'borrow_id': borrow_id,
'item_id': str(item_doc.get('_id')) if item_doc else '',
'invoice_number': invoice_data.get('invoice_number', ''),
'amount': invoice_data.get('amount'),
'repaired': repaired,
'resolved_damage_reports': resolved_count,
}
)
if repaired:
flash('Rechnung als bezahlt markiert und Element als repariert abgeschlossen.', 'success')
else:
flash('Rechnung als bezahlt markiert. Element konnte nicht repariert werden (nicht gefunden).', 'warning')
return redirect(url_for('admin_borrowings'))
flash('Rechnung erfolgreich als bezahlt markiert.', 'success')
return redirect(request.referrer or url_for('library_loans_admin'))
except Exception as e:
app.logger.error(f"Error finalizing invoice/repair for borrow {borrow_id}: {e}")
flash('Fehler beim Kombinieren von bezahlt und repariert.', 'error')
return redirect(url_for('admin_borrowings'))
app.logger.error(f"Error marking invoice paid for borrow {borrow_id}: {e}")
flash('Fehler beim Markieren als bezahlt.', 'error')
return redirect(request.referrer or url_for('library_loans_admin'))
finally:
if client:
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'])
def admin_view_invoice_pdf(borrow_id):
"""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):
"""Append an invoice correction entry without mutating the original invoice body."""
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'))
current_permissions = us.get_effective_permissions(session['username'])
if not current_permissions['pages'].get('admin_borrowings', False):
flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error')
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
try:
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
@@ -9064,6 +9080,7 @@ def admin_add_invoice_correction(borrow_id):
now = datetime.datetime.now()
correction_number = f"CORR-{now.strftime('%Y%m%d-%H%M%S')}-{str(borrow_doc.get('_id'))[-6:].upper()}"
correction_entry = {
'correction_number': correction_number,
'reason': correction_reason,
@@ -9095,6 +9112,7 @@ def admin_add_invoice_correction(borrow_id):
flash('Korrekturbuchung wurde revisionssicher ergänzt.', 'success')
return redirect(url_for('admin_borrowings'))
except Exception as e:
app.logger.error(f"Error creating invoice correction for borrow {borrow_id}: {e}")
flash('Fehler beim Anlegen der Korrekturbuchung.', 'error')
@@ -9104,6 +9122,106 @@ def admin_add_invoice_correction(borrow_id):
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'])
def library_item_invoices(item_id):
"""Show all stored invoices for one specific library item."""
+90 -51
View File
@@ -154,6 +154,7 @@
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.row-actions form {
@@ -196,7 +197,7 @@
<div class="library-admin-hero">
<div>
<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 class="hero-actions">
<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>
<div class="summary-card">
<span class="label">Direkt reparierbar</span>
<span class="label">Direkt bearbeitbar</span>
<div class="value">{{ damaged_items|length }}</div>
</div>
</div>
@@ -311,35 +312,29 @@
{% if e.status == 'active' %}
<button type="button" class="btn btn-outline-danger btn-sm" onclick="openDamageReportPrompt(this)">Schaden melden</button>
{% 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?');">
<button type="submit" class="btn btn-success btn-sm">
{% if not e.invoice_paid and e.has_damage %}
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>
{% if e.invoice_number and not e.invoice_paid %}
<form method="post" action="{{ url_for('admin_mark_invoice_paid', borrow_id=e.id) }}" onsubmit="return confirm('Rechnung als bezahlt markieren?');">
<button type="submit" class="btn btn-success btn-sm">Als bezahlt markieren</button>
</form>
{% 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 %}
<form method="post" action="{{ url_for('admin_add_invoice_correction', borrow_id=e.id) }}" onsubmit="return confirm('Korrekturbuchung hinzufügen?');">
<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="amount_delta" placeholder="Delta optional" style="padding:6px; border:1px solid #ddd; border-radius:6px; width:120px;">
<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 %}
{% 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?');">
<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>
{% endif %}
</div>
@@ -392,9 +387,8 @@
<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>
</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?');">
<button type="submit" class="btn btn-success btn-sm">Als repariert markieren</button>
</form>
<!-- Aufruf des Reparatur-Auswahl-Modals -->
<button type="button" class="btn btn-warning btn-sm" onclick="openRepairModal('{{ item.id }}', '{{ item.code }}')">Reparieren / Ersetzen</button>
</td>
</tr>
{% endfor %}
@@ -407,6 +401,7 @@
</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 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;">
@@ -464,6 +459,33 @@
</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>
(function() {
const searchInput = document.getElementById('library-search');
@@ -473,6 +495,7 @@
const damagedRows = Array.from(document.querySelectorAll('.damaged-row'));
const loansEmpty = document.getElementById('loans-empty');
const damagedEmpty = document.getElementById('damaged-empty');
const damageInvoiceModal = document.getElementById('damage-invoice-modal');
const damageInvoiceForm = document.getElementById('damage-invoice-form');
const damageInvoiceItem = document.getElementById('damage-invoice-item');
@@ -482,23 +505,56 @@
const damageInvoiceReason = document.getElementById('damage-invoice-reason');
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) {
const row = button.closest('.loan-row');
if (!row) {
return;
}
if (!row) return;
const itemId = row.dataset.itemId || '';
const itemName = row.dataset.itemName || 'Bibliotheksmedium';
const noteInput = prompt('Schadensmeldung für dieses Bibliotheksmedium:\nNotiz zum Schaden (optional):', '');
if (noteInput === null) {
return;
}
if (noteInput === null) return;
const description = noteInput.trim() || 'Schaden erneut gemeldet';
// Visuelles Feedback: Button deaktivieren und Text ändern
const originalText = button.textContent;
button.disabled = true;
button.textContent = 'Speichere...';
@@ -509,7 +565,6 @@
body: JSON.stringify({ description })
})
.then(async response => {
// Robustes JSON-Parsing (verhindert Absturz, falls der Server kein JSON zurückgibt)
const data = await response.json().catch(() => ({}));
if (!response.ok || !data.success) {
throw new Error(data.message || 'Fehler beim Speichern der Schadensmeldung.');
@@ -519,18 +574,14 @@
.then(data => {
if (confirm('Schaden gespeichert. Soll direkt eine Rechnung erstellt werden?')) {
openDamageInvoiceModal(row, description);
// Button wieder zurücksetzen, da die Seite nicht neu geladen wird
button.disabled = false;
button.textContent = originalText;
return;
}
// Bei "Abbrechen" im Confirm -> Neuladen der Tabelle
window.location.reload();
})
.catch(error => {
alert(error.message || 'Ein unbekannter Fehler ist aufgetreten.');
// Fehlerbehandlung: Button wieder aktiv schalten
button.disabled = false;
button.textContent = originalText;
});
@@ -539,17 +590,12 @@
window.openDamageReportPrompt = openDamageReportPrompt;
function openDamageInvoiceModal(row, description) {
if (!damageInvoiceModal || !damageInvoiceForm) {
console.error("Modal oder Formular nicht gefunden.");
return;
}
if (!damageInvoiceModal || !damageInvoiceForm) return;
const borrowId = row.dataset.borrowId || '';
const itemName = row.dataset.itemName || '';
const borrower = row.dataset.userName || '';
const itemCode = row.dataset.itemCode || '';
// KORREKTUR: Jetzt greifen wir auf das richtige dataset-Attribut zu
const itemCost = row.dataset.itemCost || '';
damageInvoiceForm.action = "{{ url_for('admin_create_invoice', borrow_id='__BORROW_ID__') }}".replace('__BORROW_ID__', borrowId);
@@ -557,20 +603,14 @@
damageInvoiceItem.value = itemName;
damageInvoiceBorrower.value = borrower;
damageInvoiceCode.value = itemCode;
// Feld zunächst leeren, damit der Ersetzen-Button genutzt werden kann
damageInvoiceAmount.value = '';
// Original-Preis im Button als data-Attribut hinterlegen
if (damageInvoiceReplaceBtn) {
damageInvoiceReplaceBtn.dataset.acquisition_costs = String(itemCost).replace(' EUR', '').trim();
}
damageInvoiceReason.value = description || `Schaden gemeldet für ${itemName}`;
damageInvoiceModal.style.display = 'block';
// Accessibility: Fokus ins erste aktivierbare Feld setzen
damageInvoiceAmount.focus();
}
@@ -580,7 +620,6 @@
}
}
// Event-Listener für den Ersetzen-Button
if (damageInvoiceReplaceBtn) {
damageInvoiceReplaceBtn.addEventListener('click', function() {
if (this.dataset.acquisition_costs) {