From 831ec7e71c396008afb1f5a2beb7e73e197d36ac Mon Sep 17 00:00:00 2001 From: Floke Date: Tue, 7 Apr 2026 18:10:46 +0000 Subject: [PATCH] [32788f42] Keine Zusammenfassung angegeben. Keine Zusammenfassung angegeben. --- .dev_session/SESSION_INFO | 2 +- fotograf-de-scraper/README.md | 26 +++++---- fotograf-de-scraper/backend/main.py | 57 ++++++++++++++++--- fotograf-de-scraper/frontend/src/App.tsx | 71 +++++++++++------------- 4 files changed, 97 insertions(+), 59 deletions(-) diff --git a/.dev_session/SESSION_INFO b/.dev_session/SESSION_INFO index a47dd0b49..24ca2c552 100644 --- a/.dev_session/SESSION_INFO +++ b/.dev_session/SESSION_INFO @@ -1 +1 @@ -{"task_id": "2f988f42-8544-800e-abc1-d1b1c56ade4d", "token": "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", "readme_path": "readme.md", "session_start_time": "2026-03-25T12:11:18.223098"} \ No newline at end of file +{"task_id": "32788f42-8544-80e1-a13a-c26114cf9b34", "token": "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", "readme_path": "readme.md", "session_start_time": "2026-04-07T18:10:43.355316"} \ No newline at end of file diff --git a/fotograf-de-scraper/README.md b/fotograf-de-scraper/README.md index cab431d90..b7f40b88a 100644 --- a/fotograf-de-scraper/README.md +++ b/fotograf-de-scraper/README.md @@ -21,29 +21,30 @@ Der Service besteht aus zwei Hauptkomponenten: ## ✨ Core Features ### Feature 1: Teilnehmerlisten (Vollständig) -Automatisierter Workflow zum Download und Formatieren der Anmeldelisten von `fotograf.de` als sortiertes PDF (Klassen/Gruppen) inkl. "Kinderfotos Erding" Branding. +Automatisierter Workflow zum Download und Formatieren der Anmeldelisten von `fotograf.de` als sortiertes PDF inkl. "Kinderfotos Erding" Branding. +* **Dynamische Terminologie:** Automatische Anpassung des Wordings basierend auf dem Profil (Kiga: "Kinder"/"Gruppen" vs. Schule: "Schüler"/"Klassen"). +* **Intelligentes Datum:** Extraktion des echten Auftragsdatums aus der Datenbank mit automatischer Erweiterung auf einen 2-Tages-Zeitraum (z.B. "15. + 16.04.2026"). ### Feature 2: Shooting-Planung (QR-Karten & Terminliste) Spezielles Modul für Familien-Mini-Shootings, direkt integriert in die Auftragsdetails: * **Dynamische Event-Auswahl:** Wähle direkt aus deinen Calendly-Event-Typen (z.B. "Neuching") aus. +* **Termin-Filter:** Zeigt nur noch aktuelle und zukünftige Buchungen an (alte recycelte Termine werden ignoriert). * **QR-Karten-Andruck:** * Präzises Overlay von Name, Kinderanzahl und Uhrzeit auf vorbereitete QR-Code-Bögen. - * **Korrektur:** Y-Achse um 9mm nach unten verschoben für perfekten Sitz auf den Linien. * **Einwilligungs-Checkbox (☑):** Automatischer Andruck eines Häkchens, wenn in Calendly der Veröffentlichung zugestimmt wurde. -* **Termin-Übersichtsliste (Neu):** - * Generiert eine A4-Tabelle für den Shooting-Tag. - * **6-Minuten-Takt:** Erzeugt automatisch ein Raster basierend auf dem ersten und letzten Termin, füllt Lücken für nicht gebuchte Slots leer auf. - * Inklusive Spalten für Name, Kinder, Veröffentlichungs-Status und einer Checkbox zum manuellen Abhaken. +* **Termin-Übersichtsliste:** + * Generiert eine A4-Tabelle für den Shooting-Tag im 6-Minuten-Takt. + * Füllt Lücken für nicht gebuchte Slots automatisch leer auf. -### Feature 3: Nachfass-E-Mails (Geplant) +### Feature 3: Nachfass-E-Mails (Vollständig) * Identifizierung von Käufern/Nicht-Käufern zur Generierung von CSV-Listen für den Supermailer. ### Feature 4: Verkaufs-Statistiken (Vollständig) * Detaillierte Analyse des Kaufverhaltens pro Album mit Echtzeit-Fortschrittsanzeige im Browser. -## 🛠️ Technische Details & Fixes (März 2026) +## 🛠️ Technische Details & Fixes (April 2026) * **Zeitzonen:** Automatische Konversion von Calendly-UTC-Zeiten in die lokale Zeit (`Europe/Berlin`). -* **Pagination Fix:** Das Backend blättert nun durch alle Calendly-Seiten, um sicherzustellen, dass keine Buchungen (auch bei großen Event-Historien) übersehen werden. +* **Pagination Fix:** Das Backend blättert durch alle Calendly-Seiten für lückenlose Daten. * **Logo-Integration:** Dynamisches Einbetten des Firmenlogos in alle generierten Dokumente. ## 🚀 Deployment & Konfiguration @@ -52,9 +53,10 @@ Der Service wird über eine eigene `docker-compose.yml` im Unterverzeichnis gest ### Umgebungsvariablen (`.env`) Folgende Variablen müssen in der `.env` im Verzeichnis `/fotograf-de-scraper/` definiert sein: -* `KIGA_USER` / `KIGA_PW` / `SCHULE_USER` / `SCHULE_PW`: Logins. +* `KIGA_USER` / `KIGA_PW` / `SCHULE_USER` / `SCHULE_PW`: Logins für Fotograf.de. * `CALENDLY_TOKEN`: Personal Access Token (JWT) von Calendly. ### URLs & Ports -* **Frontend:** `http://:3009` | **Backend:** `http://:8002` -* **Persistenz:** Datenbank unter `./backend/data/fotograf_jobs.db`. +* **Scraper Frontend:** `http://192.168.178.6:3009` +* **Zentrales Dashboard:** `http://192.168.178.6:8090` +* **Persistenz:** Datenbank unter `./backend/data/fotograf_jobs.db`. \ No newline at end of file diff --git a/fotograf-de-scraper/backend/main.py b/fotograf-de-scraper/backend/main.py index 94cc33c5e..cdc69964b 100644 --- a/fotograf-de-scraper/backend/main.py +++ b/fotograf-de-scraper/backend/main.py @@ -36,6 +36,22 @@ logging.basicConfig( ) logger = logging.getLogger("fotograf-scraper") +def format_job_date(date_str: str) -> str: + import re + import datetime + # Sucht nach einem Datum im Format DD.MM.YYYY + match = re.search(r'(\d{2})\.(\d{2})\.(\d{4})', date_str) + if match: + try: + day, month, year = match.groups() + dt = datetime.datetime(int(year), int(month), int(day)) + next_day = dt + datetime.timedelta(days=1) + # Format: 15. + 16.04.2026 + return f"{dt.day:02d}. + {next_day.strftime('%d.%m.%Y')}" + except Exception: + pass + return date_str + # Load environment variables load_dotenv() @@ -119,8 +135,8 @@ def generate_pdf_from_csv(csv_path: str, institution: str, date_info: str, list_ df.columns = df.columns.str.strip().str.replace("\"", "") logger.debug(f"CSV Columns: {list(df.columns)}") - group_label = "Gruppe" if list_type == 'k' else "Klasse" - person_label_plural = "Kinder" if list_type == 'k' else "Schüler" + group_label = "Gruppe" if list_type.startswith('kiga') else "Klasse" + person_label_plural = "Kinder" if list_type.startswith('kiga') else "Schüler" col_mapping = {} for col in df.columns: @@ -806,13 +822,32 @@ async def generate_appointment_list(job_id: str, event_type_name: str, db: Sessi if not raw_events: return JSONResponse(status_code=404, content={"message": "Keine passenden Termine für diesen Event-Typ gefunden."}) + # Filter out old events (keep only today and future) + from zoneinfo import ZoneInfo + now_berlin = datetime.datetime.now(ZoneInfo("Europe/Berlin")) + midnight_today = now_berlin.replace(hour=0, minute=0, second=0, microsecond=0) + + future_events = [] + for event in raw_events: + try: + start_dt = datetime.datetime.fromisoformat(event['start_time'].replace('Z', '+00:00')) + start_dt_berlin = start_dt.astimezone(ZoneInfo("Europe/Berlin")) + if start_dt_berlin >= midnight_today: + future_events.append(event) + except Exception as e: + logger.warning(f"Error parsing date for event: {e}") + future_events.append(event) # Fallback: keep event if date parsing fails + + if not future_events: + return JSONResponse(status_code=404, content={"message": "Keine zukünftigen 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_clean, event_type_name, output_path) + generate_appointment_overview_pdf(future_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}") @@ -939,7 +974,7 @@ async def download_task_csv(task_id: str): raise HTTPException(status_code=500, detail="CSV Export fehlgeschlagen.") @app.get("/api/jobs/{job_id}/generate-pdf") -async def generate_pdf(job_id: str, account_type: str): +async def generate_pdf(job_id: str, account_type: str, db: Session = Depends(get_db)): logger.info(f"API Request: Generate PDF for job {job_id} ({account_type})") username = os.getenv(f"{account_type.upper()}_USER") password = os.getenv(f"{account_type.upper()}_PW") @@ -966,8 +1001,9 @@ async def generate_pdf(job_id: str, account_type: str): # 1.5 Click on the "Personen" tab logger.info("Clicking on 'Personen' tab...") - personen_tab = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "[data-qa-id='link:photo-jobs-tabs-names_list']"))) - personen_tab.click() + personen_tab = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "[data-qa-id='link:photo-jobs-tabs-names_list']"))) + # Use JS click to avoid 'element click intercepted' errors from loading overlays + driver.execute_script("arguments[0].click();", personen_tab) # Wait for the export button to become present on the new tab logger.info("Waiting for Export Dropdown...") @@ -1013,10 +1049,17 @@ async def generate_pdf(job_id: str, account_type: str): output_pdf_name = f"Listen_{job_id}.pdf" output_pdf_path = os.path.join(temp_dir, output_pdf_name) + # Hole Auftragsdatum aus der Datenbank, falls vorhanden + job_record = db.query(DBJob).filter(DBJob.id == job_id).first() + if job_record and job_record.date: + final_date_info = format_job_date(job_record.date) + else: + final_date_info = datetime.datetime.now().strftime("%d.%m.%Y") + generate_pdf_from_csv( csv_path=csv_file, institution=institution, - date_info=datetime.datetime.now().strftime("%d.%m.%Y"), + date_info=final_date_info, list_type=account_type, output_path=output_pdf_path ) diff --git a/fotograf-de-scraper/frontend/src/App.tsx b/fotograf-de-scraper/frontend/src/App.tsx index db8d3222b..d18ecdf51 100644 --- a/fotograf-de-scraper/frontend/src/App.tsx +++ b/fotograf-de-scraper/frontend/src/App.tsx @@ -171,24 +171,18 @@ function App() { setProcessingJobId(job.id); setError(null); try { - const response = await fetch(`${API_BASE_URL}/api/jobs/${job.id}/generate-pdf?account_type=${activeTab}`); - if (!response.ok) { - const errData = await response.json(); - throw new Error(errData.detail || 'PDF Generierung fehlgeschlagen'); - } + // Direkter Download über die URL, umgeht das Blob/Mixed-Content-Sicherheitsproblem in Chrome. + const downloadUrl = `${API_BASE_URL}/api/jobs/${job.id}/generate-pdf?account_type=${activeTab}`; + window.open(downloadUrl, '_blank'); - const blob = await response.blob(); - const url = window.URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = `Listen_${job.name.replace(/\s+/g, "_")}.pdf`; - document.body.appendChild(a); - a.click(); - a.remove(); - window.URL.revokeObjectURL(url); + // Wir setzen einen künstlichen Timeout für den Lade-Indikator, + // da wir bei window.open nicht wissen, wann der Download fertig ist. + setTimeout(() => { + setProcessingJobId(null); + }, 3000); + } catch (err: any) { setError(`PDF Fehler (${job.name}): ${err.message}`); - } finally { setProcessingJobId(null); } }; @@ -228,8 +222,12 @@ function App() { a.download = `QR_Karten_Andruck_${job.id}.pdf`; document.body.appendChild(a); a.click(); - a.remove(); - window.URL.revokeObjectURL(url); + + // Delay removal and revocation to ensure download starts in all browsers + setTimeout(() => { + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + }, 100); } catch (err: any) { setError(err.message); } finally { @@ -241,28 +239,14 @@ function App() { setIsListGenerating(true); setError(null); try { - const response = await fetch(`${API_BASE_URL}/api/jobs/${job.id}/appointment-list?event_type_name=${encodeURIComponent(selectedEventType)}`); + const downloadUrl = `${API_BASE_URL}/api/jobs/${job.id}/appointment-list?event_type_name=${encodeURIComponent(selectedEventType)}`; + window.open(downloadUrl, '_blank'); - if (!response.ok) { - if (response.status === 404) { - throw new Error("Keine passenden Calendly-Termine gefunden."); - } - const errData = await response.json(); - throw new Error(errData.detail || 'PDF Generierung fehlgeschlagen'); - } - - const blob = await response.blob(); - const url = window.URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = `Terminuebersicht_${job.name.replace(/\s+/g, "_")}.pdf`; - document.body.appendChild(a); - a.click(); - a.remove(); - window.URL.revokeObjectURL(url); + setTimeout(() => { + setIsListGenerating(false); + }, 3000); } catch (err: any) { setError(`Listen-Fehler (${job.name}): ${err.message}`); - } finally { setIsListGenerating(false); } }; @@ -300,9 +284,18 @@ function App() { {/* Top Navigation Bar */}
-
- 📸 -

Fotograf.de ERP

+
+
+ 📸 +

Fotograf.de ERP

+
+ + + Zum Dashboard +
{/* Main Tabs */}