Files
Inventarsystem/Web/templates/logs.html
T
2026-04-10 14:48:52 +02:00

378 lines
12 KiB
HTML
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
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 %}