diff --git a/client/src/Pages/courseConsolidated.jsx b/client/src/Pages/courseConsolidated.jsx index 0c0c96a..5fbcc54 100644 --- a/client/src/Pages/courseConsolidated.jsx +++ b/client/src/Pages/courseConsolidated.jsx @@ -90,129 +90,112 @@ const CourseConsolidated = () => { { key: "pwdPaperSettingTeachers", role: "PwD Paper Setter" }, ]; + // ✅ NEW: Group appointments by Faculty ID + const facultyMap = {}; + for (const { key, role } of roles) { - if (!courseData[key] || courseData[key].length === 0) continue; // Skip empty roles - - const doc = new jsPDF(); - - // College Logo - const logoUrl = "/logo.png"; - const loadImage = async (url) => { - const response = await fetch(url); - const blob = await response.blob(); - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = () => resolve(reader.result); - reader.onerror = (error) => reject(error); - reader.readAsDataURL(blob); - }); - }; - - try { - const logoBase64 = await loadImage(logoUrl); - doc.addImage(logoBase64, "PNG", 10, 10, 40, 40); - } catch (error) { - console.error("Failed to load logo:", error); - } - - // Title Section - doc.setFont("times", "normal"); - doc.setTextColor(0, 0, 0); - doc.setFontSize(12); - doc.text("Date: " + new Date().toLocaleDateString(), 150, 20); - doc.setFontSize(14); - doc.text("CONFIDENTIAL", 10, 60); - doc.setFontSize(10); - doc.text(`LETTER OF APPOINTMENT AS ${role.toUpperCase()}`, 105, 70, { - align: "center", - }); - - // Fetch Emails and Prepare Table Data - let table1Data = []; - let emails = []; + if (!courseData[key] || courseData[key].length === 0) continue; for (const teacher of courseData[key]) { - const email = await fetchFacultyEmail(teacher.facultyId); - emails.push(email); - table1Data.push([ - teacher.facultyName, - "K. J. Somaiya School of Engineering", + if (!facultyMap[teacher.facultyId]) { + facultyMap[teacher.facultyId] = { + facultyName: teacher.facultyName, + email: await fetchFacultyEmail(teacher.facultyId), + appointments: [], + }; + } + facultyMap[teacher.facultyId].appointments.push({ role, - email, - ]); + teacher, + }); + } + } + + // ✅ NEW: For each Faculty, generate PDFs & send one email + for (const facultyId in facultyMap) { + const faculty = facultyMap[facultyId]; + const pdfBlobs = []; + + for (const appointment of faculty.appointments) { + const { role } = appointment; + + const doc = new jsPDF(); + + // Add Logo and Title (your original PDF content) + const logoUrl = "/logo.png"; + const loadImage = async (url) => { + const response = await fetch(url); + const blob = await response.blob(); + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result); + reader.onerror = (error) => reject(error); + reader.readAsDataURL(blob); + }); + }; + + try { + const logoBase64 = await loadImage(logoUrl); + doc.addImage(logoBase64, "PNG", 10, 10, 40, 40); + } catch (error) { + console.error("Failed to load logo:", error); + } + + doc.setFont("times", "normal"); + doc.text(`Date: ${new Date().toLocaleDateString()}`, 150, 20); + doc.text("CONFIDENTIAL", 10, 60); + doc.text(`LETTER OF APPOINTMENT AS ${role.toUpperCase()}`, 105, 70, { align: "center" }); + + autoTable(doc, { + head: [["Name", "Affiliation", "Appointment Role", "Email"]], + body: [[faculty.facultyName, "K. J. Somaiya School of Engineering", role, faculty.email]], + startY: 80, + theme: "grid", + headStyles: { fillColor: maroon, textColor: [255, 255, 255] }, + }); + + autoTable(doc, { + body: [ + ["Programme:", courseData.courseName], + ["Exam Category:", "Regular Examination"], + ["Exam Type:", courseData.examType], + ["Exam Season:", "Second Half - Winter Examination 2023"], + ["Year:", courseData.year], + ["Semester:", courseData.semester], + ["Course Name:", courseName], + ["Course Code:", courseData.courseCode], + ], + startY: doc.previousAutoTable.finalY + 10, + theme: "grid", + }); + + const pdfBlob = doc.output("blob"); + pdfBlobs.push({ blob: pdfBlob, filename: `${courseName}-${role}.pdf` }); } - // Generate Table - autoTable(doc, { - head: [["Name", "Affiliation", "Appointment Role", "Email"]], - body: table1Data, - startY: 80, - theme: "grid", - headStyles: { fillColor: maroon, textColor: [255, 255, 255] }, - styles: { textColor: [0, 0, 0] }, - }); + // ✅ NEW: Send ONE email with ALL PDFs to the Faculty + try { + const formData = new FormData(); - // Content Table - const detailsTableData = [ - ["Programme:", courseData.courseName], - ["Exam Category:", "Regular Examination"], - ["Exam Type:", courseData.examType], - ["Exam Season:", "Second Half - Winter Examination 2023"], - ["Number of Sets Required:", courseData.paperSettingTeachers.length], - ["Year:", courseData.year], - ["Semester:", courseData.semester], - ["Course Name:", courseName], - ["Course Code:", courseData.courseCode], - ]; + pdfBlobs.forEach(({ blob, filename }) => { + formData.append("pdfFiles", blob, filename); // note 'pdfFiles' is plural + }); - autoTable(doc, { - body: detailsTableData, - startY: doc.previousAutoTable.finalY + 10, - theme: "grid", - styles: { textColor: [0, 0, 0] }, - }); + formData.append("recipientEmail", faculty.email); + formData.append("facultyName", faculty.facultyName); + formData.append("courseName", courseName); - // Footer - const footerY = doc.previousAutoTable.finalY + 10; - doc.setFontSize(12); - doc.text("Dr. S. K. Ukarande", 10, footerY); - doc.text("Principal", 10, footerY + 5); - doc.text("K. J. Somaiya School of Engineering", 10, footerY + 10); - - const footerContactY = footerY + 20; - doc.setFontSize(10); - doc.text( - "Somaiya Vidyavihar, Vidyavihar (East), Mumbai-400 022, India", - 10, - footerContactY - ); - doc.text("Telephone: (91-22) 44444400, 44444404", 10, footerContactY + 5); - doc.text("Email: principal.tech@somaiya.edu", 10, footerContactY + 10); - doc.text("Web: www.somaiya.edu/kjsieit", 10, footerContactY + 15); - - // Convert PDF to Blob - const pdfBlob = doc.output("blob"); - - // Send Email to Each Faculty - for (const email of emails) { - try { - const formData = new FormData(); - formData.append("pdfFile", pdfBlob, `${courseName}-${role}.pdf`); - formData.append("fileName", `${courseName}-${role}.pdf`); - formData.append("recipientEmail", email); - formData.append("role", role); - formData.append("courseName", courseName); - - const emailResponse = await sendEmail(formData, "pdf"); - toast.success(`✅ Email sent successfully to ${email}`); - console.log("Response from server:", emailResponse); - } catch (emailError) { - toast.error(`❌ Error sending email to ${email}:`, emailError.message); - } + const emailResponse = await sendEmail(formData, "bulk-pdf"); + toast.success(`✅ Email sent successfully to ${faculty.email}`); + console.log("Response:", emailResponse); + } catch (error) { + toast.error(`❌ Error sending email to ${faculty.email}: ${error.message}`); + console.error(error); } } }; + return ( <> diff --git a/server/routes/emailRoutes.js b/server/routes/emailRoutes.js index bb3ebfd..95abb49 100644 --- a/server/routes/emailRoutes.js +++ b/server/routes/emailRoutes.js @@ -4,19 +4,31 @@ const fs = require("fs"); const multer = require("multer"); const router = express.Router(); -// Setup multer for handling file uploads +// Multer setup remains intact const storage = multer.diskStorage({ destination: function (req, file, cb) { - cb(null, "./"); // Save to the current directory (you can customize this) + cb(null, "./"); // Customize directory if needed }, filename: function (req, file, cb) { - cb(null, file.originalname); // Use the original file name + cb(null, file.originalname); }, }); - const upload = multer({ storage }); -// Route to handle email sending with file attachment +// Single transporter setup clearly moved to top (no duplication) +const transporter = nodemailer.createTransport({ + service: "gmail", + auth: { + user: "swdc.ate@gmail.com", + pass: "umlc hbkr dpga iywd", + }, + tls: { rejectUnauthorized: false }, + connectionTimeout: 30000, + greetingTimeout: 30000, + socketTimeout: 30000, +}); + +// Existing Excel route unchanged, except transporter removal router.post("/excel", upload.single("file"), async (req, res) => { const { teacher, fileName, recipientEmail } = req.body; @@ -24,24 +36,8 @@ router.post("/excel", upload.single("file"), async (req, res) => { return res.status(400).json({ error: "Missing required fields or file" }); } - // Configure Nodemailer transporter - const transporter = nodemailer.createTransport({ - service: "gmail", - auth: { - user: "swdc.ate@gmail.com", // Replace with your email - pass: "umlc hbkr dpga iywd", // Replace with your app-specific password or token - }, - tls: { - rejectUnauthorized: false, // Ignore self-signed certificate errors - }, - connectionTimeout: 15000, - greetingTimeout: 15000, - socketTimeout: 15000, - }); - - // Email options const mailOptions = { - from: "SWDC Admin ", // Replace with your email + from: "SWDC Admin ", to: recipientEmail, subject: `Examination Appointments for ${teacher}`, text: `Dear Sir/Madam, @@ -50,27 +46,18 @@ Please find attached the Excel sheet regarding various appointments for the May- Note: Kindly download the Excel sheet to view the hidden columns referring to the Scheme and other information. - If you have any queries, please contact the H.O.D or the Examination In-charge. Your cooperation regarding the upcoming examination duties is highly solicited.`, - attachments: [ - { - filename: fileName, - path: req.file.path, // Use the uploaded file's path - }, - ], + attachments: [{ filename: fileName, path: req.file.path }], }; - try { - // Send email await transporter.sendMail(mailOptions); transporter.close(); // Delete the temporary file after sending the email fs.unlinkSync(req.file.path); - res.status(200).json({ message: "Email sent successfully" }); } catch (error) { console.error("Error sending email:", error); @@ -78,7 +65,7 @@ Your cooperation regarding the upcoming examination duties is highly solicited.` } }); -// Route to send emails with PDF attachments +// Route to send emails with single PDF attachment remains intact router.post("/pdf", upload.single("pdfFile"), async (req, res) => { const { fileName, recipientEmail, role, courseName } = req.body; @@ -86,22 +73,6 @@ router.post("/pdf", upload.single("pdfFile"), async (req, res) => { return res.status(400).json({ error: "Missing required fields or file" }); } - // Configure Nodemailer transporter - const transporter = nodemailer.createTransport({ - service: "gmail", - auth: { - user: "swdc.ate@gmail.com", // Replace with your email - pass: "umlc hbkr dpga iywd", // Replace with your app-specific password - }, - tls: { - rejectUnauthorized: false, - }, - connectionTimeout: 15000, - greetingTimeout: 15000, - socketTimeout: 15000, - }); - - // Email options const mailOptions = { from: "SWDC Admin ", to: recipientEmail, @@ -115,22 +86,14 @@ If you have any queries, please contact the Examination In-charge. Best regards, Examination Department`, - attachments: [ - { - filename: fileName, - path: req.file.path, // Path to the uploaded PDF - }, - ], + attachments: [{ filename: fileName, path: req.file.path }], }; try { - // Send email await transporter.sendMail(mailOptions); transporter.close(); - // Delete the PDF file after sending - // fs.unlinkSync(req.file.path); - + fs.unlinkSync(req.file.path); res.status(200).json({ message: "Email sent successfully" }); } catch (error) { console.error("Error sending email:", error); @@ -138,4 +101,59 @@ Examination Department`, } }); +// ✅ New route clearly added to handle multiple PDF attachments +// ✅ Correct and clean bulk-pdf route clearly stated +router.post("/bulk-pdf", upload.array("pdfFiles"), async (req, res) => { + const { recipientEmail, facultyName, courseName } = req.body; + + if (!recipientEmail || !facultyName || !courseName || !req.files || req.files.length === 0) { + return res.status(400).json({ error: "Missing required fields or files" }); + } + + const attachments = req.files.map((file) => ({ + filename: file.originalname, + path: file.path, + })); + + const mailOptions = { + from: "SWDC Admin ", + to: recipientEmail, + subject: `Appointment Letters - ${courseName}`, + text: `Dear ${facultyName}, + +Please find your appointment letters attached. + +If you have any queries, please contact the Examination In-charge. + +Best regards, +Examination Department`, + attachments, + }; + + try { + await transporter.sendMail(mailOptions); + transporter.close(); + + // ✅ Ensure files are deleted after successful sending + attachments.forEach((attachment) => { + if (fs.existsSync(attachment.path)) { + fs.unlinkSync(attachment.path); + } + }); + + res.status(200).json({ message: "Bulk email sent successfully" }); + } catch (error) { + console.error("Error sending bulk email (backend):", error); + + attachments.forEach((attachment) => { + if (fs.existsSync(attachment.path)) { + fs.unlinkSync(attachment.path); + } + }); + + res.status(500).json({ error: "Failed to send bulk email", details: error.message }); + } +}); + + module.exports = router;