import { useState, useEffect, useMemo } 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); 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 { const response = await fetch(`${API_BASE_URL}/api/jobs/${job.id}/sync-participants?account_type=${activeTab}`, { method: 'POST' }); if (response.ok) { // alert("Daten erfolgreich mit Fotograf.de synchronisiert!"); fetchFastStats(job.id); // Refresh stats immediately } else { alert("Synchronisierung fehlgeschlagen."); } } catch (e) { alert("Netzwerkfehler."); } setIsSyncing(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 [releaseResponses, setReleaseResponses] = useState(null); const [isFetchingResponses, setIsFetchingResponses] = useState(false); const [releaseTab, setReleaseTab] = useState<'recipients' | 'preview' | 'codes' | 'history'>('recipients'); const [previewIndex, setPreviewIndex] = useState(0); const [releaseHistoryData, setReleaseHistoryData] = useState(null); // 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

`; const parsedReleaseEmails = useMemo(() => { const lines = releaseEmails.split('\n').filter(line => line.trim()); return lines.map(line => { const parts = line.split(','); const to = parts[0] ? parts[0].trim().toLowerCase() : ""; const firstName = parts[1] ? parts[1].trim() : "Liebe Eltern"; const childrenNames = parts[2] ? parts[2].trim() : "Euren Kindern"; const kindergartenName = selectedJob ? selectedJob.name .replace(/\(JOB\d+\)\s*/, '') .replace(/Kindergarten\s+/gi, '') // Remove "Kindergarten" prefix .replace(/\s+\d{4}$/, '') // Remove year at the end .trim() : "dem Kindergarten"; let subject = "Eure Bilder vom Kindergarten-Fotoshooting"; let body = `Guten Morgen ${firstName},

vielen Dank für Eure Teilnahme am Mini-Familien-Fotoshooting im ${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!

Liebe Grüße,
das Team von Kinderfotos Erding` + SIGNATURE_HTML; return { to: to, subject: subject, body: body, first_name: firstName }; }).filter(e => e.to); }, [releaseEmails, selectedJob]); const [reminderTab, setReminderTab] = useState<'config' | 'preview'>('config'); const [reminderPreviewIndex, setReminderPreviewIndex] = useState(0); const [mainTab, setMainTab] = useState<'vorbereitung' | 'followup' | 'statistik'>('vorbereitung'); const parsedReminderEmails = useMemo(() => { if (!reminderResult) return []; return 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 }; }); }, [reminderResult, emailSubject, emailBody, SIGNATURE_HTML]); const fetchReleaseHistoryData = async () => { try { const response = await fetch(`${API_BASE_URL}/api/publish-request/history`); if (response.ok) { const data = await response.json(); setReleaseHistoryData(data); } } catch (e) { console.error("Failed to fetch release history", e); } }; 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 fetchReleaseResponses = async () => { setIsFetchingResponses(true); try { const response = await fetch(`${API_BASE_URL}/api/publish-request/responses`); if (response.ok) { const data = await response.json(); setReleaseResponses(data); } } catch (e) { console.error("Failed to fetch release responses", e); } setIsFetchingResponses(false); }; 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 (!isGmailAuthenticated) return; setIsSendingRelease(true); setReleaseMessage("Bereite Senden vor..."); const emailsToSend = parsedReleaseEmails; if (emailsToSend.length === 0) { setReleaseMessage("⚠️ Bitte Empfänger eintragen."); setIsSendingRelease(false); return; } 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.map(e => ({ to: e.to, subject: e.subject, body: e.body })), scheduled_time: scheduledTime || null, participants: emailsToSend.map(e => ({ email: e.to, first_name: e.first_name })) }) }); const data = await response.json(); if (response.ok) { setReleaseMessage(`✅ Fertig! ${data.success} gesendet. ${data.failed.length > 0 ? '(' + data.failed.length + ' Fehler)' : ''}`); fetchReleaseHistoryData(); // Update history } else { setReleaseMessage("❌ Fehler beim Senden."); } } catch (e) { setReleaseMessage("❌ Netzwerkfehler."); } setIsSendingRelease(false); }; // 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(); fetchReleaseStats(); fetchReleaseResponses(); fetchReleaseHistoryData(); }, [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}
)} {/* Main Modal Tabs Navigation */}
{/* --- TAB: VORBEREITUNG --- */} {mainTab === 'vorbereitung' && (
📆
Shooting-Planung & Listen
Aktiv
{/* Event Selection */}

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

{/* Actions */}
{/* Action 0: Teilnehmerliste (PDF) - Moved here */}
📄 Teilnehmerliste

Lädt alle Fotograf.de Anmeldungen als PDF herunter.

{/* Action 1: QR Cards */}
📇 QR-Karten

Andruck auf Blanko-PDF.

{ 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

Tabelle mit 6-Minuten Takt.

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

Abgleich von Kindergarten-Anmeldungen mit Calendly-Buchungen.

{ if (e.target.files && e.target.files.length > 0) { handleGenerateSiblingsQrCards(selectedJob, e.target.files[0]); e.target.value = ''; // reset } }} />
)} {/* --- TAB: FOLLOW-UP --- */} {mainTab === 'followup' && ( <> {/* Sub-Tab 1: Freigabe-Anfrage */}
🖼️
Anfrage Veröffentlichung

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

{/* Tabs Navigation */}
{releaseTab === 'recipients' && (

Format: E-Mail, Vorname Elternteil, Namen der Kinder
(z.B. max@muster.de, Max, Moritz und Leni)