Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4449007a15 | |||
| 6770d42f99 | |||
| 333f9fc89a | |||
| a286236834 | |||
| 84cc8de833 | |||
| 4b3c7e646f | |||
| f9b013508c | |||
| 2a6c40eb5a | |||
| 432d6da798 | |||
| 2cdf0d2169 | |||
| 9a4d10def7 |
+70
@@ -3203,6 +3203,8 @@ def home_admin():
|
||||
flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error')
|
||||
return redirect(url_for('library_view'))
|
||||
|
||||
filter_names = it.get_filter_names()
|
||||
|
||||
return render_template(
|
||||
'main_admin.html',
|
||||
username=session['username'],
|
||||
@@ -3212,6 +3214,7 @@ def home_admin():
|
||||
student_default_borrow_days=cfg.STUDENT_DEFAULT_BORROW_DAYS,
|
||||
student_max_borrow_days=cfg.STUDENT_MAX_BORROW_DAYS,
|
||||
school_info=_get_school_info_for_export(),
|
||||
filter_names=filter_names,
|
||||
open_item=request.args.get('open_item')
|
||||
)
|
||||
|
||||
@@ -3795,6 +3798,7 @@ def mahnungen_admin():
|
||||
overdue_items=overdue_list,
|
||||
library_module_enabled=cfg.MODULES.is_enabled('library'),
|
||||
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
|
||||
email_service_enabled=cfg.EMAIL_ENABLED
|
||||
)
|
||||
|
||||
@app.route('/mahnungen_reset', methods=['POST'])
|
||||
@@ -8627,6 +8631,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():
|
||||
|
||||
@@ -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': 'Klassenstufe',
|
||||
'2': 'Fach',
|
||||
'3': 'Schlagwort'
|
||||
}
|
||||
|
||||
def set_filter_name(filter_num, name):
|
||||
|
||||
@@ -78,15 +78,15 @@
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="filter_name_1">Name Filter 1</label>
|
||||
<input type="text" id="filter_name_1" name="filter_name_1" value="{{ filter_names.get('1', 'Fach/Kategorie') }}" placeholder="z. B. Fach/Kategorie">
|
||||
<input type="text" id="filter_name_1" name="filter_name_1" value="{{ filter_names.get('1', 'Klassenstufe') }}" placeholder="z. B. Klassenstufe">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="filter_name_2">Name Filter 2</label>
|
||||
<input type="text" id="filter_name_2" name="filter_name_2" value="{{ filter_names.get('2', 'System/Bereich') }}" placeholder="z. B. System/Bereich">
|
||||
<input type="text" id="filter_name_2" name="filter_name_2" value="{{ filter_names.get('2', 'Fach') }}" placeholder="z. B. Fach">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="filter_name_3">Name Filter 3</label>
|
||||
<input type="text" id="filter_name_3" name="filter_name_3" value="{{ filter_names.get('3', 'Typ/Art') }}" placeholder="z. B. Typ/Art">
|
||||
<input type="text" id="filter_name_3" name="filter_name_3" value="{{ filter_names.get('3', 'Schlagwort') }}" placeholder="z. B. Schlagwort">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -179,10 +179,12 @@
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<div class="btn-group" role="group">
|
||||
{% if mail_service_enabled %}
|
||||
<button type="button" class="btn btn-sm btn-outline-primary"
|
||||
onclick="openEmailModal('{{ item.id }}', '{{ item.schueler_name }}', '{{ item.email }}')">
|
||||
<i class="bi bi-envelope"></i> E-Mail
|
||||
</button>
|
||||
{% endif %}
|
||||
<button type="button" class="btn btn-sm btn-outline-warning text-dark"
|
||||
onclick="resetMahnung('{{ item.id }}', '{{ item.schueler_name }}')">
|
||||
<i class="bi bi-arrow-counterclockwise"></i> Zurücksetzen
|
||||
|
||||
@@ -2343,7 +2343,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
<div class="filter-container">
|
||||
<div class="filter-group">
|
||||
<div class="filter-header">
|
||||
<label>Unterrichtsfach:</label>
|
||||
<label>{{ filter_names.get('1', 'Klassenstufe') }}</label>
|
||||
<button type="button" class="filter-toggle" onclick="toggleFilterDropdown('filter1-dropdown')">▼</button>
|
||||
<button type="button" class="clear-filter" onclick="clearFilter(1)">Clear</button>
|
||||
</div>
|
||||
@@ -2357,7 +2357,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
|
||||
<div class="filter-group">
|
||||
<div class="filter-header">
|
||||
<label>Jahrgangsstufe:</label>
|
||||
<label>{{ filter_names.get('2', 'Fach') }}</label>
|
||||
<button type="button" class="filter-toggle" onclick="toggleFilterDropdown('filter2-dropdown')">▼</button>
|
||||
<button type="button" class="clear-filter" onclick="clearFilter(2)">Clear</button>
|
||||
</div>
|
||||
@@ -2371,7 +2371,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
|
||||
<div class="filter-group">
|
||||
<div class="filter-header">
|
||||
<label>Schlagwort:</label>
|
||||
<label>{{ filter_names.get('3', 'Schlagwort') }}</label>
|
||||
<button type="button" class="filter-toggle" onclick="toggleFilterDropdown('filter3-dropdown')">▼</button>
|
||||
<button type="button" class="clear-filter" onclick="clearFilter(3)">Clear</button>
|
||||
</div>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user