Compare commits
3 Commits
1a3568f69e
...
9b4f80a44f
| Author | SHA1 | Date | |
|---|---|---|---|
| 9b4f80a44f | |||
| 1f5805e64c | |||
| 929d92afeb |
@@ -28,6 +28,14 @@ class GmailToken(Base):
|
|||||||
token_json = Column(String) # Stores the full credentials JSON
|
token_json = Column(String) # Stores the full credentials JSON
|
||||||
updated_at = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow)
|
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)
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
def get_db():
|
def get_db():
|
||||||
|
|||||||
@@ -110,6 +110,13 @@ class GmailService:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# DEV MODE OVERRIDE
|
||||||
|
dev_email = os.getenv("DEV_MODE_EMAIL_RECIPIENT")
|
||||||
|
original_to = to
|
||||||
|
if dev_email:
|
||||||
|
logger.warning(f"⚠️ DEV MODE ACTIVE: Redirecting email originally intended for {original_to} to {dev_email}")
|
||||||
|
to = dev_email
|
||||||
|
|
||||||
service = build('gmail', 'v1', credentials=creds)
|
service = build('gmail', 'v1', credentials=creds)
|
||||||
message = MIMEText(body_html, 'html')
|
message = MIMEText(body_html, 'html')
|
||||||
message['to'] = to
|
message['to'] = to
|
||||||
@@ -122,7 +129,10 @@ class GmailService:
|
|||||||
body={'raw': raw_message}
|
body={'raw': raw_message}
|
||||||
).execute()
|
).execute()
|
||||||
|
|
||||||
logger.info(f"Email sent to {to}. Message ID: {send_result['id']}")
|
if dev_email:
|
||||||
|
logger.info(f"Test-Email sent to {to} (Original target: {original_to}). Message ID: {send_result['id']}")
|
||||||
|
else:
|
||||||
|
logger.info(f"Email sent to {to}. Message ID: {send_result['id']}")
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to send email to {to}: {e}")
|
logger.error(f"Failed to send email to {to}: {e}")
|
||||||
|
|||||||
@@ -87,7 +87,10 @@ load_dotenv()
|
|||||||
# Ensure DB is created
|
# Ensure DB is created
|
||||||
Base.metadata.create_all(bind=engine)
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
|
import publish_request_api
|
||||||
|
|
||||||
app = FastAPI(title="Fotograf.de Scraper & ERP API")
|
app = FastAPI(title="Fotograf.de Scraper & ERP API")
|
||||||
|
app.include_router(publish_request_api.router)
|
||||||
|
|
||||||
# Configure CORS
|
# Configure CORS
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
|
|||||||
167
fotograf-de-scraper/backend/publish_request_api.py
Normal file
167
fotograf-de-scraper/backend/publish_request_api.py
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
from fastapi import APIRouter, Depends, HTTPException, Request, BackgroundTasks
|
||||||
|
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
|
||||||
|
import time
|
||||||
|
import asyncio
|
||||||
|
from typing import List, Dict, Optional
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/publish-request", tags=["publish-request"])
|
||||||
|
logger = logging.getLogger("publish-request")
|
||||||
|
|
||||||
|
class CodesUpload(BaseModel):
|
||||||
|
codes: str # comma separated
|
||||||
|
|
||||||
|
class SendReleaseRequest(BaseModel):
|
||||||
|
emails: List[Dict[str, str]]
|
||||||
|
scheduled_time: Optional[str] = None # e.g. "10:00"
|
||||||
|
|
||||||
|
async def delayed_send(emails: List[Dict[str, str]], scheduled_time: str, db: Session):
|
||||||
|
try:
|
||||||
|
# Calculate delay
|
||||||
|
now = datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=2))) # Berlin Time Approx
|
||||||
|
target_h, target_m = map(int, scheduled_time.split(":"))
|
||||||
|
target_time = now.replace(hour=target_h, minute=target_m, second=0, microsecond=0)
|
||||||
|
|
||||||
|
if target_time < now:
|
||||||
|
target_time += datetime.timedelta(days=1)
|
||||||
|
|
||||||
|
delay_seconds = (target_time - now).total_seconds()
|
||||||
|
logger.info(f"Scheduling {len(emails)} emails for {scheduled_time} (in {delay_seconds} seconds)")
|
||||||
|
|
||||||
|
await asyncio.sleep(delay_seconds)
|
||||||
|
|
||||||
|
service = GmailService(db)
|
||||||
|
success_count = 0
|
||||||
|
for email_data in emails:
|
||||||
|
if service.send_email(email_data["to"], email_data["subject"], email_data["body"]):
|
||||||
|
success_count += 1
|
||||||
|
await asyncio.sleep(1) # Rate limiting
|
||||||
|
|
||||||
|
logger.info(f"Scheduled send complete: {success_count}/{len(emails)} success.")
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("Error in delayed_send background task")
|
||||||
|
|
||||||
|
@router.post("/send")
|
||||||
|
async def send_requests(data: SendReleaseRequest, background_tasks: BackgroundTasks, db: Session = Depends(get_db)):
|
||||||
|
if data.scheduled_time:
|
||||||
|
background_tasks.add_task(delayed_send, data.emails, data.scheduled_time, db)
|
||||||
|
return {"status": "scheduled", "message": f"Versand für {data.scheduled_time} geplant."}
|
||||||
|
|
||||||
|
# Immediate send
|
||||||
|
service = GmailService(db)
|
||||||
|
success = 0
|
||||||
|
failed = []
|
||||||
|
for email_data in data.emails:
|
||||||
|
if service.send_email(email_data["to"], email_data["subject"], email_data["body"]):
|
||||||
|
success += 1
|
||||||
|
else:
|
||||||
|
failed.append(email_data["to"])
|
||||||
|
|
||||||
|
return {"status": "success", "success": success, "failed": failed}
|
||||||
|
|
||||||
|
@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 = """
|
||||||
|
<br><br><br>
|
||||||
|
<div style="font-family: 'Verdana', sans-serif; font-size: 11px; color: #3D3C3F;">
|
||||||
|
<p><strong>Liebe Grüße,</strong><br>Euer Team von Kinderfotos Erding</p>
|
||||||
|
<p><strong>Zierl Fotografen GmbH</strong><br>
|
||||||
|
Anton-Bruckner-Straße 5<br>85435 Erding</p>
|
||||||
|
<p><a href="https://www.kinderfotos-erding.de" style="color: #3D3C3F; text-decoration: none;">www.kinderfotos-erding.de</a></p>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
body_html = f"""
|
||||||
|
<p>Hallo,</p>
|
||||||
|
<p>vielen Dank für Eure Unterstützung und das Ausfüllen der Freigabe!</p>
|
||||||
|
<p>Als kleines Dankeschön hier Euer 25 € Rabattcode für Eure Bestellung:</p>
|
||||||
|
<p><strong style="font-size: 18px; color: #4F46E5; padding: 10px; border: 1px dashed #4F46E5; display: inline-block;">{free_code.code}</strong></p>
|
||||||
|
<p>Bitte wartet mit Eurer Bestellung, falls Ihr noch nicht bestellt habt, bis Ihr diesen Code an der Kasse einlösen könnt.</p>
|
||||||
|
{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)}
|
||||||
@@ -53,6 +53,110 @@ function App() {
|
|||||||
const [isSendingEmails, setIsSendingEmails] = useState(false);
|
const [isSendingEmails, setIsSendingEmails] = useState(false);
|
||||||
const [emailSendStatus, setEmailSendStatus] = useState<string | null>(null);
|
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 [scheduledTime, setScheduledTime] = useState(""); // New state
|
||||||
|
|
||||||
|
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/publish-request/send`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
emails: emailsToSend,
|
||||||
|
scheduled_time: scheduledTime || null
|
||||||
|
})
|
||||||
|
});
|
||||||
|
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)
|
// Email Signature (Cleaned up from user input)
|
||||||
const SIGNATURE_HTML = `
|
const SIGNATURE_HTML = `
|
||||||
<br><br>
|
<br><br>
|
||||||
@@ -453,10 +557,13 @@ function App() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE_URL}/api/gmail/send-bulk`, {
|
const response = await fetch(`${API_BASE_URL}/api/publish-request/send`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ emails: emailsToSend })
|
body: JSON.stringify({
|
||||||
|
emails: emailsToSend,
|
||||||
|
scheduled_time: scheduledTime || null
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
@@ -876,6 +983,76 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
</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"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 mb-3 bg-white p-2 rounded-lg border border-gray-100">
|
||||||
|
<span className="text-[10px] font-bold text-gray-400 uppercase">Versandzeit (Optional)</span>
|
||||||
|
<input
|
||||||
|
type="time"
|
||||||
|
value={scheduledTime}
|
||||||
|
onChange={(e) => setScheduledTime(e.target.value)}
|
||||||
|
className="text-xs border border-gray-200 rounded px-2 py-1 focus:ring-1 focus:ring-indigo-500"
|
||||||
|
/>
|
||||||
|
<span className="text-[10px] text-gray-400 italic">Leer = sofort</span>
|
||||||
|
</div>
|
||||||
|
<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 */}
|
{/* 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="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">
|
<div className="flex items-start justify-between mb-2">
|
||||||
|
|||||||
53
google_forms_webhook.js
Normal file
53
google_forms_webhook.js
Normal file
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user