Initial Commit

This commit is contained in:
2026-04-10 14:48:52 +02:00
commit fad7d9a757
81 changed files with 39657 additions and 0 deletions
+378
View File
@@ -0,0 +1,378 @@
<!--
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 title %}System Logs{% endblock %}
{% block content %}
<div class="container">
<h1>System-Protokoll</h1>
{% set conflicts = items | selectattr('ConflictDetected') | list %}
{% if conflicts %}
<div class="conflict-summary-banner" id="conflictSummaryBanner">
<strong>⚠ {{ conflicts | length }} Buchungskonflikt{{ 's' if conflicts | length > 1 else '' }} erkannt</strong> –
Geplante Reservierungen wurden aktiv, obwohl der Gegenstand bereits ausgeliehen war.
<button onclick="document.getElementById('conflictSummaryBanner').style.display='none'" class="banner-close">✕</button>
</div>
{% endif %}
<div class="filter-controls">
<input type="text" id="searchInput" placeholder="Suchen..." onkeyup="filterTable()">
<div class="date-range">
<label for="startDate">Von:</label>
<input type="date" id="startDate" onchange="filterTable()">
<label for="endDate">Bis:</label>
<input type="date" id="endDate" onchange="filterTable()">
</div>
</div>
<div class="table-container">
<table id="logsTable">
<thead>
<tr>
<th onclick="sortTable(0)">Typ ↕</th>
<th onclick="sortTable(1)">Item Name ↕</th>
<th onclick="sortTable(2)">Benutzer ↕</th>
<th onclick="sortTable(3)">Status ↕</th>
<th onclick="sortTable(4)">Ausgeliehen am ↕</th>
<th onclick="sortTable(5)">Zurückgegeben am ↕</th>
<th>Dauer</th>
<th>Hinweis</th>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr>
<td>{{ item.EventType or 'Ausleihe' }}</td>
<td>{{ item.Item }}</td>
<td>{{ item.User }}</td>
<td>
<span class="status-badge status-{{ item.Status }}">{{ item.Status }}</span>
</td>
<td>{{ item.Start }}</td>
<td>{{ item.End }}</td>
<td>{{ item.Duration }}</td>
<td>
{% if item.ConflictNote %}
<span title="{{ item.ConflictNote }}">{{ item.ConflictNote }}</span>
{% endif %}
{% if item.ConflictDetected %}
<span class="conflict-badge" title="{{ item.ConflictNote }}">⚠ Konflikt</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="navigation-buttons">
<a href="{{ url_for('home') }}" class="button">Zurück zum Dashboard</a>
<button onclick="exportToCSV()" class="export-button">Als CSV exportieren</button>
</div>
</div>
<script>
function filterTable() {
const input = document.getElementById('searchInput').value.toLowerCase();
const startDate = document.getElementById('startDate').value;
const endDate = document.getElementById('endDate').value;
const table = document.getElementById('logsTable');
const rows = table.getElementsByTagName('tr');
for (let i = 1; i < rows.length; i++) {
let show = true;
const cells = rows[i].getElementsByTagName('td');
// Text search
if (input) {
let textMatch = false;
for (let j = 0; j < cells.length; j++) {
if (cells[j].textContent.toLowerCase().indexOf(input) > -1) {
textMatch = true;
break;
}
}
show = textMatch;
}
// Date range filtering (uses Start in column 4 and End in column 5)
if (show && startDate) {
const rowDate = new Date(cells[4].textContent);
show = rowDate >= new Date(startDate);
}
if (show && endDate) {
const rowDate = new Date(cells[5].textContent);
show = rowDate <= new Date(endDate);
}
rows[i].style.display = show ? '' : 'none';
}
}
function sortTable(n) {
const table = document.getElementById('logsTable');
let switching = true;
let dir = 'asc';
let switchcount = 0;
while (switching) {
switching = false;
const rows = table.rows;
for (let i = 1; i < rows.length - 1; i++) {
let shouldSwitch = false;
const x = rows[i].getElementsByTagName('td')[n];
const y = rows[i + 1].getElementsByTagName('td')[n];
// Check if date column for proper date comparison (Start=4, End=5)
if (n === 4 || n === 5) {
const dateX = new Date(x.innerHTML);
const dateY = new Date(y.innerHTML);
if (dir === 'asc') {
if (dateX > dateY) {
shouldSwitch = true;
break;
}
} else if (dir === 'desc') {
if (dateX < dateY) {
shouldSwitch = true;
break;
}
}
} else {
if (dir === 'asc') {
if (x.innerHTML.toLowerCase() > y.innerHTML.toLowerCase()) {
shouldSwitch = true;
break;
}
} else if (dir === 'desc') {
if (x.innerHTML.toLowerCase() < y.innerHTML.toLowerCase()) {
shouldSwitch = true;
break;
}
}
}
}
if (shouldSwitch) {
rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);
switching = true;
switchcount++;
} else {
if (switchcount === 0 && dir === 'asc') {
dir = 'desc';
switching = true;
}
}
}
}
function exportToCSV() {
const table = document.getElementById('logsTable');
let csv = [];
const rows = table.querySelectorAll('tr');
for (let i = 0; i < rows.length; i++) {
const row = [], cols = rows[i].querySelectorAll('td, th');
for (let j = 0; j < cols.length; j++) {
// Escape double-quotes with double-quotes
let data = cols[j].innerText.replace(/"/g, '""');
// Add quotes if the content contains comma or newline
row.push('"' + data + '"');
}
csv.push(row.join(','));
}
// Download CSV file
const csvFile = new Blob([csv.join('\n')], {type: 'text/csv'});
const downloadLink = document.createElement('a');
downloadLink.download = 'ausleihungen_' + new Date().toISOString().slice(0,10) + '.csv';
downloadLink.href = window.URL.createObjectURL(csvFile);
downloadLink.style.display = 'none';
document.body.appendChild(downloadLink);
downloadLink.click();
}
</script>
<style>
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
h1 {
text-align: center;
margin-bottom: 30px;
color: #333;
}
.filter-controls {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
flex-wrap: wrap;
}
#searchInput {
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
width: 200px;
}
.date-range {
display: flex;
align-items: center;
gap: 10px;
}
.date-range input {
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.status-badge {
display: inline-block;
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
text-transform: capitalize;
color: #fff;
}
.status-planned { background-color: #6c757d; }
.status-active { background-color: #17a2b8; }
.status-completed { background-color: #28a745; }
.status-cancelled { background-color: #dc3545; }
.status-gemeldet { background-color: #fd7e14; }
.status-repariert { background-color: #20c997; }
.conflict-badge {
display: inline-block;
padding: 3px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
background-color: #fff3cd;
color: #856404;
border: 1px solid #ffc107;
cursor: help;
white-space: nowrap;
}
.conflict-summary-banner {
background-color: #fff3cd;
border: 1px solid #ffc107;
border-left: 4px solid #fd7e14;
color: #856404;
padding: 12px 16px;
border-radius: 4px;
margin-bottom: 16px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.banner-close {
background: none;
border: none;
font-size: 1rem;
cursor: pointer;
color: #856404;
flex-shrink: 0;
padding: 0 4px;
}
.banner-close:hover { color: #533f03; }
.table-container {
overflow-x: auto;
margin-bottom: 20px;
}
table {
width: 100%;
border-collapse: collapse;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
}
th, td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #ddd;
}
th {
background-color: #007bff;
color: white;
cursor: pointer;
}
th:hover {
background-color: #0056b3;
}
tbody tr:hover {
background-color: #f5f5f5;
}
.navigation-buttons {
display: flex;
justify-content: space-between;
}
.button, .export-button {
padding: 10px 15px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
text-decoration: none;
display: inline-block;
}
.button:hover, .export-button:hover {
background-color: #0056b3;
}
.export-button {
background-color: #28a745;
}
.export-button:hover {
background-color: #218838;
}
@media (max-width: 768px) {
.filter-controls {
flex-direction: column;
gap: 10px;
}
#searchInput {
width: 100%;
}
.date-range {
width: 100%;
justify-content: space-between;
}
}
</style>
{% endblock %}