[34588f42] Chore: Build-Artefakte und UI-Struktur-Fixes

- Frontend Produktions-Build aktualisiert.
- Syntax-Fehler in App.tsx korrigiert und Tabs-Layout stabilisiert.
This commit is contained in:
2026-04-18 13:09:23 +00:00
parent 2a85cab4ab
commit e6061868e6
9 changed files with 674 additions and 364 deletions

View File

@@ -1 +1 @@
{"task_id": "34588f42-8544-8046-85d4-d7895ed9b29c", "token": "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", "readme_path": null, "session_start_time": "2026-04-18T11:12:01.291297"} {"task_id": "34588f42-8544-8046-85d4-d7895ed9b29c", "token": "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", "readme_path": "readme.md", "session_start_time": "2026-04-18T13:09:19.478670"}

View File

@@ -42,6 +42,13 @@ class ReleaseParticipant(Base):
first_name = Column(String) first_name = Column(String)
last_updated = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow) last_updated = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow)
class ReleaseHistory(Base):
__tablename__ = "release_history"
id = Column(Integer, primary_key=True)
timestamp = Column(DateTime, default=datetime.datetime.utcnow)
recipient_count = Column(Integer)
scheduled_time = Column(String, nullable=True)
Base.metadata.create_all(bind=engine) Base.metadata.create_all(bind=engine)
def get_db(): def get_db():

View File

