From d49f6d51f4d65357d8ddcbb56e17aee0a8cf99fa Mon Sep 17 00:00:00 2001 From: Floke Date: Sat, 18 Apr 2026 13:58:53 +0000 Subject: [PATCH] [34588f42] Feature: Globaler Sync-Button & Sofort-Statistik MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Globaler 'Daten abgleichen' Button im Modal-Header integriert. - Neue fast-stats API zeigt Statistiken sofort beim Öffnen des Modals (aus DB). - UI entrümpelt und Redundanzen entfernt. --- .dev_session/SESSION_INFO | 2 +- fotograf-de-scraper/backend/main.py | 138 ++++------------------- fotograf-de-scraper/frontend/src/App.tsx | 79 ++++++++----- 3 files changed, 76 insertions(+), 143 deletions(-) diff --git a/.dev_session/SESSION_INFO b/.dev_session/SESSION_INFO index b629bfc79..2c3483c5c 100644 --- a/.dev_session/SESSION_INFO +++ b/.dev_session/SESSION_INFO @@ -1 +1 @@ -{"task_id": "34588f42-8544-8046-85d4-d7895ed9b29c", "token": "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", "readme_path": "readme.md", "session_start_time": "2026-04-18T13:09:19.478670"} \ No newline at end of file +{"task_id": "34588f42-8544-8046-85d4-d7895ed9b29c", "token": "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", "readme_path": "readme.md", "session_start_time": "2026-04-18T13:58:52.409921"} \ No newline at end of file diff --git a/fotograf-de-scraper/backend/main.py b/fotograf-de-scraper/backend/main.py index 019524141..0299abde1 100644 --- a/fotograf-de-scraper/backend/main.py +++ b/fotograf-de-scraper/backend/main.py @@ -141,120 +141,6 @@ def get_logo_base64(): logger.warning(f"Logo file not found at {logo_path}") return None -def sync_job_participants(job_id: str, account_type: str, db: Session): - logger.info(f"Syncing participants for job {job_id} ({account_type})") - username = os.getenv(f"{account_type.upper()}_USER") - password = os.getenv(f"{account_type.upper()}_PW") - - with tempfile.TemporaryDirectory() as temp_dir: - driver = setup_driver(download_path=temp_dir) - try: - if not login(driver, username, password): - raise Exception("Login failed during sync.") - - # Navigate to job names list - job_url = f"https://app.fotograf.de/config_jobs_settings/index/{job_id}" - driver.get(job_url) - wait = WebDriverWait(driver, 20) - - 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].click();", export_btn) - time.sleep(1) - - csv_btn = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, SELECTORS["export_csv_link"]))) - driver.execute_script("arguments[0].click();", csv_btn) - - # Wait for download - csv_file = None - for _ in range(30): - files = [f for f in os.listdir(temp_dir) if f.endswith('.csv')] - if files: - csv_file = os.path.join(temp_dir, files[0]) - break - time.sleep(1) - - if not csv_file: - raise Exception("CSV download timeout during sync.") - - # Parse CSV - df = None - for sep in [";", ","]: - try: - df = pd.read_csv(csv_file, sep=sep, encoding="utf-8-sig") - if len(df.columns) > 1: break - except: continue - - if df is None: - df = pd.read_csv(csv_file, sep=";", encoding="latin1") - - # Clean columns - df.columns = df.columns.str.strip().str.replace("\"", "") - - # Map columns - based on user feedback - # Expected columns: Child ID, Email der Eltern (1), Vorname Eltern (1), Nachname Eltern (1), Vorname Kind, Zugangscode (1), Logins (1), Bestellungen - - def get_col(df, patterns): - for p in patterns: - for col in df.columns: - if p.lower() in col.lower(): - return col - return None - - col_child_id = get_col(df, ["Child ID"]) - col_email = get_col(df, ["Email der Eltern", "E-Mail der Eltern"]) - col_parent_vn = get_col(df, ["Vorname Eltern", "Parent First Name"]) - col_parent_nn = get_col(df, ["Nachname Eltern", "Parent Last Name"]) - col_child_vn = get_col(df, ["Vorname Kind", "Child First Name"]) - col_child_nn = get_col(df, ["Nachname Kind", "Child Last Name"]) - col_code = get_col(df, ["Zugangscode", "Access Code"]) - col_group = get_col(df, ["Gruppe", "Klasse", "Group", "Class"]) - col_logins = get_col(df, ["Logins"]) - col_orders = get_col(df, ["Bestellungen", "Orders"]) - - # Delete old entries for this job - db.query(JobParticipant).filter(JobParticipant.job_id == job_id).delete() - - added = 0 - for _, row in df.iterrows(): - try: - logins_val = 0 - try: logins_val = int(row[col_logins]) if col_logins and pd.notna(row[col_logins]) else 0 - except: pass - - orders_val = 0 - if col_orders and pd.notna(row[col_orders]): - val = str(row[col_orders]).lower() - if val and val != "0" and val != "nein" and val != "false": - orders_val = 1 - - participant = JobParticipant( - job_id=job_id, - child_id=str(row[col_child_id]) if col_child_id and pd.notna(row[col_child_id]) else None, - vorname_kind=str(row[col_child_vn]) if col_child_vn and pd.notna(row[col_child_vn]) else None, - nachname_kind=str(row[col_child_nn]) if col_child_nn and pd.notna(row[col_child_nn]) else None, - vorname_eltern=str(row[col_parent_vn]) if col_parent_vn and pd.notna(row[col_parent_vn]) else None, - nachname_eltern=str(row[col_parent_nn]) if col_parent_nn and pd.notna(row[col_parent_nn]) else None, - email_eltern=str(row[col_email]).strip().lower() if col_email and pd.notna(row[col_email]) else None, - zugangscode=str(row[col_code]) if col_code and pd.notna(row[col_code]) else None, - gruppe=str(row[col_group]) if col_group and pd.notna(row[col_group]) else None, - logins=logins_val, - has_orders=orders_val - ) - db.add(participant) - added += 1 - except Exception as e: - logger.warning(f"Error adding participant row: {e}") - - db.commit() - logger.info(f"Sync complete. {added} participants stored for job {job_id}") - return added - - finally: - driver.quit() - def generate_pdf_from_csv(csv_path: str, institution: str, date_info: str, list_type: str, output_path: str): logger.info(f"Generating PDF for {institution} from {csv_path}") df = None @@ -1193,6 +1079,30 @@ def sync_participants(job_id: str, account_type: str, db: Session): finally: driver.quit() +@app.get("/api/jobs/{job_id}/fast-stats") +async def get_fast_stats(job_id: str, db: Session = Depends(get_db)): + participants = db.query(JobParticipant).filter(JobParticipant.job_id == job_id).all() + if not participants: + return [] + + groups = {} + for p in participants: + g_name = p.gruppe or "Unbekannt" + if g_name not in groups: + groups[g_name] = { + "Album": g_name, + "Kinder_insgesamt": 0, + "Kinder_mit_Käufen": 0, + "Kinder_Alle_Bilder_gekauft": 0 + } + groups[g_name]["Kinder_insgesamt"] += 1 + if p.has_orders: + groups[g_name]["Kinder_mit_Käufen"] += 1 + + statistics = list(groups.values()) + statistics.sort(key=lambda x: x["Album"]) + return statistics + @app.post("/api/jobs/{job_id}/sync-participants") async def sync_participants_api(job_id: str, account_type: str, db: Session = Depends(get_db)): try: diff --git a/fotograf-de-scraper/frontend/src/App.tsx b/fotograf-de-scraper/frontend/src/App.tsx index 57a32d4d0..58e28a072 100644 --- a/fotograf-de-scraper/frontend/src/App.tsx +++ b/fotograf-de-scraper/frontend/src/App.tsx @@ -47,6 +47,26 @@ function App() { const [isGmailAuthenticated, setIsGmailAuthenticated] = useState(false); const [isSyncing, setIsSyncing] = useState(false); + const fetchFastStats = async (jobId: string) => { + try { + const response = await fetch(`${API_BASE_URL}/api/jobs/${jobId}/fast-stats`); + if (response.ok) { + const data = await response.json(); + if (data && data.length > 0) { + setStatsResult(data); + } + } + } catch (e) { + console.error("Failed to fetch fast stats", e); + } + }; + + useEffect(() => { + if (selectedJob) { + fetchFastStats(selectedJob.id); + } + }, [selectedJob]); + const handleSyncParticipants = async (job: Job) => { setIsSyncing(true); try { @@ -54,7 +74,8 @@ function App() { method: 'POST' }); if (response.ok) { - alert("Daten erfolgreich mit Fotograf.de synchronisiert!"); + // alert("Daten erfolgreich mit Fotograf.de synchronisiert!"); + fetchFastStats(job.id); // Refresh stats immediately } else { alert("Synchronisierung fehlgeschlagen."); } @@ -890,11 +911,32 @@ function App() { {/* Modal Header */}
-
- -

+

+
+ + +
+

📅 {selectedJob.date} {selectedJob.status} @@ -976,27 +1018,8 @@ function App() { ))} -

Wird für QR-Karten und die Terminübersicht benötigt.

- -
- -

Aktualisiert E-Mails, Logins & Bestellstatus.

-
-
- +

Wird für QR-Karten und die Terminübersicht benötigt.

+
{/* Actions */}
@@ -1518,7 +1541,7 @@ function App() {
Verkaufsstatistik
-

Wie viele Kinder haben wie viel gekauft? Starten Sie den Job, um die Daten abzurufen.

+

Übersicht der Verkäufe basierend auf den lokal gespeicherten Daten. Nutze "Daten abgleichen" im Kopfbereich für ein Update.

{isStatsRunning ? (