[34288f42] Keine Zusammenfassung angegeben.

Keine Zusammenfassung angegeben.
This commit is contained in:
2026-04-14 08:37:51 +00:00
parent f148f40d9e
commit 2592607b04
13 changed files with 419 additions and 5 deletions

View File

@@ -1193,4 +1193,94 @@ async def generate_pdf(job_id: str, account_type: str, db: Session = Depends(get
finally:
if driver:
logger.debug("Closing driver.")
driver.quit()
driver.quit()
@app.get("/api/jobs/{job_id}/siblings-list")
async def generate_siblings_list(job_id: str, account_type: str, event_type_name: str = "", db: Session = Depends(get_db)):
logger.info(f"API Request: Generate siblings list for job {job_id}")
username = os.getenv(f"{account_type.upper()}_USER")
password = os.getenv(f"{account_type.upper()}_PW")
api_token = os.getenv("CALENDLY_TOKEN")
if not api_token:
raise HTTPException(status_code=400, detail="Calendly API token missing.")
# Get Calendly events
from qr_generator import get_calendly_events_raw
try:
# Fetch ALL events to ensure we don't miss siblings due to event name mismatches
calendly_events = get_calendly_events_raw(api_token, event_type_name=None)
logger.info(f"Fetched {len(calendly_events)} total events from Calendly for siblings check.")
except Exception as e:
logger.error(f"Error fetching Calendly events: {e}")
calendly_events = []
with tempfile.TemporaryDirectory() as temp_dir:
logger.debug(f"Using temp directory: {temp_dir}")
driver = setup_driver(download_path=temp_dir)
try:
if not login(driver, username, password):
raise HTTPException(status_code=401, detail="Login failed.")
job_url = f"https://app.fotograf.de/config_jobs_settings/index/{job_id}"
driver.get(job_url)
wait = WebDriverWait(driver, 30)
try:
institution = driver.find_element(By.TAG_NAME, "h1").text.strip()
except:
institution = "Fotoauftrag"
personen_tab = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "[data-qa-id='link:photo-jobs-tabs-names_list']")))
driver.execute_script("arguments[0].click();", personen_tab)
export_btn = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, SELECTORS["export_dropdown"])))
driver.execute_script("arguments[0].scrollIntoView(true);", export_btn)
time.sleep(1)
driver.execute_script("arguments[0].click();", export_btn)
time.sleep(2)
try:
csv_btn = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, SELECTORS["export_csv_link"])))
driver.execute_script("arguments[0].click();", csv_btn)
except TimeoutException:
raise HTTPException(status_code=500, detail="CSV Export Button nicht gefunden.")
timeout = 45
start_time = time.time()
csv_file = None
while time.time() - start_time < timeout:
files = os.listdir(temp_dir)
csv_files = [f for f in files if f.endswith('.csv')]
if csv_files:
csv_file = os.path.join(temp_dir, csv_files[0])
break
time.sleep(1)
if not csv_file:
raise HTTPException(status_code=500, detail="CSV Download fehlgeschlagen.")
output_pdf_name = f"Geschwisterliste_{job_id}.pdf"
output_pdf_path = os.path.join(temp_dir, output_pdf_name)
from siblings_logic import generate_siblings_pdf_from_csv
generate_siblings_pdf_from_csv(
csv_path=csv_file,
institution=institution,
calendly_events=calendly_events,
list_type=account_type,
output_path=output_pdf_path
)
final_storage = os.path.join("/tmp", output_pdf_name)
shutil.copy(output_pdf_path, final_storage)
return FileResponse(path=final_storage, filename=output_pdf_name, media_type="application/pdf")
except HTTPException as he:
raise he
except Exception as e:
logger.exception("Error generating siblings list")
raise HTTPException(status_code=500, detail=str(e))
finally:
if driver: driver.quit()

View File

