Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2942bcc380 | |||
| 349337e75f | |||
| 9bdd8261fe | |||
| 04c8505531 | |||
| c9edf4a4c8 | |||
| 0e0a99569e | |||
| 6e94ca20c4 | |||
| 72fdfb0898 | |||
| 9de09a2f41 | |||
| 3d5a81824f | |||
| b0344627a7 | |||
| 8d07577220 | |||
| f3c7b6cd55 | |||
| c2bfed710d | |||
| 5d90eab154 | |||
| 1eadfff9e1 | |||
| 18f096b3f2 | |||
| 4b895a52ab | |||
| 3a504afe37 | |||
| fb2ba86140 | |||
| 72e8a8cf5d | |||
| a158d16f62 | |||
| b8347b80f2 | |||
| 31356e7178 | |||
| 0409813cbe | |||
| 95e02b7a75 | |||
| 3b0d499958 | |||
| a7494648d8 | |||
| 8d8609fd70 | |||
| 18c831eb42 |
+81
-151
@@ -1560,7 +1560,7 @@ def update_appointment_statuses():
|
||||
pass
|
||||
|
||||
# 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:
|
||||
if days_overdue >= (int(student_ausleih_dauer) * 2) and mahnstufe == 1 and card.get('is_blocked') is not True:
|
||||
ausleihungen.update_one(
|
||||
{'_id': appt['_id']},
|
||||
{'$set': {'Mahnstufe': 2, 'LastUpdated': current_time}}
|
||||
@@ -2301,7 +2301,12 @@ def _upload_student_cards_excel():
|
||||
row_errors.append('Vorname und Nachname fehlen')
|
||||
|
||||
if not ausweis_id and student_name:
|
||||
ausweis_id = generate_ausweis_id(existing_ids)
|
||||
ausweis_id = generate_ausweis_id()
|
||||
if ausweis_id in existing_ids:
|
||||
row_errors.append(f'Automatisch erzeugte Ausweis-ID {ausweis_id} existiert bereits')
|
||||
else:
|
||||
while ausweis_id in existing_ids:
|
||||
ausweis_id = generate_ausweis_id()
|
||||
validation_warnings.append((row_number, f'Ausweis-ID wurde automatisch erzeugt: {ausweis_id}'))
|
||||
existing_ids.add(ausweis_id.upper())
|
||||
elif ausweis_id and not rollover_mode:
|
||||
@@ -3501,144 +3506,6 @@ def library_loans_admin():
|
||||
if client:
|
||||
client.close()
|
||||
|
||||
|
||||
def generate_test_ausleihen():
|
||||
"""
|
||||
Generiert Testdaten für die Überprüfung der Mahnungs- und Sperrlogik.
|
||||
Erstellt Dummy-Schülerausweise, Bibliotheks-Items und Ausleihungen mit manipulierten Daten (DueDate).
|
||||
"""
|
||||
current_time = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
|
||||
try:
|
||||
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
|
||||
db = client[MONGODB_DB]
|
||||
ausleihungen = db['ausleihungen']
|
||||
student_cards_col = db['student_cards']
|
||||
items_col = db['items']
|
||||
|
||||
print("Generiere Testdaten für das Mahnungssystem...")
|
||||
|
||||
# 1. Test-Schülerausweise erstellen oder aktualisieren (upsert)
|
||||
# Wir nutzen AusweisIds, die im System als Identifikation dienen
|
||||
test_students = [
|
||||
{
|
||||
"AusweisId": "TEST-AUSWEIS-01",
|
||||
"SchülerName": encrypt_text("Max Mustermann (Stufe 1 Test)"),
|
||||
"Klasse": "9a",
|
||||
"is_blocked": False
|
||||
},
|
||||
{
|
||||
"AusweisId": "TEST-AUSWEIS-02",
|
||||
"SchülerName": encrypt_text("Erika Musterfrau (Stufe 2 Test)"),
|
||||
"Klasse": "10b",
|
||||
"is_blocked": False
|
||||
},
|
||||
{
|
||||
"AusweisId": "TEST-AUSWEIS-03",
|
||||
"SchülerName": encrypt_text("Tom Frisch (Keine Mahnung Test)"),
|
||||
"Klasse": "8c",
|
||||
"is_blocked": False
|
||||
}
|
||||
]
|
||||
|
||||
for s in test_students:
|
||||
student_cards_col.update_one(
|
||||
{'AusweisId': s['AusweisId']},
|
||||
{'$set': s},
|
||||
upsert=True
|
||||
)
|
||||
|
||||
# 2. Test-Items als Bibliotheksartikel erstellen (ItemType wird gesetzt, damit it.is_library_itemgreift)
|
||||
# Passe LIBRARY_ITEM_TYPES an deine verwendeten Typen an (hier beispielhaft 'Buch' / 'Medien')
|
||||
item_type = LIBRARY_ITEM_TYPES[0] if 'LIBRARY_ITEM_TYPES' in globals() and LIBRARY_ITEM_TYPES else 'Buch'
|
||||
|
||||
item1_res = items_col.insert_one({
|
||||
"Name": "Biologie Buch (Test Stufe 1)",
|
||||
"ItemType": item_type,
|
||||
"Verfuegbar": False,
|
||||
"Exemplare": 1,
|
||||
"Code_4": "TR-9988"
|
||||
})
|
||||
item2_res = items_col.insert_one({
|
||||
"Name": "iPad Pro (Test Stufe 2)",
|
||||
"ItemType": item_type,
|
||||
"Verfuegbar": False,
|
||||
"Exemplare": 1,
|
||||
"Code_4": "TR-9988"
|
||||
})
|
||||
item3_res = items_col.insert_one({
|
||||
"Name": "Taschenrechner (Test Frisch)",
|
||||
"ItemType": item_type,
|
||||
"Verfuegbar": False,
|
||||
"Exemplare": 1,
|
||||
"Code_4": "TR-9988"
|
||||
})
|
||||
|
||||
# 3. Test-Ausleihungen generieren
|
||||
# Fall 1: 15 Tage drüber, Mahnstufe 0 -> Sollte Stufe 1 triggern (>= 14 Tage)
|
||||
due_date_stufe1 = current_time - datetime.timedelta(days=15)
|
||||
|
||||
# Fall 2: 29 Tage drüber, Mahnstufe 1 -> Sollte Stufe 2 triggern & Ausweis sperren (>= 28 Tage)
|
||||
due_date_stufe2 = current_time - datetime.timedelta(days=29)
|
||||
|
||||
# Fall 3: 5 Tage drüber, Mahnstufe 0 -> Sollte NICHTS tun (da < 14 Tage)
|
||||
due_date_frisch = current_time - datetime.timedelta(days=5)
|
||||
|
||||
# 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")
|
||||
user_val_2 = encrypt_text("TEST-AUSWEIS-02")
|
||||
user_val_3 = encrypt_text("TEST-AUSWEIS-03")
|
||||
|
||||
test_ausleihungen = [
|
||||
{
|
||||
"User": user_val_1,
|
||||
"Item": str(item1_res.inserted_id),
|
||||
"Status": "active",
|
||||
"DueDate": due_date_stufe1,
|
||||
"Mahnstufe": 0,
|
||||
"LastUpdated": current_time
|
||||
},
|
||||
{
|
||||
"User": user_val_2,
|
||||
"Item": str(item2_res.inserted_id),
|
||||
"Status": "active",
|
||||
"DueDate": due_date_stufe2,
|
||||
"Mahnstufe": 1, # Hat bereits Stufe 1, wartet auf Eskalation zu Stufe 2
|
||||
"LastUpdated": due_date_stufe2
|
||||
},
|
||||
{
|
||||
"User": user_val_3,
|
||||
"Item": str(item3_res.inserted_id),
|
||||
"Status": "active",
|
||||
"DueDate": due_date_frisch,
|
||||
"Mahnstufe": 2,
|
||||
"LastUpdated": current_time
|
||||
}
|
||||
]
|
||||
|
||||
# In die Datenbank einfügen
|
||||
ausleihungen.insert_many(test_ausleihungen)
|
||||
|
||||
print("Erfolgreich! Folgende Testdaten wurden für das Mahnungssystem in die DB geschrieben:")
|
||||
print(f"- Fall 1 (Mahnstufe 1 Trigger): Ausweis 'TEST-AUSWEIS-01', fällig war am {due_date_stufe1.strftime('%Y-%m-%d')} (15 Tage überfällig)")
|
||||
print(f"- Fall 2 (Mahnstufe 2 & Sperre): Ausweis 'TEST-AUSWEIS-02', fällig war am {due_date_stufe2.strftime('%Y-%m-%d')} (29 Tage überfällig)")
|
||||
print(f"- Fall 3 (Zu früh / Ignorieren): Ausweis 'TEST-AUSWEIS-03', fällig war am {due_date_frisch.strftime('%Y-%m-%d')} (5 Tage überfällig)")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Fehler beim Erstellen der Testdaten: {e}")
|
||||
finally:
|
||||
if client:
|
||||
client.close()
|
||||
|
||||
@app.route('/test_mahnungen')
|
||||
def test_mahnungen():
|
||||
# Testdaten (Mock-Daten), um alle if/else Bedingungen im HTML zu testen
|
||||
generate_test_ausleihen()
|
||||
create_return_reminders()
|
||||
return redirect(url_for('mahnungen_admin'))
|
||||
|
||||
|
||||
@app.route('/mahnungen_admin')
|
||||
def mahnungen_admin():
|
||||
"""Admin-Übersicht für überfällige Bibliotheks-Ausleihen."""
|
||||
@@ -3758,8 +3625,8 @@ def mahnungen_admin():
|
||||
email_service_enabled=cfg.EMAIL_ENABLED
|
||||
)
|
||||
|
||||
@app.route('/mahnungen_reset', methods=['POST'])
|
||||
def mahnungen_reset():
|
||||
@app.route('/mahnungen_reset/<int:days>', methods=['POST'])
|
||||
def mahnungen_reset(days):
|
||||
"""Setzt die Mahnstufe einer Ausleihe zurück, verlängert die Frist und entsperrt den Schülerausweis."""
|
||||
if 'username' not in session:
|
||||
return jsonify({'success': False, 'message': 'Nicht angemeldet.'}), 401
|
||||
@@ -3787,7 +3654,7 @@ def mahnungen_reset():
|
||||
return jsonify({'success': False, 'message': 'Ausleihe nicht gefunden.'}), 404
|
||||
|
||||
current_time = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
new_due_date = current_time + datetime.timedelta(days=14) # Frist um 14 Tage verlängern
|
||||
new_due_date = current_time + datetime.timedelta(days=days) # Frist um die angegebene Anzahl an Tagen verlängern
|
||||
|
||||
# 2. Mahnstufe zurücksetzen und Frist verlängern
|
||||
ausleihungen_col.update_one(
|
||||
@@ -3821,7 +3688,7 @@ def mahnungen_reset():
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Mahnstufe zurückgesetzt, Frist um 14 Tage verlängert und Ausweis entsperrt.'
|
||||
'message': f'Mahnstufe zurückgesetzt, Frist um {days} Tage verlängert und Ausweis entsperrt.'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
@@ -4850,7 +4717,8 @@ def student_cards_admin():
|
||||
if ausweis_id:
|
||||
existing = student_cards.find_one({'AusweisId': ausweis_id})
|
||||
else:
|
||||
ausweis_id = generate_ausweis_id()
|
||||
generated_ausweis_id = generate_ausweis_id()
|
||||
ausweis_id = generated_ausweis_id
|
||||
existing = False
|
||||
if not ausweis_ident:
|
||||
ausweis_ident = ausweis_id
|
||||
@@ -4890,10 +4758,10 @@ def student_cards_admin():
|
||||
|
||||
return render_template(
|
||||
'student_cards_admin.html',
|
||||
student_cards=all_cards, # Hier all_cards übergeben
|
||||
student_cards=all_cards,
|
||||
edit_mode=edit_mode,
|
||||
form_data=form_data,
|
||||
available_classes=available_classes, # Enthält nun die lesbaren Klassen
|
||||
available_classes=available_classes,
|
||||
config=cfg.get_school_info(),
|
||||
email_service_enabled=cfg.MODULES.is_enabled('mail')
|
||||
)
|
||||
@@ -5729,6 +5597,8 @@ def get_items():
|
||||
favorites = set(session.get('favorites', []))
|
||||
|
||||
available_only = str(request.args.get('available_only', '')).strip().lower() in ('1', 'true', 'yes', 'on')
|
||||
search_term = str(request.args.get('search', '')).strip()
|
||||
search_scope = str(request.args.get('search_scope', 'all')).strip().lower()
|
||||
offset_raw = request.args.get('offset')
|
||||
limit_raw = request.args.get('limit')
|
||||
light_mode_param = str(request.args.get('light_mode', '')).strip().lower()
|
||||
@@ -5764,6 +5634,42 @@ def get_items():
|
||||
'Deleted': {'$ne': True},
|
||||
}
|
||||
|
||||
if search_term:
|
||||
search_pattern = re.escape(search_term[:200])
|
||||
if search_scope in ('description', 'title_description'):
|
||||
search_fields = ['Name', 'Beschreibung']
|
||||
else:
|
||||
search_fields = [
|
||||
'Name', 'Beschreibung', 'Code_4', 'ISBN',
|
||||
'Author', 'Autor',
|
||||
]
|
||||
|
||||
direct_matches = [{field: {'$regex': search_pattern, '$options': 'i'}}
|
||||
for field in search_fields]
|
||||
if search_scope not in ('description', 'title_description') and search_term.isdigit():
|
||||
direct_matches.append({'Code_4': int(search_term)})
|
||||
grouped_parent_ids = [
|
||||
child.get('ParentItemId')
|
||||
for child in items_col.find(
|
||||
{
|
||||
'IsGroupedSubItem': True,
|
||||
'Deleted': {'$ne': True},
|
||||
'$or': direct_matches,
|
||||
},
|
||||
{'ParentItemId': 1},
|
||||
)
|
||||
if child.get('ParentItemId')
|
||||
]
|
||||
if grouped_parent_ids:
|
||||
direct_matches.append({'_id': {'$in': grouped_parent_ids}})
|
||||
|
||||
base_query = {
|
||||
'$and': [
|
||||
base_query,
|
||||
{'$or': direct_matches},
|
||||
]
|
||||
}
|
||||
|
||||
total_count = items_col.count_documents(base_query)
|
||||
|
||||
light_projection = {
|
||||
@@ -5830,6 +5736,7 @@ def get_items():
|
||||
'ParentItemId': 1,
|
||||
'Code_4': 1,
|
||||
'Verfuegbar': 1,
|
||||
'User': 1,
|
||||
'Name': 1,
|
||||
}
|
||||
# Full mode: complete child data with all details
|
||||
@@ -5838,6 +5745,7 @@ def get_items():
|
||||
'ParentItemId': 1,
|
||||
'Code_4': 1,
|
||||
'Verfuegbar': 1,
|
||||
'User': 1,
|
||||
'Name': 1,
|
||||
'Images': 1,
|
||||
'ThumbnailInfo': 1,
|
||||
@@ -5865,13 +5773,26 @@ def get_items():
|
||||
grouped_units = [itm] + grouped_children
|
||||
available_units = []
|
||||
grouped_all_codes = []
|
||||
grouped_unit_states = []
|
||||
for unit in grouped_units:
|
||||
unit_id = str(unit['_id']) if not isinstance(unit['_id'], str) else unit['_id']
|
||||
unit_code = unit.get('Code_4')
|
||||
if unit_code is not None and str(unit_code).strip() != '':
|
||||
grouped_all_codes.append(str(unit_code).strip())
|
||||
normalized_code = str(unit_code).strip()
|
||||
grouped_all_codes.append(normalized_code)
|
||||
else:
|
||||
normalized_code = ''
|
||||
|
||||
grouped_unit_states.append({
|
||||
'id': unit_id,
|
||||
'code': normalized_code,
|
||||
'available': bool(unit.get('Verfuegbar', True)),
|
||||
'borrowed_by_current_user': bool(
|
||||
username and str(unit.get('User') or '') == str(username)
|
||||
),
|
||||
})
|
||||
|
||||
if unit.get('Verfuegbar', True):
|
||||
unit_id = str(unit['_id']) if not isinstance(unit['_id'], str) else unit['_id']
|
||||
code = unit.get('Code_4') or '-'
|
||||
available_units.append({
|
||||
'id': unit_id,
|
||||
@@ -5884,6 +5805,7 @@ def get_items():
|
||||
itm['AvailableGroupedCount'] = len(available_units)
|
||||
itm['GroupedAvailableUnits'] = available_units
|
||||
itm['GroupedAllCodes'] = grouped_all_codes
|
||||
itm['GroupedUnitStates'] = grouped_unit_states
|
||||
if grouped_count > 1:
|
||||
itm['Verfuegbar'] = len(available_units) > 0
|
||||
if available_only and not itm.get('Verfuegbar', False):
|
||||
@@ -8672,7 +8594,6 @@ def register_csv():
|
||||
page_permissions = None
|
||||
|
||||
try:
|
||||
app.logger.info(f"Creating user: {username} ({name} {last_name}) with preset: {permission_preset} and password: {password}")
|
||||
us.add_user(
|
||||
username,
|
||||
password,
|
||||
@@ -10908,7 +10829,9 @@ def search_word(word):
|
||||
if (term_lower in text.lower()) or (term_lower in title_text.lower()):
|
||||
_id = i.get("_id")
|
||||
if _id is not None:
|
||||
id_set.add(str(_id))
|
||||
item_id = str(_id)
|
||||
parent_id = i.get('ParentItemId')
|
||||
id_set.add(str(parent_id or item_id))
|
||||
|
||||
return jsonify({"success": True, "response": list(id_set)})
|
||||
except Exception as e:
|
||||
@@ -13770,6 +13693,13 @@ def upload_csv_batch():
|
||||
"images_failed": error_count
|
||||
}), 200
|
||||
|
||||
@app.route('/get/asv_export', methods=['GET'])
|
||||
def get_asv_export():
|
||||
if 'username' not in session:
|
||||
flash('Bitte melden Sie sich an.', 'error')
|
||||
return redirect(url_for('login'))
|
||||
return send_from_directory(app.static_folder, 'img/asv_export_invario_2026.exfj')
|
||||
|
||||
"""
|
||||
@app.route('/test_email')
|
||||
def test_email():
|
||||
|
||||
Binary file not shown.
@@ -78,11 +78,11 @@
|
||||
<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', 'Klassenstufe') }}" placeholder="z. B. Klassenstufe">
|
||||
<input type="text" id="filter_name_1" name="filter_name_1" value="{{ filter_names.get('1', 'Fachgebiet') }}" placeholder="z. B. Fachgebiet">
|
||||
</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', 'Fach') }}" placeholder="z. B. Fach">
|
||||
<input type="text" id="filter_name_2" name="filter_name_2" value="{{ filter_names.get('2', 'Klassenstufe') }}" placeholder="z. B. Klassenstufe">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="filter_name_3">Name Filter 3</label>
|
||||
|
||||
+23
-22
@@ -1135,12 +1135,7 @@
|
||||
<a class="nav-link quick-link-pill {% if current_path == url_for('terminplaner.configure') %}nav-active{% endif %}" href="{{ url_for('terminplaner.configure') }}" data-tutorial-tip="Neuen Terminplan erstellen und an Ihr Team versenden.">Neue Planung</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="nav-item" data-nav-fixed="true">
|
||||
<button type="button" class="btn btn-link nav-link px-3" data-theme-toggle aria-label="Dark Mode umschalten" aria-pressed="false" title="Theme umschalten">
|
||||
<span class="theme-icon-light" style="display: none;">☀️</span>
|
||||
<span class="theme-icon-dark" style="display: none;">🌙</span>
|
||||
</button>
|
||||
</li>
|
||||
|
||||
<li class="nav-item dropdown ms-lg-auto">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="termMoreDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false" title="Weitere Optionen">Mehr Optionen</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="termMoreDropdown">
|
||||
@@ -1199,6 +1194,13 @@
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="{{ url_for('change_password') }}">Passwort ändern</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li class="dropdown-item" data-nav-fixed="true">
|
||||
<button type="button" class="btn btn-link nav-link px-3" data-theme-toggle aria-label="Dark Mode umschalten" aria-pressed="false" title="Theme umschalten">
|
||||
<span class="theme-icon-light" style="display: none;">☀️</span>
|
||||
<span class="theme-icon-dark" style="display: none;">🌙</span>
|
||||
</button>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="{{ url_for('logout') }}">Logout</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -1228,12 +1230,6 @@
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<li class="nav-item" data-nav-fixed="true">
|
||||
<button type="button" class="btn btn-link nav-link px-3" data-theme-toggle aria-label="Dark Mode umschalten" aria-pressed="false" title="Theme umschalten">
|
||||
<span class="theme-icon-light" style="display: none;">☀️</span>
|
||||
<span class="theme-icon-dark" style="display: none;">🌙</span>
|
||||
</button>
|
||||
</li>
|
||||
|
||||
<li class="nav-item dropdown ms-lg-auto">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="invMoreDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false" title="Weitere Optionen">
|
||||
@@ -1305,8 +1301,6 @@
|
||||
<button class="function-search-btn" type="submit">Los</button>
|
||||
</form>
|
||||
</div>
|
||||
{% if current_tenant_db %}
|
||||
<span class="navbar-text tenant-badge" title="Aktive Tenant-Datenbank">{{ current_tenant_db }}</span>
|
||||
{% endif %}
|
||||
<span class="navbar-text text-light me-3">{{ session['username'] }}</span>
|
||||
<div class="dropdown me-2 user-menu-wrap">
|
||||
@@ -1321,6 +1315,13 @@
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="{{ url_for('change_password') }}">Passwort ändern</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li class="dropdown-item" data-nav-fixed="true">
|
||||
<button type="button" class="btn btn-link nav-link px-3" data-theme-toggle aria-label="Dark Mode umschalten" aria-pressed="false" title="Theme umschalten">
|
||||
<span class="theme-icon-light" style="display: none;">☀️</span>
|
||||
<span class="theme-icon-dark" style="display: none;">🌙</span>
|
||||
</button>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="{{ url_for('logout') }}">Logout</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -1329,7 +1330,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
{% if library_module_enabled and CURRENT_MODULE == 'library' %}
|
||||
<nav class="navbar navbar-expand-lg navbar-dark" id="libraryNavbar">
|
||||
@@ -1342,7 +1342,7 @@
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0" id="libraryNavList">
|
||||
{% if current_permissions.pages.get('library_view', False) %}
|
||||
<li class="nav-item" data-nav-fixed="true">
|
||||
<a class="nav-link {% if current_path == url_for('library_view') %}nav-active{% endif %}" href="{{ url_for('library_view') }}" data-tutorial-tip="Die Bibliothek zeigt Ihnen alle Medien und verfügbaren Bücher.">Medien/Ausleihe</a>
|
||||
<a class="nav-link {% if current_path == url_for('library_view') %}nav-active{% endif %}" href="{{ url_for('library_view') }}" data-tutorial-tip="Die Bibliothek zeigt Ihnen alle Medien und verfügbaren Bücher.">Ausleihe/Rückgabe</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if 'username' in session %}
|
||||
@@ -1359,12 +1359,6 @@
|
||||
<a class="nav-link nav-priority-link {% if current_path == url_for('library_admin') %}nav-active{% endif %}" href="{{ url_for('library_admin') }}" data-tutorial-tip="Neue Bibliotheksmedien können hier aufgenommen werden.">📖 Hochladen</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="nav-item" data-nav-fixed="true">
|
||||
<button type="button" class="btn btn-link nav-link px-3" data-theme-toggle aria-label="Dark Mode umschalten" aria-pressed="false" title="Theme umschalten">
|
||||
<span class="theme-icon-light" style="display: none;">☀️</span>
|
||||
<span class="theme-icon-dark" style="display: none;">🌙</span>
|
||||
</button>
|
||||
</li>
|
||||
|
||||
<li class="nav-item dropdown ms-lg-auto">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="libMoreDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false" title="Weitere Optionen">
|
||||
@@ -1431,6 +1425,13 @@
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="{{ url_for('change_password') }}">Passwort ändern</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li class="dropdown-item" data-nav-fixed="true">
|
||||
<button type="button" class="btn btn-link nav-link px-3" data-theme-toggle aria-label="Dark Mode umschalten" aria-pressed="false" title="Theme umschalten">
|
||||
<span class="theme-icon-light" style="display: none;">☀️</span>
|
||||
<span class="theme-icon-dark" style="display: none;">🌙</span>
|
||||
</button>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="{{ url_for('logout') }}">Logout</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -26,18 +26,27 @@
|
||||
<form method="POST" action="{{ url_for('change_password') }}">
|
||||
<div class="form-group">
|
||||
<label for="current_password">Aktuelles Passwort:</label>
|
||||
<input type="password" id="current_password" name="current_password" required>
|
||||
<div class="input-group">
|
||||
<input type="password" class="form-control" id="current_password" name="current_password" required>
|
||||
<button class="btn btn-outline-secondary" type="button" onclick="togglePasswordVisibility('current_password', this)">Anzeigen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="new_password">Neues Passwort:</label>
|
||||
<input type="password" id="new_password" name="new_password" required>
|
||||
<div class="input-group">
|
||||
<input type="password" class="form-control" id="new_password" name="new_password" required>
|
||||
<button class="btn btn-outline-secondary" type="button" onclick="togglePasswordVisibility('new_password', this)">Anzeigen</button>
|
||||
</div>
|
||||
<small class="form-text text-muted">Das Passwort sollte mindestens 6 Zeichen lang sein.</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="confirm_password">Neues Passwort bestätigen:</label>
|
||||
<input type="password" id="confirm_password" name="confirm_password" required>
|
||||
<div class="input-group">
|
||||
<input type="password" class="form-control" id="confirm_password" name="confirm_password" required>
|
||||
<button class="btn btn-outline-secondary" type="button" onclick="togglePasswordVisibility('confirm_password', this)">Anzeigen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
@@ -48,6 +57,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function togglePasswordVisibility(fieldId, button) {
|
||||
var passwordInput = document.getElementById(fieldId);
|
||||
|
||||
if (passwordInput.type === 'password') {
|
||||
passwordInput.type = 'text';
|
||||
button.textContent = 'Verbergen';
|
||||
} else {
|
||||
passwordInput.type = 'password';
|
||||
button.textContent = 'Anzeigen';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.password-change-form {
|
||||
max-width: 500px;
|
||||
@@ -74,7 +97,7 @@
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
input[type="password"] {
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ced4da;
|
||||
@@ -122,6 +145,16 @@
|
||||
.btn-secondary:hover {
|
||||
background-color: #5a6268;
|
||||
}
|
||||
|
||||
.btn-outline-secondary {
|
||||
border: 1px solid #ced4da;
|
||||
background-color: #f8f9fa;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.btn-outline-secondary:hover {
|
||||
background-color: #e2e6ea;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 12px;
|
||||
@@ -141,4 +174,4 @@
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@@ -278,8 +278,8 @@
|
||||
<div class="multi-filter">
|
||||
{% for idx in range(4) %}
|
||||
<div class="form-group">
|
||||
<label for="filter1-{{ idx + 1 }}">Wert {{ idx + 1 }}:</label>
|
||||
<select id="filter1-{{ idx + 1 }}" name="filter" class="filter-dropdown-select" data-selected="{{ item.Filter[idx] if item.Filter and item.Filter|length > idx else '' }}">
|
||||
<label for="filter2-{{ idx + 1 }}">Wert {{ idx + 1 }}:</label>
|
||||
<select id="filter2-{{ idx + 1 }}" name="filter" class="filter-dropdown-select" data-selected="{{ item.Filter[idx] if item.Filter and item.Filter|length > idx else '' }}">
|
||||
<option value="">-- Optional --</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -290,8 +290,8 @@
|
||||
<div class="multi-filter">
|
||||
{% for idx in range(4) %}
|
||||
<div class="form-group">
|
||||
<label for="filter2-{{ idx + 1 }}">Wert {{ idx + 1 }}:</label>
|
||||
<select id="filter2-{{ idx + 1 }}" name="filter2" class="filter-dropdown-select" data-selected="{{ item.Filter2[idx] if item.Filter2 and item.Filter2|length > idx else '' }}">
|
||||
<label for="filter1-{{ idx + 1 }}">Wert {{ idx + 1 }}:</label>
|
||||
<select id="filter1-{{ idx + 1 }}" name="filter2" class="filter-dropdown-select" data-selected="{{ item.Filter2[idx] if item.Filter2 and item.Filter2|length > idx else '' }}">
|
||||
<option value="">-- Optional --</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -133,6 +133,34 @@
|
||||
background: var(--ui-surface);
|
||||
}
|
||||
|
||||
/* Blue edge styling for inputs and action button */
|
||||
#activeStudentCard,
|
||||
#manualItemCode {
|
||||
border: 2px solid #3b82f6 !important;
|
||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.15);
|
||||
}
|
||||
|
||||
#activeStudentCard:focus,
|
||||
#manualItemCode:focus {
|
||||
outline: none;
|
||||
border-color: #2563eb !important;
|
||||
box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.25) !important;
|
||||
}
|
||||
|
||||
#manualActionBtn {
|
||||
background: #4f46e5 !important;
|
||||
color: white !important;
|
||||
border: 2px solid #3b82f6 !important;
|
||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;
|
||||
}
|
||||
|
||||
#manualActionBtn:hover {
|
||||
background: #4338ca !important;
|
||||
border-color: #2563eb !important;
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.3);
|
||||
}
|
||||
|
||||
.library-scan-status {
|
||||
margin-top: 10px;
|
||||
font-size: 0.92em;
|
||||
@@ -605,7 +633,6 @@
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
|
||||
.detail-gallery-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -665,18 +692,18 @@
|
||||
<div class="library-scan-controls">
|
||||
<select id="scanModeSelect" aria-label="Scan-Modus">
|
||||
<option value="card_only">Nur Ausweis erfassen</option>
|
||||
<option value="quick_toggle">Schnellmodus: Ausweis + Mediencode</option>
|
||||
<option value="quick_toggle">Schnellmodus: 1x Ausweis + 1x Mediencode</option>
|
||||
<option value="continuous">Dauermodus: 1x Ausweis, dann N Medien</option>
|
||||
<option value="return_only">Nur Rückgabe (nur Mediencode)</option>
|
||||
</select>
|
||||
<input type="text" id="activeStudentCard" placeholder="Aktiver Ausweis (gescannt)">
|
||||
<input type="text" id="manualItemCode" placeholder="Manueller Mediencode (optional)" style="min-width:180px;">
|
||||
<button id="resetCardBtn" class="button" type="button">Feld zurücksetzen</button>
|
||||
<button id="toggleScannerBtn" class="button" type="button">Scanner starten</button>
|
||||
<button id="toggleScannerBtn" class="button" type="button">Handyscanner starten</button>
|
||||
<button id="physicalScannerBtn" class="button" type="button" style="margin-left:6px;">
|
||||
Physischer Scanner starten
|
||||
</button>
|
||||
<button id="manualActionBtn" class="button" type="button" style="margin-left:6px; background:#4f46e5; color:white;">Code verarbeiten</button>
|
||||
<button id="manualActionBtn" class="button" type="button" style="margin-left:6px;">Code verarbeiten</button>
|
||||
</div>
|
||||
|
||||
<div class="library-scan-reader-wrap" id="scanReaderWrap" style="display: none; margin-top: 15px;">
|
||||
@@ -1392,6 +1419,7 @@
|
||||
keyboardScannerEnabled = false;
|
||||
keyboardScanBuffer = '';
|
||||
keyboardLastKeyAt = 0;
|
||||
setActiveStudentCard('');
|
||||
document.removeEventListener('keydown', keyboardScanKeydownHandler);
|
||||
const modal = document.getElementById('physicalScannerModal');
|
||||
if (modal) modal.style.display = 'none';
|
||||
|
||||
@@ -187,7 +187,11 @@
|
||||
{% 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
|
||||
<i class="bi bi-arrow-counterclockwise"></i> Zurücksetzen (14 Tage)
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-warning text-dark"
|
||||
onclick="resetMahnungCustom('{{ item.id }}', '{{ item.schueler_name }}')">
|
||||
<i class="bi bi-arrow-counterclockwise"></i> Zurücksetzen mit eigener Frist
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
@@ -210,7 +214,7 @@
|
||||
|
||||
<!-- E-Mail Modal -->
|
||||
<div class="modal fade" id="emailModal" tabindex="-1" aria-labelledby="emailModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="emailModalLabel">Mahnungs-E-Mail senden</h5>
|
||||
@@ -228,7 +232,7 @@
|
||||
<input type="email" class="form-control" id="modalEmail" placeholder="E-Mail-Adresse eingeben...">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="modalMessage" class="form-label">Nachricht</label>
|
||||
<label for="modalMessage" class="form-label">Nachrichten-Text</label>
|
||||
<textarea class="form-control" id="modalMessage" rows="4" placeholder="Optionaler Text..."></textarea>
|
||||
</div>
|
||||
</form>
|
||||
@@ -241,6 +245,31 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom Reset Days Modal -->
|
||||
<div class="modal fade" id="customResetModal" tabindex="-1" aria-labelledby="customResetModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="customResetModalLabel">Mahnung zurücksetzen</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Schließen"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="resetModalLoanId">
|
||||
<p>Möchten Sie die Mahnung für <strong id="resetModalStudentName"></strong> wirklich zurücksetzen?</p>
|
||||
<p class="text-muted small">Dadurch wird die Mahnstufe auf 0 gesetzt und der Schülerausweis entsperrt.</p>
|
||||
<div class="mb-3">
|
||||
<label for="resetModalDays" class="form-label">Frist verlängern um (Tage):</label>
|
||||
<input type="number" class="form-control" id="resetModalDays" value="14" min="1">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||
<button type="button" class="btn btn-warning" onclick="submitCustomReset()">Mahnung zurücksetzen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function openEmailModal(loanId, studentName, studentEmail) {
|
||||
document.getElementById('modalLoanId').value = loanId;
|
||||
@@ -292,7 +321,7 @@ function resetMahnung(loanId, studentName) {
|
||||
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '';
|
||||
|
||||
fetch('/mahnungen_reset', {
|
||||
fetch('/mahnungen_reset/14', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken },
|
||||
body: JSON.stringify({ loan_id: loanId })
|
||||
@@ -301,7 +330,46 @@ function resetMahnung(loanId, studentName) {
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert(data.message);
|
||||
location.reload(); // Seite neu laden, um die aktualisierte Ansicht zu sehen
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Fehler: ' + data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert('Fehler beim Senden der Anfrage.');
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
function resetMahnungCustom(loanId, studentName) {
|
||||
document.getElementById('resetModalLoanId').value = loanId;
|
||||
document.getElementById('resetModalStudentName').textContent = studentName;
|
||||
document.getElementById('resetModalDays').value = 14;
|
||||
|
||||
var customModal = new bootstrap.Modal(document.getElementById('customResetModal'));
|
||||
customModal.show();
|
||||
}
|
||||
|
||||
function submitCustomReset() {
|
||||
const loanId = document.getElementById('resetModalLoanId').value;
|
||||
const daysToExtend = parseInt(document.getElementById('resetModalDays').value, 10);
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '';
|
||||
|
||||
if (isNaN(daysToExtend) || daysToExtend < 1) {
|
||||
alert('Bitte geben Sie eine gültige Anzahl an Tagen ein.');
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`/mahnungen_reset/${daysToExtend}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken },
|
||||
body: JSON.stringify({ loan_id: loanId })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert(data.message);
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Fehler: ' + data.message);
|
||||
}
|
||||
|
||||
@@ -2343,7 +2343,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
<div class="filter-container">
|
||||
<div class="filter-group">
|
||||
<div class="filter-header">
|
||||
<label>{{ filter_names.get('2', 'Fach') }}</label>
|
||||
<label>{{ filter_names.get('1', 'Fach') }}</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>{{ filter_names.get('1', 'Klassenstufe') }}</label>
|
||||
<label>{{ filter_names.get('2', 'Klassenstufe') }}</label>
|
||||
<button type="button" class="filter-toggle" onclick="toggleFilterDropdown('filter2-dropdown')">▼</button>
|
||||
<button type="button" class="clear-filter" onclick="clearFilter(2)">Clear</button>
|
||||
</div>
|
||||
@@ -3206,9 +3206,12 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
});
|
||||
}
|
||||
|
||||
function loadItems(offset = 0, append = false) {
|
||||
function loadItems(offset = 0, append = false, searchTerm = '', searchScope = 'all') {
|
||||
// Keep list payload lightweight; full details are fetched on-demand in openItemQuick.
|
||||
return fetch(`{{ url_for('get_items') }}?offset=${offset}&limit=${MAIN_ADMIN_ITEMS_PAGE_SIZE}&light_mode=true`)
|
||||
const searchParams = searchTerm
|
||||
? `&search=${encodeURIComponent(searchTerm)}&search_scope=${encodeURIComponent(searchScope)}`
|
||||
: '';
|
||||
return fetch(`{{ url_for('get_items') }}?offset=${offset}&limit=${MAIN_ADMIN_ITEMS_PAGE_SIZE}&light_mode=true${searchParams}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const itemsContainer = document.querySelector('#items-container');
|
||||
@@ -3241,7 +3244,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
}
|
||||
pageItems.sort((a, b) => {
|
||||
const nameA = a.Name ? a.Name.toLowerCase() : '';
|
||||
const nameB = a.Name ? a.Name.toLowerCase() : '';
|
||||
const nameB = b.Name ? b.Name.toLowerCase() : '';
|
||||
return nameA.localeCompare(nameB);
|
||||
});
|
||||
|
||||
@@ -3285,6 +3288,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
card.dataset.code = (item.Code_4 !== undefined && item.Code_4 !== null) ?
|
||||
String(item.Code_4).trim() : '';
|
||||
card.dataset.groupCodes = JSON.stringify(Array.isArray(item.GroupedAllCodes) ? item.GroupedAllCodes : []);
|
||||
card.dataset.groupedUnits = JSON.stringify(Array.isArray(item.GroupedUnitStates) ? item.GroupedUnitStates : []);
|
||||
|
||||
// Add filter values to their respective sets
|
||||
filter1Array.forEach(value => {
|
||||
@@ -3415,7 +3419,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
</div>
|
||||
{% if current_permissions.actions.get('can_borrow', False) %}
|
||||
${isAvailableForBorrow && !item.BlockedNow ?
|
||||
`<form method="POST" action="{{ url_for('ausleihen', id='') }}${item._id}">
|
||||
`<form method="POST" action="{{ url_for('ausleihen', id='') }}${item._id}" data-grouped-borrow-form>
|
||||
${isGroupedItem ? `
|
||||
<div class="grouped-borrow-controls" style="display:flex; gap:8px; flex-wrap:wrap; margin-bottom:8px; align-items:center;">
|
||||
<label style="font-size:0.85rem; margin:0;">Anzahl:</label>
|
||||
@@ -3445,6 +3449,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
:
|
||||
`<button class="ausleihen disabled-button" disabled>${item.BlockedNow ? 'Reserviert' : 'Ausgeliehen'}</button>`
|
||||
}
|
||||
${isGroupedItem ? '<div class="grouped-searched-action" data-grouped-searched-action style="display:none;"></div>' : ''}
|
||||
{% endif %}
|
||||
{% if current_permissions.actions.get('can_delete', False) %}
|
||||
<form method="POST" action="{{ url_for('delete_item', id='') }}${item._id}" style="display:inline;" onsubmit="return confirm('Sind Sie sicher, dass Sie dieses Objekt löschen möchten?')">
|
||||
@@ -3679,13 +3684,66 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
card.classList.remove('highlight-item', 'code-match', 'exact-code-match');
|
||||
});
|
||||
|
||||
applyFilters(true);
|
||||
if (codeSearchTerm) {
|
||||
loadItems(0, false, codeSearchTerm, 'all');
|
||||
} else {
|
||||
loadItems();
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
searchInput.classList.remove('searching');
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function syncSearchedGroupedAction(card, searchTerm, exactCodeMatch) {
|
||||
const searchedAction = card.querySelector('[data-grouped-searched-action]');
|
||||
if (!searchedAction) return;
|
||||
|
||||
const borrowForm = card.querySelector('[data-grouped-borrow-form]');
|
||||
searchedAction.style.display = 'none';
|
||||
searchedAction.innerHTML = '';
|
||||
|
||||
if (!exactCodeMatch || !searchTerm) {
|
||||
if (borrowForm) borrowForm.style.display = '';
|
||||
return;
|
||||
}
|
||||
|
||||
let groupedUnits = [];
|
||||
try {
|
||||
groupedUnits = JSON.parse(card.dataset.groupedUnits || '[]');
|
||||
} catch {
|
||||
groupedUnits = [];
|
||||
}
|
||||
|
||||
const matchedUnit = groupedUnits.find(unit =>
|
||||
String(unit.code || '').trim().toLowerCase() === searchTerm
|
||||
);
|
||||
if (!matchedUnit) return;
|
||||
|
||||
if (matchedUnit.available) {
|
||||
if (borrowForm) borrowForm.style.display = '';
|
||||
const specificSelect = borrowForm?.querySelector('[name="specific_item_id"]');
|
||||
if (specificSelect) {
|
||||
specificSelect.value = matchedUnit.id;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (borrowForm) borrowForm.style.display = 'none';
|
||||
searchedAction.style.display = 'block';
|
||||
if (matchedUnit.borrowed_by_current_user) {
|
||||
searchedAction.innerHTML = `
|
||||
<form method="POST" action="/zurueckgeben/${encodeURIComponent(matchedUnit.id)}">
|
||||
<button class="ausleihen" type="submit">Zurückgeben (${escapeHtml(matchedUnit.code || 'Code')})</button>
|
||||
</form>`;
|
||||
} else {
|
||||
searchedAction.innerHTML = `
|
||||
<button class="ausleihen disabled-button" type="button" disabled>
|
||||
${escapeHtml(matchedUnit.code || 'Code')} ausgeliehen
|
||||
</button>`;
|
||||
}
|
||||
}
|
||||
|
||||
function applyFilters(isDirectSearch = false) {
|
||||
const itemsContainer = document.querySelector('#items-container');
|
||||
const items = itemsContainer.querySelectorAll('.item-card');
|
||||
@@ -3732,6 +3790,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
if (passesAllFilters && matchesCodeSearch && matchesDesc && (!favoritesOnly || isFavoriteCard)) {
|
||||
item.style.display = 'block';
|
||||
visibleCount++;
|
||||
syncSearchedGroupedAction(item, searchTerm, exactCodeMatch);
|
||||
|
||||
if (exactCodeMatch) {
|
||||
item.classList.add('highlight-item', 'exact-code-match');
|
||||
@@ -3756,6 +3815,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
}
|
||||
} else {
|
||||
item.style.display = 'none';
|
||||
syncSearchedGroupedAction(item, '', false);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3830,7 +3890,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
card.classList.remove('highlight-item', 'code-match', 'exact-code-match');
|
||||
});
|
||||
|
||||
applyFilters();
|
||||
loadItems();
|
||||
}
|
||||
|
||||
// Description search using backend substring match
|
||||
@@ -3839,21 +3899,12 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
const term = (input?.value || '').trim();
|
||||
if (!term){
|
||||
descSearchIds = null;
|
||||
applyFilters(true);
|
||||
loadItems(0, false, codeSearchTerm, 'all');
|
||||
return;
|
||||
}
|
||||
input?.classList.add('searching');
|
||||
fetch(`/search_word/${encodeURIComponent(term)}`)
|
||||
.then(r=>r.json())
|
||||
.then(data=>{
|
||||
if (data && data.success && Array.isArray(data.response)){
|
||||
descSearchIds = new Set(data.response.map(String));
|
||||
} else {
|
||||
descSearchIds = new Set();
|
||||
}
|
||||
applyFilters(true);
|
||||
})
|
||||
.catch(()=>{ descSearchIds = new Set(); applyFilters(true); })
|
||||
descSearchIds = null;
|
||||
loadItems(0, false, term, 'title_description')
|
||||
.finally(()=>{ setTimeout(()=> input?.classList.remove('searching'), 300); });
|
||||
}
|
||||
|
||||
@@ -3861,7 +3912,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
const input = document.getElementById('desc-search');
|
||||
if (input) input.value = '';
|
||||
descSearchIds = null;
|
||||
applyFilters();
|
||||
loadItems(0, false, codeSearchTerm, 'all');
|
||||
}
|
||||
|
||||
// Open item modal with fresh details from backend.
|
||||
@@ -4112,17 +4163,17 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
</div>
|
||||
|
||||
<div class="detail-group">
|
||||
<div class="detail-label">Unterrichtsfach:</div>
|
||||
<div class="detail-label">{{ filter_names.get('1', 'Unterrichtsfach') }}:</div>
|
||||
<div class="detail-value">${escapeHtml(filter1Array.join(', ') || '-')}</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-group">
|
||||
<div class="detail-label">Jahrgangsstufe:</div>
|
||||
<div class="detail-label">{{ filter_names.get('2', 'Jahrgangsstufe') }}:</div>
|
||||
<div class="detail-value">${escapeHtml(filter2Array.join(', ') || '-')}</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-group">
|
||||
<div class="detail-label">Schlagwort:</div>
|
||||
<div class="detail-label">{{ filter_names.get('3', 'Schlagwort') }}:</div>
|
||||
<div class="detail-value">${escapeHtml(filter3Array.join(', ') || '-')}</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
<h2 class="card-title h5 mb-0 fw-semibold text-primary">
|
||||
<i class="bi bi-funnel me-2"></i>{{ filter_names.get('1', 'Jahrgangsstufe') }}
|
||||
</h2>
|
||||
<span class="badge bg-primary-subtle text-primary rounded-pill">Filter 1</span>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<form method="POST" action="{{ url_for('add_filter_value', filter_num=1) }}" class="mb-4">
|
||||
@@ -89,7 +88,6 @@
|
||||
<h2 class="card-title h5 mb-0 fw-semibold text-primary">
|
||||
<i class="bi bi-funnel me-2"></i>{{ filter_names.get('2', 'Fachgebiet') }}
|
||||
</h2>
|
||||
<span class="badge bg-primary-subtle text-primary rounded-pill">Filter 2</span>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<form method="POST" action="{{ url_for('add_filter_value', filter_num=2) }}" class="mb-4">
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
<!--
|
||||
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 %}Bibliotheksausweise - Inventarsystem{% endblock %}
|
||||
@@ -21,7 +13,6 @@
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
/* Neu strukturiertes Layout für Formular & Import */
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
@@ -145,6 +136,38 @@
|
||||
background: #5a6268;
|
||||
}
|
||||
|
||||
/* Filter Controls Styling */
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 15px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
background: var(--ui-bg);
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.filter-bar input,
|
||||
.filter-bar select {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-bar input {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.filter-counter {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.cards-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
@@ -255,7 +278,8 @@
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] .student-card-form,
|
||||
:root[data-theme="dark"] .import-card {
|
||||
:root[data-theme="dark"] .import-card,
|
||||
:root[data-theme="dark"] .filter-bar {
|
||||
background: var(--ui-surface) !important;
|
||||
color: var(--ui-text) !important;
|
||||
border-color: var(--ui-border) !important;
|
||||
@@ -269,7 +293,8 @@
|
||||
:root[data-theme="dark"] .import-card p,
|
||||
:root[data-theme="dark"] .rollover-hint,
|
||||
:root[data-theme="dark"] .empty-state,
|
||||
:root[data-theme="dark"] .empty-state p {
|
||||
:root[data-theme="dark"] .empty-state p,
|
||||
:root[data-theme="dark"] .filter-counter {
|
||||
color: var(--ui-text-muted) !important;
|
||||
}
|
||||
|
||||
@@ -286,6 +311,8 @@
|
||||
:root[data-theme="dark"] .form-group input,
|
||||
:root[data-theme="dark"] .form-group select,
|
||||
:root[data-theme="dark"] .form-group textarea,
|
||||
:root[data-theme="dark"] .filter-bar input,
|
||||
:root[data-theme="dark"] .filter-bar select,
|
||||
:root[data-theme="dark"] .export-buttons form,
|
||||
:root[data-theme="dark"] .export-buttons select {
|
||||
background: var(--ui-bg) !important;
|
||||
@@ -297,6 +324,8 @@
|
||||
:root[data-theme="dark"] .form-group input:focus,
|
||||
:root[data-theme="dark"] .form-group select:focus,
|
||||
:root[data-theme="dark"] .form-group textarea:focus,
|
||||
:root[data-theme="dark"] .filter-bar input:focus,
|
||||
:root[data-theme="dark"] .filter-bar select:focus,
|
||||
:root[data-theme="dark"] .export-buttons select:focus {
|
||||
border-color: #60a5fa !important;
|
||||
box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.22) !important;
|
||||
@@ -334,6 +363,16 @@
|
||||
.export-buttons > a {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.filter-counter {
|
||||
margin-left: 0;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
@@ -474,6 +513,8 @@
|
||||
<h3 style="margin:0 0 8px 0;">Excel-Import</h3>
|
||||
<p style="margin:0 0 12px 0; color:#555; font-size:13px; line-height:1.4;">
|
||||
Laden Sie eine <strong>.xlsx</strong>- oder <strong>.csv</strong>-Datei hoch (z. B. aus <strong>ASV</strong>). Erkannt werden Rufame, Nachname, Klasse*, Ausweis-ID*, lokales Differenzierungsmerkmal*, Notizen* & Ausleihdauer*.
|
||||
Hinweis: Die Daten werden Validiert und müssen dann noch einmal aus sicherheits gründen hochgeladen werden!
|
||||
Im folgenden ist eine export vorlage für ihre ASV-Software hinterlegt: <a href="{{ url_for('get_asv_export') }}" target="_blank" style="color:#007bff; text-decoration:underline;">ASV Export Vorlage</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -503,7 +544,20 @@
|
||||
<div>
|
||||
<h2 style="margin-bottom: 15px;">Registrierte Ausweise</h2>
|
||||
{% if student_cards %}
|
||||
<table class="cards-table">
|
||||
<!-- Filter Options -->
|
||||
<div class="filter-bar">
|
||||
<input type="text" id="cardSearchInput" placeholder="🔍 Name oder Ausweis-ID suchen...">
|
||||
<select id="classFilterSelect">
|
||||
<option value="">Alle Klassen</option>
|
||||
{% for cls in available_classes %}
|
||||
<option value="{{ cls }}">{{ cls }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="button" id="clearFilterBtn" class="btn-cancel" style="padding: 8px 12px; font-size: 13px;">Zurücksetzen</button>
|
||||
<span class="filter-counter" id="filterCounter">Angezeigt: {{ student_cards|length }} / {{ student_cards|length }}</span>
|
||||
</div>
|
||||
|
||||
<table class="cards-table" id="cardsTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Ausweis-ID</th>
|
||||
@@ -516,7 +570,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for card in student_cards %}
|
||||
<tr>
|
||||
<tr class="card-row" data-id="{{ card.AusweisId|lower }}" data-name="{{ card.SchülerName|lower }}" data-class="{{ card.Klasse or '' }}">
|
||||
<td><strong>{{ card.AusweisId }}</strong></td>
|
||||
<td>{{ card.SchülerName }}</td>
|
||||
<td>{{ card.Klasse or '—' }}</td>
|
||||
@@ -546,6 +600,10 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div id="noResultsMsg" class="empty-state" style="display: none;">
|
||||
<p>Keine passende Ausweise für diesen Filter gefunden.</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<p>Keine Bibliotheksausweise registriert.</p>
|
||||
@@ -557,7 +615,56 @@
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/qrcode.js"></script>
|
||||
<script>
|
||||
// All PDF exports now go through backend routes
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const searchInput = document.getElementById('cardSearchInput');
|
||||
const classSelect = document.getElementById('classFilterSelect');
|
||||
const clearBtn = document.getElementById('clearFilterBtn');
|
||||
const filterCounter = document.getElementById('filterCounter');
|
||||
const rows = document.querySelectorAll('.card-row');
|
||||
const noResultsMsg = document.getElementById('noResultsMsg');
|
||||
const totalRows = rows.length;
|
||||
|
||||
function filterCards() {
|
||||
const query = searchInput ? searchInput.value.toLowerCase().trim() : '';
|
||||
const selectedClass = classSelect ? classSelect.value : '';
|
||||
let visibleCount = 0;
|
||||
|
||||
rows.forEach(row => {
|
||||
const id = row.getAttribute('data-id') || '';
|
||||
const name = row.getAttribute('data-name') || '';
|
||||
const cardClass = row.getAttribute('data-class') || '';
|
||||
|
||||
const matchesQuery = !query || id.includes(query) || name.includes(query);
|
||||
const matchesClass = !selectedClass || cardClass === selectedClass;
|
||||
|
||||
if (matchesQuery && matchesClass) {
|
||||
row.style.display = '';
|
||||
visibleCount++;
|
||||
} else {
|
||||
row.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
if (filterCounter) {
|
||||
filterCounter.textContent = `Angezeigt: ${visibleCount} / ${totalRows}`;
|
||||
}
|
||||
|
||||
if (noResultsMsg) {
|
||||
noResultsMsg.style.display = (visibleCount === 0 && totalRows > 0) ? 'block' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
if (searchInput) searchInput.addEventListener('input', filterCards);
|
||||
if (classSelect) classSelect.addEventListener('change', filterCards);
|
||||
|
||||
if (clearBtn) {
|
||||
clearBtn.addEventListener('click', function() {
|
||||
if (searchInput) searchInput.value = '';
|
||||
if (classSelect) classSelect.value = '';
|
||||
filterCards();
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
@@ -738,7 +738,7 @@
|
||||
{% if show_library_features %}
|
||||
<div class="upload-import-panel" style="border:1px solid #dbe4ee; border-radius:8px; padding:14px; margin-bottom:16px; background:#f8fbff;">
|
||||
<h3 style="margin:0 0 8px 0;">Excel-Import Bibliothek (Mehrere Bücher)</h3>
|
||||
<p style="margin:0 0 10px 0; color:#555;">Laden Sie eine <strong>.xlsx</strong>- oder <strong>.csv</strong>-Datei hoch. Spalten werden automatisch erkannt (z.B. Name, Ort, Beschreibung, ISBN, Code, Anzahl). Für den Bibliotheksimport ist eine gültige ISBN je Zeile erforderlich.</p>
|
||||
<p style="margin:0 0 10px 0; color:#555;">Laden Sie eine <strong>.xlsx</strong>- oder <strong>.csv</strong>-Datei hoch. Spalten werden automatisch erkannt (z.B. Name, Ort, Beschreibung, ISBN, Code, Anzahl). Für den Bibliotheksimport ist eine gültige ISBN je Zeile erforderlich. Hinweis: Die Daten werden Validiert und müssen dann noch einmal aus sicherheits gründen hochgeladen werden!</p>
|
||||
<form method="POST" action="{{ url_for('upload_library_excel') }}" enctype="multipart/form-data" style="display:flex; gap:10px; flex-wrap:wrap; align-items:center;">
|
||||
<input type="file" name="library_excel" accept=".xlsx,.csv" required>
|
||||
<button type="button" class="btn btn-link" onclick="downloadSampleCsv('library')">Beispiel-CSV herunterladen</button>
|
||||
@@ -750,7 +750,7 @@
|
||||
{% else %}
|
||||
<div class="upload-import-panel" style="border:1px solid #dbe4ee; border-radius:8px; padding:14px; margin-bottom:16px; background:#f8fbff;">
|
||||
<h3 style="margin:0 0 8px 0;">Excel-Import Inventar (Mehrere Artikel)</h3>
|
||||
<p style="margin:0 0 10px 0; color:#555;">Laden Sie eine <strong>.xlsx</strong>- oder <strong>.csv</strong>-Datei hoch. Spalten werden automatisch erkannt (z.B. Name, Ort, Beschreibung, Filter1/2/3, Kosten, Jahr, Code, Anzahl).</p>
|
||||
<p style="margin:0 0 10px 0; color:#555;">Laden Sie eine <strong>.xlsx</strong>- oder <strong>.csv</strong>-Datei hoch. Spalten werden automatisch erkannt (z.B. Name, Ort, Beschreibung, Filter1/2/3, Kosten, Jahr, Code, Anzahl). Hinweis: Die Daten werden Validiert und müssen dann noch einmal aus sicherheits gründen hochgeladen werden!</p>
|
||||
<form method="POST" action="{{ url_for('upload_inventory_excel') }}" enctype="multipart/form-data" style="display:flex; gap:10px; flex-wrap:wrap; align-items:center;">
|
||||
<input type="file" name="inventory_excel" accept=".xlsx,.csv" required>
|
||||
<button type="button" class="btn btn-link" onclick="downloadSampleCsv('inventory')">Beispiel-CSV herunterladen</button>
|
||||
@@ -865,38 +865,6 @@
|
||||
<!-- Normal Item Mode: Standard Filters -->
|
||||
<div class="filter-inputs">
|
||||
<h3>{{ filter_names.get('1', 'Jahrgangsstufe') }}</h3>
|
||||
<div class="multi-filter">
|
||||
<div class="form-group">
|
||||
<label for="filter1-1">Wert 1:</label>
|
||||
<select id="filter1-1" name="filter" class="filter-dropdown-select" required>
|
||||
<option value="">-- Bitte auswählen --</option>
|
||||
<!-- Options will be loaded by JavaScript -->
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="filter1-2">Wert 2:</label>
|
||||
<select id="filter1-2" name="filter" class="filter-dropdown-select">
|
||||
<option value="">-- Optional --</option>
|
||||
<!-- Options will be loaded by JavaScript -->
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="filter1-3">Wert 3:</label>
|
||||
<select id="filter1-3" name="filter" class="filter-dropdown-select">
|
||||
<option value="">-- Optional --</option>
|
||||
<!-- Options will be loaded by JavaScript -->
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="filter1-4">Wert 4:</label>
|
||||
<select id="filter1-4" name="filter" class="filter-dropdown-select">
|
||||
<option value="">-- Optional --</option>
|
||||
<!-- Options will be loaded by JavaScript -->
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>{{ filter_names.get('2', 'Fachgebiet') }}</h3>
|
||||
<div class="multi-filter">
|
||||
<div class="form-group">
|
||||
<label for="filter2-1">Wert 1:</label>
|
||||
@@ -928,6 +896,38 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>{{ filter_names.get('2', 'Fachgebiet') }}</h3>
|
||||
<div class="multi-filter">
|
||||
<div class="form-group">
|
||||
<label for="filter1-1">Wert 1:</label>
|
||||
<select id="filter1-1" name="filter" class="filter-dropdown-select" required>
|
||||
<option value="">-- Bitte auswählen --</option>
|
||||
<!-- Options will be loaded by JavaScript -->
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="filter1-2">Wert 2:</label>
|
||||
<select id="filter1-2" name="filter" class="filter-dropdown-select">
|
||||
<option value="">-- Optional --</option>
|
||||
<!-- Options will be loaded by JavaScript -->
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="filter1-3">Wert 3:</label>
|
||||
<select id="filter1-3" name="filter" class="filter-dropdown-select">
|
||||
<option value="">-- Optional --</option>
|
||||
<!-- Options will be loaded by JavaScript -->
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="filter1-4">Wert 4:</label>
|
||||
<select id="filter1-4" name="filter" class="filter-dropdown-select">
|
||||
<option value="">-- Optional --</option>
|
||||
<!-- Options will be loaded by JavaScript -->
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>{{ filter_names.get('3', 'Schlagwort') }}</h3>
|
||||
<div class="multi-filter">
|
||||
<div class="form-group">
|
||||
|
||||
@@ -228,7 +228,12 @@
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="new-password" class="form-label">Neues Passwort:</label>
|
||||
<input type="password" class="form-control" id="new-password" name="new_password" required>
|
||||
<div class="input-group">
|
||||
<input type="password" class="form-control" id="new-password" name="new_password" required>
|
||||
<button class="btn btn-outline-secondary" type="button" id="togglePasswordBtn" onclick="togglePasswordVisibility()">
|
||||
Anzeigen
|
||||
</button>
|
||||
</div>
|
||||
<div class="password-requirements small text-muted mt-1">
|
||||
<p>Das Passwort muss mindestens 6 Zeichen lang sein.</p>
|
||||
</div>
|
||||
@@ -397,14 +402,36 @@
|
||||
modal.show();
|
||||
}
|
||||
|
||||
function togglePasswordVisibility() {
|
||||
var passwordInput = document.getElementById('new-password');
|
||||
var toggleBtn = document.getElementById('togglePasswordBtn');
|
||||
|
||||
if (passwordInput.type === 'password') {
|
||||
passwordInput.type = 'text';
|
||||
toggleBtn.textContent = 'Verbergen';
|
||||
} else {
|
||||
passwordInput.type = 'password';
|
||||
toggleBtn.textContent = 'Anzeigen';
|
||||
}
|
||||
}
|
||||
|
||||
function openResetPasswordModal(username) {
|
||||
document.getElementById('reset-username').value = username;
|
||||
document.getElementById('username-display').value = username;
|
||||
|
||||
|
||||
var passwordInput = document.getElementById('new-password');
|
||||
var toggleBtn = document.getElementById('togglePasswordBtn');
|
||||
|
||||
// Reset password field state on modal open
|
||||
passwordInput.value = '';
|
||||
passwordInput.type = 'password';
|
||||
if (toggleBtn) {
|
||||
toggleBtn.textContent = 'Anzeigen';
|
||||
}
|
||||
|
||||
var modal = new bootstrap.Modal(document.getElementById('resetPasswordModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
function openPermissionsModal(button) {
|
||||
var username = button.getAttribute('data-username') || '';
|
||||
var preset = button.getAttribute('data-preset') || 'standard_user';
|
||||
|
||||
Reference in New Issue
Block a user