Compare commits

...

21 Commits

Author SHA1 Message Date
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
Aiirondev_dev 743e5b1c16 Reviewed the edeting function
Release Inventarsystem / release-docker (push) Successful in 2m18s
2026-08-10 23:40:02 +02:00
Aiirondev_dev 4e47ef0c88 Implement library item return by code API and enhance UI for manual returns
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-10 22:17:52 +02:00
6 changed files with 1661 additions and 262 deletions
+323 -21
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
@@ -225,12 +225,95 @@ def rollover_student_card_classes(dry_run=False, *, max_class=None, graduate_lab
if client: if client:
client.close() client.close()
summary = {'examined': examined, 'updated': updated, 'failures': failures, 'dry_run': bool(dry_run)}
@app.route('/api/library_return_by_code', methods=['POST'])
def api_library_return_by_code():
"""
Return a library item by scanning its code only (no student card required).
This marks active ausleihungen for the item as completed and updates item status.
"""
if 'username' not in session:
return jsonify({'ok': False, 'message': 'Nicht angemeldet.'}), 401
if not cfg.MODULES.is_enabled('library'):
return jsonify({'ok': False, 'message': 'Bibliotheks-Modul ist deaktiviert.'}), 403
payload = request.get_json(silent=True) or {}
item_code_raw = str(payload.get('item_code') or payload.get('code') or '').strip()
if not item_code_raw:
return jsonify({'ok': False, 'message': 'Mediencode fehlt.'}), 400
normalized_isbn = normalize_and_validate_isbn(item_code_raw)
normalized_code = item_code_raw.upper()
client = None
try: try:
_append_audit_event_standalone('student_cards_rollover', summary) client = MongoClient(MONGODB_HOST, MONGODB_PORT)
except Exception: db = client[MONGODB_DB]
app.logger.warning('Audit write failed for student_cards_rollover') items_col = db['items']
return summary ausleihungen_col = db['ausleihungen']
query_or = [
{'Code_4': item_code_raw},
{'Code_4': normalized_code},
]
if normalized_isbn:
query_or.append({'ISBN': normalized_isbn})
item_doc = items_col.find_one({
'ItemType': {'$in': LIBRARY_ITEM_TYPES},
'$or': query_or
})
if not item_doc:
return jsonify({'ok': False, 'message': 'Kein Bibliotheksmedium für diesen Code gefunden.'}), 404
item_id = str(item_doc['_id'])
now = datetime.datetime.now()
# If item already available -> nothing to return
if item_doc.get('Verfuegbar', True):
return jsonify({'ok': False, 'message': 'Dieses Medium ist nicht als ausgeliehen markiert.'}), 409
# Mark active ausleihungen as completed
update_result = ausleihungen_col.update_many(
{'Item': item_id, 'Status': 'active'},
{'$set': {
'Status': 'completed',
'End': now,
'LastUpdated': now
}}
)
# Update item status to available
borrower_name = str(item_doc.get('User') or '').strip() or ''
it.update_item_status(item_id, True, borrower_name)
_append_audit_event_standalone(
event_type='ausleihung_returned_by_code',
payload={
'channel': 'library_return_code',
'item_id': item_id,
'item_name': item_doc.get('Name', ''),
'completed_records': update_result.modified_count,
'performed_by': session.get('username')
}
)
return jsonify({
'ok': True,
'action': 'returned',
'item_id': item_id,
'item_name': item_doc.get('Name', ''),
'completed_records': update_result.modified_count,
'message': f"{item_doc.get('Name', 'Medium')} wurde zurückgegeben."
}), 200
except Exception as e:
app.logger.error(f"Error in library return by code: {e}")
return jsonify({'ok': False, 'message': 'Fehler beim Verarbeiten der Rückgabe.'}), 500
finally:
if client:
client.close()
# Admin route to trigger rollover manually # Admin route to trigger rollover manually
@@ -3230,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}
} }
@@ -3249,7 +3332,12 @@ def api_library_items():
'User': 1, 'User': 1,
'Ort': 1, 'Ort': 1,
'Beschreibung': 1, 'Beschreibung': 1,
'Image': 1 'Image': 1,
'SeriesGroupId': 1,
'SeriesCount': 1,
'SeriesPosition': 1,
'IsGroupedSubItem': 1,
'ParentItemId': 1,
} }
total_count = items_db.count_documents(query) total_count = items_db.count_documents(query)
@@ -3277,6 +3365,10 @@ def api_library_items():
'Beschreibung': 1, 'Beschreibung': 1,
'Image': 1, 'Image': 1,
'ParentItemId': 1, 'ParentItemId': 1,
'SeriesGroupId': 1,
'SeriesCount': 1,
'SeriesPosition': 1,
'IsGroupedSubItem': 1,
} }
child_items = list(items_db.find({ child_items = list(items_db.find({
'ParentItemId': {'$in': parent_ids_list}, 'ParentItemId': {'$in': parent_ids_list},
@@ -3390,6 +3482,48 @@ def api_library_items():
return jsonify({'error': 'An error occurred while fetching library items'}), 500 return jsonify({'error': 'An error occurred while fetching library items'}), 500
@app.route('/api/library_group/<series_group_id>')
def api_library_group(series_group_id):
"""Fetch all items belonging to one library series group."""
if 'username' not in session:
return jsonify({'items': []}), 401
try:
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
db = client[MONGODB_DB]
items_col = db['items']
query = {
'SeriesGroupId': series_group_id,
'Deleted': {'$ne': True},
'ItemType': {'$in': ['book', 'cd', 'dvd', 'schoolbook', 'schulbuch', 'Buch', 'Schulbuch']},
}
projection = {
'Name': 1,
'ISBN': 1,
'Code_4': 1,
'Code4': 1,
'ItemType': 1,
'Ort': 1,
'Beschreibung': 1,
'SeriesGroupId': 1,
'SeriesCount': 1,
'SeriesPosition': 1,
'IsGroupedSubItem': 1,
'ParentItemId': 1,
}
items = list(items_col.find(query, projection).sort([('SeriesPosition', 1), ('Name', 1), ('_id', 1)]))
for item in items:
item['_id'] = str(item['_id'])
client.close()
return jsonify({'items': items, 'count': len(items), 'series_group_id': series_group_id})
except Exception as exc:
app.logger.error('Error loading library group %s: %s', series_group_id, exc)
return jsonify({'items': [], 'message': 'Gruppe konnte nicht geladen werden.'}), 500
@app.route('/api/library_scan_action', methods=['POST']) @app.route('/api/library_scan_action', methods=['POST'])
def api_library_scan_action(): def api_library_scan_action():
""" """
@@ -3654,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>
@@ -5646,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:
@@ -6232,6 +6367,170 @@ 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).
"""
# 1. Rechte- & Auth-Check
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'])
show_library = cfg.MODULES.is_enabled('library')
return render_template(
'edit_library.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 und bereinigen
name = sanitize_form_value(request.form.get('name'))
ort = sanitize_form_value(request.form.get('ort'))
beschreibung = sanitize_form_value(request.form.get('beschreibung'))
code_4 = sanitize_form_value(request.form.get('code_4'))
isbn_raw = sanitize_form_value(request.form.get('isbn', ''))
anschaffungs_jahr = sanitize_form_value(request.form.get('anschaffungsjahr'))
anschaffungs_kosten = sanitize_form_value(request.form.get('anschaffungskosten'))
reservierbar = 'reservierbar' in request.form
item_type_input = sanitize_form_value(request.form.get('item_type_input'))
library_category = sanitize_form_value(request.form.get('library_category'))
filter1 = expand_filter_selection(sanitize_form_value(request.form.getlist('filter')), 1)
filter2 = expand_filter_selection(sanitize_form_value(request.form.getlist('filter2')), 2)
filter3 = sanitize_form_value(request.form.getlist('filter3'))
# WICHTIG: Den aktuellen Verfügbarkeitsstatus aus der Datenbank übernehmen
verfuegbar = current_item.get('Verfuegbar', True)
# Barcode Prüfen
if code_4 and not it.is_code_unique(code_4, exclude_id=str(id)):
flash(f'Der Code "{code_4}" wird bereits verwendet.', 'error')
return redirect(redirect_target)
# ISBN und Medientyp verarbeiten
item_isbn = ''
item_type = item_type_input or current_item.get('ItemType', 'general')
if cfg.MODULES.is_enabled('library') and isbn_raw:
item_isbn = normalize_and_validate_isbn(isbn_raw)
if not item_isbn:
flash('Ungültiges ISBN-Format.', 'error')
return redirect(redirect_target)
# Bilder verarbeiten (GridFS)
images_to_keep = request.form.getlist('existing_images')
original_images = current_item.get('Images', [])
images = [img for img in original_images if img in images_to_keep]
new_files = request.files.getlist('images')
if new_files and new_files[0].filename != '':
fs = get_gridfs()
for file in new_files:
if file and file.filename:
is_allowed, error_msg = allowed_file(file.filename, file)
if not is_allowed:
flash(error_msg, 'error')
return redirect(redirect_target)
try:
secure_name = secure_filename(file.filename)
file.seek(0)
image_bytes = file.read()
if not image_bytes:
continue
optimized_io = io.BytesIO()
with Image.open(io.BytesIO(image_bytes)) as img:
if img.mode not in ('RGB', 'RGBA'):
img = img.convert('RGBA')
max_width = 800
if img.width > max_width:
ratio = max_width / img.width
img = img.resize((max_width, int(img.height * ratio)), Image.Resampling.LANCZOS)
img.save(optimized_io, format='WEBP', quality=85, optimize=True)
optimized_io.seek(0)
new_filename = f"{uuid.uuid4().hex}_{int(time.time())}.webp"
fs.put(
optimized_io,
filename=new_filename,
content_type='image/webp',
metadata={'original_filename': secure_name, 'item_id': str(id)}
)
images.append(new_filename)
except Exception as e:
app.logger.error(f"Bild-Fehler bei Item {id}: {e}")
if ort and ort not in it.get_predefined_locations():
it.add_predefined_location(ort)
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():
@@ -6253,17 +6552,20 @@ def update_group():
# 1. Shared Fields (Group Logic) # 1. Shared Fields (Group Logic)
# These apply to every item in the group # These apply to every item in the group
shared_update = { shared_update = {'LastUpdated': datetime.datetime.now()}
'Name': data.get('name'), for source_key, target_key in (
'Ort': data.get('ort'), ('name', 'Name'),
'Beschreibung': data.get('beschreibung'), ('ort', 'Ort'),
'Anschaffungsjahr': data.get('ansch_jahr'), ('beschreibung', 'Beschreibung'),
'Anschaffungskosten': data.get('ansch_kost'), ('ansch_jahr', 'Anschaffungsjahr'),
'Reservierbar': data.get('reservierbar'), ('ansch_kost', 'Anschaffungskosten'),
'ISBN': data.get('isbn'), ('reservierbar', 'Reservierbar'),
'ItemType': data.get('item_type'), ('isbn', 'ISBN'),
'LastUpdated': datetime.datetime.now() ('item_type', 'ItemType'),
} ):
value = data.get(source_key)
if value is not None:
shared_update[target_key] = value
# 2. Individual Updates (Specific Code Logic) # 2. Individual Updates (Specific Code Logic)
# Expected format: [{'id': '...', 'code_4': '...'}, ...] # Expected format: [{'id': '...', 'code_4': '...'}, ...]
@@ -7560,7 +7862,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,
+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
+275 -230
View File
@@ -355,6 +355,28 @@
color: #6b7280; color: #6b7280;
} }
/* Small confirmation popup (toast) */
.small-popup {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
background: rgba(17, 24, 39, 0.96);
color: #fff;
padding: 12px 16px;
border-radius: 8px;
box-shadow: 0 6px 24px rgba(2,6,23,0.6);
z-index: 2000;
display: flex;
gap: 10px;
align-items: center;
max-width: 90%;
font-size: 0.95em;
}
.small-popup.ok { background: rgba(16,185,129,0.95); color: #032; }
.small-popup.error { background: rgba(239,68,68,0.95); color: #210; }
.small-popup .close-x { margin-left: 8px; cursor: pointer; font-weight: 700; }
/* Modal styles */ /* Modal styles */
.modal { .modal {
display: none; display: none;
@@ -502,13 +524,19 @@
<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;">
<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>
<label style="display:flex; align-items:center; gap:8px; margin-left:6px;"> <label style="display:flex; align-items:center; gap:8px; margin-left:6px;">
<input type="checkbox" id="keyboardScannerToggle"> <input type="checkbox" id="keyboardScannerToggle">
<span style="font-size:0.9em;">Physischer Scanner</span> <span style="font-size:0.9em;">Physischer Scanner</span>
</label> </label>
<label style="display:flex; align-items:center; gap:8px; margin-left:6px;">
<input type="checkbox" id="returnOnlyToggle">
<span style="font-size:0.9em;">Nur Rückgabe (nur Mediencode)</span>
</label>
<button id="manualReturnBtn" class="button" type="button" style="margin-left:6px; background:#10b981;color:white;">Rückgabe per Code</button>
</div> </div>
<div id="scanStatus" class="library-scan-status"> <div id="scanStatus" class="library-scan-status">
Hinweis: Im Schnellmodus zuerst den Schülerausweis scannen, danach den Buch-/Mediencode. Hinweis: Im Schnellmodus zuerst den Schülerausweis scannen, danach den Buch-/Mediencode.
@@ -641,6 +669,11 @@
let keyboardScanBuffer = ''; let keyboardScanBuffer = '';
let keyboardLastKeyAt = 0; let keyboardLastKeyAt = 0;
const KEYBOARD_SCAN_INTERCHAR_MS = 100; // max time between keystrokes to consider them one scan const KEYBOARD_SCAN_INTERCHAR_MS = 100; // max time between keystrokes to consider them one scan
let editLibraryState = {
itemId: '',
seriesGroupId: '',
groupMembers: []
};
const canEditLibraryItems = (document.getElementById('libraryTableContainer')?.dataset.canEdit === '1'); const canEditLibraryItems = (document.getElementById('libraryTableContainer')?.dataset.canEdit === '1');
@@ -767,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>
@@ -914,6 +947,13 @@
const currentCallback = activeScannerCallback; const currentCallback = activeScannerCallback;
stopScanner(); stopScanner();
const returnOnly = (document.getElementById('returnOnlyToggle') || {}).checked;
if (returnOnly) {
// direct return flow
returnByCode(barcode);
return;
}
if (typeof currentCallback === "function") { if (typeof currentCallback === "function") {
currentCallback(barcode); currentCallback(barcode);
} else { } else {
@@ -940,6 +980,13 @@
keyboardScanBuffer = ''; keyboardScanBuffer = '';
keyboardLastKeyAt = 0; keyboardLastKeyAt = 0;
if (!code) return; if (!code) return;
// If return-only mode is active, attempt direct return
const returnOnly = (document.getElementById('returnOnlyToggle') || {}).checked;
if (returnOnly) {
returnByCode(code);
return;
}
// Process exactly like a camera scan // Process exactly like a camera scan
handleScanSuccess(code); handleScanSuccess(code);
return; return;
@@ -980,12 +1027,21 @@
} }
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) {
setActiveStudentCard(scannedCode); setActiveStudentCard(scannedCode);
setScanStatus(`Ausweis gesetzt: ${activeStudentCardId}`, 'ok'); setScanStatus(`Ausweis gesetzt: ${activeStudentCardId}`, 'ok');
return; return;
} }
// 3. Ausleihe/Rückgabe verarbeiten (wenn Ausweis vorhanden)
try { try {
setScanStatus('Verarbeite Mediencode...', 'warn'); setScanStatus('Verarbeite Mediencode...', 'warn');
const response = await fetch('/api/library_scan_action', { const response = await fetch('/api/library_scan_action', {
@@ -996,21 +1052,24 @@
item_code: scannedCode item_code: scannedCode
}) })
}); });
const result = await response.json(); const result = await response.json();
if (!response.ok || !result.ok) { if (!response.ok || !result.ok) {
setScanStatus(result.message || 'Scan-Aktion fehlgeschlagen.', 'error'); setScanStatus(result.message || 'Scan-Aktion fehlgeschlagen.', 'error');
return; return;
} }
if (result.action === 'borrowed') { if (result.action === 'borrowed') {
setScanStatus(`Ausgeliehen: ${result.item_name}`, 'ok'); setScanStatus(`Ausgeliehen: ${result.item_name}`, 'ok');
showSmallConfirm(`Ausgeliehen: ${result.item_name}`, 'ok');
} else if (result.action === 'returned') { } else if (result.action === 'returned') {
setScanStatus(`Zurückgegeben: ${result.item_name}`, 'ok'); setScanStatus(`Zurückgegeben: ${result.item_name}`, 'ok');
showSmallConfirm(`Zurückgegeben: ${result.item_name}`, 'ok');
} else { } else {
setScanStatus(result.message || 'Aktion durchgeführt.', 'ok'); setScanStatus(result.message || 'Aktion durchgeführt.', 'ok');
showSmallConfirm(result.message || 'Aktion durchgeführt.', 'ok');
} }
await loadLibraryItems(); await loadLibraryItems();
} catch (err) { } catch (err) {
console.error('Quick scan action failed:', err); console.error('Quick scan action failed:', err);
@@ -1018,6 +1077,33 @@
} }
} }
async function returnByCode(code) {
if (!code) return;
setScanStatus('Verarbeite Rückgabe...', 'warn');
try {
const resp = await fetch('/api/library_return_by_code', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ item_code: code })
});
const result = await resp.json();
if (!resp.ok || !result.ok) {
setScanStatus(result.message || 'Rückgabe fehlgeschlagen.', 'error');
showSmallConfirm(result.message || 'Rückgabe fehlgeschlagen.', 'error');
return false;
}
setScanStatus(result.message || `Zurückgegeben: ${result.item_name || ''}`, 'ok');
showSmallConfirm(result.message || `Zurückgegeben: ${result.item_name || ''}`, 'ok');
await loadLibraryItems();
return true;
} catch (err) {
console.error('Return by code failed:', err);
setScanStatus('Fehler bei Rückgabe.', 'error');
showSmallConfirm('Fehler bei Rückgabe.', 'error');
return false;
}
}
function scanIntoEditCode() { function scanIntoEditCode() {
const scanReaderWrap = document.getElementById('scanReaderWrap'); const scanReaderWrap = document.getElementById('scanReaderWrap');
const editCodeInput = document.getElementById('edit-code4'); const editCodeInput = document.getElementById('edit-code4');
@@ -1025,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") {
@@ -1052,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();
@@ -1073,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';
@@ -1103,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();
} }
@@ -1121,13 +1207,28 @@
el.classList.remove('ok', 'warn', 'error'); el.classList.remove('ok', 'warn', 'error');
if (kind) el.classList.add(kind); if (kind) el.classList.add(kind);
} }
function showSmallConfirm(message, kind='ok') {
// Append the small helper text in German
const helper = 'Sie können fortfahren. Dies ist nur eine kleine Benachrichtigung.';
const el = document.createElement('div');
el.className = `small-popup ${kind === 'error' ? 'error' : 'ok'}`;
el.innerHTML = `<div>${escapeHtml(String(message || ''))}</div><div style="opacity:0.9; margin-left:8px; font-size:0.85em;">${helper}</div><div class="close-x">&times;</div>`;
document.body.appendChild(el);
// close handler
el.querySelector('.close-x').addEventListener('click', () => {
if (el && el.parentNode) el.parentNode.removeChild(el);
});
// auto remove after 3 seconds
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();
} }
@@ -1192,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) {
@@ -1202,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) {
@@ -1235,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
@@ -1265,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);
}); });
} }
@@ -1286,44 +1387,107 @@
}); });
} }
// Edit Modal Form processing const manualReturnBtn = document.getElementById('manualReturnBtn');
const manualItemCode = document.getElementById('manualItemCode');
if (manualReturnBtn && manualItemCode) {
manualReturnBtn.addEventListener('click', async () => {
const code = (manualItemCode.value || '').trim();
if (!code) {
alert('Bitte einen Mediencode eingeben.');
return;
}
await returnByCode(code);
});
}
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);
if (!currentItem) return;
const updatedData = { const codeInputs = Array.from(document.querySelectorAll('#editLibraryCodesContainer input[data-item-id]'));
// Daten aus dem Formular sammeln
const sharedPayload = {
name: document.getElementById('editLibraryName').value, name: document.getElementById('editLibraryName').value,
item_type: document.getElementById('editLibraryType').value, item_type: document.getElementById('editLibraryType').value,
isbn: document.getElementById('editLibraryIsbn').value, isbn: document.getElementById('editLibraryIsbn').value,
code_4: document.getElementById('editLibraryCode4').value,
ort: document.getElementById('editLibraryLocation').value, ort: document.getElementById('editLibraryLocation').value,
beschreibung: document.getElementById('editLibraryDescription').value beschreibung: document.getElementById('editLibraryDescription').value,
ansch_jahr: currentItem.Anschaffungsjahr || '',
ansch_kost: currentItem.Anschaffungskosten || '',
reservierbar: currentItem.Reservierbar !== false,
}; };
const codeByItemId = new Map(codeInputs.map(input => [input.dataset.itemId, (input.value || '').trim()]));
const groupMembers = editLibraryState.groupMembers.length > 0 ? editLibraryState.groupMembers : [currentItem];
const isGroupedEdit = Boolean(currentItem.SeriesGroupId) && groupMembers.length > 1;
// API-Aufruf
try { try {
const response = await fetch(`/api/library_item/${itemId}/update`, { if (isGroupedEdit) {
method: 'POST', const payload = {
headers: { series_group_id: currentItem.SeriesGroupId,
'Content-Type': 'application/json', ...sharedPayload,
'X-CSRFToken': '{{ csrf_token }}', items: groupMembers.map(member => ({
'X-CSRF-Token': '{{ csrf_token }}' id: member._id,
}, code_4: codeByItemId.get(member._id) || ''
body: JSON.stringify(updatedData) }))
}); };
const result = await response.json(); const response = await fetch('/update_group', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token }}',
'X-CSRF-Token': '{{ csrf_token }}'
},
body: JSON.stringify(payload)
});
if (response.ok && result.ok) { const result = await response.json();
alert(result.message || 'Medium erfolgreich aktualisiert!'); if (response.ok && result.success) {
closeEditLibraryModal(); alert(result.message || 'Gruppe erfolgreich aktualisiert!');
closeEditLibraryModal();
pagingState.loading = false; pagingState.loading = false;
loadLibraryItems(); await loadLibraryItems();
} else {
alert(result.message || 'Fehler beim Speichern der Gruppenänderungen.');
}
} else { } else {
alert(result.message || 'Fehler beim Speichern der Änderungen.'); const primaryCodeInput = codeInputs[0];
const payload = {
name: sharedPayload.name,
item_type: sharedPayload.item_type,
isbn: sharedPayload.isbn,
code_4: primaryCodeInput ? primaryCodeInput.value.trim() : (currentItem.Code_4 || currentItem.Code4 || '').trim(),
ort: sharedPayload.ort,
beschreibung: sharedPayload.beschreibung
};
const response = await fetch(`/api/library_item/${itemId}/update`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token }}',
'X-CSRF-Token': '{{ csrf_token }}'
},
body: JSON.stringify(payload)
});
const result = await response.json();
if (response.ok && result.ok) {
alert(result.message || 'Medium erfolgreich aktualisiert!');
closeEditLibraryModal();
pagingState.loading = false;
await loadLibraryItems();
} else {
alert(result.message || 'Fehler beim Speichern der Änderungen.');
}
} }
} catch (error) { } catch (error) {
console.error('Update failed:', error); console.error('Update failed:', error);
@@ -1333,195 +1497,76 @@
} }
}); });
window.openEditLibraryItem = function(itemId) { async function fetchLibraryGroupMembers(seriesGroupId) {
const item = libraryItems.find(i => i._id === itemId); if (!seriesGroupId) return [];
if (!item) return; try {
const response = await fetch(`/api/library_group/${encodeURIComponent(seriesGroupId)}`);
// 1. Felder befüllen if (!response.ok) {
document.getElementById('editLibraryItemId').value = item._id; throw new Error(`HTTP ${response.status}`);
document.getElementById('editLibraryName').value = item.Name;
document.getElementById('editLibraryType').value = item.ItemType;
document.getElementById('editLibraryIsbn').value = item.ISBN || '';
document.getElementById('editLibraryCode4').value = item.Code_4 || '';
document.getElementById('editLibraryLocation').value = item.Ort;
document.getElementById('editLibraryDescription').value = item.Beschreibung;
// 2. Gruppen-Logik
const warningDiv = document.getElementById('editLibraryGroupWarning');
const codesContainer = document.getElementById('editLibraryAllCodes');
if (item.SeriesGroupId) {
// Filtern aus dem aktuell geladenen Array
let groupMembers = libraryItems.filter(i => i.SeriesGroupId === item.SeriesGroupId);
// SCHLÜSSEL: Wenn die Anzahl der gefundenen Elemente nicht mit SeriesCount übereinstimmt,
// haben wir die Gruppe noch nicht vollständig geladen.
if (groupMembers.length < (item.SeriesCount || 0)) {
console.warn("Gruppe noch nicht vollständig geladen. Anzeige ggf. unvollständig.");
// Optional: Zeige einen Ladehinweis im Modal
codesContainer.textContent = "Lade restliche Gruppenmitglieder...";
} else {
// Daten sind vollständig -> Anzeigen
const codeList = groupMembers
.sort((a, b) => (a.SeriesPosition || 0) - (b.SeriesPosition || 0))
.map(m => m.Code_4 || "---")
.join(', ');
codesContainer.textContent = codeList;
} }
document.getElementById('editLibraryGroupCount').textContent = groupMembers.length + " / " + (item.SeriesCount || "?"); const payload = await response.json();
warningDiv.style.display = 'block'; return Array.isArray(payload.items) ? payload.items : [];
} else { } catch (error) {
warningDiv.style.display = 'none'; console.warn('Falling back to loaded library items for group editing:', error);
return (libraryItems || []).filter(item => item.SeriesGroupId === seriesGroupId);
} }
document.getElementById('editLibraryModal').style.display = 'flex';
};
function closeEditLibraryModal() {
document.getElementById('editLibraryModal').style.display = 'none';
} }
/** function renderLibraryGroupCodeFields(groupMembers, currentItemId) {
* Event-Listener für das Formular (Initialisierung) const codesContainer = document.getElementById('editLibraryCodesContainer');
*/ const groupWarning = document.getElementById('editLibraryGroupWarning');
document.addEventListener('DOMContentLoaded', function() { const groupCount = document.getElementById('editLibraryGroupCount');
const editForm = document.getElementById('editLibraryForm'); const groupHint = document.getElementById('editLibraryGroupHint');
if (editForm) {
editForm.addEventListener('submit', async function(e) {
e.preventDefault();
const itemId = document.getElementById('editLibraryItemId').value; if (!codesContainer) return;
const currentItem = libraryItems.find(i => i._id === itemId);
if (!currentItem) return; const items = Array.isArray(groupMembers) ? groupMembers.slice() : [];
items.sort((a, b) => (a.SeriesPosition || 0) - (b.SeriesPosition || 0) || String(a.Name || '').localeCompare(String(b.Name || '')));
// 1. Alle Mitglieder der Gruppe finden, um die Code-Liste aufzubauen editLibraryState.groupMembers = items;
const groupMembers = libraryItems.filter(i => i.SeriesGroupId === currentItem.SeriesGroupId);
const individualUpdates = groupMembers.map(member => ({
id: member._id,
// Wenn dies das bearbeitete Item ist, nimm den neuen Code, sonst den alten
code_4: (member._id === itemId) ? document.getElementById('editLibraryCode4').value : member.Code_4
}));
// 2. Payload für das Backend bauen
const payload = {
series_group_id: currentItem.SeriesGroupId,
name: document.getElementById('editLibraryName').value,
ort: document.getElementById('editLibraryLocation').value,
beschreibung: document.getElementById('editLibraryDescription').value,
isbn: document.getElementById('editLibraryIsbn').value,
item_type: document.getElementById('editLibraryType').value,
items: individualUpdates
};
// 3. Request an die Gruppen-Update Route
try {
const response = await fetch('/update_group', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const result = await response.json();
if (result.success) {
alert('Gruppe erfolgreich synchronisiert!');
closeEditLibraryModal();
await loadLibraryItems(); // Daten neu laden
// renderTable(); // Ggf. Tabelle neu rendern
} else {
await loadLibraryItems();
closeEditLibraryModal();
}
} catch (error) {
console.error('Update failed:', error);
alert('Netzwerkfehler.');
}
});
if (groupWarning) {
groupWarning.style.display = items.length > 1 ? 'block' : 'none';
} }
}); if (groupCount) {
const totalCount = items.length || 1;
const declaredCount = items[0]?.SeriesCount || totalCount;
groupCount.textContent = `${totalCount} / ${declaredCount}`;
}
if (groupHint) {
groupHint.textContent = items.length > 1
? 'Jeder Code gehört zu einem eigenen Exemplar. Änderungen werden für alle Codes gespeichert.'
: 'Einzelnes Exemplar. Der Code wird direkt gespeichert.';
}
if (!items.length) {
codesContainer.innerHTML = '<div style="padding:10px 0; color:#6b7280;">Keine Codes geladen.</div>';
return;
}
codesContainer.innerHTML = items.map((member, index) => {
const codeValue = member.Code_4 || member.Code4 || '';
const labelParts = [];
if (member.SeriesPosition !== undefined && member.SeriesPosition !== null) {
labelParts.push(`Exemplar ${member.SeriesPosition}`);
} else {
labelParts.push(`Exemplar ${index + 1}`);
}
if (member._id === currentItemId) {
labelParts.push('aktuelles Medium');
}
return `
<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>
<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>
`;
}).join('');
}
function openEditLibraryItem(itemId) {
window.location.href = `/item_edit/${itemId}`;
}
</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 style="margin: 5px 0; font-size: 12px; color: #555;">
Alle aufgeführten Codes gehören zu diesem Datensatz:
</p>
<!-- Hier werden die Codes per JS eingefügt -->
<div id="editLibraryAllCodes" style="display: flex; flex-wrap: wrap; gap: 5px; margin-top: 10px;"></div>
<div style="margin-top: 15px; font-size: 11px; background: #e0f2fe; padding: 8px; border-radius: 4px;">
<strong>Hinweis:</strong> Änderungen an Titel/Ort/Beschreibung werden auf <strong>alle</strong> Exemplare der Range übertragen.
</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 for="editLibraryCode4">Code</label>
<input id="editLibraryCode4" placeholder="optional Mediencode" style="width: 100%;">
</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>
<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 style="margin: 5px 0; font-size: 12px; color: #555;">
Alle Codes in dieser Gruppe:
</p>
<!-- Hier wird die Liste als Komma-Text eingefügt -->
<div id="editLibraryAllCodes" style="font-family: monospace; font-size: 14px; font-weight: bold; color: #333; margin-top: 5px;"></div>
<div style="margin-top: 15px; font-size: 11px; background: #e0f2fe; padding: 8px; border-radius: 4px;">
<strong>Hinweis:</strong> Änderungen an Titel/Ort/Beschreibung werden auf <strong>alle</strong> Exemplare der Range übertragen.
</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 -->
-10
View File
@@ -23,16 +23,6 @@
</a> </a>
</div> </div>
<form method="POST" action="{{ url_for('admin_anonymize_names') }}" class="mb-3">
<button
type="submit"
class="btn btn-outline-danger"
onclick="return confirm('Sollen alle gespeicherten Klarnamen dauerhaft in Synonym-Kuerzel umgewandelt werden?')"
>
Gespeicherte Namen anonymisieren
</button>
</form>
<div class="filter-bar mb-3"> <div class="filter-bar mb-3">
<div class="row g-2 align-items-end"> <div class="row g-2 align-items-end">
<div class="col-md-3"> <div class="col-md-3">