[32788f42] Add Termin-Übersicht feature, dynamic Event-Type selection, and refactor QR cards UI into Job Details

This commit is contained in:
2026-03-21 13:46:26 +00:00
parent c62db8a2ef
commit f72719b9a4
4 changed files with 468 additions and 138 deletions

View File

@@ -173,6 +173,97 @@ def generate_pdf_from_csv(csv_path: str, institution: str, date_info: str, list_
logger.info(f"Writing PDF to: {output_path}")
HTML(string=html_out).write_pdf(output_path)
def generate_appointment_overview_pdf(raw_events: list, job_name: str, event_type_name: str, output_path: str):
from collections import defaultdict
from zoneinfo import ZoneInfo
parsed_events = []
for event in raw_events:
start_dt = datetime.datetime.fromisoformat(event['start_time'].replace('Z', '+00:00'))
start_dt = start_dt.astimezone(ZoneInfo("Europe/Berlin"))
num_children = ""
has_consent = False
for qa in event.get('questions_and_answers', []):
q_text = qa.get('question', '').lower()
a_text = qa.get('answer', '')
if "wie viele kinder" in q_text:
num_children = a_text
elif "schöne bilder" in q_text and "website veröffentlichen" in q_text:
if "ja, gerne" in a_text.lower():
has_consent = True
parsed_events.append({
"dt": start_dt,
"name": event['invitee_name'],
"children": num_children,
"consent": has_consent
})
grouped = defaultdict(list)
for e in parsed_events:
date_str = e['dt'].strftime("%d.%m.%Y")
grouped[date_str].append(e)
final_grouped = {}
for date_str, events in grouped.items():
events.sort(key=lambda x: x['dt'])
min_dt = events[0]['dt']
max_dt = events[-1]['dt']
slots = []
curr_dt = min_dt
event_idx = 0
while curr_dt <= max_dt or event_idx < len(events):
next_dt = curr_dt + datetime.timedelta(minutes=6)
events_in_slot = []
while event_idx < len(events) and events[event_idx]['dt'] < next_dt:
events_in_slot.append(events[event_idx])
event_idx += 1
if events_in_slot:
for e in events_in_slot:
slots.append({
"time_str": e['dt'].strftime("%H:%M"),
"name": e['name'],
"children": e['children'],
"consent": e['consent'],
"booked": True
})
else:
if curr_dt <= max_dt:
slots.append({
"time_str": curr_dt.strftime("%H:%M"),
"name": "",
"children": "",
"consent": False,
"booked": False
})
curr_dt = next_dt
final_grouped[date_str] = slots
template_dir = os.path.join(os.path.dirname(__file__), "templates")
env = Environment(loader=FileSystemLoader(template_dir))
template = env.get_template("appointment_list.html")
current_time = datetime.datetime.now().strftime("%d.%m.%Y %H:%M Uhr")
logo_base64 = get_logo_base64()
render_context = {
"job_name": job_name,
"event_type_name": event_type_name or "Alle Events",
"current_time": current_time,
"logo_base64": logo_base64,
"grouped_slots": final_grouped
}
html_out = template.render(render_context)
HTML(string=html_out).write_pdf(output_path)
# --- Selenium Scraper Functions ---
def take_error_screenshot(driver, error_name):
@@ -410,10 +501,22 @@ from sqlalchemy.orm import Session
from database import get_db, Job as DBJob, engine, Base
import math
import uuid
from qr_generator import get_calendly_events, overlay_text_on_pdf
from qr_generator import get_calendly_events, overlay_text_on_pdf, get_calendly_event_types
# --- API Endpoints ---
@app.get("/api/calendly/event-types")
async def fetch_calendly_event_types():
api_token = os.getenv("CALENDLY_TOKEN")
if not api_token:
raise HTTPException(status_code=400, detail="Calendly API token missing.")
try:
types = get_calendly_event_types(api_token)
return {"event_types": types}
except Exception as e:
logger.error(f"Error fetching Calendly event types: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/calendly/events")
async def fetch_calendly_events(start_time: str, end_time: str, event_type_name: Optional[str] = None):
"""
@@ -434,8 +537,8 @@ async def fetch_calendly_events(start_time: str, end_time: str, event_type_name:
@app.post("/api/qr-cards/generate")
async def generate_qr_cards(
start_time: str = Form(...),
end_time: str = Form(...),
start_time: str = Form(None),
end_time: str = Form(None),
event_type_name: str = Form(None),
pdf_file: UploadFile = File(...)
):
@@ -472,6 +575,40 @@ async def generate_qr_cards(
logger.error(f"Error generating QR cards: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/jobs/{job_id}/appointment-list")
async def generate_appointment_list(job_id: str, event_type_name: str, db: Session = Depends(get_db)):
logger.info(f"API Request: Generate appointment list for job {job_id}, event_type '{event_type_name}'")
api_token = os.getenv("CALENDLY_TOKEN")
if not api_token:
raise HTTPException(status_code=400, detail="Calendly API token missing.")
# 1. Fetch job name from DB
job = db.query(DBJob).filter(DBJob.id == job_id).first()
job_name = job.name if job else f"Auftrag {job_id}"
# 2. Fetch raw Calendly events (no date range needed, defaults to +6 months)
try:
from qr_generator import get_calendly_events_raw
raw_events = get_calendly_events_raw(api_token, event_type_name=event_type_name)
except Exception as e:
logger.error(f"Error fetching raw Calendly events: {e}")
raise HTTPException(status_code=500, detail=str(e))
if not raw_events:
return JSONResponse(status_code=404, content={"message": "Keine passenden Termine für diesen Event-Typ gefunden."})
# 3. Generate PDF
temp_dir = tempfile.gettempdir()
output_name = f"Terminuebersicht_{job_id}_{datetime.datetime.now().strftime('%Y%m%d')}.pdf"
output_path = os.path.join(temp_dir, output_name)
try:
generate_appointment_overview_pdf(raw_events, job_name, event_type_name, output_path)
return FileResponse(path=output_path, filename=output_name, media_type="application/pdf")
except Exception as e:
logger.error(f"Error generating appointment overview pdf: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/health")
async def health_check():
return {"status": "ok"}