Compare commits

...

3 Commits

2 changed files with 118 additions and 35 deletions
+66 -1
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.settings as cfg
import Web.modules.database.termine as termin import Web.modules.database.termine as termin
import Web.modules.database.user as us 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 # Create a blueprint instance
appoint_bp = Blueprint('terminplaner', __name__) appoint_bp = Blueprint('terminplaner', __name__)
@@ -34,6 +37,67 @@ def _current_tenant_id():
pass pass
return str(session.get('tenant_id', '') or '').strip() 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)
# Der Byte Order Mark (BOM) zwingt Excel dazu, UTF-8 (Umlaute) direkt richtig zu erkennen
output.data = b'\xef\xbb\xbf' + output.data.encode('utf-8')
# 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']) @appoint_bp.route('/client/<appointment_id>', methods=['POST', 'GET'])
def client(appointment_id): def client(appointment_id):
""" """
@@ -136,7 +200,8 @@ def client(appointment_id):
current_user=session.get('username', ''), current_user=session.get('username', ''),
tenant_id=_current_tenant_id(), tenant_id=_current_tenant_id(),
can_view_booking_names=can_view_booking_names, can_view_booking_names=can_view_booking_names,
custom_fields=custom_fields custom_fields=custom_fields,
appointment_item=appointment_item
) )
+52 -34
View File
@@ -97,16 +97,26 @@
<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 -->
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mb-4"> <div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mb-4">
<div> <div>
<h2 class="h4 fw-bold mb-0">Eingegangene Buchungen</h2> <h2 class="h4 fw-bold mb-0">Eingegangene Buchungen</h2>
<div class="text-muted small">Verwalten Sie die Termine Ihrer Klienten</div> <div class="text-muted small">Verwalten Sie die Termine Ihrer Klienten</div>
</div> </div>
<span class="badge bg-primary rounded-pill fs-6 px-3 align-self-start align-self-md-center"> <div class="d-flex gap-2 align-self-start align-self-md-center">
{{ available.slots_total - available.slots_left }} von {{ available.slots_total }} belegt <!-- Neuer CSV Export Button -->
</span> <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> </div>
<div class="bg-light p-3 rounded-3 mb-4"> <div class="bg-light p-3 rounded-3 mb-4">
<div class="row g-2 align-items-center"> <div class="row g-2 align-items-center">
<div class="col-12 col-md-8"> <div class="col-12 col-md-8">
@@ -126,6 +136,7 @@
<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>
<!-- 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 %}
@@ -133,38 +144,45 @@
</tr> </tr>
</thead> </thead>
<tbody id="booking-table-body"> <tbody id="booking-table-body">
{% for booking in available.slots_booked %} {# Wir loopen jetzt über die rohen DB-Daten statt über die beschnittene Service-Funktion #}
{% if booking is not mapping %} {% for booking in appointment_item.get('slots_booked', []) %}
<tr> <tr>
<td class="fw-semibold text-primary py-3 text-nowrap"> <!-- 1. ZEITPUNKT -->
{{ booking[0] }} <td class="fw-semibold text-primary py-3 text-nowrap">
</td> {{ 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> <td>
<span class="fw-bold text-dark">{{ booking[1] }}</span> {# 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> </td>
{% endfor %}
{% if booking|length > 2 and booking[2] %}
{% for answer in booking[2] %} <!-- 4. AKTION (LÖSCHEN) -->
<td>{{ answer if answer else '<span class="text-muted small">-</span>'|safe }}</td> <td class="text-end">
{% endfor %} <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">
{% else %} <input type="hidden" name="action" value="delete">
{% for field in custom_fields %} <input type="hidden" name="slot_time" value="{{ booking[0] }}">
<td><span class="text-muted small">-</span></td> <input type="hidden" name="target_client_name" value="{{ booking[1] }}">
{% endfor %}
{% endif %} <button type="submit" class="btn btn-sm btn-outline-danger px-3 rounded-3">
Löschen
<td class="text-end"> </button>
<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"> </form>
<input type="hidden" name="action" value="delete"> </td>
<input type="hidden" name="slot_time" value="{{ booking[0] }}"> </tr>
<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>
{% endif %}
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>