Compare commits

...

22 Commits

Author SHA1 Message Date
Aiirondev_dev 3840348a2d Changes to the formating and processing of the group codes
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-12 00:12:06 +02:00
Aiirondev_dev cdb7319c56 Changes so the edeting function can also adeit all the Codes and the entire mamunt of an itemgroup
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-11 23:58:23 +02:00
Aiirondev_dev 08bea97f0f Changes so the edeting function can also adeit all the Codes and the entire mamunt of an itemgroup
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-11 23:49:20 +02:00
Aiirondev_dev 9164cd030d Debug changes
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-11 23:07:13 +02:00
Aiirondev_dev 9227392787 Debug changes
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-11 22:56:40 +02:00
Aiirondev_dev 4917c22ae3 Debug changes
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-11 22:49:40 +02:00
Aiirondev_dev a2f2dd5a9e Debug changes
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-11 22:41:30 +02:00
Aiirondev_dev faf270ff93 Debug changes
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-11 22:35:43 +02:00
Aiirondev_dev beeb562ac4 Dubvug changes
Release Inventarsystem / release-docker (push) Successful in 2m32s
2026-08-11 22:21:28 +02:00
Aiirondev_dev 0199957545 Dubvug changes
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-11 21:35:08 +02:00
Aiirondev_dev a518adb054 Dubvug changes
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-11 21:27:39 +02:00
Aiirondev_dev 6a94d50d28 Additiopn to filter out the iduvidual items and
Release Inventarsystem / release-docker (push) Has been cancelled
2026-08-11 21:16:37 +02:00
Aiirondev_dev c90cef6dcf Addition of ort preloading for the edeting modal
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-11 15:30:26 +02:00
Aiirondev_dev b5451a4ef0 Addition of ort preloading for the edeting modal
Release Inventarsystem / release-docker (push) Successful in 2m18s
2026-08-11 15:12:17 +02:00
Aiirondev_dev a4afef8283 Fix of a stray redirect error
Release Inventarsystem / release-docker (push) Successful in 2m17s
2026-08-11 15:00:20 +02:00
Aiirondev_dev b2951eed6c Fix of broken authentication metode
Release Inventarsystem / release-docker (push) Has been cancelled
2026-08-11 10:46:38 +02:00
Aiirondev_dev 9a37c047c1 Fix of an missing parethasys
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-11 02:01:27 +02:00
Aiirondev_dev 7290fb4ed1 Fix of an missing parethasys
Release Inventarsystem / release-docker (push) Has been cancelled
2026-08-11 01:52:06 +02:00
Aiirondev_dev ee9ef3df6f Fix of an missing parethasys
Release Inventarsystem / release-docker (push) Has been cancelled
2026-08-11 01:09:53 +02:00
Aiirondev_dev 9f3799a77f Fix of an missing parethasys
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-11 00:57:51 +02:00
Aiirondev_dev e9006f5a07 Fix of an missing parethasys
Release Inventarsystem / release-docker (push) Has been cancelled
2026-08-11 00:38:48 +02:00
Aiirondev_dev a11bce17c5 Fixy of the api_library_items()
Release Inventarsystem / release-docker (push) Has been cancelled
2026-08-11 00:31:57 +02:00
6 changed files with 1425 additions and 170 deletions
+141 -4
View File
@@ -46,7 +46,7 @@ import Web.modules.inventarsystem.pdf_export as pdf_export
import Web.modules.inventarsystem.excel_export as excel_export import Web.modules.inventarsystem.excel_export as excel_export
import datetime import datetime
from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.schedulers.background import BackgroundScheduler
from bson.objectid import ObjectId from bson.objectid import ObjectId, InvalidId
from urllib.parse import urlparse, urlunparse from urllib.parse import urlparse, urlunparse
import requests import requests
import csv import csv
@@ -3313,7 +3313,7 @@ def api_library_items():
ausleihungen_db = db['ausleihungen'] ausleihungen_db = db['ausleihungen']
query = { query = {
'ItemType': {'$in': ['book', 'cd', 'dvd', 'schoolbook', 'schulbuch', 'Buch', 'Schulbuch']}, 'ItemType': {'$in': ['book', 'cd', 'CD', 'DVD', 'dvd', 'schoolbook', 'schulbuch', 'Buch', 'Schulbuch']},
'IsGroupedSubItem': {'$ne': True}, 'IsGroupedSubItem': {'$ne': True},
'Deleted': {'$ne': True} 'Deleted': {'$ne': True}
} }
@@ -3788,6 +3788,7 @@ def api_item_detail(item_id):
<h2>{html.escape(str(item.get('Name', 'Untitled')))}</h2> <h2>{html.escape(str(item.get('Name', 'Untitled')))}</h2>
<p><strong>ISBN:</strong> {html.escape(str(item.get('ISBN', item.get('Code4', '-'))))}</p> <p><strong>ISBN:</strong> {html.escape(str(item.get('ISBN', item.get('Code4', '-'))))}</p>
<p><strong>Anzahl:</strong> {html.escape(str(item.get('SeriesCount', '-')))}</p> <p><strong>Anzahl:</strong> {html.escape(str(item.get('SeriesCount', '-')))}</p>
<p><strong>Code:</strong> {html.escape(str(item.get('Code_4', '-')))}</p>
<p><strong>Ort:</strong> {html.escape(str(item.get('Ort', '-')))}</p> <p><strong>Ort:</strong> {html.escape(str(item.get('Ort', '-')))}</p>
<p><strong>Typ:</strong> {html.escape(str(item.get('ItemType', '-')))}</p> <p><strong>Typ:</strong> {html.escape(str(item.get('ItemType', '-')))}</p>
<p><strong>Kategorie:</strong> {html.escape(str(item.get('library_category', '-')))}</p> <p><strong>Kategorie:</strong> {html.escape(str(item.get('library_category', '-')))}</p>
@@ -5780,7 +5781,7 @@ def upload_item():
app.logger.warning('Audit write failed for library_item_created') app.logger.warning('Audit write failed for library_item_created')
flash(success_msg, 'success') flash(success_msg, 'success')
return redirect(url_for(success_redirect_endpoint, highlight_item=str(item_id))) return redirect(url_for(success_redirect_endpoint))
else: else:
error_msg = 'Fehler beim Hinzufügen des Elements' error_msg = 'Fehler beim Hinzufügen des Elements'
if is_mobile: if is_mobile:
@@ -6366,6 +6367,142 @@ def edit_item(id):
return redirect(url_for('home_admin')) return redirect(url_for('home_admin'))
@app.route('/item_edit/<id>', methods=['GET', 'POST'])
def item_edit(id):
"""
Endpoint zum Laden und Aktualisieren eines Eintrags (item_edit).
"""
if 'username' not in session:
if request.method == 'POST' and request.is_json:
return jsonify({'success': False, 'message': 'Nicht angemeldet.'}), 401
flash('Bitte melden Sie sich an.', 'error')
return redirect(url_for('login'))
current_permissions = us.get_effective_permissions(session['username'])
if not current_permissions['actions'].get('can_edit', False):
if request.method == 'POST' and request.is_json:
return jsonify({'success': False, 'message': 'Keine Berechtigung zum Bearbeiten.'}), 403
flash('Keine Berechtigung zum Bearbeiten.', 'error')
return redirect(url_for('home_admin'))
try:
obj_id = ObjectId(id)
except InvalidId:
flash('Ungültige Element-ID.', 'error')
return redirect(url_for('home_admin'))
# --- GET: Template anzeigen ---
if request.method == 'GET':
item = it.get_item(id)
if not item:
flash('Element nicht gefunden.', 'error')
return redirect(url_for('home_admin'))
item['_id'] = str(item['_id'])
# Codes für die Ansicht trennen (Basis-Code vs. Einzelcodes)
base_code = item.get('Code_4', '')
individual_codes = []
group_id = item.get('SeriesGroupId')
if group_id:
group_ids = it.get_group_item_ids(str(item['_id']))
if group_ids:
for gid in group_ids:
g_item = it.get_item(gid)
c4 = g_item.get('Code_4')
if c4 and c4 != base_code:
individual_codes.append(c4)
item['IndividualCodes'] = '\n'.join(individual_codes)
show_library = cfg.MODULES.is_enabled('library')
return render_template(
'item_edit.html',
username=session['username'],
item=item,
library_module_enabled=show_library,
show_library_features=show_library,
page_title=f"Bearbeiten: {item.get('Name', '')}"
)
# --- POST: Speichern ---
redirect_target = request.referrer or url_for('home_admin')
current_item = it.get_item(obj_id)
if not current_item:
flash('Element in der Datenbank nicht gefunden.', 'error')
return redirect(redirect_target)
# Formulardaten auslesen
name = sanitize_form_value(request.form.get('name'))
ort = sanitize_form_value(request.form.get('ort'))
beschreibung = sanitize_form_value(request.form.get('beschreibung'))
# Basis-Code & Einzelcodes aus der Textarea
code_4 = sanitize_form_value(request.form.get('code_4'))
individual_codes_raw = request.form.get('individual_codes', '')
individual_codes = []
for c in individual_codes_raw.replace('\r', '').split('\n'):
clean_c = sanitize_form_value(c)
if clean_c and clean_c != code_4 and clean_c not in individual_codes:
individual_codes.append(clean_c)
all_codes_to_check = [code_4] + individual_codes
# Uniqueness-Check über alle Codes
current_group_id = current_item.get('SeriesGroupId')
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db_instance = client[cfg.MONGODB_DB]
items_col = db_instance['items']
has_code_error = False
for code in all_codes_to_check:
if not code:
continue
existing = items_col.find_one({'Code_4': code, 'Deleted': {'$ne': True}})
if existing:
is_same_item = str(existing['_id']) == str(id)
is_in_same_group = current_group_id and existing.get('SeriesGroupId') == current_group_id
if not is_same_item and not is_in_same_group:
flash(f'Der Code "{code}" wird bereits von einem anderen Artikel verwendet.', 'error')
has_code_error = True
break
client.close()
if has_code_error:
return redirect(redirect_target)
# 1. Gruppen-Bestand über die korrigierte Helfer-Funktion synchronisieren
it.sync_group_codes(str(id), code_4, individual_codes)
# 2. Deine bestehende update_item Funktion aufrufen
success = it.update_item(
id=str(id),
name=name,
ort=ort,
beschreibung=beschreibung,
images=images,
verfuegbar=verfuegbar,
filter1=filter1,
filter2=filter2,
filter3=filter3,
ansch_jahr=anschaffungs_jahr,
ansch_kost=anschaffungs_kosten,
code_4=code_4,
reservierbar=reservierbar,
isbn=item_isbn,
item_type=item_type
)
if success:
flash('Artikel erfolgreich aktualisiert.', 'success')
else:
flash('Fehler beim Aktualisieren des Artikels.', 'error')
return redirect(redirect_target)
@app.route('/update_group', methods=['POST']) @app.route('/update_group', methods=['POST'])
def update_group(): def update_group():
@@ -7697,7 +7834,7 @@ def user_del():
last_name = "" last_name = ""
fullname = None fullname = None
users_list.append({ users_list.append({
'username': username, 'username': decrypt_text(username),
'admin': user.get('Admin', False), 'admin': user.get('Admin', False),
'fullname': fullname, 'fullname': fullname,
'name': name, 'name': name,
+120 -1
View File
@@ -20,6 +20,7 @@ Collection Structure:
""" """
from bson.objectid import ObjectId from bson.objectid import ObjectId
from bson.errors import InvalidId from bson.errors import InvalidId
import uuid
import datetime import datetime
import Web.modules.database.settings as cfg import Web.modules.database.settings as cfg
from Web.modules.database.settings import MongoClient from Web.modules.database.settings import MongoClient
@@ -1147,4 +1148,122 @@ def get_current_status(item_id, decrypt=True):
return None return None
except Exception as e: except Exception as e:
print(f"Error retrieving current status for item {item_id}: {e}") print(f"Error retrieving current status for item {item_id}: {e}")
return None return None
def sync_group_codes(primary_obj_id, base_code, individual_codes_list):
"""
Synchronisiert die Barcodes einer Gruppe im korrekten Schema
(angelehnt an das 'Augenmodell groß'-Vorbild).
"""
if not base_code:
return False
# Alle Ziel-Codes zusammenführen (Basis-Code an erster Stelle)
all_target_codes = [base_code]
for c in individual_codes_list:
if c and c not in all_target_codes:
all_target_codes.append(c)
item_count = len(all_target_codes)
try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
items = db['items']
primary_item = items.find_one({'_id': ObjectId(primary_obj_id)})
if not primary_item:
client.close()
return False
group_id = primary_item.get('SeriesGroupId')
# Wenn es mehr als 1 Item gibt und noch keine Gruppe existiert -> Neue GroupID erzeugen
if not group_id and item_count > 1:
group_id = str(uuid.uuid4())
# Wenn es nun eine Gruppe gibt (item_count > 1)
if item_count > 1:
# 1. Haupt-Item (Parent) aktualisieren
items.update_one(
{'_id': primary_item['_id']},
{'$set': {
'Code_4': base_code,
'SeriesGroupId': group_id,
'SeriesCount': item_count,
'SeriesPosition': 1,
'IsGroupedSubItem': False,
'ParentItemId': None
}}
)
# Bestehende Gruppenmitglieder laden
existing_items = list(items.find({'SeriesGroupId': group_id}))
existing_map = {it.get('Code_4'): it for it in existing_items if
it.get('Code_4') and str(it['_id']) != str(primary_item['_id'])}
# Alle verbleibenden Sub-Codes ab Position 2 abarbeiten
processed_sub_ids = []
for idx, code in enumerate(all_target_codes[1:], start=2):
if code in existing_map:
# Existiert bereits in der Gruppe -> Nur Position und Count aktualisieren
sub_item = existing_map[code]
processed_sub_ids.append(sub_item['_id'])
items.update_one(
{'_id': sub_item['_id']},
{'$set': {
'SeriesCount': item_count,
'SeriesPosition': idx,
'IsGroupedSubItem': True,
'ParentItemId': str(primary_item['_id'])
}}
)
else:
# Neu hinzukommender Code -> Als Klon (Sub-Item) erstellen
new_sub = primary_item.copy()
if '_id' in new_sub:
del new_sub['_id']
new_sub.update({
'Code_4': code,
'SeriesGroupId': group_id,
'SeriesCount': item_count,
'SeriesPosition': idx,
'IsGroupedSubItem': True,
'ParentItemId': str(primary_item['_id']),
'LastUpdated': primary_item.get('LastUpdated')
})
inserted_res = items.insert_one(new_sub)
processed_sub_ids.append(inserted_res.inserted_id)
# Nicht mehr benötigte Sub-Items aus dieser Gruppe entfernen
for code, sub_item in existing_map.items():
if sub_item['_id'] not in processed_sub_ids:
items.delete_one({'_id': sub_item['_id']})
else:
# Fall: Nur 1 einziges Item (keine Gruppe / Gruppe aufgelöst)
# Eventuelle alte Sub-Items dieser Gruppe löschen
if group_id:
items.delete_many({
'SeriesGroupId': group_id,
'_id': {'$ne': primary_item['_id']}
})
items.update_one(
{'_id': primary_item['_id']},
{'$set': {
'Code_4': base_code,
'SeriesGroupId': None,
'SeriesCount': 1,
'SeriesPosition': 1,
'IsGroupedSubItem': False,
'ParentItemId': None
}}
)
client.close()
return True
except Exception as e:
print(f"Error syncing group codes: {e}")
return False
+1 -1
View File
@@ -650,7 +650,7 @@ def add_user(
safe_last_name = last_name.strip() if last_name else '' safe_last_name = last_name.strip() if last_name else ''
user_doc = { user_doc = {
'Username': dp.encrypt_text(username), 'Username': username,
'Password': hashing(password), 'Password': hashing(password),
'Admin': (permission_preset == "full_access"), 'Admin': (permission_preset == "full_access"),
'active_ausleihung': None, 'active_ausleihung': None,
File diff suppressed because it is too large Load Diff
+78 -164
View File
@@ -524,7 +524,7 @@
<option value="card_only">Nur Ausweis erfassen</option> <option value="card_only">Nur Ausweis erfassen</option>
<option value="quick_toggle">Schnellmodus: Ausweis + Mediencode</option> <option value="quick_toggle">Schnellmodus: Ausweis + Mediencode</option>
</select> </select>
<input type="text" id="activeStudentCard" placeholder="Aktiver Ausweis (gescannt)" readonly> <input type="text" id="activeStudentCard" placeholder="Aktiver Ausweis (gescannt)" >
<input type="text" id="manualItemCode" placeholder="Manueller Mediencode (optional)" style="min-width:180px;"> <input type="text" id="manualItemCode" placeholder="Manueller Mediencode (optional)" style="min-width:180px;">
<button id="resetCardBtn" class="button" type="button">Ausweis löschen</button> <button id="resetCardBtn" class="button" type="button">Ausweis löschen</button>
<button id="toggleScannerBtn" class="button" type="button">Scanner starten</button> <button id="toggleScannerBtn" class="button" type="button">Scanner starten</button>
@@ -800,7 +800,7 @@
return ` return `
<tr> <tr>
<td class="table-title">${escapeHtml(item.Name || 'Untitled')}</td> <td class="table-title">${escapeHtml(item.Name || 'Untitled')}</td>
<td>${escapeHtml(item.ISBN || item.Code_4 || item.Code4 || '-')}</td> <td>${escapeHtml(item.ISBN || '-')}</td>
<td>${getItemTypeLabel(item.ItemType || 'book')}</td> <td>${getItemTypeLabel(item.ItemType || 'book')}</td>
<td style="font-weight:600; text-align:center;">${item.Quantity || item.GroupedDisplayCount || 1}</td> <td style="font-weight:600; text-align:center;">${item.Quantity || item.GroupedDisplayCount || 1}</td>
<td> <td>
@@ -1027,20 +1027,55 @@
} }
async function processQuickToggleScan(scannedCode) { async function processQuickToggleScan(scannedCode) {
// 1. Prüfen, ob "Nur Rückgabe"-Modus aktiv ist
const returnOnly = (document.getElementById('returnOnlyToggle') || {}).checked;
if (returnOnly) {
await returnByCode(scannedCode);
return;
}
// 2. Wenn kein Ausweis gesetzt ist, wird der Code als Ausweis interpretiert
if (!activeStudentCardId) { if (!activeStudentCardId) {
// If return-only mode is active, always attempt to return by code setActiveStudentCard(scannedCode);
const returnOnly = (document.getElementById('returnOnlyToggle') || {}).checked; setScanStatus(`Ausweis gesetzt: ${activeStudentCardId}`, 'ok');
if (returnOnly) { return;
await returnByCode(scannedCode); }
// 3. Ausleihe/Rückgabe verarbeiten (wenn Ausweis vorhanden)
try {
setScanStatus('Verarbeite Mediencode...', 'warn');
const response = await fetch('/api/library_scan_action', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
student_card_id: activeStudentCardId,
item_code: scannedCode
})
});
const result = await response.json();
if (!response.ok || !result.ok) {
setScanStatus(result.message || 'Scan-Aktion fehlgeschlagen.', 'error');
return; return;
} }
if (!activeStudentCardId) { if (result.action === 'borrowed') {
setActiveStudentCard(scannedCode); setScanStatus(`Ausgeliehen: ${result.item_name}`, 'ok');
setScanStatus(`Ausweis gesetzt: ${activeStudentCardId}`, 'ok'); showSmallConfirm(`Ausgeliehen: ${result.item_name}`, 'ok');
return; } else if (result.action === 'returned') {
setScanStatus(`Zurückgegeben: ${result.item_name}`, 'ok');
showSmallConfirm(`Zurückgegeben: ${result.item_name}`, 'ok');
} else {
setScanStatus(result.message || 'Aktion durchgeführt.', 'ok');
showSmallConfirm(result.message || 'Aktion durchgeführt.', 'ok');
} }
await loadLibraryItems();
} catch (err) {
console.error('Quick scan action failed:', err);
setScanStatus('Fehler beim Verarbeiten des Scans.', 'error');
}
}
async function returnByCode(code) { async function returnByCode(code) {
if (!code) return; if (!code) return;
@@ -1068,40 +1103,6 @@
return false; return false;
} }
} }
try {
setScanStatus('Verarbeite Mediencode...', 'warn');
const response = await fetch('/api/library_scan_action', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
student_card_id: activeStudentCardId,
item_code: scannedCode
})
});
const result = await response.json();
if (!response.ok || !result.ok) {
setScanStatus(result.message || 'Scan-Aktion fehlgeschlagen.', 'error');
return;
}
if (result.action === 'borrowed') {
setScanStatus(`Ausgeliehen: ${result.item_name}`, 'ok');
showSmallConfirm(`Ausgeliehen: ${result.item_name}`, 'ok');
} else if (result.action === 'returned') {
setScanStatus(`Zurückgegeben: ${result.item_name}`, 'ok');
showSmallConfirm(`Zurückgegeben: ${result.item_name}`, 'ok');
} else {
setScanStatus(result.message || 'Aktion durchgeführt.', 'ok');
showSmallConfirm(result.message || 'Aktion durchgeführt.', 'ok');
}
await loadLibraryItems();
} catch (err) {
console.error('Quick scan action failed:', err);
setScanStatus('Fehler beim Verarbeiten des Scans.', 'error');
}
}
function scanIntoEditCode() { function scanIntoEditCode() {
const scanReaderWrap = document.getElementById('scanReaderWrap'); const scanReaderWrap = document.getElementById('scanReaderWrap');
@@ -1110,15 +1111,15 @@
if (!scanReaderWrap || !editCodeInput || !scanEditBtn) { if (!scanReaderWrap || !editCodeInput || !scanEditBtn) {
return; return;
} }
if (scannerRunning && scanReaderWrap.style.display !== 'none') { if (scannerRunning && scanReaderWrap.style.display !== 'none') {
stopScanner(); stopScanner();
scanEditBtn.textContent = 'Barcode scannen'; scanEditBtn.textContent = 'Barcode scannen';
return; return;
} }
scanEditBtn.textContent = 'Scanner schließen'; scanEditBtn.textContent = 'Scanner schließen';
startScanner(function(decodedText) { startScanner(function(decodedText) {
editCodeInput.value = decodedText; editCodeInput.value = decodedText;
if(typeof validateCodeField === "function") { if(typeof validateCodeField === "function") {
@@ -1137,16 +1138,16 @@
alert('Dieses Medium ist als defekt/zerstört markiert und kann nicht ausgeliehen werden.'); alert('Dieses Medium ist als defekt/zerstört markiert und kann nicht ausgeliehen werden.');
return; return;
} }
const defaultCardId = activeStudentCardId || ''; const defaultCardId = activeStudentCardId || '';
const cardId = (window.prompt('Bitte Schülerausweis-ID eingeben:', defaultCardId) || '').trim().toUpperCase(); const cardId = (window.prompt('Bitte Schülerausweis-ID eingeben:', defaultCardId) || '').trim().toUpperCase();
if (!cardId) { if (!cardId) {
alert('Ausleihe abgebrochen: Für Bibliotheksmedien ist eine gültige Schülerausweis-ID erforderlich.'); alert('Ausleihe abgebrochen: Für Bibliotheksmedien ist eine gültige Schülerausweis-ID erforderlich.');
return; return;
} }
setActiveStudentCard(cardId); setActiveStudentCard(cardId);
const durationInput = (window.prompt('Ausleihdauer in Tagen (optional):') || '').trim(); const durationInput = (window.prompt('Ausleihdauer in Tagen (optional):') || '').trim();
const maxAvailable = Math.max(1, parseInt(selectedItem?.AvailableGroupedCount || selectedItem?.Quantity || 1, 10) || 1); 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(); const countPrompt = (window.prompt(`Anzahl ausleihen? (Standard: 1, verfügbar: ${maxAvailable})`, '1') || '').trim();
@@ -1158,29 +1159,29 @@
alert(`Es sind nur ${maxAvailable} Exemplar(e) verfügbar.`); alert(`Es sind nur ${maxAvailable} Exemplar(e) verfügbar.`);
return; return;
} }
const form = document.createElement('form'); const form = document.createElement('form');
form.method = 'POST'; form.method = 'POST';
form.action = `/ausleihen/${itemId}`; form.action = `/ausleihen/${itemId}`;
const csrfField = document.createElement('input'); const csrfField = document.createElement('input');
csrfField.type = 'hidden'; csrfField.type = 'hidden';
csrfField.name = 'csrf_token'; csrfField.name = 'csrf_token';
csrfField.value = '{{ csrf_token }}'; csrfField.value = '{{ csrf_token }}';
form.appendChild(csrfField); form.appendChild(csrfField);
const cardField = document.createElement('input'); const cardField = document.createElement('input');
cardField.type = 'hidden'; cardField.type = 'hidden';
cardField.name = 'borrower_card_id'; cardField.name = 'borrower_card_id';
cardField.value = cardId; cardField.value = cardId;
form.appendChild(cardField); form.appendChild(cardField);
const returnTargetField = document.createElement('input'); const returnTargetField = document.createElement('input');
returnTargetField.type = 'hidden'; returnTargetField.type = 'hidden';
returnTargetField.name = 'return_to'; returnTargetField.name = 'return_to';
returnTargetField.value = 'library'; returnTargetField.value = 'library';
form.appendChild(returnTargetField); form.appendChild(returnTargetField);
if (durationInput) { if (durationInput) {
const durationField = document.createElement('input'); const durationField = document.createElement('input');
durationField.type = 'hidden'; durationField.type = 'hidden';
@@ -1188,13 +1189,13 @@
durationField.value = durationInput; durationField.value = durationInput;
form.appendChild(durationField); form.appendChild(durationField);
} }
const countField = document.createElement('input'); const countField = document.createElement('input');
countField.type = 'hidden'; countField.type = 'hidden';
countField.name = 'exemplare_count'; countField.name = 'exemplare_count';
countField.value = String(borrowCount || 1); countField.value = String(borrowCount || 1);
form.appendChild(countField); form.appendChild(countField);
document.body.appendChild(form); document.body.appendChild(form);
form.submit(); form.submit();
} }
@@ -1221,13 +1222,13 @@
// auto remove after 3 seconds // auto remove after 3 seconds
setTimeout(() => { try { if (el && el.parentNode) el.parentNode.removeChild(el); } catch(e){} }, 3000); setTimeout(() => { try { if (el && el.parentNode) el.parentNode.removeChild(el); } catch(e){} }, 3000);
} }
function setActiveStudentCard(cardId) { function setActiveStudentCard(cardId) {
activeStudentCardId = (cardId || '').trim().toUpperCase(); activeStudentCardId = (cardId || '').trim().toUpperCase();
const input = document.getElementById('activeStudentCard'); const input = document.getElementById('activeStudentCard');
if (input) input.value = activeStudentCardId; if (input) input.value = activeStudentCardId;
} }
function normalizeScannedCode(code) { function normalizeScannedCode(code) {
return (code || '').trim(); return (code || '').trim();
} }
@@ -1292,7 +1293,7 @@
const resetBtn = document.getElementById('resetCardBtn'); const resetBtn = document.getElementById('resetCardBtn');
const modeSelect = document.getElementById('scanModeSelect'); const modeSelect = document.getElementById('scanModeSelect');
const keyboardToggle = document.getElementById('keyboardScannerToggle'); const keyboardToggle = document.getElementById('keyboardScannerToggle');
if (toggleBtn) { if (toggleBtn) {
toggleBtn.addEventListener('click', async () => { toggleBtn.addEventListener('click', async () => {
if (scannerRunning) { if (scannerRunning) {
@@ -1302,14 +1303,14 @@
} }
}); });
} }
if (resetBtn) { if (resetBtn) {
resetBtn.addEventListener('click', () => { resetBtn.addEventListener('click', () => {
setActiveStudentCard(''); setActiveStudentCard('');
setScanStatus('Ausweis zurückgesetzt. Bitte neu scannen.', 'warn'); setScanStatus('Ausweis zurückgesetzt. Bitte neu scannen.', 'warn');
}); });
} }
if (modeSelect) { if (modeSelect) {
modeSelect.addEventListener('change', () => { modeSelect.addEventListener('change', () => {
if (modeSelect.value === 'quick_toggle' && !activeStudentCardId) { if (modeSelect.value === 'quick_toggle' && !activeStudentCardId) {
@@ -1335,8 +1336,8 @@
} }
// Run when DOM structure is entirely ready // Run when DOM structure is entirely ready
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', async () => {
wireScannerUi(); // Setup scanner control buttons wireScannerUi(); // Setup scanner control buttons
loadLibraryItems(); // Fetch your database items right away! loadLibraryItems(); // Fetch your database items right away!
// Safely connect standard Filters and Search inputs inside DOMContentLoaded // Safely connect standard Filters and Search inputs inside DOMContentLoaded
@@ -1365,7 +1366,7 @@
document.getElementById('filterISBN').value = ''; document.getElementById('filterISBN').value = '';
document.getElementById('filterType').value = ''; document.getElementById('filterType').value = '';
document.getElementById('filterStatus').value = ''; document.getElementById('filterStatus').value = '';
activeFilters = { isbn: '', type: '', status: '' }; activeFilters = {isbn: '', type: '', status: ''};
applyFiltersAndSearch(true); applyFiltersAndSearch(true);
}); });
} }
@@ -1401,13 +1402,16 @@
const editForm = document.getElementById('editLibraryForm'); const editForm = document.getElementById('editLibraryForm');
if (editForm) { if (editForm) {
editForm.addEventListener('submit', async function(e) { editForm.addEventListener('submit', async function (e) {
e.preventDefault(); e.preventDefault();
const itemId = document.getElementById('editLibraryItemId').value; const itemId = document.getElementById('editLibraryItemId').value;
const currentItem = libraryItems.find(i => i._id === itemId); const currentItem = libraryItems.find(i => i._id === itemId);
if (!currentItem) return; if (!currentItem) return;
const codeInputs = Array.from(document.querySelectorAll('#editLibraryCodesContainer input[data-item-id]'));
// Daten aus dem Formular sammeln
const sharedPayload = { const sharedPayload = {
name: document.getElementById('editLibraryName').value, name: document.getElementById('editLibraryName').value,
item_type: document.getElementById('editLibraryType').value, item_type: document.getElementById('editLibraryType').value,
@@ -1419,11 +1423,11 @@
reservierbar: currentItem.Reservierbar !== false, reservierbar: currentItem.Reservierbar !== false,
}; };
const codeInputs = Array.from(document.querySelectorAll('#editLibraryCodesContainer input[data-item-id]'));
const codeByItemId = new Map(codeInputs.map(input => [input.dataset.itemId, (input.value || '').trim()])); const codeByItemId = new Map(codeInputs.map(input => [input.dataset.itemId, (input.value || '').trim()]));
const groupMembers = editLibraryState.groupMembers.length > 0 ? editLibraryState.groupMembers : [currentItem]; const groupMembers = editLibraryState.groupMembers.length > 0 ? editLibraryState.groupMembers : [currentItem];
const isGroupedEdit = Boolean(currentItem.SeriesGroupId) && groupMembers.length > 1; const isGroupedEdit = Boolean(currentItem.SeriesGroupId) && groupMembers.length > 1;
// API-Aufruf
try { try {
if (isGroupedEdit) { if (isGroupedEdit) {
const payload = { const payload = {
@@ -1495,7 +1499,6 @@
async function fetchLibraryGroupMembers(seriesGroupId) { async function fetchLibraryGroupMembers(seriesGroupId) {
if (!seriesGroupId) return []; if (!seriesGroupId) return [];
try { try {
const response = await fetch(`/api/library_group/${encodeURIComponent(seriesGroupId)}`); const response = await fetch(`/api/library_group/${encodeURIComponent(seriesGroupId)}`);
if (!response.ok) { if (!response.ok) {
@@ -1554,105 +1557,16 @@
labelParts.push('aktuelles Medium'); labelParts.push('aktuelles Medium');
} }
return ` return `
<div style="display:flex; flex-direction:column; gap:6px; margin-bottom:10px;"> <div style="display:flex; flex-direction:column; gap:6px; margin-bottom:10px;">
<label for="editLibraryCode-${member._id}" style="font-weight:600; font-size:0.9em; color:var(--ui-text);">${escapeHtml(labelParts.join(' · '))}</label> <label for="editLibraryCode-${member._id}" style="font-weight:600; font-size:0.9em; color:var(--ui-text);">${escapeHtml(labelParts.join(' · '))}</label>
<input id="editLibraryCode-${member._id}" data-item-id="${escapeHtml(member._id)}" value="${escapeHtml(codeValue)}" placeholder="Mediencode" style="width:100%; padding:8px 10px; border:1px solid #d0d7e2; border-radius:6px;"> <input id="editLibraryCode-${member._id}" data-item-id="${escapeHtml(member._id)}" value="${escapeHtml(codeValue)}" placeholder="Mediencode" style="width:100%; padding:8px 10px; border:1px solid #d0d7e2; border-radius:6px;">
</div> </div>
`; `;
}).join(''); }).join('');
} }
window.openEditLibraryItem = async function(itemId) { function openEditLibraryItem(itemId) {
const item = libraryItems.find(i => i._id === itemId); window.location.href = `/item_edit/${itemId}`;
if (!item) return;
editLibraryState.itemId = item._id;
editLibraryState.seriesGroupId = item.SeriesGroupId || '';
editLibraryState.groupMembers = [];
// 1. Felder befüllen
document.getElementById('editLibraryItemId').value = item._id;
document.getElementById('editLibraryName').value = item.Name;
document.getElementById('editLibraryType').value = item.ItemType;
document.getElementById('editLibraryIsbn').value = item.ISBN || '';
document.getElementById('editLibraryLocation').value = item.Ort;
document.getElementById('editLibraryDescription').value = item.Beschreibung;
const codesContainer = document.getElementById('editLibraryCodesContainer');
if (codesContainer) {
codesContainer.innerHTML = '<div style="padding:10px 0; color:#6b7280;">Lade Codes...</div>';
}
const groupMembers = item.SeriesGroupId ? await fetchLibraryGroupMembers(item.SeriesGroupId) : [item];
renderLibraryGroupCodeFields(groupMembers.length > 0 ? groupMembers : [item], item._id);
document.getElementById('editLibraryModal').style.display = 'flex';
};
function closeEditLibraryModal() {
document.getElementById('editLibraryModal').style.display = 'none';
} }
</script> </script>
<div id="editLibraryModal" class="modal" style="display:none;">
<div class="modal-content" style="max-width: 760px; padding: 25px; border-radius: 8px;">
<span class="close" onclick="closeEditLibraryModal()" style="cursor: pointer; float: right; font-size: 24px;">&times;</span>
<h3 style="margin-top:0;">Bibliotheksmedium bearbeiten</h3>
<!-- Bereich für Gruppen-Informationen (Hier konsolidiert!) -->
<div id="editLibraryGroupWarning" style="display:none; background-color: #fff; padding: 15px; border-radius: 6px; margin-bottom: 20px; border: 1px solid #0ea5e9;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; border-bottom: 1px solid #eee; padding-bottom: 10px;">
<strong style="color: #0ea5e9;">Gruppen-Range (Total: <span id="editLibraryGroupCount"></span>)</strong>
</div>
<p id="editLibraryGroupHint" style="margin: 5px 0; font-size: 12px; color: #555;">
Änderungen an Titel, Ort und Beschreibung werden auf alle Exemplare der Gruppe übertragen.
</p>
<div style="margin-top: 15px; font-size: 11px; background: #e0f2fe; padding: 8px; border-radius: 4px;">
<strong>Hinweis:</strong> Jeder Mediencode wird einzeln gespeichert, damit alle Exemplare der Gruppe korrekt bleiben.
</div>
</div>
<form id="editLibraryForm">
<input type="hidden" id="editLibraryItemId">
<div class="edit-grid">
<div class="full">
<label for="editLibraryName">Titel</label>
<input id="editLibraryName" required style="width: 100%;">
</div>
<div>
<label for="editLibraryType">Medientyp</label>
<select id="editLibraryType" style="width: 100%;">
<option value="Buch">Buch</option>
<option value="Schulbuch">Schulbuch</option>
<option value="cd">CD</option>
<option value="dvd">DVD</option>
<option value="other">Sonstige Medien</option>
</select>
</div>
<div>
<label for="editLibraryIsbn">ISBN</label>
<input id="editLibraryIsbn" placeholder="optional ISBN-10/13" style="width: 100%;">
</div>
<div>
<label>Mediencodes</label>
<div id="editLibraryCodesContainer"></div>
</div>
<div class="full">
<label for="editLibraryLocation">Ort</label>
<input id="editLibraryLocation" required style="width: 100%;">
</div>
<div class="full">
<label for="editLibraryDescription">Beschreibung</label>
<textarea id="editLibraryDescription" rows="4" required style="width: 100%;"></textarea>
</div>
</div>
<div class="edit-actions" style="margin-top:20px;">
<button type="submit" class="button" style="background:#0ea5e9;color:#fff;">Speichern & Synchronisieren</button>
<button type="button" class="button" onclick="closeEditLibraryModal()">Abbrechen</button>
</div>
</form>
</div>
</div>
{% endblock %} {% endblock %}
+56
View File
@@ -4315,6 +4315,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
hiddenInput.value = image; hiddenInput.value = image;
editForm.appendChild(hiddenInput); editForm.appendChild(hiddenInput);
} }
}); });
} }
@@ -5632,6 +5633,61 @@ document.addEventListener('DOMContentLoaded', ()=>{
futureAppointments.sort((a, b) => new Date(a.date) - new Date(b.date)); futureAppointments.sort((a, b) => new Date(a.date) - new Date(b.date));
return futureAppointments[0]; return futureAppointments[0];
} }
// Load location options
function loadLocationOptions() {
fetch('/get_predefined_locations')
.then(response => response.json())
.then(data => {
const ortSelect = document.getElementById('ort');
if (ortSelect) {
// Clear existing options except the first one
while (ortSelect.children.length > 1) {
ortSelect.removeChild(ortSelect.lastChild);
}
// Add new options - data.locations contains the array
data.locations.forEach(location => {
const option = document.createElement('option');
option.value = location;
option.textContent = location;
ortSelect.appendChild(option);
});
}
})
.catch(error => {
console.error('Error loading location options:', error);
});
}
// Function to add new location
function addNewLocation() {
const newLocationInput = document.getElementById('new-location-input');
const newLocation = newLocationInput.value.trim();
if (!newLocation) {
alert('Bitte geben Sie einen Ort ein.');
return;
}
// Add to dropdown
const ortSelect = document.getElementById('ort');
const option = document.createElement('option');
option.value = newLocation;
option.textContent = newLocation;
option.selected = true;
ortSelect.appendChild(option);
// Hide the input container
document.getElementById('new-location-container').style.display = 'none';
newLocationInput.value = '';
}
// Function to cancel adding new location
function cancelAddLocation() {
document.getElementById('new-location-container').style.display = 'none';
document.getElementById('new-location-input').value = '';
}
</script> </script>
<!-- Include edit item functions --> <!-- Include edit item functions -->