diff --git a/Web/app.py b/Web/app.py index 03b9ece..13acc50 100755 --- a/Web/app.py +++ b/Web/app.py @@ -46,7 +46,7 @@ import Web.modules.inventarsystem.pdf_export as pdf_export import Web.modules.inventarsystem.excel_export as excel_export import datetime from apscheduler.schedulers.background import BackgroundScheduler -from bson.objectid import ObjectId +from bson.objectid import ObjectId, InvalidId from urllib.parse import urlparse, urlunparse import requests import csv @@ -3314,6 +3314,7 @@ def api_library_items(): query = { 'ItemType': {'$in': ['book', 'cd', 'dvd', 'schoolbook', 'schulbuch', 'Buch', 'Schulbuch']}, + 'IsGroupedSubItem': {'$ne': True}, 'Deleted': {'$ne': True} } @@ -6366,6 +6367,181 @@ def edit_item(id): return redirect(url_for('home_admin')) +@app.route('/item_edit/', 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')) + + db = it.db + items_col = db['items'] + + # --- GET: Template anzeigen --- + if request.method == 'GET': + item = items_col.find_one({'_id': obj_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( + 'item_edit.html', + username=session['username'], + item=item, + library_module_enabled=show_library, + show_library_features=show_library, + page_title=f"Bearbeiten: {item.get('Name', '')}" + ) + + # --- POST: Speichern --- + redirect_target = request.referrer or url_for('home_admin') + current_item = items_col.find_one({'_id': obj_id}) + + if not current_item: + flash('Element in der Datenbank nicht gefunden.', 'error') + return redirect(redirect_target) + + # Formulardaten + 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')) + + # 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 + 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 + 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) + + # Datenstruktur für Update + shared_fields = { + 'Name': name, + 'Ort': ort, + 'Beschreibung': beschreibung, + 'Anschaffungsjahr': anschaffungs_jahr, + 'Anschaffungskosten': anschaffungs_kosten, + 'Reservierbar': reservierbar, + 'ISBN': item_isbn, + 'ItemType': item_type, + 'Kategorie': library_category, + 'LastUpdated': datetime.datetime.now() + } + + # Gruppen-Update + group_item_ids = it.get_group_item_ids(str(id)) + + if group_item_ids: + group_object_ids = [ObjectId(g_id) for g_id in group_item_ids] + items_col.update_many({'_id': {'$in': group_object_ids}}, {'$set': shared_fields}) + + # Individual-Update + individual_update = { + **shared_fields, + 'Code_4': code_4, + 'Images': images, + 'Filter1': filter1, + 'Filter2': filter2, + 'Filter3': filter3 + } + + items_col.update_one({'_id': obj_id}, {'$set': individual_update}) + + flash('Artikel erfolgreich aktualisiert.', 'success') + return redirect(redirect_target) + + @app.route('/update_group', methods=['POST']) def update_group(): diff --git a/Web/templates/edit_library.hmtl.html b/Web/templates/edit_library.hmtl.html new file mode 100644 index 0000000..233f951 --- /dev/null +++ b/Web/templates/edit_library.hmtl.html @@ -0,0 +1,1005 @@ + +{% extends "base.html" %} + +{% block title %}{{ page_title|default('Artikel bearbeiten') }} - Inventarsystem{% endblock %} + +{% block content %} + + +{% endif %} + + + +
+
+

{{ page_title|default('Artikel bearbeiten') }}

+ +
+ + + + + {% if show_library_features %} + +
+ +
+ + + +
+ + +
+
+ {% endif %} + + +
+ + +
+ + +
+ + + + +
+ + +
+ + +
+ + +
+ +
+ + +
+ + +
+ + {% if show_library_features %} + +
+

Medientyp

+
+ {% set current_type = item.ItemType if item.ItemType else '' %} + +
+ +

Kategorie / Fach:

+
+ +
+
+ {% else %} + +
+

Unterrichtsfach:

+
+ {% for i in range(1, 5) %} +
+ + +
+ {% endfor %} +
+ +

Jahrgangsstufe:

+
+ {% for i in range(1, 5) %} +
+ + +
+ {% endfor %} +
+ +

Schlagwörter:

