[34588f42] Feat: Freigabe-Anfrage mit Gutschein-Webhook integriert

- Datenbank um 'DiscountCode' Modell erweitert.
- Neue Backend API-Routen für Upload von Gutscheincodes, Abfrage der Verfügbarkeit und Webhook-Listener (Google Forms) zur automatischen Dankes-E-Mail erstellt.
- Frontend (App.tsx) um ein neues Tool ('Anfrage Veröffentlichung') erweitert, das anhand der CSV-Daten Platzhalter (<Name>, <Kind>, <Kindergarten>) personalisiert und Mails via Gmail versendet.
- Google Forms Webhook Script (google_forms_webhook.js) als Kopiervorlage erstellt.
This commit is contained in:
2026-04-17 20:17:30 +00:00
parent 1a3568f69e
commit 929d92afeb
5 changed files with 339 additions and 0 deletions

View File

@@ -53,6 +53,106 @@ function App() {
const [isSendingEmails, setIsSendingEmails] = useState(false);
const [emailSendStatus, setEmailSendStatus] = useState<string | null>(null);
// Release Request States
const [releaseEmails, setReleaseEmails] = useState("");
const [releaseCodes, setReleaseCodes] = useState("");
const [releaseStats, setReleaseStats] = useState<any>(null);
const [isUploadingCodes, setIsUploadingCodes] = useState(false);
const [uploadMessage, setUploadMessage] = useState("");
const [isSendingRelease, setIsSendingRelease] = useState(false);
const [releaseMessage, setReleaseMessage] = useState("");
const fetchReleaseStats = async () => {
try {
const response = await fetch(`${API_BASE_URL}/api/publish-request/stats`);
if (response.ok) {
const data = await response.json();
setReleaseStats(data);
}
} catch (e) {
console.error("Failed to fetch release stats", e);
}
};
const handleUploadCodes = async () => {
setIsUploadingCodes(true);
setUploadMessage("Lädt hoch...");
try {
const response = await fetch(`${API_BASE_URL}/api/publish-request/codes`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ codes: releaseCodes })
});
const data = await response.json();
if (response.ok) {
setUploadMessage(`${data.added} neue Codes gespeichert.`);
setReleaseCodes("");
fetchReleaseStats();
} else {
setUploadMessage("❌ Fehler beim Hochladen.");
}
} catch (e) {
setUploadMessage("❌ Netzwerkfehler.");
}
setIsUploadingCodes(false);
};
const handleSendRelease = async () => {
if (!reminderResult || !isGmailAuthenticated) return;
setIsSendingRelease(true);
setReleaseMessage("Bereite Senden vor...");
const targetEmails = releaseEmails.split(',').map(e => e.trim().toLowerCase()).filter(e => e);
// We only send to emails that are in targetEmails and exist in reminderResult
const targetRows = reminderResult.filter(row => {
const rowEmail = row["E-Mail-Adresse Käufer"]?.trim().toLowerCase();
return targetEmails.includes(rowEmail);
});
if (targetRows.length === 0) {
setReleaseMessage("⚠️ Keine passenden E-Mails im Auftrag gefunden (bitte vorher Supermailer-Analyse starten).");
setIsSendingRelease(false);
return;
}
const emailsToSend = targetRows.map(row => {
// Split the buyer name to get first name
const fullName = row["Name Käufer"] || "";
const firstName = fullName.split(' ')[0] || "Liebe Eltern";
const kindergartenName = selectedJob ? selectedJob.name.replace(/\(JOB\d+\)\s*/, '') : "dem Kindergarten";
const childrenNames = row["Kindernamen"] || "Euren Kindern";
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 Kindergarten ${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 www.kinderfotos-erding.de (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.<br><br>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>` + SIGNATURE_HTML;
return {
to: row["E-Mail-Adresse Käufer"],
subject: subject,
body: body
};
});
setReleaseMessage(`Sende ${emailsToSend.length} Mails...`);
try {
const response = await fetch(`${API_BASE_URL}/api/gmail/send-bulk`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ emails: emailsToSend })
});
const data = await response.json();
if (response.ok) {
setReleaseMessage(`✅ Fertig! ${data.success} gesendet. ${data.failed.length > 0 ? '(' + data.failed.length + ' Fehler)' : ''}`);
} else {
setReleaseMessage("❌ Fehler beim Senden.");
}
} catch (e) {
setReleaseMessage("❌ Netzwerkfehler.");
}
setIsSendingRelease(false);
};
// Email Signature (Cleaned up from user input)
const SIGNATURE_HTML = `
<br><br>
@@ -876,6 +976,65 @@ function App() {
</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">
<div className="flex items-start justify-between mb-2">
<div className="p-2 bg-indigo-50 rounded-lg text-indigo-600 text-xl">🖼</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"
/>
<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>
{/* Send Requests */}
<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">Ziel-E-Mails für Anfrage</label>
<p className="text-[10px] text-gray-400 mb-2 leading-tight">
Die Daten (Namen) werden automatisch aus der Supermailer-Analyse gezogen. Bitte die Analyse oben ('CSV für Supermailer') vorher durchlaufen lassen, damit alle Käuferdaten bereitliegen.
</p>
<textarea
placeholder="z.B. max@muster.de, anna@test.de"
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"
/>
<button
onClick={handleSendRelease}
disabled={isSendingRelease || !releaseEmails.trim() || !reminderResult || !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"
>
{isSendingRelease ? 'Sende...' : 'Anfrage-E-Mails jetzt senden'}
</button>
{!reminderResult && <p className="text-center text-[10px] mt-2 text-red-500">Bitte erst Tool 3 Analyse starten.</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>}
</div>
</div>
</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">