Compare commits

..

34 Commits

Author SHA1 Message Date
Aiirondev_dev c2c5054814 corrected type mismatch
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-12 12:40:05 +02:00
Aiirondev_dev e058bd5f46 spelling mistake
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-12 12:30:25 +02:00
Aiirondev_dev d58958db39 Changes to habe he template name right and the right correction for the library Item and a parameter mismatch
Release Inventarsystem / release-docker (push) Successful in 2m22s
2026-08-12 12:26:10 +02:00
Aiirondev_dev 9452743660 Changes to reflekt the right inventar and bibliothek item fields
Release Inventarsystem / release-docker (push) Successful in 2m17s
2026-08-12 11:59:57 +02:00
Aiirondev_dev 6a3865ef24 fix of the template name
Release Inventarsystem / release-docker (push) Successful in 2m16s
2026-08-12 11:46:59 +02:00
Aiirondev_dev 96d45710ac fix of the template name
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-12 11:45:48 +02:00
Aiirondev_dev dd3d8649a7 fix of the template name
Release Inventarsystem / release-docker (push) Successful in 3m10s
2026-08-12 11:37:05 +02:00
Aiirondev_dev 1af2a2be06 implementation of the image
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-12 00:32:00 +02:00
Aiirondev_dev 8e5e434116 fix of the template name
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-12 00:24:25 +02:00
Aiirondev_dev 2f9a93ee65 fix of the template name
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-12 00:20:48 +02:00
Aiirondev_dev 91467a1e76 fix of the template name
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-12 00:19:19 +02:00
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
Aiirondev_dev 743e5b1c16 Reviewed the edeting function
Release Inventarsystem / release-docker (push) Successful in 2m18s
2026-08-10 23:40:02 +02:00
7 changed files with 2227 additions and 296 deletions
+275 -16
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}
} }
@@ -3332,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)
@@ -3360,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},
@@ -3473,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():
""" """
@@ -3737,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>
@@ -5729,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:
@@ -6314,6 +6366,210 @@ def edit_item(id):
return redirect(url_for('home_admin')) return redirect(url_for('home_admin'))
def is_library_item(item):
"""
Prüft, ob ein Artikel ein Bibliotheks-Item ist.
- 'other', None oder Leerstring -> Inventarsystem (False)
- Jeder andere Medientyp ('Buch', 'CD', etc.) -> Bibliothek (True)
"""
if not item:
return False
item_type = item.get('ItemType', 'other')
if not item_type:
return False
return item_type.strip().lower() != 'other'
@app.route('/item_edit/<id>', methods=['GET', 'POST'])
def item_edit(id):
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'))
current_item = it.get_item(obj_id)
if not current_item:
flash('Element in der Datenbank nicht gefunden.', 'error')
return redirect(url_for('home_admin'))
# Bibliothek-Status ermitteln
library_module_active = cfg.MODULES.is_enabled('library')
is_lib_item = it.is_library_item(current_item)
show_library_features = library_module_active and is_lib_item
# -------------------------------------------------------------------
# GET METHOD
# -------------------------------------------------------------------
if request.method == 'GET':
current_item['_id'] = str(current_item['_id'])
base_code = current_item.get('Code_4', '')
individual_codes = []
if current_item.get('SeriesGroupId'):
group_ids = it.get_group_item_ids(str(current_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)
current_item['IndividualCodes'] = '\n'.join(individual_codes)
return render_template(
'edit_library.html',
username=session['username'],
item=current_item,
show_library_features=show_library_features,
library_module_enabled=library_module_active,
page_title=f"Bearbeiten: {current_item.get('Name', '')}"
)
# -------------------------------------------------------------------
# POST METHOD
# -------------------------------------------------------------------
redirect_target = request.referrer or url_for('home_admin')
name = sanitize_form_value(request.form.get('name'))
ort = sanitize_form_value(request.form.get('ort'))
beschreibung = sanitize_form_value(request.form.get('beschreibung'))
anschaffungs_jahr = sanitize_form_value(request.form.get('anschaffungsjahr'))
anschaffungs_kosten = sanitize_form_value(request.form.get('anschaffungskosten'))
reservierbar = 'reservierbar' in request.form
# Filter 1-3 für alle Objekte
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'))
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
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)
if show_library_features:
isbn_raw = sanitize_form_value(request.form.get('isbn', ''))
item_isbn = normalize_and_validate_isbn(isbn_raw) if isbn_raw else ''
item_type = sanitize_form_value(request.form.get('item_type_input', current_item.get('ItemType', 'Buch')))
library_category = sanitize_form_value(request.form.get('library_category', ''))
images = current_item.get('Images', [])
else:
item_isbn = current_item.get('ISBN', '')
item_type = current_item.get('ItemType', 'other')
library_category = current_item.get('library_category', '')
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"Image error for item {id}: {e}")
if ort and ort not in it.get_predefined_locations():
it.add_predefined_location(ort)
it.sync_group_codes(str(id), code_4, individual_codes)
success = it.update_item(
id=str(id),
name=name,
ort=ort,
beschreibung=beschreibung,
images=images,
verfuegbar=current_item.get('Verfuegbar', True),
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,
library_category=library_category
)
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():
@@ -6336,17 +6592,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': '...'}, ...]
@@ -7643,7 +7902,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,
+154 -9
View File
@@ -20,12 +20,30 @@ 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
import Web.modules.inventarsystem.data_protection as dp import Web.modules.inventarsystem.data_protection as dp
def is_library_item(item):
"""
Ermittelt zuverlässig, ob ein Objekt zur Bibliothek gehört.
Gibt True zurück, wenn ItemType ein Medientyp ist (Buch, Schulbuch, CD, DVD etc.)
ODER wenn is_library explizit True ist.
"""
if not item:
return False
# 1. Prüfe zuerst den Medientyp (ItemType)
item_type = str(item.get('ItemType', '') or '').strip().lower()
if item_type and item_type not in ['other', 'general', 'none', 'null']:
return True
# 2. Falls ItemType 'other' ist, prüfe das is_library Flag
return bool(item.get('is_library', False))
def safe_decrypt_user(encrypted_user): def safe_decrypt_user(encrypted_user):
""" """
Safely decrypt an encrypted username string. Safely decrypt an encrypted username string.
@@ -250,7 +268,10 @@ def get_group_item_ids(id):
def update_item(id, name, ort, beschreibung, images, verfuegbar, filter1, filter2, filter3, def update_item(id, name, ort, beschreibung, images, verfuegbar, filter1, filter2, filter3,
ansch_jahr, ansch_kost, code_4, reservierbar, isbn=None, item_type='general'): ansch_jahr, ansch_kost, code_4, reservierbar, isbn="", item_type='other', library_category=""):
"""
Aktualisiert ein Objekt in MongoDB und setzt is_library korrekt basierend auf dem Medientyp.
"""
try: try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB] db = client[cfg.MONGODB_DB]
@@ -258,29 +279,35 @@ def update_item(id, name, ort, beschreibung, images, verfuegbar, filter1, filter
old_item = items.find_one({'_id': ObjectId(id)}) old_item = items.find_one({'_id': ObjectId(id)})
if not old_item: if not old_item:
client.close()
return False return False
series_group_id = old_item.get('SeriesGroupId') series_group_id = old_item.get('SeriesGroupId')
# is_library automatisch anhand des neuen item_type bestimmen
is_lib = is_library_item({'ItemType': item_type})
shared_update = { shared_update = {
'Name': name, 'Name': name,
'Ort': ort, 'Ort': ort,
'Beschreibung': beschreibung, 'Beschreibung': beschreibung,
'Images': images, 'Images': images if isinstance(images, list) else [],
'Filter': filter1, 'Filter': filter1 if isinstance(filter1, list) else [],
'Filter2': filter2, 'Filter2': filter2 if isinstance(filter2, list) else [],
'Filter3': filter3, 'Filter3': filter3 if isinstance(filter3, list) else [],
'Anschaffungsjahr': ansch_jahr, 'Anschaffungsjahr': ansch_jahr,
'Anschaffungskosten': ansch_kost, 'Anschaffungskosten': ansch_kost,
'Reservierbar': reservierbar, 'Reservierbar': bool(reservierbar),
'ISBN': isbn, 'ISBN': str(isbn) if isbn else '',
'ItemType': item_type, 'ItemType': item_type,
'Verfuegbar': verfuegbar, 'is_library': is_lib,
'library_category': library_category,
'Verfuegbar': bool(verfuegbar),
'LastUpdated': datetime.datetime.now() 'LastUpdated': datetime.datetime.now()
} }
specific_update = shared_update.copy() specific_update = shared_update.copy()
specific_update['Code_4'] = code_4 specific_update['Code_4'] = str(code_4) if code_4 else ''
items.update_one({'_id': ObjectId(id)}, {'$set': specific_update}) items.update_one({'_id': ObjectId(id)}, {'$set': specific_update})
@@ -1148,3 +1175,121 @@ def get_current_status(item_id, decrypt=True):
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
+505
View File
@@ -0,0 +1,505 @@
<!--
Copyright 2025-2026 AIIrondev
Licensed under the Inventarsystem EULA.
-->
{% extends "base.html" %}
{% block title %}{{ page_title|default('Artikel bearbeiten') }} - Inventarsystem{% endblock %}
{% block content %}
<style>
.edit-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: var(--ui-surface);
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.edit-form h1 {
color: var(--ui-text);
margin-bottom: 30px;
text-align: center;
font-size: 2em;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: var(--ui-text);
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
box-sizing: border-box;
}
.form-group textarea {
height: 100px;
resize: vertical;
}
.filter-inputs {
background-color: var(--ui-surface-soft);
padding: 20px;
border-radius: 5px;
margin: 20px 0;
}
.filter-inputs h3 {
color: var(--ui-text);
margin-bottom: 15px;
font-size: 1.2em;
}
.multi-filter {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
}
.filter-dropdown-select {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: white;
}
.isbn-input-group {
display: flex;
gap: 10px;
align-items: center;
}
.isbn-input-group input {
flex: 1;
}
.fetch-isbn-button {
background-color: #007bff;
color: white;
border: none;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
white-space: nowrap;
transition: background-color 0.2s ease;
}
.fetch-isbn-button:hover {
background-color: #0056b3;
}
/* Scanner Elements */
#code4-scanner video, #code4-scanner canvas,
#isbn-scanner video, #isbn-scanner canvas {
width: 100%;
max-width: 640px;
height: auto;
border-radius: 5px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
#code4-scanner canvas, #isbn-scanner canvas {
position: absolute;
top: 0;
left: 0;
}
#code4-scanner, #isbn-scanner {
position: relative;
overflow: hidden;
}
/* Image Management */
.existing-images-grid {
display: flex;
flex-wrap: wrap;
gap: 15px;
margin-top: 10px;
}
.existing-image-card {
border: 1px solid #ddd;
border-radius: 5px;
padding: 8px;
text-align: center;
background: #fff;
width: 120px;
}
.existing-image-card img {
width: 100%;
height: 100px;
object-fit: cover;
border-radius: 4px;
}
.existing-image-card label {
font-size: 0.8em;
margin-top: 5px;
cursor: pointer;
font-weight: normal;
}
.add-new-btn {
background-color: #007bff;
color: white;
border: none;
padding: 5px 10px;
border-radius: 3px;
cursor: pointer;
margin-top: 5px;
font-size: 12px;
}
.submit-button {
background-color: #28a745;
color: white;
border: none;
padding: 15px 30px;
border-radius: 5px;
font-size: 18px;
font-weight: bold;
cursor: pointer;
width: 100%;
margin-top: 20px;
transition: background-color 0.2s ease;
}
.submit-button:hover {
background-color: #218838;
}
.edit-new-location-container {
display: none;
margin-top: 10px;
}
</style>
<div class="edit-container">
<div class="edit-form">
<h1>{{ page_title|default('Artikel bearbeiten') }}</h1>
<form method="POST" action="{{ url_for('item_edit', id=item._id) }}" enctype="multipart/form-data">
<input type="hidden" name="item_id" value="{{ item._id }}">
{% if show_library_features %}
<!-- ================= LIBRARY SPECIFIC FIELDS ================= -->
<div class="form-group">
<label for="isbn">ISBN / Barcode:</label>
<div class="isbn-input-group">
<input type="text" id="isbn" name="isbn" value="{{ item.ISBN|default('') }}" placeholder="ISBN oder Barcode eingeben...">
<button type="button" id="scan-isbn-btn" class="fetch-isbn-button">Barcode scannen</button>
<button type="button" class="fetch-isbn-button" onclick="fetchBookInfo('edit')">Informationen abrufen</button>
</div>
<div id="isbn-scanner" style="width:100%; max-width:520px; display:none; margin-top:10px;"></div>
<small id="isbn-scan-status" style="display:block; color:#666; margin-top:6px;"></small>
<div id="book-info-container"></div>
</div>
<div class="filter-inputs" style="margin-bottom: 20px;">
<h3>Medientyp</h3>
<div class="form-group">
<select name="item_type_input" id="item_type_input">
<option value="Buch" {% if item.ItemType == 'Buch' %}selected{% endif %}>Buch</option>
<option value="Schulbuch" {% if item.ItemType == 'Schulbuch' %}selected{% endif %}>Schulbuch</option>
<option value="CD" {% if item.ItemType == 'CD' %}selected{% endif %}>CD</option>
<option value="DVD" {% if item.ItemType == 'DVD' %}selected{% endif %}>DVD</option>
<option value="Sonstiges" {% if item.ItemType == 'Sonstiges' %}selected{% endif %}>Sonstiges</option>
</select>
</div>
<h3>Bibliotheks-Kategorie:</h3>
<div class="form-group">
<input type="text" name="library_category" id="library_category" value="{{ item.library_category|default('') }}" placeholder="z.B. Belletristik, Sachbücher, etc.">
</div>
</div>
{% endif %}
<!-- ================= COMMON CORE FIELDS ================= -->
<div class="form-group">
<label for="name">Name / Titel:</label>
<input type="text" id="name" name="name" value="{{ item.Name|default('') }}" required>
</div>
<div class="form-group">
<label for="ort">Ort / Standort:</label>
<select id="ort" name="ort" data-selected="{{ item.Ort|default('') }}" required>
<option value="">-- Bitte Ort auswählen --</option>
{% if item.Ort %}
<option value="{{ item.Ort }}" selected>{{ item.Ort }}</option>
{% endif %}
</select>
<button type="button" class="add-new-btn" id="add-new-location-btn">Neuen Ort hinzufügen</button>
<div id="new-location-container" class="edit-new-location-container">
<input type="text" id="new-location-input" placeholder="Neuen Ort eingeben">
<button type="button" onclick="addNewLocation()">Hinzufügen</button>
<button type="button" onclick="cancelAddLocation()">Abbrechen</button>
</div>
</div>
<div class="form-group">
<label for="beschreibung">Beschreibung:</label>
<textarea id="beschreibung" name="beschreibung" required>{{ item.Beschreibung|default('') }}</textarea>
</div>
<div class="form-group" id="primary_code_group">
<label for="code_4">Basis-Code (Haupt-Barcode)</label>
<div style="display: flex; gap: 10px;">
<input type="text" id="code_4" name="code_4" class="form-control" value="{{ item.Code_4|default('') }}" required>
<button type="button" id="scan-code4-btn" class="fetch-isbn-button">Barcode scannen</button>
</div>
<div id="code4-scanner" style="display:none; margin-top: 10px;"></div>
<small id="code4-scan-status" class="form-text text-muted"></small>
</div>
<div class="form-group" id="individual_codes_group">
<label for="individual_codes">Weitere Einzelcodes der Gruppe (je Zeile ein Code)</label>
<textarea id="individual_codes" name="individual_codes" rows="4" class="form-control" placeholder="z.B.&#10;ABC-001&#10;ABC-002">{{ item.IndividualCodes|default('') }}</textarea>
<small style="display:block; color:#666; margin-top: 5px;">Der Basis-Code steht oben. Alle weiteren Gruppenmitglieder werden hier untereinander aufgeführt.</small>
</div>
<!-- ================= SYSTEM FILTERS 1-3 (ALWAYS RENDERED) ================= -->
<div class="filter-inputs">
<h3>Unterrichtsfach (Filter 1):</h3>
<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 '' }}">
<option value="">-- Optional --</option>
</select>
</div>
{% endfor %}
</div>
<h3>Jahrgangsstufe (Filter 2):</h3>
<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 '' }}">
<option value="">-- Optional --</option>
</select>
</div>
{% endfor %}
</div>
<h3>Schlagwort (Filter 3):</h3>
<div class="multi-filter">
{% for idx in range(4) %}
<div class="form-group">
<label for="filter3-{{ idx + 1 }}">Wert {{ idx + 1 }}:</label>
<input type="text" id="filter3-{{ idx + 1 }}" name="filter3" value="{{ item.Filter3[idx] if item.Filter3 and item.Filter3|length > idx else '' }}">
</div>
{% endfor %}
</div>
</div>
<!-- ================= DATES & FINANCIALS ================= -->
<div class="form-group">
<label for="anschaffungsjahr">Anschaffungsjahr:</label>
<input type="date" id="anschaffungsjahr" name="anschaffungsjahr" value="{{ item.Anschaffungsjahr|default('') }}">
</div>
<div class="form-group">
<label for="anschaffungskosten">Anschaffungskosten (€):</label>
<input type="text" id="anschaffungskosten" name="anschaffungskosten" value="{{ item.Anschaffungskosten|default('') }}">
</div>
{% if not show_library_features %}
<!-- ================= INVENTORY IMAGE MANAGEMENT ================= -->
<div class="form-group">
<label>Bestehende Bilder behalten:</label>
{% if item.Images and item.Images|length > 0 %}
<div class="existing-images-grid">
{% for img in item.Images %}
<div class="existing-image-card">
<img src="{{ url_for('uploaded_file', filename=img) }}" alt="Bild">
<label>
<input type="checkbox" name="existing_images" value="{{ img }}" checked>
Behalten
</label>
</div>
{% endfor %}
</div>
{% else %}
<p style="color:#777; font-size:0.9em;">Keine Bilder vorhanden.</p>
{% endif %}
<label for="images" style="margin-top:15px;">Neue Bilder/Videos hinzufügen:</label>
<input type="file" id="images" name="images" accept=".jpg, .jpeg, .png, .gif, .mp4, .mov, .avi, .mkv, .webm" multiple>
</div>
{% endif %}
<div class="form-group">
<label for="reservierbar" style="display:inline-block; width:auto; margin-right:10px;">Reservierbar:</label>
<input type="checkbox" id="reservierbar" name="reservierbar" style="width:auto;" {% if item.Reservierbar %}checked{% endif %}>
</div>
<button type="submit" class="submit-button">Änderungen speichern</button>
</form>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2/dist/quagga.js"></script>
<script>
let scannerRunning = false;
let activeScannerCallback = null;
let code4LastScanned = '';
let code4LastScannedAt = 0;
function loadAndSelectFilterValues(filterNumber) {
fetch(`/get_predefined_filter_values/${filterNumber}`)
.then(res => res.json())
.then(data => {
for (let i = 1; i <= 4; i++) {
const select = document.getElementById(`filter${filterNumber}-${i}`);
if (!select) continue;
const selectedValue = select.getAttribute('data-selected') || '';
data.values.forEach(val => {
if (!val || String(val).trim() === '') return;
const opt = document.createElement('option');
opt.value = val;
opt.textContent = val;
select.appendChild(opt);
});
if (selectedValue && !Array.from(select.options).some(o => o.value === selectedValue)) {
const customOpt = document.createElement('option');
customOpt.value = selectedValue;
customOpt.textContent = selectedValue;
select.appendChild(customOpt);
}
select.value = selectedValue;
}
})
.catch(err => console.error(`Error loading Filter ${filterNumber}:`, err));
}
function loadLocationOptions() {
fetch('/get_predefined_locations')
.then(res => res.json())
.then(data => {
const select = document.getElementById('ort');
if (!select) return;
const currentVal = select.getAttribute('data-selected') || select.value;
data.locations.forEach(loc => {
if (!Array.from(select.options).some(o => o.value === loc)) {
const opt = document.createElement('option');
opt.value = loc;
opt.textContent = loc;
select.appendChild(opt);
}
});
select.value = currentVal;
})
.catch(err => console.error('Error loading locations:', err));
}
function addNewLocation() {
const input = document.getElementById('new-location-input');
const val = input.value.trim();
if (!val) return;
const select = document.getElementById('ort');
const opt = document.createElement('option');
opt.value = val;
opt.textContent = val;
opt.selected = true;
select.appendChild(opt);
document.getElementById('new-location-container').style.display = 'none';
input.value = '';
}
function cancelAddLocation() {
document.getElementById('new-location-container').style.display = 'none';
document.getElementById('new-location-input').value = '';
}
function runEngineInitialization(targetSelector, activeCallback, completionMsg, errorStatusSetter) {
if (scannerRunning) { Quagga.stop(); scannerRunning = false; }
activeScannerCallback = activeCallback;
Quagga.init({
inputStream: { name: "Live", type: "LiveStream", target: document.querySelector(targetSelector), constraints: { width: 640, height: 480, facingMode: "environment" } },
decoder: { readers: ["code_128_reader", "ean_reader", "code_39_reader", "upc_reader"] }
}, function(err) {
if (err) { errorStatusSetter("Kamera-Fehler.", true); return; }
Quagga.start(); scannerRunning = true; errorStatusSetter(completionMsg, false);
});
}
function killScannerHardware() {
if (!scannerRunning) return;
Quagga.stop(); scannerRunning = false; activeScannerCallback = null;
}
Quagga.onDetected(function(data) {
if (!data || !data.codeResult || !data.codeResult.code) return;
if (typeof activeScannerCallback === "function") activeScannerCallback(String(data.codeResult.code).trim());
});
function startCode4Scanner() {
const scannerBox = document.getElementById('code4-scanner');
const scanBtn = document.getElementById('scan-code4-btn');
const baseField = document.getElementById('code_4');
const indArea = document.getElementById('individual_codes');
if (scannerBox.style.display !== 'none') {
killScannerHardware(); scannerBox.style.display = 'none'; scanBtn.textContent = 'Barcode scannen'; return;
}
scannerBox.style.display = 'block'; scanBtn.textContent = 'Scanner stoppen';
runEngineInitialization('#code4-scanner', function(decodedText) {
const now = Date.now();
if (decodedText === code4LastScanned && (now - code4LastScannedAt) < 1500) return;
code4LastScanned = decodedText; code4LastScannedAt = now;
killScannerHardware(); scannerBox.style.display = 'none'; scanBtn.textContent = 'Barcode scannen';
if (!baseField.value.trim()) {
baseField.value = decodedText;
} else {
let codes = indArea.value.split('\n').map(c => c.trim()).filter(c => c);
if (!codes.includes(decodedText) && baseField.value.trim() !== decodedText) {
codes.push(decodedText);
indArea.value = codes.join('\n');
}
}
}, 'Scanner läuft...', msg => { document.getElementById('code4-scan-status').textContent = msg; });
}
document.addEventListener('DOMContentLoaded', function() {
loadLocationOptions();
// Always load Filter 1 and Filter 2 options for all items
loadAndSelectFilterValues(1);
loadAndSelectFilterValues(2);
const scanCodeBtn = document.getElementById('scan-code4-btn');
if (scanCodeBtn) scanCodeBtn.addEventListener('click', startCode4Scanner);
const addLocBtn = document.getElementById('add-new-location-btn');
if (addLocBtn) addLocBtn.addEventListener('click', () => {
const c = document.getElementById('new-location-container');
c.style.display = c.style.display === 'none' ? 'block' : 'none';
});
});
</script>
{% endblock %}
+163 -226
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>
@@ -669,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');
@@ -795,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>
@@ -1022,47 +1027,21 @@
} }
async function processQuickToggleScan(scannedCode) { async function processQuickToggleScan(scannedCode) {
if (!activeStudentCardId) { // 1. Prüfen, ob "Nur Rückgabe"-Modus aktiv ist
// If return-only mode is active, always attempt to return by code
const returnOnly = (document.getElementById('returnOnlyToggle') || {}).checked; const returnOnly = (document.getElementById('returnOnlyToggle') || {}).checked;
if (returnOnly) { if (returnOnly) {
await returnByCode(scannedCode); await returnByCode(scannedCode);
return; 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)
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;
}
}
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', {
@@ -1098,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');
@@ -1330,7 +1336,7 @@
} }
// 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!
@@ -1360,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);
}); });
} }
@@ -1394,24 +1400,75 @@
}); });
} }
// Edit Modal Form processing
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 {
if (isGroupedEdit) {
const payload = {
series_group_id: currentItem.SeriesGroupId,
...sharedPayload,
items: groupMembers.map(member => ({
id: member._id,
code_4: codeByItemId.get(member._id) || ''
}))
};
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)
});
const result = await response.json();
if (response.ok && result.success) {
alert(result.message || 'Gruppe erfolgreich aktualisiert!');
closeEditLibraryModal();
pagingState.loading = false;
await loadLibraryItems();
} else {
alert(result.message || 'Fehler beim Speichern der Gruppenänderungen.');
}
} else {
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`, { const response = await fetch(`/api/library_item/${itemId}/update`, {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -1419,20 +1476,19 @@
'X-CSRFToken': '{{ csrf_token }}', 'X-CSRFToken': '{{ csrf_token }}',
'X-CSRF-Token': '{{ csrf_token }}' 'X-CSRF-Token': '{{ csrf_token }}'
}, },
body: JSON.stringify(updatedData) body: JSON.stringify(payload)
}); });
const result = await response.json(); const result = await response.json();
if (response.ok && result.ok) { if (response.ok && result.ok) {
alert(result.message || 'Medium erfolgreich aktualisiert!'); alert(result.message || 'Medium erfolgreich aktualisiert!');
closeEditLibraryModal(); closeEditLibraryModal();
pagingState.loading = false; pagingState.loading = false;
loadLibraryItems(); await loadLibraryItems();
} else { } else {
alert(result.message || 'Fehler beim Speichern der Änderungen.'); alert(result.message || 'Fehler beim Speichern der Änderungen.');
} }
}
} catch (error) { } catch (error) {
console.error('Update failed:', error); console.error('Update failed:', error);
alert('Netzwerkfehler beim Aktualisieren des Mediums.'); alert('Netzwerkfehler beim Aktualisieren des Mediums.');
@@ -1441,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;
// 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('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 || "?");
warningDiv.style.display = 'block';
} else {
warningDiv.style.display = 'none';
}
document.getElementById('editLibraryModal').style.display = 'flex';
};
function closeEditLibraryModal() {
document.getElementById('editLibraryModal').style.display = 'none';
}
/**
* Event-Listener für das Formular (Initialisierung)
*/
document.addEventListener('DOMContentLoaded', function() {
const editForm = document.getElementById('editLibraryForm');
if (editForm) {
editForm.addEventListener('submit', async function(e) {
e.preventDefault();
const itemId = document.getElementById('editLibraryItemId').value;
const currentItem = libraryItems.find(i => i._id === itemId);
if (!currentItem) return;
// 1. Alle Mitglieder der Gruppe finden, um die Code-Liste aufzubauen
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 { try {
const response = await fetch('/update_group', { const response = await fetch(`/api/library_group/${encodeURIComponent(seriesGroupId)}`);
method: 'POST', if (!response.ok) {
headers: { 'Content-Type': 'application/json' }, throw new Error(`HTTP ${response.status}`);
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();
} }
const payload = await response.json();
return Array.isArray(payload.items) ? payload.items : [];
} catch (error) { } catch (error) {
console.error('Update failed:', error); console.warn('Falling back to loaded library items for group editing:', error);
alert('Netzwerkfehler.'); return (libraryItems || []).filter(item => item.SeriesGroupId === seriesGroupId);
}
} }
});
function renderLibraryGroupCodeFields(groupMembers, currentItemId) {
const codesContainer = document.getElementById('editLibraryCodesContainer');
const groupWarning = document.getElementById('editLibraryGroupWarning');
const groupCount = document.getElementById('editLibraryGroupCount');
const groupHint = document.getElementById('editLibraryGroupHint');
if (!codesContainer) 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 || '')));
editLibraryState.groupMembers = items;
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 -->