Compare commits

...

4 Commits

Author SHA1 Message Date
Kailasdevdas d0686b67aa feat: add bulk excel data import functionality 2026-04-20 15:29:46 +05:30
Kailasdevdas 740631d376 docs: update readme 2026-04-16 19:50:11 +05:30
Kailasdevdas 39e162f65c feat: use API base URL from env 2026-04-16 19:49:06 +05:30
kailasdevdas 959440e1c6 Merge pull request 'fix:fix in the blog editor' (#12) from fix/blog-view into dev
Reviewed-on: #12
2026-04-16 11:17:23 +00:00
12 changed files with 550 additions and 74 deletions
+59
View File
@@ -0,0 +1,59 @@
**GG-Node-Backend**
## Tech Stack
Runtime: Node.js (ES Modules)
Framework: Express.js (v5.x)
ORM: Prisma (PostgreSQL)
Storage: Bytescale (Image uploads)
Auth: JSON Web Tokens (JWT) & Bcrypt
Email: Postmark
## Project Structure
backend/
├── prisma/
│ └── schema.prisma
├──── src/
│ ├── app.js
│ ├── controllers/
│ ├── middlewares/
│ ├── routes/
│ ├── prisma/
│ └── utils/
├── .env
└── package.json
## Installation & Setup
**1. Prerequisites**
Node.js (v18+)
PostgreSQL Database
**2. Environment Variables**
DATABASE_URL=""
PORT=3000
JWT_SECRET=""
CORS_ALLOWED_ORIGINS=http://localhost:3001 http://localhost:3003 http://localhost:5174 http://localhost:5173
BYTESCALE_SECRET_API_KEY=""
POSTMARK_API_KEY=""
**3. Install Dependencies**
npm install
**4. Database Initialization**
# Generate Prisma Client
npm run generate
# Run migrations to create database tables
npm run migrate
## Scripts
1. npm start: Runs the server in production mode.
2. npm run migrate: Syncs your local database with the current Prisma schema.
3. npm run generate: Regenerates the Prisma Client (run this after schema changes).
4. npx prisma studio: Opens a visual editor to view and manage your database data.
+5 -1
View File
@@ -14,11 +14,15 @@ import inquiryRoutes from "./routes/inquiry.routes.js";
import academicsResearchRoutes from "./routes/academicsResearch.routes.js"; import academicsResearchRoutes from "./routes/academicsResearch.routes.js";
import emailConfigRoutes from "./routes/emailConfig.routes.js"; import emailConfigRoutes from "./routes/emailConfig.routes.js";
import newsMediaRoutes from "./routes/newsMedia.routes.js"; import newsMediaRoutes from "./routes/newsMedia.routes.js";
import importRoutes from "./routes/importRoutes.js";
dotenv.config(); dotenv.config();
const app = express(); const app = express();
app.use(express.json({ limit: "50mb" }));
app.use(express.urlencoded({ limit: "50mb", extended: true }));
const allowedOrigins = process.env.CORS_ALLOWED_ORIGINS const allowedOrigins = process.env.CORS_ALLOWED_ORIGINS
? process.env.CORS_ALLOWED_ORIGINS.split(" ") ? process.env.CORS_ALLOWED_ORIGINS.split(" ")
: ["http://localhost:3001"]; : ["http://localhost:3001"];
@@ -35,7 +39,6 @@ const corsOptions = {
allowedHeaders: "*", allowedHeaders: "*",
}; };
app.use(express.json());
app.use(cors(corsOptions)); app.use(cors(corsOptions));
app.use("/api/departments", departmentRoutes); app.use("/api/departments", departmentRoutes);
@@ -51,6 +54,7 @@ app.use("/api/inquiry", inquiryRoutes);
app.use("/api/academics", academicsResearchRoutes); app.use("/api/academics", academicsResearchRoutes);
app.use("/api/email", emailConfigRoutes); app.use("/api/email", emailConfigRoutes);
app.use("/api/newsMedia", newsMediaRoutes); app.use("/api/newsMedia", newsMediaRoutes);
app.use("/api/import", importRoutes);
const PORT = process.env.PORT || 3000; const PORT = process.env.PORT || 3000;
app.listen(PORT, () => { app.listen(PORT, () => {
+267
View File
@@ -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 });
}
};
+9
View File
@@ -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;
+5
View File
@@ -22,3 +22,8 @@ dist-ssr
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
#env files
.env
.env.*.local
+36 -62
View File
@@ -1,73 +1,47 @@
# React + TypeScript + Vite **GG-Dashboard**
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. ## Tech Stack
Currently, two official plugins are available: Framework: React 19
Build Tool: Vite + TypeScript
Styling: Tailwind CSS 4 + shadcn/ui
Rich Text: Editor.js
State/Fetch: Axios + React Hooks
Export: XLSX + File-saver
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh ## Project Structure
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## React Compiler frontend/
├── src/
│ ├── api/
│ ├── assets/
│ ├── components/
│ ├── context/
│ ├── lib/
│ ├── layout/
│ ├── pages/
│ ├── services/
│ ├── utils/
│ └── App.tsx
├── .env
├── index.html
└── package.json
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). ## Installation & Setup
## Expanding the ESLint configuration **1. Prerequisites**
Node.js (v20+)
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: **2. Environment Variables**
VITE_API_URL="http://localhost:3000/api"
```js **3. Install Dependencies**
export default defineConfig([ npm install
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this **4. Development**
tseslint.configs.recommendedTypeChecked, npm run dev
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs... ## Scripts
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: npm run dev: Starts the Vite development server with Hot Module Replacement.
npm run build: Compiles TypeScript and builds the production-ready assets.
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
+2
View File
@@ -21,6 +21,7 @@ import InquiryPage from "./pages/inquiry";
import AcademicsPage from "./pages/Academics"; import AcademicsPage from "./pages/Academics";
import NewsPage from "./pages/newsMedia"; import NewsPage from "./pages/newsMedia";
import BlogDetail from "./pages/BlogDetails"; import BlogDetail from "./pages/BlogDetails";
import ImportData from "./pages/ImportData";
export default function App() { export default function App() {
return ( return (
@@ -46,6 +47,7 @@ export default function App() {
<Route path="/inquiry" element={<InquiryPage />} /> <Route path="/inquiry" element={<InquiryPage />} />
<Route path="/academics" element={<AcademicsPage />} /> <Route path="/academics" element={<AcademicsPage />} />
<Route path="/news" element={<NewsPage />} /> <Route path="/news" element={<NewsPage />} />
<Route path="/import" element={<ImportData />} />
</Route> </Route>
</Route> </Route>
+2 -2
View File
@@ -1,10 +1,10 @@
import axios from "axios"; import axios from "axios";
import type {InternalAxiosRequestConfig} from "axios"; import type {InternalAxiosRequestConfig} from "axios";
const BASE_URL: string = "http://localhost:3000/api"; const baseURL: string = import.meta.env.VITE_API_URL;
const apiClient = axios.create({ const apiClient = axios.create({
baseURL: BASE_URL, baseURL: baseURL,
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
@@ -14,6 +14,7 @@ export function BytescaleUploader({
onChange, onChange,
folderPath, folderPath,
}: BytescaleUploaderProps) { }: BytescaleUploaderProps) {
const baseURL = import.meta.env.VITE_API_URL;
const [isUploading, setIsUploading] = useState(false); const [isUploading, setIsUploading] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
@@ -33,15 +34,11 @@ export function BytescaleUploader({
formData.append("folderPath", folderPath); formData.append("folderPath", folderPath);
try { try {
const response = await axios.post( const response = await axios.post(`${baseURL}/upload`, formData, {
"http://localhost:3000/api/upload",
formData,
{
headers: { headers: {
"Content-Type": "multipart/form-data", "Content-Type": "multipart/form-data",
}, },
}, });
);
const {fileUrl} = response.data; const {fileUrl} = response.data;
onChange(fileUrl); onChange(fileUrl);
+158
View File
@@ -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<boolean>(false);
const [status, setStatus] = useState<string>("");
const handleFileUpload = (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
setLoading(true);
setStatus("Reading Excel file...");
const reader = new FileReader();
reader.onload = async (evt: ProgressEvent<FileReader>) => {
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 (
<div style={containerStyle}>
<div style={cardStyle}>
<h2 style={{ color: "#333", marginBottom: "10px" }}>
Database Bulk Import
</h2>
<p style={{ color: "#666", marginBottom: "30px" }}>
Select the <b>gg_hospital.xlsx</b> file. This will update all tables.
</p>
<div style={{ marginBottom: "20px" }}>
<input
type="file"
accept=".xlsx, .xls"
onChange={handleFileUpload}
id="excel-upload"
style={{ display: "none" }}
disabled={loading}
/>
<label
htmlFor="excel-upload"
style={{
...buttonStyle,
backgroundColor: loading ? "#a0aec0" : "#3182ce",
cursor: loading ? "not-allowed" : "pointer",
}}
>
{loading ? "⌛ Processing..." : "📂 Choose Excel File"}
</label>
</div>
{status && (
<div
style={{
marginTop: "25px",
padding: "15px",
borderRadius: "8px",
backgroundColor: status.includes("✅") ? "#f0fff4" : "#fff5f5",
color: status.includes("✅") ? "#2f855a" : "#c53030",
border: `1px solid ${status.includes("✅") ? "#c6f6d5" : "#fed7d7"}`,
fontWeight: "500",
whiteSpace: "pre-wrap",
}}
>
{status}
</div>
)}
</div>
</div>
);
};
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;
+1 -1
View File
@@ -1,7 +1,7 @@
import axios from "axios"; import axios from "axios";
const api = axios.create({ const api = axios.create({
baseURL: "http://localhost:3000/api", baseURL: import.meta.env.VITE_API_URL,
}); });
api.interceptors.request.use((config) => { api.interceptors.request.use((config) => {
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />