Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a5d7dedea | |||
| 74e11b5370 | |||
| c0d6ad7b72 | |||
| 7da7152332 | |||
| 3c9ab099ba | |||
| a81a8775a5 | |||
| 1633118d7f | |||
| a4b900c986 |
+72
-32
@@ -1268,7 +1268,8 @@ def inject_version():
|
|||||||
'inventory_module_enabled': cfg.MODULES.is_enabled('inventory'),
|
'inventory_module_enabled': cfg.MODULES.is_enabled('inventory'),
|
||||||
'terminplan_module_enabled': cfg.MODULES.is_enabled('terminplan'),
|
'terminplan_module_enabled': cfg.MODULES.is_enabled('terminplan'),
|
||||||
'library_module_enabled': cfg.MODULES.is_enabled('library'),
|
'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,
|
'is_admin': is_admin,
|
||||||
'unread_notification_count': unread_notification_count,
|
'unread_notification_count': unread_notification_count,
|
||||||
'current_permissions': current_permissions,
|
'current_permissions': current_permissions,
|
||||||
@@ -2885,6 +2886,7 @@ def home():
|
|||||||
username=session['username'],
|
username=session['username'],
|
||||||
library_module_enabled=cfg.MODULES.is_enabled('library'),
|
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'),
|
||||||
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')
|
open_item=request.args.get('open_item')
|
||||||
@@ -2927,6 +2929,7 @@ def home_admin():
|
|||||||
username=session['username'],
|
username=session['username'],
|
||||||
library_module_enabled=cfg.MODULES.is_enabled('library'),
|
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'),
|
||||||
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(),
|
||||||
@@ -2947,6 +2950,7 @@ def tutorial_page():
|
|||||||
is_admin=us.check_admin(session['username']),
|
is_admin=us.check_admin(session['username']),
|
||||||
library_module_enabled=cfg.MODULES.is_enabled('library'),
|
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'),
|
||||||
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
|
||||||
)
|
)
|
||||||
@@ -5207,9 +5211,61 @@ def upload_item():
|
|||||||
flash(error_msg, 'error')
|
flash(error_msg, 'error')
|
||||||
return redirect(url_for(success_redirect_endpoint))
|
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]):
|
# 1. PARSE AND UNIFY CODES
|
||||||
existing_item = it._get_items_collection().find_one({"code_4": code_4[0]})
|
# -------------------------------------------------------------------
|
||||||
|
# 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:
|
||||||
|
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:
|
if existing_item:
|
||||||
error_msg = 'Der Code wird bereits verwendet. Umleitung zum Eintrag.'
|
error_msg = 'Der Code wird bereits verwendet. Umleitung zum Eintrag.'
|
||||||
if is_mobile:
|
if is_mobile:
|
||||||
@@ -5222,40 +5278,20 @@ def upload_item():
|
|||||||
|
|
||||||
flash(error_msg, 'info')
|
flash(error_msg, 'info')
|
||||||
return redirect(url_for(success_redirect_endpoint, open_item=str(existing_item['_id'])))
|
return redirect(url_for(success_redirect_endpoint, open_item=str(existing_item['_id'])))
|
||||||
else:
|
|
||||||
error_msg = 'Der Code wird bereits verwendet. Bitte wählen Sie einen anderen Code.'
|
# 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:
|
if is_mobile:
|
||||||
return jsonify({'success': False, 'message': error_msg}), 400
|
return jsonify({'success': False, 'message': error_msg}), 400
|
||||||
else:
|
else:
|
||||||
flash(error_msg, 'error')
|
flash(error_msg, 'error')
|
||||||
return redirect(url_for(success_redirect_endpoint))
|
return redirect(url_for(success_redirect_endpoint))
|
||||||
|
|
||||||
# Validate optional per-item codes
|
# -------------------------------------------------------------------
|
||||||
if individual_codes:
|
# 5. BATCH CODE GENERATOR (Remains mostly unchanged)
|
||||||
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))
|
|
||||||
|
|
||||||
def generate_unique_batch_code(base_code, position):
|
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:
|
if not base_code:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -7948,7 +7984,8 @@ def admin_borrowings():
|
|||||||
'admin_borrowings.html',
|
'admin_borrowings.html',
|
||||||
entries=entries,
|
entries=entries,
|
||||||
library_module_enabled=cfg.MODULES.is_enabled('library'),
|
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-------------------------------------------------------"""
|
"""-----------------------------------------------------------Audit Routes-------------------------------------------------------"""
|
||||||
@@ -8011,6 +8048,7 @@ def admin_audit_dashboard():
|
|||||||
audit_rows=audit_rows,
|
audit_rows=audit_rows,
|
||||||
library_module_enabled=cfg.MODULES.is_enabled('library'),
|
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'),
|
||||||
)
|
)
|
||||||
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}")
|
||||||
@@ -8826,6 +8864,7 @@ def library_item_invoices(item_id):
|
|||||||
invoices=entries,
|
invoices=entries,
|
||||||
library_module_enabled=cfg.MODULES.is_enabled('library'),
|
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')
|
||||||
)
|
)
|
||||||
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}")
|
||||||
@@ -10160,6 +10199,7 @@ def admin_damaged_items():
|
|||||||
damaged_items=damaged_rows,
|
damaged_items=damaged_rows,
|
||||||
library_module_enabled=cfg.MODULES.is_enabled('library'),
|
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')
|
||||||
)
|
)
|
||||||
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}")
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
from modules.module_registry import ModuleRegistry as mr
|
||||||
import smtplib
|
import smtplib
|
||||||
|
|
||||||
import Web.modules.database.settings as cfg
|
import Web.modules.database.settings as cfg
|
||||||
@@ -27,6 +28,9 @@ def send(email: list, subject: str, note: str, sender: str) -> bool:
|
|||||||
Output:
|
Output:
|
||||||
- bool: true if the sending worked and false if it didnt
|
- bool: true if the sending worked and false if it didnt
|
||||||
"""
|
"""
|
||||||
|
if not mr.registry.is_enabled('mail'):
|
||||||
|
return False
|
||||||
|
else:
|
||||||
msg = MIMEMultipart()
|
msg = MIMEMultipart()
|
||||||
msg['Subject'] = subject
|
msg['Subject'] = subject
|
||||||
msg['From'] = sender or cfg.EMAIL_FROM_ADDRESS or cfg.EMAIL_USERNAME
|
msg['From'] = sender or cfg.EMAIL_FROM_ADDRESS or cfg.EMAIL_USERNAME
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ def build_calendar_ics(appointment_id: str) -> str | None:
|
|||||||
time_span = item.get('time_span', []) or []
|
time_span = item.get('time_span', []) or []
|
||||||
creator = item.get('user', 'Terminplaner')
|
creator = item.get('user', 'Terminplaner')
|
||||||
note = item.get('note', '') or ''
|
note = item.get('note', '') or ''
|
||||||
|
titel = item.get('title', '') or ''
|
||||||
tenant_id = _current_tenant_id()
|
tenant_id = _current_tenant_id()
|
||||||
try:
|
try:
|
||||||
link = url_for('terminplaner.client', appointment_id=str(appointment_id), tenant=tenant_id or None, _external=True)
|
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))
|
description_lines.append('Zeitfenster: ' + '; '.join(str(entry) for entry in time_span))
|
||||||
if note:
|
if note:
|
||||||
description_lines.append('Notiz: ' + str(note))
|
description_lines.append('Notiz: ' + str(note))
|
||||||
|
if titel:
|
||||||
|
description_lines.append('Titel: ' + str(titel))
|
||||||
|
|
||||||
ics_lines = [
|
ics_lines = [
|
||||||
'BEGIN:VCALENDAR',
|
'BEGIN:VCALENDAR',
|
||||||
@@ -123,6 +126,7 @@ def build_calendar_ics(appointment_id: str) -> str | None:
|
|||||||
f'URL:{_escape_ics_text(link)}',
|
f'URL:{_escape_ics_text(link)}',
|
||||||
f'DTSTART;VALUE=DATE:{_format_ics_date(start_date)}',
|
f'DTSTART;VALUE=DATE:{_format_ics_date(start_date)}',
|
||||||
f'DTEND;VALUE=DATE:{_format_ics_date(end_date + timedelta(days=1))}',
|
f'DTEND;VALUE=DATE:{_format_ics_date(end_date + timedelta(days=1))}',
|
||||||
|
f'Titel:{_escape_ics_text(titel)}',
|
||||||
'END:VEVENT',
|
'END:VEVENT',
|
||||||
'END:VCALENDAR',
|
'END:VCALENDAR',
|
||||||
'',
|
'',
|
||||||
@@ -159,7 +163,7 @@ def build_client_slot_ics(appointment_id: str, slot_start: str, client_name: str
|
|||||||
if tenant_id:
|
if tenant_id:
|
||||||
link += f'?tenant={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"
|
summary = f"{title_name} - Terminbuchung"
|
||||||
description_lines = [
|
description_lines = [
|
||||||
f"Buchungslink: {link}",
|
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'URL:{_escape_ics_text(link)}',
|
||||||
f'DTSTART:{dt_start}',
|
f'DTSTART:{dt_start}',
|
||||||
f'DTEND:{dt_end}',
|
f'DTEND:{dt_end}',
|
||||||
|
f'Titel:{_escape_ics_text(summary)}',
|
||||||
'END:VEVENT',
|
'END:VEVENT',
|
||||||
'END:VCALENDAR',
|
'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)
|
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
|
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_time_span = _normalize_time_span(time_span)
|
||||||
normalized_mail = _normalize_mail_list(mail)
|
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)
|
id_str = str(id)
|
||||||
tenant_id = _current_tenant_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
|
link = host + "/terminplaner/client/" + id_str
|
||||||
if tenant_id:
|
if tenant_id:
|
||||||
link += f"?tenant={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}"
|
note_link = note + f"Bitte klicken sie auf den folgenden Link um einen Termin zu vereinbaren: {link}"
|
||||||
calendar_link = None
|
calendar_link = None
|
||||||
if calendar_enabled:
|
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 ''),
|
'note': str(item.get('note') or ''),
|
||||||
'link': link,
|
'link': link,
|
||||||
'calendar_link': calendar_link if item.get('calendar_enabled') else None,
|
'calendar_link': calendar_link if item.get('calendar_enabled') else None,
|
||||||
|
'title': str(item.get('title') or ''),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -173,8 +173,9 @@ def configure():
|
|||||||
mail = request.form.get('mail', '')
|
mail = request.form.get('mail', '')
|
||||||
note = request.form.get('note', '')
|
note = request.form.get('note', '')
|
||||||
add_to_calendar = request.form.get('add_to_calendar') == 'on'
|
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')
|
flash('Bitte alle Pflichtfelder ausfüllen.', 'error')
|
||||||
return render_template(
|
return render_template(
|
||||||
'termin_configure.html',
|
'termin_configure.html',
|
||||||
@@ -183,7 +184,7 @@ def configure():
|
|||||||
email_service_enabled=cfg.EMAIL_ENABLED,
|
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')
|
flash('Der Terminplan wurde angelegt.', 'success')
|
||||||
return render_template(
|
return render_template(
|
||||||
'termin_configure.html',
|
'termin_configure.html',
|
||||||
@@ -192,6 +193,7 @@ def configure():
|
|||||||
calendar_link=result.get('calendar_link'),
|
calendar_link=result.get('calendar_link'),
|
||||||
add_to_calendar=add_to_calendar,
|
add_to_calendar=add_to_calendar,
|
||||||
email_service_enabled=cfg.EMAIL_ENABLED,
|
email_service_enabled=cfg.EMAIL_ENABLED,
|
||||||
|
title=titel,
|
||||||
)
|
)
|
||||||
elif request.method == "GET":
|
elif request.method == "GET":
|
||||||
return render_template(
|
return render_template(
|
||||||
@@ -201,6 +203,7 @@ def configure():
|
|||||||
calendar_link=None,
|
calendar_link=None,
|
||||||
add_to_calendar=False,
|
add_to_calendar=False,
|
||||||
email_service_enabled=cfg.EMAIL_ENABLED,
|
email_service_enabled=cfg.EMAIL_ENABLED,
|
||||||
|
title=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -214,8 +217,10 @@ def calendar_export(appointment_id):
|
|||||||
if not ics_content:
|
if not ics_content:
|
||||||
return _appointment_not_found_response()
|
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 = 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
|
return response
|
||||||
|
|
||||||
|
|
||||||
@@ -231,8 +236,10 @@ def client_slot_calendar_export(appointment_id):
|
|||||||
if not ics_content:
|
if not ics_content:
|
||||||
return _appointment_not_found_response()
|
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 = 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
|
return response
|
||||||
|
|
||||||
@appoint_bp.route('/')
|
@appoint_bp.route('/')
|
||||||
|
|||||||
@@ -1210,7 +1210,7 @@
|
|||||||
document.getElementById('editLibraryLocation').value = item.Ort || '';
|
document.getElementById('editLibraryLocation').value = item.Ort || '';
|
||||||
document.getElementById('editLibraryDescription').value = item.Beschreibung || '';
|
document.getElementById('editLibraryDescription').value = item.Beschreibung || '';
|
||||||
|
|
||||||
document.getElementById('editLibraryModal').style.display = 'block';
|
document.getElementById('editLibraryModal').style.display = 'flex';
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeEditLibraryModal() {
|
function closeEditLibraryModal() {
|
||||||
|
|||||||
@@ -373,7 +373,7 @@
|
|||||||
|
|
||||||
<div class="filter-group">
|
<div class="filter-group">
|
||||||
<div class="filter-header">
|
<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="filter-toggle" onclick="toggleFilterDropdown('filter3-dropdown')">▼</button>
|
||||||
<button type="button" class="clear-filter" onclick="clearFilter(3)">Clear</button>
|
<button type="button" class="clear-filter" onclick="clearFilter(3)">Clear</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -420,7 +420,7 @@
|
|||||||
<span>Ort</span>
|
<span>Ort</span>
|
||||||
<span>Unterrichtsfach</span>
|
<span>Unterrichtsfach</span>
|
||||||
<span>Jahrgangsstufe</span>
|
<span>Jahrgangsstufe</span>
|
||||||
<span>Thema</span>
|
<span>Schlagwort</span>
|
||||||
<span>Barcode</span>
|
<span>Barcode</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="items-container" class="items-container">
|
<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-location"><strong>Ort:</strong> ${item.Ort || '-'}</p>
|
||||||
<p class="item-col-filter1"><strong>Unterrichtsfach:</strong> ${filter1Display}${filter1More}</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-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-code"><strong>Barcode:</strong> ${item.Code_4 || '-'}</p>
|
||||||
<p class="item-col-count"><strong>Anzahl:</strong> ${groupedCount}</p>
|
<p class="item-col-count"><strong>Anzahl:</strong> ${groupedCount}</p>
|
||||||
${isGroupedItem ? `<p class="item-col-count"><strong>Verfügbar:</strong> ${availableGroupedCount}</p>` : ''}
|
${isGroupedItem ? `<p class="item-col-count"><strong>Verfügbar:</strong> ${availableGroupedCount}</p>` : ''}
|
||||||
@@ -1682,7 +1682,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="detail-group">
|
<div class="detail-group">
|
||||||
<div class="detail-label">Thema:</div>
|
<div class="detail-label">Schlagwort:</div>
|
||||||
<div class="detail-value">${filter3Array.join(', ') || '-'}</div>
|
<div class="detail-value">${filter3Array.join(', ') || '-'}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2395,7 +2395,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
|||||||
|
|
||||||
<div class="filter-group">
|
<div class="filter-group">
|
||||||
<div class="filter-header">
|
<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="filter-toggle" onclick="toggleFilterDropdown('filter3-dropdown')">▼</button>
|
||||||
<button type="button" class="clear-filter" onclick="clearFilter(3)">Clear</button>
|
<button type="button" class="clear-filter" onclick="clearFilter(3)">Clear</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -2446,7 +2446,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
|||||||
<span>Ort</span>
|
<span>Ort</span>
|
||||||
<span>Unterrichtsfach</span>
|
<span>Unterrichtsfach</span>
|
||||||
<span>Jahrgangsstufe</span>
|
<span>Jahrgangsstufe</span>
|
||||||
<span>Thema</span>
|
<span>Schlagwort</span>
|
||||||
<span>Barcode</span>
|
<span>Barcode</span>
|
||||||
<span>Anzahl</span>
|
<span>Anzahl</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -2567,7 +2567,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3>Thema:</h3>
|
<h3>Schlagwort:</h3>
|
||||||
<div class="multi-filter">
|
<div class="multi-filter">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="edit-filter3-1">Wert 1:</label>
|
<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-location"><strong>Ort:</strong> ${item.Ort || '-'}</p>
|
||||||
<p class="item-col-filter1"><strong>Unterrichtsfach:</strong> ${filter1Display}${filter1More}</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-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-code"><strong>Barcode:</strong> ${item.Code_4 || '-'}</p>
|
||||||
<p class="item-col-count"><strong>Anzahl:</strong> ${groupedCount}</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>` : ''}
|
${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-3').value = filter2Array[2] || '';
|
||||||
document.getElementById('edit-filter2-4').value = filter2Array[3] || '';
|
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] : []);
|
const filter3Array = Array.isArray(item.Filter3) ? item.Filter3 : (item.Filter3 ? [item.Filter3] : []);
|
||||||
document.getElementById('edit-filter3-1').value = filter3Array[0] || '';
|
document.getElementById('edit-filter3-1').value = filter3Array[0] || '';
|
||||||
document.getElementById('edit-filter3-2').value = filter3Array[1] || '';
|
document.getElementById('edit-filter3-2').value = filter3Array[1] || '';
|
||||||
@@ -4543,7 +4543,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="detail-group">
|
<div class="detail-group">
|
||||||
<div class="detail-label">Thema:</div>
|
<div class="detail-label">Schlagwort:</div>
|
||||||
<div class="detail-value">${filter3Array.join(', ') || '-'}</div>
|
<div class="detail-value">${filter3Array.join(', ') || '-'}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,8 @@
|
|||||||
<div class="card-body p-4 p-md-5 bg-white">
|
<div class="card-body p-4 p-md-5 bg-white">
|
||||||
<form method="post" action="{{ url_for('terminplaner.configure') }}" class="vstack gap-3">
|
<form method="post" action="{{ url_for('terminplaner.configure') }}" class="vstack gap-3">
|
||||||
<div class="row g-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">
|
<div class="col-12 col-md-6">
|
||||||
<label for="start_date" class="form-label fw-semibold">Startdatum</label>
|
<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>
|
<input type="date" id="start_date" name="start_date" class="form-control form-control-lg" required>
|
||||||
@@ -64,6 +66,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if mail_service_enabled %}
|
||||||
<div>
|
<div>
|
||||||
<label for="mail" class="form-label fw-semibold">Empfänger-Mails</label>
|
<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">
|
<input type="text" id="mail" name="mail" class="form-control" placeholder="adresse1@schule.de, adresse2@schule.de">
|
||||||
@@ -74,6 +77,7 @@
|
|||||||
<textarea id="note" name="note" class="form-control" rows="4" placeholder="Optionaler Einführungstext für die Mail"></textarea>
|
<textarea id="note" name="note" class="form-control" rows="4" placeholder="Optionaler Einführungstext für die Mail"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
<div class="form-check">
|
<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 %}>
|
<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>
|
<label class="form-check-label fw-semibold" for="add_to_calendar">Kalendereintrag für den Nutzer erzeugen</label>
|
||||||
|
|||||||
@@ -26,12 +26,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<button id="new-booking" class="primary-button">Zur Termin-Konfiguration</button>
|
<button id="new-booking" class="primary-button">Zur Termin-Konfiguration</button>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div class="calendar-options">
|
<div class="calendar-options">
|
||||||
|
|||||||
@@ -71,6 +71,7 @@
|
|||||||
<div class="border rounded-3 p-3">
|
<div class="border rounded-3 p-3">
|
||||||
<div class="d-flex flex-column flex-lg-row justify-content-between gap-3">
|
<div class="d-flex flex-column flex-lg-row justify-content-between gap-3">
|
||||||
<div>
|
<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="fw-semibold">{{ event.date_start }} bis {{ event.date_end }}</div>
|
||||||
<div class="text-muted small">ID: {{ event.appointment_id }}</div>
|
<div class="text-muted small">ID: {{ event.appointment_id }}</div>
|
||||||
{% if event.time_span %}
|
{% if event.time_span %}
|
||||||
|
|||||||
@@ -155,6 +155,27 @@
|
|||||||
background-color: #1e7e34;
|
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 styling */
|
||||||
.isbn-input-group {
|
.isbn-input-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -840,7 +861,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3>Thema:</h3>
|
<h3>Schlagwort:</h3>
|
||||||
<div class="multi-filter">
|
<div class="multi-filter">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="filter3-1">Wert 1:</label>
|
<label for="filter3-1">Wert 1:</label>
|
||||||
@@ -1003,8 +1024,8 @@
|
|||||||
// Quagga2 engine state management properties
|
// Quagga2 engine state management properties
|
||||||
let scannerRunning = false;
|
let scannerRunning = false;
|
||||||
let activeScannerCallback = null; // Dynamically handles routing data to the active caller field
|
let activeScannerCallback = null; // Dynamically handles routing data to the active caller field
|
||||||
let code4LastScanned = '';
|
var code4LastScanned = '';
|
||||||
let code4LastScannedAt = 0;
|
var code4LastScannedAt = 0;
|
||||||
|
|
||||||
function setCode4ScanStatus(message, isError = false) {
|
function setCode4ScanStatus(message, isError = false) {
|
||||||
const statusEl = document.getElementById('code4-scan-status');
|
const statusEl = document.getElementById('code4-scan-status');
|
||||||
@@ -1112,9 +1133,9 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Globale Variablen für den Scanner-Status (falls nicht schon woanders definiert)
|
// Globale Variablen für den Scanner-Status
|
||||||
let code4LastScanned = '';
|
var code4LastScanned = '';
|
||||||
let code4LastScannedAt = 0;
|
var code4LastScannedAt = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schaltet das UI basierend auf dem ausgewählten Modus um
|
* Schaltet das UI basierend auf dem ausgewählten Modus um
|
||||||
@@ -1125,7 +1146,6 @@ function toggleScanMode() {
|
|||||||
const scanButton = document.getElementById('scan-code4-btn');
|
const scanButton = document.getElementById('scan-code4-btn');
|
||||||
const scannerBox = document.getElementById('code4-scanner');
|
const scannerBox = document.getElementById('code4-scanner');
|
||||||
|
|
||||||
// Scanner stoppen, falls der Modus gewechselt wird und er noch läuft
|
|
||||||
if (scannerBox && scannerBox.style.display !== 'none') {
|
if (scannerBox && scannerBox.style.display !== 'none') {
|
||||||
if (typeof killScannerHardware === 'function') killScannerHardware();
|
if (typeof killScannerHardware === 'function') killScannerHardware();
|
||||||
scannerBox.style.display = 'none';
|
scannerBox.style.display = 'none';
|
||||||
@@ -1133,10 +1153,10 @@ function toggleScanMode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mode === 'range') {
|
if (mode === 'range') {
|
||||||
rangeGroup.style.display = 'block';
|
if(rangeGroup) rangeGroup.style.display = 'block';
|
||||||
if(scanButton) scanButton.style.display = 'none'; // Verstecke Scanner im Range-Modus
|
if(scanButton) scanButton.style.display = 'none';
|
||||||
} else {
|
} else {
|
||||||
rangeGroup.style.display = 'none';
|
if(rangeGroup) rangeGroup.style.display = 'none'; // Hier wurde der Fehler korrigiert
|
||||||
if(scanButton) scanButton.style.display = 'inline-block';
|
if(scanButton) scanButton.style.display = 'inline-block';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1152,8 +1172,6 @@ function generateCodeRange() {
|
|||||||
const start = parseInt(startStr, 10);
|
const start = parseInt(startStr, 10);
|
||||||
const end = parseInt(endStr, 10);
|
const end = parseInt(endStr, 10);
|
||||||
|
|
||||||
// Die Länge des End-Wertes bestimmt das Padding (die führenden Nullen)
|
|
||||||
// Beispiel: Ende = "050" -> Länge 3 -> "001", "002", etc.
|
|
||||||
const paddingLength = endStr.length;
|
const paddingLength = endStr.length;
|
||||||
|
|
||||||
if (isNaN(start) || isNaN(end) || start > end) {
|
if (isNaN(start) || isNaN(end) || start > end) {
|
||||||
@@ -1163,7 +1181,6 @@ function generateCodeRange() {
|
|||||||
|
|
||||||
let generatedCodes = [];
|
let generatedCodes = [];
|
||||||
for (let i = start; i <= end; i++) {
|
for (let i = start; i <= end; i++) {
|
||||||
// Nullen auffüllen
|
|
||||||
let numStr = i.toString().padStart(paddingLength, '0');
|
let numStr = i.toString().padStart(paddingLength, '0');
|
||||||
generatedCodes.push(`${prefix}${numStr}`);
|
generatedCodes.push(`${prefix}${numStr}`);
|
||||||
}
|
}
|
||||||
@@ -1172,21 +1189,17 @@ function generateCodeRange() {
|
|||||||
const individualCodesArea = document.getElementById('individual_codes');
|
const individualCodesArea = document.getElementById('individual_codes');
|
||||||
|
|
||||||
if (generatedCodes.length > 0) {
|
if (generatedCodes.length > 0) {
|
||||||
// 1. Basis-Code setzen (falls leer oder wenn wir komplett neu generieren)
|
|
||||||
if (!codeField.value || confirm("Soll der aktuelle Basis-Code überschrieben werden?")) {
|
if (!codeField.value || confirm("Soll der aktuelle Basis-Code überschrieben werden?")) {
|
||||||
codeField.value = generatedCodes[0];
|
codeField.value = generatedCodes[0];
|
||||||
if (typeof validateCodeField === 'function') validateCodeField(codeField);
|
if (typeof validateCodeField === 'function') validateCodeField(codeField);
|
||||||
generatedCodes.shift(); // Den ersten aus dem Array entfernen, da er im Basis-Feld steht
|
generatedCodes.shift();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Den Rest in die Textarea einfügen
|
|
||||||
if (generatedCodes.length > 0) {
|
if (generatedCodes.length > 0) {
|
||||||
let existing = individualCodesArea.value.trim();
|
let existing = individualCodesArea.value.trim();
|
||||||
// Wenn schon was drin steht, machen wir einen Zeilenumbruch, sonst nicht
|
|
||||||
individualCodesArea.value = existing ? existing + '\n' + generatedCodes.join('\n') : generatedCodes.join('\n');
|
individualCodesArea.value = existing ? existing + '\n' + generatedCodes.join('\n') : generatedCodes.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Visuelles Feedback
|
|
||||||
alert(`Bereich erfolgreich generiert! (${end - start + 1} Codes)`);
|
alert(`Bereich erfolgreich generiert! (${end - start + 1} Codes)`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1206,16 +1219,16 @@ function startCode4Scanner() {
|
|||||||
// Beende evtl. laufenden ISBN Scanner
|
// Beende evtl. laufenden ISBN Scanner
|
||||||
const isbnScannerBox = document.getElementById('isbn-scanner');
|
const isbnScannerBox = document.getElementById('isbn-scanner');
|
||||||
if (isbnScannerBox && isbnScannerBox.style.display !== 'none') {
|
if (isbnScannerBox && isbnScannerBox.style.display !== 'none') {
|
||||||
if (typeof killScannerHardware === 'function') killScannerHardware();
|
killScannerHardware();
|
||||||
isbnScannerBox.style.display = 'none';
|
isbnScannerBox.style.display = 'none';
|
||||||
const isbnScanButton = document.getElementById('scan-isbn-btn');
|
const isbnScanButton = document.getElementById('scan-isbn-btn');
|
||||||
if (isbnScanButton) isbnScanButton.textContent = 'ISBN scannen';
|
if (isbnScanButton) isbnScanButton.textContent = 'Barcode scannen';
|
||||||
if (typeof setIsbnScanStatus === 'function') setIsbnScanStatus('Scanner gestoppt.');
|
if (typeof setIsbnScanStatus === 'function') setIsbnScanStatus('Scanner gestoppt.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scanner ausschalten, wenn er bereits läuft
|
// Scanner ausschalten, wenn er bereits läuft
|
||||||
if (scannerBox.style.display !== 'none') {
|
if (scannerBox.style.display !== 'none') {
|
||||||
if (typeof killScannerHardware === 'function') killScannerHardware();
|
killScannerHardware();
|
||||||
scannerBox.style.display = 'none';
|
scannerBox.style.display = 'none';
|
||||||
scanButton.textContent = 'Barcode scannen';
|
scanButton.textContent = 'Barcode scannen';
|
||||||
if (typeof setCode4ScanStatus === 'function') setCode4ScanStatus('Scanner gestoppt.');
|
if (typeof setCode4ScanStatus === 'function') setCode4ScanStatus('Scanner gestoppt.');
|
||||||
@@ -1232,7 +1245,6 @@ function startCode4Scanner() {
|
|||||||
'#code4-scanner',
|
'#code4-scanner',
|
||||||
function(decodedText) {
|
function(decodedText) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
// Verhindere Doppel-Scans desselben Codes innerhalb von 1.5 Sekunden
|
|
||||||
if (decodedText === code4LastScanned && (now - code4LastScannedAt) < 1500) {
|
if (decodedText === code4LastScanned && (now - code4LastScannedAt) < 1500) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1240,8 +1252,7 @@ function startCode4Scanner() {
|
|||||||
code4LastScannedAt = now;
|
code4LastScannedAt = now;
|
||||||
|
|
||||||
if (scanMode === 'single') {
|
if (scanMode === 'single') {
|
||||||
// VERHALTEN: Einzel-Scan
|
killScannerHardware();
|
||||||
if (typeof killScannerHardware === 'function') killScannerHardware();
|
|
||||||
scannerBox.style.display = 'none';
|
scannerBox.style.display = 'none';
|
||||||
scanButton.textContent = 'Barcode scannen';
|
scanButton.textContent = 'Barcode scannen';
|
||||||
|
|
||||||
@@ -1250,20 +1261,16 @@ function startCode4Scanner() {
|
|||||||
if (typeof setCode4ScanStatus === 'function') setCode4ScanStatus(`Code gesetzt: ${decodedText}`);
|
if (typeof setCode4ScanStatus === 'function') setCode4ScanStatus(`Code gesetzt: ${decodedText}`);
|
||||||
|
|
||||||
} else if (scanMode === 'continuous') {
|
} else if (scanMode === 'continuous') {
|
||||||
// VERHALTEN: Fortlaufender Scan
|
|
||||||
let currentAreaCodes = individualCodesArea.value.split('\n').map(c => c.trim()).filter(c => c !== '');
|
let currentAreaCodes = individualCodesArea.value.split('\n').map(c => c.trim()).filter(c => c !== '');
|
||||||
|
|
||||||
if (!codeField.value) {
|
if (!codeField.value) {
|
||||||
// Basis-Code setzen, falls noch leer
|
|
||||||
codeField.value = decodedText;
|
codeField.value = decodedText;
|
||||||
if (typeof validateCodeField === 'function') validateCodeField(codeField);
|
if (typeof validateCodeField === 'function') validateCodeField(codeField);
|
||||||
if (typeof setCode4ScanStatus === 'function') setCode4ScanStatus(`Basis-Code gesetzt: ${decodedText}`);
|
if (typeof setCode4ScanStatus === 'function') setCode4ScanStatus(`Basis-Code gesetzt: ${decodedText}`);
|
||||||
} else if (!currentAreaCodes.includes(decodedText) && codeField.value !== decodedText) {
|
} else if (!currentAreaCodes.includes(decodedText) && codeField.value !== decodedText) {
|
||||||
// In Textarea anhängen, falls Code noch nicht existiert
|
|
||||||
individualCodesArea.value += (individualCodesArea.value ? '\n' : '') + decodedText;
|
individualCodesArea.value += (individualCodesArea.value ? '\n' : '') + decodedText;
|
||||||
if (typeof setCode4ScanStatus === 'function') setCode4ScanStatus(`Zusatz-Code erfasst: ${decodedText}`);
|
if (typeof setCode4ScanStatus === 'function') setCode4ScanStatus(`Zusatz-Code erfasst: ${decodedText}`);
|
||||||
|
|
||||||
// Audio-Feedback für kontinuierliches Scannen (optional, aber sehr hilfreich)
|
|
||||||
try {
|
try {
|
||||||
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
const oscillator = audioCtx.createOscillator();
|
const oscillator = audioCtx.createOscillator();
|
||||||
@@ -1271,7 +1278,7 @@ function startCode4Scanner() {
|
|||||||
oscillator.connect(gainNode);
|
oscillator.connect(gainNode);
|
||||||
gainNode.connect(audioCtx.destination);
|
gainNode.connect(audioCtx.destination);
|
||||||
oscillator.type = 'sine';
|
oscillator.type = 'sine';
|
||||||
oscillator.frequency.value = 800; // Hoher Beep
|
oscillator.frequency.value = 800;
|
||||||
gainNode.gain.setValueAtTime(0.1, audioCtx.currentTime);
|
gainNode.gain.setValueAtTime(0.1, audioCtx.currentTime);
|
||||||
oscillator.start();
|
oscillator.start();
|
||||||
oscillator.stop(audioCtx.currentTime + 0.1);
|
oscillator.stop(audioCtx.currentTime + 0.1);
|
||||||
@@ -1286,14 +1293,15 @@ function startCode4Scanner() {
|
|||||||
'Scanner läuft. Richte die Kamera auf den Barcode.',
|
'Scanner läuft. Richte die Kamera auf den Barcode.',
|
||||||
setCode4ScanStatus
|
setCode4ScanStatus
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
console.error("runEngineInitialization is not defined. Ensure your scanner script is loaded.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ISBN Scanner-Funktion
|
||||||
|
*/
|
||||||
function startIsbnScanner() {
|
function startIsbnScanner() {
|
||||||
if (!libraryModuleEnabled) {
|
if (!libraryModuleEnabled) {
|
||||||
setIsbnScanStatus('Bibliotheks-Modul ist deaktiviert.', true);
|
if (typeof setIsbnScanStatus === 'function') setIsbnScanStatus('Bibliotheks-Modul ist deaktiviert.', true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const scannerBox = document.getElementById('isbn-scanner');
|
const scannerBox = document.getElementById('isbn-scanner');
|
||||||
@@ -1301,21 +1309,20 @@ function startCode4Scanner() {
|
|||||||
const isbnField = document.getElementById('isbn');
|
const isbnField = document.getElementById('isbn');
|
||||||
if (!scannerBox || !scanButton || !isbnField) return;
|
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 codeScannerBox = document.getElementById('code4-scanner');
|
||||||
const codeScanButton = document.getElementById('scan-code4-btn');
|
const codeScanButton = document.getElementById('scan-code4-btn');
|
||||||
if (codeScannerBox && codeScannerBox.style.display !== 'none') {
|
if (codeScannerBox && codeScannerBox.style.display !== 'none') {
|
||||||
killScannerHardware();
|
killScannerHardware();
|
||||||
if (codeScannerBox) codeScannerBox.style.display = 'none';
|
codeScannerBox.style.display = 'none';
|
||||||
if (codeScanButton) codeScanButton.textContent = 'Barcode scannen';
|
codeScanButton.textContent = 'Barcode scannen';
|
||||||
setCode4ScanStatus('Scanner gestoppt.');
|
if (typeof setCode4ScanStatus === 'function') setCode4ScanStatus('Scanner gestoppt.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scannerBox.style.display !== 'none') {
|
if (scannerBox.style.display !== 'none') {
|
||||||
killScannerHardware();
|
killScannerHardware();
|
||||||
scannerBox.style.display = 'none';
|
scannerBox.style.display = 'none';
|
||||||
scanButton.textContent = 'ISBN scannen';
|
scanButton.textContent = 'Barcode scannen';
|
||||||
setIsbnScanStatus('Scanner gestoppt.');
|
if (typeof setIsbnScanStatus === 'function') setIsbnScanStatus('Scanner gestoppt.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1327,18 +1334,17 @@ function startCode4Scanner() {
|
|||||||
function(decodedText) {
|
function(decodedText) {
|
||||||
killScannerHardware();
|
killScannerHardware();
|
||||||
scannerBox.style.display = 'none';
|
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;
|
isbnField.value = normalizedIsbn || decodedText;
|
||||||
updateIsbnLiveValidation();
|
if (typeof updateIsbnLiveValidation === 'function') updateIsbnLiveValidation();
|
||||||
|
|
||||||
if (normalizedIsbn) {
|
if (normalizedIsbn) {
|
||||||
setIsbnScanStatus(`ISBN gesetzt: ${normalizedIsbn}`);
|
if (typeof setIsbnScanStatus === 'function') setIsbnScanStatus(`ISBN gesetzt: ${normalizedIsbn}`);
|
||||||
// Automatically load book metadata after a valid ISBN scan.
|
if (typeof fetchBookInfo === 'function') fetchBookInfo('upload');
|
||||||
fetchBookInfo('upload');
|
|
||||||
} else {
|
} 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.',
|
'Scanner läuft. Bitte ISBN-Code erfassen.',
|
||||||
@@ -1346,6 +1352,32 @@ function startCode4Scanner() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- 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
|
// Load predefined filter values for dropdowns
|
||||||
function loadPredefinedFilterValues(filterNumber) {
|
function loadPredefinedFilterValues(filterNumber) {
|
||||||
fetch(`/get_predefined_filter_values/${filterNumber}`)
|
fetch(`/get_predefined_filter_values/${filterNumber}`)
|
||||||
@@ -1871,16 +1903,6 @@ function startCode4Scanner() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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');
|
const isbnField = document.getElementById('isbn');
|
||||||
if (isbnField) {
|
if (isbnField) {
|
||||||
isbnField.addEventListener('input', updateIsbnLiveValidation);
|
isbnField.addEventListener('input', updateIsbnLiveValidation);
|
||||||
|
|||||||
@@ -213,6 +213,7 @@ existing['modules'] = {
|
|||||||
'library': {'enabled': True},
|
'library': {'enabled': True},
|
||||||
'student_cards': {'enabled': True, 'default_borrow_days': 14, 'max_borrow_days': 365},
|
'student_cards': {'enabled': True, 'default_borrow_days': 14, 'max_borrow_days': 365},
|
||||||
'terminplan': {'enabled': True},
|
'terminplan': {'enabled': True},
|
||||||
|
'mail': {'enabled': True},
|
||||||
}
|
}
|
||||||
existing['trial'] = trial_config
|
existing['trial'] = trial_config
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user