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 [isSiblingsGenerating, setIsSiblingsGenerating] = useState(false); const [isSiblingsQrGenerating, setIsSiblingsQrGenerating] = useState(false); const [reminderTaskId, setReminderTaskId] = useState(null); const [reminderProgress, setReminderProgress] = useState(''); const [isReminderRunning, setIsReminderRunning] = useState(false); const [latestFile, setLatestFile] = useState(null); const [isGmailAuthenticated, setIsGmailAuthenticated] = useState(false); // Email States const [reminderResult, setReminderResult] = useState(null); const [emailSubject, setEmailSubject] = useState("Fotos von {Kindernamen}"); const [emailBody, setEmailBody] = useState("Hallo {Name Käufer},

deine Fotos sind fertig und warten auf dich! Klicke einfach auf die Links unten, um direkt zu den Galerien zu gelangen:

{LinksHTML}

Viel Spaß beim Anschauen!"); const [isSendingEmails, setIsSendingEmails] = useState(false); const [emailSendStatus, setEmailSendStatus] = useState(null); // Release Request States const [releaseEmails, setReleaseEmails] = useState(""); const [releaseCodes, setReleaseCodes] = useState(""); const [releaseStats, setReleaseStats] = useState(null); const [isUploadingCodes, setIsUploadingCodes] = useState(false); const [uploadMessage, setUploadMessage] = useState(""); const [isSendingRelease, setIsSendingRelease] = useState(false); const [releaseMessage, setReleaseMessage] = useState(""); const [scheduledTime, setScheduledTime] = useState(""); // New state const fetchReleaseStats = async () => { try { const response = await fetch(`${API_BASE_URL}/api/publish-request/stats`); if (response.ok) { const data = await response.json(); setReleaseStats(data); } } catch (e) { console.error("Failed to fetch release stats", e); } }; const handleUploadCodes = async () => { setIsUploadingCodes(true); setUploadMessage("Lädt hoch..."); try { const response = await fetch(`${API_BASE_URL}/api/publish-request/codes`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ codes: releaseCodes }) }); const data = await response.json(); if (response.ok) { setUploadMessage(`✅ ${data.added} neue Codes gespeichert.`); setReleaseCodes(""); fetchReleaseStats(); } else { setUploadMessage("❌ Fehler beim Hochladen."); } } catch (e) { setUploadMessage("❌ Netzwerkfehler."); } setIsUploadingCodes(false); }; const handleSendRelease = async () => { if (!reminderResult || !isGmailAuthenticated) return; setIsSendingRelease(true); setReleaseMessage("Bereite Senden vor..."); const targetEmails = releaseEmails.split(',').map(e => e.trim().toLowerCase()).filter(e => e); // We only send to emails that are in targetEmails and exist in reminderResult const targetRows = reminderResult.filter(row => { const rowEmail = row["E-Mail-Adresse Käufer"]?.trim().toLowerCase(); return targetEmails.includes(rowEmail); }); if (targetRows.length === 0) { setReleaseMessage("⚠️ Keine passenden E-Mails im Auftrag gefunden (bitte vorher Supermailer-Analyse starten)."); setIsSendingRelease(false); return; } const emailsToSend = targetRows.map(row => { // Split the buyer name to get first name const fullName = row["Name Käufer"] || ""; const firstName = fullName.split(' ')[0] || "Liebe Eltern"; const kindergartenName = selectedJob ? selectedJob.name.replace(/\(JOB\d+\)\s*/, '') : "dem Kindergarten"; const childrenNames = row["Kindernamen"] || "Euren Kindern"; let subject = "Eure Bilder vom Kindergarten-Fotoshooting"; let body = `Guten Morgen ${firstName},

vielen Dank für Eure Teilnahme am Mini-Familien-Fotoshooting im Kindergarten ${kindergartenName} diese Woche. Die Bilder sind jetzt bereits online, ihr solltet bald eine Mail dazu erhalten. :)

Die Bilder von ${childrenNames} gefallen uns sehr gut, sie wirken auf den Bildern sehr selbstbewusst. Gerne würden wir diese in unserer Galerie auf www.kinderfotos-erding.de (Link: Beispiel ansehen) veröffentlichen.

Um den rechtlichen Anforderungen (DSGVO) gerecht zu werden, müsstet Ihr noch dieses Formular auf unserer Website ausfüllen:
Zum Formular zur Veröffentlichung

Das hilft uns wirklich sehr, damit andere einen besseren Eindruck von unserer Arbeit gewinnen.

Als kleines Dankeschön erhaltet Ihr im Anschluss einen Rabattcode über 25 € für Eure Bestellung. Diesen senden wir Euch per separater E-Mail zu, sobald das Formular ausgefüllt ist. Bitte wartet mit Eurer Bestellung, bis wir Euch den Rabattcode zugesendet haben.

Vielen Dank für Eure Unterstützung und Euer Vertrauen!