@@ -0,0 +1,125 @@
import pandas as pd
import os
import logging
from jinja2 import Environment, FileSystemLoader
from collections import defaultdict
from main import get_berlin_now_str, get_logo_base64
from weasyprint import HTML
logger = logging.getLogger("fotograf-scraper")
def generate_siblings_pdf_from_csv(csv_path: str, institution: str, calendly_events: list, list_type: str, output_path: str):
logger.info(f"Generating Siblings PDF for {institution} from {csv_path}")
df = None
for sep in [";", ","]:
try:
test_df = pd.read_csv(csv_path, sep=sep, encoding="utf-8-sig", nrows=5)
if len(test_df.columns) > 1:
df = pd.read_csv(csv_path, sep=sep, encoding="utf-8-sig")
break
except Exception as e:
continue
if df is None:
try:
df = pd.read_csv(csv_path, sep=";", encoding="latin1")
except:
raise Exception("CSV konnte nicht gelesen werden.")
df.columns = df.columns.str.strip().str.replace("\"", "")
# Identify Email Column
email_col = next((c for c in df.columns if "email" in c.lower()), None)
if not email_col:
email_col = next((c for c in df.columns if "e-mail" in c.lower()), None)
if not email_col:
logger.warning("No email column found. Siblings logic cannot run.")
families = []
else:
# Columns mappings
group_col = next((c for c in df.columns if c.lower() in ["gruppe", "klasse", "group", "class"]), None)
lastname_col = next((c for c in df.columns if "nachname" in c.lower()), None)
firstname_col = next((c for c in df.columns if "vorname" in c.lower()), None)
wunsch_col = next((c for c in df.columns if "familie" in c.lower() or "geschwister" in c.lower() and "fotos" in c.lower()), None)
if not wunsch_col:
wunsch_col = next((c for c in df.columns if "familie / geschwister" in c.lower()), None)
# Build Calendly Dictionary for fast lookup (Email -> Time)
from zoneinfo import ZoneInfo
import datetime
calendly_map = {}
now_berlin = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
midnight_today = now_berlin.replace(hour=0, minute=0, second=0, microsecond=0)
for event in calendly_events:
try:
start_dt = datetime.datetime.fromisoformat(event['start_time'].replace('Z', '+00:00'))
start_dt = start_dt.astimezone(ZoneInfo("Europe/Berlin"))
# Allow all events for siblings logic, regardless of date, just to be sure we match them
calendly_map[event['invitee_email'].lower().strip()] = start_dt.strftime("%d.%m. %H:%M")
except:
pass
families_dict = defaultdict(list)
df = df.fillna("")
# Group by email
for _, row in df.iterrows():
email = str(row[email_col]).strip().lower()
if email and "@" in email:
families_dict[email].append(row)
families = []
for email, rows in families_dict.items():
if len(rows) > 1: # SIBLINGS DETECTED
family_last_name = str(rows[0][lastname_col]).strip() if lastname_col else "Unbekannt"
children = []
for r in rows:
child_first = str(r[firstname_col]).strip() if firstname_col else ""
child_group = str(r[group_col]).strip() if group_col else ""
children.append({"vorname": child_first, "gruppe": child_group})
# Check fotograf wunsch
fotograf_wunsch = False
if wunsch_col:
for r in rows:
val = str(r[wunsch_col]).lower()
if "ja" in val or "familien" in val or "geschwister" in val:
fotograf_wunsch = True
break
calendly_time = calendly_map.get(email, None)
families.append({
"nachname": family_last_name,
"children": children,
"fotograf_wunsch": fotograf_wunsch,
"calendly_time": calendly_time
})
# Sort by last name
families.sort(key=lambda x: x["nachname"])
template_dir = os.path.join(os.path.dirname(__file__), "templates")
env = Environment(loader=FileSystemLoader(template_dir))
template = env.get_template("siblings_list.html")
current_time = get_berlin_now_str()
logo_base64 = get_logo_base64()
render_context = {
"institution": institution,
"current_time": current_time,
"logo_base64": logo_base64,
"families": families
}
html_out = template.render(render_context)
pdf = HTML(string=html_out).write_pdf()
with open(output_path, "wb") as f:
f.write(pdf)
logger.info(f"Siblings PDF saved to {output_path}")

View File

@@ -0,0 +1,90 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
@page { size: A4 portrait; margin: 20mm; }
body { font-family: Arial, sans-serif; font-size: 11pt; }
.header { margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center; }
.institution-name { font-weight: bold; font-size: 16pt; margin-bottom: 5px; }
.doc-title { font-size: 14pt; font-weight: bold; color: #4f46e5; margin-bottom: 15px; }
.date-info { font-size: 11pt; color: #555; }
table { width: 100%; border-collapse: collapse; margin-top: 15px; }
th { text-align: left; background-color: #f3f4f6; border-bottom: 2px solid #d1d5db; padding: 8px 5px; font-size: 10pt; }
td { padding: 8px 5px; border-bottom: 1px solid #e5e7eb; font-size: 10pt; vertical-align: top; }
.checkbox { width: 20px; height: 20px; border: 1.5px solid #000; border-radius: 3px; display: inline-block; }
.footer { position: fixed; bottom: 0; left: 0; right: 0; display: flex; justify-content: space-between; font-size: 9pt; color: #888; }
.badge { display: inline-block; padding: 2px 6px; border-radius: 4px; font-size: 8.5pt; font-weight: bold; background-color: #e0e7ff; color: #3730a3; margin-left: 5px; }
.badge-time { background-color: #d1fae5; color: #065f46; font-size: 10pt; }
</style>
</head>
<body>
<div class="header" style="display: flex; justify-content: space-between; align-items: center;">
<div>
<div class="institution-name">{{ institution }}</div>
<div class="doc-title">Geschwisterliste (Einrichtungsintern)</div>
<div class="date-info">Generiert am: {{ current_time }}</div>
</div>
{% if logo_base64 %}
<div>
<img src="data:image/png;base64,{{ logo_base64 }}" alt="Logo" style="max-height: 50px;">
</div>
{% endif %}
</div>
<table>
<thead>
<tr>
<th style="width: 20%">Nachname</th>
<th style="width: 35%">Kinder in der Einrichtung (Gruppe)</th>
<th style="width: 15%">Wunsch Online</th>
<th style="width: 20%">Termin (Calendly)</th>
<th style="width: 10%; text-align: center;">Erledigt</th>
</tr>
</thead>
<tbody>
{% for family in families %}
<tr>
<td style="font-weight: bold;">{{ family.nachname }}</td>
<td>
{% for child in family.children %}
<div style="margin-bottom: 4px;">
{{ child.vorname }} <span style="color: #666; font-size: 9pt;">({{ child.gruppe }})</span>
</div>
{% endfor %}
</td>
<td>
{% if family.fotograf_wunsch %}
<span style="color: #059669; font-weight: bold;">Ja</span>
{% else %}
<span style="color: #9ca3af;">-</span>
{% endif %}
</td>
<td>
{% if family.calendly_time %}
<span class="badge badge-time">{{ family.calendly_time }}</span>
{% else %}
<span style="color: #9ca3af;">-</span>
{% endif %}
</td>
<td style="text-align: center;">
<div class="checkbox"></div>
</td>
</tr>
{% else %}
<tr>
<td colspan="5" style="text-align: center; padding: 20px; color: #666;">Keine internen Geschwisterkinder in dieser Einrichtung gefunden.</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="footer">
<div>Geschwisterliste</div>
<div>Kinderfotos Erding | www.kinderfotos-erding.de</div>
</div>
</body>
</html>