const { useState, useEffect, useRef } = React;
const SMTP_STORAGE_KEY = "cert_smtp_profiles_v1";
const FORM_STORAGE_KEY = "cert_form_data_v2";
function loadSmtpProfiles() {
try {
const saved = localStorage.getItem(SMTP_STORAGE_KEY);
if (saved) {
const parsed = JSON.parse(saved);
if (Array.isArray(parsed) && parsed.length > 0) return parsed;
}
} catch (e) {
console.error("Error loading SMTP profiles from localStorage", e);
}
return [{
id: "default_1",
label: "Default Gmail",
sender_email: "",
password: "",
smtp_server: "smtp.gmail.com",
port: "587"
}];
}
function saveSmtpProfiles(profiles) {
try {
localStorage.setItem(SMTP_STORAGE_KEY, JSON.stringify(profiles));
} catch (e) {
console.error("Error saving SMTP profiles to localStorage", e);
}
}
function loadFormData() {
try {
const saved = localStorage.getItem(FORM_STORAGE_KEY);
if (saved) {
const parsed = JSON.parse(saved);
if (parsed && typeof parsed === 'object') return parsed;
}
} catch (e) {}
const today = new Date().toISOString().split('T')[0];
return {
recipient_name: "",
recipient_email: "",
course_title: "AI IN ACTION",
issue_date: today,
sign_left: "Gouri Shankar",
sign_right: "Arpit Shah",
template_id: "ai_in_action"
};
}
function saveFormData(data) {
try {
localStorage.setItem(FORM_STORAGE_KEY, JSON.stringify(data));
} catch (e) {}
}
function App() {
const [activeTab, setActiveTab] = useState("generator");
const [certHistory, setCertHistory] = useState([]);
const [smtpProfiles, setSmtpProfiles] = useState(() => loadSmtpProfiles());
const [activeProfileId, setActiveProfileId] = useState(() => {
return localStorage.getItem("active_smtp_profile_id") || (loadSmtpProfiles()[0]?.id || "default_1");
});
useEffect(() => {
saveSmtpProfiles(smtpProfiles);
}, [smtpProfiles]);
useEffect(() => {
localStorage.setItem("active_smtp_profile_id", activeProfileId);
}, [activeProfileId]);
const activeSmtpConfig = smtpProfiles.find(p => p.id === activeProfileId) || smtpProfiles[0] || {
sender_email: "", password: "", smtp_server: "smtp.gmail.com", port: "587"
};
// =========================================================================
// API CALL 1: GET /api/certificates/history
// Fetches the complete list of issued certificates from database
// =========================================================================
const fetchHistory = async () => {
try {
const res = await fetch('/api/certificates/history');
const data = await res.json();
if (data.history) setCertHistory(data.history);
} catch (err) {
console.error("Failed to fetch certificate history", err);
}
};
useEffect(() => {
fetchHistory();
}, []);
return (
{/* Header */}
{/* Navigation Tabs Bar */}
{/* Main Active Tab Content */}
{activeTab === 'generator' && (
)}
{activeTab === 'batch' && (
)}
{activeTab === 'records' && (
)}
{activeTab === 'smtp' && (
)}
);
}
// ============================================================================
// TAB 1: Single Certificate Generator (NO AUTO-GENERATE ON REFRESH)
// ============================================================================
function GeneratorTab({ smtpConfig, fetchHistory, setActiveTab }) {
const [formData, setFormData] = useState(() => loadFormData());
const [currentCert, setCurrentCert] = useState(null);
const [loading, setLoading] = useState(false);
const [showPreviewModal, setShowPreviewModal] = useState(false);
// Save form data to localStorage whenever updated
useEffect(() => {
saveFormData(formData);
}, [formData]);
// Explicit manual generate ONLY
const handleGenerateSingle = async (e) => {
if (e) e.preventDefault();
if (!formData.recipient_name.trim()) {
alert("Please enter Recipient Name first!");
return;
}
setLoading(true);
try {
// =========================================================================
// API CALL 2: POST /api/certificates/generate
// Generates single certificate (PDF + SVG) for the current form inputs
// =========================================================================
const res = await fetch('/api/certificates/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
const data = await res.json();
setCurrentCert(data);
setShowPreviewModal(true);
fetchHistory();
} catch (err) {
alert("Error generating certificate: " + err.message);
} finally {
setLoading(false);
}
};
const handleGenerateAndSendEmail = async (e) => {
if (e) e.preventDefault();
if (!formData.recipient_name.trim()) {
alert("Please enter Recipient Name first!");
return;
}
if (!formData.recipient_email.trim()) {
alert("Please enter a valid Recipient Email Address!");
return;
}
if (!smtpConfig.sender_email || !smtpConfig.password) {
alert("Please configure your SMTP Sender Email and App Password in SMTP Settings first!");
setActiveTab("smtp");
return;
}
setLoading(true);
try {
// =========================================================================
// API CALL 3A: POST /api/certificates/generate
// Step 1: Generates the single certificate PDF & SVG files first
// =========================================================================
const res = await fetch('/api/certificates/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
const data = await res.json();
setCurrentCert(data);
// =========================================================================
// API CALL 3B: POST /api/certificates/send_email
// Step 2: Dispatches the 1-page PDF certificate email via SMTP
// =========================================================================
const emailRes = await fetch('/api/certificates/send_email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
cert_ids: [data.id],
override_email: formData.recipient_email,
smtp_config: smtpConfig
})
});
const emailData = await emailRes.json();
const result = emailData.results[0];
if (result && result.success) {
alert(`SUCCESS: Certificate generated & 1-page PDF emailed to ${formData.recipient_email}!`);
setActiveTab("records");
} else {
alert(`Certificate generated, but Email failed: ${result?.error || 'Check SMTP credentials'}`);
}
fetchHistory();
} catch (err) {
alert("Error: " + err.message);
} finally {
setLoading(false);
}
};
const downloadSvg = () => {
if (!currentCert) return;
const blob = new Blob([currentCert.svg_content], { type: 'image/svg+xml' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
const clean_name = currentCert.recipient_name.replace(/[^a-zA-Z0-9]/g, '_');
a.download = `Certificate_${clean_name}.svg`;
a.click();
};
const resetForm = () => {
const today = new Date().toISOString().split('T')[0];
setFormData({
recipient_name: "",
recipient_email: "",
course_title: "AI IN ACTION",
issue_date: today,
sign_left: "Gouri Shankar",
sign_right: "Arpit Shah",
template_id: "ai_in_action"
});
};
return (
Certificate Generator
{/* On-Demand Preview Modal */}
{showPreviewModal && currentCert && (
setShowPreviewModal(false)}>
e.stopPropagation()}>
Generated Certificate Preview
)}
);
}
// ============================================================================
// TAB 2: Batch Recipient Import with Drag & Drop
// ============================================================================
function BatchTab({ smtpConfig, fetchHistory, setActiveTab }) {
const [parsedRecipients, setParsedRecipients] = useState([]);
const [fileName, setFileName] = useState("");
const [loading, setLoading] = useState(false);
const [isDragOver, setIsDragOver] = useState(false);
const [mode, setMode] = useState("file");
const [batchText, setBatchText] = useState("");
const fileInputRef = useRef(null);
const handleFileSelected = (file) => {
if (!file) return;
setFileName(file.name);
setLoading(true);
const reader = new FileReader();
reader.onload = async (e) => {
const base64Data = e.target.result.split(',')[1];
try {
const res = await fetch('/api/certificates/parse_file', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
filename: file.name,
file_data: base64Data
})
});
const data = await res.json();
if (data.success && data.recipients) {
setParsedRecipients(data.recipients);
} else {
alert("Failed to parse file: " + (data.error || "Unknown error"));
}
} catch (err) {
alert("File parse error: " + err.message);
} finally {
setLoading(false);
}
};
reader.readAsDataURL(file);
};
const handleDragOver = (e) => {
e.preventDefault();
setIsDragOver(true);
};
const handleDragLeave = (e) => {
e.preventDefault();
setIsDragOver(false);
};
const handleDrop = (e) => {
e.preventDefault();
setIsDragOver(false);
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
handleFileSelected(e.dataTransfer.files[0]);
}
};
const handleProcessBatchImport = async (shouldSendEmail = false) => {
let items = [];
if (mode === "file") {
items = parsedRecipients.map(r => ({
recipient_name: r.recipient_name,
email: r.email,
course_title: r.course_title || "AI IN ACTION"
}));
} else {
if (!batchText.trim()) return;
const lines = batchText.trim().split('\n');
items = lines.map(line => {
const parts = line.split(',');
return {
recipient_name: parts[0]?.trim() || "Recipient",
email: parts[1]?.trim() || "",
course_title: parts[2]?.trim() || "AI IN ACTION"
};
});
}
if (items.length === 0) {
alert("No recipients to process!");
return;
}
if (shouldSendEmail && (!smtpConfig?.sender_email || !smtpConfig?.password)) {
alert("Please configure your SMTP Sender Email and App Password in SMTP Settings first!");
setActiveTab("smtp");
return;
}
setLoading(true);
try {
// Step 1: Batch generate certificates into folder & database
const res = await fetch('/api/certificates/generate_to_folder', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items, template_id: "ai_in_action" })
});
const data = await res.json();
const createdCerts = data.certificates || [];
if (shouldSendEmail && createdCerts.length > 0) {
// Step 2: Dispatch emails via SMTP for all created certificates
const certIds = createdCerts.map(c => c.id || c.cert_id);
const emailMap = {};
createdCerts.forEach((c, idx) => {
const targetEmail = items[idx]?.email || c.recipient_email || "";
if (targetEmail) emailMap[c.id || c.cert_id] = targetEmail;
});
const emailRes = await fetch('/api/certificates/send_email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
cert_ids: certIds,
email_map: emailMap,
smtp_config: smtpConfig
})
});
const emailData = await emailRes.json();
const successCount = (emailData.results || []).filter(r => r.success).length;
alert(`SUCCESS: Generated ${data.count} certificate(s) & dispatched ${successCount} 1-page PDF email(s)!`);
} else {
alert(`Successfully generated & imported ${data.count} certificates into 'certificates/' folder.`);
}
setParsedRecipients([]);
setFileName("");
setBatchText("");
fetchHistory();
setActiveTab("records");
} catch (err) {
alert("Batch Process Error: " + err.message);
} finally {
setLoading(false);
}
};
return (
Batch Recipient Import
{mode === "file" ? (
<>
e.target.files && handleFileSelected(e.target.files[0])}
/>
fileInputRef.current && fileInputRef.current.click()}
>
📁
{fileName ? `Loaded: ${fileName}` : "Drag and drop your Excel or CSV file here"}
Supports Microsoft Excel (.xlsx, .xls) and CSV (.csv) files
{parsedRecipients.length > 0 && (
Parsed Recipients Preview ({parsedRecipients.length})
| # |
Recipient Name |
Target Email Address |
Course / Title |
{parsedRecipients.map((r, idx) => (
| {idx + 1} |
{r.recipient_name} |
{r.email || No email set} |
{r.course_title} |
))}
)}
>
) : (
Paste CSV formatted text below. Format: Recipient Name, Recipient Email, Course Title per line.
)}
);
}
// ============================================================================
// TAB 3: Certificate Records & Dispatch Manager
// ============================================================================
function RecordsTab({ certHistory, fetchHistory, smtpConfig, setActiveTab }) {
const [selectedCertIds, setSelectedCertIds] = useState([]);
const [filterStatus, setFilterStatus] = useState("ALL");
const [searchQuery, setSearchQuery] = useState("");
const [loading, setLoading] = useState(false);
const [dispatchModal, setDispatchModal] = useState(null);
const toggleSelectCert = (id) => {
if (selectedCertIds.includes(id)) {
setSelectedCertIds(selectedCertIds.filter(item => item !== id));
} else {
setSelectedCertIds([...selectedCertIds, id]);
}
};
const toggleSelectAll = () => {
if (selectedCertIds.length === filteredHistory.length) {
setSelectedCertIds([]);
} else {
setSelectedCertIds(filteredHistory.map(item => item.id));
}
};
const filteredHistory = certHistory.filter(item => {
const itemStatus = (item.email_status || "NOT_SENT").toUpperCase();
const matchesStatus = filterStatus === "ALL" || itemStatus === filterStatus.toUpperCase();
const q = searchQuery.toLowerCase().trim();
const matchesQuery = !q ||
item.recipient_name.toLowerCase().includes(q) ||
item.id.toLowerCase().includes(q) ||
(item.recipient_email || "").toLowerCase().includes(q);
return matchesStatus && matchesQuery;
});
const handleDeleteRecords = async (certIds) => {
if (!confirm(`Are you sure you want to delete ${certIds.length} certificate record(s)?`)) return;
setLoading(true);
try {
// =========================================================================
// API CALL 6: POST /api/certificates/delete
// Removes selected certificate IDs from the database
// =========================================================================
const res = await fetch('/api/certificates/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ cert_ids: certIds })
});
const data = await res.json();
if (data.success) {
setSelectedCertIds(selectedCertIds.filter(id => !certIds.includes(id)));
fetchHistory();
}
} catch (err) {
alert("Delete Error: " + err.message);
} finally {
setLoading(false);
}
};
const openDispatchModal = (certObjects) => {
if (!smtpConfig.sender_email || !smtpConfig.password) {
alert("Please configure your SMTP Sender Email and App Password in SMTP Settings first!");
setActiveTab("smtp");
return;
}
setDispatchModal({
certs: certObjects.map(c => ({
id: c.id,
name: c.recipient_name,
email: c.recipient_email || ""
}))
});
};
const handleConfirmDispatch = async () => {
if (!dispatchModal || !dispatchModal.certs.length) return;
const emailMap = {};
for (const c of dispatchModal.certs) {
if (!c.email.trim()) {
alert(`Please enter a valid email address for ${c.name} (${c.id})`);
return;
}
emailMap[c.id] = c.email.trim();
}
setLoading(true);
try {
// =========================================================================
// API CALL 7: POST /api/certificates/send_email
// Bulk emails 1-page PDF certificates to selected recipients using SMTP
// =========================================================================
const res = await fetch('/api/certificates/send_email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
cert_ids: dispatchModal.certs.map(c => c.id),
email_map: emailMap,
smtp_config: smtpConfig
})
});
const data = await res.json();
const successCount = data.results.filter(r => r.success).length;
alert(`Email Dispatch Complete: ${successCount} of ${data.results.length} email(s) sent successfully.`);
setDispatchModal(null);
fetchHistory();
} catch (err) {
alert("Dispatch Error: " + err.message);
} finally {
setLoading(false);
}
};
const handleUpdateInlineEmail = async (certId, currentEmail) => {
const newEmail = prompt("Enter updated recipient email address:", currentEmail || "");
if (newEmail !== null && newEmail.trim() !== currentEmail) {
try {
// =========================================================================
// API CALL 8: POST /api/certificates/update_email
// Updates recipient email address for a certificate ID in SQLite DB
// =========================================================================
const res = await fetch('/api/certificates/update_email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ cert_id: certId, email: newEmail.trim() })
});
const data = await res.json();
if (data.success) fetchHistory();
} catch (err) {
alert("Failed to update email: " + err.message);
}
}
};
const downloadSelected = () => {
const selectedObjects = certHistory.filter(item => selectedCertIds.includes(item.id));
selectedObjects.forEach(cert => {
fetch('/api/certificates/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
recipient_name: cert.recipient_name,
course_title: cert.course_title,
issue_date: cert.issue_date,
template_id: cert.template_id
})
})
.then(res => res.json())
.then(data => {
const blob = new Blob([data.svg_content], { type: 'image/svg+xml' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `Certificate_${data.recipient_name.replace(/ /g, '_')}_${data.id}.svg`;
a.click();
});
});
};
return (
Certificate Records & Email Dispatch
{filteredHistory.length} Record(s)
{selectedCertIds.length > 0 && (
{selectedCertIds.length} item(s) selected
)}
{dispatchModal && (
Confirm Targeted Email Dispatch
Verify the target recipient email addresses before dispatching single-page PDF certificates:
{dispatchModal.certs.map((c, idx) => (
))}
)}
);
}
// ============================================================================
// TAB 4: Dedicated SMTP Settings
// ============================================================================
function SmtpTab({ smtpProfiles, setSmtpProfiles, activeProfileId, setActiveProfileId, setActiveTab }) {
const [formProfile, setFormProfile] = useState({
id: "",
label: "",
sender_email: "",
password: "",
smtp_server: "smtp.gmail.com",
port: "587"
});
const [isEditing, setIsEditing] = useState(false);
const handleSaveProfile = (e) => {
e.preventDefault();
if (!formProfile.sender_email || !formProfile.password) {
alert("Please enter Sender Email Address and App Password!");
return;
}
let updated = [];
if (isEditing && formProfile.id) {
updated = smtpProfiles.map(p => p.id === formProfile.id ? formProfile : p);
} else {
const newId = "smtp_" + Date.now();
const newProfile = {
...formProfile,
id: newId,
label: formProfile.label || formProfile.sender_email.split('@')[0] || "SMTP Account"
};
updated = [...smtpProfiles, newProfile];
setActiveProfileId(newId);
}
setSmtpProfiles(updated);
resetForm();
alert("SMTP Account saved & persisted in localStorage!");
};
const handleDeleteProfile = (id) => {
if (smtpProfiles.length <= 1) {
alert("You must keep at least 1 SMTP account profile.");
return;
}
if (!confirm("Are you sure you want to delete this SMTP account profile?")) return;
const updated = smtpProfiles.filter(p => p.id !== id);
setSmtpProfiles(updated);
if (activeProfileId === id) {
setActiveProfileId(updated[0]?.id || "");
}
};
const handleEditProfile = (profile) => {
setFormProfile(profile);
setIsEditing(true);
};
const resetForm = () => {
setFormProfile({
id: "",
label: "",
sender_email: "",
password: "",
smtp_server: "smtp.gmail.com",
port: "587"
});
setIsEditing(false);
};
return (
{isEditing ? "Edit SMTP Account" : "Add New SMTP Account"}
{isEditing && (
)}
Saved SMTP Accounts ({smtpProfiles.length})
Saved credentials remain stored in your browser even after refreshing.
{smtpProfiles.map((p) => {
const isActive = p.id === activeProfileId;
return (
{p.label || "SMTP Account"}
{isActive ? (
ACTIVE
) : (
)}
{p.sender_email || No email set}
);
})}
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render();