Compare commits

..

15 Commits

Author SHA1 Message Date
Aiirondev_dev 2942bcc380 Der Jinja-Fehler in base.html ist behoben. 2026-10-07 16:12:31 +02:00
Aiirondev_dev 349337e75f changes to dont reflekt the testing database name 2026-10-07 16:03:32 +02:00
Aiirondev_dev 9bdd8261fe changes to dont include the testing information 2026-10-07 16:02:39 +02:00
Aiirondev_dev 04c8505531 Die Suche wurde angepasst:Titel und Beschreibung werden jetzt direkt gemeinsam durchsucht.
Teiltreffer funktionieren, z. B.:
Rechen findet Rechenschiffchen
ein Fragment aus der Beschreibung findet den Artikel ebenfalls
Groß-/Kleinschreibung spielt keine Rolle
Die zusätzliche lokale ID-Filterung wurde entfernt, da sie Treffer teilweise wieder ausgeblendet hat.
Die Suche verwendet nun die serverseitige MongoDB-Abfrage mit regulären Teiltreffern.
2026-10-07 15:55:07 +02:00
Aiirondev_dev c9edf4a4c8 Die Inventarsuche wurde korrigiert. 2026-10-07 15:45:47 +02:00
Aiirondev_dev 0e0a99569e Implementation of the right Code ussage 2026-10-02 15:13:46 +02:00
Aiirondev_dev 6e94ca20c4 changes to the Mahnungssystem level fix 2026-09-27 14:40:21 +02:00
Aiirondev_dev 72fdfb0898 changes to the style of the mahnungs zurücksetzungs Modal 2026-09-27 14:35:01 +02:00
Aiirondev_dev 9de09a2f41 custom Mahnungs reset implementation 2026-09-27 14:20:44 +02:00
Aiirondev_dev 3d5a81824f changes to the Student Card clearing 2026-09-25 22:02:26 +02:00
Aiirondev_dev b0344627a7 removal of unused Code 2026-09-25 17:01:56 +02:00
Aiirondev_dev 8d07577220 changes to the asv export 2026-09-24 22:21:55 +02:00
Aiirondev_dev f3c7b6cd55 implementation of password visibility changing fuinction for the own password change 2026-09-24 17:26:25 +02:00
Aiirondev_dev c2bfed710d changes to the upload function 2026-09-24 16:46:32 +02:00
Aiirondev_dev 5d90eab154 changes to the edeting filter displaying 2026-09-24 16:40:21 +02:00
11 changed files with 290 additions and 216 deletions
+71 -146
View File
@@ -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}}
@@ -3506,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."""
@@ -3763,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
@@ -3792,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(
@@ -3826,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:
@@ -5735,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()
@@ -5770,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 = {
@@ -5836,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
@@ -5844,6 +5745,7 @@ def get_items():
'ParentItemId': 1,
'Code_4': 1,
'Verfuegbar': 1,
'User': 1,
'Name': 1,
'Images': 1,
'ThumbnailInfo': 1,
@@ -5871,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,
@@ -5890,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):
@@ -10913,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:
@@ -13775,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.
-3
View File
@@ -1301,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">
@@ -1332,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">
+38 -5
View File
@@ -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 %}
+4 -4
View File
@@ -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>
+1
View File
@@ -1419,6 +1419,7 @@
keyboardScannerEnabled = false;
keyboardScanBuffer = '';
keyboardLastKeyAt = 0;
setActiveStudentCard('');
document.removeEventListener('keydown', keyboardScanKeydownHandler);
const modal = document.getElementById('physicalScannerModal');
if (modal) modal.style.display = 'none';
+73 -5
View File
@@ -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);
}
+70 -19
View File
@@ -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.
-2
View File
@@ -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
View File
@@ -514,6 +514,7 @@
<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>
+32 -32
View File
@@ -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">