Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 480bd00a54 | |||
| fdf699363a | |||
| dd495a8b13 | |||
| ca57961d4b | |||
| 3923939387 |
@@ -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):
|
||||
|
||||
@@ -37,7 +37,7 @@ def _current_tenant_id():
|
||||
@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:
|
||||
@@ -74,35 +74,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')
|
||||
|
||||
custom_answers = request.form.getlist('custom_answers')
|
||||
action = request.form.get('action', 'book')
|
||||
|
||||
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,
|
||||
custom_fields=custom_fields
|
||||
)
|
||||
# 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))
|
||||
|
||||
if appointment_service.book_slot(appointment_id, start_daytime, username, custom=custom_answers):
|
||||
return redirect(
|
||||
url_for(
|
||||
'terminplaner.client_success',
|
||||
# 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',
|
||||
|
||||
@@ -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,49 +91,163 @@
|
||||
</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>
|
||||
</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 %}
|
||||
{% if can_view_booking_names %}
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mb-4">
|
||||
<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>
|
||||
<h2 class="h4 fw-bold mb-0">Eingegangene Buchungen</h2>
|
||||
<div class="text-muted small">Verwalten Sie die Termine Ihrer Klienten</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<span class="badge bg-primary rounded-pill fs-6 px-3 align-self-start align-self-md-center">
|
||||
{{ available.slots_total - available.slots_left }} von {{ available.slots_total }} belegt
|
||||
</span>
|
||||
</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>
|
||||
|
||||
{% 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>
|
||||
{% 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">
|
||||
{% for booking in available.slots_booked %}
|
||||
<tr>
|
||||
<td class="fw-semibold text-primary py-3 text-nowrap">
|
||||
{% if booking is mapping %}
|
||||
{{ booking.start }}
|
||||
{% else %}
|
||||
{{ booking[0] }}
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<span class="fw-bold text-dark">
|
||||
{% if booking is mapping %}
|
||||
{{ booking.name }}
|
||||
{% else %}
|
||||
{{ booking[1] }}
|
||||
{% endif %}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
{% set answers = none %}
|
||||
{% if booking is mapping %}
|
||||
{# Versucht alle gängigen Bezeichnungen für das Antwort-Array zu treffen #}
|
||||
{% set answers = booking.custom or booking.custom_fields or booking.custom_answers %}
|
||||
{% elif booking|length > 2 %}
|
||||
{% set answers = booking[2] %}
|
||||
{% endif %}
|
||||
|
||||
{% if answers %}
|
||||
{% for answer in answers %}
|
||||
<td>{{ answer if answer else '<span class="text-muted small">-</span>'|safe }}</td>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{# Fallback: Zeige Striche, wenn keine Antworten da sind #}
|
||||
{% for field in custom_fields %}
|
||||
<td><span class="text-muted small">-</span></td>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<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="{% if booking is mapping %}{{ booking.start }}{% else %}{{ booking[0] }}{% endif %}">
|
||||
<input type="hidden" name="target_client_name" value="{% if booking is mapping %}{{ booking.name }}{% else %}{{ booking[1] }}{% endif %}">
|
||||
|
||||
<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 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="d-flex flex-column flex-sm-row gap-2 pt-2">
|
||||
<button type="submit" class="btn btn-primary btn-lg">Termin buchen</button>
|
||||
<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>
|
||||
@@ -152,6 +273,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 () {
|
||||
|
||||
Reference in New Issue
Block a user