feat(inventory): add repair resolution workflow and invoice correction logic
Release Inventarsystem / release-docker (push) Successful in 2m14s
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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user