adjustments to the user review of his appointments
This commit is contained in:
@@ -37,7 +37,7 @@ def _current_tenant_id():
|
|||||||
@appoint_bp.route('/client/<appointment_id>', methods=['POST', 'GET'])
|
@appoint_bp.route('/client/<appointment_id>', methods=['POST', 'GET'])
|
||||||
def client(appointment_id):
|
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()
|
guard = _require_module_enabled()
|
||||||
if guard:
|
if guard:
|
||||||
@@ -74,9 +74,34 @@ def client(appointment_id):
|
|||||||
available_for_view['slots_booked'] = sanitized_bookings
|
available_for_view['slots_booked'] = sanitized_bookings
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
|
action = request.form.get('action', 'book')
|
||||||
|
|
||||||
|
# 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')
|
start_daytime = request.form.get('start_day_time')
|
||||||
username = request.form.get('client_name')
|
username = request.form.get('client_name')
|
||||||
|
|
||||||
custom_answers = request.form.getlist('custom_answers')
|
custom_answers = request.form.getlist('custom_answers')
|
||||||
|
|
||||||
if not start_daytime or not username:
|
if not start_daytime or not username:
|
||||||
|
|||||||
@@ -39,13 +39,14 @@
|
|||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col-12 col-xxl-11">
|
<div class="col-12 col-xxl-11">
|
||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
|
|
||||||
<div class="col-12 col-lg-4">
|
<div class="col-12 col-lg-4">
|
||||||
<div class="card border-0 shadow-lg rounded-4 h-100">
|
<div class="card border-0 shadow-lg rounded-4 h-100">
|
||||||
<div class="card-body p-4 p-md-5">
|
<div class="card-body p-4 p-md-5">
|
||||||
<p class="text-uppercase text-muted fw-semibold mb-2">Terminplaner</p>
|
<p class="text-uppercase text-muted fw-semibold mb-2">Terminplaner</p>
|
||||||
{% if 'username' in session %}
|
{% if can_view_booking_names %}
|
||||||
<h1 class="h3 fw-bold mb-3">Termin Übersicht - {{ available.title }}</h1>
|
<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 eingegenagen sind verwalten.</p>
|
<p class="text-muted mb-4">Hier können Sie alle Buchungen, die bereits eingegangen sind, einsehen und verwalten.</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
<h1 class="h3 fw-bold mb-3">Termin buchen - {{ available.title }}</h1>
|
<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>
|
<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>
|
||||||
@@ -64,7 +65,7 @@
|
|||||||
<div>{{ available.slot_lenght }} Minuten</div>
|
<div>{{ available.slot_lenght }} Minuten</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if 'username' in session %}
|
{% if not can_view_booking_names %}
|
||||||
<div class="p-3 rounded-3 bg-light mb-3">
|
<div class="p-3 rounded-3 bg-light mb-3">
|
||||||
<div class="fw-semibold mb-2">Gewählter Termin</div>
|
<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 id="selected-slot-badge" class="text-muted small">Noch kein Slot ausgewählt.</div>
|
||||||
@@ -74,14 +75,13 @@
|
|||||||
{% if available.slots_booked %}
|
{% if available.slots_booked %}
|
||||||
<div class="p-3 rounded-3 bg-light">
|
<div class="p-3 rounded-3 bg-light">
|
||||||
<div class="fw-semibold mb-2">Bereits gebucht</div>
|
<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 %}
|
{% for booking in available.slots_booked %}
|
||||||
<li>
|
<li>
|
||||||
{{ booking.start }}
|
{% if booking is mapping %}
|
||||||
{% if can_view_booking_names and booking.name %}
|
{{ booking.get('start', '') }} - Belegt
|
||||||
- {{ booking.name }}
|
|
||||||
{% else %}
|
{% else %}
|
||||||
- Belegt
|
{{ booking[0] }} - {% if can_view_booking_names %}<span class="fw-semibold text-dark">{{ booking[1] }}</span>{% else %}Belegt{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -91,60 +91,90 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 col-lg-8">
|
<div class="col-12 col-lg-8">
|
||||||
<div class="card border-0 shadow-lg rounded-4 h-100">
|
<div class="card border-0 shadow-lg rounded-4 h-100">
|
||||||
<div class="card-body p-4 p-md-5 bg-white">
|
<div class="card-body p-4 p-md-5 bg-white">
|
||||||
|
|
||||||
{% if can_view_booking_names %}
|
{% if can_view_booking_names %}
|
||||||
<!-- ADMIN / AUTOREN ANSICHT: Buchungstabelle -->
|
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mb-4">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div>
|
||||||
<h2 class="h4 fw-bold mb-0">Eingegangene Buchungen</h2>
|
<h2 class="h4 fw-bold mb-0">Eingegangene Buchungen</h2>
|
||||||
<span class="badge bg-primary rounded-pill fs-6 px-3">
|
<div class="text-muted small">Verwalten Sie die Termine Ihrer Klienten</div>
|
||||||
{{ available.slots_total - available.slots_left }} Gebucht
|
</div>
|
||||||
|
<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>
|
</span>
|
||||||
</div>
|
</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 %}
|
{% if available.slots_booked %}
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover align-middle border-top">
|
<table class="table table-hover align-middle border-top" id="admin-booking-table">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" class="py-3">Zeitpunkt</th>
|
<th scope="col" class="py-3">Zeitpunkt</th>
|
||||||
<th scope="col" class="py-3">Name</th>
|
<th scope="col" class="py-3">Name</th>
|
||||||
<!-- Dynamische Spaltenüberschriften für Custom Fields -->
|
|
||||||
{% for field_label in custom_fields %}
|
{% for field_label in custom_fields %}
|
||||||
<th scope="col" class="py-3">{{ field_label }}</th>
|
<th scope="col" class="py-3">{{ field_label }}</th>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
<th scope="col" class="py-3 text-end">Aktion</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody id="booking-table-body">
|
||||||
{% for booking in available.slots_booked %}
|
{% for booking in available.slots_booked %}
|
||||||
|
{% if booking is not mapping %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fw-semibold text-primary py-3">
|
<td class="fw-semibold text-primary py-3 text-nowrap">
|
||||||
{{ booking[0] }}
|
{{ booking[0] }}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="fw-bold">{{ booking[1] }}</span>
|
<span class="fw-bold text-dark">{{ booking[1] }}</span>
|
||||||
</td>
|
</td>
|
||||||
<!-- Custom Fields Antworten auslesen -->
|
|
||||||
{% if booking|length > 2 and booking[2] %}
|
{% if booking|length > 2 and booking[2] %}
|
||||||
{% for answer in booking[2] %}
|
{% for answer in booking[2] %}
|
||||||
<td>{{ answer if answer else '<span class="text-muted small">-</span>'|safe }}</td>
|
<td>{{ answer if answer else '<span class="text-muted small">-</span>'|safe }}</td>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<!-- Fallback falls alte Buchung ohne Custom Fields existiert -->
|
|
||||||
{% for field in custom_fields %}
|
{% for field in custom_fields %}
|
||||||
<td><span class="text-muted small">-</span></td>
|
<td><span class="text-muted small">-</span></td>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% 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="{{ 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>
|
</tr>
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="no-results-msg" class="text-center py-4 text-muted d-none">
|
||||||
|
Keine Buchungen entsprechen Ihrem Filter.
|
||||||
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="text-center py-5 text-muted">
|
<div class="text-center py-5 text-muted">
|
||||||
<i class="bi bi-calendar-x display-4 d-block mb-3"></i>
|
|
||||||
<p class="mb-0">Bisher sind noch keine Buchungen für diesen Terminplaner eingegangen.</p>
|
<p class="mb-0">Bisher sind noch keine Buchungen für diesen Terminplaner eingegangen.</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -154,7 +184,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<!-- CLIENT ANSICHT: Normaler Buchungs-Workflow -->
|
|
||||||
<h2 class="h4 fw-bold mb-3">Termin im Kalender auswählen</h2>
|
<h2 class="h4 fw-bold mb-3">Termin im Kalender auswählen</h2>
|
||||||
|
|
||||||
<div class="day-slider-wrap mb-3">
|
<div class="day-slider-wrap mb-3">
|
||||||
@@ -188,6 +217,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="d-flex flex-column flex-sm-row gap-2 pt-2">
|
<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>
|
<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>
|
<a class="btn btn-outline-secondary btn-lg" href="{{ url_for('terminplaner.main', tenant=tenant_id) }}">Zur Übersicht</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -197,6 +227,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -222,6 +253,52 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.15/index.global.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
|
|||||||
Reference in New Issue
Block a user