Compare commits
2 Commits
9b4f80a44f
...
ba06e6d033
| Author | SHA1 | Date | |
|---|---|---|---|
| ba06e6d033 | |||
| 3f6b27a89f |
@@ -36,6 +36,12 @@ class DiscountCode(Base):
|
|||||||
assigned_to_email = Column(String, nullable=True)
|
assigned_to_email = Column(String, nullable=True)
|
||||||
used_at = Column(DateTime, nullable=True)
|
used_at = Column(DateTime, nullable=True)
|
||||||
|
|
||||||
|
class ReleaseParticipant(Base):
|
||||||
|
__tablename__ = "release_participants"
|
||||||
|
email = Column(String, primary_key=True)
|
||||||
|
first_name = Column(String)
|
||||||
|
last_updated = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow)
|
||||||
|
|
||||||
Base.metadata.create_all(bind=engine)
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
def get_db():
|
def get_db():
|
||||||
|
|||||||
@@ -1,26 +1,38 @@
|
|||||||
from fastapi import APIRouter, Depends, HTTPException, Request, BackgroundTasks
|
from database import get_db, DiscountCode, ReleaseParticipant
|
||||||
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"])
|
router = APIRouter(prefix="/api/publish-request", tags=["publish-request"])
|
||||||
logger = logging.getLogger("publish-request")
|
logger = logging.getLogger("publish-request")
|
||||||
|
|
||||||
|
# Official Project Signature
|
||||||
|
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>
|
||||||
|
"""
|
||||||
|
|
||||||
class CodesUpload(BaseModel):
|
class CodesUpload(BaseModel):
|
||||||
codes: str # comma separated
|
codes: str # comma separated
|
||||||
|
|
||||||
class SendReleaseRequest(BaseModel):
|
class SendReleaseRequest(BaseModel):
|
||||||
emails: List[Dict[str, str]]
|
emails: List[Dict[str, str]]
|
||||||
scheduled_time: Optional[str] = None # e.g. "10:00"
|
scheduled_time: Optional[str] = None # e.g. "10:00"
|
||||||
|
participants: Optional[List[Dict[str, str]]] = None # [{email, first_name}]
|
||||||
|
|
||||||
async def delayed_send(emails: List[Dict[str, str]], scheduled_time: str, db: Session):
|
async def delayed_send(emails: List[Dict[str, str]], scheduled_time: str, db_session_factory):
|
||||||
try:
|
try:
|
||||||
# Calculate delay
|
# Calculate delay
|
||||||
now = datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=2))) # Berlin Time Approx
|
now = datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=2))) # Berlin Time Approx
|
||||||
@@ -35,6 +47,9 @@ async def delayed_send(emails: List[Dict[str, str]], scheduled_time: str, db: Se
|
|||||||
|
|
||||||
await asyncio.sleep(delay_seconds)
|
await asyncio.sleep(delay_seconds)
|
||||||
|
|
||||||
|
# We need a fresh DB session for the background task
|
||||||
|
db = db_session_factory()
|
||||||
|
try:
|
||||||
service = GmailService(db)
|
service = GmailService(db)
|
||||||
success_count = 0
|
success_count = 0
|
||||||
for email_data in emails:
|
for email_data in emails:
|
||||||
@@ -43,13 +58,30 @@ async def delayed_send(emails: List[Dict[str, str]], scheduled_time: str, db: Se
|
|||||||
await asyncio.sleep(1) # Rate limiting
|
await asyncio.sleep(1) # Rate limiting
|
||||||
|
|
||||||
logger.info(f"Scheduled send complete: {success_count}/{len(emails)} success.")
|
logger.info(f"Scheduled send complete: {success_count}/{len(emails)} success.")
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception("Error in delayed_send background task")
|
logger.exception("Error in delayed_send background task")
|
||||||
|
|
||||||
@router.post("/send")
|
@router.post("/send")
|
||||||
async def send_requests(data: SendReleaseRequest, background_tasks: BackgroundTasks, db: Session = Depends(get_db)):
|
async def send_requests(data: SendReleaseRequest, background_tasks: BackgroundTasks, db: Session = Depends(get_db)):
|
||||||
|
# Store participant names for later (webhook)
|
||||||
|
if data.participants:
|
||||||
|
for p in data.participants:
|
||||||
|
email = p.get("email", "").strip().lower()
|
||||||
|
first_name = p.get("first_name", "").strip()
|
||||||
|
if email and first_name:
|
||||||
|
existing = db.query(ReleaseParticipant).filter(ReleaseParticipant.email == email).first()
|
||||||
|
if existing:
|
||||||
|
existing.first_name = first_name
|
||||||
|
else:
|
||||||
|
db.add(ReleaseParticipant(email=email, first_name=first_name))
|
||||||
|
db.commit()
|
||||||
|
|
||||||
if data.scheduled_time:
|
if data.scheduled_time:
|
||||||
background_tasks.add_task(delayed_send, data.emails, data.scheduled_time, db)
|
# Pass a way to get a new session to the background task
|
||||||
|
from database import SessionLocal
|
||||||
|
background_tasks.add_task(delayed_send, data.emails, data.scheduled_time, SessionLocal)
|
||||||
return {"status": "scheduled", "message": f"Versand für {data.scheduled_time} geplant."}
|
return {"status": "scheduled", "message": f"Versand für {data.scheduled_time} geplant."}
|
||||||
|
|
||||||
# Immediate send
|
# Immediate send
|
||||||
@@ -113,9 +145,12 @@ async def handle_webhook(request: Request, db: Session = Depends(get_db)):
|
|||||||
free_code = db.query(DiscountCode).filter(DiscountCode.is_used == 0).first()
|
free_code = db.query(DiscountCode).filter(DiscountCode.is_used == 0).first()
|
||||||
if not free_code:
|
if not free_code:
|
||||||
logger.error("NO FREE DISCOUNT CODES LEFT!")
|
logger.error("NO FREE DISCOUNT CODES LEFT!")
|
||||||
# Fallback logic: Notify admin?
|
|
||||||
return {"status": "error", "message": "No codes available"}
|
return {"status": "error", "message": "No codes available"}
|
||||||
|
|
||||||
|
# Look up participant name
|
||||||
|
participant = db.query(ReleaseParticipant).filter(ReleaseParticipant.email == email).first()
|
||||||
|
first_name = participant.first_name if participant else "Ihr Lieben"
|
||||||
|
|
||||||
# Mark as used
|
# Mark as used
|
||||||
free_code.is_used = 1
|
free_code.is_used = 1
|
||||||
free_code.assigned_to_email = email
|
free_code.assigned_to_email = email
|
||||||
@@ -126,23 +161,16 @@ async def handle_webhook(request: Request, db: Session = Depends(get_db)):
|
|||||||
service = GmailService(db)
|
service = GmailService(db)
|
||||||
subject = "Dankeschön für Eure Freigabe & Euer Rabattcode"
|
subject = "Dankeschön für Eure Freigabe & Euer Rabattcode"
|
||||||
|
|
||||||
# HTML Signature from frontend
|
# Image provided by user
|
||||||
SIGNATURE_HTML = """
|
INSTRUCTIONS_IMAGE_URL = "https://mail.google.com/mail/u/2?ui=2&ik=719adaa3c5&attid=0.1&permmsgid=msg-a:r7482671925923393616&th=196e322c399dbc7f&view=fimg&fur=ip&permmsgid=msg-a:r7482671925923393616&sz=s0-l75-ft&attbid=ANGjdJ9_U6ayMFgwbupt4HalTKO867IHx6N70eNbPfQmTLNzRXilJxI-n8a1gjM8xVcP5HEOgaVxfp3FnJPzTYEEYhK4gSU-Il_0a6OtzFYscp55_W4iyxuxjyPvK4&disp=emb&realattid=ii_maspzxv50&zw"
|
||||||
<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"""
|
body_html = f"""
|
||||||
<p>Hallo,</p>
|
<p>Hallo {first_name},</p>
|
||||||
<p>vielen Dank für Eure Unterstützung und das Ausfüllen der Freigabe!</p>
|
<p>Vielen Dank nochmal für die Freigabe zur Veröffentlichung, das ist super nett von Euch!</p>
|
||||||
<p>Als kleines Dankeschön hier Euer 25 € Rabattcode für Eure Bestellung:</p>
|
<p>Hier ist euer Gutscheincode über 25 Euro: <strong style="font-size: 18px; color: #4F46E5;">{free_code.code}</strong></p>
|
||||||
<p><strong style="font-size: 18px; color: #4F46E5; padding: 10px; border: 1px dashed #4F46E5; display: inline-block;">{free_code.code}</strong></p>
|
<p>Um den Gutschein einzugeben, musst du auf den Preis des Warenkorbs drücken (über dem Button zur Kasse gehen):</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>
|
<p><img src="{INSTRUCTIONS_IMAGE_URL}" alt="Anleitung Gutschein einlösen" style="max-width: 100%; border: 1px solid #ddd; border-radius: 8px;"></p>
|
||||||
|
<p>Liebe Grüße,<br>das Team von Kinderfotos Erding</p>
|
||||||
{SIGNATURE_HTML}
|
{SIGNATURE_HTML}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@@ -99,40 +99,35 @@ function App() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSendRelease = async () => {
|
const handleSendRelease = async () => {
|
||||||
if (!reminderResult || !isGmailAuthenticated) return;
|
if (!isGmailAuthenticated) return;
|
||||||
setIsSendingRelease(true);
|
setIsSendingRelease(true);
|
||||||
setReleaseMessage("Bereite Senden vor...");
|
setReleaseMessage("Bereite Senden vor...");
|
||||||
|
|
||||||
const targetEmails = releaseEmails.split(',').map(e => e.trim().toLowerCase()).filter(e => e);
|
const lines = releaseEmails.split('\n').filter(line => line.trim());
|
||||||
|
|
||||||
// We only send to emails that are in targetEmails and exist in reminderResult
|
if (lines.length === 0) {
|
||||||
const targetRows = reminderResult.filter(row => {
|
setReleaseMessage("⚠️ Bitte Daten eingeben.");
|
||||||
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);
|
setIsSendingRelease(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const emailsToSend = targetRows.map(row => {
|
const emailsToSend = lines.map(line => {
|
||||||
// Split the buyer name to get first name
|
const parts = line.split(',');
|
||||||
const fullName = row["Name Käufer"] || "";
|
const to = parts[0] ? parts[0].trim().toLowerCase() : "";
|
||||||
const firstName = fullName.split(' ')[0] || "Liebe Eltern";
|
const firstName = parts[1] ? parts[1].trim() : "Liebe Eltern";
|
||||||
|
const childrenNames = parts[2] ? parts[2].trim() : "Euren Kindern";
|
||||||
const kindergartenName = selectedJob ? selectedJob.name.replace(/\(JOB\d+\)\s*/, '') : "dem Kindergarten";
|
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 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;
|
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 {
|
return {
|
||||||
to: row["E-Mail-Adresse Käufer"],
|
to: to,
|
||||||
subject: subject,
|
subject: subject,
|
||||||
body: body
|
body: body,
|
||||||
|
first_name: firstName // Extra field for participant mapping
|
||||||
};
|
};
|
||||||
});
|
}).filter(e => e.to);
|
||||||
|
|
||||||
setReleaseMessage(`Sende ${emailsToSend.length} Mails...`);
|
setReleaseMessage(`Sende ${emailsToSend.length} Mails...`);
|
||||||
|
|
||||||
@@ -141,8 +136,9 @@ function App() {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
emails: emailsToSend,
|
emails: emailsToSend.map(e => ({ to: e.to, subject: e.subject, body: e.body })),
|
||||||
scheduled_time: scheduledTime || null
|
scheduled_time: scheduledTime || null,
|
||||||
|
participants: emailsToSend.map(e => ({ email: e.to, first_name: e.first_name }))
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
@@ -1018,15 +1014,16 @@ function App() {
|
|||||||
|
|
||||||
{/* Send Requests */}
|
{/* Send Requests */}
|
||||||
<div className="bg-gray-50 p-3 rounded-lg border border-gray-100">
|
<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>
|
<label className="text-[10px] font-bold text-gray-500 uppercase block mb-2">Empfänger-Daten (Pro Zeile)</label>
|
||||||
<p className="text-[10px] text-gray-400 mb-2 leading-tight">
|
<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.
|
Format: <b>E-Mail, Vorname Elternteil, Namen der Kinder</b><br/>
|
||||||
|
(z.B. <i>max@muster.de, Max, Moritz und Leni</i>)
|
||||||
</p>
|
</p>
|
||||||
<textarea
|
<textarea
|
||||||
placeholder="z.B. max@muster.de, anna@test.de"
|
placeholder="max@muster.de, Max, Moritz und Leni anna@test.de, Anna, Lisa"
|
||||||
value={releaseEmails}
|
value={releaseEmails}
|
||||||
onChange={(e) => setReleaseEmails(e.target.value)}
|
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"
|
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 font-mono"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 mb-3 bg-white p-2 rounded-lg border border-gray-100">
|
<div className="flex items-center gap-2 mb-3 bg-white p-2 rounded-lg border border-gray-100">
|
||||||
@@ -1041,12 +1038,12 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={handleSendRelease}
|
onClick={handleSendRelease}
|
||||||
disabled={isSendingRelease || !releaseEmails.trim() || !reminderResult || !isGmailAuthenticated}
|
disabled={isSendingRelease || !releaseEmails.trim() || !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"
|
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'}
|
{isSendingRelease ? 'Sende...' : 'Anfrage-E-Mails jetzt senden'}
|
||||||
</button>
|
</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>}
|
{!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>}
|
{releaseMessage && <p className="text-center text-xs mt-2 font-bold text-indigo-600">{releaseMessage}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user