diff --git a/fotograf-de-scraper/backend/database.py b/fotograf-de-scraper/backend/database.py
index 64d6d88f7..2d8264725 100644
--- a/fotograf-de-scraper/backend/database.py
+++ b/fotograf-de-scraper/backend/database.py
@@ -28,6 +28,14 @@ class GmailToken(Base):
token_json = Column(String) # Stores the full credentials JSON
updated_at = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow)
+class DiscountCode(Base):
+ __tablename__ = "discount_codes"
+ id = Column(Integer, primary_key=True)
+ code = Column(String, unique=True, index=True)
+ is_used = Column(Integer, default=0) # 0 for false, 1 for true
+ assigned_to_email = Column(String, nullable=True)
+ used_at = Column(DateTime, nullable=True)
+
Base.metadata.create_all(bind=engine)
def get_db():
diff --git a/fotograf-de-scraper/backend/main.py b/fotograf-de-scraper/backend/main.py
index 1b171558e..79b91e4c3 100644
--- a/fotograf-de-scraper/backend/main.py
+++ b/fotograf-de-scraper/backend/main.py
@@ -87,7 +87,10 @@ load_dotenv()
# Ensure DB is created
Base.metadata.create_all(bind=engine)
+import publish_request_api
+
app = FastAPI(title="Fotograf.de Scraper & ERP API")
+app.include_router(publish_request_api.router)
# Configure CORS
app.add_middleware(
diff --git a/fotograf-de-scraper/backend/publish_request_api.py b/fotograf-de-scraper/backend/publish_request_api.py
new file mode 100644
index 000000000..956fb7eb9
--- /dev/null
+++ b/fotograf-de-scraper/backend/publish_request_api.py
@@ -0,0 +1,116 @@
+from fastapi import APIRouter, Depends, HTTPException, Request
+from pydantic import BaseModel
+from sqlalchemy.orm import Session
+from database import get_db, DiscountCode
+import datetime
+import logging
+from gmail_service import GmailService
+import re
+
+router = APIRouter(prefix="/api/publish-request", tags=["publish-request"])
+logger = logging.getLogger("publish-request")
+
+class CodesUpload(BaseModel):
+ codes: str # comma separated
+
+@router.get("/stats")
+def get_stats(db: Session = Depends(get_db)):
+ total = db.query(DiscountCode).count()
+ used = db.query(DiscountCode).filter(DiscountCode.is_used == 1).count()
+ available = total - used
+ return {"total": total, "used": used, "available": available}
+
+@router.post("/codes")
+def upload_codes(data: CodesUpload, db: Session = Depends(get_db)):
+ codes_list = [c.strip() for c in data.codes.split(",") if c.strip()]
+ added = 0
+ for code in set(codes_list):
+ existing = db.query(DiscountCode).filter(DiscountCode.code == code).first()
+ if not existing:
+ new_code = DiscountCode(code=code, is_used=0)
+ db.add(new_code)
+ added += 1
+ db.commit()
+ return {"status": "success", "added": added}
+
+class WebhookData(BaseModel):
+ email: str
+
+@router.post("/webhook")
+async def handle_webhook(request: Request, db: Session = Depends(get_db)):
+ # Try to parse JSON from Google Forms webhook
+ try:
+ data = await request.json()
+ except:
+ raise HTTPException(status_code=400, detail="Invalid JSON")
+
+ # We expect {"email": "..."} or similar from the Google Apps Script
+ email = data.get("email") or data.get("Email")
+ if not email:
+ logger.error(f"Webhook received without email: {data}")
+ return {"status": "error", "message": "Email not found in webhook payload"}
+
+ email = email.strip().lower()
+
+ # Check if this email already got a code
+ already_assigned = db.query(DiscountCode).filter(DiscountCode.assigned_to_email == email).first()
+ if already_assigned:
+ logger.info(f"Email {email} already received code {already_assigned.code}")
+ return {"status": "success", "message": "Already sent"}
+
+ # Get a free code
+ free_code = db.query(DiscountCode).filter(DiscountCode.is_used == 0).first()
+ if not free_code:
+ logger.error("NO FREE DISCOUNT CODES LEFT!")
+ # Fallback logic: Notify admin?
+ return {"status": "error", "message": "No codes available"}
+
+ # Mark as used
+ free_code.is_used = 1
+ free_code.assigned_to_email = email
+ free_code.used_at = datetime.datetime.utcnow()
+ db.commit()
+
+ # Send Thank You Email with GmailService
+ service = GmailService(db)
+ subject = "Dankeschön für Eure Freigabe & Euer Rabattcode"
+
+ # HTML Signature from frontend
+ SIGNATURE_HTML = """
+
+
+
Liebe Grüße, Euer Team von Kinderfotos Erding
+
Zierl Fotografen GmbH
+ Anton-Bruckner-Straße 5 85435 Erding
+
www.kinderfotos-erding.de
+
+ """
+
+ body_html = f"""
+ Hallo,
+ vielen Dank für Eure Unterstützung und das Ausfüllen der Freigabe!
+ Als kleines Dankeschön hier Euer 25 € Rabattcode für Eure Bestellung:
+ {free_code.code}
+ Bitte wartet mit Eurer Bestellung, falls Ihr noch nicht bestellt habt, bis Ihr diesen Code an der Kasse einlösen könnt.
+ {SIGNATURE_HTML}
+ """
+
+ try:
+ success = service.send_email(email, subject, body_html)
+ if success:
+ logger.info(f"Successfully sent code {free_code.code} to {email}")
+ return {"status": "success", "message": "Email sent"}
+ else:
+ logger.error(f"Failed to send email to {email}")
+ free_code.is_used = 0
+ free_code.assigned_to_email = None
+ free_code.used_at = None
+ db.commit()
+ return {"status": "error", "message": "Failed to send email"}
+ except Exception as e:
+ logger.exception("Error sending webhook email")
+ free_code.is_used = 0
+ free_code.assigned_to_email = None
+ free_code.used_at = None
+ db.commit()
+ return {"status": "error", "message": str(e)}
diff --git a/fotograf-de-scraper/frontend/src/App.tsx b/fotograf-de-scraper/frontend/src/App.tsx
index a47884abb..51330bb7b 100644
--- a/fotograf-de-scraper/frontend/src/App.tsx
+++ b/fotograf-de-scraper/frontend/src/App.tsx
@@ -53,6 +53,106 @@ function App() {
const [isSendingEmails, setIsSendingEmails] = useState(false);
const [emailSendStatus, setEmailSendStatus] = useState(null);
+ // Release Request States
+ const [releaseEmails, setReleaseEmails] = useState("");
+ const [releaseCodes, setReleaseCodes] = useState("");
+ const [releaseStats, setReleaseStats] = useState(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}, 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. :) 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: Beispiel ansehen ) veröffentlichen. Um den rechtlichen Anforderungen (DSGVO) gerecht zu werden, müsstet Ihr noch dieses Formular auf unserer Website ausfüllen:Zum Formular zur Veröffentlichung 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. Vielen Dank für Eure Unterstützung und Euer Vertrauen! ` + 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 = `
@@ -876,6 +976,65 @@ function App() {
+ {/* Tool 4: Freigabe-Anfrage */}
+
+
+
Anfrage Veröffentlichung
+
Sende personalisierte DSGVO-Anfragen für ausgewählte Eltern inkl. Gutschein-Webhook.
+
+
+ {/* Upload Codes */}
+
+
+ Gutscheincodes hinzufügen
+ {releaseStats && (
+ Verfügbar: {releaseStats.available} (Verwendet: {releaseStats.used})
+ )}
+
+
+
+ {/* Send Requests */}
+
+
Ziel-E-Mails für Anfrage
+
+ 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.
+
+
+
+
+
{/* Tool 3: Follow-up Emails */}
diff --git a/google_forms_webhook.js b/google_forms_webhook.js
new file mode 100644
index 000000000..470968b9f
--- /dev/null
+++ b/google_forms_webhook.js
@@ -0,0 +1,53 @@
+// ANLEITUNG ZUR EINRICHTUNG IM GOOGLE FORMULAR
+// 1. Öffne dein Google Formular (Freigabe zur Veröffentlichung).
+// 2. Klicke oben rechts auf das Drei-Punkte-Menü und wähle "Skript-Editor".
+// 3. Kopiere diesen Code hinein und speichere (Strg+S).
+// 4. Ersetze die WEBHOOK_URL durch deine korrekte Domain.
+// 5. Klicke im Skript-Editor links auf die "Uhr" (Trigger).
+// 6. Füge einen neuen Trigger hinzu:
+// - Funktion: onSubmit
+// - Ereignisquelle: Aus Formular
+// - Ereignistyp: Beim Senden des Formulars
+// 7. Akzeptiere die Berechtigungen von Google.
+
+const WEBHOOK_URL = "https://floke-ai.duckdns.org/fotograf-de-api/api/publish-request/webhook";
+
+function onSubmit(e) {
+ try {
+ // Hole alle Antworten
+ var itemResponses = e.response.getItemResponses();
+ var email = e.response.getRespondentEmail(); // Geht nur, wenn "E-Mail-Adressen erfassen" aktiv ist!
+
+ // Fallback: Wenn E-Mail-Erfassung nicht global an ist, suche nach einem Feld namens "E-Mail"
+ if (!email) {
+ for (var i = 0; i < itemResponses.length; i++) {
+ var title = itemResponses[i].getItem().getTitle().toLowerCase();
+ if (title.indexOf("e-mail") !== -1 || title.indexOf("email") !== -1) {
+ email = itemResponses[i].getResponse();
+ break;
+ }
+ }
+ }
+
+ if (!email) {
+ Logger.log("Keine E-Mail-Adresse gefunden.");
+ return;
+ }
+
+ var payload = {
+ "email": email
+ };
+
+ var options = {
+ "method": "post",
+ "contentType": "application/json",
+ "payload": JSON.stringify(payload)
+ };
+
+ UrlFetchApp.fetch(WEBHOOK_URL, options);
+ Logger.log("Webhook erfolgreich an " + WEBHOOK_URL + " gesendet. Email: " + email);
+
+ } catch (err) {
+ Logger.log("Fehler: " + err.toString());
+ }
+}
\ No newline at end of file