[33e88f42] Keine Zusammenfassung angegeben.

Keine Zusammenfassung angegeben.
This commit is contained in:
2026-04-10 21:51:12 +00:00
parent c2f614d7ad
commit 5e0186c534
8 changed files with 460 additions and 22 deletions

View File

@@ -42,8 +42,64 @@ function App() {
const [reminderProgress, setReminderProgress] = useState<string>('');
const [isReminderRunning, setIsReminderRunning] = useState(false);
const [latestFile, setLatestFile] = useState<any>(null);
const [isGmailAuthenticated, setIsGmailAuthenticated] = useState(false);
// Email States
const [reminderResult, setReminderResult] = useState<any[] | null>(null);
const [emailSubject, setEmailSubject] = useState("Fotos von {Kindernamen}");
const [emailBody, setEmailBody] = useState("Hallo {Name Käufer},<br><br>deine Fotos sind fertig und warten auf dich! Klicke einfach auf die Links unten, um direkt zu den Galerien zu gelangen:<br><br>{LinksHTML}<br><br>Viel Spaß beim Anschauen!");
const [isSendingEmails, setIsSendingEmails] = useState(false);
const [emailSendStatus, setEmailSendStatus] = useState<string | null>(null);
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://192.168.178.6:8002';
// 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'
: (import.meta.env.VITE_API_BASE_URL || 'http://192.168.178.6:8002');
const checkGmailAuth = async () => {
try {
const response = await fetch(`${API_BASE_URL}/api/gmail/status`);
if (response.ok) {
const data = await response.json();
setIsGmailAuthenticated(data.authenticated);
}
} catch (err) {
console.error("Failed to check Gmail auth status");
}
};
const handleGmailLogin = async () => {
try {
const response = await fetch(`${API_BASE_URL}/api/auth/google`);
if (response.ok) {
const data = await response.json();
window.location.href = data.url;
}
} catch (err) {
setError("Konnte Gmail-Anmeldung nicht starten.");
}
};
const fetchLatestFile = async () => {
try {
@@ -83,6 +139,7 @@ function App() {
fetchJobs(activeTab, false);
}
fetchLatestFile();
checkGmailAuth();
}, [activeTab]);
const handleRefresh = () => fetchJobs(activeTab, true);
@@ -157,6 +214,18 @@ function App() {
};
}, [statsTaskId, isStatsRunning]);
const handleFetchReminderResult = async (taskId: string) => {
try {
const res = await fetch(`${API_BASE_URL}/api/tasks/${taskId}`);
if (res.ok) {
const data = await res.json();
setReminderResult(data.result);
}
} catch (err) {
console.error("Failed to fetch reminder results");
}
};
useEffect(() => {
let interval: any;
if (reminderTaskId && isReminderRunning) {
@@ -168,8 +237,7 @@ function App() {
setReminderProgress(data.progress || 'Verarbeite...');
if (data.status === 'completed') {
setIsReminderRunning(false);
// Auto-trigger download or show button? The user wants a CSV.
// Let's keep the task ID so we can show a download button.
handleFetchReminderResult(reminderTaskId);
} else if (data.status === 'error') {
setError(data.progress || 'Ein Fehler ist aufgetreten.');
setIsReminderRunning(false);
@@ -296,6 +364,78 @@ function App() {
setError("Download fehlgeschlagen.");
}
};
const handleSendEmails = async () => {
if (!reminderResult || !isGmailAuthenticated) return;
setIsSendingEmails(true);
setEmailSendStatus("Sende...");
const emailsToSend = reminderResult.map(row => {
let subject = emailSubject.replace(/{Kindernamen}/g, row["Kindernamen"]);
let body = emailBody
.replace(/{Name Käufer}/g, row["Name Käufer"])
.replace(/{Kindernamen}/g, row["Kindernamen"])
.replace(/{LinksHTML}/g, row["LinksHTML"])
.replace(/\n/g, "<br>");
return {
to: row["E-Mail-Adresse Käufer"],
subject: subject,
body: body + SIGNATURE_HTML
};
});
try {
const response = await fetch(`${API_BASE_URL}/api/gmail/send-bulk`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ emails: emailsToSend })
});
if (response.ok) {
const data = await response.json();
setEmailSendStatus(`✅ Fertig! ${data.success} gesendet.`);
if (data.failed.length > 0) {
setEmailSendStatus(prev => `${prev} (${data.failed.length} Fehler)`);
}
} else {
throw new Error("Sende-Fehler");
}
} catch (err) {
setEmailSendStatus("❌ Fehler beim Senden");
} finally {
setIsSendingEmails(false);
}
};
const handleSendTestEmail = async () => {
if (!isGmailAuthenticated) return;
setIsSendingEmails(true);
try {
const response = await fetch(`${API_BASE_URL}/api/gmail/send-bulk`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
emails: [{
to: "floke.com@gmail.com",
subject: "Test-E-Mail vom Fotograf Tool (inkl. Signatur)",
body: "Hallo! Das ist eine Test-E-Mail, um die Gmail-API-Integration zu verifizieren. Wenn du das liest, funktioniert alles perfekt! 🚀" + SIGNATURE_HTML
}]
})
});
if (response.ok) {
alert("Test-E-Mail erfolgreich an floke.com@gmail.com gesendet!");
} else {
alert("Fehler beim Senden der Test-E-Mail.");
}
} catch (err) {
alert("Netzwerkfehler beim Senden der Test-E-Mail.");
} finally {
setIsSendingEmails(false);
}
};
const currentJobs = jobsCache[activeTab];
return (
@@ -317,6 +457,28 @@ function App() {
Zum Dashboard
</a>
<button
onClick={handleGmailLogin}
className={`flex text-xs font-medium transition-colors items-center gap-1 px-2 py-1 rounded border ${
isGmailAuthenticated
? 'bg-emerald-50 text-emerald-600 border-emerald-200'
: 'bg-amber-50 text-amber-600 border-amber-200 hover:bg-amber-100'
}`}
>
<span className="text-lg">{isGmailAuthenticated ? '✅' : '✉️'}</span>
{isGmailAuthenticated ? 'Gmail verbunden' : 'Gmail verbinden'}
</button>
{isGmailAuthenticated && (
<button
onClick={handleSendTestEmail}
disabled={isSendingEmails}
className="text-[10px] bg-white border border-gray-200 text-gray-400 hover:text-indigo-600 hover:border-indigo-200 px-2 py-1 rounded transition-all"
>
{isSendingEmails ? 'Sende...' : 'Test-Mail senden'}
</button>
)}
{latestFile && (
<div className="hidden lg:flex items-center gap-2">
<span className="text-xs text-gray-400">Letzte Datei:</span>
@@ -623,13 +785,70 @@ function App() {
<p className="text-xs break-words">{reminderProgress}</p>
</div>
) : reminderTaskId ? (
<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"
>
<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 herunterladen
</button>
<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"
>
<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>
{reminderResult && isGmailAuthenticated && (
<div className="mt-4 border-t border-gray-100 pt-4 bg-gray-50 p-4 rounded-xl space-y-4">
<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>
<div className="space-y-2">
<label className="text-[10px] font-bold text-gray-400 uppercase">Betreff</label>
<input
value={emailSubject}
onChange={(e) => setEmailSubject(e.target.value)}
className="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg focus:ring-2 focus:ring-indigo-500"
/>
</div>
<div className="space-y-2">
<label className="text-[10px] font-bold text-gray-400 uppercase">Nachricht (HTML erlaubt)</label>
<textarea
value={emailBody}
onChange={(e) => setEmailBody(e.target.value)}
rows={4}
className="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg focus:ring-2 focus:ring-indigo-500 font-mono"
/>
<div className="flex justify-between items-center text-[10px] text-gray-400">
<span>Platzhalter: {"{Name Käufer}"}, {"{Kindernamen}"}, {"{LinksHTML}"}</span>
<span className="flex items-center gap-1">
<svg className="w-3 h-3 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
Signatur "Kinderfotos Erding" wird automatisch angehängt
</span>
</div>
</div>
<button
onClick={handleSendEmails}
disabled={isSendingEmails}
className="w-full py-2.5 bg-indigo-600 text-white text-sm font-bold rounded-lg hover:bg-indigo-700 disabled:opacity-50 transition-all shadow-md flex items-center justify-center gap-2"
>
{isSendingEmails ? (
<>
<svg className="animate-spin h-4 w-4" viewBox="0 0 24 24" fill="none"><circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" /><path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" /></svg>
Sende Mails...
</>
) : (
<>Mails jetzt versenden</>
)}
</button>
{emailSendStatus && (
<p className="text-center text-xs font-bold text-indigo-600">{emailSendStatus}</p>
)}
</div>
)}
</div>
) : (
<button
onClick={() => handleStartReminderAnalysis(selectedJob)}

View File

@@ -4,4 +4,5 @@ import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
base: '/fotograf-de/', // Ensures assets are loaded with the correct prefix behind NGINX
})