[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:
@@ -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"}
|
||||
@@ -42,6 +42,13 @@ class ReleaseParticipant(Base):
|
||||
first_name = Column(String)
|
||||
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)
|
||||
|
||||
def get_db():
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, BackgroundTasks
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
from database import get_db, DiscountCode, ReleaseParticipant
|
||||
from database import get_db, DiscountCode, ReleaseParticipant, ReleaseHistory
|
||||
import datetime
|
||||
import logging
|
||||
from gmail_service import GmailService
|
||||
@@ -96,9 +96,18 @@ async def send_requests(data: SendReleaseRequest, background_tasks: BackgroundTa
|
||||
if data.scheduled_time:
|
||||
# Pass a way to get a new session to the background task
|
||||
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)
|
||||
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
|
||||
service = GmailService(db)
|
||||
success = 0
|
||||
@@ -111,6 +120,11 @@ async def send_requests(data: SendReleaseRequest, background_tasks: BackgroundTa
|
||||
|
||||
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")
|
||||
def get_stats(db: Session = Depends(get_db)):
|
||||
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
1
fotograf-de-scraper/frontend/dist/assets/index-CSrvX8PS.css
vendored
Normal file
1
fotograf-de-scraper/frontend/dist/assets/index-CSrvX8PS.css
vendored
Normal file
File diff suppressed because one or more lines are too long
47
fotograf-de-scraper/frontend/dist/assets/index-FME2rxM6.js
vendored
Normal file
47
fotograf-de-scraper/frontend/dist/assets/index-FME2rxM6.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
fotograf-de-scraper/frontend/dist/index.html
vendored
4
fotograf-de-scraper/frontend/dist/index.html
vendored
@@ -5,8 +5,8 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/fotograf-de/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Fotograf.de ERP</title>
|
||||
<script type="module" crossorigin src="/fotograf-de/assets/index-9o0T5Jx2.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/fotograf-de/assets/index-BaSYoDWO.css">
|
||||
<script type="module" crossorigin src="/fotograf-de/assets/index-FME2rxM6.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/fotograf-de/assets/index-CSrvX8PS.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import './App.css';
|
||||
|
||||
interface Job {
|
||||
@@ -64,6 +64,92 @@ function App() {
|
||||
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 {
|
||||
@@ -119,38 +205,13 @@ function App() {
|
||||
setIsSendingRelease(true);
|
||||
setReleaseMessage("Bereite Senden vor...");
|
||||
|
||||
const lines = releaseEmails.split('\n').filter(line => line.trim());
|
||||
|
||||
if (lines.length === 0) {
|
||||
setReleaseMessage("⚠️ Bitte Daten eingeben.");
|
||||
const emailsToSend = parsedReleaseEmails;
|
||||
if (emailsToSend.length === 0) {
|
||||
setReleaseMessage("⚠️ Bitte Empfänger eintragen.");
|
||||
setIsSendingRelease(false);
|
||||
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...`);
|
||||
|
||||
try {
|
||||
@@ -166,6 +227,7 @@ function App() {
|
||||
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.");
|
||||
}
|
||||
@@ -175,27 +237,6 @@ function App() {
|
||||
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
|
||||
const API_BASE_URL = window.location.hostname.includes('duckdns.org')
|
||||
? '/fotograf-de-api'
|
||||
@@ -266,6 +307,7 @@ function App() {
|
||||
checkGmailAuth();
|
||||
fetchReleaseStats();
|
||||
fetchReleaseResponses();
|
||||
fetchReleaseHistoryData();
|
||||
}, [activeTab]);
|
||||
|
||||
const handleRefresh = () => fetchJobs(activeTab, true);
|
||||
@@ -865,35 +907,38 @@ function App() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<h4 className="text-lg font-bold text-gray-800 mb-4">Verfügbare Tools</h4>
|
||||
|
||||
<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>
|
||||
{/* Main Modal Tabs Navigation */}
|
||||
<div className="flex border-b border-gray-200 mb-6 gap-2">
|
||||
<button
|
||||
onClick={() => handleGeneratePdf(selectedJob)}
|
||||
disabled={processingJobId === selectedJob.id || isStatsRunning}
|
||||
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"
|
||||
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'}`}
|
||||
>
|
||||
{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>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Tool 2: Shooting-Planung (QR & List) */}
|
||||
<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</h5>
|
||||
<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">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 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>
|
||||
))}
|
||||
</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>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="md:col-span-2 grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{/* Action 1: QR Cards */}
|
||||
<div className="bg-gray-50 p-4 rounded-lg flex flex-col justify-between">
|
||||
<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-gray-800 mb-1">📇 QR-Zugangskarten</h6>
|
||||
<p className="text-xs text-gray-600 mb-3">Druckt Namen und Uhrzeit auf vorbereitete PDF-Bögen.</p>
|
||||
<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"
|
||||
@@ -941,30 +1002,30 @@ function App() {
|
||||
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...' : 'Blanko PDF hochladen & starten'}
|
||||
{isQrGenerating ? 'Generiere...' : 'Upload & Start'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
<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>
|
||||
<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...' : 'PDF Liste generieren'}
|
||||
{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">
|
||||
<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">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 className="grid grid-cols-2 gap-3 mt-auto">
|
||||
<button
|
||||
@@ -1002,41 +1063,50 @@ function App() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tool 4: Freigabe-Anfrage */}
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-5 hover:border-amber-300 transition-colors shadow-sm mt-4">
|
||||
{/* --- 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>
|
||||
<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>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Upload 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-16"
|
||||
/>
|
||||
{/* Tabs Navigation */}
|
||||
<div className="flex border-b border-gray-200 mb-4">
|
||||
<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"
|
||||
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'}`}
|
||||
>
|
||||
{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>
|
||||
{uploadMessage && <p className="text-center text-xs mt-2 font-bold text-gray-600">{uploadMessage}</p>}
|
||||
</div>
|
||||
|
||||
{/* Send Requests */}
|
||||
<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">
|
||||
@@ -1047,8 +1117,56 @@ function App() {
|
||||
placeholder="max@muster.de, Max, Moritz und Leni 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-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 →
|
||||
</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"
|
||||
>←</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"
|
||||
>→</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>
|
||||
@@ -1060,22 +1178,52 @@ function App() {
|
||||
/>
|
||||
<span className="text-[10px] text-gray-400 italic">Leer = sofort</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleSendRelease}
|
||||
disabled={isSendingRelease || !releaseEmails.trim() || !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"
|
||||
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...' : 'Anfrage-E-Mails jetzt senden'}
|
||||
{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</h6>
|
||||
<h6 className="text-[10px] font-bold text-gray-500 uppercase">Eingegangene Antworten (Codes)</h6>
|
||||
<button
|
||||
onClick={fetchReleaseResponses}
|
||||
disabled={isFetchingResponses}
|
||||
@@ -1088,7 +1236,7 @@ function App() {
|
||||
{!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-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">
|
||||
<thead className="bg-gray-50 text-gray-400 sticky top-0">
|
||||
<tr>
|
||||
@@ -1110,45 +1258,124 @@ function App() {
|
||||
</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>
|
||||
|
||||
{/* Tool 3: Follow-up Emails */}
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-5 hover:border-amber-300 transition-colors shadow-sm">
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
{!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>
|
||||
<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>
|
||||
<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 ? (
|
||||
<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">
|
||||
<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>
|
||||
<p className="text-xs break-words">{reminderProgress}</p>
|
||||
<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-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>
|
||||
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>
|
||||
|
||||
{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="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-gray-500">
|
||||
{reminderResult.length} Empfänger identifiziert.
|
||||
<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
|
||||
@@ -1169,11 +1396,56 @@ function App() {
|
||||
<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>
|
||||
<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 →
|
||||
</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"
|
||||
>←</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"
|
||||
>→</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}
|
||||
@@ -1186,58 +1458,55 @@ function App() {
|
||||
Sende Mails...
|
||||
</>
|
||||
) : (
|
||||
<>Mails jetzt versenden</>
|
||||
<>{parsedReminderEmails.length} Erinnerungs-Mails jetzt versenden</>
|
||||
)}
|
||||
</button>
|
||||
{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>
|
||||
) : (
|
||||
<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>
|
||||
{/* 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>
|
||||
<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 ? (
|
||||
<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">
|
||||
<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>
|
||||
<p className="text-xs break-words">{statsProgress}</p>
|
||||
<p className="text-xs text-gray-500 break-words">{statsProgress}</p>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => handleStartStatistics(selectedJob)}
|
||||
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
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Optional Data View Area (for later use, e.g., showing the stats table here) */}
|
||||
{/* Data View Area */}
|
||||
{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">
|
||||
<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>
|
||||
@@ -1280,7 +1549,9 @@ function App() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modal Footer */}
|
||||
|
||||
Reference in New Issue
Block a user