+
+ {% for i in range(1, 5) %} +
+ + +
+ {% endfor %} +
+
+ {% endif %} + + +
+ + +
+ +
+ + +
+ + +
+ +
+ {% if item.Images %} + {% for img in item.Images %} +
+ + +
+ {% endfor %} + {% else %} + Keine Bilder vorhanden. + {% endif %} +
+ + + +
+
+ + +
+ + +
+ + +
+
+
+ + + \ No newline at end of file diff --git a/Web/templates/library_table.html b/Web/templates/library_table.html index bb7047f..fcd744b 100644 --- a/Web/templates/library_table.html +++ b/Web/templates/library_table.html @@ -800,7 +800,7 @@ return ` ${escapeHtml(item.Name || 'Untitled')} - ${escapeHtml(item.Code_4 || item.Code4 || '-')} + ${escapeHtml(Item.ISBN || '-')} ${getItemTypeLabel(item.ItemType || 'book')} ${item.Quantity || item.GroupedDisplayCount || 1} @@ -1565,197 +1565,8 @@ }).join(''); } - window.openEditLibraryItem = async function (itemId) { - const item = libraryItems.find(i => i._id === itemId); - if (!item) return; - - editLibraryState.itemId = item._id; - editLibraryState.seriesGroupId = item.SeriesGroupId || ''; - editLibraryState.groupMembers = []; - - // 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('editLibraryDescription').value = item.Beschreibung; - - // Orte laden und bestehenden Ort setzen - await loadEditLocationOptions(item.Ort); - - const codesContainer = document.getElementById('editLibraryCodesContainer'); - if (codesContainer) { - codesContainer.innerHTML = '
Lade Codes...
'; - } - - const groupMembers = item.SeriesGroupId ? await fetchLibraryGroupMembers(item.SeriesGroupId) : [item]; - renderLibraryGroupCodeFields(groupMembers.length > 0 ? groupMembers : [item], item._id); - - document.getElementById('editLibraryModal').style.display = 'flex'; - }; - - function closeEditLibraryModal() { - document.getElementById('editLibraryModal').style.display = 'none'; - // Setzt das Feld für neuen Ort sicherheitshalber zurück, wenn geschlossen wird - cancelEditAddLocation(); - } - - // Lädt die Optionen in das Dropdown und wählt den aktuellen Ort des Mediums aus - async function loadEditLocationOptions(currentLocation) { - try { - const response = await fetch('/get_predefined_locations'); - const data = await response.json(); - const ortSelect = document.getElementById('editLibraryLocation'); - - if (ortSelect) { - // Bestehende Optionen löschen (außer die erste Platzhalter-Option) - while (ortSelect.children.length > 1) { - ortSelect.removeChild(ortSelect.lastChild); - } - - let locationFound = false; - - // Neue Optionen aus der Datenbank hinzufügen - data.locations.forEach(location => { - const option = document.createElement('option'); - option.value = location; - option.textContent = location; - ortSelect.appendChild(option); - - if (location === currentLocation) { - locationFound = true; - } - }); - - // WICHTIG: Falls der aktuell hinterlegte Ort nicht in der Datenbank-Liste ist, - // fügen wir ihn als temporäre Option hinzu, damit er nicht beim Speichern überschrieben wird. - if (currentLocation && !locationFound) { - const option = document.createElement('option'); - option.value = currentLocation; - option.textContent = currentLocation; - ortSelect.appendChild(option); - } - - // Zuletzt den aktuellen Ort anwählen - if (currentLocation) { - ortSelect.value = currentLocation; - } - } - } catch (error) { - console.error('Error loading location options:', error); - } - } - - // Funktion um einen neuen Ort ins Bearbeiten-Dropdown aufzunehmen - function addEditNewLocation() { - const newLocationInput = document.getElementById('edit-new-location-input'); - const newLocation = newLocationInput.value.trim(); - - if (!newLocation) { - alert('Bitte geben Sie einen Ort ein.'); - return; - } - - // Füge Option dem Dropdown hinzu - const ortSelect = document.getElementById('editLibraryLocation'); - const option = document.createElement('option'); - option.value = newLocation; - option.textContent = newLocation; - ortSelect.appendChild(option); - - // Wähle die neue Option direkt aus - ortSelect.value = newLocation; - - // Verstecke die Eingabemaske wieder - cancelEditAddLocation(); - } - - // Funktion zum Abbrechen der Orteingabe - function cancelEditAddLocation() { - const container = document.getElementById('edit-new-location-container'); - if(container) container.style.display = 'none'; - - const input = document.getElementById('edit-new-location-input'); - if(input) input.value = ''; + function openEditLibraryItem(itemId) { + window.location.href = `/item_edit/${itemId}`; } - - {% endblock %} \ No newline at end of file