[34288f42] Keine Zusammenfassung angegeben.
Keine Zusammenfassung angegeben.
This commit is contained in:
@@ -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()
|
||||
|
||||
|
||||
125
fotograf-de-scraper/backend/siblings_logic.py
Normal file
125
fotograf-de-scraper/backend/siblings_logic.py
Normal 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}")
|
||||
90
fotograf-de-scraper/backend/templates/siblings_list.html
Normal file
90
fotograf-de-scraper/backend/templates/siblings_list.html
Normal 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>
|
||||
Reference in New Issue
Block a user