[33e88f42] Keine Zusammenfassung angegeben.
Keine Zusammenfassung angegeben.
This commit is contained in:
@@ -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)}
|
||||
|
||||
Reference in New Issue
Block a user