Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 333f9fc89a | |||
| a286236834 | |||
| 84cc8de833 | |||
| 4b3c7e646f | |||
| f9b013508c | |||
| 2a6c40eb5a | |||
| 432d6da798 | |||
| 2cdf0d2169 | |||
| 9a4d10def7 | |||
| 10ae98245c | |||
| 359a8f8ab1 | |||
| 3171455f55 | |||
| a35bb3e048 | |||
| a0409a03dc | |||
| 627b1b76c4 | |||
| cd199f105d |
+85
-20
@@ -1516,6 +1516,7 @@ def update_appointment_statuses():
|
||||
student_card = _decrypt_student_card_doc(card) if '_decrypt_student_card_doc' in globals() else card
|
||||
student_name = student_card.get('SchülerName', target_ausweis_id)
|
||||
student_class = student_card.get('Klasse', '—')
|
||||
student_ausleih_dauer = student_card.get('StandardAusleihdauer', 14)
|
||||
|
||||
# Gegenstandsdetails laden
|
||||
item_name = "Unbekannter Artikel"
|
||||
@@ -1530,8 +1531,8 @@ def update_appointment_statuses():
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# STUFE 2: >= 28 Tage überfällig -> Ausweis sperren, Stufe 2 setzen & Admins benachrichtigen
|
||||
if days_overdue >= 28 and mahnstufe < 2:
|
||||
# STUFE 2: >= 2 * student_ausleih_dauer Tage überfällig -> Ausweis sperren, Stufe 2 setzen & Admins benachrichtigen
|
||||
if days_overdue >= (int(student_ausleih_dauer) * 2) and mahnstufe < 2:
|
||||
ausleihungen.update_one(
|
||||
{'_id': appt['_id']},
|
||||
{'$set': {'Mahnstufe': 2, 'LastUpdated': current_time}}
|
||||
@@ -1573,7 +1574,7 @@ def update_appointment_statuses():
|
||||
app.logger.warning(f"Mahnstufe 2 & Ausweis-Sperre für Schülerausweis '{student_name}' ({target_ausweis_id}) gesetzt.")
|
||||
|
||||
# STUFE 1: >= 14 Tage überfällig -> Stufe 1 setzen & Admins benachrichtigen
|
||||
elif days_overdue >= 14 and mahnstufe == 0:
|
||||
elif days_overdue >= int(student_ausleih_dauer) and mahnstufe == 0:
|
||||
ausleihungen.update_one(
|
||||
{'_id': appt['_id']},
|
||||
{'$set': {'Mahnstufe': 1, 'LastUpdated': current_time}}
|
||||
@@ -3628,13 +3629,13 @@ def generate_test_ausleihen():
|
||||
|
||||
# Je nachdem, ob dein System die User-Felder in Ausleihungen verschlüsselt speichert:
|
||||
# Hier wird encrypt_text verwendet, falls die Funktion im Scope liegt.
|
||||
user_val_1 = encrypt_text("TEST-AUSWEIS-01") if 'encrypt_text' in globals() else "TEST-AUSWEIS-01"
|
||||
user_val_2 = encrypt_text("TEST-AUSWEIS-02") if 'encrypt_text' in globals() else "TEST-AUSWEIS-02"
|
||||
user_val_3 = encrypt_text("TEST-AUSWEIS-03") if 'encrypt_text' in globals() else "TEST-AUSWEIS-03"
|
||||
user_val_1 = encrypt_text("TEST-AUSWEIS-01")
|
||||
user_val_2 = encrypt_text("TEST-AUSWEIS-02")
|
||||
user_val_3 = encrypt_text("TEST-AUSWEIS-03")
|
||||
|
||||
test_ausleihungen = [
|
||||
{
|
||||
"User": encrypt_text(user_val_1),
|
||||
"User": user_val_1,
|
||||
"Item": str(item1_res.inserted_id),
|
||||
"Status": "active",
|
||||
"DueDate": due_date_stufe1,
|
||||
@@ -3642,7 +3643,7 @@ def generate_test_ausleihen():
|
||||
"LastUpdated": current_time
|
||||
},
|
||||
{
|
||||
"User": encrypt_text(user_val_2),
|
||||
"User": user_val_2,
|
||||
"Item": str(item2_res.inserted_id),
|
||||
"Status": "active",
|
||||
"DueDate": due_date_stufe2,
|
||||
@@ -3650,11 +3651,11 @@ def generate_test_ausleihen():
|
||||
"LastUpdated": due_date_stufe2
|
||||
},
|
||||
{
|
||||
"User": encrypt_text(user_val_3),
|
||||
"User": user_val_3,
|
||||
"Item": str(item3_res.inserted_id),
|
||||
"Status": "active",
|
||||
"DueDate": due_date_frisch,
|
||||
"Mahnstufe": 0,
|
||||
"Mahnstufe": 2,
|
||||
"LastUpdated": current_time
|
||||
}
|
||||
]
|
||||
@@ -3751,8 +3752,6 @@ def mahnungen_admin():
|
||||
item_name = item_doc.get('Name', item_id)
|
||||
item_code = item_doc.get('Code_4', '')
|
||||
|
||||
# ENTFERNT: item_name = f"{item_name} ({item_code})" - Wir übergeben es separat ans Frontend
|
||||
|
||||
raw_user = str(record.get('User') or '')
|
||||
decrypted_user = decrypt_text(raw_user)
|
||||
ausweis_id = (decrypted_user if decrypted_user else raw_user).strip().upper()
|
||||
@@ -3783,14 +3782,6 @@ def mahnungen_admin():
|
||||
'is_blocked': is_blocked
|
||||
})
|
||||
|
||||
|
||||
return render_template(
|
||||
'mahnungen_admin.html',
|
||||
overdue_items=overdue_list,
|
||||
# KORRIGIERT: 'overdue_items' statt 'overdue_list', damit es zum HTML passt
|
||||
library_module_enabled=cfg.MODULES.is_enabled('library'),
|
||||
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
|
||||
)
|
||||
except Exception as e:
|
||||
app.logger.error(f"Fehler beim Laden der Mahnungsverwaltung: {e}")
|
||||
flash('Fehler beim Laden der Mahnungsverwaltung.', 'error')
|
||||
@@ -3799,6 +3790,14 @@ def mahnungen_admin():
|
||||
if client:
|
||||
client.close()
|
||||
|
||||
return render_template(
|
||||
'mahnungen_admin.html',
|
||||
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'])
|
||||
def mahnungen_reset():
|
||||
"""Setzt die Mahnstufe einer Ausleihe zurück, verlängert die Frist und entsperrt den Schülerausweis."""
|
||||
@@ -8629,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():
|
||||
|
||||
@@ -17,7 +17,7 @@ def _build_smtp_client():
|
||||
smtp.starttls()
|
||||
smtp.ehlo()
|
||||
if cfg.EMAIL_USERNAME:
|
||||
smtp.login(cfg.EMAIL_USERNAME, cfg.EMAIL_PASSWORD or "")
|
||||
smtp.login("no-reply@invario-software.de", "#,EATwIn,68" or "")
|
||||
return smtp
|
||||
|
||||
|
||||
@@ -68,14 +68,20 @@ def send(email: list | str, subject: str, note: str, sender: str) -> bool:
|
||||
|
||||
msg = MIMEMultipart("alternative")
|
||||
msg["Subject"] = str(subject)
|
||||
msg["From"] = f"{sender} <{cfg.EMAIL_USERNAME}>"
|
||||
msg["From"] = f"{sender} <no-reply@invario-software.de>"
|
||||
msg["To"] = str(recipient)
|
||||
|
||||
msg.attach(MIMEText(text_content, "plain"))
|
||||
msg.attach(MIMEText(html_content, "html"))
|
||||
|
||||
#smtp.sendmail(
|
||||
# from_addr=cfg.EMAIL_USERNAME,
|
||||
# to_addrs=[recipient],
|
||||
# msg=msg.as_string()
|
||||
#)
|
||||
|
||||
smtp.sendmail(
|
||||
from_addr=cfg.EMAIL_USERNAME,
|
||||
from_addr="no-reply@invario-software.de",
|
||||
to_addrs=[recipient],
|
||||
msg=msg.as_string()
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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