import { useState, useEffect } from 'react'; import './App.css'; interface Job { id: string; name: string; url: string; status: string; date: string; shooting_type: string; last_updated?: string; } type AccountType = 'kiga' | 'schule'; function App() { const [activeTab, setActiveTab] = useState('kiga'); const [jobsCache, setJobsCache] = useState>({ kiga: null, schule: null, }); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); // New state for the detail modal const [selectedJob, setSelectedJob] = useState(null); const [processingJobId, setProcessingJobId] = useState(null); // States for Statistics Polling const [statsTaskId, setStatsTaskId] = useState(null); const [statsProgress, setStatsProgress] = useState(''); const [statsResult, setStatsResult] = useState(null); const [isStatsRunning, setIsStatsRunning] = useState(false); // States for QR Generator const [isQrGenerating, setIsQrGenerating] = useState(false); const [eventTypes, setEventTypes] = useState([]); const [selectedEventType, setSelectedEventType] = useState(""); const [isListGenerating, setIsListGenerating] = useState(false); const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://192.168.178.6:8002'; const fetchJobs = async (account: AccountType, forceRefresh = false) => { setIsLoading(true); setError(null); try { const response = await fetch(`${API_BASE_URL}/api/jobs?account_type=${account}&force_refresh=${forceRefresh}`); if (!response.ok) { const errData = await response.json(); throw new Error(errData.detail || 'Fehler beim Abrufen der Aufträge'); } const data: Job[] = await response.json(); setJobsCache(prev => ({ ...prev, [account]: data })); } catch (err: any) { setError(err.message); } finally { setIsLoading(false); } }; useEffect(() => { // Only fetch if we haven't already fetched it if (jobsCache[activeTab] === null) { fetchJobs(activeTab, false); } }, [activeTab]); const handleRefresh = () => fetchJobs(activeTab, true); useEffect(() => { const fetchEventTypes = async () => { try { const res = await fetch(`${API_BASE_URL}/api/calendly/event-types`); if (res.ok) { const data = await res.json(); setEventTypes(data.event_types || []); if (data.event_types && data.event_types.length > 0) { setSelectedEventType(data.event_types[0].name); } } } catch (err) { console.error("Failed to fetch event types:", err); } }; fetchEventTypes(); }, []); // Statistics Task Functions const handleStartStatistics = async (job: Job) => { setIsStatsRunning(true); setStatsResult(null); setStatsProgress('Starte Analyse...'); setError(null); try { const response = await fetch(`${API_BASE_URL}/api/jobs/${job.id}/statistics?account_type=${activeTab}`, { method: 'POST' }); if (!response.ok) throw new Error('Konnte Statistik-Prozess nicht starten.'); const data = await response.json(); setStatsTaskId(data.task_id); } catch (err: any) { setError(err.message); setIsStatsRunning(false); } }; useEffect(() => { let interval: any; if (statsTaskId && isStatsRunning) { interval = setInterval(async () => { try { const res = await fetch(`${API_BASE_URL}/api/tasks/${statsTaskId}`); if (!res.ok) throw new Error('Task Status Request failed'); const data = await res.json(); setStatsProgress(data.progress || 'Verarbeite...'); if (data.status === 'completed') { setStatsResult(data.result); setIsStatsRunning(false); setStatsTaskId(null); } else if (data.status === 'error') { setError(data.progress || 'Ein Fehler ist aufgetreten.'); setIsStatsRunning(false); setStatsTaskId(null); } } catch (err: any) { console.error("Polling Error:", err); } }, 1000); } return () => { if (interval) clearInterval(interval); }; }, [statsTaskId, isStatsRunning]); const handleGeneratePdf = async (job: Job) => { 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'); } 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); } catch (err: any) { setError(`PDF Fehler (${job.name}): ${err.message}`); } finally { setProcessingJobId(null); } }; const handleGenerateQrCards = async (job: Job, file: File) => { if (!file) { setError("Bitte wähle eine PDF-Vorlage aus."); return; } setIsQrGenerating(true); setError(null); const formData = new FormData(); formData.append('pdf_file', file); if (selectedEventType) formData.append('event_type_name', selectedEventType); try { const response = await fetch(`${API_BASE_URL}/api/qr-cards/generate`, { method: 'POST', body: formData, }); 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 || 'Generierung fehlgeschlagen'); } const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `QR_Karten_Andruck_${job.id}.pdf`; document.body.appendChild(a); a.click(); a.remove(); window.URL.revokeObjectURL(url); } catch (err: any) { setError(err.message); } finally { setIsQrGenerating(false); } }; const handleGenerateAppointmentList = async (job: Job) => { setIsListGenerating(true); setError(null); try { const response = await fetch(`${API_BASE_URL}/api/jobs/${job.id}/appointment-list?event_type_name=${encodeURIComponent(selectedEventType)}`); 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); } catch (err: any) { setError(`Listen-Fehler (${job.name}): ${err.message}`); } finally { setIsListGenerating(false); } }; const currentJobs = jobsCache[activeTab]; return (
{/* Top Navigation Bar */}
📸

