[34588f42] Feat: Versandzeit-Steuerung für Freigabe-Anfragen hinzugefügt
- Backend unterstützt nun zeitgesteuerten Versand (scheduled_time) via BackgroundTasks. - Frontend um ein Zeitauswahl-Feld erweitert.
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
from fastapi import APIRouter, Depends, HTTPException, Request, BackgroundTasks
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from database import get_db, DiscountCode
|
from database import get_db, DiscountCode
|
||||||
@@ -6,6 +6,9 @@ import datetime
|
|||||||
import logging
|
import logging
|
||||||
from gmail_service import GmailService
|
from gmail_service import GmailService
|
||||||
import re
|
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")
|
||||||
@@ -13,6 +16,54 @@ logger = logging.getLogger("publish-request")
|
|||||||
class CodesUpload(BaseModel):
|
class CodesUpload(BaseModel):
|
||||||
codes: str # comma separated
|
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")
|
@router.get("/stats")
|
||||||
def get_stats(db: Session = Depends(get_db)):
|
def get_stats(db: Session = Depends(get_db)):
|
||||||
total = db.query(DiscountCode).count()
|
total = db.query(DiscountCode).count()
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ function App() {
|
|||||||
const [uploadMessage, setUploadMessage] = useState("");
|
const [uploadMessage, setUploadMessage] = useState("");
|
||||||
const [isSendingRelease, setIsSendingRelease] = useState(false);
|
const [isSendingRelease, setIsSendingRelease] = useState(false);
|
||||||
const [releaseMessage, setReleaseMessage] = useState("");
|
const [releaseMessage, setReleaseMessage] = useState("");
|
||||||
|
const [scheduledTime, setScheduledTime] = useState(""); // New state
|
||||||
|
|
||||||
const fetchReleaseStats = async () => {
|
const fetchReleaseStats = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -136,10 +137,13 @@ function App() {
|
|||||||
setReleaseMessage(`Sende ${emailsToSend.length} Mails...`);
|
setReleaseMessage(`Sende ${emailsToSend.length} Mails...`);
|
||||||
|
|
||||||
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
|
||||||
|
})
|
||||||
});
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
@@ -553,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) {
|
||||||
@@ -1021,6 +1028,17 @@ function App() {
|
|||||||
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"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<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
|
<button
|
||||||
onClick={handleSendRelease}
|
onClick={handleSendRelease}
|
||||||
disabled={isSendingRelease || !releaseEmails.trim() || !reminderResult || !isGmailAuthenticated}
|
disabled={isSendingRelease || !releaseEmails.trim() || !reminderResult || !isGmailAuthenticated}
|
||||||
|
|||||||
Reference in New Issue
Block a user