From d0686b67aae77f90e850192b86cf517458e35685 Mon Sep 17 00:00:00 2001 From: Kailasdevdas Date: Mon, 20 Apr 2026 15:29:46 +0530 Subject: [PATCH] feat: add bulk excel data import functionality --- backend/src/app.js | 6 +- backend/src/controllers/importController.js | 267 ++++++++++++++++++++ backend/src/routes/importRoutes.js | 9 + frontend/src/App.tsx | 2 + frontend/src/pages/ImportData.tsx | 158 ++++++++++++ 5 files changed, 441 insertions(+), 1 deletion(-) create mode 100644 backend/src/controllers/importController.js create mode 100644 backend/src/routes/importRoutes.js create mode 100644 frontend/src/pages/ImportData.tsx diff --git a/backend/src/app.js b/backend/src/app.js index 7efc6a8..16b3d26 100644 --- a/backend/src/app.js +++ b/backend/src/app.js @@ -14,11 +14,15 @@ import inquiryRoutes from "./routes/inquiry.routes.js"; import academicsResearchRoutes from "./routes/academicsResearch.routes.js"; import emailConfigRoutes from "./routes/emailConfig.routes.js"; import newsMediaRoutes from "./routes/newsMedia.routes.js"; +import importRoutes from "./routes/importRoutes.js"; dotenv.config(); const app = express(); +app.use(express.json({ limit: "50mb" })); +app.use(express.urlencoded({ limit: "50mb", extended: true })); + const allowedOrigins = process.env.CORS_ALLOWED_ORIGINS ? process.env.CORS_ALLOWED_ORIGINS.split(" ") : ["http://localhost:3001"]; @@ -35,7 +39,6 @@ const corsOptions = { allowedHeaders: "*", }; -app.use(express.json()); app.use(cors(corsOptions)); app.use("/api/departments", departmentRoutes); @@ -51,6 +54,7 @@ app.use("/api/inquiry", inquiryRoutes); app.use("/api/academics", academicsResearchRoutes); app.use("/api/email", emailConfigRoutes); app.use("/api/newsMedia", newsMediaRoutes); +app.use("/api/import", importRoutes); const PORT = process.env.PORT || 3000; app.listen(PORT, () => { diff --git a/backend/src/controllers/importController.js b/backend/src/controllers/importController.js new file mode 100644 index 0000000..18b9b54 --- /dev/null +++ b/backend/src/controllers/importController.js @@ -0,0 +1,267 @@ +import { PrismaClient } from "@prisma/client"; + +const prisma = new PrismaClient(); + +export const bulkImportExcelData = async (req, res) => { + try { + const { + departments, + doctors, + timings, + careers, + inquiries, + academics, + appointments, + candidates, + news, + } = req.body; + + console.log("🚀 Starting Robust Data Import..."); + + // 1. DEPARTMENTS + if (departments) { + for (const row of departments) { + if (!row.SL_NO) continue; + await prisma.department.upsert({ + where: { departmentId: row.SL_NO.toString() }, + update: { + name: row.Department?.toString(), + para1: row.para1?.toString() || null, + para2: row.para2?.toString() || null, + para3: row.para3?.toString() || null, + facilities: row.facilities?.toString() || null, + services: row.services?.toString() || null, + }, + create: { + departmentId: row.SL_NO.toString(), + name: row.Department?.toString(), + para1: row.para1?.toString() || null, + para2: row.para2?.toString() || null, + para3: row.para3?.toString() || null, + facilities: row.facilities?.toString() || null, + services: row.services?.toString() || null, + }, + }); + } + } + + // 2. DOCTORS + if (doctors) { + for (const row of doctors) { + if (!row.GG_ID) continue; + const doctor = await prisma.doctor.upsert({ + where: { doctorId: row.GG_ID.toString() }, + update: { + name: row.Name?.toString(), + designation: row.Designation?.toString() || null, + workingStatus: row["Working Status"]?.toString() || null, + qualification: row.Qualification?.toString() || null, + }, + create: { + doctorId: row.GG_ID.toString(), + name: row.Name?.toString(), + designation: row.Designation?.toString() || null, + workingStatus: row["Working Status"]?.toString() || null, + qualification: row.Qualification?.toString() || null, + }, + }); + + if (row.Department_ID) { + const dept = await prisma.department.findUnique({ + where: { departmentId: row.Department_ID.toString() }, + }); + if (dept) { + await prisma.doctorDepartment.upsert({ + where: { + doctorId_departmentId: { + doctorId: doctor.id, + departmentId: dept.id, + }, + }, + update: {}, + create: { + doctorId: doctor.id, + departmentId: dept.id, + }, + }); + } + } + } + } + + // 3. TIMINGS + if (timings) { + for (const row of timings) { + if (!row.GG_ID) continue; + const doctor = await prisma.doctor.findUnique({ + where: { doctorId: row.GG_ID.toString() }, + include: { departments: true }, + }); + + if (doctor && doctor.departments.length > 0) { + const doctorDeptId = doctor.departments[0].id; + const rawAdd = row.Additional?.toString() || ""; + const rawMon = row.Monday?.toString() || ""; + const isAppt = (val) => /appointment|basis|on call/i.test(val); + + let finalAdd = rawAdd; + if (!finalAdd && isAppt(rawMon)) finalAdd = rawMon; + + await prisma.doctorTiming.upsert({ + where: { doctorDepartmentId: doctorDeptId }, + update: { + monday: isAppt(rawMon) ? null : row.Monday?.toString() || null, + tuesday: row.Tuesday?.toString() || null, + wednesday: row.Wednesday?.toString() || null, + thursday: row.Thursday?.toString() || null, + friday: row.Friday?.toString() || null, + saturday: row.Saturday?.toString() || null, + sunday: row.Sunday?.toString() || null, + additional: finalAdd || null, + }, + create: { + doctorDepartmentId: doctorDeptId, + monday: isAppt(rawMon) ? null : row.Monday?.toString() || null, + tuesday: row.Tuesday?.toString() || null, + wednesday: row.Wednesday || null, + thursday: row.Thursday || null, + friday: row.Friday || null, + saturday: row.Saturday || null, + sunday: row.Sunday || null, + additional: finalAdd || null, + }, + }); + } + } + } + + // 4. CAREERS + if (careers) { + for (const row of careers) { + if (!row.Post) continue; + const cId = row.Id ? parseInt(row.Id) : undefined; + const data = { + post: row.Post?.toString(), + designation: row.Designation?.toString() || null, + qualification: row.Qualification?.toString() || null, + experienceNeed: row.ExperienceNeed?.toString() || null, + email: row.HiringEmail?.toString() || null, + number: row.Number?.toString() || null, + status: row.Status?.toString() || "new", + }; + if (cId) { + await prisma.career.upsert({ + where: { id: cId }, + update: data, + create: { ...data, id: cId }, + }); + } else { + await prisma.career.create({ data }); + } + } + } + + // 5. INQUIRIES + if (inquiries) { + for (const row of inquiries) { + if (!row.FullName) continue; + await prisma.inquiry.create({ + data: { + fullName: row.FullName.toString(), + number: row.Number?.toString() || "", + emailId: row.EmailId?.toString() || null, + subject: row.Subject?.toString() || null, + message: row.Message?.toString() || null, + createdAt: row.Date ? new Date(row.Date) : new Date(), + }, + }); + } + } + + // 6. ACADEMICS & RESEARCH (FIXED HERE) + if (academics) { + for (const row of academics) { + if (!row.FullName) continue; + await prisma.academicsResearch.create({ + data: { + fullName: row.FullName.toString(), + number: row.Number?.toString() || "", + emailId: row.EmailId?.toString() || null, + subject: row.Subject?.toString() || null, // Force String + courseName: row["Course Name"]?.toString() || null, + message: row.Message?.toString() || null, + createdAt: row.Date ? new Date(row.Date) : new Date(), + }, + }); + } + } + + // 7. APPOINTMENTS + if (appointments) { + for (const row of appointments) { + if (!row.FullName) continue; + const docId = row.Doctor?.toString(); + const deptId = row["Department Id"]?.toString(); + if (docId && deptId) { + await prisma.appointment + .create({ + data: { + name: row.FullName.toString(), + mobileNumber: row.Number?.toString() || "", + email: row["Email Id"]?.toString() || null, + message: row.Message?.toString() || null, + date: row.Date ? new Date(row.Date) : new Date(), + doctorId: docId, + departmentId: deptId, + }, + }) + .catch(() => {}); + } + } + } + + // 8. CANDIDATES + if (candidates) { + for (const row of candidates) { + if (!row.FullName || !row.CareerId) continue; + await prisma.candidate + .create({ + data: { + fullName: row.FullName.toString(), + mobile: row.Number?.toString() || "", + email: row.EmailId?.toString() || "", + subject: row.Subject?.toString() || "", + coverLetter: row["Cover Letter"]?.toString() || "", + careerId: parseInt(row.CareerId), + createdAt: row.Date ? new Date(row.Date) : new Date(), + }, + }) + .catch(() => {}); + } + } + + // 9. NEWS & MEDIA + if (news) { + for (const row of news) { + if (!row.Headline) continue; + await prisma.newsMedia.create({ + data: { + headline: row.Headline.toString(), + content: row.Content?.toString() || null, + firstPara: row.FirstPara?.toString() || null, + secondPara: row.SecondPara?.toString() || null, + author: row.Author?.toString() || null, + date: row.Date ? new Date(row.Date) : null, + }, + }); + } + } + + res + .status(200) + .json({ success: true, message: "✅ Import completed successfully!" }); + } catch (error) { + console.error("❌ Bulk Import Error:", error); + res.status(500).json({ success: false, error: error.message }); + } +}; diff --git a/backend/src/routes/importRoutes.js b/backend/src/routes/importRoutes.js new file mode 100644 index 0000000..32d6033 --- /dev/null +++ b/backend/src/routes/importRoutes.js @@ -0,0 +1,9 @@ +import express from "express"; +import { bulkImportExcelData } from "../controllers/importController.js"; +import jwtAuthMiddleware from "../middleware/auth.js"; + +const router = express.Router(); + +router.post("/bulk", jwtAuthMiddleware, bulkImportExcelData); + +export default router; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 1838c21..a57396c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -21,6 +21,7 @@ import InquiryPage from "./pages/inquiry"; import AcademicsPage from "./pages/Academics"; import NewsPage from "./pages/newsMedia"; import BlogDetail from "./pages/BlogDetails"; +import ImportData from "./pages/ImportData"; export default function App() { return ( @@ -46,6 +47,7 @@ export default function App() { } /> } /> } /> + } /> diff --git a/frontend/src/pages/ImportData.tsx b/frontend/src/pages/ImportData.tsx new file mode 100644 index 0000000..28a5881 --- /dev/null +++ b/frontend/src/pages/ImportData.tsx @@ -0,0 +1,158 @@ +import React, { useState, ChangeEvent } from "react"; +import * as XLSX from "xlsx"; +import apiClient from "@/api/client"; + +interface ImportPayload { + departments: any[]; + doctors: any[]; + timings: any[]; + careers: any[]; + inquiries: any[]; + academics: any[]; + appointments: any[]; + candidates: any[]; + news: any[]; +} + +const ImportData: React.FC = () => { + const [loading, setLoading] = useState(false); + const [status, setStatus] = useState(""); + + const handleFileUpload = (e: ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + setLoading(true); + setStatus("Reading Excel file..."); + + const reader = new FileReader(); + reader.onload = async (evt: ProgressEvent) => { + try { + const bstr = evt.target?.result; + if (!bstr) throw new Error("Failed to read file content."); + + const wb = XLSX.read(bstr, { type: "binary" }); + + const payload: ImportPayload = { + departments: XLSX.utils.sheet_to_json(wb.Sheets["Departments"]) || [], + doctors: XLSX.utils.sheet_to_json(wb.Sheets["Doctors"]) || [], + timings: XLSX.utils.sheet_to_json(wb.Sheets["Doctor Timings"]) || [], + careers: XLSX.utils.sheet_to_json(wb.Sheets["Careers"]) || [], + inquiries: XLSX.utils.sheet_to_json(wb.Sheets["Inquiry"]) || [], + academics: + XLSX.utils.sheet_to_json(wb.Sheets["Academics & Research"]) || [], + appointments: + XLSX.utils.sheet_to_json(wb.Sheets["Appointment"]) || [], + candidates: XLSX.utils.sheet_to_json(wb.Sheets["Candidate"]) || [], + news: XLSX.utils.sheet_to_json(wb.Sheets["News & Media"]) || [], + }; + + setStatus("Uploading data to server (this may take a moment)..."); + + const response = await apiClient.post("/import/bulk", payload); + + if (response.status === 200) { + setStatus("✅ ALL DATA IMPORT COMPLETED SUCCESSFULLY!"); + } else { + setStatus("❌ Server responded with an error."); + } + } catch (err: any) { + console.error("Import Error:", err); + const errorMsg = err.response?.data?.error || "Error processing file."; + setStatus(`❌ ${errorMsg}`); + } finally { + setLoading(false); + if (e.target) e.target.value = ""; + } + }; + + reader.onerror = () => { + setStatus("❌ Failed to read the file."); + setLoading(false); + }; + + reader.readAsBinaryString(file); + }; + + return ( +
+
+

+ Database Bulk Import +

+

+ Select the gg_hospital.xlsx file. This will update all tables. +

+ +
+ + +
+ + {status && ( +
+ {status} +
+ )} +
+
+ ); +}; + +const containerStyle: React.CSSProperties = { + display: "flex", + justifyContent: "center", + alignItems: "center", + minHeight: "80vh", + backgroundColor: "#f7fafc", + fontFamily: "'Segoe UI', Tahoma, Geneva, Verdana, sans-serif", +}; + +const cardStyle: React.CSSProperties = { + backgroundColor: "white", + padding: "40px", + borderRadius: "12px", + boxShadow: "0 4px 6px rgba(0,0,0,0.1)", + maxWidth: "500px", + width: "100%", + textAlign: "center", +}; + +const buttonStyle: React.CSSProperties = { + padding: "12px 24px", + color: "white", + borderRadius: "6px", + fontSize: "16px", + fontWeight: "bold", + transition: "all 0.2s ease", + display: "inline-block", +}; + +export default ImportData; -- 2.43.0