Fotograf.de ERP

{/* Main Tabs */}
{/* Main Content Area */}
{/* Action Bar */}

{activeTab === 'kiga' ? 'Kindergarten Aufträge' : 'Schul Aufträge'}

{currentJobs === null ? "Noch nicht geladen." : `${currentJobs.length} aktive Projekte gefunden.`}

{/* Global Errors */} {error && !selectedJob && (

Systemfehler

{error}

)} {/* Empty State */} {currentJobs !== null && currentJobs.length === 0 && !isLoading && !error && (

Keine Projekte

Es wurden keine aktiven Aufträge für diesen Account gefunden.

)} {/* Grid of Job Cards */} {currentJobs !== null && currentJobs.length > 0 && (
{currentJobs.map((job) => (
setSelectedJob(job)} > {/* Card Header (Status Color Band) */}
{job.id ? `ID: ${job.id}` : 'Unbekannte ID'} {job.date || 'Kein Datum'}

{job.name}

{job.status}

Typ: {job.shooting_type}

{/* Card Footer */}
Details verwalten →
))}
)}
{/* --- Detail Modal Overlay --- */} {selectedJob && (
{/* Background backdrop */} {/* Modal Panel */}
{/* Modal Header */}

📅 {selectedJob.date} {selectedJob.status} In Fotograf.de öffnen

{/* Modal Body - Tools & Actions Grid */}
{error && processingJobId === selectedJob.id && (
{error}
)}

Verfügbare Tools

{/* Tool 1: PDF List */}
📄
Teilnehmerliste (PDF)

Lädt die Anmeldungen herunter und formatiert sie als sauberes PDF (Gruppen/Klassen sortiert).

{/* Tool 2: Shooting-Planung (QR & List) */}
📆
Shooting-Planung
Neu
{/* Event Selection */}

Die Termine für diesen Event-Typ werden aus Calendly importiert.

{/* Actions */}
{/* Action 1: QR Cards */}
📇 QR-Zugangskarten

Druckt Namen und Uhrzeit auf vorbereitete PDF-Bögen.

{ if (e.target.files && e.target.files.length > 0) { handleGenerateQrCards(selectedJob, e.target.files[0]); e.target.value = ''; // reset } }} />
{/* Action 2: Appointment List */}
📄 Termin-Übersicht

PDF mit 6-Minuten Taktung und Lücken für den Shooting-Tag.

{/* Tool 3: Follow-up Emails */}
✉️
Demnächst
Nachfass-Mails (Supermailer)

Analysiert das Kaufverhalten und generiert eine fertige CSV-Liste für den Supermailer.

{/* Tool 4: Statistics */}
📊
Aktiv
Verkaufsstatistik

Durchforstet alle Alben und liefert eine Übersicht: Wie viele Kinder haben wie viel gekauft?

{isStatsRunning ? (
Läuft im Hintergrund...

{statsProgress}

) : ( )}
{/* Optional Data View Area (for later use, e.g., showing the stats table here) */} {statsResult && (

Ergebnis der Auswertung

Erfolgreich abgeschlossen
{statsResult.map((row, idx) => ( ))} {/* Total Row */}
Album Kinder (Gesamt) Mit Käufen Alle Bilder
{row.Album} {row.Kinder_insgesamt} {row.Kinder_mit_Käufen} {row.Kinder_Alle_Bilder_gekauft}
Gesamt ({statsResult.length} Alben) {statsResult.reduce((sum, row) => sum + (row.Kinder_insgesamt || 0), 0)} {statsResult.reduce((sum, row) => sum + (row.Kinder_mit_Käufen || 0), 0)} {statsResult.reduce((sum, row) => sum + (row.Kinder_Alle_Bilder_gekauft || 0), 0)}
)}
{/* Modal Footer */}
)}
); } export default App;