Compare commits

...

14 Commits

13 changed files with 655 additions and 277 deletions
+231 -117
View File
@@ -1268,7 +1268,8 @@ def inject_version():
'inventory_module_enabled': cfg.MODULES.is_enabled('inventory'),
'terminplan_module_enabled': cfg.MODULES.is_enabled('terminplan'),
'library_module_enabled': cfg.MODULES.is_enabled('library'),
'student_cards_module_enabled': cfg.MODULES.is_enabled('student_cards'),
'student_cards_module_enabled': cfg.MODULES.is_enabled('student_cards'),#
'mail_module_enabled': cfg.MODULES.is_enabled('mail'),
'is_admin': is_admin,
'unread_notification_count': unread_notification_count,
'current_permissions': current_permissions,
@@ -2885,6 +2886,7 @@ def home():
username=session['username'],
library_module_enabled=cfg.MODULES.is_enabled('library'),
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
mail_module_enabled=cfg.MODULES.is_enabled('mail'),
student_default_borrow_days=cfg.STUDENT_DEFAULT_BORROW_DAYS,
student_max_borrow_days=cfg.STUDENT_MAX_BORROW_DAYS,
open_item=request.args.get('open_item')
@@ -2927,6 +2929,7 @@ def home_admin():
username=session['username'],
library_module_enabled=cfg.MODULES.is_enabled('library'),
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
mail_module_enabled=cfg.MODULES.is_enabled('mail'),
student_default_borrow_days=cfg.STUDENT_DEFAULT_BORROW_DAYS,
student_max_borrow_days=cfg.STUDENT_MAX_BORROW_DAYS,
school_info=_get_school_info_for_export(),
@@ -2947,6 +2950,7 @@ def tutorial_page():
is_admin=us.check_admin(session['username']),
library_module_enabled=cfg.MODULES.is_enabled('library'),
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
mail_module_enabled=cfg.MODULES.is_enabled('mail'),
student_default_borrow_days=cfg.STUDENT_DEFAULT_BORROW_DAYS,
student_max_borrow_days=cfg.STUDENT_MAX_BORROW_DAYS
)
@@ -5207,55 +5211,87 @@ def upload_item():
flash(error_msg, 'error')
return redirect(url_for(success_redirect_endpoint))
# 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]):
existing_item = it._get_items_collection().find_one({"code_4": code_4[0]})
if existing_item:
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'])))
# -------------------------------------------------------------------
# 1. PARSE AND UNIFY CODES
# -------------------------------------------------------------------
# Assuming `code_4` might come in as a single string or a list depending on your form setup
primary_code_raw = request.form.get('code_4', '').strip()
individual_codes_raw = request.form.get('individual_codes', '').strip()
all_item_codes = []
if primary_code_raw:
all_item_codes.append(primary_code_raw)
if individual_codes_raw:
# Split textarea by newlines, clean whitespace, and ignore empty lines
extra_codes = [c.strip() for c in individual_codes_raw.replace('\r', '').split('\n') if c.strip()]
# Add to master list, ensuring no duplicates within the submitted list itself
for c in extra_codes:
if c not in all_item_codes:
all_item_codes.append(c)
# -------------------------------------------------------------------
# 2. OVERRIDE ITEM COUNT
# -------------------------------------------------------------------
# If the user scanned/generated codes, the amount of codes IS the item count.
# Otherwise, fallback to a manual item_count field if it exists.
if len(all_item_codes) > 0:
item_count = len(all_item_codes)
else:
try:
item_count = int(request.form.get('item_count', 1))
except ValueError:
item_count = 1
# -------------------------------------------------------------------
# 3. IMAGE VALIDATION
# -------------------------------------------------------------------
if upload_mode != 'library' and not is_duplicating and not images and not duplicate_images and not book_cover_image:
error_msg = 'Bitte laden Sie mindestens ein Bild hoch'
if is_mobile:
return jsonify({'success': False, 'message': error_msg}), 400
else:
error_msg = 'Der Code wird bereits verwendet. Bitte wählen Sie einen anderen Code.'
flash(error_msg, 'error')
return redirect(url_for(success_redirect_endpoint))
# -------------------------------------------------------------------
# 4. UNIFIED DB UNIQUENESS VALIDATION
# -------------------------------------------------------------------
# Validate every code in our master list against the database
for code in all_item_codes:
if not it.is_code_unique(code):
# Special case: If they only uploaded ONE item, redirect them to that existing item
if item_count == 1:
existing_item = it._get_items_collection().find_one({"code_4": code})
if existing_item:
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'])))
# If multiple items are being uploaded, just throw a standard error
error_msg = f'Der Code "{code}" wird bereits von einem anderen Artikel verwendet. Bitte überprüfen Sie Ihre Eingaben.'
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
if individual_codes:
if len(individual_codes) > item_count:
error_msg = f'Zu viele Einzelcodes angegeben ({len(individual_codes)}), erlaubt sind maximal {item_count}.'
if is_mobile:
return jsonify({'success': False, 'message': error_msg}), 400
flash(error_msg, 'error')
return redirect(url_for(success_redirect_endpoint))
if len(set(individual_codes)) != len(individual_codes):
error_msg = 'Doppelte Einzelcodes erkannt. Bitte alle Codes eindeutig eintragen.'
if is_mobile:
return jsonify({'success': False, 'message': error_msg}), 400
flash(error_msg, 'error')
return redirect(url_for(success_redirect_endpoint))
for specific_code in individual_codes:
if not it.is_code_unique(specific_code):
error_msg = f'Der Einzelcode "{specific_code}" wird bereits verwendet.'
if is_mobile:
return jsonify({'success': False, 'message': error_msg}), 400
flash(error_msg, 'error')
return redirect(url_for(success_redirect_endpoint))
# -------------------------------------------------------------------
# 5. BATCH CODE GENERATOR (Remains mostly unchanged)
# -------------------------------------------------------------------
def generate_unique_batch_code(base_code, position):
"""Generate a unique code for every item in a batch."""
"""Generate a unique code for every item in a batch if no specific code is provided."""
if not base_code:
return None
@@ -7948,7 +7984,8 @@ def admin_borrowings():
'admin_borrowings.html',
entries=entries,
library_module_enabled=cfg.MODULES.is_enabled('library'),
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards')
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
mail_module_enabled=cfg.MODULES.is_enabled('mail')
)
"""-----------------------------------------------------------Audit Routes-------------------------------------------------------"""
@@ -8011,6 +8048,7 @@ def admin_audit_dashboard():
audit_rows=audit_rows,
library_module_enabled=cfg.MODULES.is_enabled('library'),
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
mail_module_enabled=cfg.MODULES.is_enabled('mail'),
)
except Exception as exc:
app.logger.error(f"Error loading audit dashboard: {exc}")
@@ -8826,6 +8864,7 @@ def library_item_invoices(item_id):
invoices=entries,
library_module_enabled=cfg.MODULES.is_enabled('library'),
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
mail_module_enabled=cfg.MODULES.is_enabled('mail')
)
except Exception as e:
app.logger.error(f"Error loading invoice history for item {item_id}: {e}")
@@ -9376,17 +9415,137 @@ def search_word(word):
except Exception as e:
return jsonify({"success": False, "response": str(e)})
def _fetch_from_google_books(clean_isbn):
"""Source 1: Google Books API (Free, No Key required for basic use)"""
try:
url = f"https://www.googleapis.com/books/v1/volumes?q=isbn:{clean_isbn}"
response = requests.get(url, timeout=5)
if response.status_code != 200:
return None
data = response.json()
if data.get('totalItems', 0) > 0 and data.get('items'):
book_info = data['items'][0].get('volumeInfo', {})
sale_info = data['items'][0].get('saleInfo', {})
price = None
retail_price = sale_info.get('retailPrice', {})
list_price = sale_info.get('listPrice', {})
if retail_price and 'amount' in retail_price:
price = f"{retail_price['amount']} {retail_price.get('currencyCode', '')}"
elif list_price and 'amount' in list_price:
price = f"{list_price['amount']} {list_price.get('currencyCode', '')}"
thumbnail = book_info.get('imageLinks', {}).get('thumbnail', '')
if thumbnail:
thumbnail = thumbnail.replace('http:', 'https:')
return {
"title": book_info.get('title', 'Unknown Title'),
"authors": ', '.join(book_info.get('authors', ['Unknown Author'])),
"publisher": book_info.get('publisher', 'Unknown Publisher'),
"publishedDate": book_info.get('publishedDate', 'Unknown Date'),
"description": book_info.get('description', 'Keine Beschreibung verfügbar'),
"pageCount": book_info.get('pageCount', 'Unknown'),
"price": price,
"thumbnail": thumbnail,
"source": "google-books"
}
except Exception as e:
print(f"Google Books error: {e}")
return None
def _fetch_from_lobid_germany(clean_isbn):
"""
Source 2: Lobid.org (hbz Network)
The ultimate free, keyless fallback for German Schoolbooks and DACH literature.
"""
try:
url = f"https://lobid.org/resources?q=isbn:{clean_isbn}&format=json"
response = requests.get(url, timeout=5)
if response.status_code != 200:
return None
data = response.json()
if 'member' in data and len(data['member']) > 0:
book = data['member'][0]
# Extract Authors (Lobid uses a nested contribution array)
authors = []
for contrib in book.get('contribution', []):
if 'agent' in contrib and 'label' in contrib['agent']:
authors.append(contrib['agent']['label'])
# Extract Publishers
publishers = []
for pub in book.get('publication', []):
if 'publishedBy' in pub:
# sometimes publishedBy is a list, sometimes a string
pub_data = pub['publishedBy']
if isinstance(pub_data, list):
publishers.extend(pub_data)
else:
publishers.append(pub_data)
# Extract Date
pub_date = 'Unknown Date'
if book.get('publication'):
pub_date = book['publication'][0].get('startDate', 'Unknown Date')
# Extract Pages
pages = book.get('extent', ['Unknown'])[0] if 'extent' in book else 'Unknown'
return {
"title": book.get('title', 'Unknown Title'),
"authors": ', '.join(authors) if authors else 'Unknown Author',
"publisher": ', '.join(publishers) if publishers else 'Unknown Publisher',
"publishedDate": str(pub_date),
"description": 'Keine Beschreibung verfügbar (Schulbuch / Fachliteratur)',
"pageCount": pages,
"price": None,
"thumbnail": "", # Lobid rarely has covers, we will fallback to OpenLibrary cover below
"source": "lobid-germany"
}
except Exception as e:
print(f"Lobid error: {e}")
return None
def _fetch_from_open_library(clean_isbn):
"""Source 3: Open Library Search API (Free, No Key required)"""
try:
url = f"https://openlibrary.org/search.json?isbn={clean_isbn}"
response = requests.get(url, timeout=5)
if response.status_code != 200:
return None
data = response.json()
if data.get('numFound', 0) > 0 and data.get('docs'):
book_info = data['docs'][0]
return {
"title": book_info.get('title', 'Unknown Title'),
"authors": ', '.join(book_info.get('author_name', ['Unknown Author'])),
"publisher": ', '.join(book_info.get('publisher', ['Unknown Publisher'])),
"publishedDate": str(book_info.get('publish_date', ['Unknown Date'])[0]),
"description": 'Keine Beschreibung verfügbar',
"pageCount": book_info.get('number_of_pages_median', 'Unknown'),
"price": None,
"thumbnail": f"https://covers.openlibrary.org/b/isbn/{clean_isbn}-L.jpg",
"source": "openlibrary"
}
except Exception as e:
print(f"OpenLibrary Search error: {e}")
return None
@app.route('/fetch_book_info/<isbn>')
def fetch_book_info(isbn):
"""
API endpoint to fetch book information by ISBN using Google Books API
Args:
isbn (str): ISBN to look up
Returns:
dict: Book information or error message
API endpoint to fetch book information by ISBN using multiple open sources.
Optimized for global literature AND German educational books.
"""
# Authorization Checks
if 'username' not in session or not us.check_admin(session['username']):
return jsonify({"error": "Not authorized"}), 403
@@ -9394,79 +9553,33 @@ def fetch_book_info(isbn):
return jsonify({"error": "Bibliotheks-Modul ist deaktiviert."}), 403
try:
# Validation
clean_isbn = normalize_and_validate_isbn(isbn)
if not clean_isbn:
return jsonify({"error": "Ungültige ISBN. Bitte ISBN-10 oder ISBN-13 verwenden."}), 400
# First source: Google Books
response = requests.get(
f"https://www.googleapis.com/books/v1/volumes?q=isbn:{clean_isbn}",
timeout=10
)
# Define the Provider Chain (Order matters: General -> German/School -> Global Fallback)
providers = [
_fetch_from_google_books,
_fetch_from_lobid_germany,
_fetch_from_open_library
]
if response.status_code == 200:
data = response.json()
if data.get('totalItems', 0) > 0 and data.get('items'):
book_info = data['items'][0].get('volumeInfo', {})
sale_info = data['items'][0].get('saleInfo', {})
price = None
retail_price = sale_info.get('retailPrice', {})
list_price = sale_info.get('listPrice', {})
if retail_price and 'amount' in retail_price:
price = f"{retail_price['amount']} {retail_price.get('currencyCode', '')}"
elif list_price and 'amount' in list_price:
price = f"{list_price['amount']} {list_price.get('currencyCode', '')}"
thumbnail = book_info.get('imageLinks', {}).get('thumbnail', '')
if thumbnail:
thumbnail = thumbnail.replace('http:', 'https:')
return jsonify({
"title": book_info.get('title', 'Unknown Title'),
"authors": ', '.join(book_info.get('authors', ['Unknown Author'])),
"publisher": book_info.get('publisher', 'Unknown Publisher'),
"publishedDate": book_info.get('publishedDate', 'Unknown Date'),
"description": book_info.get('description', 'No description available'),
"pageCount": book_info.get('pageCount', 'Unknown'),
"price": price,
"thumbnail": thumbnail,
"isbn": clean_isbn,
"source": "google-books"
})
# Fallback: OpenLibrary
ol_response = requests.get(
f"https://openlibrary.org/isbn/{clean_isbn}.json",
timeout=10
)
if ol_response.status_code == 200:
ol_data = ol_response.json()
author_names = []
for author_ref in ol_data.get('authors', []):
key = author_ref.get('key')
if not key:
continue
try:
author_resp = requests.get(f"https://openlibrary.org{key}.json", timeout=8)
if author_resp.status_code == 200:
author_names.append(author_resp.json().get('name'))
except Exception:
continue
return jsonify({
"title": ol_data.get('title', 'Unknown Title'),
"authors": ', '.join([a for a in author_names if a]) if author_names else 'Unknown Author',
"publisher": ', '.join(ol_data.get('publishers', [])) if ol_data.get('publishers') else 'Unknown Publisher',
"publishedDate": ol_data.get('publish_date', 'Unknown Date'),
"description": (ol_data.get('description', {}).get('value') if isinstance(ol_data.get('description'), dict) else ol_data.get('description')) or 'No description available',
"pageCount": ol_data.get('number_of_pages', 'Unknown'),
"price": None,
"thumbnail": f"https://covers.openlibrary.org/b/isbn/{clean_isbn}-L.jpg",
"isbn": clean_isbn,
"source": "openlibrary"
})
# Iterate through providers until a book is found
for provider in providers:
book_data = provider(clean_isbn)
if book_data:
# Add the ISBN back into the response payload
book_data["isbn"] = clean_isbn
# If a provider found the book but had no cover, assign a default OpenLibrary cover fallback
if not book_data.get("thumbnail"):
book_data["thumbnail"] = f"https://covers.openlibrary.org/b/isbn/{clean_isbn}-L.jpg"
return jsonify(book_data)
# If all providers fail
return jsonify({"error": f"Kein Buch zu dieser ISBN gefunden: {clean_isbn}"}), 404
except Exception as e:
@@ -10086,6 +10199,7 @@ def admin_damaged_items():
damaged_items=damaged_rows,
library_module_enabled=cfg.MODULES.is_enabled('library'),
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
mail_module_enabled=cfg.MODULES.is_enabled('mail')
)
except Exception as exc:
app.logger.error(f"Error loading damaged-items admin view: {exc}")
+19 -15
View File
@@ -1,5 +1,6 @@
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from modules.module_registry import ModuleRegistry as mr
import smtplib
import Web.modules.database.settings as cfg
@@ -27,21 +28,24 @@ def send(email: list, subject: str, note: str, sender: str) -> bool:
Output:
- bool: true if the sending worked and false if it didnt
"""
msg = MIMEMultipart()
msg['Subject'] = subject
msg['From'] = sender or cfg.EMAIL_FROM_ADDRESS or cfg.EMAIL_USERNAME
msg['To'] = ', '.join(email) if isinstance(email, (list, tuple)) else str(email)
msg.attach(MIMEText(note))
smtp = None
try:
smtp = _build_smtp_client()
smtp.sendmail(from_addr=msg['From'], to_addrs=email, msg=msg.as_string())
return True
except Exception:
if not mr.registry.is_enabled('mail'):
return False
finally:
else:
msg = MIMEMultipart()
msg['Subject'] = subject
msg['From'] = sender or cfg.EMAIL_FROM_ADDRESS or cfg.EMAIL_USERNAME
msg['To'] = ', '.join(email) if isinstance(email, (list, tuple)) else str(email)
msg.attach(MIMEText(note))
smtp = None
try:
if smtp:
smtp.quit()
smtp = _build_smtp_client()
smtp.sendmail(from_addr=msg['From'], to_addrs=email, msg=msg.as_string())
return True
except Exception:
pass
return False
finally:
try:
if smtp:
smtp.quit()
except Exception:
pass
+10 -4
View File
@@ -82,6 +82,7 @@ def build_calendar_ics(appointment_id: str) -> str | None:
time_span = item.get('time_span', []) or []
creator = item.get('user', 'Terminplaner')
note = item.get('note', '') or ''
titel = item.get('title', '') or ''
tenant_id = _current_tenant_id()
try:
link = url_for('terminplaner.client', appointment_id=str(appointment_id), tenant=tenant_id or None, _external=True)
@@ -108,6 +109,8 @@ def build_calendar_ics(appointment_id: str) -> str | None:
description_lines.append('Zeitfenster: ' + '; '.join(str(entry) for entry in time_span))
if note:
description_lines.append('Notiz: ' + str(note))
if titel:
description_lines.append('Titel: ' + str(titel))
ics_lines = [
'BEGIN:VCALENDAR',
@@ -123,6 +126,7 @@ def build_calendar_ics(appointment_id: str) -> str | None:
f'URL:{_escape_ics_text(link)}',
f'DTSTART;VALUE=DATE:{_format_ics_date(start_date)}',
f'DTEND;VALUE=DATE:{_format_ics_date(end_date + timedelta(days=1))}',
f'Titel:{_escape_ics_text(titel)}',
'END:VEVENT',
'END:VCALENDAR',
'',
@@ -159,7 +163,7 @@ def build_client_slot_ics(appointment_id: str, slot_start: str, client_name: str
if tenant_id:
link += f'?tenant={tenant_id}'
title_name = str(client_name or '').strip() or 'Termin'
title_name = item.get('title') or f"Terminbuchung mit {client_name}" if client_name else "Terminbuchung"
summary = f"{title_name} - Terminbuchung"
description_lines = [
f"Buchungslink: {link}",
@@ -185,6 +189,7 @@ def build_client_slot_ics(appointment_id: str, slot_start: str, client_name: str
f'URL:{_escape_ics_text(link)}',
f'DTSTART:{dt_start}',
f'DTEND:{dt_end}',
f'Titel:{_escape_ics_text(summary)}',
'END:VEVENT',
'END:VCALENDAR',
'',
@@ -192,7 +197,7 @@ def build_client_slot_ics(appointment_id: str, slot_start: str, client_name: str
return '\r\n'.join(ics_lines)
def new(date_start: str, date_end: str, time_span: list, slots: int, slot_lenght: int, user: str, mail: list=[], note:str="", calendar_enabled: bool=False) -> dict:
def new(date_start: str, date_end: str, time_span: list, slots: int, slot_lenght: int, user: str, mail: list=[], note:str="", calendar_enabled: bool=False, title: str="") -> dict:
"""
Generates a link for the executive to send to his clients to book a time Slot
@@ -208,7 +213,7 @@ def new(date_start: str, date_end: str, time_span: list, slots: int, slot_lenght
"""
normalized_time_span = _normalize_time_span(time_span)
normalized_mail = _normalize_mail_list(mail)
id = termin.add(date_start, date_end, normalized_time_span, slots, slot_lenght, user, normalized_mail, note, calendar_enabled=calendar_enabled)
id = termin.add(date_start, date_end, normalized_time_span, slots, slot_lenght, user, normalized_mail, note, calendar_enabled=calendar_enabled, title=title)
id_str = str(id)
tenant_id = _current_tenant_id()
@@ -219,7 +224,7 @@ def new(date_start: str, date_end: str, time_span: list, slots: int, slot_lenght
link = host + "/terminplaner/client/" + id_str
if tenant_id:
link += f"?tenant={tenant_id}"
subject = f"Terminanfrage von {user}"
subject = f"{title} - Bitte Termin vereinbaren" if title else f"Terminanfrage von {user} - Bitte Termin vereinbaren"
note_link = note + f"Bitte klicken sie auf den folgenden Link um einen Termin zu vereinbaren: {link}"
calendar_link = None
if calendar_enabled:
@@ -459,6 +464,7 @@ def get_user_upcoming_events(user: str, limit: int = 25) -> list[dict]:
'note': str(item.get('note') or ''),
'link': link,
'calendar_link': calendar_link if item.get('calendar_enabled') else None,
'title': str(item.get('title') or ''),
}
)
+11 -4
View File
@@ -173,8 +173,9 @@ def configure():
mail = request.form.get('mail', '')
note = request.form.get('note', '')
add_to_calendar = request.form.get('add_to_calendar') == 'on'
titel = request.form.get('titel', '').strip()
if not start or not end or not time or not slots_amount or not slot_lenght:
if not start or not end or not time or not slots_amount or not slot_lenght or not titel:
flash('Bitte alle Pflichtfelder ausfüllen.', 'error')
return render_template(
'termin_configure.html',
@@ -183,7 +184,7 @@ def configure():
email_service_enabled=cfg.EMAIL_ENABLED,
)
result = appointment_service.new(start, end, time, slots_amount, slot_lenght, session["username"], mail, note, calendar_enabled=add_to_calendar)
result = appointment_service.new(start, end, time, slots_amount, slot_lenght, session["username"], mail, note, calendar_enabled=add_to_calendar, title=titel)
flash('Der Terminplan wurde angelegt.', 'success')
return render_template(
'termin_configure.html',
@@ -192,6 +193,7 @@ def configure():
calendar_link=result.get('calendar_link'),
add_to_calendar=add_to_calendar,
email_service_enabled=cfg.EMAIL_ENABLED,
title=titel,
)
elif request.method == "GET":
return render_template(
@@ -201,6 +203,7 @@ def configure():
calendar_link=None,
add_to_calendar=False,
email_service_enabled=cfg.EMAIL_ENABLED,
title=None,
)
@@ -214,8 +217,10 @@ def calendar_export(appointment_id):
if not ics_content:
return _appointment_not_found_response()
title = str(request.args.get('title', '') or '').strip() or appointment_id
response = Response(ics_content, mimetype='text/calendar; charset=utf-8')
response.headers['Content-Disposition'] = f'attachment; filename=terminplan-{appointment_id}.ics'
response.headers['Content-Disposition'] = f'attachment; filename=terminplan-{title}-{appointment_id}.ics'
return response
@@ -231,8 +236,10 @@ def client_slot_calendar_export(appointment_id):
if not ics_content:
return _appointment_not_found_response()
title = str(request.args.get('title', '') or '').strip() or appointment_id
response = Response(ics_content, mimetype='text/calendar; charset=utf-8')
response.headers['Content-Disposition'] = f'attachment; filename=termin-{appointment_id}-{slot_start.replace(" ", "_").replace(":", "")}.ics'
response.headers['Content-Disposition'] = f'attachment; filename=termin-{title}-{appointment_id}-{slot_start.replace(" ", "_").replace(":", "")}.ics'
return response
@appoint_bp.route('/')
-5
View File
@@ -1295,11 +1295,6 @@
</li>
{% endif %}
{% if 'username' in session %}
{% if current_permissions.pages.get('my_borrowed_items', True) %}
<li class="nav-item">
<a class="nav-link quick-link-pill {% if current_path == url_for('my_borrowed_items') %}nav-active{% endif %}" href="{{ url_for('my_borrowed_items') }}" data-tutorial-tip="Hier sehen Sie Ihre bibliotheksbezogenen Ausleihen.">Meine Medien</a>
</li>
{% endif %}
{% if current_permissions.pages.get('tutorial_page', True) %}
<li class="nav-item">
<a class="nav-link quick-link-pill {% if current_path == url_for('tutorial_page') %}nav-active{% endif %}" href="{{ url_for('tutorial_page') }}" data-tutorial-tip="Nutzen Sie das Tutorial, um die Bibliotheksfunktionen kennenzulernen.">Tutorial</a>
+121 -36
View File
@@ -431,7 +431,7 @@
type="text"
id="librarySearch"
class="library-search-input"
placeholder="Nach Titel, Autor, ISBN suchen..."
placeholder="Nach Titel, ISBN suchen..."
>
<button
id="filterToggleBtn"
@@ -512,7 +512,6 @@
<thead>
<tr>
<th style="width: 24%;">Titel</th>
<th style="width: 14%;">Autor/Künstler</th>
<th style="width: 12%;">ISBN/Code</th>
<th style="width: 8%;">Typ</th>
<th style="width: 8%;">Anzahl</th>
@@ -1058,7 +1057,7 @@
document.getElementById('detailModal').style.display = 'none';
}
// =========================================================================
// =========================================================================
// 5. EVENT LISTENERS INITIALIZATION & ON-LOAD INITIALIZER
// =========================================================================
function wireScannerUi() {
@@ -1094,43 +1093,129 @@
}
}
// Connect standard Filters and Search inputs
document.getElementById('filterToggleBtn').addEventListener('click', () => {
filterPanelOpen = !filterPanelOpen;
document.getElementById('filterPanel').classList.toggle('open');
document.getElementById('filterToggleBtn').classList.toggle('active');
});
document.getElementById('applyFilterBtn').addEventListener('click', () => {
activeFilters.isbn = document.getElementById('filterISBN').value.toLowerCase();
activeFilters.type = document.getElementById('filterType').value;
activeFilters.status = document.getElementById('filterStatus').value;
applyFiltersAndSearch(true);
});
document.getElementById('clearFilterBtn').addEventListener('click', () => {
document.getElementById('filterISBN').value = '';
document.getElementById('filterType').value = '';
document.getElementById('filterStatus').value = '';
activeFilters = { isbn: '', type: '', status: '' };
applyFiltersAndSearch(true);
});
document.getElementById('librarySearch').addEventListener('input', (e) => {
currentSearchTerm = (e.target.value || '').toLowerCase();
applyFiltersAndSearch(true);
});
document.getElementById('loadMoreBtn').addEventListener('click', () => {
renderedCount = Math.min(renderedCount + RENDER_BATCH_COUNT, visibleItems.length);
renderItems();
});
// Run when DOM structure is entirely ready
document.addEventListener('DOMContentLoaded', () => {
wireScannerUi(); // Setup scanner control buttons
loadLibraryItems(); // Fetch your database items right away!
// Safely connect standard Filters and Search inputs inside DOMContentLoaded
const filterToggleBtn = document.getElementById('filterToggleBtn');
if (filterToggleBtn) {
filterToggleBtn.addEventListener('click', () => {
filterPanelOpen = !filterPanelOpen;
document.getElementById('filterPanel').classList.toggle('open');
filterToggleBtn.classList.toggle('active');
});
}
const applyFilterBtn = document.getElementById('applyFilterBtn');
if (applyFilterBtn) {
applyFilterBtn.addEventListener('click', () => {
activeFilters.isbn = document.getElementById('filterISBN').value.toLowerCase();
activeFilters.type = document.getElementById('filterType').value;
activeFilters.status = document.getElementById('filterStatus').value;
applyFiltersAndSearch(true);
});
}
const clearFilterBtn = document.getElementById('clearFilterBtn');
if (clearFilterBtn) {
clearFilterBtn.addEventListener('click', () => {
document.getElementById('filterISBN').value = '';
document.getElementById('filterType').value = '';
document.getElementById('filterStatus').value = '';
activeFilters = { isbn: '', type: '', status: '' };
applyFiltersAndSearch(true);
});
}
const librarySearch = document.getElementById('librarySearch');
if (librarySearch) {
librarySearch.addEventListener('input', (e) => {
currentSearchTerm = (e.target.value || '').toLowerCase();
applyFiltersAndSearch(true);
});
}
const loadMoreBtn = document.getElementById('loadMoreBtn');
if (loadMoreBtn) {
loadMoreBtn.addEventListener('click', () => {
renderedCount = Math.min(renderedCount + RENDER_BATCH_COUNT, visibleItems.length);
renderItems();
});
}
// Edit Modal Form processing
const editForm = document.getElementById('editLibraryForm');
if (editForm) {
editForm.addEventListener('submit', async function(e) {
e.preventDefault();
const itemId = document.getElementById('editLibraryItemId').value;
const updatedData = {
name: document.getElementById('editLibraryName').value,
item_type: document.getElementById('editLibraryType').value,
isbn: document.getElementById('editLibraryIsbn').value,
code_4: document.getElementById('editLibraryCode4').value,
ort: document.getElementById('editLibraryLocation').value,
beschreibung: document.getElementById('editLibraryDescription').value
};
try {
const response = await fetch(`/api/library_item/${itemId}/update`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token }}',
'X-CSRF-Token': '{{ csrf_token }}'
},
body: JSON.stringify(updatedData)
});
const result = await response.json();
if (response.ok && result.ok) {
alert(result.message || 'Medium erfolgreich aktualisiert!');
closeEditLibraryModal();
pagingState.loading = false;
loadLibraryItems();
} else {
alert(result.message || 'Fehler beim Speichern der Änderungen.');
}
} catch (error) {
console.error('Update failed:', error);
alert('Netzwerkfehler beim Aktualisieren des Mediums.');
}
});
}
});
// Modal Control Functions (accessible globally from table buttons)
function openEditLibraryItem(itemId) {
const item = libraryItems.find(i => i._id === itemId);
if (!item) {
alert('Fehler: Das Medium konnte nicht gefunden werden.');
return;
}
// Populate the form fields - Using correct German properties from MongoDB fields
document.getElementById('editLibraryItemId').value = item._id || '';
document.getElementById('editLibraryName').value = item.Name || '';
document.getElementById('editLibraryType').value = item.ItemType || 'book';
document.getElementById('editLibraryIsbn').value = item.ISBN || '';
document.getElementById('editLibraryCode4').value = item.Code_4 || item.Code4 || '';
document.getElementById('editLibraryLocation').value = item.Ort || '';
document.getElementById('editLibraryDescription').value = item.Beschreibung || '';
document.getElementById('editLibraryModal').style.display = 'flex';
}
function closeEditLibraryModal() {
document.getElementById('editLibraryModal').style.display = 'none';
}
</script>
<div id="editLibraryModal" class="modal" style="display:none;">
@@ -1178,4 +1263,4 @@
</form>
</div>
</div>
{% endblock %}
{% endblock %}
+4 -4
View File
@@ -373,7 +373,7 @@
<div class="filter-group">
<div class="filter-header">
<label>Thema:</label>
<label>Schlagwort:</label>
<button type="button" class="filter-toggle" onclick="toggleFilterDropdown('filter3-dropdown')"></button>
<button type="button" class="clear-filter" onclick="clearFilter(3)">Clear</button>
</div>
@@ -420,7 +420,7 @@
<span>Ort</span>
<span>Unterrichtsfach</span>
<span>Jahrgangsstufe</span>
<span>Thema</span>
<span>Schlagwort</span>
<span>Barcode</span>
</div>
<div id="items-container" class="items-container">
@@ -1057,7 +1057,7 @@
<p class="item-col-location"><strong>Ort:</strong> ${item.Ort || '-'}</p>
<p class="item-col-filter1"><strong>Unterrichtsfach:</strong> ${filter1Display}${filter1More}</p>
<p class="item-col-filter2"><strong>Jahrgangsstufe:</strong> ${filter2Display}${filter2More}</p>
<p class="item-col-filter3"><strong>Thema:</strong> ${filter3Display}${filter3More}</p>
<p class="item-col-filter3"><strong>Schlagwort:</strong> ${filter3Display}${filter3More}</p>
<p class="item-col-code"><strong>Barcode:</strong> ${item.Code_4 || '-'}</p>
<p class="item-col-count"><strong>Anzahl:</strong> ${groupedCount}</p>
${isGroupedItem ? `<p class="item-col-count"><strong>Verfügbar:</strong> ${availableGroupedCount}</p>` : ''}
@@ -1682,7 +1682,7 @@
</div>
<div class="detail-group">
<div class="detail-label">Thema:</div>
<div class="detail-label">Schlagwort:</div>
<div class="detail-value">${filter3Array.join(', ') || '-'}</div>
</div>
+6 -6
View File
@@ -2395,7 +2395,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
<div class="filter-group">
<div class="filter-header">
<label>Thema:</label>
<label>Schlagwort:</label>
<button type="button" class="filter-toggle" onclick="toggleFilterDropdown('filter3-dropdown')"></button>
<button type="button" class="clear-filter" onclick="clearFilter(3)">Clear</button>
</div>
@@ -2446,7 +2446,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
<span>Ort</span>
<span>Unterrichtsfach</span>
<span>Jahrgangsstufe</span>
<span>Thema</span>
<span>Schlagwort</span>
<span>Barcode</span>
<span>Anzahl</span>
</div>
@@ -2567,7 +2567,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
</div>
</div>
<h3>Thema:</h3>
<h3>Schlagwort:</h3>
<div class="multi-filter">
<div class="form-group">
<label for="edit-filter3-1">Wert 1:</label>
@@ -3686,7 +3686,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
<p class="item-col-location"><strong>Ort:</strong> ${item.Ort || '-'}</p>
<p class="item-col-filter1"><strong>Unterrichtsfach:</strong> ${filter1Display}${filter1More}</p>
<p class="item-col-filter2"><strong>Jahrgangsstufe:</strong> ${filter2Display}${filter2More}</p>
<p class="item-col-filter3"><strong>Thema:</strong> ${filter3Display}${filter3More}</p>
<p class="item-col-filter3"><strong>Schlagwort:</strong> ${filter3Display}${filter3More}</p>
<p class="item-col-code"><strong>Barcode:</strong> ${item.Code_4 || '-'}</p>
<p class="item-col-count"><strong>Anzahl:</strong> ${groupedCount}</p>
${hasDamage ? `<div class="damage-badge">${damageCount > 0 ? `Schäden gemeldet: ${damageCount}` : 'Schäden gemeldet'}</div>` : ''}
@@ -4229,7 +4229,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
document.getElementById('edit-filter2-3').value = filter2Array[2] || '';
document.getElementById('edit-filter2-4').value = filter2Array[3] || '';
// Fill in filter 3 (Thema)
// Fill in filter 3 (Schlagwort)
const filter3Array = Array.isArray(item.Filter3) ? item.Filter3 : (item.Filter3 ? [item.Filter3] : []);
document.getElementById('edit-filter3-1').value = filter3Array[0] || '';
document.getElementById('edit-filter3-2').value = filter3Array[1] || '';
@@ -4543,7 +4543,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
</div>
<div class="detail-group">
<div class="detail-label">Thema:</div>
<div class="detail-label">Schlagwort:</div>
<div class="detail-value">${filter3Array.join(', ') || '-'}</div>
</div>
+13 -9
View File
@@ -14,6 +14,8 @@
<div class="card-body p-4 p-md-5 bg-white">
<form method="post" action="{{ url_for('terminplaner.configure') }}" class="vstack gap-3">
<div class="row g-3">
<labal for="title" class="form-label fw-semibold">Titel des Terminplans</label>
<input type="text" id="title" name="title" class="form-control form-control-lg" placeholder="z.B. Elternsprechtag Klasse 10a" value="{{ title or '' }}" required>
<div class="col-12 col-md-6">
<label for="start_date" class="form-label fw-semibold">Startdatum</label>
<input type="date" id="start_date" name="start_date" class="form-control form-control-lg" required>
@@ -64,16 +66,18 @@
</div>
</div>
<div>
<label for="mail" class="form-label fw-semibold">Empfänger-Mails</label>
<input type="text" id="mail" name="mail" class="form-control" placeholder="adresse1@schule.de, adresse2@schule.de">
</div>
<div>
<label for="note" class="form-label fw-semibold">Notiz</label>
<textarea id="note" name="note" class="form-control" rows="4" placeholder="Optionaler Einführungstext für die Mail"></textarea>
</div>
{% if mail_service_enabled %}
<div>
<label for="mail" class="form-label fw-semibold">Empfänger-Mails</label>
<input type="text" id="mail" name="mail" class="form-control" placeholder="adresse1@schule.de, adresse2@schule.de">
</div>
<div>
<label for="note" class="form-label fw-semibold">Notiz</label>
<textarea id="note" name="note" class="form-control" rows="4" placeholder="Optionaler Einführungstext für die Mail"></textarea>
</div>
{% endif %}
<div class="form-check">
<input class="form-check-input" type="checkbox" id="add_to_calendar" name="add_to_calendar" {% if add_to_calendar %}checked{% endif %}>
<label class="form-check-label fw-semibold" for="add_to_calendar">Kalendereintrag für den Nutzer erzeugen</label>
-6
View File
@@ -26,12 +26,6 @@
</div>
<button id="new-booking" class="primary-button">Zur Termin-Konfiguration</button>
</div>
<div class="calendar-legend">
<span class="legend-item"><span class="legend-color current"></span> Aktuelle Termine</span>
<span class="legend-item"><span class="legend-color planned"></span> Geplante Termine</span>
<span class="legend-item"><span class="legend-color completed"></span> Abgeschlossene Termine</span>
<span class="legend-item"><span class="legend-color your-bookings"></span> Ihre Termine</span>
</div>
</div>
<div class="calendar-options">
+1
View File
@@ -71,6 +71,7 @@
<div class="border rounded-3 p-3">
<div class="d-flex flex-column flex-lg-row justify-content-between gap-3">
<div>
<h3 class="h6 fw-bold mb-1">{{ event.title or "Terminplan" }}</h3>
<div class="fw-semibold">{{ event.date_start }} bis {{ event.date_end }}</div>
<div class="text-muted small">ID: {{ event.appointment_id }}</div>
{% if event.time_span %}
+238 -71
View File
@@ -155,6 +155,27 @@
background-color: #1e7e34;
}
/* Quagga Scanner Video Styles */
#code4-scanner video, #code4-scanner canvas,
#isbn-scanner video, #isbn-scanner canvas {
width: 100%;
max-width: 640px;
height: auto;
border-radius: 5px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
#code4-scanner canvas, #isbn-scanner canvas {
position: absolute;
top: 0;
left: 0;
}
#code4-scanner, #isbn-scanner {
position: relative;
overflow: hidden;
}
/* ISBN input group styling */
.isbn-input-group {
display: flex;
@@ -831,7 +852,7 @@
<!-- Options will be loaded by JavaScript -->
</select>
</div>
<div class="form-group">
<div class="formupload_admin-group">
<label for="filter2-4">Wert 4:</label>
<select id="filter2-4" name="filter2" class="filter-dropdown-select">
<option value="">-- Optional --</option>
@@ -840,7 +861,7 @@
</div>
</div>
<h3>Thema:</h3>
<h3>Schlagwort:</h3>
<div class="multi-filter">
<div class="form-group">
<label for="filter3-1">Wert 1:</label>
@@ -869,25 +890,41 @@
<label for="anschaffungskosten">Anschaffungskosten (€)</label>
<input id="anschaffungskosten" name="anschaffungskosten">
</div>
<div class="form-group">
<label for="code_4">Code</label>
<div style="display:flex; gap:8px; flex-wrap:wrap; align-items:center;">
<input id="code_4" name="code_4">
<button type="button" id="scan-code4-btn" class="fetch-isbn-button">Barcode scannen</button>
<div class="form-group">
<label for="scan_mode">Erfassungs-Modus:</label>
<select id="scan_mode" name="scan_mode" class="form-control" onchange="toggleScanMode()">
<option value="single">Einzel-Code (Scanner stoppt nach 1 Scan)</option>
<option value="continuous">Fortlaufend scannen (Mehrere Codes nacheinander)</option>
<option value="range">Code-Bereich generieren (z.B. ABC-001 bis ABC-010)</option>
</select>
</div>
<div id="range_generator_group" class="form-group" style="display:none; background: #f9f9f9; padding: 15px; border-radius: 5px; border: 1px solid #ddd; margin-bottom: 15px;">
<label style="font-weight: bold;">Code-Bereich automatisch generieren:</label>
<div style="display: flex; gap: 10px; margin-bottom: 10px; align-items: center;">
<input type="text" id="range_prefix" placeholder="Präfix (z.B. IT-)" class="form-control" style="flex: 2;">
<input type="number" id="range_start" placeholder="Start (z.B. 1)" class="form-control" style="flex: 1;">
<span style="font-weight: bold;">bis</span>
<input type="text" id="range_end" placeholder="Ende (z.B. 020)" class="form-control" style="flex: 1;">
</div>
<small style="display:block; color:#666;">Einzelcode manuell setzen oder per Scanner erfassen.</small>
<div id="code4-scanner" style="width:100%; max-width:520px; display:none; margin-top:10px;"></div>
<small id="code4-scan-status" style="display:block; color:#666; margin-top:6px;"></small>
<small style="display:block; color:#666; margin-bottom: 10px;">Tipp: Die Anzahl der Ziffern im "Ende"-Feld bestimmt die führenden Nullen (z.B. Ende "050" macht aus Start "1" einen "001").</small>
<button type="button" class="btn btn-secondary" onclick="generateCodeRange()">Bereich generieren & einfügen</button>
</div>
<div class="form-group">
<label for="item_count">Anzahl dieser Sorte</label>
<input type="number" id="item_count" name="item_count" min="1" max="100" value="1">
<small style="display:block; color:#666;">Es werden eigene Items erstellt, aber in der Übersicht als ein Artikel gebündelt angezeigt.</small>
<div class="form-group" id="primary_code_group">
<label for="code_4">Basis-Code</label>
<div style="display: flex; gap: 10px;">
<input type="text" id="code_4" name="code_4" class="form-control" placeholder="Haupt-Barcode" required>
<button type="button" id="scan-code4-btn" class="btn btn-primary">Barcode scannen</button>
</div>
<div id="code4-scanner" style="display:none; margin-top: 10px;"></div>
<small id="code4-scan-status" class="form-text text-muted"></small>
</div>
<div class="form-group">
<label for="individual_codes">Einzelcodes (optional, je Zeile ein Code)</label>
<textarea id="individual_codes" name="individual_codes" rows="4" placeholder="z.B.\nABC-001\nABC-002"></textarea>
<small style="display:block; color:#666;">Bei Anzahl > 1 können hier individuelle Codes pro Item gesetzt werden. Der Scanner setzt immer zuerst den Basis-Code im Feld oben; weitere Einzelcodes können hier bei Bedarf manuell ergänzt werden.</small>
<div class="form-group" id="individual_codes_group">
<label for="individual_codes">Weitere Einzelcodes (je Zeile ein Code)</label>
<textarea id="individual_codes" name="individual_codes" rows="6" placeholder="z.B.&#10;ABC-001&#10;ABC-002" class="form-control"></textarea>
<small style="display:block; color:#666; margin-top: 5px;">Bei Anzahl > 1 können hier individuelle Codes pro Item gesetzt werden. Der Scanner setzt immer zuerst den Basis-Code im Feld oben; weitere Einzelcodes werden hier angehängt.</small>
</div>
<!-- Image upload (hidden for library mode) -->
<div class="form-group" {% if show_library_features %}style="display:none;"{% endif %}>
@@ -987,8 +1024,8 @@
// Quagga2 engine state management properties
let scannerRunning = false;
let activeScannerCallback = null; // Dynamically handles routing data to the active caller field
let code4LastScanned = '';
let code4LastScannedAt = 0;
var code4LastScanned = '';
var code4LastScannedAt = 0;
function setCode4ScanStatus(message, isError = false) {
const statusEl = document.getElementById('code4-scan-status');
@@ -1087,7 +1124,7 @@
// Unified tracking capture listener
Quagga.onDetected(function(data) {
if (!data || !data.codeResult || !data.codeResult.code) return;
const rawBarcode = String(data.codeResult.code || '').trim();
const currentTargetCallback = activeScannerCallback;
@@ -1096,59 +1133,175 @@
}
});
// Globale Variablen für den Scanner-Status
var code4LastScanned = '';
var code4LastScannedAt = 0;
/**
* Schaltet das UI basierend auf dem ausgewählten Modus um
*/
function toggleScanMode() {
const mode = document.getElementById('scan_mode').value;
const rangeGroup = document.getElementById('range_generator_group');
const scanButton = document.getElementById('scan-code4-btn');
const scannerBox = document.getElementById('code4-scanner');
if (scannerBox && scannerBox.style.display !== 'none') {
if (typeof killScannerHardware === 'function') killScannerHardware();
scannerBox.style.display = 'none';
if (scanButton) scanButton.textContent = 'Barcode scannen';
}
if (mode === 'range') {
if(rangeGroup) rangeGroup.style.display = 'block';
if(scanButton) scanButton.style.display = 'none';
} else {
if(rangeGroup) rangeGroup.style.display = 'none'; // Hier wurde der Fehler korrigiert
if(scanButton) scanButton.style.display = 'inline-block';
}
}
/**
* Generiert eine Liste von Codes basierend auf den Eingaben
*/
function generateCodeRange() {
const prefix = document.getElementById('range_prefix').value.trim();
const startStr = document.getElementById('range_start').value.trim();
const endStr = document.getElementById('range_end').value.trim();
const start = parseInt(startStr, 10);
const end = parseInt(endStr, 10);
const paddingLength = endStr.length;
if (isNaN(start) || isNaN(end) || start > end) {
alert("Bitte gültige Zahlen für Start und Ende eingeben. Der Startwert darf nicht größer als der Endwert sein.");
return;
}
let generatedCodes = [];
for (let i = start; i <= end; i++) {
let numStr = i.toString().padStart(paddingLength, '0');
generatedCodes.push(`${prefix}${numStr}`);
}
const codeField = document.getElementById('code_4');
const individualCodesArea = document.getElementById('individual_codes');
if (generatedCodes.length > 0) {
if (!codeField.value || confirm("Soll der aktuelle Basis-Code überschrieben werden?")) {
codeField.value = generatedCodes[0];
if (typeof validateCodeField === 'function') validateCodeField(codeField);
generatedCodes.shift();
}
if (generatedCodes.length > 0) {
let existing = individualCodesArea.value.trim();
individualCodesArea.value = existing ? existing + '\n' + generatedCodes.join('\n') : generatedCodes.join('\n');
}
alert(`Bereich erfolgreich generiert! (${end - start + 1} Codes)`);
}
}
/**
* Scanner-Funktion (Single & Continuous Modus)
*/
function startCode4Scanner() {
const scannerBox = document.getElementById('code4-scanner');
const scanButton = document.getElementById('scan-code4-btn');
const codeField = document.getElementById('code_4');
const individualCodesArea = document.getElementById('individual_codes');
const scanMode = document.getElementById('scan_mode').value;
if (!scannerBox || !scanButton || !codeField) return;
// Automatically clean up running ISBN scanner to prevent track multi-binding
// Beende evtl. laufenden ISBN Scanner
const isbnScannerBox = document.getElementById('isbn-scanner');
const isbnScanButton = document.getElementById('scan-isbn-btn');
if (isbnScannerBox && isbnScannerBox.style.display !== 'none') {
killScannerHardware();
if (isbnScannerBox) isbnScannerBox.style.display = 'none';
if (isbnScanButton) isbnScanButton.textContent = 'ISBN scannen';
setIsbnScanStatus('Scanner gestoppt.');
isbnScannerBox.style.display = 'none';
const isbnScanButton = document.getElementById('scan-isbn-btn');
if (isbnScanButton) isbnScanButton.textContent = 'Barcode scannen';
if (typeof setIsbnScanStatus === 'function') setIsbnScanStatus('Scanner gestoppt.');
}
// Scanner ausschalten, wenn er bereits läuft
if (scannerBox.style.display !== 'none') {
killScannerHardware();
scannerBox.style.display = 'none';
scanButton.textContent = 'Barcode scannen';
setCode4ScanStatus('Scanner gestoppt.');
if (typeof setCode4ScanStatus === 'function') setCode4ScanStatus('Scanner gestoppt.');
return;
}
// Scanner starten
scannerBox.style.display = 'block';
scanButton.textContent = 'Scanner stoppen';
scanButton.textContent = scanMode === 'continuous' ? 'Fortlaufenden Scan stoppen' : 'Scanner stoppen';
if (typeof setCode4ScanStatus === 'function') setCode4ScanStatus('Scanner initialisiert. Richte die Kamera auf den Barcode...');
runEngineInitialization(
'#code4-scanner',
function(decodedText) {
const now = Date.now();
if (decodedText === code4LastScanned && (now - code4LastScannedAt) < 1500) {
return;
}
code4LastScanned = decodedText;
code4LastScannedAt = now;
if (typeof runEngineInitialization === 'function') {
runEngineInitialization(
'#code4-scanner',
function(decodedText) {
const now = Date.now();
if (decodedText === code4LastScanned && (now - code4LastScannedAt) < 1500) {
return;
}
code4LastScanned = decodedText;
code4LastScannedAt = now;
if (scanMode === 'single') {
killScannerHardware();
scannerBox.style.display = 'none';
scanButton.textContent = 'Barcode scannen';
codeField.value = decodedText;
if (typeof validateCodeField === 'function') validateCodeField(codeField);
if (typeof setCode4ScanStatus === 'function') setCode4ScanStatus(`Code gesetzt: ${decodedText}`);
killScannerHardware();
scannerBox.style.display = 'none';
scanButton.textContent = 'Barcode scannen';
} else if (scanMode === 'continuous') {
let currentAreaCodes = individualCodesArea.value.split('\n').map(c => c.trim()).filter(c => c !== '');
codeField.value = decodedText;
validateCodeField(codeField);
setCode4ScanStatus(`Code_4 gesetzt: ${decodedText}`);
},
'Scanner läuft. Der Scan wird im Basis-Codefeld übernommen.',
setCode4ScanStatus
);
if (!codeField.value) {
codeField.value = decodedText;
if (typeof validateCodeField === 'function') validateCodeField(codeField);
if (typeof setCode4ScanStatus === 'function') setCode4ScanStatus(`Basis-Code gesetzt: ${decodedText}`);
} else if (!currentAreaCodes.includes(decodedText) && codeField.value !== decodedText) {
individualCodesArea.value += (individualCodesArea.value ? '\n' : '') + decodedText;
if (typeof setCode4ScanStatus === 'function') setCode4ScanStatus(`Zusatz-Code erfasst: ${decodedText}`);
try {
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioCtx.createOscillator();
const gainNode = audioCtx.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioCtx.destination);
oscillator.type = 'sine';
oscillator.frequency.value = 800;
gainNode.gain.setValueAtTime(0.1, audioCtx.currentTime);
oscillator.start();
oscillator.stop(audioCtx.currentTime + 0.1);
} catch (e) {
console.log("Audio feedback not supported");
}
} else {
if (typeof setCode4ScanStatus === 'function') setCode4ScanStatus(`Code ${decodedText} wurde bereits gescannt.`);
}
}
},
'Scanner läuft. Richte die Kamera auf den Barcode.',
setCode4ScanStatus
);
}
}
/**
* ISBN Scanner-Funktion
*/
function startIsbnScanner() {
if (!libraryModuleEnabled) {
setIsbnScanStatus('Bibliotheks-Modul ist deaktiviert.', true);
if (typeof setIsbnScanStatus === 'function') setIsbnScanStatus('Bibliotheks-Modul ist deaktiviert.', true);
return;
}
const scannerBox = document.getElementById('isbn-scanner');
@@ -1156,21 +1309,20 @@
const isbnField = document.getElementById('isbn');
if (!scannerBox || !scanButton || !isbnField) return;
// Automatically clean up running Base Code_4 scanner to prevent track multi-binding
const codeScannerBox = document.getElementById('code4-scanner');
const codeScanButton = document.getElementById('scan-code4-btn');
if (codeScannerBox && codeScannerBox.style.display !== 'none') {
killScannerHardware();
if (codeScannerBox) codeScannerBox.style.display = 'none';
if (codeScanButton) codeScanButton.textContent = 'Barcode scannen';
setCode4ScanStatus('Scanner gestoppt.');
codeScannerBox.style.display = 'none';
codeScanButton.textContent = 'Barcode scannen';
if (typeof setCode4ScanStatus === 'function') setCode4ScanStatus('Scanner gestoppt.');
}
if (scannerBox.style.display !== 'none') {
killScannerHardware();
scannerBox.style.display = 'none';
scanButton.textContent = 'ISBN scannen';
setIsbnScanStatus('Scanner gestoppt.');
scanButton.textContent = 'Barcode scannen';
if (typeof setIsbnScanStatus === 'function') setIsbnScanStatus('Scanner gestoppt.');
return;
}
@@ -1182,18 +1334,17 @@
function(decodedText) {
killScannerHardware();
scannerBox.style.display = 'none';
scanButton.textContent = 'ISBN scannen';
scanButton.textContent = 'Barcode scannen';
const normalizedIsbn = normalizeIsbnClient(decodedText);
const normalizedIsbn = typeof normalizeIsbnClient === 'function' ? normalizeIsbnClient(decodedText) : decodedText;
isbnField.value = normalizedIsbn || decodedText;
updateIsbnLiveValidation();
if (typeof updateIsbnLiveValidation === 'function') updateIsbnLiveValidation();
if (normalizedIsbn) {
setIsbnScanStatus(`ISBN gesetzt: ${normalizedIsbn}`);
// Automatically load book metadata after a valid ISBN scan.
fetchBookInfo('upload');
if (typeof setIsbnScanStatus === 'function') setIsbnScanStatus(`ISBN gesetzt: ${normalizedIsbn}`);
if (typeof fetchBookInfo === 'function') fetchBookInfo('upload');
} else {
setIsbnScanStatus('Kein gültiges ISBN-Format erkannt, der gescannte Wert bleibt aber im Feld.', true);
if (typeof setIsbnScanStatus === 'function') setIsbnScanStatus('Kein gültiges ISBN-Format erkannt, der gescannte Wert bleibt aber im Feld.', true);
}
},
'Scanner läuft. Bitte ISBN-Code erfassen.',
@@ -1201,6 +1352,32 @@
);
}
// --- WICHTIG: Event Listener Binden sobald DOM geladen ist ---
document.addEventListener('DOMContentLoaded', function() {
// 1. Basis-Code Scanner Button aktivieren
const scanCode4Btn = document.getElementById('scan-code4-btn');
if (scanCode4Btn) {
scanCode4Btn.addEventListener('click', startCode4Scanner);
}
// 2. ISBN Scanner Button aktivieren
const scanIsbnBtn = document.getElementById('scan-isbn-btn');
if (scanIsbnBtn) {
scanIsbnBtn.addEventListener('click', startIsbnScanner);
}
// 3. Dropdown-Modus Wechsel überwachen
const scanModeSelect = document.getElementById('scan_mode');
if (scanModeSelect) {
scanModeSelect.addEventListener('change', toggleScanMode);
}
// 4. ISBN Live-Validierung
const isbnInput = document.getElementById('isbn');
if (isbnInput && typeof updateIsbnLiveValidation === 'function') {
isbnInput.addEventListener('input', updateIsbnLiveValidation);
}
});
// Load predefined filter values for dropdowns
function loadPredefinedFilterValues(filterNumber) {
fetch(`/get_predefined_filter_values/${filterNumber}`)
@@ -1726,16 +1903,6 @@
});
}
const scanCodeBtn = document.getElementById('scan-code4-btn');
if (scanCodeBtn) {
scanCodeBtn.addEventListener('click', startCode4Scanner);
}
const scanIsbnBtn = document.getElementById('scan-isbn-btn');
if (scanIsbnBtn) {
scanIsbnBtn.addEventListener('click', startIsbnScanner);
}
const isbnField = document.getElementById('isbn');
if (isbnField) {
isbnField.addEventListener('input', updateIsbnLiveValidation);
+1
View File
@@ -213,6 +213,7 @@ existing['modules'] = {
'library': {'enabled': True},
'student_cards': {'enabled': True, 'default_borrow_days': 14, 'max_borrow_days': 365},
'terminplan': {'enabled': True},
'mail': {'enabled': True},
}
existing['trial'] = trial_config