Compare commits

...

5 Commits

Author SHA1 Message Date
Aiirondev_dev 1122618a51 slight mispelled variable name 2026-06-27 11:05:35 +02:00
Aiirondev_dev 54d56fc463 link klorrektion 2026-06-27 10:53:28 +02:00
Aiirondev_dev 9da4ff3ec8 circuilar import fix 2026-06-27 10:08:12 +02:00
Aiirondev_dev 884464cfe5 pdf download for a client Brief 2026-06-27 10:00:10 +02:00
Aiirondev_dev 0be4e3ca58 return is beeing ignored to the submition 2026-06-27 00:35:44 +02:00
4 changed files with 237 additions and 7 deletions
+2 -1
View File
@@ -310,4 +310,5 @@ def get_upcoming_for_user(user: str, limit: int = 25):
return []
finally:
if client:
client.close()
client.close()
+175 -3
View File
@@ -1,17 +1,57 @@
from flask import Blueprint, render_template, request, session, url_for, redirect, flash
from flask import Response
from flask import Blueprint, render_template, request, session, url_for, redirect, flash, make_response, Response, send_file
import Web.modules.terminplaner.backend_server as appointment_service
import Web.modules.database.settings as cfg
import Web.modules.database.termine as termin
import Web.modules.database.user as us
from Web.modules.terminplaner.backend_server import _resolve_public_base_url
import csv
import io
from flask import make_response, flash, redirect, url_for, session
import qrcode
import os
import tempfile
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import cm
from reportlab.lib.colors import grey, HexColor
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Image
# Create a blueprint instance
appoint_bp = Blueprint('terminplaner', __name__)
def _get_school_info_for_export():
"""
Get school information for PDF exports from configuration or database.
Returns default info if not configured.
"""
try:
if hasattr(cfg, 'get_school_info'):
return cfg.get_school_info()
school_info = {
'name': 'Schulname',
'address': 'Schuladresse',
'postal_code': 'PLZ',
'city': 'Stadt',
'school_number': '000000',
'it_admin': 'IT-Beauftragter/in',
'logo_path': '',
}
return school_info
except Exception:
# Return defaults if anything fails
return {
'name': 'Schulname',
'address': 'Schuladresse',
'postal_code': 'PLZ',
'city': 'Stadt',
'school_number': '000000',
'it_admin': 'IT-Beauftragter/in',
'logo_path': '',
}
def _require_module_enabled():
if not cfg.MODULES.is_enabled('terminplan'):
flash('Der Terminplaner ist deaktiviert.', 'info')
@@ -364,6 +404,138 @@ def client_slot_calendar_export(appointment_id):
response.headers['Content-Disposition'] = f'attachment; filename=termin-{title}-{appointment_id}-{slot_start.replace(" ", "_").replace(":", "")}.ics'
return response
@appoint_bp.route('/export_pdf_brief/<plan_id>', methods=['GET'])
def export_pdf_brief(plan_id):
# 1. Daten holen (Hier als Beispiel, passe dies auf deine Datenbank an)
# terminplan = Terminplan.query.get(plan_id)
school_info = _get_school_info_for_export()
school_name = school_info.get('name', 'Schulname')
address = school_info.get('address', 'Adresse')
postal_code = school_info.get('postal_code', 'PLZ')
city = school_info.get('city', 'Stadt')
school_number = school_info.get('school_number', '000000')
it_admin = school_info.get('it_admin', 'IT-Beauftragter/in')
tenant_id = _current_tenant_id()
try:
link = url_for('terminplaner.client', appointment_id=plan_id, tenant=tenant_id or None, _external=True)
except Exception:
host = _resolve_public_base_url()
link = host + "/terminplaner/client/" + plan_id
if tenant_id:
link += f"?tenant={tenant_id}"
schul_daten = {
"schulname": school_name,
"strasse": address,
"plz_ort": f"{postal_code} {city}",
"schulnummer": school_number,
"it_admin": it_admin
}
plan_daten = {
"titel": termin.get_item(plan_id).get('title', 'Terminplan'), # terminplan.title
"link": link, # terminplan.link
"notizen": termin.get_item(plan_id).get('note', '') # terminplan.note
}
# 2. QR-Code Bild im temporären Ordner erstellen
qr = qrcode.QRCode(version=1, box_size=10, border=0)
qr.add_data(plan_daten["link"])
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
fd, qr_path = tempfile.mkstemp(suffix=".png")
os.close(fd)
img.save(qr_path)
# 3. PDF im Speicher aufbauen (BytesIO)
pdf_buffer = io.BytesIO()
doc = SimpleDocTemplate(
pdf_buffer,
pagesize=A4,
rightMargin=2*cm,
leftMargin=2.5*cm,
topMargin=2.5*cm,
bottomMargin=2*cm
)
styles = getSampleStyleSheet()
styles.add(ParagraphStyle(name='Sender', fontSize=8, textColor=grey))
styles.add(ParagraphStyle(name='Address', fontSize=10, leading=14))
styles.add(ParagraphStyle(name='Date', fontSize=10, alignment=2))
styles.add(ParagraphStyle(name='Subject', fontSize=14, fontName='Helvetica-Bold', spaceAfter=16, textColor=HexColor('#0f4c5c')))
styles.add(ParagraphStyle(name='Body', fontSize=11, leading=16, spaceAfter=12))
styles.add(ParagraphStyle(name='Notes', fontSize=10, leading=14, textColor=HexColor("#444444")))
elements = []
# Absenderzeile
sender_text = f"<u>{schul_daten['schulname']}{schul_daten['strasse']}{schul_daten['plz_ort']}</u>"
elements.append(Paragraph(sender_text, styles['Sender']))
elements.append(Spacer(1, 1.5*cm))
# Sichtfenster-Adresse (Generisch)
elements.append(Paragraph("An die<br/>Teilnehmerinnen und Teilnehmer<br/>des Termins", styles['Address']))
elements.append(Spacer(1, 2*cm))
# Datum (Hier statisch zum Test, ggf. dynamisch per datetime)
import datetime
heute = datetime.datetime.now().strftime("%d.%m.%Y")
elements.append(Paragraph(f"{schul_daten['plz_ort']}, den {heute}", styles['Date']))
elements.append(Spacer(1, 1*cm))
# Betreff
elements.append(Paragraph(f"Einladung zur Terminbuchung: {plan_daten['titel']}", styles['Subject']))
# Text
elements.append(Paragraph("Sehr geehrte Damen und Herren,", styles['Body']))
elements.append(Paragraph("hiermit möchten wir Sie herzlich einladen, einen Termin für unsere anstehende Veranstaltung zu buchen. Um den Prozess für alle Beteiligten so einfach und effizient wie möglich zu gestalten, nutzen wir unser Online-Buchungssystem.", styles['Body']))
elements.append(Spacer(1, 0.5*cm))
# Link
elements.append(Paragraph("<b>Ihr persönlicher Buchungslink:</b>", styles['Body']))
link_html = f'<a href="{plan_daten["link"]}?" color="#16697a">{plan_daten["link"]}</a>'
elements.append(Paragraph(link_html, styles['Body']))
# Das vorhin erstellte QR-Code Bild einfügen
elements.append(Spacer(1, 0.2*cm))
elements.append(Image(qr_path, width=3*cm, height=3*cm, hAlign='LEFT'))
elements.append(Spacer(1, 0.5*cm))
# Notizen (falls vorhanden)
if plan_daten.get('notizen'):
elements.append(Paragraph("<b>Zusätzliche Informationen zum Termin:</b>", styles['Body']))
elements.append(Paragraph(plan_daten['notizen'], styles['Notes']))
elements.append(Spacer(1, 1.5*cm))
# Grußformel
elements.append(Paragraph("Mit freundlichen Grüßen,", styles['Body']))
elements.append(Spacer(1, 1.5*cm))
elements.append(Paragraph(f"<b>{schul_daten['schulname']}</b>", styles['Body']))
# PDF fertigstellen
doc.build(elements)
# Temporäres Bild löschen
if os.path.exists(qr_path):
os.remove(qr_path)
# Buffer auf Anfang zurücksetzen
pdf_buffer.seek(0)
# An Nutzer ausliefern
return send_file(
pdf_buffer,
mimetype='application/pdf',
as_attachment=True,
download_name=f"Einladung_{plan_daten['titel'].replace(' ', '_')}.pdf"
)
@appoint_bp.route('/')
def main():
guard = _require_module_enabled()
+14 -1
View File
@@ -155,7 +155,7 @@
const slotLengthInput = document.getElementById('slot_length');
const clientsperslot = document.getElementById('clients_per_slot')
const slotsAmountsInput = document.getElementById('slots_amounts');
const slotsAmountsDisplay = document.getElementById('slots_amounts_display'); // Neu: Anzeige-Element
const slotsAmountsDisplay = document.getElementById('slots_amounts_display');
if (!startDateInput || !endDateInput || !buildButton || !daysContainer || !timeFrameTextarea) {
return;
@@ -442,6 +442,19 @@
if (startDateInput.value && endDateInput.value) {
renderRows();
}
const configForm = document.querySelector('form');
if (configForm) {
configForm.addEventListener('keydown', function(event) {
if (event.key === 'Enter') {
if (event.target.tagName === 'TEXTAREA') return;
if (event.target.tagName === 'BUTTON' && event.target.type === 'submit') return;
event.preventDefault();
}
});
}
})();
</script>
{% endblock %}
+46 -2
View File
@@ -6,6 +6,8 @@
<div class="container py-4">
<div class="row justify-content-center">
<div class="col-12 col-lg-11 col-xl-10">
<!-- Hero Sektion -->
<section class="p-4 p-md-5 rounded-4 shadow-lg" style="background: linear-gradient(135deg, rgba(15,76,92,0.96), rgba(22,105,122,0.92)); color: #fff;">
<div class="d-flex flex-column flex-lg-row justify-content-between gap-4 align-items-start align-items-lg-end">
<div>
@@ -20,6 +22,39 @@
</div>
</section>
<!-- NEU EINGEBAUT: Erfolgsmeldung für gerade erstellte Buchungslinks -->
{% if generated_link %}
<div class="alert alert-success mt-4 shadow-sm rounded-4">
<div class="fw-bold mb-1">Buchungslink erstellt</div>
<div class="mb-2">
{% if mail_service_enabled %}
Teilen Sie diesen Link mit den Teilnehmenden oder versenden Sie ihn direkt per E-Mail.
{% else %}
Der E-Mail-Service ist deaktiviert. Teilen Sie diesen Link manuell mit den Teilnehmenden.
{% endif %}
</div>
<a href="{{ generated_link }}" class="d-inline-block text-break mb-3">{{ generated_link }}</a>
<!-- PDF Export per ReportLab -->
<div class="pt-3 border-top border-success-subtle">
<div class="fw-semibold mb-1">Einladungsbrief (inkl. QR-Code)</div>
<p class="small text-muted mb-2">Laden Sie einen automatisch generierten Brief herunter, der den Buchungslink und einen passenden QR-Code enthält.</p>
<a href="{{ url_for('terminplaner.export_pdf_brief', plan_id=plan_id) }}" class="btn btn-outline-success btn-sm">
📄 Brief als PDF herunterladen
</a>
</div>
{% if calendar_link %}
<div class="mt-3 pt-3 border-top border-success-subtle">
<div class="fw-semibold mb-1">Kalendereintrag</div>
<a href="{{ calendar_link }}" class="btn btn-outline-primary btn-sm">.ics herunterladen</a>
</div>
{% endif %}
</div>
{% endif %}
<!-- ENDE NEUER BLOCK -->
<!-- Info-Karten -->
<div class="row g-4 mt-1">
<div class="col-12 col-md-4">
<div class="card h-100 shadow-sm border-0 rounded-4">
@@ -59,6 +94,7 @@
<p class="mb-0 text-muted">Sie können Termine anlegen, den Kalender prüfen und Buchungslinks verteilen.</p>
</div>
<!-- "Tabelle" (Liste) der kommenden Termine -->
<div class="mt-4 p-4 rounded-4 bg-white shadow-sm">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-2 mb-3">
<h2 class="h5 fw-bold mb-0">Kommende Termine</h2>
@@ -84,10 +120,18 @@
<div class="text-lg-end">
<div class="small mb-2">Gebucht: <strong>{{ event.slots_booked }}</strong> / {{ event.slots_total }} | Frei: <strong>{{ event.slots_left }}</strong></div>
<div class="d-flex flex-wrap gap-2 justify-content-lg-end">
<a class="btn btn-sm btn-primary" href="{{ event.link }}" target="_blank" rel="noopener">Client-Link öffnen</a>
<!-- Hinzugefügt: Direkter PDF-Export auch bei bestehenden Plänen -->
<a class="btn btn-sm btn-outline-success" href="{{ url_for('terminplaner.export_pdf_brief', plan_id=event.appointment_id) }}" title="Brief herunterladen">
📄 PDF
</a>
<a class="btn btn-sm btn-primary" href="{{ event.link }}" target="_blank" rel="noopener">Client-Link</a>
{% if event.calendar_link %}
<a class="btn btn-sm btn-outline-primary" href="{{ event.calendar_link }}">.ics</a>
{% endif %}
<form method="post" action="{{ url_for('terminplaner.delete_appointment', appointment_id=event.appointment_id, tenant=tenant_id) }}" class="d-inline" onsubmit="return confirm('Diesen Terminplan wirklich löschen?');">
<button type="submit" class="btn btn-sm btn-outline-danger">Entfernen</button>
</form>
@@ -105,4 +149,4 @@
</div>
</div>
</div>
{% endblock %}
{% endblock %}