Files
Brancheneinstufung2/fotograf-de-scraper/frontend/src/App.tsx
Floke 25daaf0afe [34588f42] Keine Zusammenfassung angegeben.
Keine Zusammenfassung angegeben.
2026-04-18 13:09:23 +00:00

1583 lines
82 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<AccountType>('kiga');
const [jobsCache, setJobsCache] = useState<Record<AccountType, Job[] | null>>({
kiga: null,
schule: null,
});
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// New state for the detail modal
const [selectedJob, setSelectedJob] = useState<Job | null>(null);
const [processingJobId, setProcessingJobId] = useState<string | null>(null);
// States for Statistics Polling
const [statsTaskId, setStatsTaskId] = useState<string | null>(null);
const [statsProgress, setStatsProgress] = useState<string>('');
const [statsResult, setStatsResult] = useState<any[] | null>(null);
const [isStatsRunning, setIsStatsRunning] = useState(false);
// States for QR Generator
const [isQrGenerating, setIsQrGenerating] = useState(false);
const [eventTypes, setEventTypes] = useState<any[]>([]);
const [selectedEventType, setSelectedEventType] = useState<string>("");
const [isListGenerating, setIsListGenerating] = useState(false);
const [isSiblingsGenerating, setIsSiblingsGenerating] = useState(false);
const [isSiblingsQrGenerating, setIsSiblingsQrGenerating] = useState(false);
const [reminderTaskId, setReminderTaskId] = useState<string | null>(null);
const [reminderProgress, setReminderProgress] = useState<string>('');
const [isReminderRunning, setIsReminderRunning] = useState(false);
const [latestFile, setLatestFile] = useState<any>(null);
const [isGmailAuthenticated, setIsGmailAuthenticated] = useState(false);
// Email States
const [reminderResult, setReminderResult] = useState<any[] | null>(null);
const [emailSubject, setEmailSubject] = useState("Fotos von {Kindernamen}");
const [emailBody, setEmailBody] = useState("Hallo {Name Käufer},<br><br>deine Fotos sind fertig und warten auf dich! Klicke einfach auf die Links unten, um direkt zu den Galerien zu gelangen:<br><br>{LinksHTML}<br><br>Viel Spaß beim Anschauen!");
const [isSendingEmails, setIsSendingEmails] = useState(false);
const [emailSendStatus, setEmailSendStatus] = useState<string | null>(null);
// Release Request States
const [releaseEmails, setReleaseEmails] = useState("");
const [releaseCodes, setReleaseCodes] = useState("");
const [releaseStats, setReleaseStats] = useState<any>(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<any[] | null>(null);
const [isFetchingResponses, setIsFetchingResponses] = useState(false);
const [releaseTab, setReleaseTab] = useState<'recipients' | 'preview' | 'codes' | 'history'>('recipients');
const [previewIndex, setPreviewIndex] = useState(0);
const [releaseHistoryData, setReleaseHistoryData] = useState<any[] | null>(null);
// Email Signature (Cleaned up from user input)
const SIGNATURE_HTML = `
<br><br>
<span style="color: #888;">--</span><br>
<div dir="ltr">
<table border="0" cellspacing="0" cellpadding="0" style="border-collapse:collapse; margin-top: 5px;">
<tbody>
<tr>
<td width="220" valign="top" style="padding-right: 15px;">
<img width="200" src="https://lh3.googleusercontent.com/d/1K7RODOqKE2e1nRJ3D4dEWdjthoTMyXUq" alt="Kinderfotos Erding Logo" style="display: block;">
</td>
<td valign="bottom" style="padding-left: 15px; border-left: 1px solid #ddd; font-family: sans-serif; font-size: 13px; color: #333; line-height: 1.5;">
<p style="margin: 0;"><b>Kinderfotos Erding</b> | <a href="http://www.kinderfotos-erding.de/" target="_blank" style="color: #1155cc; text-decoration: none;">www.kinderfotos-erding.de</a></p>
<p style="margin: 0; color: #666;">Gartenstr. 10 | 85445 Oberding | 08122-8470867</p>
</td>
</tr>
</tbody>
</table>
</div>
`;
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},<br><br>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. :)<br><br>Die Bilder von ${childrenNames} gefallen uns sehr gut, sie wirken auf den Bildern sehr selbstbewusst. Gerne würden wir diese in unserer Galerie auf <a href="https://www.kinderfotos-erding.de/angebot/schulfotograf/#5134aec7-5498-49ab-b26e-73574510c90a">www.kinderfotos-erding.de</a> (Link: <a href="https://www.kinderfotos-erding.de/angebot/schulfotograf/#5134aec7-5498-49ab-b26e-73574510c90a">Beispiel ansehen</a>) veröffentlichen.<br><br>Um den rechtlichen Anforderungen (DSGVO) gerecht zu werden, müsstet Ihr noch dieses Formular auf unserer Website ausfüllen:<br><a href="https://www.kinderfotos-erding.de/angebot/schulfotograf/freigabe-zur-veroeffentlichung/"><b>Zum Formular zur Veröffentlichung</b></a><br><br>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.<br><br>Vielen Dank für Eure Unterstützung und Euer Vertrauen!<br><br>Liebe Grüße,<br>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, "<br>");
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, "<br>");
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 (
<div className="min-h-screen bg-gray-50 text-gray-900 font-sans">
{/* Top Navigation Bar */}
<header className="bg-white shadow-sm sticky top-0 z-10">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">
<div className="flex items-center gap-6">
<div className="flex items-center gap-2">
<span className="text-2xl">📸</span>
<h1 className="text-xl font-bold text-gray-800 tracking-tight">Fotograf.de ERP</h1>
</div>
<a
href="http://192.168.178.6:8090"
className="hidden sm:flex text-xs font-medium text-gray-500 hover:text-indigo-600 transition-colors items-center gap-1 bg-gray-50 px-2 py-1 rounded border border-gray-200"
>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" /></svg>
Zum Dashboard
</a>
<button
onClick={handleGmailLogin}
className={`flex text-xs font-medium transition-colors items-center gap-1 px-2 py-1 rounded border ${
isGmailAuthenticated
? 'bg-emerald-50 text-emerald-600 border-emerald-200'
: 'bg-amber-50 text-amber-600 border-amber-200 hover:bg-amber-100'
}`}
>
<span className="text-lg">{isGmailAuthenticated ? '✅' : '✉️'}</span>
{isGmailAuthenticated ? 'Gmail verbunden' : 'Gmail verbinden'}
</button>
{isGmailAuthenticated && (
<button
onClick={handleSendTestEmail}
disabled={isSendingEmails}
className="text-[10px] bg-white border border-gray-200 text-gray-400 hover:text-indigo-600 hover:border-indigo-200 px-2 py-1 rounded transition-all"
>
{isSendingEmails ? 'Sende...' : 'Test-Mail senden'}
</button>
)}
{latestFile && (
<div className="hidden lg:flex items-center gap-2">
<span className="text-xs text-gray-400">Letzte Datei:</span>
<a
href={`${API_BASE_URL}/api/jobs/download-latest`}
target="_blank"
rel="noopener noreferrer"
className="text-xs font-bold text-emerald-600 hover:text-emerald-700 underline flex items-center gap-1"
>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a2 2 0 002 2h12a2 2 0 002-2v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /></svg>
{latestFile.display_name} ({latestFile.timestamp})
</a>
</div>
)}
</div>
{/* Main Tabs */}
<nav className="flex space-x-1 bg-gray-100 p-1 rounded-lg">
<button
onClick={() => setActiveTab('kiga')}
className={`px-4 py-2 rounded-md text-sm font-medium transition-all duration-200 ${
activeTab === 'kiga' ? 'bg-white shadow text-indigo-700' : 'text-gray-500 hover:text-gray-700'
}`}
>
Kindergarten
</button>
<button
onClick={() => setActiveTab('schule')}
className={`px-4 py-2 rounded-md text-sm font-medium transition-all duration-200 ${
activeTab === 'schule' ? 'bg-white shadow text-indigo-700' : 'text-gray-500 hover:text-gray-700'
}`}
>
Schule
</button>
</nav>
</div>
</header>
{/* Main Content Area */}
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Action Bar */}
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-8 gap-4">
<div>
<h2 className="text-2xl font-bold text-gray-900">
{activeTab === 'kiga' ? 'Kindergarten Aufträge' : 'Schul Aufträge'}
</h2>
<p className="text-sm text-gray-500 mt-1">
{currentJobs === null ? "Noch nicht geladen." : `${currentJobs.length} aktive Projekte gefunden.`}
</p>
</div>
<button
onClick={handleRefresh}
disabled={isLoading}
className="flex items-center gap-2 px-5 py-2.5 bg-indigo-600 text-white text-sm font-medium rounded-lg hover:bg-indigo-700 focus:ring-4 focus:ring-indigo-100 transition-all disabled:opacity-70 disabled:cursor-not-allowed shadow-sm"
>
{isLoading ? (
<>
<svg className="animate-spin h-4 w-4" viewBox="0 0 24 24" fill="none">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
Synchronisiere...
</>
) : (
<>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /></svg>
{currentJobs === null ? 'Aufträge abrufen' : 'Liste aktualisieren'}
</>
)}
</button>
</div>
{/* Global Errors */}
{error && !selectedJob && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg mb-8 flex items-start gap-3">
<svg className="w-5 h-5 mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20"><path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" /></svg>
<div>
<h3 className="font-semibold text-sm">Systemfehler</h3>
<p className="text-sm mt-1">{error}</p>
</div>
</div>
)}
{/* Empty State */}
{currentJobs !== null && currentJobs.length === 0 && !isLoading && !error && (
<div className="text-center py-16 bg-white rounded-xl border border-dashed border-gray-300">
<svg className="mx-auto h-12 w-12 text-gray-300" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"><path vectorEffect="non-scaling-stroke" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 13h6m-3-3v6m-9 1V7a2 2 0 012-2h6l2 2h6a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z" /></svg>
<h3 className="mt-2 text-sm font-semibold text-gray-900">Keine Projekte</h3>
<p className="mt-1 text-sm text-gray-500">Es wurden keine aktiven Aufträge für diesen Account gefunden.</p>
</div>
)}
{/* Grid of Job Cards */}
{currentJobs !== null && currentJobs.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
{currentJobs.map((job) => (
<div
key={job.id}
className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden hover:shadow-md hover:border-indigo-300 transition-all duration-200 cursor-pointer flex flex-col h-full"
onClick={() => setSelectedJob(job)}
>
{/* Card Header (Status Color Band) */}
<div className={`h-2 w-full ${job.status.toLowerCase().includes('abgeschlossen') ? 'bg-gray-300' : 'bg-indigo-500'}`}></div>
<div className="p-6 flex-1 flex flex-col">
<div className="flex justify-between items-start mb-4">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
{job.id ? `ID: ${job.id}` : 'Unbekannte ID'}
</span>
<span className="text-sm font-medium text-gray-500 bg-gray-50 px-2 py-1 rounded">
{job.date || 'Kein Datum'}
</span>
</div>
<h3 className="text-lg font-bold text-gray-900 leading-tight mb-2 line-clamp-2">
{job.name}
</h3>
<div className="mt-auto pt-4 flex flex-col gap-2">
<p className="text-sm text-gray-600 flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-emerald-500"></span>
{job.status}
</p>
<p className="text-xs text-gray-400">Typ: {job.shooting_type}</p>
</div>
</div>
{/* Card Footer */}
<div className="bg-gray-50 px-6 py-3 border-t border-gray-100 flex justify-between items-center">
<span className="text-sm font-medium text-indigo-600 group-hover:text-indigo-800">
Details verwalten &rarr;
</span>
</div>
</div>
))}
</div>
)}
</main>
{/* --- Detail Modal Overlay --- */}
{selectedJob && (
<div className="fixed inset-0 z-50 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
{/* Background backdrop */}
<div className="fixed inset-0 bg-gray-900 bg-opacity-75 transition-opacity" aria-hidden="true" onClick={() => setSelectedJob(null)}></div>
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
{/* Modal Panel */}
<div className="inline-block align-bottom bg-gray-50 rounded-2xl text-left overflow-hidden shadow-2xl transform transition-all sm:my-8 sm:align-middle sm:max-w-4xl w-full">
{/* Modal Header */}
<div className="bg-white px-6 py-6 border-b border-gray-200 flex justify-between items-start">
<div>
<h3 className="text-2xl font-bold text-gray-900" id="modal-title">
{selectedJob.name}
</h3>
<p className="text-sm text-gray-500 mt-1 flex items-center gap-3">
<span>📅 {selectedJob.date}</span>
<span></span>
<span className="text-emerald-600 font-medium">{selectedJob.status}</span>
<span></span>
<a href={selectedJob.url} target="_blank" rel="noopener noreferrer" className="text-indigo-600 hover:underline inline-flex items-center gap-1">
In Fotograf.de öffnen <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" /></svg>
</a>
</p>
</div>
<button
type="button"
className="bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
onClick={() => setSelectedJob(null)}
>
<span className="sr-only">Schließen</span>
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Modal Body - Tools & Actions Grid */}
<div className="px-6 py-8">
{error && processingJobId === selectedJob.id && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg mb-6 text-sm">
{error}
</div>
)}
{/* Main Modal Tabs Navigation */}
<div className="flex border-b border-gray-200 mb-6 gap-2">
<button
onClick={() => setMainTab('vorbereitung')}
className={`px-4 py-2 text-sm font-bold flex items-center gap-2 ${mainTab === 'vorbereitung' ? 'text-emerald-600 border-b-2 border-emerald-600' : 'text-gray-500 hover:text-gray-700'}`}
>
<span className="text-lg">📆</span> Vorbereitung
</button>
<button
onClick={() => setMainTab('followup')}
className={`px-4 py-2 text-sm font-bold flex items-center gap-2 ${mainTab === 'followup' ? 'text-indigo-600 border-b-2 border-indigo-600' : 'text-gray-500 hover:text-gray-700'}`}
>
<span className="text-lg"></span> Follow-Up
</button>
<button
onClick={() => setMainTab('statistik')}
className={`px-4 py-2 text-sm font-bold flex items-center gap-2 ${mainTab === 'statistik' ? 'text-purple-600 border-b-2 border-purple-600' : 'text-gray-500 hover:text-gray-700'}`}
>
<span className="text-lg">📊</span> Statistik
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* --- TAB: VORBEREITUNG --- */}
{mainTab === 'vorbereitung' && (
<div className="bg-white border border-gray-200 rounded-xl p-5 hover:border-emerald-300 transition-colors shadow-sm md:col-span-2">
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-emerald-50 rounded-lg text-emerald-600 text-xl">📆</div>
<h5 className="font-bold text-gray-900 text-lg">Shooting-Planung & Listen</h5>
</div>
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-emerald-100 text-emerald-800">Aktiv</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* Event Selection */}
<div className="md:col-span-1 border-r border-gray-100 pr-4">
<label className="block text-sm font-bold text-gray-700 mb-2">Calendly Event auswählen</label>
<select
value={selectedEventType}
onChange={(e) => {
setSelectedEventType(e.target.value);
localStorage.setItem('fotograf_selected_event_type', e.target.value);
}}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 text-sm"
>
{eventTypes.length === 0 && <option value="">Lade Events...</option>}
{eventTypes.map(et => (
<option key={et.uri} value={et.name}>{et.name}</option>
))}
</select>
<p className="text-xs text-gray-500 mt-2">Wird für QR-Karten und die Terminübersicht benötigt.</p>
</div>
{/* Actions */}
<div className="md:col-span-2 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{/* Action 0: Teilnehmerliste (PDF) - Moved here */}
<div className="bg-blue-50 p-4 rounded-lg flex flex-col justify-between border border-blue-100">
<div>
<h6 className="font-bold text-sm text-blue-800 mb-1">📄 Teilnehmerliste</h6>
<p className="text-xs text-blue-600 mb-3">Lädt alle Fotograf.de Anmeldungen als PDF herunter.</p>
</div>
<button
onClick={() => handleGeneratePdf(selectedJob)}
disabled={processingJobId === selectedJob.id || isStatsRunning}
className="w-full px-3 py-2 bg-blue-600 text-white text-xs font-bold rounded-lg hover:bg-blue-700 disabled:opacity-50 transition-all flex justify-center items-center mt-auto"
>
{processingJobId === selectedJob.id ? 'Lädt...' : 'PDF generieren'}
</button>
</div>
{/* Action 1: QR Cards */}
<div className="bg-gray-50 p-4 rounded-lg flex flex-col justify-between border border-gray-100">
<div>
<h6 className="font-bold text-sm text-gray-800 mb-1">📇 QR-Karten</h6>
<p className="text-xs text-gray-600 mb-3">Andruck auf Blanko-PDF.</p>
<input
type="file"
accept=".pdf"
id={`qr-upload-${selectedJob.id}`}
className="hidden"
onChange={(e) => {
if (e.target.files && e.target.files.length > 0) {
handleGenerateQrCards(selectedJob, e.target.files[0]);
e.target.value = ''; // reset
}
}}
/>
</div>
<button
onClick={() => document.getElementById(`qr-upload-${selectedJob.id}`)?.click()}
disabled={!selectedEventType || isQrGenerating}
className="w-full px-3 py-2 bg-emerald-600 text-white text-xs font-bold rounded-lg hover:bg-emerald-700 disabled:opacity-50 transition-all flex justify-center items-center gap-2 mt-auto"
>
{isQrGenerating ? 'Generiere...' : 'Upload & Start'}
</button>
</div>
{/* Action 2: Appointment List */}
<div className="bg-gray-50 p-4 rounded-lg flex flex-col justify-between border border-gray-100">
<div>
<h6 className="font-bold text-sm text-gray-800 mb-1">📋 Terminübersicht</h6>
<p className="text-xs text-gray-600 mb-3">Tabelle mit 6-Minuten Takt.</p>
</div>
<button
onClick={() => handleGenerateAppointmentList(selectedJob)}
disabled={!selectedEventType || isListGenerating}
className="w-full px-3 py-2 bg-indigo-600 text-white text-xs font-bold rounded-lg hover:bg-indigo-700 disabled:opacity-50 transition-all flex justify-center items-center gap-2 mt-auto"
>
{isListGenerating ? 'Generiere...' : 'Generieren'}
</button>
</div>
{/* Action 3: Siblings List */}
<div className="bg-gray-50 p-4 rounded-lg flex flex-col justify-between sm:col-span-2 lg:col-span-3 border border-gray-100">
<div>
<h6 className="font-bold text-sm text-gray-800 mb-1">👨👩👧👦 Geschwisterliste (Einrichtungsintern)</h6>
<p className="text-xs text-gray-600 mb-3">Abgleich von Kindergarten-Anmeldungen mit Calendly-Buchungen.</p>
</div>
<div className="grid grid-cols-2 gap-3 mt-auto">
<button
onClick={() => handleGenerateSiblingsList(selectedJob)}
disabled={isSiblingsGenerating}
className="w-full px-3 py-2 bg-teal-600 text-white text-xs font-bold rounded-lg hover:bg-teal-700 disabled:opacity-50 transition-all flex justify-center items-center gap-2"
>
{isSiblingsGenerating ? 'Generiere...' : '📄 PDF Liste'}
</button>
<div>
<input
type="file"
accept=".pdf"
id={`siblings-qr-upload-${selectedJob.id}`}
className="hidden"
onChange={(e) => {
if (e.target.files && e.target.files.length > 0) {
handleGenerateSiblingsQrCards(selectedJob, e.target.files[0]);
e.target.value = ''; // reset
}
}}
/>
<button
onClick={() => document.getElementById(`siblings-qr-upload-${selectedJob.id}`)?.click()}
disabled={isSiblingsQrGenerating}
className="w-full px-3 py-2 bg-emerald-600 text-white text-xs font-bold rounded-lg hover:bg-emerald-700 disabled:opacity-50 transition-all flex justify-center items-center gap-2 h-full"
>
{isSiblingsQrGenerating ? 'Generiere...' : '📇 QR-Karten drucken'}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
)}
{/* --- TAB: FOLLOW-UP --- */}
{mainTab === 'followup' && (
<> {/* Sub-Tab 1: Freigabe-Anfrage */}
<div className="bg-white border border-gray-200 rounded-xl p-5 hover:border-indigo-300 transition-colors shadow-sm md:col-span-2">
<div className="flex items-start justify-between mb-2">
<div className="flex items-center gap-3">
<div className="p-2 bg-indigo-50 rounded-lg text-indigo-600 text-xl">🖼</div>
<h5 className="font-bold text-gray-900 text-lg">Anfrage Veröffentlichung</h5>
</div>
</div>
<p className="text-sm text-gray-500 mb-4 line-clamp-2">Sende personalisierte DSGVO-Anfragen für ausgewählte Eltern inkl. Gutschein-Webhook.</p>
{/* Tabs Navigation */}
<div className="flex border-b border-gray-200 mb-4">
<button
onClick={() => setReleaseTab('recipients')}
className={`px-4 py-2 text-xs font-bold ${releaseTab === 'recipients' ? 'text-indigo-600 border-b-2 border-indigo-600' : 'text-gray-500 hover:text-gray-700'}`}
>
1. Empfängerliste
</button>
<button
onClick={() => setReleaseTab('preview')}
className={`px-4 py-2 text-xs font-bold ${releaseTab === 'preview' ? 'text-indigo-600 border-b-2 border-indigo-600' : 'text-gray-500 hover:text-gray-700'}`}
>
2. Vorschau & Versand
</button>
<button
onClick={() => setReleaseTab('codes')}
className={`px-4 py-2 text-xs font-bold ${releaseTab === 'codes' ? 'text-indigo-600 border-b-2 border-indigo-600' : 'text-gray-500 hover:text-gray-700'}`}
>
Gutschein-Codes
</button>
<button
onClick={() => setReleaseTab('history')}
className={`px-4 py-2 text-xs font-bold ${releaseTab === 'history' ? 'text-indigo-600 border-b-2 border-indigo-600' : 'text-gray-500 hover:text-gray-700'}`}
>
Antworten & Historie
</button>
</div>
<div className="space-y-4">
{releaseTab === 'recipients' && (
<div className="bg-gray-50 p-3 rounded-lg border border-gray-100">
<label className="text-[10px] font-bold text-gray-500 uppercase block mb-2">Empfänger-Daten (Pro Zeile)</label>
<p className="text-[10px] text-gray-400 mb-2 leading-tight">
Format: <b>E-Mail, Vorname Elternteil, Namen der Kinder</b><br/>
(z.B. <i>max@muster.de, Max, Moritz und Leni</i>)
</p>
<textarea
placeholder="max@muster.de, Max, Moritz und Leni&#10;anna@test.de, Anna, Lisa"
value={releaseEmails}
onChange={(e) => setReleaseEmails(e.target.value)}
className="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg focus:ring-2 focus:ring-indigo-500 mb-2 h-40 font-mono"
/>
<div className="flex justify-between items-center">
<span className="text-xs text-gray-500">{parsedReleaseEmails.length} Empfänger erkannt</span>
<button
onClick={() => { setReleaseTab('preview'); setPreviewIndex(0); }}
className="px-4 py-1.5 bg-indigo-100 text-indigo-700 text-xs font-bold rounded-lg hover:bg-indigo-200 transition-colors"
>
Zur Vorschau &rarr;
</button>
</div>
</div>
)}
{releaseTab === 'preview' && (
<div className="bg-gray-50 p-3 rounded-lg border border-gray-100">
{parsedReleaseEmails.length === 0 ? (
<div className="text-center py-6 text-gray-500 text-sm">Bitte zuerst Empfänger eintragen.</div>
) : (
<div>
<div className="flex justify-between items-center mb-3">
<h6 className="text-[10px] font-bold text-gray-500 uppercase">Vorschau für Empfänger {previewIndex + 1} von {parsedReleaseEmails.length}</h6>
<div className="flex items-center gap-2">
<button
onClick={() => setPreviewIndex(Math.max(0, previewIndex - 1))}
disabled={previewIndex === 0}
className="p-1 bg-white border border-gray-200 rounded text-gray-600 disabled:opacity-30"
>&larr;</button>
<button
onClick={() => setPreviewIndex(Math.min(parsedReleaseEmails.length - 1, previewIndex + 1))}
disabled={previewIndex >= parsedReleaseEmails.length - 1}
className="p-1 bg-white border border-gray-200 rounded text-gray-600 disabled:opacity-30"
>&rarr;</button>
</div>
</div>
{parsedReleaseEmails[previewIndex] && (
<div className="bg-white border border-gray-200 rounded-lg p-4 mb-4">
<div className="text-xs text-gray-500 mb-1 border-b border-gray-100 pb-2">
<span className="font-bold">An:</span> {parsedReleaseEmails[previewIndex].to}
</div>
<div className="text-sm font-bold text-gray-800 mt-2 mb-2">
{parsedReleaseEmails[previewIndex].subject}
</div>
<div
className="text-xs text-gray-700 space-y-2 email-preview"
dangerouslySetInnerHTML={{__html: parsedReleaseEmails[previewIndex].body}}
/>
</div>
)}
<div className="flex items-center gap-2 mb-3 bg-white p-2 rounded-lg border border-gray-100">
<span className="text-[10px] font-bold text-gray-400 uppercase">Versandzeit (Optional)</span>
<input
type="time"
value={scheduledTime}
onChange={(e) => setScheduledTime(e.target.value)}
className="text-xs border border-gray-200 rounded px-2 py-1 focus:ring-1 focus:ring-indigo-500"
/>
<span className="text-[10px] text-gray-400 italic">Leer = sofort</span>
</div>
<button
onClick={handleSendRelease}
disabled={isSendingRelease || !isGmailAuthenticated}
className="w-full px-3 py-2 bg-indigo-600 text-white text-sm font-bold rounded-lg hover:bg-indigo-700 disabled:opacity-50 shadow-sm"
>
{isSendingRelease ? 'Sende...' : `${parsedReleaseEmails.length} Anfrage-E-Mails jetzt senden`}
</button>
{!isGmailAuthenticated && <p className="text-center text-[10px] mt-2 text-red-500">Gmail nicht verbunden.</p>}
{releaseMessage && <p className="text-center text-xs mt-2 font-bold text-indigo-600">{releaseMessage}</p>}
</div>
)}
</div>
)}
{releaseTab === 'codes' && (
<div className="bg-gray-50 p-3 rounded-lg border border-gray-100">
<div className="flex justify-between items-center mb-2">
<label className="text-[10px] font-bold text-gray-500 uppercase">Gutscheincodes hinzufügen</label>
{releaseStats && (
<span className="text-xs text-indigo-600 font-bold">Verfügbar: {releaseStats.available} (Verwendet: {releaseStats.used})</span>
)}
</div>
<textarea
placeholder="z.B. CODE1, CODE2, CODE3"
value={releaseCodes}
onChange={(e) => setReleaseCodes(e.target.value)}
className="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg focus:ring-2 focus:ring-indigo-500 mb-2 h-24"
/>
<button
onClick={handleUploadCodes}
disabled={isUploadingCodes || !releaseCodes.trim()}
className="w-full px-3 py-1.5 bg-indigo-100 text-indigo-700 text-xs font-bold rounded-lg hover:bg-indigo-200 disabled:opacity-50"
>
{isUploadingCodes ? 'Lädt hoch...' : 'Codes speichern'}
</button>
{uploadMessage && <p className="text-center text-xs mt-2 font-bold text-gray-600">{uploadMessage}</p>}
</div>
)}
{releaseTab === 'history' && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Responses List */}
<div className="bg-white border border-gray-100 rounded-lg p-3 shadow-inner">
<div className="flex justify-between items-center mb-3">
<h6 className="text-[10px] font-bold text-gray-500 uppercase">Eingegangene Antworten (Codes)</h6>
<button
onClick={fetchReleaseResponses}
disabled={isFetchingResponses}
className="text-[10px] bg-indigo-50 text-indigo-600 px-2 py-1 rounded hover:bg-indigo-100 transition-colors flex items-center gap-1"
>
{isFetchingResponses ? '...' : '🔄 Aktualisieren'}
</button>
</div>
{!releaseResponses || releaseResponses.length === 0 ? (
<p className="text-[10px] text-gray-400 italic text-center py-4">Noch keine Antworten eingegangen.</p>
) : (
<div className="max-h-60 overflow-y-auto rounded border border-gray-50">
<table className="min-w-full text-[10px] text-left">
<thead className="bg-gray-50 text-gray-400 sticky top-0">
<tr>
<th className="px-2 py-1">E-Mail</th>
<th className="px-2 py-1">Code</th>
<th className="px-2 py-1">Datum</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-50">
{releaseResponses.map((res, idx) => (
<tr key={idx} className="hover:bg-indigo-50/30">
<td className="px-2 py-1 text-gray-700 truncate max-w-[120px]" title={res.email}>{res.email}</td>
<td className="px-2 py-1 font-mono font-bold text-indigo-600">{res.code}</td>
<td className="px-2 py-1 text-gray-400">{new Date(res.used_at).toLocaleDateString('de-DE', {day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit'})}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
{/* Send Out History List */}
<div className="bg-white border border-gray-100 rounded-lg p-3 shadow-inner">
<div className="flex justify-between items-center mb-3">
<h6 className="text-[10px] font-bold text-gray-500 uppercase">Versand-Historie (Anfragen)</h6>
<button
onClick={fetchReleaseHistoryData}
className="text-[10px] bg-indigo-50 text-indigo-600 px-2 py-1 rounded hover:bg-indigo-100 transition-colors flex items-center gap-1"
>
🔄 Aktualisieren
</button>
</div>
{!releaseHistoryData || releaseHistoryData.length === 0 ? (
<p className="text-[10px] text-gray-400 italic text-center py-4">Noch keine Versand-Aktivitäten.</p>
) : (
<div className="max-h-60 overflow-y-auto rounded border border-gray-50">
<table className="min-w-full text-[10px] text-left">
<thead className="bg-gray-50 text-gray-400 sticky top-0">
<tr>
<th className="px-2 py-1">Datum/Zeit</th>
<th className="px-2 py-1">Empfänger</th>
<th className="px-2 py-1">Geplant für</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-50">
{releaseHistoryData.map((h, idx) => (
<tr key={idx} className="hover:bg-indigo-50/30">
<td className="px-2 py-1 text-gray-700">{new Date(h.timestamp).toLocaleDateString('de-DE', {day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit'})}</td>
<td className="px-2 py-1 font-bold text-indigo-600">{h.recipient_count}</td>
<td className="px-2 py-1 text-gray-400">{h.scheduled_time || 'Sofort'}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
)}
</div>
{/* Sub-Tab 2: Nachfass-Mails */}
<div className="bg-white border border-gray-200 rounded-xl p-5 hover:border-amber-300 transition-colors shadow-sm md:col-span-2">
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-amber-50 rounded-lg text-amber-600 text-xl"></div>
<h5 className="font-bold text-gray-900 text-lg">Nachfassen (Erinnerungen)</h5>
</div>
</div>
<div className="bg-gray-50 p-4 rounded-lg border border-gray-100 flex flex-col">
<h6 className="font-bold text-sm text-gray-800 mb-1">Erinnerungen (0-1 Logins)</h6>
<p className="text-xs text-gray-600 mb-4">Identifiziert Nicht-Käufer für den Supermailer oder Gmail Direkt-Versand.</p>
<div className="mt-auto">
{isReminderRunning ? (
<div className="w-full bg-white p-3 rounded border border-gray-200 text-sm flex flex-col gap-2">
<div className="flex items-center gap-2">
<svg className="animate-spin h-4 w-4 text-amber-600" viewBox="0 0 24 24" fill="none"><circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" /><path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" /></svg>
<span className="font-medium text-gray-700">Analyse läuft...</span>
</div>
<p className="text-xs text-gray-500 break-words">{reminderProgress}</p>
</div>
) : reminderTaskId ? (
<div className="space-y-3">
<button
onClick={() => handleDownloadReminderCsv(reminderTaskId)}
className="w-full px-4 py-2 bg-emerald-600 text-white text-xs font-bold rounded-lg hover:bg-emerald-700 transition-colors shadow-sm flex items-center justify-center gap-2"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a2 2 0 002 2h12a2 2 0 002-2v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /></svg>
CSV für Supermailer
</button>
</div>
) : (
<button
onClick={() => handleStartReminderAnalysis(selectedJob)}
disabled={processingJobId !== null || isReminderRunning || isStatsRunning}
className="w-full px-4 py-2 bg-amber-600 text-white text-xs font-bold rounded-lg hover:bg-amber-700 disabled:opacity-50 transition-colors shadow-sm"
>
Analyse starten
</button>
)}
</div>
</div>
</div>
{/* Expanded Gmail Action for Reminders (only visible after reminder analysis) */}
{reminderTaskId && reminderResult && isGmailAuthenticated && (
<div className="mt-4 border-t border-gray-100 pt-4 bg-gray-50 p-4 rounded-xl space-y-4">
<div className="flex items-center justify-between">
<h6 className="font-bold text-gray-900 flex items-center gap-2">
<span>🚀</span> Gmail Direkt-Versand
</h6>
<p className="text-xs text-indigo-600 font-bold bg-indigo-50 px-2 py-1 rounded">
{reminderResult.length} Empfänger identifiziert
</p>
</div>
{/* Reminder Tabs Navigation */}
<div className="flex border-b border-gray-200 mb-2">
<button
onClick={() => setReminderTab('config')}
className={`px-4 py-2 text-xs font-bold ${reminderTab === 'config' ? 'text-indigo-600 border-b-2 border-indigo-600' : 'text-gray-500 hover:text-gray-700'}`}
>
1. Text konfigurieren
</button>
<button
onClick={() => { setReminderTab('preview'); setReminderPreviewIndex(0); }}
className={`px-4 py-2 text-xs font-bold ${reminderTab === 'preview' ? 'text-indigo-600 border-b-2 border-indigo-600' : 'text-gray-500 hover:text-gray-700'}`}
>
2. Vorschau & Versand
</button>
</div>
{reminderTab === 'config' && (
<div className="space-y-4">
<div className="space-y-2">
<label className="text-[10px] font-bold text-gray-400 uppercase">Betreff</label>
<input
value={emailSubject}
onChange={(e) => setEmailSubject(e.target.value)}
className="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg focus:ring-2 focus:ring-indigo-500"
/>
</div>
<div className="space-y-2">
<label className="text-[10px] font-bold text-gray-400 uppercase">Nachricht (HTML erlaubt)</label>
<textarea
value={emailBody}
onChange={(e) => setEmailBody(e.target.value)}
rows={4}
className="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg focus:ring-2 focus:ring-indigo-500 font-mono"
/>
<div className="flex justify-between items-center text-[10px] text-gray-400">
<span>Platzhalter: {"{Name Käufer}"}, {"{Kindernamen}"}, {"{LinksHTML}"}</span>
<span className="flex items-center gap-1">
<svg className="w-3 h-3 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
Signatur "Kinderfotos Erding" wird automatisch angehängt
</span>
</div>
</div>
<button
onClick={() => { setReminderTab('preview'); setReminderPreviewIndex(0); }}
className="w-full px-4 py-2 bg-indigo-100 text-indigo-700 text-sm font-bold rounded-lg hover:bg-indigo-200 transition-colors"
>
Zur Vorschau &rarr;
</button>
</div>
)}
{reminderTab === 'preview' && (
<div>
{parsedReminderEmails.length === 0 ? (
<div className="text-center py-6 text-gray-500 text-sm">Keine Empfänger für die Vorschau.</div>
) : (
<div>
<div className="flex justify-between items-center mb-3">
<h6 className="text-[10px] font-bold text-gray-500 uppercase">Vorschau für Empfänger {reminderPreviewIndex + 1} von {parsedReminderEmails.length}</h6>
<div className="flex items-center gap-2">
<button
onClick={() => setReminderPreviewIndex(Math.max(0, reminderPreviewIndex - 1))}
disabled={reminderPreviewIndex === 0}
className="p-1 bg-white border border-gray-200 rounded text-gray-600 disabled:opacity-30"
>&larr;</button>
<button
onClick={() => setReminderPreviewIndex(Math.min(parsedReminderEmails.length - 1, reminderPreviewIndex + 1))}
disabled={reminderPreviewIndex >= parsedReminderEmails.length - 1}
className="p-1 bg-white border border-gray-200 rounded text-gray-600 disabled:opacity-30"
>&rarr;</button>
</div>
</div>
{parsedReminderEmails[reminderPreviewIndex] && (
<div className="bg-white border border-gray-200 rounded-lg p-4 mb-4">
<div className="text-xs text-gray-500 mb-1 border-b border-gray-100 pb-2">
<span className="font-bold">An:</span> {parsedReminderEmails[reminderPreviewIndex].to}
</div>
<div className="text-sm font-bold text-gray-800 mt-2 mb-2">
{parsedReminderEmails[reminderPreviewIndex].subject}
</div>
<div
className="text-xs text-gray-700 space-y-2 email-preview"
dangerouslySetInnerHTML={{__html: parsedReminderEmails[reminderPreviewIndex].body}}
/>
</div>
)}
<button
onClick={handleSendEmails}
disabled={isSendingEmails}
className="w-full py-2.5 bg-indigo-600 text-white text-sm font-bold rounded-lg hover:bg-indigo-700 disabled:opacity-50 transition-all shadow-md flex items-center justify-center gap-2"
>
{isSendingEmails ? (
<>
<svg className="animate-spin h-4 w-4" viewBox="0 0 24 24" fill="none"><circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" /><path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" /></svg>
Sende Mails...
</>
) : (
<>{parsedReminderEmails.length} Erinnerungs-Mails jetzt versenden</>
)}
</button>
{emailSendStatus && (
<p className="text-center text-xs font-bold text-indigo-600 mt-2">{emailSendStatus}</p>
)}
</div>
)}
</div>
)}
</div>
)}
</div>
</>
)}
{/* --- TAB: STATISTIK --- */} {mainTab === 'statistik' && (
<div className="bg-white border border-gray-200 rounded-xl p-5 hover:border-purple-300 transition-colors shadow-sm md:col-span-2">
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-purple-50 rounded-lg text-purple-600 text-xl">📊</div>
<h5 className="font-bold text-gray-900 text-lg">Verkaufsstatistik</h5>
</div>
</div>
<p className="text-sm text-gray-500 mb-4">Wie viele Kinder haben wie viel gekauft? Starten Sie den Job, um die Daten abzurufen.</p>
<div className="mb-6">
{isStatsRunning ? (
<div className="w-full bg-gray-50 p-3 rounded border border-gray-200 text-sm flex flex-col gap-2">
<div className="flex items-center gap-2">
<svg className="animate-spin h-4 w-4 text-purple-600" viewBox="0 0 24 24" fill="none"><circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" /><path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" /></svg>
<span className="font-medium text-gray-700">Läuft im Hintergrund...</span>
</div>
<p className="text-xs text-gray-500 break-words">{statsProgress}</p>
</div>
) : (
<button
onClick={() => handleStartStatistics(selectedJob)}
disabled={processingJobId !== null || isStatsRunning}
className="px-4 py-2 bg-purple-600 text-white text-sm font-bold rounded-lg hover:bg-purple-700 disabled:opacity-50 transition-colors shadow-sm"
>
Statistik-Lauf starten
</button>
)}
</div>
{/* Data View Area */}
{statsResult && (
<div className="mt-4 border-t border-gray-200 pt-6">
<div className="flex justify-between items-center mb-4">
<h4 className="text-lg font-bold text-gray-800">Ergebnis der Auswertung</h4>
<span className="text-xs bg-emerald-100 text-emerald-800 px-2 py-1 rounded font-medium">Erfolgreich abgeschlossen</span>
</div>
<div className="overflow-x-auto rounded-lg border border-gray-200">
<table className="min-w-full divide-y divide-gray-200 text-sm text-left">
<thead className="bg-gray-50 text-gray-500 uppercase">
<tr>
<th className="px-4 py-3 font-medium">Album</th>
<th className="px-4 py-3 font-medium text-center">Kinder (Gesamt)</th>
<th className="px-4 py-3 font-medium text-center">Mit Käufen</th>
<th className="px-4 py-3 font-medium text-center">Alle Bilder</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{statsResult.map((row, idx) => (
<tr key={idx} className="hover:bg-gray-50">
<td className="px-4 py-3 font-medium text-gray-900">{row.Album}</td>
<td className="px-4 py-3 text-center text-gray-600">{row.Kinder_insgesamt}</td>
<td className="px-4 py-3 text-center text-emerald-600 font-medium">{row.Kinder_mit_Käufen}</td>
<td className="px-4 py-3 text-center text-indigo-600 font-medium">{row.Kinder_Alle_Bilder_gekauft}</td>
</tr>
))}
{/* Total Row */}
<tr className="bg-gray-50 font-bold border-t-2 border-gray-300">
<td className="px-4 py-3 text-gray-900">Gesamt ({statsResult.length} Alben)</td>
<td className="px-4 py-3 text-center text-gray-900">
{statsResult.reduce((sum, row) => sum + (row.Kinder_insgesamt || 0), 0)}
</td>
<td className="px-4 py-3 text-center text-emerald-700">
{statsResult.reduce((sum, row) => sum + (row.Kinder_mit_Käufen || 0), 0)}
</td>
<td className="px-4 py-3 text-center text-indigo-700">
{statsResult.reduce((sum, row) => sum + (row.Kinder_Alle_Bilder_gekauft || 0), 0)}
</td>
</tr>
</tbody>
</table>
</div>
</div>
)}
</div>
)}
</div>
</div>
{/* Modal Footer */}
<div className="bg-gray-100 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse border-t border-gray-200">
<button
type="button"
className="w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
onClick={() => {
setSelectedJob(null);
setStatsResult(null);
setStatsTaskId(null);
}}
>
Schließen
</button>
</div>
</div>
</div>
</div>
)}
</div>
);
}
export default App;