` + SIGNATURE_HTML; return { to: row["E-Mail-Adresse Käufer"], subject: subject, body: body }; }); setReleaseMessage(`Sende ${emailsToSend.length} Mails...`); try { const response = await fetch(`${API_BASE_URL}/api/publish-request/send`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ emails: emailsToSend, scheduled_time: scheduledTime || null }) }); const data = await response.json(); if (response.ok) { setReleaseMessage(`✅ Fertig! ${data.success} gesendet. ${data.failed.length > 0 ? '(' + data.failed.length + ' Fehler)' : ''}`); } else { setReleaseMessage("❌ Fehler beim Senden."); } } catch (e) { setReleaseMessage("❌ Netzwerkfehler."); } setIsSendingRelease(false); }; // Email Signature (Cleaned up from user input) const SIGNATURE_HTML = `

--
Kinderfotos Erding Logo

Kinderfotos Erding | www.kinderfotos-erding.de

Gartenstr. 10 | 85445 Oberding | 08122-8470867

`; // If we are on duckdns, use the relative path, otherwise use local IP const API_BASE_URL = window.location.hostname.includes('duckdns.org') ? '/fotograf-de-api' : (import.meta.env.VITE_API_BASE_URL || 'http://192.168.178.6:8002'); const checkGmailAuth = async () => { try { const response = await fetch(`${API_BASE_URL}/api/gmail/status`); if (response.ok) { const data = await response.json(); setIsGmailAuthenticated(data.authenticated); } } catch (err) { console.error("Failed to check Gmail auth status"); } }; const handleGmailLogin = async () => { try { const response = await fetch(`${API_BASE_URL}/api/auth/google`); if (response.ok) { const data = await response.json(); window.location.href = data.url; } } catch (err) { setError("Konnte Gmail-Anmeldung nicht starten."); } }; const fetchLatestFile = async () => { try { const response = await fetch(`${API_BASE_URL}/api/jobs/latest-file`); if (response.ok) { const data = await response.json(); if (data.has_file) { setLatestFile(data); } } } catch (err) { console.error("Failed to fetch latest file info"); } }; 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); } fetchLatestFile(); checkGmailAuth(); }, [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) { const savedType = localStorage.getItem('fotograf_selected_event_type'); const typeExists = data.event_types.some((et: any) => et.name === savedType); if (savedType && typeExists) { setSelectedEventType(savedType); } else { 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 handleFetchReminderResult = async (taskId: string) => { try { const res = await fetch(`${API_BASE_URL}/api/tasks/${taskId}`); if (res.ok) { const data = await res.json(); setReminderResult(data.result); } } catch (err) { console.error("Failed to fetch reminder results"); } }; useEffect(() => { let interval: any; if (reminderTaskId && isReminderRunning) { interval = setInterval(async () => { try { const res = await fetch(`${API_BASE_URL}/api/tasks/${reminderTaskId}`); if (!res.ok) throw new Error('Task Status Request failed'); const data = await res.json(); setReminderProgress(data.progress || 'Verarbeite...'); if (data.status === 'completed') { setIsReminderRunning(false); handleFetchReminderResult(reminderTaskId); } else if (data.status === 'error') { setError(data.progress || 'Ein Fehler ist aufgetreten.'); setIsReminderRunning(false); setReminderTaskId(null); } } catch (err: any) { console.error("Polling Error:", err); } }, 1000); } return () => { if (interval) clearInterval(interval); }; }, [reminderTaskId, isReminderRunning]); const handleGeneratePdf = async (job: Job) => { setProcessingJobId(job.id); setError(null); try { // 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'); // 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); fetchLatestFile(); }, 3000); } catch (err: any) { setError(`PDF Fehler (${job.name}): ${err.message}`); 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(); // Delay removal and revocation to ensure download starts in all browsers setTimeout(() => { document.body.removeChild(a); window.URL.revokeObjectURL(url); fetchLatestFile(); }, 100); } catch (err: any) { setError(err.message); } finally { setIsQrGenerating(false); } }; const handleGenerateSiblingsQrCards = async (job: Job, file: File) => { if (!file) { setError("Bitte wähle eine PDF-Vorlage aus."); return; } setIsSiblingsQrGenerating(true); setError(null); const formData = new FormData(); formData.append('pdf_file', file); try { const response = await fetch(`${API_BASE_URL}/api/jobs/${job.id}/siblings-qr-cards?account_type=${activeTab}`, { method: 'POST', body: formData, }); if (!response.ok) { if (response.status === 404) { throw new Error("Keine Geschwisterkinder für QR-Karten gefunden."); } const errData = await response.json(); throw new Error(errData.detail || 'Generierung fehlgeschlagen'); } const result = await response.json(); if (result.status === "success" && result.download_url) { window.open(`${API_BASE_URL}${result.download_url}`, '_blank'); setTimeout(fetchLatestFile, 2000); } else { throw new Error("Download URL could not be retrieved from server."); } } catch (err: any) { setError(err.message); } finally { setIsSiblingsQrGenerating(false); } }; const handleGenerateSiblingsList = async (job: Job) => { setIsSiblingsGenerating(true); setError(null); try { const downloadUrl = `${API_BASE_URL}/api/jobs/${job.id}/siblings-list?account_type=${activeTab}&event_type_name=${encodeURIComponent(selectedEventType)}`; window.open(downloadUrl, '_blank'); setTimeout(() => { setIsSiblingsGenerating(false); fetchLatestFile(); }, 3000); } catch (err: any) { setError(`Geschwisterlisten-Fehler (${job.name}): ${err.message}`); setIsSiblingsGenerating(false); } }; const handleGenerateAppointmentList = async (job: Job) => { setIsListGenerating(true); setError(null); try { const downloadUrl = `${API_BASE_URL}/api/jobs/${job.id}/appointment-list?event_type_name=${encodeURIComponent(selectedEventType)}`; window.open(downloadUrl, '_blank'); setTimeout(() => { setIsListGenerating(false); fetchLatestFile(); }, 3000); } catch (err: any) { setError(`Listen-Fehler (${job.name}): ${err.message}`); setIsListGenerating(false); } }; const handleStartReminderAnalysis = async (job: Job) => { setIsReminderRunning(true); setReminderProgress('Starte Analyse...'); setError(null); try { const response = await fetch(`${API_BASE_URL}/api/jobs/${job.id}/reminder-analysis?account_type=${activeTab}`, { method: 'POST' }); if (!response.ok) throw new Error('Konnte Analyse nicht starten.'); const data = await response.json(); setReminderTaskId(data.task_id); } catch (err: any) { setError(err.message); setIsReminderRunning(false); } }; const handleDownloadReminderCsv = async (taskId: string) => { try { window.open(`${API_BASE_URL}/api/tasks/${taskId}/download-csv`, '_blank'); setTimeout(fetchLatestFile, 2000); } catch (err: any) { setError("Download fehlgeschlagen."); } }; const handleSendEmails = async () => { if (!reminderResult || !isGmailAuthenticated) return; setIsSendingEmails(true); setEmailSendStatus("Sende..."); const emailsToSend = reminderResult.map(row => { let subject = emailSubject.replace(/{Kindernamen}/g, row["Kindernamen"]); let body = emailBody .replace(/{Name Käufer}/g, row["Name Käufer"]) .replace(/{Kindernamen}/g, row["Kindernamen"]) .replace(/{LinksHTML}/g, row["LinksHTML"]) .replace(/\n/g, "
"); return { to: row["E-Mail-Adresse Käufer"], subject: subject, body: body + SIGNATURE_HTML }; }); try { const response = await fetch(`${API_BASE_URL}/api/publish-request/send`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ emails: emailsToSend, scheduled_time: scheduledTime || null }) }); if (response.ok) { const data = await response.json(); setEmailSendStatus(`✅ Fertig! ${data.success} gesendet.`); if (data.failed.length > 0) { setEmailSendStatus(prev => `${prev} (${data.failed.length} Fehler)`); } } else { throw new Error("Sende-Fehler"); } } catch (err) { setEmailSendStatus("❌ Fehler beim Senden"); } finally { setIsSendingEmails(false); } }; const handleSendTestEmail = async () => { if (!isGmailAuthenticated) return; setIsSendingEmails(true); try { const response = await fetch(`${API_BASE_URL}/api/gmail/send-bulk`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ emails: [{ to: "floke.com@gmail.com", subject: "Test-E-Mail vom Fotograf Tool (inkl. Signatur)", body: "Hallo! Das ist eine Test-E-Mail, um die Gmail-API-Integration zu verifizieren. Wenn du das liest, funktioniert alles perfekt! 🚀" + SIGNATURE_HTML }] }) }); if (response.ok) { alert("Test-E-Mail erfolgreich an floke.com@gmail.com gesendet!"); } else { alert("Fehler beim Senden der Test-E-Mail."); } } catch (err) { alert("Netzwerkfehler beim Senden der Test-E-Mail."); } finally { setIsSendingEmails(false); } }; const currentJobs = jobsCache[activeTab]; return (
{/* Top Navigation Bar */}
📸

Fotograf.de ERP

Zum Dashboard {isGmailAuthenticated && ( )} {latestFile && ( )}
{/* 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.

{/* Action 3: Siblings List */}
👨‍👩‍👧‍👦 Geschwisterliste (Einrichtungsintern)

Sucht nach Geschwisterkindern in der Einrichtung und gleicht diese mit Calendly ab.

{ if (e.target.files && e.target.files.length > 0) { handleGenerateSiblingsQrCards(selectedJob, e.target.files[0]); e.target.value = ''; // reset } }} />
{/* Tool 4: Freigabe-Anfrage */}
🖼️
Anfrage Veröffentlichung

Sende personalisierte DSGVO-Anfragen für ausgewählte Eltern inkl. Gutschein-Webhook.

{/* Upload Codes */}
{releaseStats && ( Verfügbar: {releaseStats.available} (Verwendet: {releaseStats.used}) )}