Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 073652c63d | |||
| 23d9d0871a | |||
| 576d31a384 | |||
| c43bb30301 | |||
| 7eeea1d319 | |||
| 56486bcbb2 | |||
| 2c3fb779ce | |||
| 299d11c1d0 | |||
| 4d1f95cb1f | |||
| 6b197fd9d1 | |||
| 58064ee249 | |||
| f7573b9a62 | |||
| 85d758fadb | |||
| 212dd2abcf | |||
| 689819df08 | |||
| 457d32a087 | |||
| cded7b02fe | |||
| f314b5ae70 | |||
| 77c4e1b4c5 | |||
| e17f098c47 | |||
| f41acc78cd | |||
| a7a5341961 | |||
| 9ace2e2e5e |
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
NUITKA_BUILD=0
|
NUITKA_BUILD=0
|
||||||
INVENTAR_HTTP_PORT=10000
|
INVENTAR_HTTP_PORT=10000
|
||||||
INVENTAR_HTTP_PORTS=10000
|
INVENTAR_HTTP_PORTS=10000
|
||||||
INVENTAR_APP_IMAGE=ghcr.io/aiirondev/legendary-octo-garbanzo:latest
|
INVENTAR_APP_IMAGE=ghcr.io/aiirondev/legendary-octo-garbanzo:v0.7.42
|
||||||
INVENTAR_APP_CPUS=1.0
|
INVENTAR_APP_CPUS=1.0
|
||||||
INVENTAR_APP_MEM_LIMIT=384m
|
INVENTAR_APP_MEM_LIMIT=384m
|
||||||
INVENTAR_APP_MEM_SWAP_LIMIT=768m
|
INVENTAR_APP_MEM_SWAP_LIMIT=768m
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
v0.7.42
|
||||||
+192
-127
@@ -25,9 +25,10 @@ Features:
|
|||||||
- Booking and reservation of items
|
- Booking and reservation of items
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from flask import Flask, render_template, request, redirect, url_for, session, flash, send_from_directory, get_flashed_messages, jsonify, Response, make_response, send_file
|
from flask import Flask, render_template, request, redirect, url_for, session, flash, send_from_directory, get_flashed_messages, jsonify, Response, make_response, send_file, abort
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||||
|
from jinja2 import TemplateNotFound
|
||||||
import user as us
|
import user as us
|
||||||
import items as it
|
import items as it
|
||||||
import ausleihung as au
|
import ausleihung as au
|
||||||
@@ -269,40 +270,6 @@ def _get_csrf_token():
|
|||||||
def _is_csrf_exempt_request():
|
def _is_csrf_exempt_request():
|
||||||
return request.method in {'GET', 'HEAD', 'OPTIONS', 'TRACE'}
|
return request.method in {'GET', 'HEAD', 'OPTIONS', 'TRACE'}
|
||||||
|
|
||||||
def _is_library_module_path(path):
|
|
||||||
"""Return True when the current request path belongs to the library module."""
|
|
||||||
if not path:
|
|
||||||
return False
|
|
||||||
|
|
||||||
library_prefixes = (
|
|
||||||
'/library',
|
|
||||||
'/library_',
|
|
||||||
'/student_cards',
|
|
||||||
)
|
|
||||||
return path.startswith(library_prefixes)
|
|
||||||
|
|
||||||
def _is_inventory_module_path(path):
|
|
||||||
"""Return True when the current request path belongs to the inventory module."""
|
|
||||||
if not path:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if path == '/' or path.startswith('/home'):
|
|
||||||
return True
|
|
||||||
|
|
||||||
inventory_prefixes = (
|
|
||||||
'/scanner',
|
|
||||||
'/inventory',
|
|
||||||
'/upload_admin',
|
|
||||||
'/manage_filters',
|
|
||||||
'/manage_locations',
|
|
||||||
'/admin_borrowings',
|
|
||||||
'/admin_damaged_items',
|
|
||||||
'/admin/borrowings',
|
|
||||||
'/admin/damaged_items',
|
|
||||||
'/terminplan',
|
|
||||||
)
|
|
||||||
return path.startswith(inventory_prefixes)
|
|
||||||
|
|
||||||
|
|
||||||
@app.before_request
|
@app.before_request
|
||||||
def _enforce_csrf_protection():
|
def _enforce_csrf_protection():
|
||||||
@@ -387,14 +354,28 @@ def _enforce_active_session_user():
|
|||||||
@app.before_request
|
@app.before_request
|
||||||
def _enforce_module_access():
|
def _enforce_module_access():
|
||||||
endpoint = request.endpoint or ''
|
endpoint = request.endpoint or ''
|
||||||
if endpoint == 'static' or endpoint.startswith('static') or request.path.startswith('/api/'):
|
if endpoint == 'static' or endpoint.startswith('static'):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if not cfg.LIBRARY_MODULE_ENABLED and _is_library_module_path(request.path):
|
for name, matcher in cfg.MODULES._path_matchers.items():
|
||||||
return "Bibliotheks-Modul ist deaktiviert.", 403
|
if matcher(request.path) and not cfg.MODULES.is_enabled(name):
|
||||||
|
msg = {
|
||||||
|
'library': "Bibliotheks-Modul ist deaktiviert.",
|
||||||
|
'inventory': "Inventar-Modul ist deaktiviert.",
|
||||||
|
'student_cards': "Schülerausweis-Modul ist deaktiviert."
|
||||||
|
}.get(name, f"{name.capitalize()}-Modul ist deaktiviert.")
|
||||||
|
|
||||||
if not cfg.INVENTORY_MODULE_ENABLED and _is_inventory_module_path(request.path):
|
if request.path.startswith('/api/') or request.is_json:
|
||||||
return "Inventar-Modul ist deaktiviert.", 403
|
return jsonify({'ok': False, 'message': msg}), 403
|
||||||
|
|
||||||
|
flash(msg, 'info')
|
||||||
|
|
||||||
|
if name != 'inventory' and cfg.MODULES.is_enabled('inventory'):
|
||||||
|
return redirect(url_for('home'))
|
||||||
|
elif name != 'library' and cfg.MODULES.is_enabled('library'):
|
||||||
|
return redirect(url_for('library_view'))
|
||||||
|
|
||||||
|
return redirect(url_for('my_borrowed_items'))
|
||||||
|
|
||||||
""" -----------------------------------------------------------After Request Handlers----------------------------------------------------------------------------- """
|
""" -----------------------------------------------------------After Request Handlers----------------------------------------------------------------------------- """
|
||||||
|
|
||||||
@@ -432,6 +413,26 @@ def _set_security_headers(response):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@app.errorhandler(TemplateNotFound)
|
||||||
|
def handle_template_not_found(e):
|
||||||
|
"""Handle missing template files with a 404 response."""
|
||||||
|
app.logger.error(f"Template not found: {e.name}")
|
||||||
|
if request.is_json or request.path.startswith('/api/'):
|
||||||
|
return jsonify({'error': 'Template not found', 'status': 404}), 404
|
||||||
|
return abort(404)
|
||||||
|
|
||||||
|
|
||||||
|
@app.errorhandler(404)
|
||||||
|
def handle_not_found(e):
|
||||||
|
"""Handle 404 errors with a user-friendly response."""
|
||||||
|
app.logger.warning(f"404 Not Found: {request.path}")
|
||||||
|
if request.is_json or request.path.startswith('/api/'):
|
||||||
|
return jsonify({'error': 'Not found', 'status': 404}), 404
|
||||||
|
if 'username' in session:
|
||||||
|
return render_template('error.html', error_code=404, error_message='Seite nicht gefunden.'), 404
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
|
||||||
def _csrf_error_response(message='CSRF token fehlt oder ist ungültig.'):
|
def _csrf_error_response(message='CSRF token fehlt oder ist ungültig.'):
|
||||||
if request.is_json or request.path.startswith('/api/') or request.path in {'/download_book_cover', '/proxy_image', '/log_mobile_issue'}:
|
if request.is_json or request.path.startswith('/api/') or request.path in {'/download_book_cover', '/proxy_image', '/log_mobile_issue'}:
|
||||||
return jsonify({'error': message}), 400
|
return jsonify({'error': message}), 400
|
||||||
@@ -440,24 +441,18 @@ def _csrf_error_response(message='CSRF token fehlt oder ist ungültig.'):
|
|||||||
|
|
||||||
def _get_current_module(path):
|
def _get_current_module(path):
|
||||||
"""Resolve the active UI module for navbar separation."""
|
"""Resolve the active UI module for navbar separation."""
|
||||||
|
mod = cfg.MODULES.get_module_for_path(path)
|
||||||
if cfg.LIBRARY_MODULE_ENABLED and _is_library_module_path(path):
|
if mod:
|
||||||
session['last_module'] = 'library'
|
session['last_module'] = mod
|
||||||
return 'library'
|
return mod
|
||||||
if cfg.INVENTORY_MODULE_ENABLED and _is_inventory_module_path(path):
|
|
||||||
session['last_module'] = 'inventory'
|
|
||||||
return 'inventory'
|
|
||||||
|
|
||||||
last_module = session.get('last_module')
|
last_module = session.get('last_module')
|
||||||
if last_module == 'library' and cfg.LIBRARY_MODULE_ENABLED:
|
if last_module and cfg.MODULES.is_enabled(last_module):
|
||||||
return 'library'
|
return last_module
|
||||||
if last_module == 'inventory' and cfg.INVENTORY_MODULE_ENABLED:
|
|
||||||
return 'inventory'
|
|
||||||
|
|
||||||
# Default fallback: prefer inventory if enabled, otherwise library, else inventory
|
if cfg.MODULES.is_enabled('inventory'):
|
||||||
if cfg.INVENTORY_MODULE_ENABLED:
|
|
||||||
return 'inventory'
|
return 'inventory'
|
||||||
return 'library' if cfg.LIBRARY_MODULE_ENABLED else 'inventory'
|
return 'library' if cfg.MODULES.is_enabled('library') else 'inventory'
|
||||||
|
|
||||||
"""---------------------------------------------User Access Permissions----------------------------------------------------------------------------- """
|
"""---------------------------------------------User Access Permissions----------------------------------------------------------------------------- """
|
||||||
|
|
||||||
@@ -1063,9 +1058,9 @@ def inject_version():
|
|||||||
'csrf_token': csrf_token,
|
'csrf_token': csrf_token,
|
||||||
'CURRENT_MODULE': current_module,
|
'CURRENT_MODULE': current_module,
|
||||||
'school_periods': SCHOOL_PERIODS,
|
'school_periods': SCHOOL_PERIODS,
|
||||||
'inventory_module_enabled': cfg.INVENTORY_MODULE_ENABLED,
|
'inventory_module_enabled': cfg.MODULES.is_enabled('inventory'),
|
||||||
'library_module_enabled': cfg.LIBRARY_MODULE_ENABLED,
|
'library_module_enabled': cfg.MODULES.is_enabled('library'),
|
||||||
'student_cards_module_enabled': cfg.STUDENT_CARDS_MODULE_ENABLED,
|
'student_cards_module_enabled': cfg.MODULES.is_enabled('student_cards'),
|
||||||
'is_admin': is_admin,
|
'is_admin': is_admin,
|
||||||
'unread_notification_count': unread_notification_count,
|
'unread_notification_count': unread_notification_count,
|
||||||
'current_permissions': current_permissions,
|
'current_permissions': current_permissions,
|
||||||
@@ -1750,7 +1745,7 @@ def _upload_student_cards_excel():
|
|||||||
flash('Administratorrechte erforderlich.', 'error')
|
flash('Administratorrechte erforderlich.', 'error')
|
||||||
return redirect(url_for('home_admin'))
|
return redirect(url_for('home_admin'))
|
||||||
|
|
||||||
if not cfg.STUDENT_CARDS_MODULE_ENABLED:
|
if not cfg.MODULES.is_enabled('student_cards'):
|
||||||
flash('Schülerausweis-Modul ist deaktiviert.', 'error')
|
flash('Schülerausweis-Modul ist deaktiviert.', 'error')
|
||||||
return redirect(url_for('home_admin'))
|
return redirect(url_for('home_admin'))
|
||||||
|
|
||||||
@@ -1945,7 +1940,7 @@ def _upload_excel_items(scope='inventory'):
|
|||||||
fallback_route = 'library_admin' if is_library_scope else 'upload_admin'
|
fallback_route = 'library_admin' if is_library_scope else 'upload_admin'
|
||||||
|
|
||||||
if is_library_scope:
|
if is_library_scope:
|
||||||
if not cfg.LIBRARY_MODULE_ENABLED:
|
if not cfg.MODULES.is_enabled('library'):
|
||||||
flash('Bibliotheks-Modul ist deaktiviert.', 'error')
|
flash('Bibliotheks-Modul ist deaktiviert.', 'error')
|
||||||
return redirect(url_for('home'))
|
return redirect(url_for('home'))
|
||||||
|
|
||||||
@@ -2643,8 +2638,8 @@ def home():
|
|||||||
flash('Bitte mit registriertem Konto anmelden!', 'error')
|
flash('Bitte mit registriertem Konto anmelden!', 'error')
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
if not cfg.INVENTORY_MODULE_ENABLED:
|
if not cfg.MODULES.is_enabled('inventory'):
|
||||||
if cfg.LIBRARY_MODULE_ENABLED:
|
if cfg.MODULES.is_enabled('library'):
|
||||||
return redirect(url_for('library_view'))
|
return redirect(url_for('library_view'))
|
||||||
else:
|
else:
|
||||||
return "Weder Inventar- noch Bibliotheks-Modul sind aktiviert.", 403
|
return "Weder Inventar- noch Bibliotheks-Modul sind aktiviert.", 403
|
||||||
@@ -2653,10 +2648,11 @@ def home():
|
|||||||
return render_template(
|
return render_template(
|
||||||
'main.html',
|
'main.html',
|
||||||
username=session['username'],
|
username=session['username'],
|
||||||
library_module_enabled=cfg.LIBRARY_MODULE_ENABLED,
|
library_module_enabled=cfg.MODULES.is_enabled('library'),
|
||||||
student_cards_module_enabled=cfg.STUDENT_CARDS_MODULE_ENABLED,
|
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
|
||||||
student_default_borrow_days=cfg.STUDENT_DEFAULT_BORROW_DAYS,
|
student_default_borrow_days=cfg.STUDENT_DEFAULT_BORROW_DAYS,
|
||||||
student_max_borrow_days=cfg.STUDENT_MAX_BORROW_DAYS
|
student_max_borrow_days=cfg.STUDENT_MAX_BORROW_DAYS,
|
||||||
|
open_item=request.args.get('open_item')
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
permissions = _get_current_user_permissions() or us.build_default_permission_payload('standard_user')
|
permissions = _get_current_user_permissions() or us.build_default_permission_payload('standard_user')
|
||||||
@@ -2682,8 +2678,8 @@ def home_admin():
|
|||||||
flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error')
|
flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error')
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
if not cfg.INVENTORY_MODULE_ENABLED:
|
if not cfg.MODULES.is_enabled('inventory'):
|
||||||
if cfg.LIBRARY_MODULE_ENABLED:
|
if cfg.MODULES.is_enabled('library'):
|
||||||
return redirect(url_for('library_admin'))
|
return redirect(url_for('library_admin'))
|
||||||
else:
|
else:
|
||||||
return "Weder Inventar- noch Bibliotheks-Modul sind aktiviert.", 403
|
return "Weder Inventar- noch Bibliotheks-Modul sind aktiviert.", 403
|
||||||
@@ -2694,11 +2690,12 @@ def home_admin():
|
|||||||
return render_template(
|
return render_template(
|
||||||
'main_admin.html',
|
'main_admin.html',
|
||||||
username=session['username'],
|
username=session['username'],
|
||||||
library_module_enabled=cfg.LIBRARY_MODULE_ENABLED,
|
library_module_enabled=cfg.MODULES.is_enabled('library'),
|
||||||
student_cards_module_enabled=cfg.STUDENT_CARDS_MODULE_ENABLED,
|
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
|
||||||
student_default_borrow_days=cfg.STUDENT_DEFAULT_BORROW_DAYS,
|
student_default_borrow_days=cfg.STUDENT_DEFAULT_BORROW_DAYS,
|
||||||
student_max_borrow_days=cfg.STUDENT_MAX_BORROW_DAYS,
|
student_max_borrow_days=cfg.STUDENT_MAX_BORROW_DAYS,
|
||||||
school_info=_get_school_info_for_export(),
|
school_info=_get_school_info_for_export(),
|
||||||
|
open_item=request.args.get('open_item')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -2713,8 +2710,8 @@ def tutorial_page():
|
|||||||
'tutorial.html',
|
'tutorial.html',
|
||||||
username=session['username'],
|
username=session['username'],
|
||||||
is_admin=us.check_admin(session['username']),
|
is_admin=us.check_admin(session['username']),
|
||||||
library_module_enabled=cfg.LIBRARY_MODULE_ENABLED,
|
library_module_enabled=cfg.MODULES.is_enabled('library'),
|
||||||
student_cards_module_enabled=cfg.STUDENT_CARDS_MODULE_ENABLED,
|
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
|
||||||
student_default_borrow_days=cfg.STUDENT_DEFAULT_BORROW_DAYS,
|
student_default_borrow_days=cfg.STUDENT_DEFAULT_BORROW_DAYS,
|
||||||
student_max_borrow_days=cfg.STUDENT_MAX_BORROW_DAYS
|
student_max_borrow_days=cfg.STUDENT_MAX_BORROW_DAYS
|
||||||
)
|
)
|
||||||
@@ -2732,16 +2729,16 @@ def library_view():
|
|||||||
if 'username' not in session:
|
if 'username' not in session:
|
||||||
flash('Bitte mit registriertem Konto anmelden!', 'error')
|
flash('Bitte mit registriertem Konto anmelden!', 'error')
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
if not cfg.LIBRARY_MODULE_ENABLED:
|
if not cfg.MODULES.is_enabled('library'):
|
||||||
flash('Bibliotheks-Modul ist deaktiviert.', 'error')
|
flash('Bibliotheks-Modul ist deaktiviert.', 'error')
|
||||||
return redirect(url_for('home'))
|
return redirect(url_for('home'))
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'library_table.html',
|
'library_table.html',
|
||||||
username=session['username'],
|
username=session['username'],
|
||||||
library_module_enabled=cfg.LIBRARY_MODULE_ENABLED,
|
library_module_enabled=cfg.MODULES.is_enabled('library'),
|
||||||
is_admin=us.check_admin(session['username']),
|
is_admin=us.check_admin(session['username']),
|
||||||
student_cards_module_enabled=cfg.STUDENT_CARDS_MODULE_ENABLED,
|
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
|
||||||
student_default_borrow_days=cfg.STUDENT_DEFAULT_BORROW_DAYS,
|
student_default_borrow_days=cfg.STUDENT_DEFAULT_BORROW_DAYS,
|
||||||
student_max_borrow_days=cfg.STUDENT_MAX_BORROW_DAYS
|
student_max_borrow_days=cfg.STUDENT_MAX_BORROW_DAYS
|
||||||
)
|
)
|
||||||
@@ -2756,7 +2753,7 @@ def library_loans_admin():
|
|||||||
if not us.check_admin(session['username']):
|
if not us.check_admin(session['username']):
|
||||||
flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error')
|
flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error')
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
if not cfg.LIBRARY_MODULE_ENABLED:
|
if not cfg.MODULES.is_enabled('library'):
|
||||||
flash('Bibliotheks-Modul ist deaktiviert.', 'error')
|
flash('Bibliotheks-Modul ist deaktiviert.', 'error')
|
||||||
return redirect(url_for('home_admin'))
|
return redirect(url_for('home_admin'))
|
||||||
|
|
||||||
@@ -2857,8 +2854,8 @@ def library_loans_admin():
|
|||||||
'library_borrowings_admin.html',
|
'library_borrowings_admin.html',
|
||||||
loan_entries=loan_entries,
|
loan_entries=loan_entries,
|
||||||
damaged_items=damaged_items,
|
damaged_items=damaged_items,
|
||||||
library_module_enabled=cfg.LIBRARY_MODULE_ENABLED,
|
library_module_enabled=cfg.MODULES.is_enabled('library'),
|
||||||
student_cards_module_enabled=cfg.STUDENT_CARDS_MODULE_ENABLED,
|
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
app.logger.error(f"Error loading library loans admin view: {e}")
|
app.logger.error(f"Error loading library loans admin view: {e}")
|
||||||
@@ -2993,9 +2990,9 @@ def api_library_scan_action():
|
|||||||
"""
|
"""
|
||||||
if 'username' not in session:
|
if 'username' not in session:
|
||||||
return jsonify({'ok': False, 'message': 'Nicht angemeldet.'}), 401
|
return jsonify({'ok': False, 'message': 'Nicht angemeldet.'}), 401
|
||||||
if not cfg.LIBRARY_MODULE_ENABLED:
|
if not cfg.MODULES.is_enabled('library'):
|
||||||
return jsonify({'ok': False, 'message': 'Bibliotheks-Modul ist deaktiviert.'}), 403
|
return jsonify({'ok': False, 'message': 'Bibliotheks-Modul ist deaktiviert.'}), 403
|
||||||
if not cfg.STUDENT_CARDS_MODULE_ENABLED:
|
if not cfg.MODULES.is_enabled('student_cards'):
|
||||||
return jsonify({'ok': False, 'message': 'Schülerausweis-Modul ist deaktiviert.'}), 403
|
return jsonify({'ok': False, 'message': 'Schülerausweis-Modul ist deaktiviert.'}), 403
|
||||||
|
|
||||||
payload = request.get_json(silent=True) or {}
|
payload = request.get_json(silent=True) or {}
|
||||||
@@ -3199,7 +3196,7 @@ def api_library_item_update(item_id):
|
|||||||
return jsonify({'ok': False, 'message': 'Nicht angemeldet.'}), 401
|
return jsonify({'ok': False, 'message': 'Nicht angemeldet.'}), 401
|
||||||
if not us.check_admin(session['username']):
|
if not us.check_admin(session['username']):
|
||||||
return jsonify({'ok': False, 'message': 'Administratorrechte erforderlich.'}), 403
|
return jsonify({'ok': False, 'message': 'Administratorrechte erforderlich.'}), 403
|
||||||
if not cfg.LIBRARY_MODULE_ENABLED:
|
if not cfg.MODULES.is_enabled('library'):
|
||||||
return jsonify({'ok': False, 'message': 'Bibliotheks-Modul ist deaktiviert.'}), 403
|
return jsonify({'ok': False, 'message': 'Bibliotheks-Modul ist deaktiviert.'}), 403
|
||||||
|
|
||||||
payload = request.get_json(silent=True) or {}
|
payload = request.get_json(silent=True) or {}
|
||||||
@@ -3330,8 +3327,8 @@ def upload_admin():
|
|||||||
'upload_admin.html',
|
'upload_admin.html',
|
||||||
username=session['username'],
|
username=session['username'],
|
||||||
duplicate_data=duplicate_data,
|
duplicate_data=duplicate_data,
|
||||||
library_module_enabled=cfg.LIBRARY_MODULE_ENABLED,
|
library_module_enabled=cfg.MODULES.is_enabled('library'),
|
||||||
student_cards_module_enabled=cfg.STUDENT_CARDS_MODULE_ENABLED,
|
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
|
||||||
show_library_features=False,
|
show_library_features=False,
|
||||||
upload_mode='item',
|
upload_mode='item',
|
||||||
page_title='Artikel hochladen',
|
page_title='Artikel hochladen',
|
||||||
@@ -3352,7 +3349,7 @@ def library_admin():
|
|||||||
if not _action_access_allowed(permissions, 'can_insert'):
|
if not _action_access_allowed(permissions, 'can_insert'):
|
||||||
flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error')
|
flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error')
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
if not cfg.LIBRARY_MODULE_ENABLED:
|
if not cfg.MODULES.is_enabled('library'):
|
||||||
flash('Bibliotheks-Modul ist deaktiviert.', 'error')
|
flash('Bibliotheks-Modul ist deaktiviert.', 'error')
|
||||||
return redirect(url_for('home_admin'))
|
return redirect(url_for('home_admin'))
|
||||||
|
|
||||||
@@ -3396,8 +3393,8 @@ def library_admin():
|
|||||||
'upload_admin.html',
|
'upload_admin.html',
|
||||||
username=session['username'],
|
username=session['username'],
|
||||||
duplicate_data=duplicate_data,
|
duplicate_data=duplicate_data,
|
||||||
library_module_enabled=cfg.LIBRARY_MODULE_ENABLED,
|
library_module_enabled=cfg.MODULES.is_enabled('library'),
|
||||||
student_cards_module_enabled=cfg.STUDENT_CARDS_MODULE_ENABLED,
|
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
|
||||||
show_library_features=True,
|
show_library_features=True,
|
||||||
upload_mode='library',
|
upload_mode='library',
|
||||||
page_title='Bücher hochladen',
|
page_title='Bücher hochladen',
|
||||||
@@ -3417,7 +3414,7 @@ def student_cards_admin():
|
|||||||
if not us.check_admin(session['username']):
|
if not us.check_admin(session['username']):
|
||||||
flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error')
|
flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error')
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
if not cfg.STUDENT_CARDS_MODULE_ENABLED:
|
if not cfg.MODULES.is_enabled('student_cards'):
|
||||||
flash('Schülerausweis-Modul ist deaktiviert.', 'error')
|
flash('Schülerausweis-Modul ist deaktiviert.', 'error')
|
||||||
return redirect(url_for('home_admin'))
|
return redirect(url_for('home_admin'))
|
||||||
|
|
||||||
@@ -3543,8 +3540,8 @@ def student_cards_admin():
|
|||||||
edit_mode=edit_mode,
|
edit_mode=edit_mode,
|
||||||
form_data=form_data,
|
form_data=form_data,
|
||||||
config={'default': cfg.STUDENT_DEFAULT_BORROW_DAYS},
|
config={'default': cfg.STUDENT_DEFAULT_BORROW_DAYS},
|
||||||
library_module_enabled=cfg.LIBRARY_MODULE_ENABLED,
|
library_module_enabled=cfg.MODULES.is_enabled('library'),
|
||||||
student_cards_module_enabled=cfg.STUDENT_CARDS_MODULE_ENABLED
|
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -3560,7 +3557,7 @@ def student_cards_print():
|
|||||||
if not us.check_admin(session['username']):
|
if not us.check_admin(session['username']):
|
||||||
flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error')
|
flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error')
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
if not cfg.STUDENT_CARDS_MODULE_ENABLED:
|
if not cfg.MODULES.is_enabled('student_cards'):
|
||||||
flash('Schülerausweis-Modul ist deaktiviert.', 'error')
|
flash('Schülerausweis-Modul ist deaktiviert.', 'error')
|
||||||
return redirect(url_for('home_admin'))
|
return redirect(url_for('home_admin'))
|
||||||
|
|
||||||
@@ -3592,7 +3589,7 @@ def student_card_barcode_print():
|
|||||||
if not us.check_admin(session['username']):
|
if not us.check_admin(session['username']):
|
||||||
flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error')
|
flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error')
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
if not cfg.STUDENT_CARDS_MODULE_ENABLED:
|
if not cfg.MODULES.is_enabled('student_cards'):
|
||||||
flash('Schülerausweis-Modul ist deaktiviert.', 'error')
|
flash('Schülerausweis-Modul ist deaktiviert.', 'error')
|
||||||
return redirect(url_for('home_admin'))
|
return redirect(url_for('home_admin'))
|
||||||
|
|
||||||
@@ -3623,7 +3620,7 @@ def student_card_barcode_download():
|
|||||||
if not us.check_admin(session['username']):
|
if not us.check_admin(session['username']):
|
||||||
flash('Zugriff verweigert.', 'error')
|
flash('Zugriff verweigert.', 'error')
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
if not cfg.STUDENT_CARDS_MODULE_ENABLED:
|
if not cfg.MODULES.is_enabled('student_cards'):
|
||||||
flash('Schülerausweis-Modul ist deaktiviert.', 'error')
|
flash('Schülerausweis-Modul ist deaktiviert.', 'error')
|
||||||
return redirect(url_for('home_admin'))
|
return redirect(url_for('home_admin'))
|
||||||
|
|
||||||
@@ -3797,7 +3794,7 @@ def student_card_single_barcode_download(card_id):
|
|||||||
if not us.check_admin(session['username']):
|
if not us.check_admin(session['username']):
|
||||||
flash('Zugriff verweigert.', 'error')
|
flash('Zugriff verweigert.', 'error')
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
if not cfg.STUDENT_CARDS_MODULE_ENABLED:
|
if not cfg.MODULES.is_enabled('student_cards'):
|
||||||
flash('Schülerausweis-Modul ist deaktiviert.', 'error')
|
flash('Schülerausweis-Modul ist deaktiviert.', 'error')
|
||||||
return redirect(url_for('home_admin'))
|
return redirect(url_for('home_admin'))
|
||||||
|
|
||||||
@@ -4666,7 +4663,7 @@ def upload_item():
|
|||||||
|
|
||||||
item_isbn = ''
|
item_isbn = ''
|
||||||
item_type = 'general'
|
item_type = 'general'
|
||||||
if cfg.LIBRARY_MODULE_ENABLED:
|
if cfg.MODULES.is_enabled('library'):
|
||||||
item_isbn = normalize_and_validate_isbn(isbn_raw)
|
item_isbn = normalize_and_validate_isbn(isbn_raw)
|
||||||
if isbn_raw and not item_isbn:
|
if isbn_raw and not item_isbn:
|
||||||
error_msg = 'Ungültige ISBN. Bitte ISBN-10 oder ISBN-13 verwenden.'
|
error_msg = 'Ungültige ISBN. Bitte ISBN-10 oder ISBN-13 verwenden.'
|
||||||
@@ -4678,7 +4675,7 @@ def upload_item():
|
|||||||
item_type = 'book'
|
item_type = 'book'
|
||||||
|
|
||||||
if upload_mode == 'library':
|
if upload_mode == 'library':
|
||||||
if not cfg.LIBRARY_MODULE_ENABLED:
|
if not cfg.MODULES.is_enabled('library'):
|
||||||
error_msg = 'Bibliotheks-Modul ist deaktiviert.'
|
error_msg = 'Bibliotheks-Modul ist deaktiviert.'
|
||||||
if is_mobile:
|
if is_mobile:
|
||||||
return jsonify({'success': False, 'message': error_msg}), 400
|
return jsonify({'success': False, 'message': error_msg}), 400
|
||||||
@@ -4704,12 +4701,26 @@ def upload_item():
|
|||||||
|
|
||||||
# Check if base code is unique for single-item uploads
|
# Check if base code is unique for single-item uploads
|
||||||
if code_4 and item_count == 1 and not it.is_code_unique(code_4[0]):
|
if code_4 and item_count == 1 and not it.is_code_unique(code_4[0]):
|
||||||
error_msg = 'Der Code wird bereits verwendet. Bitte wählen Sie einen anderen Code.'
|
existing_item = it._get_items_collection().find_one({"code_4": code_4[0]})
|
||||||
if is_mobile:
|
if existing_item:
|
||||||
return jsonify({'success': False, 'message': error_msg}), 400
|
error_msg = 'Der Code wird bereits verwendet. Umleitung zum Eintrag.'
|
||||||
|
if is_mobile:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': error_msg,
|
||||||
|
'existing_item_id': str(existing_item['_id']),
|
||||||
|
'redirect_to_item': True
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
flash(error_msg, 'info')
|
||||||
|
return redirect(url_for(success_redirect_endpoint, open_item=str(existing_item['_id'])))
|
||||||
else:
|
else:
|
||||||
flash(error_msg, 'error')
|
error_msg = 'Der Code wird bereits verwendet. Bitte wählen Sie einen anderen Code.'
|
||||||
return redirect(url_for(success_redirect_endpoint))
|
if is_mobile:
|
||||||
|
return jsonify({'success': False, 'message': error_msg}), 400
|
||||||
|
else:
|
||||||
|
flash(error_msg, 'error')
|
||||||
|
return redirect(url_for(success_redirect_endpoint))
|
||||||
|
|
||||||
# Validate optional per-item codes
|
# Validate optional per-item codes
|
||||||
if individual_codes:
|
if individual_codes:
|
||||||
@@ -5853,7 +5864,7 @@ def edit_item(id):
|
|||||||
|
|
||||||
item_isbn = ''
|
item_isbn = ''
|
||||||
item_type = 'general'
|
item_type = 'general'
|
||||||
if cfg.LIBRARY_MODULE_ENABLED:
|
if cfg.MODULES.is_enabled('library'):
|
||||||
item_isbn = normalize_and_validate_isbn(isbn_raw)
|
item_isbn = normalize_and_validate_isbn(isbn_raw)
|
||||||
if isbn_raw and not item_isbn:
|
if isbn_raw and not item_isbn:
|
||||||
flash('Ungültige ISBN. Bitte ISBN-10 oder ISBN-13 verwenden.', 'error')
|
flash('Ungültige ISBN. Bitte ISBN-10 oder ISBN-13 verwenden.', 'error')
|
||||||
@@ -6136,7 +6147,7 @@ def ausleihen(id):
|
|||||||
|
|
||||||
username = session['username']
|
username = session['username']
|
||||||
requested_return_to = (request.form.get('return_to') or '').strip().lower()
|
requested_return_to = (request.form.get('return_to') or '').strip().lower()
|
||||||
if requested_return_to == 'library' and cfg.LIBRARY_MODULE_ENABLED:
|
if requested_return_to == 'library' and cfg.MODULES.is_enabled('library'):
|
||||||
redirect_target = 'library_view'
|
redirect_target = 'library_view'
|
||||||
else:
|
else:
|
||||||
redirect_target = 'home_admin' if us.check_admin(username) else 'home'
|
redirect_target = 'home_admin' if us.check_admin(username) else 'home'
|
||||||
@@ -6154,7 +6165,7 @@ def ausleihen(id):
|
|||||||
|
|
||||||
# Library media can only be borrowed with a valid student card.
|
# Library media can only be borrowed with a valid student card.
|
||||||
if is_library_item:
|
if is_library_item:
|
||||||
if not cfg.STUDENT_CARDS_MODULE_ENABLED:
|
if not cfg.MODULES.is_enabled('student_cards'):
|
||||||
flash('Bibliotheksmedien können nur mit aktivem Schülerausweis-Modul ausgeliehen werden.', 'error')
|
flash('Bibliotheksmedien können nur mit aktivem Schülerausweis-Modul ausgeliehen werden.', 'error')
|
||||||
return redirect(url_for(redirect_target))
|
return redirect(url_for(redirect_target))
|
||||||
if not student_card_id:
|
if not student_card_id:
|
||||||
@@ -6183,7 +6194,7 @@ def ausleihen(id):
|
|||||||
if client:
|
if client:
|
||||||
client.close()
|
client.close()
|
||||||
|
|
||||||
if cfg.STUDENT_CARDS_MODULE_ENABLED:
|
if cfg.MODULES.is_enabled('student_cards'):
|
||||||
duration_raw = (request.form.get('borrow_duration_days') or '').strip()
|
duration_raw = (request.form.get('borrow_duration_days') or '').strip()
|
||||||
if duration_raw:
|
if duration_raw:
|
||||||
try:
|
try:
|
||||||
@@ -7103,8 +7114,8 @@ def register():
|
|||||||
return redirect(url_for('home'))
|
return redirect(url_for('home'))
|
||||||
return render_template(
|
return render_template(
|
||||||
'register.html',
|
'register.html',
|
||||||
library_module_enabled=cfg.LIBRARY_MODULE_ENABLED,
|
library_module_enabled=cfg.MODULES.is_enabled('library'),
|
||||||
student_cards_module_enabled=cfg.STUDENT_CARDS_MODULE_ENABLED,
|
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
|
||||||
student_default_borrow_days=cfg.STUDENT_DEFAULT_BORROW_DAYS,
|
student_default_borrow_days=cfg.STUDENT_DEFAULT_BORROW_DAYS,
|
||||||
student_max_borrow_days=cfg.STUDENT_MAX_BORROW_DAYS
|
student_max_borrow_days=cfg.STUDENT_MAX_BORROW_DAYS
|
||||||
)
|
)
|
||||||
@@ -7170,8 +7181,8 @@ def user_del():
|
|||||||
return render_template(
|
return render_template(
|
||||||
'user_del.html',
|
'user_del.html',
|
||||||
users=users_list,
|
users=users_list,
|
||||||
library_module_enabled=cfg.LIBRARY_MODULE_ENABLED,
|
library_module_enabled=cfg.MODULES.is_enabled('library'),
|
||||||
student_cards_module_enabled=cfg.STUDENT_CARDS_MODULE_ENABLED
|
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -7311,8 +7322,8 @@ def admin_borrowings():
|
|||||||
return render_template(
|
return render_template(
|
||||||
'admin_borrowings.html',
|
'admin_borrowings.html',
|
||||||
entries=entries,
|
entries=entries,
|
||||||
library_module_enabled=cfg.LIBRARY_MODULE_ENABLED,
|
library_module_enabled=cfg.MODULES.is_enabled('library'),
|
||||||
student_cards_module_enabled=cfg.STUDENT_CARDS_MODULE_ENABLED
|
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards')
|
||||||
)
|
)
|
||||||
|
|
||||||
"""-----------------------------------------------------------Audit Routes-------------------------------------------------------"""
|
"""-----------------------------------------------------------Audit Routes-------------------------------------------------------"""
|
||||||
@@ -7373,8 +7384,8 @@ def admin_audit_dashboard():
|
|||||||
'admin_audit.html',
|
'admin_audit.html',
|
||||||
verify_result=verify_result,
|
verify_result=verify_result,
|
||||||
audit_rows=audit_rows,
|
audit_rows=audit_rows,
|
||||||
library_module_enabled=cfg.LIBRARY_MODULE_ENABLED,
|
library_module_enabled=cfg.MODULES.is_enabled('library'),
|
||||||
student_cards_module_enabled=cfg.STUDENT_CARDS_MODULE_ENABLED,
|
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
app.logger.error(f"Error loading audit dashboard: {exc}")
|
app.logger.error(f"Error loading audit dashboard: {exc}")
|
||||||
@@ -8120,7 +8131,7 @@ def library_item_invoices(item_id):
|
|||||||
if 'username' not in session or not us.check_admin(session['username']):
|
if 'username' not in session or not us.check_admin(session['username']):
|
||||||
flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error')
|
flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error')
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
if not cfg.LIBRARY_MODULE_ENABLED:
|
if not cfg.MODULES.is_enabled('library'):
|
||||||
flash('Bibliotheks-Modul ist deaktiviert.', 'error')
|
flash('Bibliotheks-Modul ist deaktiviert.', 'error')
|
||||||
return redirect(url_for('home_admin'))
|
return redirect(url_for('home_admin'))
|
||||||
|
|
||||||
@@ -8188,8 +8199,8 @@ def library_item_invoices(item_id):
|
|||||||
'isbn': item_doc.get('ISBN', ''),
|
'isbn': item_doc.get('ISBN', ''),
|
||||||
},
|
},
|
||||||
invoices=entries,
|
invoices=entries,
|
||||||
library_module_enabled=cfg.LIBRARY_MODULE_ENABLED,
|
library_module_enabled=cfg.MODULES.is_enabled('library'),
|
||||||
student_cards_module_enabled=cfg.STUDENT_CARDS_MODULE_ENABLED,
|
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
app.logger.error(f"Error loading invoice history for item {item_id}: {e}")
|
app.logger.error(f"Error loading invoice history for item {item_id}: {e}")
|
||||||
@@ -8515,8 +8526,8 @@ def logs():
|
|||||||
return render_template(
|
return render_template(
|
||||||
'logs.html',
|
'logs.html',
|
||||||
items=formatted_items,
|
items=formatted_items,
|
||||||
library_module_enabled=cfg.LIBRARY_MODULE_ENABLED,
|
library_module_enabled=cfg.MODULES.is_enabled('library'),
|
||||||
student_cards_module_enabled=cfg.STUDENT_CARDS_MODULE_ENABLED
|
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -8576,13 +8587,19 @@ def manage_filters():
|
|||||||
# Get predefined filter values
|
# Get predefined filter values
|
||||||
filter1_values = it.get_predefined_filter_values(1)
|
filter1_values = it.get_predefined_filter_values(1)
|
||||||
filter2_values = it.get_predefined_filter_values(2)
|
filter2_values = it.get_predefined_filter_values(2)
|
||||||
|
filter3_values = it.get_predefined_filter_values(3)
|
||||||
|
|
||||||
|
# Get custom filter names
|
||||||
|
filter_names = it.get_filter_names()
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'manage_filters.html',
|
'manage_filters.html',
|
||||||
filter1_values=filter1_values,
|
filter1_values=filter1_values,
|
||||||
filter2_values=filter2_values,
|
filter2_values=filter2_values,
|
||||||
library_module_enabled=cfg.LIBRARY_MODULE_ENABLED,
|
filter3_values=filter3_values,
|
||||||
student_cards_module_enabled=cfg.STUDENT_CARDS_MODULE_ENABLED
|
filter_names=filter_names,
|
||||||
|
library_module_enabled=cfg.MODULES.is_enabled('library'),
|
||||||
|
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards')
|
||||||
)
|
)
|
||||||
|
|
||||||
@app.route('/add_filter_value/<int:filter_num>', methods=['POST'])
|
@app.route('/add_filter_value/<int:filter_num>', methods=['POST'])
|
||||||
@@ -8644,6 +8661,41 @@ def remove_filter_value(filter_num, value):
|
|||||||
|
|
||||||
return redirect(url_for('manage_filters'))
|
return redirect(url_for('manage_filters'))
|
||||||
|
|
||||||
|
@app.route('/edit_filter_value/<int:filter_num>/<string:old_value>', methods=['POST'])
|
||||||
|
def edit_filter_value(filter_num, old_value):
|
||||||
|
"""
|
||||||
|
Edit a predefined value from the specified filter.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filter_num (int): Filter number (1, 2 or 3)
|
||||||
|
old_value (str): Value to edit
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
flask.Response: Redirect to filter management page
|
||||||
|
"""
|
||||||
|
if 'username' not in session or not us.check_admin(session['username']):
|
||||||
|
return jsonify({'success': False, 'error': 'Not authorized'}), 403
|
||||||
|
|
||||||
|
new_value = sanitize_form_value(request.form.get('new_value'))
|
||||||
|
|
||||||
|
if not new_value:
|
||||||
|
flash('Bitte geben Sie einen neuen Wert ein', 'error')
|
||||||
|
return redirect(url_for('manage_filters'))
|
||||||
|
|
||||||
|
if new_value == FILTER_SELECT_ALL_TOKEN:
|
||||||
|
flash('Dieser Wert ist reserviert und kann nicht als Filterwert gespeichert werden.', 'error')
|
||||||
|
return redirect(url_for('manage_filters'))
|
||||||
|
|
||||||
|
# Update the value from the filter
|
||||||
|
success = it.edit_predefined_filter_value(filter_num, old_value, new_value)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
flash(f'Wert "{old_value}" wurde in "{new_value}" bei Filter {filter_num} geändert', 'success')
|
||||||
|
else:
|
||||||
|
flash(f'Fehler beim Ändern des Wertes oder Wert existiert bereits in Filter {filter_num}', 'error')
|
||||||
|
|
||||||
|
return redirect(url_for('manage_filters'))
|
||||||
|
|
||||||
@app.route('/get_predefined_filter_values/<int:filter_num>')
|
@app.route('/get_predefined_filter_values/<int:filter_num>')
|
||||||
def get_predefined_filter_values(filter_num):
|
def get_predefined_filter_values(filter_num):
|
||||||
"""
|
"""
|
||||||
@@ -8713,7 +8765,7 @@ def fetch_book_info(isbn):
|
|||||||
if 'username' not in session or not us.check_admin(session['username']):
|
if 'username' not in session or not us.check_admin(session['username']):
|
||||||
return jsonify({"error": "Not authorized"}), 403
|
return jsonify({"error": "Not authorized"}), 403
|
||||||
|
|
||||||
if not cfg.LIBRARY_MODULE_ENABLED:
|
if not cfg.MODULES.is_enabled('library'):
|
||||||
return jsonify({"error": "Bibliotheks-Modul ist deaktiviert."}), 403
|
return jsonify({"error": "Bibliotheks-Modul ist deaktiviert."}), 403
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -8808,7 +8860,7 @@ def download_book_cover():
|
|||||||
return jsonify({"error": "Not authorized"}), 403
|
return jsonify({"error": "Not authorized"}), 403
|
||||||
if not us.check_admin(session['username']):
|
if not us.check_admin(session['username']):
|
||||||
return jsonify({"error": "Admin privileges required"}), 403
|
return jsonify({"error": "Admin privileges required"}), 403
|
||||||
if not cfg.LIBRARY_MODULE_ENABLED:
|
if not cfg.MODULES.is_enabled('library'):
|
||||||
return jsonify({"error": "Bibliotheks-Modul ist deaktiviert."}), 403
|
return jsonify({"error": "Bibliotheks-Modul ist deaktiviert."}), 403
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -9175,8 +9227,8 @@ def notifications_view():
|
|||||||
user_notifications=user_notifications,
|
user_notifications=user_notifications,
|
||||||
admin_notifications=admin_notifications,
|
admin_notifications=admin_notifications,
|
||||||
is_admin_user=is_admin_user,
|
is_admin_user=is_admin_user,
|
||||||
library_module_enabled=cfg.LIBRARY_MODULE_ENABLED,
|
library_module_enabled=cfg.MODULES.is_enabled('library'),
|
||||||
student_cards_module_enabled=cfg.STUDENT_CARDS_MODULE_ENABLED,
|
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
app.logger.error(f"Error loading notifications: {exc}")
|
app.logger.error(f"Error loading notifications: {exc}")
|
||||||
@@ -9407,8 +9459,8 @@ def admin_damaged_items():
|
|||||||
return render_template(
|
return render_template(
|
||||||
'admin_damaged_items.html',
|
'admin_damaged_items.html',
|
||||||
damaged_items=damaged_rows,
|
damaged_items=damaged_rows,
|
||||||
library_module_enabled=cfg.LIBRARY_MODULE_ENABLED,
|
library_module_enabled=cfg.MODULES.is_enabled('library'),
|
||||||
student_cards_module_enabled=cfg.STUDENT_CARDS_MODULE_ENABLED,
|
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
app.logger.error(f"Error loading damaged-items admin view: {exc}")
|
app.logger.error(f"Error loading damaged-items admin view: {exc}")
|
||||||
@@ -9511,8 +9563,8 @@ def manage_locations():
|
|||||||
return render_template(
|
return render_template(
|
||||||
'manage_locations.html',
|
'manage_locations.html',
|
||||||
location_values=location_values,
|
location_values=location_values,
|
||||||
library_module_enabled=cfg.LIBRARY_MODULE_ENABLED,
|
library_module_enabled=cfg.MODULES.is_enabled('library'),
|
||||||
student_cards_module_enabled=cfg.STUDENT_CARDS_MODULE_ENABLED
|
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -9610,18 +9662,31 @@ def admin_school_settings():
|
|||||||
try:
|
try:
|
||||||
updated_school = cfg.update_school_info(school_info)
|
updated_school = cfg.update_school_info(school_info)
|
||||||
current_school = updated_school
|
current_school = updated_school
|
||||||
|
|
||||||
|
# Also process filter names
|
||||||
|
filter_name_1 = request.form.get('filter_name_1')
|
||||||
|
if filter_name_1: it.set_filter_name(1, filter_name_1.strip())
|
||||||
|
filter_name_2 = request.form.get('filter_name_2')
|
||||||
|
if filter_name_2: it.set_filter_name(2, filter_name_2.strip())
|
||||||
|
filter_name_3 = request.form.get('filter_name_3')
|
||||||
|
if filter_name_3: it.set_filter_name(3, filter_name_3.strip())
|
||||||
|
|
||||||
flash('Schulstammdaten wurden erfolgreich gespeichert.', 'success')
|
flash('Schulstammdaten wurden erfolgreich gespeichert.', 'success')
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
app.logger.error(f'Could not update school settings: {exc}\n{traceback.format_exc()}')
|
app.logger.error(f'Could not update school settings: {exc}\n{traceback.format_exc()}')
|
||||||
flash('Die Schulstammdaten konnten nicht gespeichert werden.', 'error')
|
flash('Die Schulstammdaten konnten nicht gespeichert werden.', 'error')
|
||||||
|
|
||||||
|
# Get current filter names to display in form
|
||||||
|
filter_names = it.get_filter_names()
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'admin_school_settings.html',
|
'admin_school_settings.html',
|
||||||
school_info=current_school,
|
school_info=current_school,
|
||||||
|
filter_names=filter_names,
|
||||||
tenant_id=tenant_id,
|
tenant_id=tenant_id,
|
||||||
tenant_db=tenant_db,
|
tenant_db=tenant_db,
|
||||||
library_module_enabled=cfg.LIBRARY_MODULE_ENABLED,
|
library_module_enabled=cfg.MODULES.is_enabled('library'),
|
||||||
student_cards_module_enabled=cfg.STUDENT_CARDS_MODULE_ENABLED,
|
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
|
||||||
)
|
)
|
||||||
|
|
||||||
@app.route('/check_code_unique/<code>')
|
@app.route('/check_code_unique/<code>')
|
||||||
|
|||||||
+86
-3
@@ -570,7 +570,10 @@ def get_primary_filters():
|
|||||||
items = db['items']
|
items = db['items']
|
||||||
filters = [f for f in items.distinct('Filter', _active_record_query(_non_library_query())) if f]
|
filters = [f for f in items.distinct('Filter', _active_record_query(_non_library_query())) if f]
|
||||||
client.close()
|
client.close()
|
||||||
return filters
|
|
||||||
|
# Add predefined values
|
||||||
|
predefined = get_predefined_filter_values(1)
|
||||||
|
return sorted(list(set(filters + predefined)))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error retrieving primary filters: {e}")
|
print(f"Error retrieving primary filters: {e}")
|
||||||
return []
|
return []
|
||||||
@@ -589,7 +592,10 @@ def get_secondary_filters():
|
|||||||
items = db['items']
|
items = db['items']
|
||||||
filters = [f for f in items.distinct('Filter2', _active_record_query(_non_library_query())) if f]
|
filters = [f for f in items.distinct('Filter2', _active_record_query(_non_library_query())) if f]
|
||||||
client.close()
|
client.close()
|
||||||
return filters
|
|
||||||
|
# Add predefined values
|
||||||
|
predefined = get_predefined_filter_values(2)
|
||||||
|
return sorted(list(set(filters + predefined)))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error retrieving secondary filters: {e}")
|
print(f"Error retrieving secondary filters: {e}")
|
||||||
return []
|
return []
|
||||||
@@ -608,7 +614,10 @@ def get_tertiary_filters():
|
|||||||
items = db['items']
|
items = db['items']
|
||||||
filters = [f for f in items.distinct('Filter3', _active_record_query(_non_library_query())) if f]
|
filters = [f for f in items.distinct('Filter3', _active_record_query(_non_library_query())) if f]
|
||||||
client.close()
|
client.close()
|
||||||
return filters
|
|
||||||
|
# Add predefined values
|
||||||
|
predefined = get_predefined_filter_values(3)
|
||||||
|
return sorted(list(set(filters + predefined)))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error retrieving tertiary filters: {e}")
|
print(f"Error retrieving tertiary filters: {e}")
|
||||||
return []
|
return []
|
||||||
@@ -785,6 +794,80 @@ def remove_predefined_filter_value(filter_num, value):
|
|||||||
return result.modified_count > 0
|
return result.modified_count > 0
|
||||||
|
|
||||||
|
|
||||||
|
def edit_predefined_filter_value(filter_num, old_value, new_value):
|
||||||
|
"""
|
||||||
|
Edit a predefined value from a filter and update all matching items.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filter_num (int): Filter number (1 for Unterrichtsfach, 2 for Jahrgangsstufe)
|
||||||
|
old_value (str): Value to replace
|
||||||
|
new_value (str): New value
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if value was updated, False otherwise
|
||||||
|
"""
|
||||||
|
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||||
|
db = client[cfg.MONGODB_DB]
|
||||||
|
filter_presets = db['filter_presets']
|
||||||
|
|
||||||
|
# Check if the new value already exists
|
||||||
|
existing = filter_presets.find_one({
|
||||||
|
'filter_num': filter_num,
|
||||||
|
'values': new_value
|
||||||
|
})
|
||||||
|
|
||||||
|
if existing and old_value != new_value:
|
||||||
|
client.close()
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Update the value in the filter
|
||||||
|
result = filter_presets.update_one(
|
||||||
|
{'filter_num': filter_num, 'values': old_value},
|
||||||
|
{'$set': {'values.$': new_value}}
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.modified_count > 0:
|
||||||
|
items = db['items']
|
||||||
|
filter_field = 'Filter' if filter_num == 1 else f'Filter{filter_num}'
|
||||||
|
|
||||||
|
# Also update all items that use this filter
|
||||||
|
items.update_many(
|
||||||
|
{filter_field: old_value},
|
||||||
|
{'$set': {filter_field: new_value}}
|
||||||
|
)
|
||||||
|
|
||||||
|
client.close()
|
||||||
|
return result.modified_count > 0
|
||||||
|
|
||||||
|
def get_filter_names():
|
||||||
|
"""Get customized filter category names."""
|
||||||
|
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||||
|
db = client[cfg.MONGODB_DB]
|
||||||
|
names_doc = db.settings.find_one({'setting_type': 'filter_names'})
|
||||||
|
client.close()
|
||||||
|
if names_doc and 'names' in names_doc:
|
||||||
|
return names_doc['names']
|
||||||
|
return {
|
||||||
|
'1': 'Fach/Kategorie',
|
||||||
|
'2': 'System/Bereich',
|
||||||
|
'3': 'Typ/Art'
|
||||||
|
}
|
||||||
|
|
||||||
|
def set_filter_name(filter_num, name):
|
||||||
|
"""Set custom name for a filter category."""
|
||||||
|
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||||
|
db = client[cfg.MONGODB_DB]
|
||||||
|
names = get_filter_names()
|
||||||
|
names[str(filter_num)] = name
|
||||||
|
db.settings.update_one(
|
||||||
|
{'setting_type': 'filter_names'},
|
||||||
|
{'$set': {'names': names}},
|
||||||
|
upsert=True
|
||||||
|
)
|
||||||
|
client.close()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
# === LOCATION MANAGEMENT ===
|
# === LOCATION MANAGEMENT ===
|
||||||
|
|
||||||
def get_predefined_locations():
|
def get_predefined_locations():
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
class ModuleRegistry:
|
||||||
|
def __init__(self):
|
||||||
|
self._modules: Dict[str, Any] = {}
|
||||||
|
self._path_matchers = {}
|
||||||
|
|
||||||
|
def register(self, name: str, settings_bool, path_matcher=lambda path: False):
|
||||||
|
self._modules[name] = settings_bool
|
||||||
|
self._path_matchers[name] = path_matcher
|
||||||
|
|
||||||
|
def is_enabled(self, name: str) -> bool:
|
||||||
|
if name not in self._modules:
|
||||||
|
return False
|
||||||
|
return bool(self._modules[name])
|
||||||
|
|
||||||
|
def get_all_status(self) -> Dict[str, bool]:
|
||||||
|
return {name: bool(sys_bool) for name, sys_bool in self._modules.items()}
|
||||||
|
|
||||||
|
def get_module_for_path(self, path: str) -> str:
|
||||||
|
for name, matcher in self._path_matchers.items():
|
||||||
|
if self.is_enabled(name) and matcher(path):
|
||||||
|
return name
|
||||||
|
return None
|
||||||
|
|
||||||
|
registry = ModuleRegistry()
|
||||||
+22
-1
@@ -205,11 +205,32 @@ class _TenantAwareBool:
|
|||||||
return f"_TenantAwareBool(module_name={self.module_name!r}, value={self.resolve()!r})"
|
return f"_TenantAwareBool(module_name={self.module_name!r}, value={self.resolve()!r})"
|
||||||
|
|
||||||
|
|
||||||
|
from module_registry import registry as MODULES
|
||||||
|
|
||||||
INVENTORY_MODULE_ENABLED = _TenantAwareBool('inventory', _get(_conf, ['modules', 'inventory', 'enabled'], DEFAULTS['modules']['inventory']['enabled']))
|
INVENTORY_MODULE_ENABLED = _TenantAwareBool('inventory', _get(_conf, ['modules', 'inventory', 'enabled'], DEFAULTS['modules']['inventory']['enabled']))
|
||||||
LIBRARY_MODULE_ENABLED = _TenantAwareBool('library', _get(_conf, ['modules', 'library', 'enabled'], DEFAULTS['modules']['library']['enabled']))
|
LIBRARY_MODULE_ENABLED = _TenantAwareBool('library', _get(_conf, ['modules', 'library', 'enabled'], DEFAULTS['modules']['library']['enabled']))
|
||||||
STUDENT_CARDS_MODULE_ENABLED = _TenantAwareBool('student_cards', _get(_conf, ['modules', 'student_cards', 'enabled'], DEFAULTS['modules']['student_cards']['enabled']))
|
STUDENT_CARDS_MODULE_ENABLED = _TenantAwareBool('student_cards', _get(_conf, ['modules', 'student_cards', 'enabled'], DEFAULTS['modules']['student_cards']['enabled']))
|
||||||
|
|
||||||
|
def _match_inventory(path):
|
||||||
|
if not path: return False
|
||||||
|
if path == '/' or path.startswith('/home'): return True
|
||||||
|
return path.startswith(('/scanner', '/inventory', '/upload_admin', '/manage_filters', '/manage_locations', '/admin_borrowings', '/admin_damaged_items', '/admin/borrowings', '/admin/damaged_items', '/terminplan'))
|
||||||
|
|
||||||
|
def _match_library(path):
|
||||||
|
if not path: return False
|
||||||
|
return path.startswith(('/library', '/library_', '/student_cards'))
|
||||||
|
|
||||||
|
def _match_student_cards(path):
|
||||||
|
if not path: return False
|
||||||
|
return path.startswith(('/student_cards'))
|
||||||
|
|
||||||
|
# Register core modules into the pipeline
|
||||||
|
MODULES.register('inventory', INVENTORY_MODULE_ENABLED, _match_inventory)
|
||||||
|
MODULES.register('library', LIBRARY_MODULE_ENABLED, _match_library)
|
||||||
|
MODULES.register('student_cards', STUDENT_CARDS_MODULE_ENABLED, _match_student_cards)
|
||||||
|
|
||||||
STUDENT_DEFAULT_BORROW_DAYS = int(_get(_conf, ['modules', 'student_cards', 'default_borrow_days'], DEFAULTS['modules']['student_cards']['default_borrow_days']))
|
STUDENT_DEFAULT_BORROW_DAYS = int(_get(_conf, ['modules', 'student_cards', 'default_borrow_days'], DEFAULTS['modules']['student_cards']['default_borrow_days']))
|
||||||
STUDENT_MAX_BORROW_DAYS = int(_get(_conf, ['modules', 'student_cards', 'max_borrow_days'], DEFAULTS['modules']['student_cards']['max_borrow_days']))
|
STUDENT_MAX_BORROW_DAYS = int(_get(_conf, ["modules", "student_cards", "max_borrow_days"], DEFAULTS["modules"]["student_cards"]["max_borrow_days"]))
|
||||||
|
|
||||||
# Upload/paths
|
# Upload/paths
|
||||||
ALLOWED_EXTENSIONS = set(_get(_conf, ['allowed_extensions'], DEFAULTS['upload']['allowed_extensions']))
|
ALLOWED_EXTENSIONS = set(_get(_conf, ['allowed_extensions'], DEFAULTS['upload']['allowed_extensions']))
|
||||||
|
|||||||
@@ -10,13 +10,13 @@
|
|||||||
.planned-appointments-panel {
|
.planned-appointments-panel {
|
||||||
margin: 15px 0;
|
margin: 15px 0;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
background-color: #e1f5fe;
|
background-color: var(--ui-surface-soft, #e1f5fe);
|
||||||
border-left: 4px solid #03a9f4;
|
border-left: 4px solid #03a9f4;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.planned-appointments-panel h4 {
|
.planned-appointments-panel h4 {
|
||||||
color: #0277bd;
|
color: var(--ui-title, #0277bd);
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
@@ -30,9 +30,9 @@
|
|||||||
|
|
||||||
.appointment-item {
|
.appointment-item {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background-color: #f9fdff;
|
background-color: var(--ui-surface, #f9fdff);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid #b3e5fc;
|
border: 1px solid var(--ui-border, #b3e5fc);
|
||||||
}
|
}
|
||||||
|
|
||||||
.appointment-header {
|
.appointment-header {
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
|
|
||||||
.appointment-date {
|
.appointment-date {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #0288d1;
|
color: var(--ui-title, #0288d1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.appointment-period {
|
.appointment-period {
|
||||||
@@ -86,14 +86,14 @@
|
|||||||
.appointment-user {
|
.appointment-user {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
color: #546e7a;
|
color: var(--ui-text-muted, #546e7a);
|
||||||
}
|
}
|
||||||
|
|
||||||
.appointment-notes {
|
.appointment-notes {
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
color: #455a64;
|
color: var(--ui-text, #455a64);
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
padding-top: 5px;
|
padding-top: 5px;
|
||||||
border-top: 1px solid #e1f5fe;
|
border-top: 1px solid var(--ui-border, #e1f5fe);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|||||||
+112
-10
@@ -22,16 +22,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root[data-theme="dark"] {
|
:root[data-theme="dark"] {
|
||||||
--ui-bg: #0f172a;
|
/* Tiefes Blau-Theme (Dark Blue) */
|
||||||
--ui-bg-accent: #1e293b;
|
--ui-bg: #0b1120;
|
||||||
|
--ui-bg-accent: #111827;
|
||||||
--ui-surface: #1e293b;
|
--ui-surface: #1e293b;
|
||||||
--ui-surface-soft: #25314a;
|
--ui-surface-soft: #334155;
|
||||||
--ui-border: #334155;
|
--ui-border: #475569;
|
||||||
--ui-text: #f1f5f9;
|
--ui-text: #f8fafc;
|
||||||
--ui-text-muted: #94a3b8;
|
--ui-text-muted: #94a3b8;
|
||||||
--ui-title: #ffffff;
|
--ui-title: #ffffff;
|
||||||
--ui-shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.4);
|
--ui-shadow-sm: 0 4px 12px rgba(0, 0, 0, 0.4), inset 0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||||
--ui-shadow-md: 0 10px 24px rgba(0, 0, 0, 0.5);
|
--ui-shadow-md: 0 10px 30px rgba(0, 0, 0, 0.6), inset 0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||||
|
|
||||||
|
/* Abgerundetes Format im gesamten Dark Mode */
|
||||||
|
--ui-radius: 20px;
|
||||||
|
--ui-btn-radius: 99px; /* Pillenform für Buttons */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Safe Area Support for iPad notches, Dynamic Island, and rounded corners */
|
/* Safe Area Support for iPad notches, Dynamic Island, and rounded corners */
|
||||||
@@ -57,9 +62,17 @@ body {
|
|||||||
:root[data-theme="dark"] input,
|
:root[data-theme="dark"] input,
|
||||||
:root[data-theme="dark"] select,
|
:root[data-theme="dark"] select,
|
||||||
:root[data-theme="dark"] textarea {
|
:root[data-theme="dark"] textarea {
|
||||||
background-color: var(--ui-surface-soft) !important;
|
background-color: var(--ui-bg) !important;
|
||||||
color: var(--ui-text) !important;
|
color: var(--ui-text) !important;
|
||||||
border-color: var(--ui-border) !important;
|
border: 1px solid var(--ui-border) !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
box-shadow: inset 0 1px 2px rgba(0,0,0,0.3) !important;
|
||||||
|
}
|
||||||
|
:root[data-theme="dark"] input:focus,
|
||||||
|
:root[data-theme="dark"] select:focus,
|
||||||
|
:root[data-theme="dark"] textarea:focus {
|
||||||
|
border-color: #52a8ff !important;
|
||||||
|
box-shadow: 0 0 0 2px rgba(82, 168, 255, 0.2), inset 0 1px 2px rgba(0,0,0,0.3) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@@ -295,7 +308,7 @@ select:focus {
|
|||||||
|
|
||||||
/* Modal dialog styling */
|
/* Modal dialog styling */
|
||||||
.modal-dialog-white {
|
.modal-dialog-white {
|
||||||
background: white;
|
background: var(--ui-surface);
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -896,3 +909,92 @@ html, body {
|
|||||||
padding-bottom: env(safe-area-inset-bottom);
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ========================================================= */
|
||||||
|
/* UNIVERSAL DARK MODE OVERRIDES FOR HARDCODED HTML STYLES */
|
||||||
|
/* ========================================================= */
|
||||||
|
:root[data-theme="dark"] .bg-white,
|
||||||
|
:root[data-theme="dark"] .bg-light,
|
||||||
|
:root[data-theme="dark"] [style*="background-color: #fff"],
|
||||||
|
:root[data-theme="dark"] [style*="background-color: #ffffff"],
|
||||||
|
:root[data-theme="dark"] [style*="background-color: #f8f9fa"],
|
||||||
|
:root[data-theme="dark"] [style*="background-color: #f5f5f5"],
|
||||||
|
:root[data-theme="dark"] [style*="background: #fff"],
|
||||||
|
:root[data-theme="dark"] [style*="background: #ffffff"],
|
||||||
|
:root[data-theme="dark"] [style*="background: #f8f9fa"],
|
||||||
|
:root[data-theme="dark"] [style*="background: #f5f5f5"],
|
||||||
|
:root[data-theme="dark"] [style*="background: white"],
|
||||||
|
:root[data-theme="dark"] [style*="background-color: white"],
|
||||||
|
:root[data-theme="dark"] .modal-content,
|
||||||
|
:root[data-theme="dark"] .card,
|
||||||
|
:root[data-theme="dark"] .dropdown-menu {
|
||||||
|
background-color: var(--ui-surface) !important;
|
||||||
|
background: var(--ui-surface) !important;
|
||||||
|
color: var(--ui-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="dark"] .text-dark,
|
||||||
|
:root[data-theme="dark"] .text-black,
|
||||||
|
:root[data-theme="dark"] [style*="color: #000"],
|
||||||
|
:root[data-theme="dark"] [style*="color: rgb(0, 0, 0)"],
|
||||||
|
:root[data-theme="dark"] [style*="color: black"],
|
||||||
|
:root[data-theme="dark"] [style*="color: #333"] {
|
||||||
|
color: var(--ui-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="dark"] .text-muted {
|
||||||
|
color: var(--ui-text-muted) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix table backgrounds that might have hardcoded light colors */
|
||||||
|
:root[data-theme="dark"] .table-light,
|
||||||
|
:root[data-theme="dark"] table.table,
|
||||||
|
:root[data-theme="dark"] table [style*="background: #fff"] {
|
||||||
|
background-color: var(--ui-bg) !important;
|
||||||
|
color: var(--ui-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix list items */
|
||||||
|
:root[data-theme="dark"] .list-group-item {
|
||||||
|
background-color: var(--ui-surface) !important;
|
||||||
|
color: var(--ui-text) !important;
|
||||||
|
border-color: var(--ui-border) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Additional inline style coverage */
|
||||||
|
:root[data-theme="dark"] [style*="background: #eef4ff"],
|
||||||
|
:root[data-theme="dark"] [style*="background-color: #eef4ff"],
|
||||||
|
:root[data-theme="dark"] [style*="background: #e9ecef"],
|
||||||
|
:root[data-theme="dark"] [style*="background-color: #e9ecef"],
|
||||||
|
:root[data-theme="dark"] [style*="background: #e2e8f0"],
|
||||||
|
:root[data-theme="dark"] [style*="background-color: #e2e8f0"],
|
||||||
|
:root[data-theme="dark"] [style*="background-color: #fefefe"],
|
||||||
|
:root[data-theme="dark"] [style*="background: #fffdfd"] {
|
||||||
|
background-color: var(--ui-surface) !important;
|
||||||
|
background: var(--ui-surface) !important;
|
||||||
|
color: var(--ui-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="dark"] .badge-light,
|
||||||
|
:root[data-theme="dark"] .bg-light {
|
||||||
|
background-color: var(--ui-surface-soft) !important;
|
||||||
|
color: var(--ui-text) !important;
|
||||||
|
border: 1px solid var(--ui-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix Bookmark/Favorite Button in Dark Mode */
|
||||||
|
:root[data-theme="dark"] .bookmark-btn {
|
||||||
|
background: var(--ui-surface-soft) !important;
|
||||||
|
border-color: #f59e0b !important;
|
||||||
|
color: #f59e0b !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="dark"] .bookmark-btn:hover,
|
||||||
|
:root[data-theme="dark"] .bookmark-btn.active {
|
||||||
|
background: #f59e0b !important;
|
||||||
|
color: var(--ui-bg) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="dark"] .favorite-item {
|
||||||
|
outline: 2px solid #f59e0b !important;
|
||||||
|
}
|
||||||
|
|||||||
@@ -71,6 +71,25 @@
|
|||||||
<small>Upload ersetzt das bisherige Logo. Wenn kein neues Logo gewählt wird, bleibt das aktuelle erhalten.</small>
|
<small>Upload ersetzt das bisherige Logo. Wenn kein neues Logo gewählt wird, bleibt das aktuelle erhalten.</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<hr style="margin: 24px 0; border: 0; border-top: 1px solid #e5e7eb;">
|
||||||
|
<h3 style="font-size: 1.1rem; margin-bottom: 16px; color: #111827;">Globales Filter-Wording</h3>
|
||||||
|
<p style="font-size: 0.9rem; color: #6b7280; margin-bottom: 16px;">Hier kannst du die globalen Bezeichnungen für die drei Haupt-Kategorien (Filter) der Anwendung ändern.</p>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="filter_name_1">Name Filter 1</label>
|
||||||
|
<input type="text" id="filter_name_1" name="filter_name_1" value="{{ filter_names.get('1', 'Fach/Kategorie') }}" placeholder="z. B. Fach/Kategorie">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="filter_name_2">Name Filter 2</label>
|
||||||
|
<input type="text" id="filter_name_2" name="filter_name_2" value="{{ filter_names.get('2', 'System/Bereich') }}" placeholder="z. B. System/Bereich">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="filter_name_3">Name Filter 3</label>
|
||||||
|
<input type="text" id="filter_name_3" name="filter_name_3" value="{{ filter_names.get('3', 'Typ/Art') }}" placeholder="z. B. Typ/Art">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="actions-row">
|
<div class="actions-row">
|
||||||
<button type="submit" class="btn btn-primary">Schulstammdaten speichern</button>
|
<button type="submit" class="btn btn-primary">Schulstammdaten speichern</button>
|
||||||
<a href="{{ url_for('home_admin') }}" class="btn btn-secondary">Zurück zur Admin-Übersicht</a>
|
<a href="{{ url_for('home_admin') }}" class="btn btn-secondary">Zurück zur Admin-Übersicht</a>
|
||||||
|
|||||||
@@ -329,7 +329,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* iPad/Tablet responsive adjustments */
|
/* iPad/Tablet responsive adjustments */
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 768px) {
|
||||||
.navbar .container-fluid {
|
.navbar .container-fluid {
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding-left: max(env(safe-area-inset-left, 0), 10px);
|
padding-left: max(env(safe-area-inset-left, 0), 10px);
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
{% if error_code == 404 %}
|
||||||
|
Seite nicht gefunden - Inventarsystem
|
||||||
|
{% else %}
|
||||||
|
Fehler - Inventarsystem
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="error-container" style="text-align: center; padding: 60px 20px; min-height: 70vh; display: flex; flex-direction: column; justify-content: center; align-items: center;">
|
||||||
|
<div style="max-width: 600px;">
|
||||||
|
<!-- Error Code Display -->
|
||||||
|
<div style="font-size: 120px; font-weight: bold; color: #ef4444; margin-bottom: 20px;">
|
||||||
|
{{ error_code }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error Title -->
|
||||||
|
<h1 style="font-size: 32px; margin: 20px 0; color: #1f2937;">
|
||||||
|
{% if error_code == 404 %}
|
||||||
|
Seite nicht gefunden
|
||||||
|
{% else %}
|
||||||
|
Ein Fehler ist aufgetreten
|
||||||
|
{% endif %}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<!-- Error Message -->
|
||||||
|
<p style="font-size: 18px; color: #6b7280; margin: 20px 0 40px;">
|
||||||
|
{{ error_message or 'Es tut uns leid, aber die angeforderte Seite konnte nicht gefunden werden.' }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div style="display: flex; gap: 15px; justify-content: center; flex-wrap: wrap;">
|
||||||
|
<a href="{{ url_for('home') }}" class="button" style="background-color: #3b82f6; color: white; padding: 12px 30px; text-decoration: none; border-radius: 5px; display: inline-block; font-weight: 500;">
|
||||||
|
Zur Startseite
|
||||||
|
</a>
|
||||||
|
<a href="javascript:history.back()" class="button" style="background-color: #6b7280; color: white; padding: 12px 30px; text-decoration: none; border-radius: 5px; display: inline-block; font-weight: 500;">
|
||||||
|
Zurück
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Additional Help -->
|
||||||
|
<div style="margin-top: 60px; padding: 20px; background-color: #f3f4f6; border-radius: 8px;">
|
||||||
|
<p style="color: #6b7280; font-size: 14px;">
|
||||||
|
Wenn Sie denken, dass dies ein Fehler ist, oder Sie Hilfe benötigen,
|
||||||
|
<a href="{{ url_for('impressum') }}" style="color: #3b82f6; text-decoration: none;">kontaktieren Sie den Administrator</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.error-container {
|
||||||
|
animation: fadeIn 0.3s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container my-4">
|
<div class="container my-4">
|
||||||
<div class="impressum-content bg-white p-4 shadow rounded">
|
<div class="impressum-content p-4 shadow rounded" style="background: var(--ui-surface); color: var(--ui-text);">
|
||||||
<h1 class="mb-4 text-center">Impressum</h1>
|
<h1 class="mb-4 text-center">Impressum</h1>
|
||||||
<p class="text-center mb-5">(Angaben gemäß § 5 DDG)</p>
|
<p class="text-center mb-5">(Angaben gemäß § 5 DDG)</p>
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
<h3 class="mb-3">Kontaktinformationen</h3>
|
<h3 class="mb-3">Kontaktinformationen</h3>
|
||||||
<address class="mb-0">
|
<address class="mb-0">
|
||||||
<p><strong>Name:</strong> Invario UG</p>
|
<p><strong>Name:</strong> Invario UG</p>
|
||||||
<p><strong>Adresse:</strong> Musterstraße 123<br>12345 Musterstadt<br>Deutschland</p>
|
<p><strong>Adresse:</strong> Am Sportplatz 10<br>83052 Bruckmühl<br>Deutschland</p>
|
||||||
</address>
|
</address>
|
||||||
<p><strong>E-Mail:</strong> <a href="mailto:info@invario.eu">info@invario.eu</a></p>
|
<p><strong>E-Mail:</strong> <a href="mailto:info@invario.eu">info@invario.eu</a></p>
|
||||||
</div>
|
</div>
|
||||||
@@ -29,8 +29,8 @@
|
|||||||
<h3 class="mb-3">Verantwortlich für den Inhalt</h3>
|
<h3 class="mb-3">Verantwortlich für den Inhalt</h3>
|
||||||
<p>(nach § 18 Abs. 2 MStV)</p>
|
<p>(nach § 18 Abs. 2 MStV)</p>
|
||||||
<p>Invario UG<br>
|
<p>Invario UG<br>
|
||||||
Musterstraße 123<br>
|
Am Sportplatz 10<br>
|
||||||
12345 Musterstadt<br>
|
83052 Bruckmühl<br>
|
||||||
Deutschland</p>
|
Deutschland</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+80
-9
@@ -751,6 +751,7 @@
|
|||||||
let mainItemsSentinel = null;
|
let mainItemsSentinel = null;
|
||||||
let mainItemsLoadingIndicator = null;
|
let mainItemsLoadingIndicator = null;
|
||||||
let mainItemsScrollPrefetchBound = false;
|
let mainItemsScrollPrefetchBound = false;
|
||||||
|
let cardVirtualizationObserver = null;
|
||||||
|
|
||||||
function ensureMainItemsLoadingIndicator() {
|
function ensureMainItemsLoadingIndicator() {
|
||||||
const itemsContainer = document.querySelector('#items-container');
|
const itemsContainer = document.querySelector('#items-container');
|
||||||
@@ -759,11 +760,14 @@
|
|||||||
if (!mainItemsLoadingIndicator) {
|
if (!mainItemsLoadingIndicator) {
|
||||||
mainItemsLoadingIndicator = document.createElement('div');
|
mainItemsLoadingIndicator = document.createElement('div');
|
||||||
mainItemsLoadingIndicator.id = 'main-items-loading-indicator';
|
mainItemsLoadingIndicator.id = 'main-items-loading-indicator';
|
||||||
mainItemsLoadingIndicator.className = 'items-loading-indicator';
|
mainItemsLoadingIndicator.className = 'items-loading-indicator empty-frames-placeholder';
|
||||||
mainItemsLoadingIndicator.innerHTML = `
|
mainItemsLoadingIndicator.innerHTML = `
|
||||||
<div class="loading-track"><div class="loading-fill"></div></div>
|
<div class="empty-frame p-3 mx-2" style="flex: 1; height: 100px; border-radius: 12px; background: rgba(255,255,255,0.02); min-width: 250px;"></div>
|
||||||
<div class="loading-label">Weitere Objekte werden vorbereitet...</div>
|
<div class="empty-frame p-3 mx-2" style="flex: 1; height: 100px; border-radius: 12px; background: rgba(255,255,255,0.02); min-width: 250px; display: none;" class="d-none d-md-block"></div>
|
||||||
|
<div class="empty-frame p-3 mx-2" style="flex: 1; height: 100px; border-radius: 12px; background: rgba(255,255,255,0.02); min-width: 250px; display: none;" class="d-none d-lg-block"></div>
|
||||||
`;
|
`;
|
||||||
|
mainItemsLoadingIndicator.style.display = 'flex';
|
||||||
|
mainItemsLoadingIndicator.style.width = '100%';
|
||||||
}
|
}
|
||||||
|
|
||||||
itemsContainer.appendChild(mainItemsLoadingIndicator);
|
itemsContainer.appendChild(mainItemsLoadingIndicator);
|
||||||
@@ -1189,6 +1193,59 @@
|
|||||||
mainItemsScrollPrefetchBound = true;
|
mainItemsScrollPrefetchBound = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup DOM virtualization for items
|
||||||
|
if (!cardVirtualizationObserver) {
|
||||||
|
cardVirtualizationObserver = new IntersectionObserver((entries) => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
const card = entry.target;
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
if (card.dataset.deloaded === 'true') {
|
||||||
|
card.innerHTML = card.dataset.savedHtml;
|
||||||
|
card.dataset.deloaded = 'false';
|
||||||
|
card.classList.remove('is-deloaded');
|
||||||
|
card.classList.remove('empty-frame');
|
||||||
|
const favBtn = card.querySelector('.bookmark-btn');
|
||||||
|
if (favBtn) {
|
||||||
|
favBtn.addEventListener('click', (ev) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
toggleFavorite(card.dataset.itemId, favBtn, card);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const cardContent = card.querySelector('.card-content');
|
||||||
|
if (cardContent) {
|
||||||
|
cardContent.addEventListener('click', function(e) {
|
||||||
|
if (e.target.tagName === 'BUTTON' || e.target.tagName === 'A' || e.target.tagName === 'INPUT' ||
|
||||||
|
e.target.closest('button') || e.target.closest('a') || e.target.closest('.image-container')) return;
|
||||||
|
e.stopPropagation();
|
||||||
|
openItemQuick(card.dataset.itemId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (card.dataset.deloaded !== 'true') {
|
||||||
|
const rect = card.getBoundingClientRect();
|
||||||
|
if (rect.height > 50) {
|
||||||
|
card.style.minHeight = rect.height + 'px';
|
||||||
|
card.dataset.itemId = card.querySelector('.card-content')?.dataset.itemId || '';
|
||||||
|
card.dataset.savedHtml = card.innerHTML;
|
||||||
|
card.innerHTML = '';
|
||||||
|
card.dataset.deloaded = 'true';
|
||||||
|
card.classList.add('is-deloaded');
|
||||||
|
card.classList.add('empty-frame');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, {
|
||||||
|
// Keep approx ~120 items rendered in DOM relative to scroll position.
|
||||||
|
// Assuming ~100-400px per item/row, this margin unloads elements beyond that range.
|
||||||
|
rootMargin: isMobileLayout ? '18000px 0px 18000px 0px' : '9000px 0px 9000px 0px'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Observe all valid cards
|
||||||
|
document.querySelectorAll('.item-card').forEach(card => cardVirtualizationObserver.observe(card));
|
||||||
|
|
||||||
if (!mainItemsHasMore) return;
|
if (!mainItemsHasMore) return;
|
||||||
|
|
||||||
mainItemsObserver = new IntersectionObserver(entries => {
|
mainItemsObserver = new IntersectionObserver(entries => {
|
||||||
@@ -2419,29 +2476,29 @@
|
|||||||
// Add these functions right after the loadFilterState() function
|
// Add these functions right after the loadFilterState() function
|
||||||
|
|
||||||
function rebuildFilter3Options() {
|
function rebuildFilter3Options() {
|
||||||
if (!allItems || allItems.length === 0) return;
|
if (!allItems) return;
|
||||||
|
|
||||||
// Get current filter 1 and filter 2 selections
|
// Get current filter 1 and filter 2 selections
|
||||||
const filter1Selections = activeFilters.filter1;
|
const filter1Selections = activeFilters.filter1;
|
||||||
const filter2Selections = activeFilters.filter2;
|
const filter2Selections = activeFilters.filter2;
|
||||||
|
|
||||||
// If both filters are empty, show all filter3 values
|
// If both filters are empty, show all filter3 values from server
|
||||||
if (filter1Selections.length === 0 && filter2Selections.length === 0) {
|
if (filter1Selections.length === 0 && filter2Selections.length === 0) {
|
||||||
// Get all unique filter3 values
|
// Include dynamically added ones from loaded items too, just in case
|
||||||
const allFilter3Values = new Set();
|
const combinedFilter3Values = new Set(typeof allFilter3Values !== 'undefined' ? [...allFilter3Values] : []);
|
||||||
allItems.forEach(item => {
|
allItems.forEach(item => {
|
||||||
const filter3Array = Array.isArray(item.Filter3) ? item.Filter3 :
|
const filter3Array = Array.isArray(item.Filter3) ? item.Filter3 :
|
||||||
(item.Filter3 ? [item.Filter3] : []);
|
(item.Filter3 ? [item.Filter3] : []);
|
||||||
|
|
||||||
filter3Array.forEach(value => {
|
filter3Array.forEach(value => {
|
||||||
if (value && typeof value === 'string' && value.trim() !== '') {
|
if (value && typeof value === 'string' && value.trim() !== '') {
|
||||||
allFilter3Values.add(value);
|
combinedFilter3Values.add(value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Populate filter 3 dropdown with all values
|
// Populate filter 3 dropdown with all values
|
||||||
populateFilterOptions('filter3-options', [...allFilter3Values].sort(), 3);
|
populateFilterOptions('filter3-options', [...combinedFilter3Values].sort(), 3);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3114,6 +3171,8 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: transform 0.2s, box-shadow 0.2s;
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
content-visibility: auto;
|
||||||
|
contain-intrinsic-size: 300px 450px;
|
||||||
}
|
}
|
||||||
.item-card:hover {
|
.item-card:hover {
|
||||||
transform: translateY(-5px);
|
transform: translateY(-5px);
|
||||||
@@ -4614,6 +4673,18 @@ function openItemQuick(id){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
{% if open_item %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
if (typeof openEditModalFromServer === 'function') {
|
||||||
|
setTimeout(function() {
|
||||||
|
openEditModalFromServer('{{ open_item }}');
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
<style>
|
<style>
|
||||||
/* Admin/Main Content Container align */
|
/* Admin/Main Content Container align */
|
||||||
|
|||||||
+109
-21
@@ -403,6 +403,8 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: transform 0.2s, box-shadow 0.2s;
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
content-visibility: auto;
|
||||||
|
contain-intrinsic-size: 300px 450px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-card.bulk-selected {
|
.item-card.bulk-selected {
|
||||||
@@ -2738,6 +2740,9 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
|||||||
let descSearchIds = null; // Set of matching IDs or null when disabled
|
let descSearchIds = null; // Set of matching IDs or null when disabled
|
||||||
let currentUsername = '';
|
let currentUsername = '';
|
||||||
let allItems = []; // Cache for all items
|
let allItems = []; // Cache for all items
|
||||||
|
let allFilter1Values = new Set();
|
||||||
|
let allFilter2Values = new Set();
|
||||||
|
let allFilter3Values = new Set();
|
||||||
let bulkDeleteSelection = new Set();
|
let bulkDeleteSelection = new Set();
|
||||||
let bulkDeleteDrawerOpen = false;
|
let bulkDeleteDrawerOpen = false;
|
||||||
const studentCardsModuleEnabled = {{ 'true' if student_cards_module_enabled else 'false' }};
|
const studentCardsModuleEnabled = {{ 'true' if student_cards_module_enabled else 'false' }};
|
||||||
@@ -2876,30 +2881,29 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
|||||||
}
|
}
|
||||||
|
|
||||||
function rebuildFilter3Options() {
|
function rebuildFilter3Options() {
|
||||||
if (!allItems || allItems.length === 0) return;
|
if (!allItems) return;
|
||||||
|
|
||||||
// Get current filter 1 and filter 2 selections
|
// Get current filter 1 and filter 2 selections
|
||||||
const filter1Selections = activeFilters.filter1;
|
const filter1Selections = activeFilters.filter1;
|
||||||
const filter2Selections = activeFilters.filter2;
|
const filter2Selections = activeFilters.filter2;
|
||||||
|
|
||||||
// If both filters are empty, show all filter3 values
|
// If both filters are empty, show all filter3 values from server
|
||||||
if (filter1Selections.length === 0 && filter2Selections.length === 0) {
|
if (filter1Selections.length === 0 && filter2Selections.length === 0) {
|
||||||
// Get all unique filter3 values
|
// Include dynamically added ones from loaded items too, just in case
|
||||||
const allFilter3Values = new Set();
|
const combinedFilter3Values = new Set(typeof allFilter3Values !== 'undefined' ? [...allFilter3Values] : []);
|
||||||
allItems.forEach(item => {
|
allItems.forEach(item => {
|
||||||
const filter3Array = Array.isArray(item.Filter3) ? item.Filter3 :
|
const filter3Array = Array.isArray(item.Filter3) ? item.Filter3 :
|
||||||
(item.Filter3 ? [item.Filter3] : []);
|
(item.Filter3 ? [item.Filter3] : []);
|
||||||
|
|
||||||
filter3Array.forEach(value => {
|
filter3Array.forEach(value => {
|
||||||
if (value && typeof value === 'string' && value.trim() !== '') {
|
if (value && typeof value === 'string' && value.trim() !== '') {
|
||||||
allFilter3Values.add(value);
|
combinedFilter3Values.add(value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Populate filter 3 dropdown with all values
|
// Populate filter 3 dropdown with all values
|
||||||
populateFilterOptions('filter3-options', [...allFilter3Values].sort(), 3);
|
populateFilterOptions('filter3-options', [...combinedFilter3Values].sort(), 3);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter items based on filter1 and filter2 selections
|
// Filter items based on filter1 and filter2 selections
|
||||||
@@ -3258,6 +3262,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
|||||||
// Load predefined filter values for dropdowns
|
// Load predefined filter values for dropdowns
|
||||||
loadPredefinedFilterValues(1);
|
loadPredefinedFilterValues(1);
|
||||||
loadPredefinedFilterValues(2);
|
loadPredefinedFilterValues(2);
|
||||||
|
loadPredefinedFilterValues(3);
|
||||||
|
|
||||||
// Set up edit form submission
|
// Set up edit form submission
|
||||||
setupEditFormSubmission();
|
setupEditFormSubmission();
|
||||||
@@ -3282,11 +3287,24 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch user status to get current username
|
// Fetch user status and global filters
|
||||||
fetch('/user_status')
|
Promise.all([
|
||||||
.then(response => response.json())
|
fetch('/user_status').then(response => response.json()),
|
||||||
.then(data => {
|
fetch('/get_filter').then(response => response.json())
|
||||||
currentUsername = data.username;
|
])
|
||||||
|
.then(([userData, filterData]) => {
|
||||||
|
currentUsername = userData.username;
|
||||||
|
|
||||||
|
if (filterData.filter1) {
|
||||||
|
allFilter1Values = new Set(filterData.filter1.filter(value => value && typeof value === 'string' && value.trim() !== ''));
|
||||||
|
}
|
||||||
|
if (filterData.filter2) {
|
||||||
|
allFilter2Values = new Set(filterData.filter2.filter(value => value && typeof value === 'string' && value.trim() !== ''));
|
||||||
|
}
|
||||||
|
if (filterData.filter3) {
|
||||||
|
allFilter3Values = new Set(filterData.filter3.filter(value => value && typeof value === 'string' && value.trim() !== ''));
|
||||||
|
}
|
||||||
|
|
||||||
// Now load items with username information
|
// Now load items with username information
|
||||||
loadItems();
|
loadItems();
|
||||||
// Setup card images display after a short delay to ensure items are loaded
|
// Setup card images display after a short delay to ensure items are loaded
|
||||||
@@ -3295,7 +3313,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
|||||||
}, 300);
|
}, 300);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Error fetching user status:', error);
|
console.error('Error fetching user status or filters:', error);
|
||||||
loadItems(); // Load items anyway in case of error
|
loadItems(); // Load items anyway in case of error
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -3356,6 +3374,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
|||||||
let mainAdminItemsLoadingIndicator = null;
|
let mainAdminItemsLoadingIndicator = null;
|
||||||
let mainAdminItemsScrollPrefetchBound = false;
|
let mainAdminItemsScrollPrefetchBound = false;
|
||||||
let totalItemsInSystem = 0;
|
let totalItemsInSystem = 0;
|
||||||
|
let cardVirtualizationObserver = null;
|
||||||
|
|
||||||
function ensureMainAdminItemsLoadingIndicator() {
|
function ensureMainAdminItemsLoadingIndicator() {
|
||||||
const itemsContainer = document.querySelector('#items-container');
|
const itemsContainer = document.querySelector('#items-container');
|
||||||
@@ -3364,11 +3383,14 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
|||||||
if (!mainAdminItemsLoadingIndicator) {
|
if (!mainAdminItemsLoadingIndicator) {
|
||||||
mainAdminItemsLoadingIndicator = document.createElement('div');
|
mainAdminItemsLoadingIndicator = document.createElement('div');
|
||||||
mainAdminItemsLoadingIndicator.id = 'main-admin-items-loading-indicator';
|
mainAdminItemsLoadingIndicator.id = 'main-admin-items-loading-indicator';
|
||||||
mainAdminItemsLoadingIndicator.className = 'items-loading-indicator';
|
mainAdminItemsLoadingIndicator.className = 'items-loading-indicator empty-frames-placeholder';
|
||||||
mainAdminItemsLoadingIndicator.innerHTML = `
|
mainAdminItemsLoadingIndicator.innerHTML = `
|
||||||
<div class="loading-track"><div class="loading-fill"></div></div>
|
<div class="empty-frame p-3 mx-2" style="flex: 1; height: 100px; border-radius: 12px; background: rgba(255,255,255,0.02); min-width: 250px;"></div>
|
||||||
<div class="loading-label">Weitere Objekte werden vorbereitet...</div>
|
<div class="empty-frame p-3 mx-2" style="flex: 1; height: 100px; border-radius: 12px; background: rgba(255,255,255,0.02); min-width: 250px; display: none;" class="d-none d-md-block"></div>
|
||||||
|
<div class="empty-frame p-3 mx-2" style="flex: 1; height: 100px; border-radius: 12px; background: rgba(255,255,255,0.02); min-width: 250px; display: none;" class="d-none d-lg-block"></div>
|
||||||
`;
|
`;
|
||||||
|
mainAdminItemsLoadingIndicator.style.display = 'flex';
|
||||||
|
mainAdminItemsLoadingIndicator.style.width = '100%';
|
||||||
}
|
}
|
||||||
|
|
||||||
itemsContainer.appendChild(mainAdminItemsLoadingIndicator);
|
itemsContainer.appendChild(mainAdminItemsLoadingIndicator);
|
||||||
@@ -3695,10 +3717,10 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Populate filter options
|
// Populate filter options using server-provided full filter lists when available.
|
||||||
populateFilterOptions('filter1-options', [...filter1Values].sort(), 1);
|
populateFilterOptions('filter1-options', allFilter1Values.size ? [...allFilter1Values].sort() : [...filter1Values].sort(), 1);
|
||||||
populateFilterOptions('filter2-options', [...filter2Values].sort(), 2);
|
populateFilterOptions('filter2-options', allFilter2Values.size ? [...allFilter2Values].sort() : [...filter2Values].sort(), 2);
|
||||||
populateFilterOptions('filter3-options', [...filter3Values].sort(), 3);
|
populateFilterOptions('filter3-options', allFilter3Values.size ? [...allFilter3Values].sort() : [...filter3Values].sort(), 3);
|
||||||
|
|
||||||
if (!append) {
|
if (!append) {
|
||||||
itemsContainer.scrollLeft = 0;
|
itemsContainer.scrollLeft = 0;
|
||||||
@@ -3783,6 +3805,60 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
|||||||
mainAdminItemsScrollPrefetchBound = true;
|
mainAdminItemsScrollPrefetchBound = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup DOM virtualization for items
|
||||||
|
if (!cardVirtualizationObserver) {
|
||||||
|
cardVirtualizationObserver = new IntersectionObserver((entries) => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
const card = entry.target;
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
if (card.dataset.deloaded === 'true') {
|
||||||
|
card.innerHTML = card.dataset.savedHtml;
|
||||||
|
card.dataset.deloaded = 'false';
|
||||||
|
card.classList.remove('is-deloaded');
|
||||||
|
card.classList.remove('empty-frame');
|
||||||
|
const favBtn = card.querySelector('.bookmark-btn');
|
||||||
|
if (favBtn) {
|
||||||
|
favBtn.addEventListener('click', (ev) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
toggleFavorite(card.dataset.itemId, favBtn, card);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const cardContent = card.querySelector('.card-content');
|
||||||
|
if (cardContent) {
|
||||||
|
cardContent.addEventListener('click', function(e) {
|
||||||
|
if (e.target.tagName === 'BUTTON' || e.target.tagName === 'A' || e.target.tagName === 'INPUT' ||
|
||||||
|
e.target.closest('button') || e.target.closest('a') || e.target.closest('.image-container') ||
|
||||||
|
e.target.closest('.bulk-delete-toggle') || e.target.closest('label')) return;
|
||||||
|
e.stopPropagation();
|
||||||
|
openItemQuick(card.dataset.itemId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (card.dataset.deloaded !== 'true') {
|
||||||
|
const rect = card.getBoundingClientRect();
|
||||||
|
if (rect.height > 50) {
|
||||||
|
card.style.minHeight = rect.height + 'px';
|
||||||
|
card.dataset.itemId = card.querySelector('.card-content')?.dataset.itemId || '';
|
||||||
|
card.dataset.savedHtml = card.innerHTML;
|
||||||
|
card.innerHTML = '';
|
||||||
|
card.dataset.deloaded = 'true';
|
||||||
|
card.classList.add('is-deloaded');
|
||||||
|
card.classList.add('empty-frame');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, {
|
||||||
|
// Keep approx ~120 items rendered in DOM relative to scroll position.
|
||||||
|
// Assuming ~100-400px per item/row, this margin unloads elements beyond that range.
|
||||||
|
rootMargin: isMobileLayout ? '18000px 0px 18000px 0px' : '9000px 0px 9000px 0px'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Observe all valid cards
|
||||||
|
document.querySelectorAll('.item-card').forEach(card => cardVirtualizationObserver.observe(card));
|
||||||
|
|
||||||
if (!mainAdminItemsHasMore) return;
|
if (!mainAdminItemsHasMore) return;
|
||||||
|
|
||||||
mainAdminItemsObserver = new IntersectionObserver(entries => {
|
mainAdminItemsObserver = new IntersectionObserver(entries => {
|
||||||
@@ -5716,4 +5792,16 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
{% if open_item %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
if (typeof openEditModalFromServer === 'function') {
|
||||||
|
setTimeout(function() {
|
||||||
|
openEditModalFromServer('{{ open_item }}');
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -15,16 +15,16 @@
|
|||||||
<h1 class="mb-4">Filterwerte verwalten</h1>
|
<h1 class="mb-4">Filterwerte verwalten</h1>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<!-- Filter 1: Unterrichtsfach -->
|
<!-- Filter 1 -->
|
||||||
<div class="col-md-6">
|
<div class="col-md-4">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h2 class="card-title h5 mb-0">Unterrichtsfach (Filter 1)</h2>
|
<h2 class="card-title h5 mb-0">{{ filter_names.get('1', 'Fach/Kategorie') }} (Filter 1)</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="POST" action="{{ url_for('add_filter_value', filter_num=1) }}" class="mb-4">
|
<form method="POST" action="{{ url_for('add_filter_value', filter_num=1) }}" class="mb-4">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" name="value" class="form-control" placeholder="Neues Unterrichtsfach..." required>
|
<input type="text" name="value" class="form-control" placeholder="Neuer Wert..." required>
|
||||||
<button type="submit" class="btn btn-primary">Hinzufügen</button>
|
<button type="submit" class="btn btn-primary">Hinzufügen</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -33,13 +33,25 @@
|
|||||||
{% if filter1_values %}
|
{% if filter1_values %}
|
||||||
<div class="list-group">
|
<div class="list-group">
|
||||||
{% for value in filter1_values %}
|
{% for value in filter1_values %}
|
||||||
<div class="list-group-item d-flex justify-content-between align-items-center">
|
<div class="list-group-item">
|
||||||
{{ value }}
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
<form method="POST" action="{{ url_for('remove_filter_value', filter_num=1, value=value) }}" class="d-inline">
|
<span>{{ value }}</span>
|
||||||
<button type="submit" class="btn btn-sm btn-danger"
|
<div>
|
||||||
onclick="return confirm('Sind Sie sicher, dass Sie den Wert \"' + '{{ value }}'.replace(/'/g, '\\\'') + '\" löschen möchten?');">
|
<button type="button" class="btn btn-sm btn-secondary me-1" onclick="toggleEdit('edit-1-{{ loop.index }}')">Bearbeiten</button>
|
||||||
Entfernen
|
<form method="POST" action="{{ url_for('remove_filter_value', filter_num=1, value=value) }}" class="d-inline">
|
||||||
</button>
|
<button type="submit" class="btn btn-sm btn-danger"
|
||||||
|
onclick="return confirm('Sind Sie sicher, dass Sie den Wert \"' + '{{ value }}'.replace(/'/g, '\\\'') + '\" löschen möchten?');">
|
||||||
|
Entfernen
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form id="edit-1-{{ loop.index }}" method="POST" action="{{ url_for('edit_filter_value', filter_num=1, old_value=value) }}" style="display: none;" class="mt-2">
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<input type="text" name="new_value" class="form-control" value="{{ value }}" required>
|
||||||
|
<button type="submit" class="btn btn-primary" onclick="return confirm('Tipp: Das Ändern des Namens aktualisiert auch alle Einträge in der Datenbank, die diesen Filter verwenden. Fortfahren?');">Speichern</button>
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="toggleEdit('edit-1-{{ loop.index }}')">Abbrechen</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -51,16 +63,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Filter 2: Jahrgangsstufe -->
|
<!-- Filter 2 -->
|
||||||
<div class="col-md-6">
|
<div class="col-md-4">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h2 class="card-title h5 mb-0">Jahrgangsstufe (Filter 2)</h2>
|
<h2 class="card-title h5 mb-0">{{ filter_names.get('2', 'System/Bereich') }} (Filter 2)</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="POST" action="{{ url_for('add_filter_value', filter_num=2) }}" class="mb-4">
|
<form method="POST" action="{{ url_for('add_filter_value', filter_num=2) }}" class="mb-4">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" name="value" class="form-control" placeholder="Neue Jahrgangsstufe..." required>
|
<input type="text" name="value" class="form-control" placeholder="Neuer Wert..." required>
|
||||||
<button type="submit" class="btn btn-primary">Hinzufügen</button>
|
<button type="submit" class="btn btn-primary">Hinzufügen</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -69,13 +81,73 @@
|
|||||||
{% if filter2_values %}
|
{% if filter2_values %}
|
||||||
<div class="list-group">
|
<div class="list-group">
|
||||||
{% for value in filter2_values %}
|
{% for value in filter2_values %}
|
||||||
<div class="list-group-item d-flex justify-content-between align-items-center">
|
<div class="list-group-item">
|
||||||
{{ value }}
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
<form method="POST" action="{{ url_for('remove_filter_value', filter_num=2, value=value) }}" class="d-inline">
|
<span>{{ value }}</span>
|
||||||
<button type="submit" class="btn btn-sm btn-danger"
|
<div>
|
||||||
onclick="return confirm('Sind Sie sicher, dass Sie den Wert \"' + '{{ value }}'.replace(/'/g, '\\\'') + '\" löschen möchten?');">
|
<button type="button" class="btn btn-sm btn-secondary me-1" onclick="toggleEdit('edit-2-{{ loop.index }}')">Bearbeiten</button>
|
||||||
Entfernen
|
<form method="POST" action="{{ url_for('remove_filter_value', filter_num=2, value=value) }}" class="d-inline">
|
||||||
</button>
|
<button type="submit" class="btn btn-sm btn-danger"
|
||||||
|
onclick="return confirm('Sind Sie sicher, dass Sie den Wert \"' + '{{ value }}'.replace(/'/g, '\\\'') + '\" löschen möchten?');">
|
||||||
|
Entfernen
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form id="edit-2-{{ loop.index }}" method="POST" action="{{ url_for('edit_filter_value', filter_num=2, old_value=value) }}" style="display: none;" class="mt-2">
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<input type="text" name="new_value" class="form-control" value="{{ value }}" required>
|
||||||
|
<button type="submit" class="btn btn-primary" onclick="return confirm('Tipp: Das Ändern des Namens aktualisiert auch alle Einträge in der Datenbank, die diesen Filter verwenden. Fortfahren?');">Speichern</button>
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="toggleEdit('edit-2-{{ loop.index }}')">Abbrechen</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-info">Keine Werte definiert.</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter 3 -->
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2 class="card-title h5 mb-0">{{ filter_names.get('3', 'Typ/Art') }} (Filter 3)</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST" action="{{ url_for('add_filter_value', filter_num=3) }}" class="mb-4">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" name="value" class="form-control" placeholder="Neuer Wert..." required>
|
||||||
|
<button type="submit" class="btn btn-primary">Hinzufügen</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<h5 class="mb-3">Vorhandene Werte</h5>
|
||||||
|
{% if filter3_values %}
|
||||||
|
<div class="list-group">
|
||||||
|
{% for value in filter3_values %}
|
||||||
|
<div class="list-group-item">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<span>{{ value }}</span>
|
||||||
|
<div>
|
||||||
|
<button type="button" class="btn btn-sm btn-secondary me-1" onclick="toggleEdit('edit-3-{{ loop.index }}')">Bearbeiten</button>
|
||||||
|
<form method="POST" action="{{ url_for('remove_filter_value', filter_num=3, value=value) }}" class="d-inline">
|
||||||
|
<button type="submit" class="btn btn-sm btn-danger"
|
||||||
|
onclick="return confirm('Sind Sie sicher, dass Sie den Wert \"' + '{{ value }}'.replace(/'/g, '\\\'') + '\" löschen möchten?');">
|
||||||
|
Entfernen
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form id="edit-3-{{ loop.index }}" method="POST" action="{{ url_for('edit_filter_value', filter_num=3, old_value=value) }}" style="display: none;" class="mt-2">
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<input type="text" name="new_value" class="form-control" value="{{ value }}" required>
|
||||||
|
<button type="submit" class="btn btn-primary" onclick="return confirm('Tipp: Das Ändern des Namens aktualisiert auch alle Einträge in der Datenbank, die diesen Filter verwenden. Fortfahren?');">Speichern</button>
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="toggleEdit('edit-3-{{ loop.index }}')">Abbrechen</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -97,4 +169,15 @@
|
|||||||
<a href="{{ url_for('home_admin') }}" class="btn btn-secondary">Zurück zur Admin-Übersicht</a>
|
<a href="{{ url_for('home_admin') }}" class="btn btn-secondary">Zurück zur Admin-Übersicht</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function toggleEdit(id) {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (el.style.display === 'none') {
|
||||||
|
el.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
el.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -709,7 +709,6 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="upload-container">
|
<div class="upload-container">
|
||||||
<a href="{{ url_for(back_target|default('home_admin')) }}" class="nav-back-button">← Zurück zur Artikelübersicht</a>
|
|
||||||
|
|
||||||
{% if show_library_features %}
|
{% if show_library_features %}
|
||||||
<div style="border:1px solid #dbe4ee; border-radius:8px; padding:14px; margin-bottom:16px; background:#f8fbff;">
|
<div style="border:1px solid #dbe4ee; border-radius:8px; padding:14px; margin-bottom:16px; background:#f8fbff;">
|
||||||
|
|||||||
@@ -97,6 +97,10 @@ def get_tenant_config(tenant_id=None):
|
|||||||
if tenant_id in TENANT_REGISTRY:
|
if tenant_id in TENANT_REGISTRY:
|
||||||
return TENANT_REGISTRY[tenant_id] or {}
|
return TENANT_REGISTRY[tenant_id] or {}
|
||||||
|
|
||||||
|
for alias in _tenant_db_aliases(tenant_id):
|
||||||
|
if alias in TENANT_REGISTRY:
|
||||||
|
return TENANT_REGISTRY[alias] or {}
|
||||||
|
|
||||||
return TENANT_REGISTRY.get('default', {}) or {}
|
return TENANT_REGISTRY.get('default', {}) or {}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
from pymongo import MongoClient
|
||||||
|
import Web.settings as cfg
|
||||||
|
|
||||||
|
def get_filter_names():
|
||||||
|
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||||
|
db = client[cfg.MONGODB_DB]
|
||||||
|
names = db.settings.find_one({'setting_type': 'filter_names'})
|
||||||
|
client.close()
|
||||||
|
if names:
|
||||||
|
return names.get('names', {
|
||||||
|
'1': 'Fach/Kategorie',
|
||||||
|
'2': 'System/Bereich',
|
||||||
|
'3': 'Typ/Art'
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
'1': 'Fach/Kategorie',
|
||||||
|
'2': 'System/Bereich',
|
||||||
|
'3': 'Typ/Art'
|
||||||
|
}
|
||||||
|
|
||||||
|
def set_filter_name(filter_num, name):
|
||||||
|
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||||
|
db = client[cfg.MONGODB_DB]
|
||||||
|
names = get_filter_names()
|
||||||
|
names[str(filter_num)] = name
|
||||||
|
db.settings.update_one(
|
||||||
|
{'setting_type': 'filter_names'},
|
||||||
|
{'$set': {'names': names}},
|
||||||
|
upsert=True
|
||||||
|
)
|
||||||
|
client.close()
|
||||||
|
return True
|
||||||
+1
-7
@@ -498,8 +498,6 @@ def check_nm_pwd(username, password):
|
|||||||
finally:
|
finally:
|
||||||
client.close()
|
client.close()
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def add_user(
|
def add_user(
|
||||||
username,
|
username,
|
||||||
@@ -536,16 +534,12 @@ def add_user(
|
|||||||
for key, value in page_permissions.items():
|
for key, value in page_permissions.items():
|
||||||
permission_defaults['pages'][str(key)] = bool(value)
|
permission_defaults['pages'][str(key)] = bool(value)
|
||||||
|
|
||||||
alias_first = name if str(name or '').strip() else username
|
|
||||||
alias_last = last_name if str(last_name or '').strip() else ''
|
|
||||||
name_alias = build_name_synonym(alias_first, alias_last)
|
|
||||||
|
|
||||||
user_doc = {
|
user_doc = {
|
||||||
'Username': username,
|
'Username': username,
|
||||||
'Password': hashing(password),
|
'Password': hashing(password),
|
||||||
'Admin': False,
|
'Admin': False,
|
||||||
'active_ausleihung': None,
|
'active_ausleihung': None,
|
||||||
'name': name_alias,
|
'name': name.strip() if name else '',
|
||||||
'last_name': last_name.strip() if last_name else '',
|
'last_name': last_name.strip() if last_name else '',
|
||||||
'IsStudent': bool(is_student),
|
'IsStudent': bool(is_student),
|
||||||
'PermissionPreset': permission_defaults['preset'],
|
'PermissionPreset': permission_defaults['preset'],
|
||||||
|
|||||||
+47
-25
@@ -1,41 +1,63 @@
|
|||||||
version: "3.8"
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
build: .
|
image: ghcr.io/aiirondev/legendary-octo-garbanzo:v0.7.39
|
||||||
container_name: inventory-app
|
container_name: inventarsystem-app
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
ports:
|
||||||
- MONGO_URL=mongodb://mongodb:27017/inventar
|
- "10000:8000"
|
||||||
- REDIS_URL=redis://redis:6379
|
|
||||||
- BASE_URL=https://inventar.maximiliangruendinger.de #alle öffentliche subdomains
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- mongodb
|
- mongodb
|
||||||
- redis
|
- redis
|
||||||
|
environment:
|
||||||
|
INVENTAR_MONGODB_HOST: mongodb
|
||||||
|
INVENTAR_MONGODB_PORT: "27017"
|
||||||
|
INVENTAR_MONGODB_DB: Inventarsystem
|
||||||
|
INVENTAR_BACKUP_FOLDER: /data/backups
|
||||||
|
INVENTAR_LOGS_FOLDER: /data/logs
|
||||||
|
expose:
|
||||||
|
- "8000"
|
||||||
|
volumes:
|
||||||
|
- ./config.json:/app/config.json:ro
|
||||||
|
- app_uploads:/app/Web/uploads
|
||||||
|
- app_thumbnails:/app/Web/thumbnails
|
||||||
|
- app_previews:/app/Web/previews
|
||||||
|
- app_qrcodes:/app/Web/QRCodes
|
||||||
|
- app_backups:/data/backups
|
||||||
|
- app_logs:/data/logs
|
||||||
|
|
||||||
mongodb:
|
mongodb:
|
||||||
image: mongo:latest
|
image: mongo:7.0
|
||||||
container_name: mongodb
|
container_name: inventarsystem-mongodb
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- mongo_data:/data/db
|
- mongodb_data:/data/db
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping').ok"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:alpine
|
image: redis:7-alpine
|
||||||
container_name: redis
|
container_name: inventarsystem-redis
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
command: redis-server --appendonly yes --maxmemory 512mb --maxmemory-policy allkeys-lru
|
||||||
cloudflared:
|
ports:
|
||||||
image: cloudflare/cloudflared:latest
|
- "6379:6379"
|
||||||
container_name: cloudflared
|
|
||||||
restart: unless-stopped
|
|
||||||
# Der Tunnel-Name 'homeserver' muss zu deiner credentials.json passen
|
|
||||||
command: tunnel run homeserver
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./config.yml:/etc/cloudflared/config.yml
|
- redis_data:/data
|
||||||
- ./credentials.json:/etc/cloudflared/credentials.json
|
healthcheck:
|
||||||
depends_on:
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
- app
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
mongo_data:
|
mongodb_data:
|
||||||
|
app_uploads:
|
||||||
|
app_thumbnails:
|
||||||
|
app_previews:
|
||||||
|
app_qrcodes:
|
||||||
|
app_backups:
|
||||||
|
app_logs:
|
||||||
|
redis_data:
|
||||||
|
|||||||
Reference in New Issue
Block a user