Compare commits

...

11 Commits

6 changed files with 401 additions and 65 deletions
+15 -2
View File
@@ -3302,7 +3302,13 @@ def api_library_items():
borrower = doc.get('User', '')
# Decrypt if value is encrypted (decrypt_text returns original if not encrypted)
try:
borrower = decrypt_text(borrower) if borrower else ''
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
student_cards = db['student_cards']
card = student_cards.find_one({'_id': ObjectId(borrow_id)})
card = _decrypt_student_card_doc(card)
client.close()
borrower = card['SchülerName']
except Exception:
# Fallback: keep original string if decryption fails
borrower = borrower or ''
@@ -8368,8 +8374,15 @@ def admin_create_invoice(borrow_id):
flash('Für diese Ausleihe existiert bereits eine Rechnung. Bitte Korrekturbuchung verwenden.', 'warning')
return redirect(url_for('admin_borrowings'))
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
student_cards = db['student_cards']
card = student_cards.find_one({'_id': ObjectId(borrow_id)})
card = _decrypt_student_card_doc(card)
client.close()
borrower = card['SchülerName']
invoice_number = existing_invoice.get('invoice_number') or _build_invoice_number(borrow_doc['_id'], now)
borrower = borrow_doc.get('User', '')
item_name = item_doc.get('Name', '')
item_code = item_doc.get('Code_4', '')
+3 -2
View File
@@ -38,7 +38,7 @@ def _active_record_query(extra_query=None):
return base_query
def add(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=""):
def add(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="", custom_fields: list = ()):
try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client)
@@ -54,8 +54,9 @@ def add(date_start: str, date_end: str, time_span: list, slots: int, slot_lenght
'mail': mail,
'note': note,
'title': title,
'custom_fields': custom_fields,
'calendar_enabled': bool(calendar_enabled),
'slots_booked': [], # -> [(start_time, name), ...]the list gets there indexes as the slot 1-defined so is can be counted without an extra variable
'slots_booked': [], # -> [(start_time, name,(custom1, custom2,...)), ...]the list gets there indexes as the slot 1-defined so is can be counted without an extra variable
'Created': datetime.datetime.now(),
'LastUpdated': datetime.datetime.now()
}
+10 -4
View File
@@ -198,7 +198,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, slot_length, user: str, mail: list=None, note:str="", calendar_enabled: bool=False, title: str="") -> dict:
def new(date_start: str, date_end: str, time_span: list, slots, slot_length, user: str, mail: list=None, note:str="", calendar_enabled: bool=False, title: str="", custom_fields: list = ()) -> dict:
"""
Generates a link for the executive to send to his clients to book a time Slot
"""
@@ -207,6 +207,11 @@ def new(date_start: str, date_end: str, time_span: list, slots, slot_length, use
except (ValueError, TypeError):
slots_int = 0
try:
custom_fields.pop(-1)
except :
pass
try:
slot_length_int = int(slot_length)
except (ValueError, TypeError):
@@ -215,7 +220,7 @@ def new(date_start: str, date_end: str, time_span: list, slots, slot_length, use
normalized_time_span = _normalize_time_span(time_span)
normalized_mail = _normalize_mail_list(mail or [])
id = termin.add(date_start, date_end, normalized_time_span, slots_int, slot_length_int, user, normalized_mail, note, calendar_enabled=calendar_enabled, title=title)
id = termin.add(date_start, date_end, normalized_time_span, slots_int, slot_length_int, user, normalized_mail, note, calendar_enabled=calendar_enabled, title=title, custom_fields=custom_fields)
id_str = str(id)
tenant_id = _current_tenant_id()
@@ -254,7 +259,7 @@ def new(date_start: str, date_end: str, time_span: list, slots, slot_length, use
}
def book_slot(id, date_start_time, name):
def book_slot(id, date_start_time, name, custom:list=()):
try:
item = termin.get_item(id)
if not item:
@@ -273,7 +278,7 @@ def book_slot(id, date_start_time, name):
if existing[0] == date_start_time and existing[1] == name:
return False
slots.append((date_start_time, name))
slots.append((date_start_time, name, custom))
success = termin.update(id, slots)
return bool(success)
except Exception as e:
@@ -418,6 +423,7 @@ def get_user_upcoming_events(user: str, limit: int = 25) -> list[dict]:
'link': link,
'calendar_link': calendar_link if item.get('calendar_enabled') else None,
'title': str(item.get('title') or ''),
'custom_fields': list(item.get('custom_fields')),
}
)
+124 -26
View File
@@ -4,6 +4,9 @@ import Web.modules.terminplaner.backend_server as appointment_service
import Web.modules.database.settings as cfg
import Web.modules.database.termine as termin
import Web.modules.database.user as us
import csv
import io
from flask import make_response, flash, redirect, url_for, session
# Create a blueprint instance
appoint_bp = Blueprint('terminplaner', __name__)
@@ -34,10 +37,70 @@ def _current_tenant_id():
pass
return str(session.get('tenant_id', '') or '').strip()
@appoint_bp.route('/client/<appointment_id>/export_csv')
def export_csv(appointment_id):
"""
Generiert einen CSV-Export aller Buchungen für den Ersteller.
"""
# 1. Berechtigungsprüfung (analog zur Client-Route)
current_user = str(session.get('username', '') or '').strip()
appointment_item = termin.get_item(appointment_id) or {}
appointment_owner = str(appointment_item.get('user', '') or '').strip()
can_view = False
if current_user:
try:
can_view = bool(us.check_admin(current_user) or current_user == appointment_owner)
except Exception:
can_view = bool(current_user == appointment_owner)
if not can_view:
flash('Nicht autorisiert für diesen Export.', 'error')
return redirect(url_for('terminplaner.client', appointment_id=appointment_id))
custom_fields = appointment_item.get('custom_fields', [])
bookings = appointment_item.get('slots_booked', []) or []
# 2. CSV im Speicher erstellen
si = io.StringIO()
# Wir nutzen ein Semikolon als Trennzeichen das öffnet Excel im deutschsprachigen Raum direkt fehlerfrei
cw = csv.writer(si, delimiter=';', quoting=csv.QUOTE_MINIMAL)
# Tabellenkopf schreiben
headers = ['Zeitpunkt', 'Name'] + list(custom_fields)
cw.writerow(headers)
# Datenzeilen schreiben
for booking in bookings:
if isinstance(booking, (list, tuple)):
row = [booking[0], booking[1]]
# Antworten holen und sicherstellen, dass Lücken (falls vorhanden) mit Leerstrings gefüllt werden
answers = booking[2] if len(booking) > 2 else []
for i in range(len(custom_fields)):
if i < len(answers):
row.append(answers[i])
else:
row.append('')
cw.writerow(row)
# 3. Response vorbereiten und UTF-8-BOM für Excel mitsenden
response_content = si.getvalue()
output = make_response(response_content)
output.data = b'\xef\xbb\xbf' + output.data
# Dateiname generieren
title_slug = appointment_item.get('title', 'export').replace(' ', '_')
output.headers["Content-Disposition"] = f"attachment; filename=buchungen_{title_slug}.csv"
output.headers["Content-Type"] = "text/csv; charset=utf-8"
return output
@appoint_bp.route('/client/<appointment_id>', methods=['POST', 'GET'])
def client(appointment_id):
"""
The Route for the terminplaner to work with the client
The Route for the terminplaner to work with the client and allow owners to manage it.
"""
guard = _require_module_enabled()
if guard:
@@ -50,6 +113,10 @@ def client(appointment_id):
current_user = str(session.get('username', '') or '').strip()
appointment_item = termin.get_item(appointment_id) or {}
appointment_owner = str(appointment_item.get('user', '') or '').strip()
# Extract the custom fields defined by the appointment owner
custom_fields = appointment_item.get('custom_fields', [])
can_view_booking_names = False
if current_user:
try:
@@ -70,31 +137,60 @@ def client(appointment_id):
available_for_view['slots_booked'] = sanitized_bookings
if request.method == 'POST':
start_daytime = request.form.get('start_day_time')
username = request.form.get('client_name')
if not start_daytime or not username:
flash('Bitte Name und gewünschte Uhrzeit angeben.', 'error')
return render_template(
'termin_client.html',
appointment_id=appointment_id,
available=available_for_view,
current_user=session.get('username', ''),
tenant_id=_current_tenant_id(),
can_view_booking_names=can_view_booking_names,
)
action = request.form.get('action', 'book')
if appointment_service.book_slot(appointment_id, start_daytime, username):
return redirect(
url_for(
'terminplaner.client_success',
# Fall 1: ADMIN/AUTOR storniert einen Termin
if action == 'delete' and can_view_booking_names:
slot_time = request.form.get('slot_time')
client_name = request.form.get('target_client_name')
# Aktuelle Buchungen direkt aus dem DB-Item holen
current_slots = appointment_item.get('slots_booked', []) or []
# Filtere den zu löschenden Slot heraus (Prüfung auf Zeit und Name)
updated_slots = [
slot for slot in current_slots
if not (isinstance(slot, (list, tuple)) and slot[0] == slot_time and slot[1] == client_name)
]
# In DB schreiben via deiner existierenden termin.update() Funktion
if termin.update(appointment_id, updated_slots):
flash('Buchung wurde erfolgreich gelöscht.', 'success')
else:
flash('Fehler beim Löschen der Buchung.', 'error')
return redirect(url_for('terminplaner.client', appointment_id=appointment_id, tenant=_current_tenant_id() or None))
# Fall 2: NORMALER CLIENT bucht einen Termin
elif action == 'book':
start_daytime = request.form.get('start_day_time')
username = request.form.get('client_name')
custom_answers = request.form.getlist('custom_answers')
if not start_daytime or not username:
flash('Bitte Name und gewünschte Uhrzeit angeben.', 'error')
return render_template(
'termin_client.html',
appointment_id=appointment_id,
tenant=_current_tenant_id() or None,
start=start_daytime,
name=username,
available=available_for_view,
current_user=session.get('username', ''),
tenant_id=_current_tenant_id(),
can_view_booking_names=can_view_booking_names,
custom_fields=custom_fields
)
)
flash('Der Termin konnte nicht gespeichert werden.', 'error')
if appointment_service.book_slot(appointment_id, start_daytime, username, custom=custom_answers):
return redirect(
url_for(
'terminplaner.client_success',
appointment_id=appointment_id,
tenant=_current_tenant_id() or None,
start=start_daytime,
name=username,
)
)
flash('Der Termin konnte nicht gespeichert werden.', 'error')
return render_template(
'termin_client.html',
@@ -103,6 +199,8 @@ def client(appointment_id):
current_user=session.get('username', ''),
tenant_id=_current_tenant_id(),
can_view_booking_names=can_view_booking_names,
custom_fields=custom_fields,
appointment_item=appointment_item
)
@@ -169,13 +267,13 @@ def configure():
end = request.form.get('end_date')
time = request.form.get('time_frame')
slots_amount = request.form.get('slots_amounts')
slot_length = request.form.get('slot_length') # Korrigiert: slot_length
slot_length = request.form.get('slot_length')
mail = request.form.get('mail', '')
note = request.form.get('note', '')
add_to_calendar = request.form.get('add_to_calendar') == 'on'
title = request.form.get('title', '').strip() # Korrigiert: title statt titel
title = request.form.get('title', '').strip()
custom = request.form.getlist('custom_fields')
# Abfrage ebenfalls auf title und slot_length angepasst
if not start or not end or not time or not slots_amount or not slot_length or not title:
flash('Bitte alle Pflichtfelder ausfüllen.', 'error')
return render_template(
@@ -186,7 +284,7 @@ def configure():
)
# Variablen im Funktionsaufruf aktualisiert
result = appointment_service.new(start, end, time, slots_amount, slot_length, session["username"], mail, note, calendar_enabled=add_to_calendar, title=title)
result = appointment_service.new(start, end, time, slots_amount, slot_length, session["username"], mail, note, calendar_enabled=add_to_calendar, title=title, custom_fields=custom)
flash('Der Terminplan wurde angelegt.', 'success')
return render_template(
'termin_configure.html',
+206 -31
View File
@@ -39,13 +39,19 @@
<div class="row justify-content-center">
<div class="col-12 col-xxl-11">
<div class="row g-4">
<div class="col-12 col-lg-4">
<div class="card border-0 shadow-lg rounded-4 h-100">
<div class="card-body p-4 p-md-5">
<p class="text-uppercase text-muted fw-semibold mb-2">Terminplaner</p>
<h1 class="h3 fw-bold mb-3">Termin buchen - {{ available.title }}</h1>
<p class="text-muted mb-4">Wählen Sie im Kalender einen freien Slot aus. Den gewählten Termin können Sie danach direkt wie in einem Kalender-Block verschieben.</p>
{% if can_view_booking_names %}
<h1 class="h3 fw-bold mb-3">Termin Übersicht - {{ available.title }}</h1>
<p class="text-muted mb-4">Hier können Sie alle Buchungen, die bereits eingegangen sind, einsehen und verwalten.</p>
{% else %}
<h1 class="h3 fw-bold mb-3">Termin buchen - {{ available.title }}</h1>
<p class="text-muted mb-4">Wählen Sie im Kalender einen freien Slot aus. Den gewählten Termin können Sie danach direkt wie in einem Kalender-Block verschieben.</p>
{% endif %}
<div class="p-3 rounded-3 bg-light mb-3">
<div class="fw-semibold">Zeitraum</div>
<div>{{ available.date_start }} bis {{ available.date_end }}</div>
@@ -58,23 +64,24 @@
<div class="fw-semibold">Slot-Länge</div>
<div>{{ available.slot_lenght }} Minuten</div>
</div>
{% if not can_view_booking_names %}
<div class="p-3 rounded-3 bg-light mb-3">
<div class="fw-semibold mb-2">Gewählter Termin</div>
<div id="selected-slot-badge" class="text-muted small">Noch kein Slot ausgewählt.</div>
</div>
{% endif %}
{% if available.slots_booked %}
<div class="p-3 rounded-3 bg-light">
<div class="fw-semibold mb-2">Bereits gebucht</div>
<ul class="mb-0 small">
<ul class="mb-0 small text-muted">
{% for booking in available.slots_booked %}
<li>
{{ booking.start }}
{% if can_view_booking_names and booking.name %}
- {{ booking.name }}
{% if booking is mapping %}
{{ booking.get('start', '') }} - Belegt
{% else %}
- Belegt
{{ booking[0] }} - {% if can_view_booking_names %}<span class="fw-semibold text-dark">{{ booking[1] }}</span>{% else %}Belegt{% endif %}
{% endif %}
</li>
{% endfor %}
@@ -84,39 +91,161 @@
</div>
</div>
</div>
<div class="col-12 col-lg-8">
<div class="card border-0 shadow-lg rounded-4 h-100">
<div class="card-body p-4 p-md-5 bg-white">
<h2 class="h4 fw-bold mb-3">Termin im Kalender auswählen</h2>
<div class="day-slider-wrap mb-3">
<div class="d-flex justify-content-between align-items-center gap-2 mb-2">
<span class="small text-muted">Tag wählen</span>
<strong id="day-slider-label" class="small"></strong>
{% if can_view_booking_names %}
<!-- ADMIN / AUTOREN ANSICHT -->
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mb-4">
<div>
<h2 class="h4 fw-bold mb-0">Eingegangene Buchungen</h2>
<div class="text-muted small">Verwalten Sie die Termine Ihrer Klienten</div>
</div>
<div class="d-flex gap-2 align-self-start align-self-md-center">
<!-- Neuer CSV Export Button -->
<a href="{{ url_for('terminplaner.export_csv', appointment_id=appointment_id) }}" class="btn btn-outline-success rounded-3 d-inline-flex align-items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-download" viewBox="0 0 16 16">
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v2.5a.5.5 0 0 1 .5-.5"/>
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708z"/>
</svg>
Excel / CSV Export
</a>
<span class="badge bg-primary rounded-pill fs-6 px-3 d-flex align-items-center">
{{ available.slots_total - available.slots_left }} von {{ available.slots_total }} belegt
</span>
</div>
</div>
<input id="day-slider" type="range" class="form-range m-0" min="0" max="0" value="0">
</div>
<div id="client-slot-calendar" class="mb-4"></div>
<form id="client-booking-form" method="post" action="{{ url_for('terminplaner.client', appointment_id=appointment_id, tenant=tenant_id) }}" class="vstack gap-3">
<div>
<label for="start_day_time" class="form-label fw-semibold">Gewünschter Zeitpunkt</label>
<input type="text" id="start_day_time" name="start_day_time" class="form-control form-control-lg" placeholder="Bitte im Kalender auswählen" readonly required>
<div class="form-text">Klicken Sie auf einen freien Slot oder verschieben Sie den gewählten Block.</div>
<div class="bg-light p-3 rounded-3 mb-4">
<div class="row g-2 align-items-center">
<div class="col-12 col-md-8">
<label for="table-search" class="form-label small fw-semibold text-muted mb-1">Live-Filter (Name, Uhrzeit, Zusatzfelder...)</label>
<input type="text" id="table-search" class="form-control form-control-lg bg-white border" placeholder="Suchbegriff eingeben...">
</div>
<div class="col-12 col-md-4 d-flex align-items-end h-100 mt-md-4">
<button type="button" id="clear-filter-btn" class="btn btn-outline-secondary w-100 btn-lg">Filter zurücksetzen</button>
</div>
</div>
</div>
<div>
<label for="client_name" class="form-label fw-semibold">Ihr Name</label>
<input type="text" id="client_name" name="client_name" class="form-control form-control-lg" placeholder="Vor- und Nachname" required>
{% if available.slots_booked %}
<div class="table-responsive">
<table class="table table-hover align-middle border-top" id="admin-booking-table">
<thead class="table-light">
<tr>
<th scope="col" class="py-3">Zeitpunkt</th>
<th scope="col" class="py-3">Name</th>
<!-- Spaltenüberschriften für Custom Fields -->
{% for field_label in custom_fields %}
<th scope="col" class="py-3">{{ field_label }}</th>
{% endfor %}
<th scope="col" class="py-3 text-end">Aktion</th>
</tr>
</thead>
<tbody id="booking-table-body">
{# Wir loopen jetzt über die rohen DB-Daten statt über die beschnittene Service-Funktion #}
{% for booking in appointment_item.get('slots_booked', []) %}
<tr>
<!-- 1. ZEITPUNKT -->
<td class="fw-semibold text-primary py-3 text-nowrap">
{{ booking[0] }}
</td>
<!-- 2. CLIENT NAME -->
<td>
<span class="fw-bold text-dark">{{ booking[1] }}</span>
</td>
<!-- 3. CUSTOM FIELDS (Präziser Index-Abgleich gegen Spaltenkopf) -->
{% for field in custom_fields %}
<td>
{# Prüft ob für dieses Feld eine Antwort am exakt gleichen Index existiert #}
{% if booking|length > 2 and booking[2] and booking[2]|length > loop.index0 %}
{% set answer = booking[2][loop.index0] %}
{{ answer if answer else '<span class="text-muted small">-</span>'|safe }}
{% else %}
<span class="text-muted small">-</span>
{% endif %}
</td>
{% endfor %}
<!-- 4. AKTION (LÖSCHEN) -->
<td class="text-end">
<form method="post" action="{{ url_for('terminplaner.client', appointment_id=appointment_id, tenant=tenant_id) }}" onsubmit="return confirm('Möchten Sie diesen Termin wirklich löschen?');" class="d-inline">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="slot_time" value="{{ booking[0] }}">
<input type="hidden" name="target_client_name" value="{{ booking[1] }}">
<button type="submit" class="btn btn-sm btn-outline-danger px-3 rounded-3">
Löschen
</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="d-flex flex-column flex-sm-row gap-2 pt-2">
<button type="submit" class="btn btn-primary btn-lg">Termin buchen</button>
<div id="no-results-msg" class="text-center py-4 text-muted d-none">
Keine Buchungen entsprechen Ihrem Filter.
</div>
{% else %}
<div class="text-center py-5 text-muted">
<p class="mb-0">Bisher sind noch keine Buchungen für diesen Terminplaner eingegangen.</p>
</div>
{% endif %}
<div class="pt-4 border-top mt-4">
<a class="btn btn-outline-secondary btn-lg" href="{{ url_for('terminplaner.main', tenant=tenant_id) }}">Zur Übersicht</a>
</div>
</form>
{% else %}
<h2 class="h4 fw-bold mb-3">Termin im Kalender auswählen</h2>
<div class="day-slider-wrap mb-3">
<div class="d-flex justify-content-between align-items-center gap-2 mb-2">
<span class="small text-muted">Tag wählen</span>
<strong id="day-slider-label" class="small"></strong>
</div>
<input id="day-slider" type="range" class="form-range m-0" min="0" max="0" value="0">
</div>
<div id="client-slot-calendar" class="mb-4"></div>
<form id="client-booking-form" method="post" action="{{ url_for('terminplaner.client', appointment_id=appointment_id, tenant=tenant_id) }}" class="vstack gap-3">
<div>
<label for="start_day_time" class="form-label fw-semibold">Gewünschter Zeitpunkt</label>
<input type="text" id="start_day_time" name="start_day_time" class="form-control form-control-lg" placeholder="Bitte im Kalender auswählen" readonly required>
<div class="form-text">Klicken Sie auf einen freien Slot oder verschieben Sie den gewählten Block.</div>
</div>
<div>
<label for="client_name" class="form-label fw-semibold">Ihr Name</label>
<input type="text" id="client_name" name="client_name" class="form-control form-control-lg" placeholder="Vor- und Nachname" required>
</div>
{% if custom_fields %}
{% for field_label in custom_fields %}
<div>
<label class="form-label fw-semibold">{{ field_label }}</label>
<input type="text" name="custom_answers" class="form-control form-control-lg" placeholder="{{ field_label }} eingeben" required>
</div>
{% endfor %}
{% endif %}
<div class="d-flex flex-column flex-sm-row gap-2 pt-2">
<input type="hidden" name="action" value="book">
<button type="submit" class="btn btn-primary btn-lg">Termin buchen</button>
<a class="btn btn-outline-secondary btn-lg" href="{{ url_for('terminplaner.main', tenant=tenant_id) }}">Zur Übersicht</a>
</div>
</form>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
@@ -142,6 +271,52 @@
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const searchInput = document.getElementById('table-search');
const clearBtn = document.getElementById('clear-filter-btn');
const tableBody = document.getElementById('booking-table-body');
const noResultsMsg = document.getElementById('no-results-msg');
if (searchInput && tableBody) {
const rows = tableBody.querySelectorAll('tr');
searchInput.addEventListener('input', function () {
const query = this.value.toLowerCase().trim();
let visibleRowsCount = 0;
rows.forEach(row => {
// Liest den Textinhalt aller Spalten der Zeile aus
const rowText = row.textContent.toLowerCase();
if (rowText.includes(query)) {
row.classList.remove('d-none');
visibleRowsCount++;
} else {
row.classList.add('d-none');
}
});
// Falls kein Filterergebnis gefunden wurde, Nachricht einblenden
if (visibleRowsCount === 0 && query !== '') {
noResultsMsg.classList.remove('d-none');
} else {
noResultsMsg.classList.add('d-none');
}
});
// Filter zurücksetzen Button
if (clearBtn) {
clearBtn.addEventListener('click', function () {
searchInput.value = '';
rows.forEach(row => row.classList.remove('d-none'));
noResultsMsg.classList.add('d-none');
searchInput.focus();
});
}
}
});
</script>
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.15/index.global.min.js"></script>
<script>
(function () {
+43
View File
@@ -77,6 +77,16 @@
<div class="form-text">Wird aus der gebuchten Zeit (abzüglich Pausen) und der Slot-Länge berechnet.</div>
</div>
</div>
<div id="custom-fields-container" class="mt-4">
<h3 class="mb-3">Custom Fields</h3>
<div class="mb-3 custom-field-row">
<input type="text"
name="custom_fields"
class="form-control form-control-lg custom-dynamic-input"
placeholder="Zusatzfeld hinzufügen (z.B. Firma, Kundennummer...)">
</div>
</div>
{% if mail_service_enabled %}
<div>
@@ -351,6 +361,39 @@
syncTextarea();
}
document.addEventListener('DOMContentLoaded', function () {
const container = document.getElementById('custom-fields-container');
// Monitor inputs inside the container using event delegation
container.addEventListener('input', function (event) {
// Only trigger if the user is typing inside our dynamic inputs
if (event.target.classList.contains('custom-dynamic-input')) {
const allInputs = container.querySelectorAll('.custom-dynamic-input');
const lastInput = allInputs[allInputs.length - 1];
// If the user typed something into the absolute last input field, spawn a new one
if (lastInput.value.trim() !== '') {
createNewFieldRow(container);
}
}
});
// Helper function to generate and append a clean new input row
function createNewFieldRow(targetContainer) {
const rowDiv = document.createElement('div');
rowDiv.className = 'mb-3 custom-field-row';
rowDiv.innerHTML = `
<input type="text"
name="custom_fields"
class="form-control form-control-lg custom-dynamic-input"
placeholder="Nächstes Zusatzfeld...">
`;
targetContainer.appendChild(rowDiv);
}
});
// Event Listeners Registration
buildButton.addEventListener('click', renderRows);
startDateInput.addEventListener('change', renderRows);