addition of a csv export funktion for further Processing of the User

This commit is contained in:
2026-06-26 17:41:51 +02:00
parent 7d13dc264c
commit 8e11fbf969
2 changed files with 80 additions and 6 deletions
+64
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):
""" """
+16 -6
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">
@@ -146,7 +156,7 @@
<td> <td>
<span class="fw-bold text-dark">{{ booking[1] }}</span> <span class="fw-bold text-dark">{{ booking[1] }}</span>
</td> </td>
<!-- 3. CUSTOM FIELDS (Präziser Index-Abgleich gegen Spaltenkopf) --> <!-- 3. CUSTOM FIELDS (Präziser Index-Abgleich gegen Spaltenkopf) -->
{% for field in custom_fields %} {% for field in custom_fields %}
<td> <td>
@@ -166,7 +176,7 @@
<input type="hidden" name="action" value="delete"> <input type="hidden" name="action" value="delete">
<input type="hidden" name="slot_time" value="{{ booking[0] }}"> <input type="hidden" name="slot_time" value="{{ booking[0] }}">
<input type="hidden" name="target_client_name" value="{{ booking[1] }}"> <input type="hidden" name="target_client_name" value="{{ booking[1] }}">
<button type="submit" class="btn btn-sm btn-outline-danger px-3 rounded-3"> <button type="submit" class="btn btn-sm btn-outline-danger px-3 rounded-3">
Löschen Löschen
</button> </button>