Spaces:
Sleeping
Sleeping
| import React, { useState, useEffect } from 'react'; | |
| import axios from 'axios'; | |
| import { motion } from 'framer-motion'; | |
| import { FaPaperPlane, FaDownload, FaChevronLeft, FaEnvelope, FaKey } from 'react-icons/fa'; | |
| const ReviewSendStep = ({ fileData, designParams, mappings, onBack }) => { | |
| const [eventName, setEventName] = useState(""); | |
| const [eventDate, setEventDate] = useState(""); | |
| const [clientCompany, setClientCompany] = useState(""); | |
| const [subject, setSubject] = useState("Your Attendance Certificate - {event_name}_{event_date}"); | |
| const [body, setBody] = useState("Dear Dr {first_name},\n\nThank you for attending {event_name}.\n\nYour certificate is attached.\n\nBest Regards,\n\nVolaris Team on behalf of {client_company}"); | |
| const [fromEmail, setFromEmail] = useState(""); | |
| const [senderName, setSenderName] = useState("Volaris Team"); | |
| const [sending, setSending] = useState(false); | |
| const [status, setStatus] = useState(null); | |
| const [progress, setProgress] = useState(null); | |
| const [jobId, setJobId] = useState(null); | |
| const [failedEmails, setFailedEmails] = useState([]); // Store failed email details | |
| // Fetch email configuration on mount | |
| useEffect(() => { | |
| const fetchEmailConfig = async () => { | |
| try { | |
| const response = await axios.get("/api/email/config"); | |
| setFromEmail(response.data.from_email); | |
| } catch (error) { | |
| console.error("Failed to fetch email config:", error); | |
| } | |
| }; | |
| fetchEmailConfig(); | |
| }, []); | |
| const handleSend = async () => { | |
| if (!eventName || !eventDate || !clientCompany) { | |
| alert("Please fill in all required fields: Event Name, Event Date, and Client Company"); | |
| return; | |
| } | |
| setSending(true); | |
| const formData = new FormData(); | |
| formData.append("subject", subject); | |
| formData.append("body", body); | |
| formData.append("name_column", mappings.name_column); | |
| formData.append("email_column", mappings.email_column); | |
| formData.append("event_name", eventName); | |
| formData.append("event_date", eventDate); | |
| formData.append("client_company", clientCompany); | |
| formData.append("sender_name", senderName); | |
| formData.append("name_color", designParams.name_color); | |
| formData.append("font_size", designParams.font_size); | |
| formData.append("fontname", designParams.fontname); | |
| if (designParams.x !== null) formData.append("x", designParams.x); | |
| if (designParams.y !== null) formData.append("y", designParams.y); | |
| try { | |
| const response = await axios.post("/api/email/send", formData); | |
| const newJobId = response.data.job_id; | |
| setJobId(newJobId); | |
| setProgress({ sent: 0, total: fileData.total_rows, failed: 0 }); | |
| // Start polling for progress | |
| const pollInterval = setInterval(async () => { | |
| try { | |
| const progressResponse = await axios.get(`/api/email/progress/${newJobId}`); | |
| const progressData = progressResponse.data; | |
| setProgress(progressData); | |
| // Stop polling when all emails are sent | |
| if (progressData.sent >= progressData.total) { | |
| clearInterval(pollInterval); | |
| setSending(false); | |
| // Store failed emails for display | |
| if (progressData.failed_emails && progressData.failed_emails.length > 0) { | |
| setFailedEmails(progressData.failed_emails); | |
| } | |
| const successCount = progressData.sent - progressData.failed; | |
| setStatus({ | |
| type: progressData.failed > 0 ? 'warning' : 'success', | |
| msg: `Successfully sent ${successCount} emails!${progressData.failed > 0 ? ` (${progressData.failed} failed)` : ''}` | |
| }); | |
| setProgress(null); | |
| } | |
| } catch (pollError) { | |
| console.error("Progress polling error:", pollError); | |
| } | |
| }, 500); // Poll every 500ms | |
| } catch (error) { | |
| setStatus({ type: 'error', msg: "Failed to start sending process." }); | |
| console.error(error); | |
| setSending(false); | |
| } | |
| }; | |
| const handleDownload = async () => { | |
| const formData = new FormData(); | |
| formData.append("name_column", mappings.name_column); | |
| formData.append("email_column", mappings.email_column); | |
| formData.append("name_color", designParams.name_color); | |
| formData.append("font_size", designParams.font_size); | |
| formData.append("fontname", designParams.fontname); | |
| if (designParams.x !== null) formData.append("x", designParams.x); | |
| if (designParams.y !== null) formData.append("y", designParams.y); | |
| try { | |
| const response = await axios.post("/api/certificates/generate", formData, { | |
| responseType: 'blob' | |
| }); | |
| const url = window.URL.createObjectURL(new Blob([response.data])); | |
| const link = document.createElement('a'); | |
| link.href = url; | |
| link.setAttribute('download', 'certificates.zip'); | |
| document.body.appendChild(link); | |
| link.click(); | |
| } catch (error) { | |
| console.error("Download failed", error); | |
| } | |
| }; | |
| return ( | |
| <div className="max-w-5xl mx-auto"> | |
| <div className="text-center mb-8"> | |
| <h2 className="text-2xl font-bold text-gray-900 mb-2">Review & Send</h2> | |
| <p className="text-gray-600">Configure your email settings and launch the campaign.</p> | |
| </div> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-8"> | |
| {/* Email Configuration */} | |
| <div className="space-y-6"> | |
| <div className="bg-white border border-gray-200 p-6 rounded-lg shadow-sm"> | |
| <h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center"> | |
| Event Details | |
| </h3> | |
| <div className="space-y-4"> | |
| <div> | |
| <label className="block text-xs font-medium text-gray-700 mb-1">Event Name *</label> | |
| <input | |
| type="text" | |
| className="w-full border border-gray-300 rounded-lg p-3 text-gray-900 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none" | |
| placeholder="e.g., Annual Medical Conference" | |
| value={eventName} | |
| onChange={(e) => setEventName(e.target.value)} | |
| /> | |
| </div> | |
| <div> | |
| <label className="block text-xs font-medium text-gray-700 mb-1">Event Date *</label> | |
| <input | |
| type="text" | |
| className="w-full border border-gray-300 rounded-lg p-3 text-gray-900 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none" | |
| placeholder="e.g., November 2024" | |
| value={eventDate} | |
| onChange={(e) => setEventDate(e.target.value)} | |
| /> | |
| </div> | |
| <div> | |
| <label className="block text-xs font-medium text-gray-700 mb-1">Client Company *</label> | |
| <input | |
| type="text" | |
| className="w-full border border-gray-300 rounded-lg p-3 text-gray-900 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none" | |
| placeholder="e.g., Pharmaceutical Corp" | |
| value={clientCompany} | |
| onChange={(e) => setClientCompany(e.target.value)} | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="bg-white border border-gray-200 p-6 rounded-lg shadow-sm"> | |
| <h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center"> | |
| <FaEnvelope className="mr-2 text-blue-600" /> Email Configuration | |
| </h3> | |
| <div className="space-y-4"> | |
| <div> | |
| <label className="block text-xs font-medium text-gray-700 mb-1">Sender Name</label> | |
| <input | |
| type="text" | |
| className="w-full border border-gray-300 rounded-lg p-3 text-gray-900 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none" | |
| placeholder="e.g., Volaris Team" | |
| value={senderName} | |
| onChange={(e) => setSenderName(e.target.value)} | |
| /> | |
| <p className="text-xs text-gray-500 mt-1">This name appears in the recipient's inbox</p> | |
| </div> | |
| <div> | |
| <label className="block text-xs font-medium text-gray-700 mb-1">Sender Email</label> | |
| <div className="w-full border border-gray-200 rounded-lg p-3 bg-gray-50 text-gray-700 font-medium"> | |
| {fromEmail || 'Loading...'} | |
| </div> | |
| <p className="text-xs text-gray-500 mt-1">Configured in environment settings</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="bg-white border border-gray-200 p-6 rounded-lg shadow-sm"> | |
| <h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center"> | |
| <FaEnvelope className="mr-2 text-blue-600" /> Email Content | |
| </h3> | |
| <div className="space-y-4"> | |
| <div> | |
| <label className="block text-xs font-medium text-gray-700 mb-1">Subject Line</label> | |
| <input | |
| type="text" | |
| className="w-full border border-gray-300 rounded-lg p-3 text-gray-900 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none" | |
| value={subject} | |
| onChange={(e) => setSubject(e.target.value)} | |
| /> | |
| </div> | |
| <div> | |
| <label className="block text-xs font-medium text-gray-700 mb-1">Email Body</label> | |
| <textarea | |
| rows={6} | |
| className="w-full border border-gray-300 rounded-lg p-3 text-gray-900 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none font-mono text-sm" | |
| value={body} | |
| onChange={(e) => setBody(e.target.value)} | |
| /> | |
| <p className="text-xs text-gray-500 mt-2"> | |
| Variables: <span className="text-blue-600 font-mono">{'{first_name}'}</span>, <span className="text-blue-600 font-mono">{'{name}'}</span>, <span className="text-blue-600 font-mono">{'{event_name}'}</span>, <span className="text-blue-600 font-mono">{'{event_date}'}</span>, <span className="text-blue-600 font-mono">{'{client_company}'}</span> | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Summary & Actions */} | |
| <div className="space-y-6"> | |
| <div className="bg-gradient-to-br from-blue-50 to-blue-100 border border-blue-200 p-6 rounded-lg"> | |
| <h3 className="text-lg font-semibold text-gray-900 mb-4">Campaign Summary</h3> | |
| <ul className="space-y-3 text-sm text-gray-700"> | |
| <li className="flex justify-between border-b border-blue-200 pb-2"> | |
| <span>Total Recipients</span> | |
| <span className="font-bold text-gray-900">{fileData.total_rows}</span> | |
| </li> | |
| <li className="flex justify-between border-b border-blue-200 pb-2"> | |
| <span>Name Column</span> | |
| <span className="font-mono text-blue-700">{mappings.name_column}</span> | |
| </li> | |
| <li className="flex justify-between border-b border-blue-200 pb-2"> | |
| <span>Email Column</span> | |
| <span className="font-mono text-blue-700">{mappings.email_column}</span> | |
| </li> | |
| </ul> | |
| </div> | |
| <div className="space-y-4"> | |
| <motion.button | |
| whileHover={{ scale: 1.02 }} | |
| whileTap={{ scale: 0.98 }} | |
| onClick={handleDownload} | |
| className="w-full py-4 px-6 border border-blue-600 text-blue-600 rounded-lg hover:bg-blue-50 font-medium flex items-center justify-center space-x-2 transition-colors" | |
| > | |
| <FaDownload /> | |
| <span>Download All Certificates (ZIP)</span> | |
| </motion.button> | |
| <motion.button | |
| whileHover={{ scale: 1.02 }} | |
| whileTap={{ scale: 0.98 }} | |
| onClick={handleSend} | |
| disabled={sending} | |
| className={`w-full py-4 px-6 rounded-lg text-white font-bold shadow-md flex items-center justify-center space-x-2 transition-all ${sending | |
| ? 'bg-gray-400 cursor-not-allowed' | |
| : 'bg-green-600 hover:bg-green-700 hover:shadow-lg' | |
| }`} | |
| > | |
| {sending ? ( | |
| <span>Sending Campaign...</span> | |
| ) : ( | |
| <> | |
| <FaPaperPlane /> | |
| <span>Send Certificates Now</span> | |
| </> | |
| )} | |
| </motion.button> | |
| </div> | |
| {progress && ( | |
| <motion.div | |
| initial={{ opacity: 0, y: 10 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| className="bg-white border border-blue-200 p-6 rounded-lg shadow-sm" | |
| > | |
| <h4 className="text-sm font-semibold text-gray-900 mb-3">Sending Progress</h4> | |
| <div className="space-y-3"> | |
| <div className="flex justify-between text-sm text-gray-700 mb-2"> | |
| <span className="font-medium">{progress.sent} / {progress.total} emails sent</span> | |
| <span className="text-blue-600 font-bold">{Math.round((progress.sent / progress.total) * 100)}%</span> | |
| </div> | |
| <div className="w-full bg-gray-200 rounded-full h-3 overflow-hidden"> | |
| <motion.div | |
| className="bg-gradient-to-r from-blue-500 to-green-500 h-full rounded-full" | |
| initial={{ width: 0 }} | |
| animate={{ width: `${(progress.sent / progress.total) * 100}%` }} | |
| transition={{ duration: 0.3 }} | |
| /> | |
| </div> | |
| {progress.failed > 0 && ( | |
| <p className="text-xs text-red-600">⚠️ {progress.failed} failed</p> | |
| )} | |
| </div> | |
| </motion.div> | |
| )} | |
| {status && ( | |
| <motion.div | |
| initial={{ opacity: 0, y: 10 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| className={`p-4 rounded-lg border text-center ${status.type === 'success' | |
| ? 'bg-green-50 border-green-200 text-green-700' | |
| : status.type === 'warning' | |
| ? 'bg-yellow-50 border-yellow-200 text-yellow-700' | |
| : 'bg-red-50 border-red-200 text-red-700' | |
| }`} | |
| > | |
| {status.msg} | |
| </motion.div> | |
| )} | |
| {/* Failed Emails Details */} | |
| {failedEmails.length > 0 && ( | |
| <motion.div | |
| initial={{ opacity: 0, y: 10 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| className="bg-red-50 border border-red-200 p-6 rounded-lg" | |
| > | |
| <h4 className="text-sm font-semibold text-red-800 mb-3 flex items-center"> | |
| ⚠️ Failed Emails ({failedEmails.length}) | |
| </h4> | |
| <div className="max-h-48 overflow-y-auto space-y-2"> | |
| {failedEmails.map((failed, index) => ( | |
| <div key={index} className="bg-white rounded p-3 border border-red-100"> | |
| <div className="flex justify-between items-start"> | |
| <div> | |
| <p className="text-sm font-medium text-gray-900">{failed.name}</p> | |
| <p className="text-xs text-gray-600">{failed.email}</p> | |
| </div> | |
| </div> | |
| <p className="text-xs text-red-600 mt-1 font-mono">{failed.error}</p> | |
| </div> | |
| ))} | |
| </div> | |
| </motion.div> | |
| )} | |
| </div> | |
| </div> | |
| <div className="mt-8"> | |
| <button | |
| onClick={onBack} | |
| className="text-gray-600 hover:text-gray-900 flex items-center space-x-2 transition-colors font-medium" | |
| > | |
| <FaChevronLeft /> | |
| <span>Back to Design</span> | |
| </button> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default ReviewSendStep; | |