Compare commits

...

9 Commits

Author SHA1 Message Date
Aiirondev_dev 6770d42f99 Default Filternames Changed
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-24 16:00:58 +02:00
Aiirondev_dev 333f9fc89a New Modal Overlay
Release Inventarsystem / release-docker (push) Successful in 2m18s
2026-08-24 15:45:08 +02:00
Aiirondev_dev a286236834 New Modal Overlay
Release Inventarsystem / release-docker (push) Successful in 2m17s
2026-08-24 15:41:33 +02:00
Aiirondev_dev 84cc8de833 Reverse of faulty changes
Release Inventarsystem / release-docker (push) Successful in 2m16s
2026-08-24 14:52:50 +02:00
Aiirondev_dev 4b3c7e646f Replaced the Browser side pop ups with custom ones from the site itsself
Release Inventarsystem / release-docker (push) Successful in 2m17s
2026-08-24 14:45:21 +02:00
Aiirondev_dev f9b013508c Change of the Schaden melden button to be correctly considert a button
Release Inventarsystem / release-docker (push) Successful in 2m16s
2026-08-24 14:12:28 +02:00
Aiirondev_dev 2a6c40eb5a Fix for the CRSF Token implementation callability
Release Inventarsystem / release-docker (push) Successful in 2m16s
2026-08-24 12:35:16 +02:00
Aiirondev_dev 432d6da798 Fix for the CRSF Token implementation
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-24 12:12:55 +02:00
Aiirondev_dev 2cdf0d2169 implementation of the export for users
Release Inventarsystem / release-docker (push) Successful in 3m8s
2026-08-24 11:56:03 +02:00
5 changed files with 370 additions and 23 deletions
+66
View File
@@ -8628,6 +8628,72 @@ def register_csv():
mimetype='application/pdf'
)
@app.route('/export_users_csv', methods=['POST'])
def export_users_csv():
"""
Exports a filtered list of users to a CSV file.
Receives a JSON array of usernames from the frontend.
"""
if 'username' not in session:
flash('Ihnen ist es nicht gestattet, diese Aktion auszuführen.', 'error')
return redirect(url_for('login'))
usernames_json = request.form.get('usernames')
if not usernames_json:
flash('Keine Benutzer zum Exportieren ausgewählt.', 'error')
return redirect(url_for('user_del'))
try:
target_usernames = json.loads(usernames_json)
except json.JSONDecodeError:
flash('Ungültige Daten beim Export übermittelt.', 'error')
return redirect(url_for('user_del'))
# Fetch all users
all_users = us.get_all_users()
# Create CSV in memory
si = io.StringIO()
# Delimiter set to semicolon (standard for Excel in German locales)
cw = csv.writer(si, delimiter=';')
# Write headers
cw.writerow(['Benutzername', 'Vorname', 'Nachname', 'Administrator', 'Rechte-Preset'])
for user in all_users:
encrypted_username = user.get('Username')
if not encrypted_username:
continue
uname = decrypt_text(encrypted_username)
# Only process users that were present in the frontend's filtered view
if uname in target_usernames:
try:
permissions_payload = us.get_effective_permissions(uname)
preset = permissions_payload.get('preset', 'standard_user')
except Exception:
preset = 'standard_user'
try:
name = us.get_name(uname) or ""
last_name = us.get_last_name(uname) or ""
except Exception:
name = ""
last_name = ""
admin_status = "Ja" if user.get('Admin', False) else "Nein"
cw.writerow([uname, name, last_name, admin_status, preset])
# Prefix with BOM (Byte Order Mark) to ensure Excel reads UTF-8 (Umlauts) correctly
output = '\ufeff' + si.getvalue()
return Response(
output,
mimetype="text/csv",
headers={"Content-disposition": "attachment; filename=benutzer_export.csv"}
)
@app.route('/user_del', methods=['GET'])
def user_del():
+3 -3
View File
@@ -911,9 +911,9 @@ def get_filter_names():
if names_doc and 'names' in names_doc:
return names_doc['names']
return {
'1': 'Fach/Kategorie',
'2': 'System/Bereich',
'3': 'Typ/Art'
'1': 'Klasse',
'2': 'Fach',
'3': 'Schlagwort'
}
def set_filter_name(filter_num, name):
+28 -2
View File
@@ -122,7 +122,7 @@
letter-spacing: 0.04em;
color: #64748b;
background: var(--ui-surface-soft);
user-select: none; /* Verhindert Textmarkierung beim Klicken */
user-select: none;
}
.library-table tr:hover td {
@@ -151,7 +151,7 @@
.badge-open { background: #fee2e2; color: #991b1b; }
.badge-paid { background: #dcfce7; color: #166534; }
.badge-damaged { background: #fee2e2; color: #991b1b; }
.badge-class { background: #f1f5f9; color: #475569; border: 1px solid #cbd5e1; } /* Neues Badge für Klasse */
.badge-class { background: #f1f5f9; color: #475569; border: 1px solid #cbd5e1; }
.row-actions {
display: flex;
@@ -168,6 +168,32 @@
white-space: nowrap;
}
.row-actions .btn-outline-danger {
color: #dc2626;
background-color: #fef2f2; /* Light red tint so it doesn't blend into the white table */
border: 2px solid #dc2626; /* Thicker, clear border */
padding: 6px 14px;
border-radius: 6px;
font-weight: bold;
font-size: 0.85rem;
cursor: pointer;
box-shadow: 0 2px 4px rgba(220, 38, 38, 0.1); /* Subtle shadow for depth */
transition: all 0.2s ease;
}
.row-actions .btn-outline-danger:hover {
background-color: #dc2626;
color: #ffffff;
box-shadow: 0 4px 8px rgba(220, 38, 38, 0.25);
transform: translateY(-1px); /* Slight lift effect on hover */
}
.row-actions .btn-outline-danger:active {
transform: translateY(0);
box-shadow: 0 1px 2px rgba(220, 38, 38, 0.15); /* Pressed state */
}
/* ------------------------------------------------------ */
.muted {
color: #6b7280;
font-size: 0.92rem;
+211 -10
View File
@@ -614,6 +614,199 @@
</div>
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2/dist/quagga.js"></script>
<script>
// =========================================================================
// CUSTOM MODAL HELPERS (Replaces native browser alert, confirm & prompt)
// =========================================================================
function createModalOverlay() {
const overlay = document.createElement('div');
overlay.style.position = 'fixed';
overlay.style.top = '0';
overlay.style.left = '0';
overlay.style.width = '100vw';
overlay.style.height = '100vh';
overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.6)';
overlay.style.display = 'flex';
overlay.style.alignItems = 'center';
overlay.style.justifyContent = 'center';
overlay.style.zIndex = '999999';
return overlay;
}
function createModalBox() {
const box = document.createElement('div');
box.style.backgroundColor = '#fff';
box.style.padding = '24px';
box.style.borderRadius = '8px';
box.style.boxShadow = '0 10px 25px rgba(0,0,0,0.2)';
box.style.fontFamily = 'sans-serif';
box.style.minWidth = '320px';
box.style.maxWidth = '90%';
box.style.textAlign = 'left';
box.style.color = '#333';
return box;
}
function customAlert(message) {
return new Promise((resolve) => {
const overlay = createModalOverlay();
const box = createModalBox();
const text = document.createElement('p');
text.textContent = message;
text.style.marginBottom = '20px';
text.style.lineHeight = '1.5';
text.style.whiteSpace = 'pre-wrap';
const btnContainer = document.createElement('div');
btnContainer.style.display = 'flex';
btnContainer.style.justifyContent = 'flex-end';
const btn = document.createElement('button');
btn.textContent = 'OK';
btn.style.padding = '8px 16px';
btn.style.cursor = 'pointer';
btn.style.border = 'none';
btn.style.backgroundColor = '#007bff';
btn.style.color = '#fff';
btn.style.borderRadius = '4px';
btn.onclick = () => {
document.body.removeChild(overlay);
resolve();
};
btnContainer.appendChild(btn);
box.appendChild(text);
box.appendChild(btnContainer);
overlay.appendChild(box);
document.body.appendChild(overlay);
btn.focus();
});
}
function customConfirm(message) {
return new Promise((resolve) => {
const overlay = createModalOverlay();
const box = createModalBox();
const text = document.createElement('p');
text.textContent = message;
text.style.marginBottom = '20px';
text.style.lineHeight = '1.5';
text.style.whiteSpace = 'pre-wrap';
const btnContainer = document.createElement('div');
btnContainer.style.display = 'flex';
btnContainer.style.justifyContent = 'flex-end';
btnContainer.style.gap = '10px';
const cancelBtn = document.createElement('button');
cancelBtn.textContent = 'Abbrechen';
cancelBtn.style.padding = '8px 16px';
cancelBtn.style.cursor = 'pointer';
cancelBtn.style.border = '1px solid #ccc';
cancelBtn.style.backgroundColor = '#f8f9fa';
cancelBtn.style.color = '#333';
cancelBtn.style.borderRadius = '4px';
const okBtn = document.createElement('button');
okBtn.textContent = 'OK';
okBtn.style.padding = '8px 16px';
okBtn.style.cursor = 'pointer';
okBtn.style.border = 'none';
okBtn.style.backgroundColor = '#007bff';
okBtn.style.color = '#fff';
okBtn.style.borderRadius = '4px';
const closeAndResolve = (val) => {
document.body.removeChild(overlay);
resolve(val);
};
cancelBtn.onclick = () => closeAndResolve(false);
okBtn.onclick = () => closeAndResolve(true);
btnContainer.appendChild(cancelBtn);
btnContainer.appendChild(okBtn);
box.appendChild(text);
box.appendChild(btnContainer);
overlay.appendChild(box);
document.body.appendChild(overlay);
okBtn.focus();
});
}
function customPrompt(message, defaultValue = '') {
return new Promise((resolve) => {
const overlay = createModalOverlay();
const box = createModalBox();
const text = document.createElement('p');
text.textContent = message;
text.style.marginBottom = '15px';
text.style.lineHeight = '1.5';
text.style.whiteSpace = 'pre-wrap';
const input = document.createElement('input');
input.type = 'text';
input.value = defaultValue;
input.style.width = '100%';
input.style.marginBottom = '20px';
input.style.padding = '10px';
input.style.boxSizing = 'border-box';
input.style.border = '1px solid #ccc';
input.style.borderRadius = '4px';
const btnContainer = document.createElement('div');
btnContainer.style.display = 'flex';
btnContainer.style.justifyContent = 'flex-end';
btnContainer.style.gap = '10px';
const cancelBtn = document.createElement('button');
cancelBtn.textContent = 'Abbrechen';
cancelBtn.style.padding = '8px 16px';
cancelBtn.style.cursor = 'pointer';
cancelBtn.style.border = '1px solid #ccc';
cancelBtn.style.backgroundColor = '#f8f9fa';
cancelBtn.style.color = '#333';
cancelBtn.style.borderRadius = '4px';
const okBtn = document.createElement('button');
okBtn.textContent = 'OK';
okBtn.style.padding = '8px 16px';
okBtn.style.cursor = 'pointer';
okBtn.style.border = 'none';
okBtn.style.backgroundColor = '#007bff';
okBtn.style.color = '#fff';
okBtn.style.borderRadius = '4px';
const closeAndResolve = (val) => {
document.body.removeChild(overlay);
resolve(val);
};
cancelBtn.onclick = () => closeAndResolve(null);
okBtn.onclick = () => closeAndResolve(input.value);
input.onkeydown = (e) => {
if (e.key === 'Enter') closeAndResolve(input.value);
if (e.key === 'Escape') closeAndResolve(null);
};
btnContainer.appendChild(cancelBtn);
btnContainer.appendChild(okBtn);
box.appendChild(text);
box.appendChild(input);
box.appendChild(btnContainer);
overlay.appendChild(box);
document.body.appendChild(overlay);
input.focus();
input.select();
});
}
// =========================================================================
// 1. GLOBAL STATE DEFINITIONS
// =========================================================================
@@ -884,7 +1077,7 @@
name: "Live",
type: "LiveStream",
// This MUST match the inner div where the video should appear
target: document.querySelector('.library-scan-reader'),
target: document.querySelector('.library-scan-reader'),
constraints: {
width: 640,
height: 480,
@@ -897,7 +1090,7 @@
},
decoder: {
// Keep only the barcode types you actually use to improve performance
readers: ["code_128_reader", "ean_reader", "code_39_reader"]
readers: ["code_128_reader", "ean_reader", "code_39_reader"]
},
locate: true
}, function(err) {
@@ -1243,31 +1436,39 @@
// =========================================================================
// 4. UI INTERACTIONS & UTILITIES
// =========================================================================
function borrowItem(itemId) {
async function borrowItem(itemId) {
const selectedItem = (libraryItems || []).find(item => item._id === itemId);
if (selectedItem && selectedItem.LibraryDisplayStatus === 'damaged') {
alert('Dieses Medium ist als defekt/zerstört markiert und kann nicht ausgeliehen werden.');
await customAlert('Dieses Medium ist als defekt/zerstört markiert und kann nicht ausgeliehen werden.');
return;
}
const defaultCardId = activeStudentCardId || '';
const cardId = (window.prompt('Bitte Bibliotheksausweis-ID eingeben:', defaultCardId) || '').trim().toUpperCase();
const promptCardResult = await customPrompt('Bitte Bibliotheksausweis-ID eingeben:', defaultCardId);
if (promptCardResult === null) return;
const cardId = promptCardResult.trim().toUpperCase();
if (!cardId) {
alert('Ausleihe abgebrochen: Für Bibliotheksmedien ist eine gültige Bibliotheksausweis-ID erforderlich.');
await customAlert('Ausleihe abgebrochen: Für Bibliotheksmedien ist eine gültige Bibliotheksausweis-ID erforderlich.');
return;
}
setActiveStudentCard(cardId);
const durationInput = (window.prompt('Ausleihdauer in Tagen (optional):') || '').trim();
const promptDurationResult = await customPrompt('Ausleihdauer in Tagen (optional):');
if (promptDurationResult === null) return;
const durationInput = promptDurationResult.trim();
const maxAvailable = Math.max(1, parseInt(selectedItem?.AvailableGroupedCount || selectedItem?.Quantity || 1, 10) || 1);
const countPrompt = (window.prompt(`Anzahl ausleihen? (Standard: 1, verfügbar: ${maxAvailable})`, '1') || '').trim();
let borrowCount = parseInt(countPrompt || '1', 10);
const countPromptResult = await customPrompt(`Anzahl ausleihen? (Standard: 1, verfügbar: ${maxAvailable})`, '1');
if (countPromptResult === null) return;
let borrowCount = parseInt(countPromptResult.trim() || '1', 10);
if (!Number.isFinite(borrowCount) || borrowCount < 1) {
borrowCount = 1;
}
if (borrowCount > maxAvailable) {
alert(`Es sind nur ${maxAvailable} Exemplar(e) verfügbar.`);
await customAlert(`Es sind nur ${maxAvailable} Exemplar(e) verfügbar.`);
return;
}
+62 -8
View File
@@ -53,8 +53,11 @@
<option value="desc">Absteigend</option>
</select>
</div>
<div class="col-md-2">
<button class="btn btn-secondary w-100" onclick="resetFilters()">Filter zurücksetzen</button>
<div class="col-md-3">
<div class="d-flex gap-2">
<button class="btn btn-secondary flex-grow-1" onclick="resetFilters()">Zurücksetzen</button>
<button class="btn btn-success flex-grow-1" onclick="exportFilteredUsers()">Exportieren</button>
</div>
</div>
</div>
<div class="mt-2 text-muted small" id="filter-count"></div>
@@ -82,19 +85,20 @@
<td>{{ permission_presets.get(user.permission_preset, {}).get('label', user.permission_preset) }}</td>
<td class="actions">
<form method="POST" action="{{ url_for('delete_user') }}" class="d-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="username" value="{{ user.username }}">
<button type="submit" class="btn btn-danger btn-sm" onclick="return confirm('Sind Sie sicher, dass Sie den Benutzer {{ user.username }} löschen möchten?')">
Löschen
</button>
</form>
<button type="button" class="btn btn-primary btn-sm"
<button type="button" class="btn btn-primary btn-sm"
data-username="{{ user.username }}"
data-firstname="{{ user.name if user.name else '' }}"
data-lastname="{{ user.last_name if user.last_name else '' }}"
onclick="openEditUserModal(this)">
Bearbeiten
</button>
<button type="button" class="btn btn-warning btn-sm"
<button type="button" class="btn btn-warning btn-sm"
onclick="openResetPasswordModal('{{ user.username }}')">
Passwort zurücksetzen
</button>
@@ -124,6 +128,7 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="POST" action="{{ url_for('admin_update_user_permissions') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="modal-body permissions-modal-body">
<input type="hidden" id="perm-username" name="username">
<div class="mb-3">
@@ -180,6 +185,7 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="POST" action="{{ url_for('admin_update_user_name') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="modal-body">
<input type="hidden" id="edit-username" name="username">
<div class="mb-3">
@@ -213,6 +219,7 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="POST" action="{{ url_for('admin_reset_user_password') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="modal-body">
<input type="hidden" id="reset-username" name="username">
<div class="mb-3">
@@ -313,6 +320,54 @@
applyFilters();
}
function exportFilteredUsers() {
var table = document.getElementById('user-table');
var tbody = table.querySelector('tbody');
var rows = Array.from(tbody.querySelectorAll('tr'));
var visibleUsernames = [];
// Sammle alle Benutzernamen, die durch den Filter aktuell NICHT versteckt sind
rows.forEach(function(row) {
if (row.style.display !== 'none') {
var cells = row.querySelectorAll('td');
if (cells.length > 0) {
visibleUsernames.push(cells[0].textContent.trim());
}
}
});
if (visibleUsernames.length === 0) {
alert('Keine Benutzer zum Exportieren vorhanden.');
return;
}
// Dynamisches Formular erstellen
var form = document.createElement('form');
form.method = 'POST';
form.action = "{{ url_for('export_users_csv') }}";
// 1. CSRF-Token als Wert (ohne Klammern) übergeben
var csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = 'csrf_token';
csrfInput.value = "{{ csrf_token }}";
form.appendChild(csrfInput);
// 2. Gefilterte Benutzernamen hinzufügen
var input = document.createElement('input');
input.type = 'hidden';
input.name = 'usernames';
input.value = JSON.stringify(visibleUsernames);
form.appendChild(input);
document.body.appendChild(form);
form.submit();
// Aufräumen
document.body.removeChild(form);
}
document.addEventListener('DOMContentLoaded', function() {
applyFilters();
document.getElementById('filter-search').addEventListener('input', applyFilters);
@@ -332,12 +387,12 @@
var username = button.getAttribute('data-username');
var name = button.getAttribute('data-firstname');
var lastName = button.getAttribute('data-lastname');
document.getElementById('edit-username').value = username;
document.getElementById('edit-username-display').value = username;
document.getElementById('edit-name').value = name;
document.getElementById('edit-last-name').value = lastName;
var modal = new bootstrap.Modal(document.getElementById('editUserModal'));
modal.show();
}
@@ -345,8 +400,7 @@
function openResetPasswordModal(username) {
document.getElementById('reset-username').value = username;
document.getElementById('username-display').value = username;
// Open modal using Bootstrap
var modal = new bootstrap.Modal(document.getElementById('resetPasswordModal'));
modal.show();
}