2faf284a0e
- Introduced a new audit logging system that creates a tamper-evident chain of events in MongoDB. - Added functions for appending audit events, ensuring index integrity, and verifying the audit chain. - Implemented soft-delete functionality for inventory and borrowing records to comply with GoBD regulations. - Added UI components for displaying invoice corrections and audit dashboard, including mismatch reporting. - Created a CLI utility for verifying the integrity of the audit chain. - Enhanced invoice management by preventing overwrites and allowing correction entries with reasons and optional deltas.
326 lines
15 KiB
HTML
326 lines
15 KiB
HTML
<!--
|
|
Copyright 2025-2026 AIIrondev
|
|
|
|
Licensed under the Inventarsystem EULA (Endbenutzer-Lizenzvertrag).
|
|
See Legal/LICENSE for the full license text.
|
|
Unauthorized commercial use, SaaS hosting, or removal of branding is prohibited.
|
|
For commercial licensing inquiries: https://github.com/AIIrondev
|
|
-->
|
|
{% extends 'base.html' %}
|
|
{% block content %}
|
|
<div class="container">
|
|
<h1>Ausleihen verwalten</h1>
|
|
<p>Liste aller aktiven und geplanten Ausleihen. Sie können einzelne Einträge zurücksetzen oder bei Schäden eine Rechnung als PDF erstellen.</p>
|
|
|
|
{% if entries and entries|length > 0 %}
|
|
<!-- Filter Section -->
|
|
<div style="margin-bottom: 20px; padding: 15px; background-color: #f5f5f5; border-radius: 5px;">
|
|
<h3>Filter</h3>
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 15px;">
|
|
<div>
|
|
<label for="user-filter" style="display: block; margin-bottom: 5px; font-weight: bold;">Benutzer:</label>
|
|
<input
|
|
type="text"
|
|
id="user-filter"
|
|
placeholder="Nach Benutzer filtern..."
|
|
style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 3px;"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label for="item-filter" style="display: block; margin-bottom: 5px; font-weight: bold;">Element:</label>
|
|
<input
|
|
type="text"
|
|
id="item-filter"
|
|
placeholder="Nach Element filtern..."
|
|
style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 3px;"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px;">
|
|
<div>
|
|
<label for="status-filter" style="display: block; margin-bottom: 5px; font-weight: bold;">Status:</label>
|
|
<select
|
|
id="status-filter"
|
|
style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 3px;"
|
|
>
|
|
<option value="">Alle</option>
|
|
<option value="active">Aktiv</option>
|
|
<option value="planned">Geplant</option>
|
|
</select>
|
|
</div>
|
|
<div style="display: flex; align-items: flex-end;">
|
|
<button
|
|
id="reset-filters"
|
|
class="btn btn-secondary"
|
|
style="width: 100%;"
|
|
>
|
|
Filter zurücksetzen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<table class="table" style="width:100%; border-collapse: collapse;">
|
|
<thead>
|
|
<tr>
|
|
<th style="text-align:left; padding:8px; border-bottom:1px solid #ddd;">Status</th>
|
|
<th style="text-align:left; padding:8px; border-bottom:1px solid #ddd;">Element</th>
|
|
<th style="text-align:left; padding:8px; border-bottom:1px solid #ddd;">Element-ID</th>
|
|
<th style="text-align:left; padding:8px; border-bottom:1px solid #ddd;">Benutzer</th>
|
|
<th style="text-align:left; padding:8px; border-bottom:1px solid #ddd;">Start</th>
|
|
<th style="text-align:left; padding:8px; border-bottom:1px solid #ddd;">Ende</th>
|
|
<th style="text-align:left; padding:8px; border-bottom:1px solid #ddd;">Stunde</th>
|
|
<th style="text-align:left; padding:8px; border-bottom:1px solid #ddd;">Preis</th>
|
|
<th style="text-align:left; padding:8px; border-bottom:1px solid #ddd;">Rechnung</th>
|
|
<th style="text-align:left; padding:8px; border-bottom:1px solid #ddd;">Notizen</th>
|
|
<th style="text-align:left; padding:8px; border-bottom:1px solid #ddd;">Aktion</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="borrowing-table-body">
|
|
{% for e in entries %}
|
|
<tr class="borrowing-row"
|
|
data-user="{{ e.user }}"
|
|
data-item="{{ e.item_name }}"
|
|
data-status="{{ e.status }}"
|
|
data-borrow-id="{{ e.id }}"
|
|
data-item-id="{{ e.item_id }}"
|
|
data-item-name="{{ e.item_name }}"
|
|
data-item-code="{{ e.item_code }}"
|
|
data-item-cost="{{ e.item_cost_raw }}"
|
|
data-user-name="{{ e.user }}">
|
|
<td style="padding:8px; border-bottom:1px solid #eee;">{{ e.status }}</td>
|
|
<td style="padding:8px; border-bottom:1px solid #eee;">{{ e.item_name }}</td>
|
|
<td style="padding:8px; border-bottom:1px solid #eee; font-family:monospace;">{{ e.item_id }}</td>
|
|
<td style="padding:8px; border-bottom:1px solid #eee;">{{ e.user }}</td>
|
|
<td style="padding:8px; border-bottom:1px solid #eee;">{{ e.start }}</td>
|
|
<td style="padding:8px; border-bottom:1px solid #eee;">{{ e.end }}</td>
|
|
<td style="padding:8px; border-bottom:1px solid #eee;">{{ e.period }}</td>
|
|
<td style="padding:8px; border-bottom:1px solid #eee;">{{ e.item_cost }}</td>
|
|
<td style="padding:8px; border-bottom:1px solid #eee;">
|
|
{% if e.invoice_number %}
|
|
<div style="font-weight:600;">{{ e.invoice_number }}</div>
|
|
<div style="font-size:0.85rem; color:#666;">{{ e.invoice_amount }}</div>
|
|
{% if e.invoice_corrections_count %}
|
|
<div style="font-size:0.78rem; color:#7c2d12; margin-top:4px;">{{ e.invoice_corrections_count }} Korrektur(en)</div>
|
|
{% endif %}
|
|
{% if e.invoice_paid %}
|
|
<div style="display:inline-block; margin-top:6px; padding:2px 8px; border-radius:999px; background:#dcfce7; color:#166534; font-size:0.75rem; font-weight:700;">Bezahlt</div>
|
|
{% if e.invoice_paid_at %}
|
|
<div style="font-size:0.75rem; color:#666; margin-top:4px;">am {{ e.invoice_paid_at }}</div>
|
|
{% endif %}
|
|
{% else %}
|
|
<div style="display:inline-block; margin-top:6px; padding:2px 8px; border-radius:999px; background:#fee2e2; color:#991b1b; font-size:0.75rem; font-weight:700;">Offen</div>
|
|
{% endif %}
|
|
{% else %}
|
|
<div style="font-size:0.9rem; color:#666;">Noch keine Rechnung</div>
|
|
{% endif %}
|
|
</td>
|
|
<td style="padding:8px; border-bottom:1px solid #eee;">{{ e.notes }}</td>
|
|
<td style="padding:8px; border-bottom:1px solid #eee;">
|
|
<div style="display:flex; flex-wrap:wrap; gap:8px;">
|
|
<button type="button" class="btn btn-danger" onclick="openInvoiceModal(this)" {% if e.status != 'active' %}disabled{% endif %}>Rechnung erstellen</button>
|
|
{% 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">
|
|
{% 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>
|
|
{% 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:160px;">
|
|
<input type="text" name="amount_delta" placeholder="Delta (optional)" style="padding:6px; border:1px solid #ddd; border-radius:6px; width:120px;">
|
|
<button type="submit" class="btn btn-outline-danger">Korrektur</button>
|
|
</form>
|
|
{% endif %}
|
|
<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">Zurücksetzen</button>
|
|
</form>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
|
|
<div id="no-results" style="display: none; text-align: center; padding: 20px; color: #666;">
|
|
<p>Keine Einträge mit den aktuellen Filtern gefunden.</p>
|
|
</div>
|
|
|
|
{% else %}
|
|
<div class="empty-state">
|
|
<p>Keine aktiven oder geplanten Ausleihen gefunden.</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div id="invoice-modal" 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:#fff; 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>
|
|
<h2 style="margin:0;">Rechnung erstellen</h2>
|
|
<p style="margin:6px 0 0; color:#666;">Preis wird aus den Anschaffungskosten vorbelegt und kann bei Bedarf angepasst werden.</p>
|
|
</div>
|
|
<button type="button" class="btn btn-secondary" onclick="closeInvoiceModal()">Schließen</button>
|
|
</div>
|
|
|
|
<form id="invoice-form" method="post" action="">
|
|
<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(240px, 1fr)); gap:16px; margin-bottom:16px;">
|
|
<div>
|
|
<label for="invoice-item" style="display:block; font-weight:700; margin-bottom:6px;">Element</label>
|
|
<input id="invoice-item" type="text" readonly style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px; background:#f8fafc;">
|
|
</div>
|
|
<div>
|
|
<label for="invoice-borrower" style="display:block; font-weight:700; margin-bottom:6px;">Schüler</label>
|
|
<input id="invoice-borrower" type="text" readonly style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px; background:#f8fafc;">
|
|
</div>
|
|
<div>
|
|
<label for="invoice-code" style="display:block; font-weight:700; margin-bottom:6px;">Element-ID / Code</label>
|
|
<input id="invoice-code" type="text" readonly style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px; background:#f8fafc;">
|
|
</div>
|
|
<div>
|
|
<label for="invoice-amount" style="display:block; font-weight:700; margin-bottom:6px;">Preis</label>
|
|
<input id="invoice-amount" name="invoice_amount" type="text" required style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px;" placeholder="z.B. 12,50">
|
|
</div>
|
|
</div>
|
|
|
|
<div style="margin-bottom:16px;">
|
|
<label for="damage-reason" style="display:block; font-weight:700; margin-bottom:6px;">Schadensbeschreibung</label>
|
|
<textarea id="damage-reason" name="damage_reason" rows="5" required style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px; resize:vertical;" placeholder="Beschreiben Sie kurz den Schaden oder die Zerstörung."></textarea>
|
|
</div>
|
|
|
|
<div style="display:flex; flex-wrap:wrap; gap:16px; align-items:center; margin-bottom:18px;">
|
|
<label style="display:flex; align-items:center; gap:8px;">
|
|
<input type="checkbox" name="mark_destroyed" checked>
|
|
Element als zerstört markieren
|
|
</label>
|
|
<label style="display:flex; align-items:center; gap:8px;">
|
|
<input type="checkbox" name="close_borrowing" checked>
|
|
Ausleihe abschließen
|
|
</label>
|
|
</div>
|
|
|
|
<div style="display:flex; justify-content:flex-end; gap:10px;">
|
|
<button type="button" class="btn btn-secondary" onclick="closeInvoiceModal()">Abbrechen</button>
|
|
<button type="submit" class="btn btn-danger">PDF-Rechnung erstellen</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const userFilter = document.getElementById('user-filter');
|
|
const itemFilter = document.getElementById('item-filter');
|
|
const statusFilter = document.getElementById('status-filter');
|
|
const resetButton = document.getElementById('reset-filters');
|
|
const tableBody = document.getElementById('borrowing-table-body');
|
|
const noResults = document.getElementById('no-results');
|
|
|
|
function applyFilters() {
|
|
const userValue = userFilter.value.toLowerCase();
|
|
const itemValue = itemFilter.value.toLowerCase();
|
|
const statusValue = statusFilter.value;
|
|
|
|
let visibleCount = 0;
|
|
|
|
document.querySelectorAll('.borrowing-row').forEach(row => {
|
|
const user = row.getAttribute('data-user').toLowerCase();
|
|
const item = row.getAttribute('data-item').toLowerCase();
|
|
const status = row.getAttribute('data-status');
|
|
|
|
const userMatch = user.includes(userValue);
|
|
const itemMatch = item.includes(itemValue);
|
|
const statusMatch = statusValue === '' || status === statusValue;
|
|
|
|
const shouldShow = userMatch && itemMatch && statusMatch;
|
|
|
|
if (shouldShow) {
|
|
row.style.display = '';
|
|
visibleCount++;
|
|
} else {
|
|
row.style.display = 'none';
|
|
}
|
|
});
|
|
|
|
// Show/hide no results message
|
|
if (visibleCount === 0) {
|
|
noResults.style.display = 'block';
|
|
} else {
|
|
noResults.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
function resetFilters() {
|
|
userFilter.value = '';
|
|
itemFilter.value = '';
|
|
statusFilter.value = '';
|
|
applyFilters();
|
|
}
|
|
|
|
function normalizeMoneyInput(value) {
|
|
if (!value) {
|
|
return '';
|
|
}
|
|
return String(value).replace(' EUR', '').trim();
|
|
}
|
|
|
|
window.openInvoiceModal = function(button) {
|
|
const row = button.closest('.borrowing-row');
|
|
if (!row) {
|
|
return;
|
|
}
|
|
|
|
const borrowId = row.getAttribute('data-borrow-id') || '';
|
|
const itemName = row.getAttribute('data-item-name') || '';
|
|
const borrower = row.getAttribute('data-user-name') || '';
|
|
const itemCode = row.getAttribute('data-item-code') || '';
|
|
const itemCost = row.getAttribute('data-item-cost') || '';
|
|
|
|
const modal = document.getElementById('invoice-modal');
|
|
const form = document.getElementById('invoice-form');
|
|
const amountField = document.getElementById('invoice-amount');
|
|
const reasonField = document.getElementById('damage-reason');
|
|
|
|
form.action = "{{ url_for('admin_create_invoice', borrow_id='__BORROW_ID__') }}".replace('__BORROW_ID__', borrowId);
|
|
document.getElementById('invoice-item').value = itemName;
|
|
document.getElementById('invoice-borrower').value = borrower;
|
|
document.getElementById('invoice-code').value = itemCode;
|
|
amountField.value = normalizeMoneyInput(itemCost);
|
|
reasonField.value = 'Das Element ' + itemName + ' wurde während der Ausleihe beschädigt oder zerstört.';
|
|
|
|
modal.style.display = 'block';
|
|
amountField.focus();
|
|
};
|
|
|
|
window.closeInvoiceModal = function() {
|
|
const modal = document.getElementById('invoice-modal');
|
|
if (modal) {
|
|
modal.style.display = 'none';
|
|
}
|
|
};
|
|
|
|
const modal = document.getElementById('invoice-modal');
|
|
if (modal) {
|
|
modal.addEventListener('click', function(event) {
|
|
if (event.target === modal) {
|
|
closeInvoiceModal();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Add event listeners
|
|
userFilter.addEventListener('input', applyFilters);
|
|
itemFilter.addEventListener('input', applyFilters);
|
|
statusFilter.addEventListener('change', applyFilters);
|
|
resetButton.addEventListener('click', resetFilters);
|
|
});
|
|
</script>
|
|
{% endblock %}
|