diff --git a/fotograf-de-scraper/backend/assets/OpenSans-Regular.ttf b/fotograf-de-scraper/backend/assets/OpenSans-Regular.ttf new file mode 100644 index 000000000..8529c432c Binary files /dev/null and b/fotograf-de-scraper/backend/assets/OpenSans-Regular.ttf differ diff --git a/fotograf-de-scraper/backend/main.py b/fotograf-de-scraper/backend/main.py index a4fa7164d..b9b05dcbe 100644 --- a/fotograf-de-scraper/backend/main.py +++ b/fotograf-de-scraper/backend/main.py @@ -182,6 +182,7 @@ def generate_appointment_overview_pdf(raw_events: list, job_name: str, event_typ 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', []): @@ -189,11 +190,12 @@ def generate_appointment_overview_pdf(raw_events: list, job_name: str, event_typ 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(): + elif "veröffentlichen" in q_text or "bilder" in q_text: + if "ja" in a_text.lower(): has_consent = True parsed_events.append({ + "dt": start_dt, "name": event['invitee_name'], "children": num_children, @@ -211,6 +213,7 @@ def generate_appointment_overview_pdf(raw_events: list, job_name: str, event_typ min_dt = events[0]['dt'] max_dt = events[-1]['dt'] + slots = [] curr_dt = min_dt event_idx = 0 @@ -230,7 +233,8 @@ def generate_appointment_overview_pdf(raw_events: list, job_name: str, event_typ "name": e['name'], "children": e['children'], "consent": e['consent'], - "booked": True + "booked": True, + "dt": e['dt'] }) else: if curr_dt <= max_dt: @@ -239,12 +243,54 @@ def generate_appointment_overview_pdf(raw_events: list, job_name: str, event_typ "name": "", "children": "", "consent": False, - "booked": False + "booked": False, + "dt": curr_dt }) curr_dt = next_dt - final_grouped[date_str] = slots + # Compress empty slots if there are more than 2 in a row + compressed_slots = [] + empty_streak = [] + + for slot in slots: + if slot["booked"]: + if len(empty_streak) > 2: + start_time = empty_streak[0]["time_str"] + end_dt = empty_streak[-1]["dt"] + datetime.timedelta(minutes=6) + end_time = end_dt.strftime("%H:%M") + compressed_slots.append({ + "is_compressed": True, + "time_str": f"{start_time} - {end_time}", + "name": "--- Freie Zeit / Pause ---", + "children": "", + "consent": False, + "booked": False + }) + else: + compressed_slots.extend(empty_streak) + empty_streak = [] + compressed_slots.append(slot) + else: + empty_streak.append(slot) + + if len(empty_streak) > 2: + start_time = empty_streak[0]["time_str"] + end_dt = empty_streak[-1]["dt"] + datetime.timedelta(minutes=6) + end_time = end_dt.strftime("%H:%M") + compressed_slots.append({ + "is_compressed": True, + "time_str": f"{start_time} - {end_time}", + "name": "--- Freie Zeit / Pause ---", + "children": "", + "consent": False, + "booked": False + }) + else: + compressed_slots.extend(empty_streak) + + final_grouped[date_str] = compressed_slots + template_dir = os.path.join(os.path.dirname(__file__), "templates") env = Environment(loader=FileSystemLoader(template_dir)) @@ -582,9 +628,15 @@ async def generate_appointment_list(job_id: str, event_type_name: str, db: Sessi 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() + # 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}" + import re + job_name_clean = re.sub(r'^JOB\d+\s*', '', job_name) + # 2. Fetch raw Calendly events (no date range needed, defaults to +6 months) try: @@ -603,7 +655,7 @@ async def generate_appointment_list(job_id: str, event_type_name: str, db: Sessi output_path = os.path.join(temp_dir, output_name) try: - generate_appointment_overview_pdf(raw_events, job_name, event_type_name, output_path) + generate_appointment_overview_pdf(raw_events, job_name_clean, 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}") diff --git a/fotograf-de-scraper/backend/qr_generator.py b/fotograf-de-scraper/backend/qr_generator.py index 3e40091bd..4f4b4edbe 100644 --- a/fotograf-de-scraper/backend/qr_generator.py +++ b/fotograf-de-scraper/backend/qr_generator.py @@ -149,6 +149,7 @@ def get_calendly_events(api_token: str, start_time: str = None, end_time: str = has_consent = False questions_and_answers = item.get('questions_and_answers', []) + for q_a in questions_and_answers: q_text = q_a.get('question', '').lower() a_text = q_a.get('answer', '') @@ -157,19 +158,13 @@ def get_calendly_events(api_token: str, start_time: str = None, end_time: str = num_children = a_text elif "nachricht" in q_text or "anmerkung" in q_text: additional_notes = a_text - elif "schöne bilder" in q_text and "website veröffentlichen" in q_text: - if "ja, gerne" in a_text.lower(): + elif "veröffentlichen" in q_text or "bilder" in q_text: + if "ja" in a_text.lower(): has_consent = True - # Construct the final string: "[☑] Name, X Kinder // HH:MM Uhr (Notes)" - # matching: Halime Türe, 1 Kind // 12:00 Uhr - final_text = "" - if has_consent: - # We use a placeholder character or string that we will handle in overlay_text_on_pdf - # because standard Helvetica doesn't support Unicode checkbox. - final_text += "☑ " - - final_text += f"{name}" + + # Construct the final string: "Name, X Kinder // HH:MM Uhr ☑" + final_text = f"{name}" if num_children: final_text += f", {num_children}" @@ -177,8 +172,12 @@ def get_calendly_events(api_token: str, start_time: str = None, end_time: str = if additional_notes: final_text += f" ({additional_notes})" + + if has_consent: + final_text += " ☑" formatted_data.append(final_text) + logger.info(f"Processed {len(formatted_data)} invitees.") return formatted_data @@ -219,6 +218,13 @@ def overlay_text_on_pdf(base_pdf_path: str, output_pdf_path: str, texts: list): # We need to process pairs of texts for each page text_pairs = [texts[i:i+2] for i in range(0, len(texts), 2)] + + # Load OpenSans font to support UTF-8 extended characters + from reportlab.pdfbase.ttfonts import TTFont + from reportlab.pdfbase import pdfmetrics + font_path = os.path.join(os.path.dirname(__file__), "assets", "OpenSans-Regular.ttf") + pdfmetrics.registerFont(TTFont('OpenSans', font_path)) + for page_idx, pair in enumerate(text_pairs): if page_idx >= total_pages: break # Safety first @@ -228,28 +234,27 @@ def overlay_text_on_pdf(base_pdf_path: str, output_pdf_path: str, texts: list): can = canvas.Canvas(packet, pagesize=A4) # Draw the text. - # We handle the "☑" character manually since Helvetica might not support it. def draw_text_with_checkbox(can, x, y, text): - current_x = x - if text.startswith("☑ "): - # Draw a checkbox manually + can.setFont("OpenSans", 12) + if text.endswith(" ☑"): + clean_text = text[:-2] # remove the checkmark part + can.drawString(x, y, clean_text) + + # Calculate width to place the checkbox right after the text + text_width = can.stringWidth(clean_text, "OpenSans", 12) + box_x = x + text_width + 8 + size = 10 - # Draw box (baseline adjustment to align with text) - can.rect(x, y - 1, size, size) - # Draw checkmark + can.rect(box_x, y - 1, size, size) can.setLineWidth(1.5) - can.line(x + 2, y + 3, x + 4.5, y + 0.5) - can.line(x + 4.5, y + 0.5, x + 8.5, y + 7) + can.line(box_x + 2, y + 3, box_x + 4.5, y + 0.5) + can.line(box_x + 4.5, y + 0.5, box_x + 8.5, y + 7) can.setLineWidth(1) - - # Move text X position to the right - current_x += size + 5 - text = text[2:] # Remove the "☑ " from string - - can.setFont("Helvetica", 12) - can.drawString(current_x, y, text) + else: + can.drawString(x, y, text) if len(pair) > 0: + draw_text_with_checkbox(can, x_pos, y_pos_1, pair[0]) if len(pair) > 1: draw_text_with_checkbox(can, x_pos, y_pos_2, pair[1]) diff --git a/fotograf-de-scraper/backend/templates/appointment_list.html b/fotograf-de-scraper/backend/templates/appointment_list.html index 32aeafca5..ae431933f 100644 --- a/fotograf-de-scraper/backend/templates/appointment_list.html +++ b/fotograf-de-scraper/backend/templates/appointment_list.html @@ -49,6 +49,7 @@ margin: 0 0 10px 0; color: #34495e; } + .date-header { background-color: #ecf0f1; padding: 8px 12px; @@ -57,6 +58,10 @@ font-weight: bold; font-size: 13pt; border-left: 4px solid #3498db; + page-break-before: always; + } + .first-date-header { + page-break-before: avoid; } table { width: 100%; @@ -64,23 +69,25 @@ margin-bottom: 20px; page-break-inside: auto; } - tr { - page-break-inside: avoid; - page-break-after: auto; - } th, td { border: 1px solid #bdc3c7; - padding: 8px; + padding: 6px 8px; /* Narrower rows */ text-align: left; vertical-align: middle; } - th { - background-color: #f8f9fa; - font-weight: bold; - color: #2c3e50; + .empty-row td { + height: 25px; /* Narrower empty rows */ + color: transparent; } - .time-col { width: 12%; white-space: nowrap; } - .family-col { width: 35%; } + .compressed-row td { + background-color: #fcfcfc; + color: #7f8c8d !important; + font-style: italic; + text-align: center; + } + .time-col { width: 14%; white-space: nowrap; font-weight: bold; } + .family-col { width: 33%; } + .children-col { width: 15%; text-align: center; } .consent-col { width: 20%; text-align: center; } .done-col { width: 18%; text-align: center; } @@ -102,7 +109,14 @@ + + {% for date, slots in grouped_slots.items() %} + + {% if not loop.first %} +
+ {% endif %} +

{{ job_name }}

@@ -116,8 +130,7 @@
- {% for date, slots in grouped_slots.items() %} -
{{ date }}
+
{{ date }}
@@ -131,26 +144,36 @@ {% for slot in slots %} - - - - - - - + {% if slot.is_compressed %} + + + + + {% else %} + + + + + + + + {% endif %} {% endfor %}
{{ slot.time_str }}{{ slot.name if slot.booked else '' }}{{ slot.children if slot.booked else '' }} - -
{{ slot.time_str }}{{ slot.name }}
{{ slot.time_str }}{{ slot.name if slot.booked else '' }}{{ slot.children if slot.booked else '' }} + {% if slot.booked %} + + {% endif %} +
{% endfor %} + \ No newline at end of file