Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8e11fbf969 | |||
| 7d13dc264c | |||
| 480bd00a54 |
@@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user