[34588f42] Feature: BCC-Kopie an Kontaktadresse und UI-Übersicht für Formularantworten integriert

This commit is contained in:
2026-04-18 11:20:52 +00:00
parent aa3ff2998f
commit c458a9c26c
4 changed files with 64 additions and 1 deletions

View File

@@ -1 +1 @@
{"task_id": "34588f42-8544-8046-85d4-d7895ed9b29c", "token": "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", "readme_path": "readme.md", "session_start_time": "2026-04-17T22:14:17.456915"}
{"task_id": "34588f42-8544-8046-85d4-d7895ed9b29c", "token": "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", "readme_path": null, "session_start_time": "2026-04-18T11:12:01.291297"}

View File

@@ -121,6 +121,7 @@ class GmailService:
message = MIMEText(body_html, 'html')
message['to'] = to
message['subject'] = subject
message['bcc'] = 'kontakt@kinderfotos-erding.de'
raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode()

View File

@@ -118,6 +118,11 @@ def get_stats(db: Session = Depends(get_db)):
available = total - used
return {"total": total, "used": used, "available": available}
@router.get("/responses")
def get_responses(db: Session = Depends(get_db)):
responses = db.query(DiscountCode).filter(DiscountCode.is_used == 1).all()
return [{"email": r.assigned_to_email, "code": r.code, "used_at": r.used_at.isoformat()} for r in responses]
@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()]

View File

@@ -62,6 +62,8 @@ function App() {
const [isSendingRelease, setIsSendingRelease] = useState(false);
const [releaseMessage, setReleaseMessage] = useState("");
const [scheduledTime, setScheduledTime] = useState(""); // New state
const [releaseResponses, setReleaseResponses] = useState<any[] | null>(null);
const [isFetchingResponses, setIsFetchingResponses] = useState(false);
const fetchReleaseStats = async () => {
try {
@@ -75,6 +77,20 @@ function App() {
}
};
const fetchReleaseResponses = async () => {
setIsFetchingResponses(true);
try {
const response = await fetch(`${API_BASE_URL}/api/publish-request/responses`);
if (response.ok) {
const data = await response.json();
setReleaseResponses(data);
}
} catch (e) {
console.error("Failed to fetch release responses", e);
}
setIsFetchingResponses(false);
};
const handleUploadCodes = async () => {
setIsUploadingCodes(true);
setUploadMessage("Lädt hoch...");
@@ -248,6 +264,8 @@ function App() {
}
fetchLatestFile();
checkGmailAuth();
fetchReleaseStats();
fetchReleaseResponses();
}, [activeTab]);
const handleRefresh = () => fetchJobs(activeTab, true);
@@ -1053,6 +1071,45 @@ function App() {
{!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>
{/* Responses List */}
<div className="bg-white border border-gray-100 rounded-lg p-3 shadow-inner">
<div className="flex justify-between items-center mb-3">
<h6 className="text-[10px] font-bold text-gray-500 uppercase">Eingegangene Antworten</h6>
<button
onClick={fetchReleaseResponses}
disabled={isFetchingResponses}
className="text-[10px] bg-indigo-50 text-indigo-600 px-2 py-1 rounded hover:bg-indigo-100 transition-colors flex items-center gap-1"
>
{isFetchingResponses ? '...' : '🔄 Aktualisieren'}
</button>
</div>
{!releaseResponses || releaseResponses.length === 0 ? (
<p className="text-[10px] text-gray-400 italic text-center py-4">Noch keine Antworten eingegangen.</p>
) : (
<div className="max-h-40 overflow-y-auto rounded border border-gray-50">
<table className="min-w-full text-[10px] text-left">
<thead className="bg-gray-50 text-gray-400 sticky top-0">
<tr>
<th className="px-2 py-1">E-Mail</th>
<th className="px-2 py-1">Code</th>
<th className="px-2 py-1">Datum</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-50">
{releaseResponses.map((res, idx) => (
<tr key={idx} className="hover:bg-indigo-50/30">
<td className="px-2 py-1 text-gray-700 truncate max-w-[120px]" title={res.email}>{res.email}</td>
<td className="px-2 py-1 font-mono font-bold text-indigo-600">{res.code}</td>
<td className="px-2 py-1 text-gray-400">{new Date(res.used_at).toLocaleDateString('de-DE', {day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit'})}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
</div>