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 */} +
+
+ + {releaseStats && ( + Verfügbar: {releaseStats.available} (Verwendet: {releaseStats.used}) + )} +
+