@@ -1,7 +1,7 @@
from fastapi import APIRouter, Depends, HTTPException, Request, BackgroundTasks from fastapi import APIRouter, Depends, HTTPException, Request, BackgroundTasks
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from database import get_db, DiscountCode, ReleaseParticipant from database import get_db, DiscountCode, ReleaseParticipant, ReleaseHistory
import datetime import datetime
import logging import logging
from gmail_service import GmailService from gmail_service import GmailService
@@ -96,9 +96,18 @@ async def send_requests(data: SendReleaseRequest, background_tasks: BackgroundTa
if data.scheduled_time: if data.scheduled_time:
# Pass a way to get a new session to the background task # Pass a way to get a new session to the background task
from database import SessionLocal from database import SessionLocal
# Log to history
db.add(ReleaseHistory(recipient_count=len(data.emails), scheduled_time=data.scheduled_time))
db.commit()
background_tasks.add_task(delayed_send, data.emails, data.scheduled_time, SessionLocal) background_tasks.add_task(delayed_send, data.emails, data.scheduled_time, SessionLocal)
return {"status": "scheduled", "message": f"Versand für {data.scheduled_time} geplant."} return {"status": "scheduled", "message": f"Versand für {data.scheduled_time} geplant."}
# Log immediate send to history
db.add(ReleaseHistory(recipient_count=len(data.emails), scheduled_time="Sofort"))
db.commit()
# Immediate send # Immediate send
service = GmailService(db) service = GmailService(db)
success = 0 success = 0
@@ -111,6 +120,11 @@ async def send_requests(data: SendReleaseRequest, background_tasks: BackgroundTa
return {"status": "success", "success": success, "failed": failed} return {"status": "success", "success": success, "failed": failed}
@router.get("/history")
def get_history(db: Session = Depends(get_db)):
history = db.query(ReleaseHistory).order_by(ReleaseHistory.timestamp.desc()).all()
return [{"id": h.id, "timestamp": h.timestamp.isoformat(), "recipient_count": h.recipient_count, "scheduled_time": h.scheduled_time} for h in history]
@router.get("/stats") @router.get("/stats")
def get_stats(db: Session = Depends(get_db)): def get_stats(db: Session = Depends(get_db)):
total = db.query(DiscountCode).count() total = db.query(DiscountCode).count()

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -5,8 +5,8 @@
<link rel="icon" type="image/svg+xml" href="/fotograf-de/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/fotograf-de/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Fotograf.de ERP</title> <title>Fotograf.de ERP</title>
<script type="module" crossorigin src="/fotograf-de/assets/index-9o0T5Jx2.js"></script> <script type="module" crossorigin src="/fotograf-de/assets/index-FME2rxM6.js"></script>
<link rel="stylesheet" crossorigin href="/fotograf-de/assets/index-BaSYoDWO.css"> <link rel="stylesheet" crossorigin href="/fotograf-de/assets/index-CSrvX8PS.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react'; import { useState, useEffect, useMemo } from 'react';
import './App.css'; import './App.css';
interface Job { interface Job {
@@ -64,6 +64,92 @@ function App() {
const [scheduledTime, setScheduledTime] = useState(""); // New state const [scheduledTime, setScheduledTime] = useState(""); // New state
const [releaseResponses, setReleaseResponses] = useState<any[] | null>(null); const [releaseResponses, setReleaseResponses] = useState<any[] | null>(null);
const [isFetchingResponses, setIsFetchingResponses] = useState(false); 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 () => { const fetchReleaseStats = async () => {
try { try {
@@ -119,38 +205,13 @@ function App() {
setIsSendingRelease(true); setIsSendingRelease(true);
setReleaseMessage("Bereite Senden vor..."); setReleaseMessage("Bereite Senden vor...");
const lines = releaseEmails.split('\n').filter(line => line.trim()); const emailsToSend = parsedReleaseEmails;
if (emailsToSend.length === 0) {
if (lines.length === 0) { setReleaseMessage("⚠️ Bitte Empfänger eintragen.");
setReleaseMessage("⚠️ Bitte Daten eingeben.");
setIsSendingRelease(false); setIsSendingRelease(false);
return; return;
} }
const emailsToSend = 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 (e.g., 2026)
.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 // Extra field for participant mapping
};
}).filter(e => e.to);
setReleaseMessage(`Sende ${emailsToSend.length} Mails...`); setReleaseMessage(`Sende ${emailsToSend.length} Mails...`);
try { try {
@@ -166,6 +227,7 @@ function App() {
const data = await response.json(); const data = await response.json();
if (response.ok) { if (response.ok) {
setReleaseMessage(`✅ Fertig! ${data.success} gesendet. ${data.failed.length > 0 ? '(' + data.failed.length + ' Fehler)' : ''}`); setReleaseMessage(`✅ Fertig! ${data.success} gesendet. ${data.failed.length > 0 ? '(' + data.failed.length + ' Fehler)' : ''}`);
fetchReleaseHistoryData(); // Update history
} else { } else {
setReleaseMessage("❌ Fehler beim Senden."); setReleaseMessage("❌ Fehler beim Senden.");
} }
@@ -175,27 +237,6 @@ function App() {
setIsSendingRelease(false); setIsSendingRelease(false);
}; };
// 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>
`;
// If we are on duckdns, use the relative path, otherwise use local IP // If we are on duckdns, use the relative path, otherwise use local IP
const API_BASE_URL = window.location.hostname.includes('duckdns.org') const API_BASE_URL = window.location.hostname.includes('duckdns.org')
? '/fotograf-de-api' ? '/fotograf-de-api'
@@ -266,6 +307,7 @@ function App() {
checkGmailAuth(); checkGmailAuth();
fetchReleaseStats(); fetchReleaseStats();
fetchReleaseResponses(); fetchReleaseResponses();
fetchReleaseHistoryData();
}, [activeTab]); }, [activeTab]);
const handleRefresh = () => fetchJobs(activeTab, true); const handleRefresh = () => fetchJobs(activeTab, true);
@@ -865,35 +907,38 @@ function App() {
</div> </div>
)} )}
<h4 className="text-lg font-bold text-gray-800 mb-4">Verfügbare Tools</h4> {/* Main Modal Tabs Navigation */}
<div className="flex border-b border-gray-200 mb-6 gap-2">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Tool 1: PDF List */}
<div className="bg-white border border-gray-200 rounded-xl p-5 hover:border-blue-300 transition-colors shadow-sm">
<div className="flex items-start justify-between mb-2">
<div className="p-2 bg-blue-50 rounded-lg text-blue-600 text-xl">📄</div>
</div>
<h5 className="font-bold text-gray-900 mb-1">Teilnehmerliste (PDF)</h5>
<p className="text-sm text-gray-500 mb-4 line-clamp-2">Lädt die Anmeldungen herunter und formatiert sie als sauberes PDF (Gruppen/Klassen sortiert).</p>
<button <button
onClick={() => handleGeneratePdf(selectedJob)} onClick={() => setMainTab('vorbereitung')}
disabled={processingJobId === selectedJob.id || isStatsRunning} 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'}`}
className="w-full flex justify-center items-center gap-2 px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 focus:ring-4 focus:ring-blue-100 disabled:opacity-50 transition-colors"
> >
{processingJobId === selectedJob.id ? 'Lädt CSV via Selenium...' : 'PDF generieren & speichern'} <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> </button>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Tool 2: Shooting-Planung (QR & List) */} {/* --- 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="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-start justify-between mb-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="p-2 bg-emerald-50 rounded-lg text-emerald-600 text-xl">📆</div> <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</h5> <h5 className="font-bold text-gray-900 text-lg">Shooting-Planung & Listen</h5>
</div> </div>
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-emerald-100 text-emerald-800">Neu</span> <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>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
@@ -913,16 +958,32 @@ function App() {
<option key={et.uri} value={et.name}>{et.name}</option> <option key={et.uri} value={et.name}>{et.name}</option>
))} ))}
</select> </select>
<p className="text-xs text-gray-500 mt-2">Die Termine für diesen Event-Typ werden aus Calendly importiert.</p> <p className="text-xs text-gray-500 mt-2">Wird für QR-Karten und die Terminübersicht benötigt.</p>
</div> </div>
{/* Actions */} {/* Actions */}
<div className="md:col-span-2 grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="md:col-span-2 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{/* Action 1: QR Cards */}
<div className="bg-gray-50 p-4 rounded-lg flex flex-col justify-between"> {/* 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> <div>
<h6 className="font-bold text-sm text-gray-800 mb-1">📇 QR-Zugangskarten</h6> <h6 className="font-bold text-sm text-blue-800 mb-1">📄 Teilnehmerliste</h6>
<p className="text-xs text-gray-600 mb-3">Druckt Namen und Uhrzeit auf vorbereitete PDF-Bögen.</p> <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 <input
type="file" type="file"
accept=".pdf" accept=".pdf"
@@ -941,30 +1002,30 @@ function App() {
disabled={!selectedEventType || isQrGenerating} 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" 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...' : 'Blanko PDF hochladen & starten'} {isQrGenerating ? 'Generiere...' : 'Upload & Start'}
</button> </button>
</div> </div>
{/* Action 2: Appointment List */} {/* Action 2: Appointment List */}
<div className="bg-gray-50 p-4 rounded-lg flex flex-col justify-between"> <div className="bg-gray-50 p-4 rounded-lg flex flex-col justify-between border border-gray-100">
<div> <div>
<h6 className="font-bold text-sm text-gray-800 mb-1">📄 Termin-Übersicht</h6> <h6 className="font-bold text-sm text-gray-800 mb-1">📋 Terminübersicht</h6>
<p className="text-xs text-gray-600 mb-3">PDF mit 6-Minuten Taktung und Lücken für den Shooting-Tag.</p> <p className="text-xs text-gray-600 mb-3">Tabelle mit 6-Minuten Takt.</p>
</div> </div>
<button <button
onClick={() => handleGenerateAppointmentList(selectedJob)} onClick={() => handleGenerateAppointmentList(selectedJob)}
disabled={!selectedEventType || isListGenerating} 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" 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...' : 'PDF Liste generieren'} {isListGenerating ? 'Generiere...' : 'Generieren'}
</button> </button>
</div> </div>
{/* Action 3: Siblings List */} {/* Action 3: Siblings List */}
<div className="bg-gray-50 p-4 rounded-lg flex flex-col justify-between sm:col-span-2"> <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> <div>
<h6 className="font-bold text-sm text-gray-800 mb-1">👨👩👧👦 Geschwisterliste (Einrichtungsintern)</h6> <h6 className="font-bold text-sm text-gray-800 mb-1">👨👩👧👦 Geschwisterliste (Einrichtungsintern)</h6>
<p className="text-xs text-gray-600 mb-3">Sucht nach Geschwisterkindern in der Einrichtung und gleicht diese mit Calendly ab.</p> <p className="text-xs text-gray-600 mb-3">Abgleich von Kindergarten-Anmeldungen mit Calendly-Buchungen.</p>
</div> </div>
<div className="grid grid-cols-2 gap-3 mt-auto"> <div className="grid grid-cols-2 gap-3 mt-auto">
<button <button
@@ -1002,41 +1063,50 @@ function App() {
</div> </div>
</div> </div>
</div> </div>
)}
{/* Tool 4: Freigabe-Anfrage */} {/* --- TAB: FOLLOW-UP --- */}
<div className="bg-white border border-gray-200 rounded-xl p-5 hover:border-amber-300 transition-colors shadow-sm mt-4"> {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-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> <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> </div>
<h5 className="font-bold text-gray-900 mb-1">Anfrage Veröffentlichung</h5>
<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> <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>
<div className="space-y-4"> {/* Tabs Navigation */}
{/* Upload Codes */} <div className="flex border-b border-gray-200 mb-4">
<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-16"
/>
<button <button
onClick={handleUploadCodes} onClick={() => setReleaseTab('recipients')}
disabled={isUploadingCodes || !releaseCodes.trim()} 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'}`}
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'} 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> </button>
{uploadMessage && <p className="text-center text-xs mt-2 font-bold text-gray-600">{uploadMessage}</p>}
</div> </div>
{/* Send Requests */} <div className="space-y-4">
{releaseTab === 'recipients' && (
<div className="bg-gray-50 p-3 rounded-lg border border-gray-100"> <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> <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"> <p className="text-[10px] text-gray-400 mb-2 leading-tight">
@@ -1047,8 +1117,56 @@ function App() {
placeholder="max@muster.de, Max, Moritz und Leni&#10;anna@test.de, Anna, Lisa" placeholder="max@muster.de, Max, Moritz und Leni&#10;anna@test.de, Anna, Lisa"
value={releaseEmails} value={releaseEmails}
onChange={(e) => setReleaseEmails(e.target.value)} 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-20 font-mono" 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"> <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> <span className="text-[10px] font-bold text-gray-400 uppercase">Versandzeit (Optional)</span>
@@ -1060,22 +1178,52 @@ function App() {
/> />
<span className="text-[10px] text-gray-400 italic">Leer = sofort</span> <span className="text-[10px] text-gray-400 italic">Leer = sofort</span>
</div> </div>
<button <button
onClick={handleSendRelease} onClick={handleSendRelease}
disabled={isSendingRelease || !releaseEmails.trim() || !isGmailAuthenticated} disabled={isSendingRelease || !isGmailAuthenticated}
className="w-full px-3 py-1.5 bg-indigo-600 text-white text-sm font-bold rounded-lg hover:bg-indigo-700 disabled:opacity-50 shadow-sm" 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...' : 'Anfrage-E-Mails jetzt senden'} {isSendingRelease ? 'Sende...' : `${parsedReleaseEmails.length} Anfrage-E-Mails jetzt senden`}
</button> </button>
{!isGmailAuthenticated && <p className="text-center text-[10px] mt-2 text-red-500">Gmail nicht verbunden.</p>} {!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>} {releaseMessage && <p className="text-center text-xs mt-2 font-bold text-indigo-600">{releaseMessage}</p>}
</div> </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 */} {/* Responses List */}
<div className="bg-white border border-gray-100 rounded-lg p-3 shadow-inner"> <div className="bg-white border border-gray-100 rounded-lg p-3 shadow-inner">
<div className="flex justify-between items-center mb-3"> <div className="flex justify-between items-center mb-3">
<h6 className="text-[10px] font-bold text-gray-500 uppercase">Eingegangene Antworten</h6> <h6 className="text-[10px] font-bold text-gray-500 uppercase">Eingegangene Antworten (Codes)</h6>
<button <button
onClick={fetchReleaseResponses} onClick={fetchReleaseResponses}
disabled={isFetchingResponses} disabled={isFetchingResponses}
@@ -1088,7 +1236,7 @@ function App() {
{!releaseResponses || releaseResponses.length === 0 ? ( {!releaseResponses || releaseResponses.length === 0 ? (
<p className="text-[10px] text-gray-400 italic text-center py-4">Noch keine Antworten eingegangen.</p> <p className="text-[10px] text-gray-400 italic text-center py-4">Noch keine Antworten eingegangen.</p>
) : ( ) : (
<div className="max-h-40 overflow-y-auto rounded border border-gray-50"> <div className="max-h-60 overflow-y-auto rounded border border-gray-50">
<table className="min-w-full text-[10px] text-left"> <table className="min-w-full text-[10px] text-left">
<thead className="bg-gray-50 text-gray-400 sticky top-0"> <thead className="bg-gray-50 text-gray-400 sticky top-0">
<tr> <tr>
@@ -1110,45 +1258,124 @@ function App() {
</div> </div>
)} )}
</div> </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> </div>
{/* Tool 3: Follow-up Emails */} {!releaseHistoryData || releaseHistoryData.length === 0 ? (
<div className="bg-white border border-gray-200 rounded-xl p-5 hover:border-amber-300 transition-colors shadow-sm"> <p className="text-[10px] text-gray-400 italic text-center py-4">Noch keine Versand-Aktivitäten.</p>
<div className="flex items-start justify-between mb-2"> ) : (
<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> <div className="p-2 bg-amber-50 rounded-lg text-amber-600 text-xl"></div>
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-700">Aktiv</span> <h5 className="font-bold text-gray-900 text-lg">Nachfassen (Erinnerungen)</h5>
</div>
</div> </div>
<h5 className="font-bold text-gray-900 mb-1">Nachfass-Mails (Supermailer)</h5>
<p className="text-sm text-gray-500 mb-4 line-clamp-2">Analysiert das Kaufverhalten und generiert eine fertige CSV-Liste für den Supermailer.</p>
<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 ? ( {isReminderRunning ? (
<div className="w-full bg-gray-100 p-3 rounded-lg text-sm text-gray-700 flex flex-col gap-2"> <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"> <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> <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-amber-700">Analyse läuft...</span> <span className="font-medium text-gray-700">Analyse läuft...</span>
</div> </div>
<p className="text-xs break-words">{reminderProgress}</p> <p className="text-xs text-gray-500 break-words">{reminderProgress}</p>
</div> </div>
) : reminderTaskId ? ( ) : reminderTaskId ? (
<div className="space-y-3"> <div className="space-y-3">
<button <button
onClick={() => handleDownloadReminderCsv(reminderTaskId)} onClick={() => handleDownloadReminderCsv(reminderTaskId)}
className="w-full px-4 py-2 bg-emerald-600 text-white text-sm font-medium rounded-lg hover:bg-emerald-700 transition-colors shadow-sm flex items-center justify-center gap-2" 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> <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 CSV für Supermailer
</button> </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>
{reminderResult && isGmailAuthenticated && ( </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="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"> <h6 className="font-bold text-gray-900 flex items-center gap-2">
<span>🚀</span> Gmail Direkt-Versand <span>🚀</span> Gmail Direkt-Versand
</h6> </h6>
<p className="text-xs text-gray-500"> <p className="text-xs text-indigo-600 font-bold bg-indigo-50 px-2 py-1 rounded">
{reminderResult.length} Empfänger identifiziert. {reminderResult.length} Empfänger identifiziert
</p> </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"> <div className="space-y-2">
<label className="text-[10px] font-bold text-gray-400 uppercase">Betreff</label> <label className="text-[10px] font-bold text-gray-400 uppercase">Betreff</label>
<input <input
@@ -1169,11 +1396,56 @@ function App() {
<div className="flex justify-between items-center text-[10px] text-gray-400"> <div className="flex justify-between items-center text-[10px] text-gray-400">
<span>Platzhalter: {"{Name Käufer}"}, {"{Kindernamen}"}, {"{LinksHTML}"}</span> <span>Platzhalter: {"{Name Käufer}"}, {"{Kindernamen}"}, {"{LinksHTML}"}</span>
<span className="flex items-center gap-1"> <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> <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 Signatur "Kinderfotos Erding" wird automatisch angehängt
</span> </span>
</div> </div>
</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 <button
onClick={handleSendEmails} onClick={handleSendEmails}
@@ -1186,58 +1458,55 @@ function App() {
Sende Mails... Sende Mails...
</> </>
) : ( ) : (
<>Mails jetzt versenden</> <>{parsedReminderEmails.length} Erinnerungs-Mails jetzt versenden</>
)} )}
</button> </button>
{emailSendStatus && ( {emailSendStatus && (
<p className="text-center text-xs font-bold text-indigo-600">{emailSendStatus}</p> <p className="text-center text-xs font-bold text-indigo-600 mt-2">{emailSendStatus}</p>
)} )}
</div> </div>
)} )}
</div> </div>
) : (
<button
onClick={() => handleStartReminderAnalysis(selectedJob)}
disabled={processingJobId !== null || isReminderRunning || isStatsRunning}
className="w-full px-4 py-2 bg-amber-600 text-white text-sm font-medium rounded-lg hover:bg-amber-700 disabled:opacity-50 transition-colors shadow-sm"
>
Analyse starten (Dauert lange)
</button>
)} )}
</div> </div>
{/* Tool 4: Statistics */} )}
<div className="bg-white border border-gray-200 rounded-xl p-5 hover:border-purple-300 transition-colors shadow-sm">
<div className="flex items-start justify-between mb-2">
<div className="p-2 bg-purple-50 rounded-lg text-purple-600 text-xl">📊</div>
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-purple-100 text-purple-700">Aktiv</span>
</div> </div>
<h5 className="font-bold text-gray-900 mb-1">Verkaufsstatistik</h5> </>
<p className="text-sm text-gray-500 mb-4 line-clamp-2">Durchforstet alle Alben und liefert eine Übersicht: Wie viele Kinder haben wie viel gekauft?</p> )}
{/* --- 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 ? ( {isStatsRunning ? (
<div className="w-full bg-gray-100 p-3 rounded-lg text-sm text-gray-700 flex flex-col gap-2"> <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"> <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> <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-purple-700">Läuft im Hintergrund...</span> <span className="font-medium text-gray-700">Läuft im Hintergrund...</span>
</div> </div>
<p className="text-xs break-words">{statsProgress}</p> <p className="text-xs text-gray-500 break-words">{statsProgress}</p>
</div> </div>
) : ( ) : (
<button <button
onClick={() => handleStartStatistics(selectedJob)} onClick={() => handleStartStatistics(selectedJob)}
disabled={processingJobId !== null || isStatsRunning} disabled={processingJobId !== null || isStatsRunning}
className="w-full px-4 py-2 bg-purple-600 text-white text-sm font-medium rounded-lg hover:bg-purple-700 disabled:opacity-50 transition-colors shadow-sm" 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 Statistik-Lauf starten
</button> </button>
)} )}
</div> </div>
</div> {/* Data View Area */}
{/* Optional Data View Area (for later use, e.g., showing the stats table here) */}
{statsResult && ( {statsResult && (
<div className="mt-8 border-t border-gray-200 pt-6"> <div className="mt-4 border-t border-gray-200 pt-6">
<div className="flex justify-between items-center mb-4"> <div className="flex justify-between items-center mb-4">
<h4 className="text-lg font-bold text-gray-800">Ergebnis der Auswertung</h4> <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> <span className="text-xs bg-emerald-100 text-emerald-800 px-2 py-1 rounded font-medium">Erfolgreich abgeschlossen</span>
@@ -1280,7 +1549,9 @@ function App() {
</div> </div>
</div> </div>
)} )}
</div>
)}
</div>
</div> </div>
{/* Modal Footer */} {/* Modal Footer */}