Compare commits

..

1 Commits

Author SHA1 Message Date
ARJUN S THAMPI c6a041ac10 feat: add pagination in appointment 2026-03-27 12:09:46 +05:30
180 changed files with 13091 additions and 22139 deletions
-34
View File
@@ -1,34 +0,0 @@
# Include any files or directories that you don't want to be copied to your
# container here (e.g., local build artifacts, temporary files, etc.).
#
# For more help, visit the .dockerignore file reference guide at
# https://docs.docker.com/go/build-context-dockerignore/
**/.classpath
**/.dockerignore
# **/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/.next
**/.cache
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/charts
**/docker-compose*
**/compose.y*ml
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
**/build
**/dist
LICENSE
README.md
-2
View File
@@ -1,2 +0,0 @@
.env
node_modules
-14
View File
@@ -1,14 +0,0 @@
node_modules
dist
build
coverage
.next
out
*.log
backend/node_modules
backend/dist
frontend/node_modules
frontend/build
frontend/dist
-13
View File
@@ -1,13 +0,0 @@
{
"printWidth": 120,
"useTabs": true,
"tabWidth": 2,
"trailingComma": "es5",
"semi": true,
"singleQuote": true,
"bracketSpacing": true,
"arrowParens": "always",
"jsxSingleQuote": false,
"bracketSameLine": false,
"endOfLine": "lf"
}
-4
View File
@@ -1,4 +0,0 @@
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
}
-105
View File
@@ -1,105 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Repository layout
Monorepo with two independent npm packages plus a shared Docker dev stack:
- `backend/` — Node.js + Express 5 (ESM) API, Prisma ORM over PostgreSQL.
- `frontend/` — React 19 + Vite + TypeScript admin dashboard, styled with Tailwind 4 + shadcn/ui.
- `docker/` — Dev Dockerfiles + `entrypoint.sh` (used by `docker-compose.dev.yml` at repo root).
There is no root-level package script — run npm commands inside `backend/` or `frontend/`.
## Common commands
### Docker (full stack)
```bash
docker compose -f docker-compose.dev.yml up --build # backend :5008, frontend :3008, postgres :5432 (internal)
docker compose -f docker-compose.dev.yml down
```
The backend container's `entrypoint.sh` runs `prisma generate` then `prisma db push` (NOT `migrate deploy`) on every start — schema changes propagate without a migration step in the Docker dev flow.
### Backend (`cd backend`)
```bash
npm run dev # nodemon src/app.js
npm start # node src/app.js
npm run migrate # npx prisma migrate dev (use this locally; Docker uses db push)
npm run generate # npx prisma generate
npx prisma studio # GUI for the DB
npm run create-user <username> <password> [role] # role defaults to "admin"
```
In Docker, create an admin via:
```bash
docker exec -it gg-backend-api-backend-1 node src/utils/createUser.js <name> <password> <role>
```
### Frontend (`cd frontend`)
```bash
npm run dev # Vite dev server
npm run build # tsc -b && vite build
npm run lint # eslint .
npm run preview
```
There is no test runner configured in either package.
## Architecture
### Backend
Single Express app assembled in `backend/src/app.js`. Each domain follows the same triplet:
- `src/routes/<domain>.routes.js` — declares the Express router, applies `jwtAuthMiddleware` per-route (mixed public/protected within the same router; see `blog.routes.js` for the pattern: list/detail public, admin endpoints + writes protected).
- `src/controllers/<domain>.controller.js` — handler logic; uses Prisma directly.
- Mounted at `/api/<domain>` in `app.js`.
Domains: `departments`, `auth`, `blogs`, `upload`, `doctors`, `careers`, `candidates`, `appointments`, `inquiry`, `academics`, `email`, `newsMedia`, `import`. Static `uploads/` is served at `/uploads`.
Cross-cutting pieces:
- **Prisma client** — `src/prisma/client.js` exports a singleton. Always import from there rather than instantiating `new PrismaClient()` (the import controller currently instantiates its own — match the singleton pattern when adding new code).
- **Auth** — `src/middleware/auth.js` expects `Authorization: Bearer <jwt>`, attaches the decoded payload to `req.user`. Tokens are issued by `src/utils/jwt.js` using `JWT_SECRET`. Passwords hashed via `src/utils/password.js` (bcrypt).
- **Email** — `src/utils/sendEmail.js` wraps Postmark with `EMAIL_FROM` as sender. Recipient lists are looked up by category via `getEmailsByType(type)` against the `EmailConfig` table — controllers call this to fan out notifications (e.g. on new appointments/inquiries).
- **CORS** — `CORS_ALLOWED_ORIGINS` is a **space-separated** list (not comma-separated). Requests with no `Origin` header are allowed.
- **Body limits** — JSON/urlencoded bodies are capped at 50mb to support image-laden Editor.js payloads and bulk Excel imports.
### Data model (Prisma — `backend/prisma/schema.prisma`)
Note the natural-key relations: `Appointment.doctorId` references `Doctor.doctorId` (the string business ID), not `Doctor.id`. Same for `Department`. The `SL_NO` / `GG_ID` columns from imports become these business IDs. `Doctor``Department` is many-to-many through `DoctorDepartment`, which has a 1:1 `DoctorTiming` for weekday schedules. `NewsMedia` has cascading `NewsImage` children.
### Frontend
- Routing in `src/App.tsx`: `PublicRoute` wraps `/` (Login); everything else is wrapped by `ProtectedRoute``DashboardLayout`. Unknown paths redirect to `/department`.
- `src/context/AuthContext.tsx` is the auth source of truth; `src/services/api.ts` is the shared Axios instance that auto-injects `Authorization: Bearer <token>` from `localStorage.getItem("token")`.
- `src/api/<domain>.ts` files are thin wrappers around that axios client, one per backend domain — keep this pattern when adding endpoints.
- Path alias `@/*``src/*` (configured in both `vite.config.ts` and `tsconfig`).
- File uploads go through Bytescale (`src/components/BytescaleUploader`) — server only stores the resulting URL.
- The `/import` page parses Excel via `xlsx` and POSTs structured payloads to `/api/import`, where `importController.js` upserts across multiple tables. When changing import behavior, the frontend's column names (`SL_NO`, `GG_ID`, `Department`, `Working Status`, etc.) and the controller's destructured keys (`departments`, `doctors`, `timings`, …) must stay in sync.
## Environment variables
`backend/.env`:
```
DATABASE_URL=postgresql://user:password@db:5432/mydb
PORT=5008
JWT_SECRET=...
CORS_ALLOWED_ORIGINS=http://localhost:5173 # space-separated for multiple
BYTESCALE_SECRET_API_KEY=...
POSTMARK_API_KEY=...
EMAIL_FROM=admin@example.com
```
`frontend/.env`:
```
VITE_API_URL=http://localhost:5008/api
```
Vendored
-221
View File
@@ -1,221 +0,0 @@
// Single source of truth for env injection.
// Adding/removing a variable means editing one `keys` list — nothing else.
// `path` is the Vault KV path; `file` is the output path relative to ${WORKSPACE}.
def ENV_LAYOUT = [
[path: 'ggh-dev/root', file: '.env', keys: [
'POSTGRES_DB', 'POSTGRES_PASSWORD', 'POSTGRES_USER',
]],
[path: 'ggh-dev/backend', file: 'backend/.env', keys: [
'BYTESCALE_SECRET_API_KEY',
'CORS_ALLOWED_ORIGINS',
'DATABASE_URL',
'EMAIL_FROM',
'JWT_SECRET',
'PORT',
'POSTMARK_API_KEY',
]],
[path: 'ggh-dev/dashboard-frontend', file: 'frontend/.env', keys: [
'VITE_API_URL',
]],
]
pipeline {
agent { label 'jagent06' }
options {
disableConcurrentBuilds()
}
triggers {
GenericTrigger(
causeString: 'Triggered by Gitea webhook',
tokenCredentialId: 'ggh-dev',
printContributedVariables: true,
printPostContent: true
)
}
environment {
DOCKER_COMPOSE_FILE = "${WORKSPACE}/docker-compose.dev.yml"
DOCKER_BUILDKIT = '1'
COMPOSE_DOCKER_CLI_BUILD = '1'
}
stages {
stage('Checkout Code') {
steps {
checkout([
$class: 'GitSCM',
branches: [[name: '*/dev']],
userRemoteConfigs: [[
url: 'https://gt.mgsigma.net/GGH/gg-backend.git',
credentialsId: 'SYS_BOT_PAT'
]]
])
}
}
stage('Inject .env from Vault') {
steps {
script {
def vaultSecrets = ENV_LAYOUT.collect { layer ->
[
path: layer.path,
engineVersion: 2,
secretValues: layer.keys.collect { k ->
[vaultKey: k, envVar: k]
}
]
}
def writtenFiles = ENV_LAYOUT.collect { it.file }.join(', ')
withVault(
configuration: [
vaultUrl: 'https://vault.mgsigma.net',
vaultCredentialId: 'vault-cicd-01'
],
vaultSecrets: vaultSecrets
) {
// Single-quoted sh + withEnv keeps secret names out of the
// Groovy GString. Values are read at shell runtime via
// `printenv`, so Jenkins's static analyzer has nothing to flag.
ENV_LAYOUT.each { layer ->
withEnv([
"ENV_FILE=${layer.file}",
"ENV_KEYS=${layer.keys.join(' ')}"
]) {
sh '''
set +x
umask 077
mkdir -p "$(dirname "$WORKSPACE/$ENV_FILE")"
: > "$WORKSPACE/$ENV_FILE"
for k in $ENV_KEYS; do
printf '%s=%s\n' "$k" "$(printenv "$k")" >> "$WORKSPACE/$ENV_FILE"
done
'''
}
}
echo "[INFO] env files written: ${writtenFiles}"
}
}
}
}
stage('Deploy') {
steps {
script {
if (!fileExists(env.DOCKER_COMPOSE_FILE)) {
error "docker-compose.dev.yml not found at ${env.DOCKER_COMPOSE_FILE}"
}
}
sh '''
set -e
if [ -n "$(docker compose -f docker-compose.dev.yml ps -q 2>/dev/null)" ]; then
echo "[INFO] Stopping existing containers..."
docker compose -f docker-compose.dev.yml down --remove-orphans
else
echo "[INFO] No running containers — skipping stop."
fi
# Dangling-only prune — keeps tagged images and BuildKit cache so
# subsequent builds reuse layers. Do NOT use `docker system prune -af`.
docker image prune -f >/dev/null 2>&1 || true
echo "[INFO] Building and starting services..."
docker compose -f docker-compose.dev.yml up -d --build --remove-orphans
'''
}
}
}
post {
always {
script {
env.BUILD_INFO_HASH = sh(
script: "git rev-parse --short HEAD 2>/dev/null || echo N/A",
returnStdout: true
).trim()
env.BUILD_INFO_AUTHOR = sh(
script: "git log -1 --pretty=format:'%an' 2>/dev/null || echo N/A",
returnStdout: true
).trim()
env.BUILD_INFO_MESSAGE = sh(
script: "git log -1 --pretty=format:'%s' 2>/dev/null || echo 'No commit message found'",
returnStdout: true
).trim()
def cause = currentBuild.getBuildCauses()?.getAt(0)?.shortDescription ?: 'Unknown'
env.BUILD_INFO_TRIGGER = cause.contains('Started by user') ? 'Manual trigger' : cause
}
}
success {
script {
emailext(
subject: "[DEV] ${env.JOB_NAME} #${env.BUILD_NUMBER} succeeded",
body: renderEmail('Build succeeded', '#16a34a', buildInfo()),
mimeType: 'text/html',
to: 'admin@msigmagokulam.com, ashir@mgsigma.net, kailasdevdas@msigmagokulam.com'
)
}
}
failure {
script {
emailext(
subject: "[DEV] ${env.JOB_NAME} #${env.BUILD_NUMBER} failed",
body: renderEmail('Build failed', '#dc2626', buildInfo()),
mimeType: 'text/html',
to: 'admin@msigmagokulam.com, ashir@mgsigma.net, kailasdevdas@msigmagokulam.com'
)
}
}
}
}
def buildInfo() {
return [
hash: env.BUILD_INFO_HASH ?: 'N/A',
author: env.BUILD_INFO_AUTHOR ?: 'N/A',
message: env.BUILD_INFO_MESSAGE ?: 'N/A',
trigger: env.BUILD_INFO_TRIGGER ?: 'Unknown',
duration: currentBuild.durationString.replace(' and counting', '')
]
}
def renderEmail(String label, String accent, Map info) {
return """\
<!DOCTYPE html>
<html>
<body style="margin:0; padding:0; background-color:#f6f7f9;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" style="background-color:#f6f7f9; padding:40px 16px;">
<tr><td align="center">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="560" style="max-width:560px; background:#ffffff; border-radius:12px; overflow:hidden; box-shadow:0 1px 3px rgba(15,23,42,0.08); font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;">
<tr><td style="height:3px; background:${accent};"></td></tr>
<tr><td style="padding:32px 40px 0 40px;">
<div style="font-size:12px; font-weight:600; color:${accent}; letter-spacing:0.8px; text-transform:uppercase; margin-bottom:6px;">${label} · development</div>
<div style="font-size:20px; font-weight:600; color:#0f172a; line-height:1.35;">${env.JOB_NAME}<span style="color:#94a3b8; font-weight:400;"> &middot; #${env.BUILD_NUMBER}</span></div>
</td></tr>
<tr><td style="padding:20px 40px 8px 40px;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" style="font-size:14px; color:#0f172a;">
<tr><td width="110" style="padding:7px 0; color:#64748b;">Commit</td><td style="padding:7px 0; font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; font-size:13px;">${info.hash}</td></tr>
<tr><td style="padding:7px 0; color:#64748b;">Author</td><td style="padding:7px 0;">${info.author}</td></tr>
<tr><td style="padding:7px 0; color:#64748b; vertical-align:top;">Message</td><td style="padding:7px 0;">${info.message}</td></tr>
<tr><td style="padding:7px 0; color:#64748b;">Triggered by</td><td style="padding:7px 0;">${info.trigger}</td></tr>
<tr><td style="padding:7px 0; color:#64748b;">Duration</td><td style="padding:7px 0;">${info.duration}</td></tr>
</table>
</td></tr>
<tr><td style="padding:20px 40px 32px 40px;">
<a href="${env.BUILD_URL}" style="display:inline-block; padding:10px 18px; background:#0f172a; color:#ffffff; text-decoration:none; border-radius:8px; font-size:14px; font-weight:500;">View build</a>
<a href="${env.BUILD_URL}console" style="display:inline-block; padding:10px 14px; color:#475569; text-decoration:none; font-size:14px; font-weight:500;">Console output &rarr;</a>
</td></tr>
<tr><td style="padding:14px 40px; background:#f8fafc; border-top:1px solid #e2e8f0; font-size:12px; color:#94a3b8;">
Automated notification from Jenkins
</td></tr>
</table>
</td></tr>
</table>
</body>
</html>
"""
}
-224
View File
@@ -1,224 +0,0 @@
// Single source of truth for env injection.
// Adding/removing a variable means editing one `keys` list — nothing else.
// `path` is the Vault KV path; `file` is the output path relative to ${WORKSPACE}.
def ENV_LAYOUT = [
[path: 'ggh-prod/root', file: '.env', keys: [
'POSTGRES_DB', 'POSTGRES_PASSWORD', 'POSTGRES_USER',
]],
[path: 'ggh-prod/backend', file: 'backend/.env', keys: [
'BYTESCALE_SECRET_API_KEY',
'CORS_ALLOWED_ORIGINS',
'DATABASE_URL',
'EMAIL_FROM',
'JWT_SECRET',
'PORT',
'POSTMARK_API_KEY',
]],
[path: 'ggh-prod/dashboard-frontend', file: 'frontend/.env', keys: [
'VITE_API_URL',
]],
]
pipeline {
agent {
label 'jagent06'
}
options {
disableConcurrentBuilds()
}
environment {
REMOTE_HOST = "root@170.187.237.83"
REMOTE_DEPLOY_DIR = "/root/gg-backend"
SSH_CREDENTIALS_ID = "ssh_id_ed25519"
COMPOSE_FILE = "docker-compose.prod.yml"
}
stages {
stage('Checkout Code') {
steps {
git credentialsId: 'SYS_BOT_PAT', url: 'https://gt.mgsigma.net/GGH/gg-backend.git', branch: 'main'
}
}
stage('Inject .env from Vault') {
steps {
script {
def vaultSecrets = ENV_LAYOUT.collect { layer ->
[
path: layer.path,
engineVersion: 2,
secretValues: layer.keys.collect { k ->
[vaultKey: k, envVar: k]
}
]
}
def writtenFiles = ENV_LAYOUT.collect { it.file }.join(', ')
withVault(
configuration: [
vaultUrl: 'https://vault.mgsigma.net',
vaultCredentialId: 'vault-cicd-01'
],
vaultSecrets: vaultSecrets
) {
// Single-quoted sh + withEnv keeps secret names out of the
// Groovy GString. Values are read at shell runtime via
// `printenv`, so Jenkins's static analyzer has nothing to flag.
ENV_LAYOUT.each { layer ->
withEnv([
"ENV_FILE=${layer.file}",
"ENV_KEYS=${layer.keys.join(' ')}"
]) {
sh '''
set +x
umask 077
mkdir -p "$(dirname "$WORKSPACE/$ENV_FILE")"
: > "$WORKSPACE/$ENV_FILE"
for k in $ENV_KEYS; do
printf '%s=%s\n' "$k" "$(printenv "$k")" >> "$WORKSPACE/$ENV_FILE"
done
'''
}
}
echo "[INFO] env files written: ${writtenFiles}"
}
}
}
}
stage('Sync sources to prod VPS') {
steps {
sshagent (credentials: ["${env.SSH_CREDENTIALS_ID}"]) {
sh """
ssh -o StrictHostKeyChecking=no $REMOTE_HOST 'mkdir -p $REMOTE_DEPLOY_DIR'
rsync -az --delete \\
--exclude='.git/' \\
--exclude='node_modules/' \\
--exclude='backend/node_modules/' \\
--exclude='frontend/node_modules/' \\
--exclude='frontend/dist/' \\
-e "ssh -o StrictHostKeyChecking=no" \\
./ $REMOTE_HOST:$REMOTE_DEPLOY_DIR/
"""
}
}
}
stage('Deploy on prod VPS') {
steps {
sshagent (credentials: ["${env.SSH_CREDENTIALS_ID}"]) {
sh """
ssh -o StrictHostKeyChecking=no $REMOTE_HOST '
set -e
cd $REMOTE_DEPLOY_DIR
if [ -n "\$(docker compose -f $COMPOSE_FILE ps -q 2>/dev/null)" ]; then
echo "[INFO] Stopping existing containers..."
docker compose -f $COMPOSE_FILE down --remove-orphans
else
echo "[INFO] No running containers — skipping stop."
fi
docker image prune -f >/dev/null 2>&1 || true
echo "[INFO] Building and starting services..."
docker compose -f $COMPOSE_FILE up -d --build --remove-orphans
'
"""
}
}
}
}
post {
always {
script {
env.BUILD_INFO_HASH = sh(
script: "git rev-parse --short HEAD 2>/dev/null || echo N/A",
returnStdout: true
).trim()
env.BUILD_INFO_AUTHOR = sh(
script: "git log -1 --pretty=format:'%an' 2>/dev/null || echo N/A",
returnStdout: true
).trim()
env.BUILD_INFO_MESSAGE = sh(
script: "git log -1 --pretty=format:'%s' 2>/dev/null || echo 'No commit message found'",
returnStdout: true
).trim()
def cause = currentBuild.getBuildCauses()?.getAt(0)?.shortDescription ?: 'Unknown'
env.BUILD_INFO_TRIGGER = cause.contains('Started by user') ? 'Manual trigger' : cause
}
}
success {
script {
emailext(
subject: "[PROD] ${env.JOB_NAME} #${env.BUILD_NUMBER} succeeded",
body: renderEmail('Build succeeded', '#16a34a', buildInfo()),
mimeType: 'text/html',
to: 'admin@msigmagokulam.com, ashir@mgsigma.net, kailasdevdas@msigmagokulam.com'
)
}
}
failure {
script {
emailext(
subject: "[PROD] ${env.JOB_NAME} #${env.BUILD_NUMBER} failed",
body: renderEmail('Build failed', '#dc2626', buildInfo()),
mimeType: 'text/html',
to: 'admin@msigmagokulam.com, ashir@mgsigma.net, kailasdevdas@msigmagokulam.com'
)
}
}
}
}
def buildInfo() {
return [
hash: env.BUILD_INFO_HASH ?: 'N/A',
author: env.BUILD_INFO_AUTHOR ?: 'N/A',
message: env.BUILD_INFO_MESSAGE ?: 'N/A',
trigger: env.BUILD_INFO_TRIGGER ?: 'Unknown',
duration: currentBuild.durationString.replace(' and counting', '')
]
}
def renderEmail(String label, String accent, Map info) {
return """\
<!DOCTYPE html>
<html>
<body style="margin:0; padding:0; background-color:#f6f7f9;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" style="background-color:#f6f7f9; padding:40px 16px;">
<tr><td align="center">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="560" style="max-width:560px; background:#ffffff; border-radius:12px; overflow:hidden; box-shadow:0 1px 3px rgba(15,23,42,0.08); font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;">
<tr><td style="height:3px; background:${accent};"></td></tr>
<tr><td style="padding:32px 40px 0 40px;">
<div style="font-size:12px; font-weight:600; color:${accent}; letter-spacing:0.8px; text-transform:uppercase; margin-bottom:6px;">${label} · production</div>
<div style="font-size:20px; font-weight:600; color:#0f172a; line-height:1.35;">${env.JOB_NAME}<span style="color:#94a3b8; font-weight:400;"> &middot; #${env.BUILD_NUMBER}</span></div>
</td></tr>
<tr><td style="padding:20px 40px 8px 40px;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" style="font-size:14px; color:#0f172a;">
<tr><td width="110" style="padding:7px 0; color:#64748b;">Commit</td><td style="padding:7px 0; font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; font-size:13px;">${info.hash}</td></tr>
<tr><td style="padding:7px 0; color:#64748b;">Author</td><td style="padding:7px 0;">${info.author}</td></tr>
<tr><td style="padding:7px 0; color:#64748b; vertical-align:top;">Message</td><td style="padding:7px 0;">${info.message}</td></tr>
<tr><td style="padding:7px 0; color:#64748b;">Triggered by</td><td style="padding:7px 0;">${info.trigger}</td></tr>
<tr><td style="padding:7px 0; color:#64748b;">Duration</td><td style="padding:7px 0;">${info.duration}</td></tr>
</table>
</td></tr>
<tr><td style="padding:20px 40px 32px 40px;">
<a href="${env.BUILD_URL}" style="display:inline-block; padding:10px 18px; background:#0f172a; color:#ffffff; text-decoration:none; border-radius:8px; font-size:14px; font-weight:500;">View build</a>
<a href="${env.BUILD_URL}console" style="display:inline-block; padding:10px 14px; color:#475569; text-decoration:none; font-size:14px; font-weight:500;">Console output &rarr;</a>
</td></tr>
<tr><td style="padding:14px 40px; background:#f8fafc; border-top:1px solid #e2e8f0; font-size:12px; color:#94a3b8;">
Automated notification from Jenkins
</td></tr>
</table>
</td></tr>
</table>
</body>
</html>
"""
}
-94
View File
@@ -1,94 +0,0 @@
# Docker Setup (Backend + Frontend + PostgreSQL)
This project provides a complete development environment using **Docker Compose** for:
- Backend (Node.js / Express / Prisma)
- Frontend (Vite / React)
- PostgreSQL Database
---
## Project Structure
```
.
├── backend/
├── frontend/
├── docker/
│ └── dev/
│ ├── Dockerfile.main
│ └── Dockerfile.frontend
├── docker-compose.dev.yml
```
---
## Prerequisites
Make sure you have installed:
- Docker
- Docker Compose
---
## Environment Variables
### Backend (`backend/.env`)
```env
DATABASE_URL=postgresql://user:password@db:5432/mydb
PORT=5008
JWT_SECRET=your_secret_here
CORS_ALLOWED_ORIGINS=http://localhost:5173
BYTESCALE_SECRET_API_KEY=your_key
POSTMARK_API_KEY=your_key
EMAIL_FROM=admin@example.com
```
### Frontend (`frontend/.env`)
```env
VITE_API_URL=http://localhost:5008/api
```
---
## Running the Project
### Start containers
```bash
docker compose -f docker-compose.dev.yml up --build
```
## Create User
```bash
docker exec -it gg-backend-api-backend-1 node src/utils/createUser.js <name> <password> <role>
```
---
### Stop containers
```bash
docker compose -f docker-compose.dev.yml down
```
---
## Database (PostgreSQL)
- User: `user`
- Password: `password`
- DB: `mydb`
Data is persisted using Docker volume:
```
postgres_data
```
+1 -1
View File
@@ -1,5 +1,5 @@
node_modules
# Keep environment variables out of version control
.env*
.env
/src/generated/prisma
-59
View File
@@ -1,59 +0,0 @@
**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=5008
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.
+2 -110
View File
@@ -9,7 +9,6 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"@bytescale/sdk": "^3.53.0",
"@editorjs/editorjs": "^2.31.4",
"@editorjs/header": "^2.8.8",
"@editorjs/list": "^2.0.9",
@@ -22,21 +21,13 @@
"express-session": "^1.19.0",
"jsonwebtoken": "^9.0.3",
"multer": "^2.1.1",
"node-fetch": "^3.3.2",
"postmark": "^4.0.7",
"prisma": "^6.19.2",
"slugify": "^1.6.9"
"prisma": "^6.19.2"
},
"devDependencies": {
"nodemon": "^3.1.11"
}
},
"node_modules/@bytescale/sdk": {
"version": "3.53.0",
"resolved": "https://registry.npmjs.org/@bytescale/sdk/-/sdk-3.53.0.tgz",
"integrity": "sha512-qCeNup3pSjaklXuBrO9JeKbozZEs/PjQEvuqCiOAWLBRl6lDjd0V9gRVYqyttPimXYFoV+J/7dmPWtK6RfOABQ==",
"license": "MIT"
},
"node_modules/@codexteam/icons": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/@codexteam/icons/-/icons-0.0.5.tgz",
@@ -608,15 +599,6 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/data-uri-to-buffer": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -917,29 +899,6 @@
"node": ">=8.0.0"
}
},
"node_modules/fetch-blob": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "paypal",
"url": "https://paypal.me/jimmywarting"
}
],
"license": "MIT",
"dependencies": {
"node-domexception": "^1.0.0",
"web-streams-polyfill": "^3.0.3"
},
"engines": {
"node": "^12.20 || >= 14.13"
}
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -1031,18 +990,6 @@
"node": ">= 0.6"
}
},
"node_modules/formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
"license": "MIT",
"dependencies": {
"fetch-blob": "^3.1.2"
},
"engines": {
"node": ">=12.20.0"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -1574,44 +1521,6 @@
"node": "^18 || ^20 || >= 21"
}
},
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
"deprecated": "Use your platform's native DOMException instead",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "github",
"url": "https://paypal.me/jimmywarting"
}
],
"license": "MIT",
"engines": {
"node": ">=10.5.0"
}
},
"node_modules/node-fetch": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
"license": "MIT",
"dependencies": {
"data-uri-to-buffer": "^4.0.0",
"fetch-blob": "^3.1.4",
"formdata-polyfill": "^4.0.10"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/node-fetch"
}
},
"node_modules/node-fetch-native": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz",
@@ -1818,6 +1727,7 @@
"integrity": "sha512-XTKeKxtQElcq3U9/jHyxSPgiRgeYDKxWTPOf6NkXA0dNj5j40MfEsZkMbyNpwDWCUv7YBFUl7I2VK/6ALbmhEg==",
"hasInstallScript": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@prisma/config": "6.19.2",
"@prisma/engines": "6.19.2"
@@ -2154,15 +2064,6 @@
"node": ">=10"
}
},
"node_modules/slugify": {
"version": "1.6.9",
"resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.9.tgz",
"integrity": "sha512-vZ7rfeehZui7wQs438JXBckYLkIIdfHOXsaVEUMyS5fHo1483l1bMdo0EDSWYclY0yZKFOipDy4KHuKs6ssvdg==",
"license": "MIT",
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
@@ -2306,15 +2207,6 @@
"node": ">= 0.8"
}
},
"node_modules/web-streams-polyfill": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
"license": "MIT",
"engines": {
"node": ">= 8"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+2 -6
View File
@@ -8,15 +8,13 @@
"start": "node src/app.js",
"prisma": "prisma",
"migrate": "npx prisma migrate dev",
"generate": "npx prisma generate",
"create-user": "node src/utils/createUser.js"
"generate": "npx prisma generate"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "module",
"dependencies": {
"@bytescale/sdk": "^3.53.0",
"@editorjs/editorjs": "^2.31.4",
"@editorjs/header": "^2.8.8",
"@editorjs/list": "^2.0.9",
@@ -29,10 +27,8 @@
"express-session": "^1.19.0",
"jsonwebtoken": "^9.0.3",
"multer": "^2.1.1",
"node-fetch": "^3.3.2",
"postmark": "^4.0.7",
"prisma": "^6.19.2",
"slugify": "^1.6.9"
"prisma": "^6.19.2"
},
"devDependencies": {
"nodemon": "^3.1.11"
+5 -5
View File
@@ -1,14 +1,14 @@
// This file was generated by Prisma, and assumes you have installed the following:
// npm install --save-dev prisma dotenv
import 'dotenv/config';
import { defineConfig } from 'prisma/config';
import "dotenv/config";
import {defineConfig} from "prisma/config";
export default defineConfig({
schema: 'prisma/schema.prisma',
schema: "prisma/schema.prisma",
migrations: {
path: 'prisma/migrations',
path: "prisma/migrations",
},
datasource: {
url: process.env['DATABASE_URL'],
url: process.env["DATABASE_URL"],
},
});
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "Doctor" ADD COLUMN "image" TEXT;
@@ -1,8 +0,0 @@
/*
Warnings:
- Added the required column `slug` to the `Blog` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "Blog" ADD COLUMN "slug" TEXT NOT NULL;
@@ -1,8 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[slug]` on the table `Blog` will be added. If there are existing duplicate values, this will fail.
*/
-- CreateIndex
CREATE UNIQUE INDEX "Blog_slug_key" ON "Blog"("slug");
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "Department" ADD COLUMN "image" TEXT;
@@ -1,12 +0,0 @@
-- CreateTable
CREATE TABLE "NewsImage" (
"id" SERIAL NOT NULL,
"url" TEXT NOT NULL,
"newsMediaId" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "NewsImage_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "NewsImage" ADD CONSTRAINT "NewsImage_newsMediaId_fkey" FOREIGN KEY ("newsMediaId") REFERENCES "NewsMedia"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -1,14 +0,0 @@
-- AlterTable
ALTER TABLE "Career" ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN "sortOrder" INTEGER NOT NULL DEFAULT 0;
-- AlterTable
ALTER TABLE "Department" ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN "sortOrder" INTEGER NOT NULL DEFAULT 0;
-- AlterTable
ALTER TABLE "Doctor" ADD COLUMN "globalSortOrder" INTEGER NOT NULL DEFAULT 0,
ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true;
-- AlterTable
ALTER TABLE "DoctorDepartment" ADD COLUMN "sortOrder" INTEGER NOT NULL DEFAULT 0;
@@ -1,11 +0,0 @@
-- AlterTable
ALTER TABLE "Career" ALTER COLUMN "sortOrder" SET DEFAULT 1000;
-- AlterTable
ALTER TABLE "Department" ALTER COLUMN "sortOrder" SET DEFAULT 1000;
-- AlterTable
ALTER TABLE "Doctor" ALTER COLUMN "globalSortOrder" SET DEFAULT 1000;
-- AlterTable
ALTER TABLE "DoctorDepartment" ALTER COLUMN "sortOrder" SET DEFAULT 1000;
@@ -1,63 +0,0 @@
-- CreateTable
CREATE TABLE "HealthCheckCategory" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"description" TEXT,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"sortOrder" INTEGER NOT NULL DEFAULT 1000,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "HealthCheckCategory_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "HealthPackage" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"description" TEXT,
"price" DECIMAL(10,2),
"discountedPrice" DECIMAL(10,2),
"inclusions" JSONB NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"isFeatured" BOOLEAN NOT NULL DEFAULT false,
"sortOrder" INTEGER NOT NULL DEFAULT 1000,
"categoryId" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "HealthPackage_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "HealthPackageInquiry" (
"id" SERIAL NOT NULL,
"fullName" TEXT NOT NULL,
"mobileNumber" TEXT NOT NULL,
"email" TEXT,
"preferredDate" TIMESTAMP(3),
"message" TEXT,
"packageId" INTEGER NOT NULL,
"status" TEXT NOT NULL DEFAULT 'pending',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "HealthPackageInquiry_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "HealthCheckCategory_name_key" ON "HealthCheckCategory"("name");
-- CreateIndex
CREATE UNIQUE INDEX "HealthCheckCategory_slug_key" ON "HealthCheckCategory"("slug");
-- CreateIndex
CREATE UNIQUE INDEX "HealthPackage_slug_key" ON "HealthPackage"("slug");
-- AddForeignKey
ALTER TABLE "HealthPackage" ADD CONSTRAINT "HealthPackage_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "HealthCheckCategory"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "HealthPackageInquiry" ADD CONSTRAINT "HealthPackageInquiry_packageId_fkey" FOREIGN KEY ("packageId") REFERENCES "HealthPackage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -1,3 +0,0 @@
-- AlterTable
ALTER TABLE "HealthPackageInquiry" ADD COLUMN "age" INTEGER,
ADD COLUMN "gender" TEXT;
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "HealthPackage" ALTER COLUMN "inclusions" SET DEFAULT '{}';
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "HealthCheckCategory" ALTER COLUMN "slug" DROP NOT NULL;
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "HealthPackage" ADD COLUMN "image" TEXT;
@@ -1,50 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[seoId]` on the table `Doctor` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable
ALTER TABLE "Doctor" ADD COLUMN "professionalSummary" TEXT,
ADD COLUMN "seoId" INTEGER;
-- CreateTable
CREATE TABLE "DoctorSpecialization" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"doctorId" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "DoctorSpecialization_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Seo" (
"id" SERIAL NOT NULL,
"seoTitle" TEXT,
"metaDescription" TEXT,
"focusKeyphrase" TEXT,
"slug" TEXT,
"tags" TEXT[],
"ogTitle" TEXT,
"ogDescription" TEXT,
"ogImage" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Seo_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Seo_slug_key" ON "Seo"("slug");
-- CreateIndex
CREATE UNIQUE INDEX "Doctor_seoId_key" ON "Doctor"("seoId");
-- AddForeignKey
ALTER TABLE "Doctor" ADD CONSTRAINT "Doctor_seoId_fkey" FOREIGN KEY ("seoId") REFERENCES "Seo"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "DoctorSpecialization" ADD CONSTRAINT "DoctorSpecialization_doctorId_fkey" FOREIGN KEY ("doctorId") REFERENCES "Doctor"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "Doctor" ADD COLUMN "experience" INTEGER;
@@ -1,14 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[seoId]` on the table `HealthPackage` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable
ALTER TABLE "HealthPackage" ADD COLUMN "seoId" INTEGER;
-- CreateIndex
CREATE UNIQUE INDEX "HealthPackage_seoId_key" ON "HealthPackage"("seoId");
-- AddForeignKey
ALTER TABLE "HealthPackage" ADD CONSTRAINT "HealthPackage_seoId_fkey" FOREIGN KEY ("seoId") REFERENCES "Seo"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -1,22 +0,0 @@
-- CreateEnum
CREATE TYPE "BannerMediaType" AS ENUM ('IMAGE', 'VIDEO');
-- CreateTable
CREATE TABLE "HomepageBanner" (
"id" SERIAL NOT NULL,
"title" TEXT,
"subtitle" TEXT,
"mediaType" "BannerMediaType" NOT NULL,
"desktopMediaUrl" TEXT NOT NULL,
"mobileMediaUrl" TEXT,
"buttonText" TEXT,
"buttonLink" TEXT,
"openInNewTab" BOOLEAN NOT NULL DEFAULT false,
"textAlignment" TEXT DEFAULT 'left',
"sortOrder" INTEGER NOT NULL DEFAULT 1000,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "HomepageBanner_pkey" PRIMARY KEY ("id")
);
@@ -1,13 +0,0 @@
-- CreateTable
CREATE TABLE "InsurancePartner" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"logo" TEXT NOT NULL,
"websiteUrl" TEXT,
"sortOrder" INTEGER NOT NULL DEFAULT 1000,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "InsurancePartner_pkey" PRIMARY KEY ("id")
);
@@ -1,18 +0,0 @@
-- CreateEnum
CREATE TYPE "AccreditationType" AS ENUM ('ACCREDITATION', 'CERTIFICATION');
-- CreateTable
CREATE TABLE "Accreditation" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
"type" "AccreditationType" NOT NULL,
"logo" TEXT,
"image" TEXT,
"description" TEXT,
"sortOrder" INTEGER NOT NULL DEFAULT 1000,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Accreditation_pkey" PRIMARY KEY ("id")
);
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "Doctor" ADD COLUMN "isFeatured" BOOLEAN NOT NULL DEFAULT false;
+4 -181
View File
@@ -21,18 +21,10 @@ model Doctor {
id Int @id @default(autoincrement())
doctorId String @unique
name String
image String?
designation String?
experience Int?
workingStatus String?
qualification String?
isActive Boolean @default(true)
isFeatured Boolean @default(false)
globalSortOrder Int @default(1000)
specializations DoctorSpecialization[]
professionalSummary String? @db.Text
seoId Int? @unique
seo Seo? @relation(fields: [seoId], references: [id])
departments DoctorDepartment[]
appointments Appointment[]
@@ -44,8 +36,6 @@ model Department {
id Int @id @default(autoincrement())
departmentId String @unique
name String
image String?
para1 String?
para2 String?
@@ -53,9 +43,6 @@ model Department {
facilities String?
services String?
isActive Boolean @default(true)
sortOrder Int @default(1000)
doctors DoctorDepartment[]
appointments Appointment[]
@@ -71,7 +58,7 @@ model DoctorDepartment {
doctor Doctor @relation(fields: [doctorId], references: [id])
department Department @relation(fields: [departmentId], references: [id])
sortOrder Int @default(1000)
timing DoctorTiming?
createdAt DateTime @default(now())
@@ -106,7 +93,6 @@ model Blog {
image String?
content Json
isActive Boolean @default(true)
slug String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -121,8 +107,7 @@ model Career {
email String?
number String?
status String @default("new")
isActive Boolean @default(true)
sortOrder Int @default(1000)
candidates Candidate[]
createdAt DateTime @default(now())
@@ -190,6 +175,7 @@ model AcademicsResearch {
updatedAt DateTime @updatedAt
}
model EmailConfig {
id Int @id @default(autoincrement())
name String
@@ -210,172 +196,9 @@ model NewsMedia {
secondPara String?
author String?
date DateTime?
images NewsImage[]
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model NewsImage {
id Int @id @default(autoincrement())
url String
newsMediaId Int
newsMedia NewsMedia @relation(fields: [newsMediaId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
}
model HealthCheckCategory {
id Int @id @default(autoincrement())
name String @unique
slug String? @unique
description String?
isActive Boolean @default(true)
sortOrder Int @default(1000)
packages HealthPackage[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model HealthPackage {
id Int @id @default(autoincrement())
name String
slug String @unique
description String?
price Decimal? @db.Decimal(10, 2)
image String?
discountedPrice Decimal? @db.Decimal(10, 2)
inclusions Json @default("{}")
isActive Boolean @default(true)
isFeatured Boolean @default(false)
sortOrder Int @default(1000)
categoryId Int
category HealthCheckCategory @relation(fields: [categoryId], references: [id])
inquiries HealthPackageInquiry[]
seoId Int? @unique
seo Seo? @relation(fields: [seoId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model HealthPackageInquiry {
id Int @id @default(autoincrement())
fullName String
mobileNumber String
email String?
age Int?
gender String?
preferredDate DateTime?
message String?
packageId Int
healthPackage HealthPackage @relation(fields: [packageId], references: [id])
status String @default("pending")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model DoctorSpecialization {
id Int @id @default(autoincrement())
name String
description String? @db.Text
doctorId Int
doctor Doctor @relation(fields: [doctorId],references: [id],onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Seo {
id Int @id @default(autoincrement())
doctor Doctor?
healthPackage HealthPackage?
seoTitle String?
metaDescription String? @db.Text
focusKeyphrase String?
slug String? @unique
tags String[]
ogTitle String?
ogDescription String? @db.Text
ogImage String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model HomepageBanner {
id Int @id @default(autoincrement())
title String?
subtitle String?
mediaType BannerMediaType
desktopMediaUrl String
mobileMediaUrl String?
buttonText String?
buttonLink String?
openInNewTab Boolean @default(false)
textAlignment String? @default("left")
sortOrder Int @default(1000)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
enum BannerMediaType {
IMAGE
VIDEO
}
model InsurancePartner {
id Int @id @default(autoincrement())
name String
logo String
websiteUrl String?
sortOrder Int @default(1000)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Accreditation {
id Int @id @default(autoincrement())
title String
type AccreditationType
logo String?
image String?
description String? @db.Text
sortOrder Int @default(1000)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
enum AccreditationType {
ACCREDITATION
CERTIFICATION
}
+35 -47
View File
@@ -1,70 +1,58 @@
import express from 'express';
import dotenv from 'dotenv';
import cors from 'cors';
import express from "express";
import dotenv from "dotenv";
import cors from "cors";
import departmentRoutes from './routes/department.routes.js';
import authRoutes from './routes/auth.routes.js';
import blogRoutes from './routes/blog.routes.js';
import uploadRoutes from './routes/upload.routes.js';
import doctorRoutes from './routes/doctor.routes.js';
import careerRoutes from './routes/career.routes.js';
import candidateRoutes from './routes/candidate.routes.js';
import appointmentRoutes from './routes/appointment.routes.js';
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';
import healthCheckRoutes from './routes/healthCheck.route.js';
import homepageBannerRoutes from './routes/homepageBanner.routes.js';
import insurancePartnerRoutes from './routes/insurancePartner.routes.js';
import accreditationRoutes from './routes/accreditation.routes.js';
import departmentRoutes from "./routes/department.routes.js";
import authRoutes from "./routes/auth.routes.js";
import blogRoutes from "./routes/blog.routes.js";
import uploadRoutes from "./routes/upload.routes.js";
import doctorRoutes from "./routes/doctor.routes.js";
import careerRoutes from "./routes/career.routes.js";
import candidateRoutes from "./routes/candidate.routes.js";
import appointmentRoutes from "./routes/appointment.routes.js";
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";
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'];
? process.env.CORS_ALLOWED_ORIGINS.split(" ")
: ["http://localhost:3001"];
const corsOptions = {
origin: function (origin, callback) {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
callback(new Error("Not allowed by CORS"));
}
},
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: '*',
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"],
allowedHeaders: "*",
};
app.use(express.json());
app.use(cors(corsOptions));
app.use('/api/departments', departmentRoutes);
app.use('/api/auth', authRoutes);
app.use('/api/blogs', blogRoutes);
app.use('/uploads', express.static('uploads'));
app.use('/api/upload', uploadRoutes);
app.use('/api/doctors', doctorRoutes);
app.use('/api/careers', careerRoutes);
app.use('/api/candidates', candidateRoutes);
app.use('/api/appointments', appointmentRoutes);
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);
app.use('/api/health-check', healthCheckRoutes);
app.use('/api/homepage-banners', homepageBannerRoutes);
app.use('/api/insurance-partners', insurancePartnerRoutes);
app.use('/api/accreditation', accreditationRoutes);
app.use("/api/departments", departmentRoutes);
app.use("/api/auth", authRoutes);
app.use("/api/blogs", blogRoutes);
app.use("/uploads", express.static("uploads"));
app.use("/api/upload", uploadRoutes);
app.use("/api/doctors", doctorRoutes);
app.use("/api/careers", careerRoutes);
app.use("/api/candidates", candidateRoutes);
app.use("/api/appointments", appointmentRoutes);
app.use("/api/inquiry", inquiryRoutes);
app.use("/api/academics", academicsResearchRoutes);
app.use("/api/email", emailConfigRoutes);
app.use("/api/newsMedia", newsMediaRoutes);
const PORT = process.env.PORT || 5008;
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
@@ -1,17 +1,18 @@
import prisma from '../prisma/client.js';
import { sendEmail } from '../utils/sendEmail.js';
import { getEmailsByType } from '../utils/getEmailByTypes.js';
import prisma from "../prisma/client.js";
import { sendEmail } from "../utils/sendEmail.js";
import { getEmailsByType } from "../utils/getEmailByTypes.js";
// CREATE ACADEMICS & RESEARCH
export const createAcademicsResearch = async (req, res) => {
try {
const { fullName, number, emailId, subject, courseName, message } = req.body;
const { fullName, number, emailId, subject, courseName, message } =
req.body;
if (!fullName || !number) {
return res.status(400).json({
success: false,
message: 'Full name and number are required',
message: "Full name and number are required",
});
}
@@ -27,97 +28,42 @@ export const createAcademicsResearch = async (req, res) => {
});
try {
const emailList = await getEmailsByType('ACADEMICS');
const emailList = await getEmailsByType("ACADEMICS");
if (emailList && emailList.length > 0) {
await sendEmail({
to: emailList,
subject: 'New Academics & Research Inquiry',
subject: "New Academics & Research Inquiry",
html: `
<div style="font-family: Arial, sans-serif; background-color: #f4f6f8; padding: 20px;">
<h2>New Academics & Research Inquiry</h2>
<div style="max-width: 600px; margin: auto; background: #ffffff; border-radius: 10px; overflow: hidden; box-shadow: 0 4px 10px rgba(0,0,0,0.05);">
<p><b>Name:</b> ${fullName}</p>
<p><b>Phone:</b> ${number}</p>
<p><b>Email:</b> ${emailId || "-"}</p>
<!-- Header -->
<div style="background-color: #0d6efd; color: #ffffff; padding: 20px;">
<h2 style="margin: 0;">GG Hospital</h2>
<p style="margin: 5px 0 0; font-size: 14px;">
New Academics & Research Inquiry
</p>
</div>
<p><b>Course:</b> ${courseName || "-"}</p>
<p><b>Subject:</b> ${subject || "-"}</p>
<!-- Body -->
<div style="padding: 20px; color: #333;">
<h3 style="margin-top: 0;">Contact Details</h3>
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 8px 0;"><b>Name:</b></td>
<td style="padding: 8px 0;">${fullName}</td>
</tr>
<tr>
<td style="padding: 8px 0;"><b>Phone:</b></td>
<td style="padding: 8px 0;">${number}</td>
</tr>
<tr>
<td style="padding: 8px 0;"><b>Email:</b></td>
<td style="padding: 8px 0;">${emailId || '-'}</td>
</tr>
<tr>
<td style="padding: 8px 0;"><b>Course:</b></td>
<td style="padding: 8px 0;">${courseName || '-'}</td>
</tr>
<tr>
<td style="padding: 8px 0;"><b>Subject:</b></td>
<td style="padding: 8px 0;">${subject || '-'}</td>
</tr>
</table>
<!-- Message Box -->
<div style="margin-top: 20px;">
<h3>Message</h3>
<div style="
background: #f8f9fa;
padding: 15px;
border-radius: 6px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
overflow-wrap: anywhere;
">
${message ? message.replace(/\n/g, '<br/>') : '-'}
</div>
</div>
</div>
<!-- Footer -->
<div style="background: #f1f1f1; padding: 15px; text-align: center; font-size: 12px; color: #666;">
This message was sent from the GG Hospital website (Academics & Research Inquiry).
</div>
</div>
</div>
<p><b>Message:</b></p>
<p>${message || "-"}</p>
`,
});
}
} catch (err) {
console.error('Academics email failed:', err);
console.error("Academics email failed:", err);
}
res.status(200).json({
success: true,
status: 200,
data,
message: 'Academics & Research added successfully',
message: "Academics & Research added successfully",
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to add Academics & Research inquiry',
message: "Failed to add Academics & Research inquiry",
});
}
};
@@ -128,7 +74,7 @@ export const getAcademicsResearch = async (req, res) => {
try {
const data = await prisma.academicsResearch.findMany({
orderBy: {
createdAt: 'desc',
createdAt: "desc",
},
});
@@ -139,7 +85,7 @@ export const getAcademicsResearch = async (req, res) => {
} catch (error) {
res.status(500).json({
success: false,
message: 'Failed to fetch records',
message: "Failed to fetch records",
});
}
};
@@ -159,7 +105,7 @@ export const getSingleAcademicsResearch = async (req, res) => {
if (!data) {
return res.status(404).json({
success: false,
message: 'Record not found',
message: "Record not found",
});
}
@@ -170,7 +116,7 @@ export const getSingleAcademicsResearch = async (req, res) => {
} catch (error) {
res.status(500).json({
success: false,
message: 'Failed to fetch record',
message: "Failed to fetch record",
});
}
};
@@ -189,12 +135,12 @@ export const deleteAcademicsResearch = async (req, res) => {
res.json({
success: true,
message: 'Record deleted successfully',
message: "Record deleted successfully",
});
} catch (error) {
res.status(500).json({
success: false,
message: 'Failed to delete record',
message: "Failed to delete record",
});
}
};
@@ -1,189 +0,0 @@
import prisma from '../prisma/client.js';
export const createAccreditation = async (req, res) => {
try {
const { title, type, logo, image, description, sortOrder, isActive } = req.body;
if (!title || !type) {
return res.status(400).json({
success: false,
message: 'Title and type are required',
});
}
const accreditation = await prisma.accreditation.create({
data: {
title,
type,
logo,
image,
description,
sortOrder: sortOrder ? Number(sortOrder) : 1000,
isActive: isActive ?? true,
},
});
res.status(201).json({
success: true,
data: accreditation,
message: 'Accreditation created successfully',
});
} catch (error) {
console.error('PRISMA TRANSACTION ERROR:', error);
res.status(500).json({
success: false,
message: 'Failed to create accreditation',
});
}
};
export const getAccreditations = async (req, res) => {
try {
const { type } = req.query;
const accreditations = await prisma.accreditation.findMany({
where: type
? {
type,
}
: undefined,
orderBy: {
sortOrder: 'asc',
},
});
res.json({
success: true,
data: accreditations,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to fetch accreditations',
});
}
};
export const getActiveAccreditations = async (req, res) => {
try {
const { type } = req.query;
const accreditations = await prisma.accreditation.findMany({
where: {
isActive: true,
...(type && { type }),
},
orderBy: {
sortOrder: 'asc',
},
});
res.json({
success: true,
data: accreditations,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to fetch accreditations',
});
}
};
export const getAccreditation = async (req, res) => {
try {
const { id } = req.params;
const accreditation = await prisma.accreditation.findUnique({
where: {
id: Number(id),
},
});
if (!accreditation) {
return res.status(404).json({
success: false,
message: 'Accreditation not found',
});
}
res.json({
success: true,
data: accreditation,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to fetch accreditation',
});
}
};
export const updateAccreditation = async (req, res) => {
try {
const { id } = req.params;
const { title, type, logo, image, description, sortOrder, isActive } = req.body;
const updateData = {};
if (title !== undefined) updateData.title = title;
if (type !== undefined) updateData.type = type;
if (logo !== undefined) updateData.logo = logo;
if (image !== undefined) updateData.image = image;
if (description !== undefined) updateData.description = description;
if (sortOrder !== undefined) updateData.sortOrder = Number(sortOrder);
if (isActive !== undefined) updateData.isActive = isActive;
const accreditation = await prisma.accreditation.update({
where: {
id: Number(id),
},
data: updateData,
});
res.json({
success: true,
data: accreditation,
message: 'Accreditation updated successfully',
});
} catch (error) {
console.error('PRISMA TRANSACTION ERROR:', error);
res.status(500).json({
success: false,
message: 'Failed to update accreditation',
});
}
};
export const deleteAccreditation = async (req, res) => {
try {
const { id } = req.params;
await prisma.accreditation.delete({
where: {
id: Number(id),
},
});
res.json({
success: true,
message: 'Accreditation deleted successfully',
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to delete accreditation',
});
}
};
+97 -159
View File
@@ -1,15 +1,16 @@
import prisma from '../prisma/client.js';
import { sendEmail } from '../utils/sendEmail.js';
import { getEmailsByType } from '../utils/getEmailByTypes.js';
import prisma from "../prisma/client.js";
import { sendEmail } from "../utils/sendEmail.js";
import { getEmailsByType } from "../utils/getEmailByTypes.js";
export const createAppointment = async (req, res) => {
try {
const { name, mobileNumber, email, message, date, doctorId, departmentId } = req.body;
const { name, mobileNumber, email, message, date, doctorId, departmentId } =
req.body;
if (!name || !mobileNumber || !doctorId || !departmentId || !date) {
return res.status(400).json({
success: false,
message: 'Required fields missing',
message: "Required fields missing",
});
}
@@ -30,111 +31,38 @@ export const createAppointment = async (req, res) => {
});
try {
const emailList = await getEmailsByType('APPOINTMENT');
const emailList = await getEmailsByType("APPOINTMENT");
if (emailList) {
await sendEmail({
to: emailList,
subject: 'New Appointment Booked',
subject: "New Appointment Booked",
html: `
<div style="font-family: Arial, sans-serif; background-color: #f4f6f8; padding: 20px;">
<div style="max-width: 600px; margin: auto; background: #ffffff; border-radius: 10px; overflow: hidden; box-shadow: 0 4px 10px rgba(0,0,0,0.05);">
<!-- Header -->
<div style="background-color: #0d6efd; color: #ffffff; padding: 20px;">
<h2 style="margin: 0;">GG Hospital</h2>
<p style="margin: 5px 0 0; font-size: 14px;">
New Appointment Booked
</p>
</div>
<!-- Body -->
<div style="padding: 20px; color: #333;">
<h3 style="margin-top: 0;">Patient Details</h3>
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 8px 0;"><b>Name:</b></td>
<td style="padding: 8px 0;">${name}</td>
</tr>
<tr>
<td style="padding: 8px 0;"><b>Phone:</b></td>
<td style="padding: 8px 0;">${mobileNumber}</td>
</tr>
<tr>
<td style="padding: 8px 0;"><b>Email:</b></td>
<td style="padding: 8px 0;">${email || '-'}</td>
</tr>
</table>
<h3 style="margin-top: 20px;">Appointment Details</h3>
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 8px 0;"><b>Doctor:</b></td>
<td style="padding: 8px 0;">${appointment.doctor?.name || '-'}</td>
</tr>
<tr>
<td style="padding: 8px 0;"><b>Department:</b></td>
<td style="padding: 8px 0;">${appointment.department?.name || '-'}</td>
</tr>
<tr>
<td style="padding: 8px 0;"><b>Date:</b></td>
<td style="padding: 8px 0;">
${new Date(date).toLocaleDateString('en-GB', {
day: '2-digit',
month: 'long',
year: 'numeric',
})}
</td>
</tr>
</table>
<!-- Message Box -->
<div style="margin-top: 20px;">
<h3>Message</h3>
<div style="
background: #f8f9fa;
padding: 15px;
border-radius: 6px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
overflow-wrap: anywhere;
">
${message ? message.replace(/\n/g, '<br/>') : '-'}
</div>
</div>
</div>
<!-- Footer -->
<div style="background: #f1f1f1; padding: 15px; text-align: center; font-size: 12px; color: #666;">
This appointment was booked via the GG Hospital website.
</div>
</div>
</div>
<h2>New Appointment Booked</h2>
<p><b>Name:</b> ${name}</p>
<p><b>Phone:</b> ${mobileNumber}</p>
<p><b>Email:</b> ${email || "-"}</p>
<p><b>Doctor:</b> ${appointment.doctor?.name}</p>
<p><b>Department:</b> ${appointment.department?.name}</p>
<p><b>Date:</b> ${new Date(date).toLocaleDateString()}</p>
<p><b>Message:</b> ${message || "-"}</p>
`,
});
}
} catch (err) {
console.error('Email failed:', err);
console.error("Email failed:", err);
}
res.status(201).json({
success: true,
message: 'Appointment booked successfully',
message: "Appointment booked successfully",
data: appointment,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to create appointment',
message: "Failed to create appointment",
});
}
};
@@ -143,58 +71,74 @@ export const createAppointment = async (req, res) => {
export const getAppointments = async (req, res) => {
try {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const skip = (page - 1) * limit;
const page = parseInt(req.query.page);
const limit = parseInt(req.query.limit);
const { date, startDate, endDate, search } = req.query;
const search = req.query.search || "";
const doctor = req.query.doctor || "";
const department = req.query.department || "";
const date = req.query.date || "";
const where = {};
if (!page && !limit) {
const appointments = await prisma.appointment.findMany({
include: {
doctor: true,
department: true,
},
orderBy: { createdAt: "desc" },
});
const hasSingleDate = date && date.trim() !== '';
const hasRange = (startDate && startDate.trim() !== '') || (endDate && endDate.trim() !== '');
if (hasSingleDate) {
const start = new Date(date);
start.setHours(0, 0, 0, 0);
const end = new Date(date);
end.setHours(23, 59, 59, 999);
where.date = {
gte: start,
lte: end,
};
return res.status(200).json({
success: true,
data: appointments,
meta: null,
});
}
if (!hasSingleDate && hasRange) {
const dateFilter = {};
const currentPage = page || 1;
const currentLimit = limit || 10;
const skip = (currentPage - 1) * currentLimit;
if (startDate && startDate.trim() !== '') {
const start = new Date(startDate);
start.setHours(0, 0, 0, 0);
dateFilter.gte = start;
}
if (endDate && endDate.trim() !== '') {
const end = new Date(endDate);
end.setHours(23, 59, 59, 999);
dateFilter.lte = end;
}
where.date = dateFilter;
}
if (search && search.trim() !== '') {
where.OR = [
{ name: { contains: search, mode: 'insensitive' } },
const where = {
AND: [
search
? {
OR: [
{ name: { contains: search, mode: "insensitive" } },
{ mobileNumber: { contains: search } },
{ email: { contains: search, mode: 'insensitive' } },
];
{ email: { contains: search, mode: "insensitive" } },
],
}
: {},
doctor
? {
doctor: {
name: { contains: doctor, mode: "insensitive" },
},
}
: {},
department
? {
department: {
name: { contains: department, mode: "insensitive" },
},
}
: {},
date
? {
date: {
gte: new Date(date),
lt: new Date(
new Date(date).setDate(new Date(date).getDate() + 1),
),
},
}
: {},
],
};
const [appointments, total] = await Promise.all([
prisma.appointment.findMany({
@@ -203,34 +147,28 @@ export const getAppointments = async (req, res) => {
doctor: true,
department: true,
},
orderBy: {
createdAt: 'desc',
},
orderBy: { createdAt: "desc" },
skip,
take: limit,
}),
prisma.appointment.count({
where,
take: currentLimit,
}),
prisma.appointment.count({ where }),
]);
res.status(200).json({
return res.status(200).json({
success: true,
data: appointments,
pagination: {
meta: {
total,
page,
limit,
totalPages: Math.ceil(total / limit),
page: currentPage,
limit: currentLimit,
totalPages: Math.ceil(total / currentLimit),
},
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to fetch appointments',
message: "Failed to fetch appointments",
});
}
};
@@ -254,7 +192,7 @@ export const getAppointment = async (req, res) => {
if (!appointment) {
return res.status(404).json({
success: false,
message: 'Appointment not found',
message: "Appointment not found",
});
}
@@ -266,7 +204,7 @@ export const getAppointment = async (req, res) => {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to fetch appointment',
message: "Failed to fetch appointment",
});
}
};
@@ -286,7 +224,7 @@ export const getAppointmentsByDoctor = async (req, res) => {
department: true,
},
orderBy: {
date: 'asc',
date: "asc",
},
});
@@ -298,7 +236,7 @@ export const getAppointmentsByDoctor = async (req, res) => {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to fetch doctor appointments',
message: "Failed to fetch doctor appointments",
});
}
};
@@ -327,7 +265,7 @@ export const getAppointmentsByDepartment = async (req, res) => {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to fetch department appointments',
message: "Failed to fetch department appointments",
});
}
};
@@ -351,14 +289,14 @@ export const updateAppointment = async (req, res) => {
res.status(200).json({
success: true,
message: 'Appointment updated successfully',
message: "Appointment updated successfully",
data: appointment,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to update appointment',
message: "Failed to update appointment",
});
}
};
@@ -377,13 +315,13 @@ export const deleteAppointment = async (req, res) => {
res.status(200).json({
success: true,
message: 'Appointment deleted successfully',
message: "Appointment deleted successfully",
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to delete appointment',
message: "Failed to delete appointment",
});
}
};
+10 -10
View File
@@ -1,6 +1,6 @@
import prisma from '../prisma/client.js';
import { generateToken } from '../utils/jwt.js';
import { hashPassword, comparePassword } from '../utils/password.js';
import prisma from "../prisma/client.js";
import {generateToken} from "../utils/jwt.js";
import {hashPassword, comparePassword} from "../utils/password.js";
/**
* REGISTER
@@ -10,7 +10,7 @@ export async function register(req, res) {
const {username, password, role} = req.body;
if (!username || !password) {
return res.status(400).json({ error: 'Username and password required' });
return res.status(400).json({error: "Username and password required"});
}
const existingUser = await prisma.user.findUnique({
@@ -18,7 +18,7 @@ export async function register(req, res) {
});
if (existingUser) {
return res.status(409).json({ error: 'Username already exists' });
return res.status(409).json({error: "Username already exists"});
}
const hashedPassword = await hashPassword(password);
@@ -27,12 +27,12 @@ export async function register(req, res) {
data: {
username,
password: hashedPassword,
role: role || 'admin',
role: role || "admin",
},
});
res.status(201).json({
message: 'User registered successfully',
message: "User registered successfully",
user: {
id: user.id,
username: user.username,
@@ -49,7 +49,7 @@ export async function login(req, res) {
const {username, password} = req.body;
if (!username || !password) {
return res.status(400).json({ error: 'Username and password required' });
return res.status(400).json({error: "Username and password required"});
}
const user = await prisma.user.findUnique({
@@ -57,13 +57,13 @@ export async function login(req, res) {
});
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
return res.status(401).json({error: "Invalid credentials"});
}
const isValid = await comparePassword(password, user.password);
if (!isValid) {
return res.status(401).json({ error: 'Invalid credentials' });
return res.status(401).json({error: "Invalid credentials"});
}
const token = generateToken({
+6 -28
View File
@@ -1,5 +1,4 @@
import prisma from '../prisma/client.js';
import slugify from 'slugify';
import prisma from "../prisma/client.js";
/* CREATE BLOG */
@@ -14,13 +13,12 @@ export async function createBlog(req, res) {
image,
content,
isActive,
slug: slugify(title),
},
});
res.json(blog);
} catch (error) {
res.status(500).json({ error: 'Blog creation failed' });
res.status(500).json({error: "Blog creation failed"});
}
}
@@ -30,7 +28,7 @@ export async function getBlogs(req, res) {
try {
const blogs = await prisma.blog.findMany({
where: {isActive: true},
orderBy: { createdAt: 'desc' },
orderBy: {createdAt: "desc"},
});
res.json(blogs);
@@ -44,7 +42,7 @@ export async function getBlogs(req, res) {
export async function getAllBlogs(req, res) {
try {
const blogs = await prisma.blog.findMany({
orderBy: { createdAt: 'desc' },
orderBy: {createdAt: "desc"},
});
res.json(blogs);
@@ -56,26 +54,6 @@ export async function getAllBlogs(req, res) {
/* GET SINGLE BLOG */
export async function getBlog(req, res) {
try {
const slug = req.params.slug;
const blog = await prisma.blog.findUnique({
where: { slug },
});
if (!blog) {
return res.status(404).json({ error: 'Blog not found' });
}
res.json(blog);
} catch (error) {
res.status(500).json({ error: error.message });
}
}
/* GET SINGLE BLOG (ADMIN)*/
export async function getBlogForAdmin(req, res) {
try {
const id = Number(req.params.id);
@@ -84,7 +62,7 @@ export async function getBlogForAdmin(req, res) {
});
if (!blog) {
return res.status(404).json({ error: 'Blog not found' });
return res.status(404).json({error: "Blog not found"});
}
res.json(blog);
@@ -125,7 +103,7 @@ export async function deleteBlog(req, res) {
where: {id},
});
res.json({ message: 'Blog deleted successfully' });
res.json({message: "Blog deleted successfully"});
} catch (error) {
res.status(500).json({error: error.message});
}
+30 -92
View File
@@ -1,18 +1,19 @@
import prisma from '../prisma/client.js';
import prisma from "../prisma/client.js";
import { sendEmail } from '../utils/sendEmail.js';
import { getEmailsByType } from '../utils/getEmailByTypes.js';
import { sendEmail } from "../utils/sendEmail.js";
import { getEmailsByType } from "../utils/getEmailByTypes.js";
// CREATE CANDIDATE
export const createCandidate = async (req, res) => {
try {
const { fullName, mobile, email, subject, coverLetter, careerId } = req.body;
const { fullName, mobile, email, subject, coverLetter, careerId } =
req.body;
if (!fullName || !mobile || !email || !careerId) {
return res.status(400).json({
success: false,
message: 'Required fields missing',
message: "Required fields missing",
});
}
@@ -31,105 +32,42 @@ export const createCandidate = async (req, res) => {
});
try {
const emailList = await getEmailsByType('CANDIDATE');
const emailList = await getEmailsByType("CANDIDATE");
if (emailList && emailList.length > 0) {
await sendEmail({
to: emailList,
subject: 'New Job Application Received',
subject: "New Job Application Received",
html: `
<div style="font-family: Arial, sans-serif; background-color: #f4f6f8; padding: 20px;">
<h2>New Candidate Application</h2>
<div style="max-width: 600px; margin: auto; background: #ffffff; border-radius: 10px; overflow: hidden; box-shadow: 0 4px 10px rgba(0,0,0,0.05);">
<p><b>Name:</b> ${fullName}</p>
<p><b>Phone:</b> ${mobile}</p>
<p><b>Email:</b> ${email}</p>
<!-- Header -->
<div style="background-color: #0d6efd; color: #ffffff; padding: 20px;">
<h2 style="margin: 0;">GG Hospital</h2>
<p style="margin: 5px 0 0; font-size: 14px;">
New Job Application Received
</p>
</div>
<p><b>Applied For:</b> ${candidate.career?.post || "-"}</p>
<p><b>Designation:</b> ${candidate.career?.designation || "-"}</p>
<!-- Body -->
<div style="padding: 20px; color: #333;">
<h3 style="margin-top: 0;">Candidate Details</h3>
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 8px 0;"><b>Name:</b></td>
<td style="padding: 8px 0;">${fullName}</td>
</tr>
<tr>
<td style="padding: 8px 0;"><b>Phone:</b></td>
<td style="padding: 8px 0;">${mobile}</td>
</tr>
<tr>
<td style="padding: 8px 0;"><b>Email:</b></td>
<td style="padding: 8px 0;">${email}</td>
</tr>
</table>
<h3 style="margin-top: 20px;">Application Details</h3>
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 8px 0;"><b>Applied For:</b></td>
<td style="padding: 8px 0;">${candidate.career?.post || '-'}</td>
</tr>
<tr>
<td style="padding: 8px 0;"><b>Designation:</b></td>
<td style="padding: 8px 0;">${candidate.career?.designation || '-'}</td>
</tr>
<tr>
<td style="padding: 8px 0;"><b>Subject:</b></td>
<td style="padding: 8px 0;">${subject || '-'}</td>
</tr>
</table>
<!-- Cover Letter -->
<div style="margin-top: 20px;">
<h3>Cover Letter</h3>
<div style="
background: #f8f9fa;
padding: 15px;
border-radius: 6px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
overflow-wrap: anywhere;
">
${coverLetter ? coverLetter.replace(/\n/g, '<br/>') : '-'}
</div>
</div>
</div>
<!-- Footer -->
<div style="background: #f1f1f1; padding: 15px; text-align: center; font-size: 12px; color: #666;">
This application was submitted via the GG Hospital careers page.
</div>
</div>
</div>
<p><b>Subject:</b> ${subject || "-"}</p>
<p><b>Cover Letter:</b></p>
<p>${coverLetter || "-"}</p>
`,
});
}
} catch (err) {
console.error('Candidate email failed:', err);
console.error("Candidate email failed:", err);
}
res.status(201).json({
success: true,
message: 'Application submitted successfully',
message: "Application submitted successfully",
data: candidate,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to create candidate',
message: "Failed to create candidate",
});
}
};
@@ -143,7 +81,7 @@ export const getCandidates = async (req, res) => {
career: true,
},
orderBy: {
createdAt: 'desc',
createdAt: "desc",
},
});
@@ -155,7 +93,7 @@ export const getCandidates = async (req, res) => {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to fetch candidates',
message: "Failed to fetch candidates",
});
}
};
@@ -178,7 +116,7 @@ export const getCandidate = async (req, res) => {
if (!candidate) {
return res.status(404).json({
success: false,
message: 'Candidate not found',
message: "Candidate not found",
});
}
@@ -190,7 +128,7 @@ export const getCandidate = async (req, res) => {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to fetch candidate',
message: "Failed to fetch candidate",
});
}
};
@@ -209,7 +147,7 @@ export const getCandidatesByCareer = async (req, res) => {
career: true,
},
orderBy: {
createdAt: 'desc',
createdAt: "desc",
},
});
@@ -221,7 +159,7 @@ export const getCandidatesByCareer = async (req, res) => {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to fetch candidates',
message: "Failed to fetch candidates",
});
}
};
@@ -241,14 +179,14 @@ export const updateCandidate = async (req, res) => {
res.status(200).json({
success: true,
message: 'Candidate updated successfully',
message: "Candidate updated successfully",
data: candidate,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to update candidate',
message: "Failed to update candidate",
});
}
};
@@ -267,13 +205,13 @@ export const deleteCandidate = async (req, res) => {
res.status(200).json({
success: true,
message: 'Candidate deleted successfully',
message: "Candidate deleted successfully",
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to delete candidate',
message: "Failed to delete candidate",
});
}
};
+20 -24
View File
@@ -1,14 +1,11 @@
import prisma from '../prisma/client.js';
import prisma from "../prisma/client.js";
// GET ALL CAREERS
export const getAllCareers = async (req, res) => {
try {
const { admin } = req.query;
const careers = await prisma.career.findMany({
where: admin === 'true' ? {} : { isActive: true },
orderBy: [{ sortOrder: 'asc' }, { createdAt: 'desc' }],
orderBy: {createdAt: "desc"},
});
const response = careers.map((c) => ({
@@ -20,8 +17,6 @@ export const getAllCareers = async (req, res) => {
email: c.email,
number: c.number,
status: c.status,
isActive: c.isActive,
sortOrder: c.sortOrder,
}));
return res.status(200).json({
@@ -32,7 +27,7 @@ export const getAllCareers = async (req, res) => {
console.error(error);
return res.status(500).json({
success: false,
message: 'Failed to fetch careers',
message: "Failed to fetch careers",
});
}
};
@@ -41,12 +36,20 @@ export const getAllCareers = async (req, res) => {
export const createCareer = async (req, res) => {
try {
const { post, designation, qualification, experienceNeed, email, number, status, isActive, sortOrder } = req.body;
const {
post,
designation,
qualification,
experienceNeed,
email,
number,
status,
} = req.body;
if (!post || !designation) {
return res.status(400).json({
success: false,
message: 'Post and designation are required',
message: "Post and designation are required",
});
}
@@ -59,21 +62,19 @@ export const createCareer = async (req, res) => {
email,
number,
status,
isActive: isActive !== undefined ? isActive : true,
sortOrder: sortOrder !== undefined ? Number(sortOrder) : 0,
},
});
return res.status(201).json({
success: true,
message: 'Career created successfully',
message: "Career created successfully",
data: career,
});
} catch (error) {
console.error(error);
return res.status(500).json({
success: false,
message: 'Failed to create career',
message: "Failed to create career",
});
}
};
@@ -83,27 +84,22 @@ export const createCareer = async (req, res) => {
export const updateCareer = async (req, res) => {
try {
const {id} = req.params;
const updateData = { ...req.body };
if (updateData.sortOrder !== undefined) {
updateData.sortOrder = Number(updateData.sortOrder);
}
const career = await prisma.career.update({
where: {id: Number(id)},
data: updateData,
data: req.body,
});
return res.status(200).json({
success: true,
message: 'Career updated successfully',
message: "Career updated successfully",
data: career,
});
} catch (error) {
console.error(error);
return res.status(500).json({
success: false,
message: 'Failed to update career',
message: "Failed to update career",
});
}
};
@@ -120,13 +116,13 @@ export const deleteCareer = async (req, res) => {
return res.status(200).json({
success: true,
message: 'Career deleted successfully',
message: "Career deleted successfully",
});
} catch (error) {
console.error(error);
return res.status(500).json({
success: false,
message: 'Failed to delete career',
message: "Failed to delete career",
});
}
};
@@ -1,25 +1,19 @@
import prisma from '../prisma/client.js';
import prisma from "../prisma/client.js";
export const getAllDepartments = async (req, res) => {
try {
const { admin } = req.query;
const departments = await prisma.department.findMany({
where: admin === 'true' ? {} : { isActive: true },
orderBy: [{ sortOrder: 'asc' }, { name: 'asc' }],
orderBy: {name: "asc"},
});
const response = departments.map((dep) => ({
departmentId: dep.departmentId,
name: dep.name,
image: dep.image ?? '',
para1: dep.para1 ?? '',
para2: dep.para2 ?? '',
para3: dep.para3 ?? '',
facilities: dep.facilities ?? '',
services: dep.services ?? '',
isActive: dep.isActive,
sortOrder: dep.sortOrder,
para1: dep.para1 ?? "",
para2: dep.para2 ?? "",
para3: dep.para3 ?? "",
facilities: dep.facilities ?? "",
services: dep.services ?? "",
}));
return res.status(200).json({
@@ -30,122 +24,75 @@ export const getAllDepartments = async (req, res) => {
console.error(error);
return res.status(500).json({
success: false,
message: 'Failed to fetch departments',
});
}
};
export const getDepartmentByName = async (req, res) => {
try {
const { name } = req.query;
if (!name) {
return res.status(400).json({
success: false,
message: 'Department name is required',
});
}
const department = await prisma.department.findFirst({
where: {
name: name,
isActive: true,
},
});
if (!department) {
return res.status(404).json({
success: false,
message: 'Department not found or inactive',
});
}
const response = {
departmentId: department.departmentId,
name: department.name,
image: department.image ?? '',
para1: department.para1 ?? '',
para2: department.para2 ?? '',
para3: department.para3 ?? '',
facilities: department.facilities ?? '',
services: department.services ?? '',
isActive: department.isActive,
sortOrder: department.sortOrder,
};
return res.status(200).json({
success: true,
data: [response],
});
} catch (error) {
console.error(error);
return res.status(500).json({
success: false,
message: 'Failed to fetch department',
message: "Failed to fetch departments",
});
}
};
export async function createDepartment(req, res) {
try {
const { departmentId, name, image, para1, para2, para3, facilities, services, isActive, sortOrder } = req.body;
const {departmentId, name, para1, para2, para3, facilities, services} =
req.body;
if (!departmentId || !name) {
return res.status(400).json({ error: 'departmentId and name are required' });
return res
.status(400)
.json({error: "departmentId and name are required"});
}
const department = await prisma.department.create({
data: {
departmentId,
name,
image,
para1,
para2,
para3,
facilities,
services,
isActive: isActive !== undefined ? isActive : true,
sortOrder: sortOrder !== undefined ? Number(sortOrder) : 0,
},
});
res.status(201).json({
message: 'Department created successfully',
message: "Department created successfully",
data: department,
});
} catch (error) {
if (error.code === 'P2002') {
return res.status(409).json({ error: 'Department already exists' });
if (error.code === "P2002") {
return res.status(409).json({error: "Department already exists"});
}
console.error(error);
res.status(500).json({ error: 'Failed to create department' });
res.status(500).json({error: "Failed to create department"});
}
}
export const updateDepartment = async (req, res) => {
try {
const {departmentId} = req.params;
const updateData = { ...req.body };
if (updateData.sortOrder !== undefined) {
updateData.sortOrder = Number(updateData.sortOrder);
}
const {name, para1, para2, para3, facilities, services} = req.body;
const department = await prisma.department.update({
where: {departmentId},
data: updateData,
data: {
name,
para1,
para2,
para3,
facilities,
services,
},
});
return res.status(200).json({
success: true,
message: 'Department updated successfully',
message: "Department updated successfully",
data: department,
});
} catch (error) {
console.error(error);
return res.status(500).json({
success: false,
message: 'Failed to update department',
message: "Failed to update department",
});
}
};
@@ -160,13 +107,13 @@ export const deleteDepartment = async (req, res) => {
return res.status(200).json({
success: true,
message: 'Department deleted successfully',
message: "Department deleted successfully",
});
} catch (error) {
console.error(error);
return res.status(500).json({
success: false,
message: 'Failed to delete department',
message: "Failed to delete department",
});
}
};
+141 -495
View File
@@ -1,61 +1,33 @@
import prisma from '../prisma/client.js';
import prisma from "../prisma/client.js";
// get doctors
export const getAllDoctors = async (req, res) => {
try {
const { admin } = req.query;
const doctors = await prisma.doctor.findMany({
where: admin === 'true' ? {} : { isActive: true },
include: {
seo: true,
departments: {
include: {
department: true,
timing: true,
},
},
specializations: {
orderBy: {
createdAt: 'asc',
},
},
},
orderBy: [{ globalSortOrder: 'asc' }, { name: 'asc' }],
orderBy: {name: "asc"},
});
const formatted = doctors.map((doc, index) => ({
const formatted = doctors.map((doc, index) => {
return {
SL_NO: String(index + 1),
doctorId: doc.doctorId,
name: doc.name,
image: doc.image ?? '',
designation: doc.designation,
workingStatus: doc.workingStatus,
qualification: doc.qualification,
isActive: doc.isActive,
isFeatured: doc.isFeatured,
experience: doc.experience,
professionalSummary: doc.professionalSummary,
globalSortOrder: doc.globalSortOrder,
specializations: doc.specializations.map((item) => ({
id: item.id,
name: item.name,
description: item.description,
})),
seo: {
seoTitle: doc.seo?.seoTitle ?? '',
metaDescription: doc.seo?.metaDescription ?? '',
focusKeyphrase: doc.seo?.focusKeyphrase ?? '',
slug: doc.seo?.slug ?? '',
tags: doc.seo?.tags ?? [],
ogTitle: doc.seo?.ogTitle ?? '',
ogDescription: doc.seo?.ogDescription ?? '',
ogImage: doc.seo?.ogImage ?? '',
},
departments: doc.departments.map((d) => {
const t = d.timing || {};
const timingArray = [
t.monday && `Monday ${t.monday}`,
t.tuesday && `Tuesday ${t.tuesday}`,
@@ -70,11 +42,12 @@ export const getAllDoctors = async (req, res) => {
return {
departmentId: d.department.departmentId,
departmentName: d.department.name,
timing: timingArray.join(' & '),
deptSortOrder: d.sortOrder,
timing: timingArray.join(" & "),
};
}),
}));
};
});
res.status(200).json({
success: true,
@@ -84,7 +57,7 @@ export const getAllDoctors = async (req, res) => {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to fetch doctors',
message: "Failed to fetch doctors",
});
}
};
@@ -94,16 +67,10 @@ export const getAllDoctors = async (req, res) => {
export const getDoctorByDoctorId = async (req, res) => {
try {
const {doctorId} = req.params;
const { admin } = req.query;
const doctor = await prisma.doctor.findFirst({
where: {
doctorId,
...(admin === 'true' ? {} : { isActive: true }),
},
const doctor = await prisma.doctor.findUnique({
where: {doctorId},
include: {
seo: true,
specializations: true,
departments: {
include: {
department: true,
@@ -116,37 +83,16 @@ export const getDoctorByDoctorId = async (req, res) => {
if (!doctor) {
return res.status(404).json({
success: false,
message: 'Doctor not found',
message: "Doctor not found",
});
}
const response = {
doctorId: doctor.doctorId,
name: doctor.name,
image: doctor.image ?? '',
designation: doctor.designation,
workingStatus: doctor.workingStatus,
qualification: doctor.qualification,
experience: doctor.experience,
professionalSummary: doctor.professionalSummary,
isActive: doctor.isActive,
isFeatured: doctor.isFeatured,
seo: {
seoTitle: doctor.seo?.seoTitle ?? '',
metaDescription: doctor.seo?.metaDescription ?? '',
focusKeyphrase: doctor.seo?.focusKeyphrase ?? '',
slug: doctor.seo?.slug ?? '',
tags: doctor.seo?.tags ?? [],
ogTitle: doctor.seo?.ogTitle ?? '',
ogDescription: doctor.seo?.ogDescription ?? '',
ogImage: doctor.seo?.ogImage ?? '',
},
specializations:
doctor.specializations?.map((item) => ({
id: item.id,
name: item.name,
description: item.description,
})) ?? [],
departments: doctor.departments.map((d) => ({
departmentId: d.department.departmentId,
departmentName: d.department.name,
@@ -162,71 +108,7 @@ export const getDoctorByDoctorId = async (req, res) => {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to fetch doctor',
});
}
};
// get doctors by department
export const getDoctorsByDepartmentId = async (req, res) => {
try {
const { Department_ID } = req.query;
if (!Department_ID) {
return res.status(400).json({
success: false,
message: 'Department_ID is required',
});
}
const department = await prisma.department.findUnique({
where: { departmentId: Department_ID },
});
if (!department) {
return res.status(404).json({
success: false,
message: 'Department not found',
});
}
const doctorsInDept = await prisma.doctorDepartment.findMany({
where: {
departmentId: department.id,
doctor: { isActive: true },
},
include: {
doctor: {
include: {
seo: {
select: {
slug: true,
},
},
},
},
},
orderBy: { sortOrder: 'asc' },
});
const result = doctorsInDept.map((d) => ({
GG_ID: d.doctor.doctorId,
Name: d.doctor.name,
image: d.doctor.image ?? '',
designation: d.doctor.designation,
hierarchyOrder: d.sortOrder,
slug: d.doctor.seo?.slug ?? '',
}));
res.status(200).json({
success: true,
data: result,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to fetch doctors',
message: "Failed to fetch doctor",
});
}
};
@@ -237,71 +119,19 @@ export const createDoctor = async (req, res) => {
const {
doctorId,
name,
image,
designation,
workingStatus,
qualification,
isActive,
isFeatured,
globalSortOrder,
departments,
experience,
professionalSummary,
seoTitle,
metaDescription,
focusKeyphrase,
slug,
tags,
specializations,
ogTitle,
ogDescription,
ogImage,
} = req.body;
const messages = [];
if (!doctorId) messages.push('Doctor ID is required');
if (!name?.trim()) messages.push('Doctor name is required');
if (!designation?.trim()) messages.push('Designation is required');
if (!qualification?.trim()) messages.push('Qualification is required');
if (!departments || departments.length === 0) {
messages.push('At least one department is required');
}
if (messages.length > 0) {
return res.status(400).json({
success: false,
message: messages.join(', '),
});
}
const seo = await prisma.seo.create({
data: {
seoTitle,
metaDescription,
focusKeyphrase,
slug: slug ? slug : null,
tags: tags || [],
// Open Graph
ogTitle,
ogDescription,
ogImage,
},
});
const doctor = await prisma.doctor.create({
data: {
doctorId,
name,
image,
designation,
workingStatus,
qualification,
experience: experience ? Number(experience) : null,
professionalSummary,
seoId: seo.id,
isActive: isActive !== undefined ? isActive : true,
isFeatured: isFeatured !== undefined ? isFeatured : false,
globalSortOrder: globalSortOrder !== undefined ? Number(globalSortOrder) : 0,
},
});
@@ -316,7 +146,6 @@ export const createDoctor = async (req, res) => {
data: {
doctorId: doctor.id,
departmentId: department.id,
sortOrder: dep.sortOrder !== undefined ? Number(dep.sortOrder) : 0,
},
});
@@ -329,258 +158,26 @@ export const createDoctor = async (req, res) => {
});
}
}
if (specializations?.length) {
await prisma.doctorSpecialization.createMany({
data: specializations
.filter((item) => item.name?.trim())
.map((item) => ({
name: item.name.trim(),
description: item.description?.trim() || null,
doctorId: doctor.id,
})),
});
}
res.status(201).json({
success: true,
message: 'Doctor created successfully',
message: "Doctor created successfully",
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to create doctor',
message: "Failed to create doctor",
});
}
};
//update doctors
export const updateDoctor = async (req, res) => {
try {
const { doctorId, action } = req.params;
const {
name,
designation,
image,
workingStatus,
qualification,
isActive,
isFeatured,
globalSortOrder,
departments,
experience,
professionalSummary,
seoTitle,
metaDescription,
ogTitle,
ogDescription,
focusKeyphrase,
slug,
tags,
ogImage,
specializations,
} = req.body;
if (!doctorId) {
return res.status(400).json({
success: false,
message: 'Doctor ID is required',
});
}
const doctor = await prisma.doctor.findUnique({ where: { doctorId } });
if (!doctor) return res.status(404).json({ success: false, message: 'Doctor not found' });
if (action === 'toggleStatus') {
await prisma.doctor.update({
where: { id: doctor.id },
data: {
isActive: !doctor.isActive,
},
});
return res.status(200).json({
success: true,
message: `Doctor has been ${doctor.isActive ? 'deactivated' : 'activated'} successfully`,
});
}
if (action === 'toggleFeatured') {
await prisma.doctor.update({
where: { id: doctor.id },
data: {
isFeatured: !doctor.isFeatured,
},
});
return res.status(200).json({
success: true,
message: `Doctor has been ${doctor.isFeatured ? 'removed from featured' : 'marked as featured'} successfully`,
});
}
const messages = [];
if (!doctorId) messages.push('Doctor ID is required');
if (!name?.trim()) messages.push('Doctor name is required');
if (!qualification?.trim()) messages.push('Qualification is required');
if (!designation?.trim()) messages.push('Designation is required');
if (!departments || departments.length === 0) {
messages.push('At least one department is required');
}
if (messages.length > 0) {
return res.status(400).json({
success: false,
message: messages.join(', '),
});
}
await prisma.doctor.update({
where: { id: doctor.id },
data: {
name,
designation,
image,
workingStatus,
qualification,
isActive: isActive !== undefined ? isActive : undefined,
isFeatured: isFeatured !== undefined ? isFeatured : undefined,
experience: experience ? Number(experience) : null,
professionalSummary,
globalSortOrder: globalSortOrder !== undefined ? Number(globalSortOrder) : undefined,
},
});
if (doctor.seoId) {
await prisma.seo.update({
where: {
id: doctor.seoId,
},
data: {
seoTitle,
metaDescription,
ogTitle,
ogDescription,
ogImage,
focusKeyphrase,
slug: slug ? slug : null,
tags: tags || [],
},
});
} else {
const seo = await prisma.seo.create({
data: {
ogImage,
metaDescription,
seoTitle,
ogDescription,
ogTitle,
focusKeyphrase,
slug: slug ? slug : null,
tags: tags || [],
},
});
await prisma.doctor.update({
where: {
id: doctor.id,
},
data: {
seoId: seo.id,
},
});
}
// Update Departments & Timings
if (Array.isArray(departments)) {
const oldRelations = await prisma.doctorDepartment.findMany({
where: {
doctorId: doctor.id,
},
include: {
timing: true,
},
});
// Delete old timings
for (const rel of oldRelations) {
if (rel.timing) {
await prisma.doctorTiming.deleteMany({
where: {
doctorDepartmentId: rel.id,
},
});
}
}
// Delete old departments
await prisma.doctorDepartment.deleteMany({
where: {
doctorId: doctor.id,
},
});
// Recreate departments + timings
for (const dep of departments) {
const department = await prisma.department.findUnique({
where: {
departmentId: dep.departmentId,
},
});
if (!department) continue;
const doctorDepartment = await prisma.doctorDepartment.create({
data: {
doctorId: doctor.id,
departmentId: department.id,
sortOrder: dep.sortOrder !== undefined ? Number(dep.sortOrder) : 0,
},
});
if (dep.timing && Object.keys(dep.timing).length > 0) {
const { id, doctorDepartmentId, createdAt, updatedAt, ...cleanTiming } = dep.timing;
await prisma.doctorTiming.create({
data: {
doctorDepartmentId: doctorDepartment.id,
...cleanTiming,
},
});
}
}
}
// Update Specializations
if (Array.isArray(specializations)) {
await prisma.doctorSpecialization.deleteMany({
where: {
doctorId: doctor.id,
},
});
if (specializations.length) {
await prisma.doctorSpecialization.createMany({
data: specializations
.filter((item) => item.name?.trim())
.map((item) => ({
name: item.name.trim(),
description: item.description?.trim() || null,
doctorId: doctor.id,
})),
});
}
}
res.status(200).json({ success: true, message: 'Doctor updated successfully' });
} catch (error) {
console.error('Update Error:', error);
res.status(500).json({ success: false, message: 'Failed to update doctor' });
}
};
//delete doctor
export const deleteDoctor = async (req, res) => {
try {
const {doctorId} = req.params;
const {name, designation, workingStatus, qualification, departments} =
req.body;
const doctor = await prisma.doctor.findUnique({
where: {doctorId},
@@ -589,7 +186,91 @@ export const deleteDoctor = async (req, res) => {
if (!doctor) {
return res.status(404).json({
success: false,
message: 'Doctor not found',
message: "Doctor not found",
});
}
await prisma.doctor.update({
where: {id: doctor.id},
data: {
name,
designation,
workingStatus,
qualification,
},
});
const oldRelations = await prisma.doctorDepartment.findMany({
where: {doctorId: doctor.id},
});
for (const rel of oldRelations) {
await prisma.doctorTiming.deleteMany({
where: {doctorDepartmentId: rel.id},
});
}
await prisma.doctorDepartment.deleteMany({
where: {doctorId: doctor.id},
});
for (const dep of departments) {
const department = await prisma.department.findUnique({
where: {departmentId: dep.departmentId},
});
if (!department) continue;
const doctorDepartment = await prisma.doctorDepartment.create({
data: {
doctorId: doctor.id,
departmentId: department.id,
},
});
if (dep.timing) {
await prisma.doctorTiming.create({
data: {
doctorDepartmentId: doctorDepartment.id,
...dep.timing,
},
});
}
}
res.status(200).json({
success: true,
message: "Doctor updated successfully",
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to update doctor",
});
}
};
//delete doctor
export const deleteDoctor = async (req, res) => {
try {
const {doctorId} = req.params;
if (!doctorId) {
return res.status(400).json({
success: false,
message: "Doctor ID is required",
});
}
const doctor = await prisma.doctor.findUnique({
where: {doctorId},
});
if (!doctor) {
return res.status(404).json({
success: false,
message: `Doctor with ID ${doctorId} not found`,
});
}
@@ -613,13 +294,13 @@ export const deleteDoctor = async (req, res) => {
res.status(200).json({
success: true,
message: 'Doctor deleted successfully',
message: `Doctor ${doctorId} deleted successfully`,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to delete doctor',
message: "Failed to delete doctor",
});
}
};
@@ -639,19 +320,23 @@ export const getDoctorTimings = async (req, res) => {
});
const result = doctors.map((doc) => {
const timing = doc.departments[0]?.timing || {};
let timing = {};
if (doc.departments.length > 0) {
timing = doc.departments[0].timing ?? {};
}
return {
Doctor_ID: doc.doctorId,
Doctor: doc.name,
Monday: timing.monday || '',
Tuesday: timing.tuesday || '',
Wednesday: timing.wednesday || '',
Thursday: timing.thursday || '',
Friday: timing.friday || '',
Saturday: timing.saturday || '',
Sunday: timing.sunday || '',
Additional: timing.additional || '',
Monday: timing?.monday ?? "",
Tuesday: timing?.tuesday ?? "",
Wednesday: timing?.wednesday ?? "",
Thursday: timing?.thursday ?? "",
Friday: timing?.friday ?? "",
Saturday: timing?.saturday ?? "",
Sunday: timing?.sunday ?? "",
Additional: timing?.additional ?? "",
};
});
@@ -663,7 +348,7 @@ export const getDoctorTimings = async (req, res) => {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to fetch doctor timings',
message: "Failed to fetch doctor timings",
});
}
};
@@ -672,13 +357,9 @@ export const getDoctorTimings = async (req, res) => {
export const getDoctorTimingById = async (req, res) => {
try {
const {doctorId} = req.params;
const { admin } = req.query;
const doctor = await prisma.doctor.findFirst({
where: {
doctorId,
...(admin === 'true' ? {} : { isActive: true }),
},
const doctor = await prisma.doctor.findUnique({
where: {doctorId},
include: {
departments: {
include: {
@@ -692,19 +373,33 @@ export const getDoctorTimingById = async (req, res) => {
if (!doctor) {
return res.status(404).json({
success: false,
message: 'Doctor not found',
message: "Doctor not found",
});
}
const result = {
doctorId: doctor.doctorId,
doctorName: doctor.name,
departments: doctor.departments.map((d) => ({
departments: doctor.departments.map((d) => {
const t = d.timing || {};
return {
departmentId: d.department.departmentId,
departmentName: d.department.name,
deptSortOrder: d.sortOrder,
timing: d.timing || {},
})),
timing: {
monday: t.monday || "",
tuesday: t.tuesday || "",
wednesday: t.wednesday || "",
thursday: t.thursday || "",
friday: t.friday || "",
saturday: t.saturday || "",
sunday: t.sunday || "",
additional: t.additional || "",
},
};
}),
};
res.status(200).json({
@@ -715,56 +410,7 @@ export const getDoctorTimingById = async (req, res) => {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to fetch doctor timing',
});
}
};
export const getFeaturedDoctors = async (req, res) => {
try {
const doctors = await prisma.doctor.findMany({
where: {
isActive: true,
isFeatured: true,
},
include: {
seo: {
select: {
slug: true,
},
},
departments: {
include: {
department: true,
},
},
},
orderBy: [{ globalSortOrder: 'asc' }, { name: 'asc' }],
});
const data = doctors.map((doc) => ({
doctorId: doc.doctorId,
name: doc.name,
image: doc.image ?? '',
designation: doc.designation,
qualification: doc.qualification,
experience: doc.experience,
slug: doc.seo?.slug ?? '',
departments: doc.departments.map((d) => ({
departmentId: d.department.departmentId,
departmentName: d.department.name,
})),
}));
res.status(200).json({
success: true,
data,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to fetch featured doctors',
message: "Failed to fetch doctor timing",
});
}
};
@@ -1,4 +1,4 @@
import prisma from '../prisma/client.js';
import prisma from "../prisma/client.js";
// CREATE
export const createEmailConfig = async (req, res) => {
@@ -8,7 +8,7 @@ export const createEmailConfig = async (req, res) => {
if (!name || !email || !type) {
return res.status(400).json({
success: false,
message: 'Name, Email and Type are required',
message: "Name, Email and Type are required",
});
}
@@ -23,14 +23,14 @@ export const createEmailConfig = async (req, res) => {
res.status(201).json({
success: true,
message: 'Email config created',
message: "Email config created",
data: newEmail,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to create email config',
message: "Failed to create email config",
});
}
};
@@ -40,7 +40,7 @@ export const getEmailConfigs = async (req, res) => {
try {
const emails = await prisma.emailConfig.findMany({
orderBy: {
createdAt: 'desc',
createdAt: "desc",
},
});
@@ -52,7 +52,7 @@ export const getEmailConfigs = async (req, res) => {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to fetch email configs',
message: "Failed to fetch email configs",
});
}
};
@@ -71,7 +71,7 @@ export const getEmailConfig = async (req, res) => {
if (!email) {
return res.status(404).json({
success: false,
message: 'Email config not found',
message: "Email config not found",
});
}
@@ -83,7 +83,7 @@ export const getEmailConfig = async (req, res) => {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to fetch email config',
message: "Failed to fetch email config",
});
}
};
@@ -102,14 +102,14 @@ export const updateEmailConfig = async (req, res) => {
res.status(200).json({
success: true,
message: 'Email config updated',
message: "Email config updated",
data: updated,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to update email config',
message: "Failed to update email config",
});
}
};
@@ -127,13 +127,13 @@ export const deleteEmailConfig = async (req, res) => {
res.status(200).json({
success: true,
message: 'Email config deleted',
message: "Email config deleted",
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to delete email config',
message: "Failed to delete email config",
});
}
};
@@ -1,518 +0,0 @@
import prisma from '../prisma/client.js';
import { sendEmail } from '../utils/sendEmail.js';
import { getEmailsByType } from '../utils/getEmailByTypes.js';
export const getAllCategories = async (req, res) => {
try {
const { admin } = req.query;
const categories = await prisma.healthCheckCategory.findMany({
where: admin === 'true' ? {} : { isActive: true },
orderBy: { sortOrder: 'asc' },
include: {
_count: { select: { packages: true } },
},
});
return res.status(200).json({ success: true, data: categories });
} catch (error) {
return res.status(500).json({ success: false, message: 'Failed to fetch categories' });
}
};
export const createCategory = async (req, res) => {
try {
const { name, slug, description, isActive, sortOrder } = req.body;
const category = await prisma.healthCheckCategory.create({
data: {
name,
slug: slug || null,
description,
isActive: isActive ?? true,
sortOrder: sortOrder ? Number(sortOrder) : 1000,
},
});
return res.status(201).json({ success: true, message: 'Category created', data: category });
} catch (error) {
console.error(error);
return res.status(500).json({ success: false, message: 'Failed to create category' });
}
};
export const updateCategory = async (req, res) => {
try {
const { id } = req.params;
const data = { ...req.body };
delete data.id;
delete data._count;
delete data.createdAt;
delete data.updatedAt;
if (data.sortOrder !== undefined) data.sortOrder = Number(data.sortOrder);
if (data.slug === '') data.slug = null;
const updatedCategory = await prisma.$transaction(async (tx) => {
const category = await tx.healthCheckCategory.update({
where: { id: Number(id) },
data,
});
if (data.isActive === false) {
await tx.healthPackage.updateMany({
where: { categoryId: Number(id) },
data: { isActive: false },
});
}
return category;
});
return res.status(200).json({
success: true,
message: 'Category updated',
data: updatedCategory,
});
} catch (error) {
console.error(error);
return res.status(500).json({ success: false, message: 'Failed to update category' });
}
};
export const deleteCategory = async (req, res) => {
try {
const { id } = req.params;
await prisma.healthCheckCategory.delete({
where: { id: Number(id) },
});
return res.status(200).json({ success: true, message: 'Category deleted successfully' });
} catch (error) {
console.error(error);
return res.status(500).json({
success: false,
message: 'Failed to delete category. Ensure no packages are linked to it.',
});
}
};
export const getAllPackages = async (req, res) => {
try {
const { admin, categorySlug } = req.query;
const packages = await prisma.healthPackage.findMany({
where: {
AND: [admin === 'true' ? {} : { isActive: true }, categorySlug ? { category: { slug: categorySlug } } : {}],
},
include: {
category: true,
seo: true,
},
orderBy: [{ sortOrder: 'asc' }, { createdAt: 'desc' }],
});
return res.status(200).json({ success: true, data: packages });
} catch (error) {
console.error(error);
return res.status(500).json({ success: false, message: 'Failed to fetch packages' });
}
};
export const createPackage = async (req, res) => {
try {
const {
name,
slug,
description,
price,
image,
discountedPrice,
inclusions,
categoryId,
isActive,
isFeatured,
sortOrder,
seo,
} = req.body;
const healthPackage = await prisma.healthPackage.create({
data: {
name,
slug,
description,
price,
image,
discountedPrice,
inclusions,
categoryId: Number(categoryId),
isActive: isActive ?? true,
isFeatured: isFeatured ?? false,
sortOrder: sortOrder ? Number(sortOrder) : 1000,
...(seo && {
seo: {
create: {
seoTitle: seo.seoTitle,
metaDescription: seo.metaDescription,
focusKeyphrase: seo.focusKeyphrase,
slug: slug,
tags: seo.tags || [],
ogTitle: seo.ogTitle,
ogDescription: seo.ogDescription,
ogImage: seo.ogImage,
},
},
}),
},
include: {
category: true,
seo: true,
},
});
return res.status(201).json({ success: true, message: 'Package created', data: healthPackage });
} catch (error) {
console.error(error);
return res.status(500).json({ success: false, message: 'Failed to create package' });
}
};
export const updatePackage = async (req, res) => {
try {
const { id } = req.params;
const data = { ...req.body };
delete data.id;
delete data.category;
delete data.createdAt;
delete data.updatedAt;
delete data.seoId;
if (data.categoryId) data.categoryId = Number(data.categoryId);
if (data.sortOrder) data.sortOrder = Number(data.sortOrder);
const existingPackage = await prisma.healthPackage.findUnique({
where: { id: Number(id) },
select: { slug: true },
});
const seoSlug = data.slug || existingPackage.slug;
const updated = await prisma.healthPackage.update({
where: { id: Number(id) },
data: {
...data,
seo: data.seo
? {
upsert: {
create: {
seoTitle: data.seo.seoTitle,
metaDescription: data.seo.metaDescription,
focusKeyphrase: data.seo.focusKeyphrase,
slug: seoSlug,
tags: data.seo.tags || [],
ogTitle: data.seo.ogTitle,
ogDescription: data.seo.ogDescription,
ogImage: data.seo.ogImage,
},
update: {
seoTitle: data.seo.seoTitle,
metaDescription: data.seo.metaDescription,
focusKeyphrase: data.seo.focusKeyphrase,
slug: seoSlug,
tags: data.seo.tags || [],
ogTitle: data.seo.ogTitle,
ogDescription: data.seo.ogDescription,
ogImage: data.seo.ogImage,
},
},
}
: undefined,
},
include: {
category: true,
seo: true,
},
});
return res.status(200).json({ success: true, message: 'Package updated', data: updated });
} catch (error) {
console.error(error);
return res.status(500).json({ success: false, message: 'Update failed' });
}
};
export const deletePackage = async (req, res) => {
try {
const { id } = req.params;
await prisma.healthPackage.delete({
where: { id: Number(id) },
});
return res.status(200).json({
success: true,
message: 'Package deleted',
});
} catch (error) {
console.error(error);
return res.status(500).json({
success: false,
message: 'Delete failed',
});
}
};
export const createPackageInquiry = async (req, res) => {
try {
const { fullName, mobileNumber, email, age, gender, preferredDate, packageId, message } = req.body;
const inquiry = await prisma.healthPackageInquiry.create({
data: {
fullName,
mobileNumber,
email,
age: age ? Number(age) : null,
gender,
preferredDate: preferredDate ? new Date(preferredDate) : null,
message,
packageId: Number(packageId),
},
include: {
healthPackage: true,
},
});
try {
const emailList = await getEmailsByType('HCINQUIRY');
if (emailList) {
await sendEmail({
to: emailList,
subject: 'New Health Checkup Package Inquiry',
html: `
<div style="font-family: Arial, sans-serif; background-color: #f4f6f8; padding: 20px;">
<div style="max-width: 600px; margin: auto; background: #ffffff; border-radius: 10px; overflow: hidden; box-shadow: 0 4px 10px rgba(0,0,0,0.05);">
<!-- Header -->
<div style="background-color: #0d6efd; color: #ffffff; padding: 20px;">
<h2 style="margin: 0;">GG Hospital</h2>
<p style="margin: 5px 0 0; font-size: 14px;">
New Health Checkup Package Inquiry
</p>
</div>
<!-- Body -->
<div style="padding: 20px; color: #333;">
<h3 style="margin-top: 0;">Inquirer Details</h3>
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 8px 0; width: 35%;"><b>Name:</b></td>
<td style="padding: 8px 0;">${fullName}</td>
</tr>
<tr>
<td style="padding: 8px 0;"><b>Phone:</b></td>
<td style="padding: 8px 0;">${mobileNumber}</td>
</tr>
<tr>
<td style="padding: 8px 0;"><b>Email:</b></td>
<td style="padding: 8px 0;">${email || '-'}</td>
</tr>
<tr>
<td style="padding: 8px 0;"><b>Age:</b></td>
<td style="padding: 8px 0;">${age || '-'}</td>
</tr>
<tr>
<td style="padding: 8px 0;"><b>Gender:</b></td>
<td style="padding: 8px 0;">${gender || '-'}</td>
</tr>
</table>
<h3 style="margin-top: 20px;">Package Details</h3>
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 8px 0; width: 35%;"><b>Package Name:</b></td>
<td style="padding: 8px 0;">${inquiry.healthPackage?.name || 'Unknown Package'}</td>
</tr>
<tr>
<td style="padding: 8px 0;"><b>Preferred Date:</b></td>
<td style="padding: 8px 0;">
${
preferredDate
? new Date(preferredDate).toLocaleDateString('en-GB', {
day: '2-digit',
month: 'long',
year: 'numeric',
})
: 'Not specified'
}
</td>
</tr>
</table>
<!-- Message Box -->
<div style="margin-top: 20px;">
<h3>Message</h3>
<div style="
background: #f8f9fa;
padding: 15px;
border-radius: 6px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
overflow-wrap: anywhere;
">
${message ? message.replace(/\n/g, '<br/>') : '-'}
</div>
</div>
</div>
<!-- Footer -->
<div style="background: #f1f1f1; padding: 15px; text-align: center; font-size: 12px; color: #666;">
This inquiry was submitted via the GG Hospital website.
</div>
</div>
</div>
`,
});
}
} catch (err) {
console.error('Email failed:', err);
}
return res.status(201).json({
success: true,
message: 'Booking inquiry sent successfully',
data: inquiry,
});
} catch (error) {
console.error(error);
return res.status(500).json({ success: false, message: 'Failed to submit inquiry' });
}
};
export const getPackageBySlug = async (req, res) => {
try {
const { slug } = req.params;
const healthPackage = await prisma.healthPackage.findFirst({
where: { slug, isActive: true },
include: {
category: true,
seo: true,
},
});
if (!healthPackage) {
return res.status(404).json({ success: false, message: 'Package not found' });
}
return res.status(200).json({
success: true,
data: healthPackage,
});
} catch (error) {
console.error(error);
return res.status(500).json({ success: false, message: 'Failed to fetch package' });
}
};
export const getAllInquiries = async (req, res) => {
try {
const { page = 1, limit = 10, filterDate, startDate, endDate } = req.query;
const queryPage = parseInt(page);
const queryLimit = parseInt(limit);
const skip = (queryPage - 1) * queryLimit;
let where = {};
if (filterDate) {
where.preferredDate = {
gte: new Date(`${filterDate}T00:00:00.000Z`),
lte: new Date(`${filterDate}T23:59:59.999Z`),
};
} else if (startDate || endDate) {
where.preferredDate = {};
if (startDate) {
where.preferredDate.gte = new Date(`${startDate}T00:00:00.000Z`);
}
if (endDate) {
where.preferredDate.lte = new Date(`${endDate}T23:59:59.999Z`);
}
}
const [total, inquiries] = await prisma.$transaction([
prisma.healthPackageInquiry.count({ where }),
prisma.healthPackageInquiry.findMany({
where,
skip,
take: queryLimit,
include: {
healthPackage: {
include: {
category: true,
},
},
},
orderBy: { createdAt: 'desc' },
}),
]);
return res.status(200).json({
success: true,
data: inquiries,
pagination: {
total,
page: queryPage,
limit: queryLimit,
totalPages: Math.ceil(total / queryLimit),
},
});
} catch (error) {
console.error(error);
return res.status(500).json({ success: false, message: 'Failed to fetch inquiries' });
}
};
export const getFeaturedPackages = async (req, res) => {
try {
const packages = await prisma.healthPackage.findMany({
where: {
isActive: true,
isFeatured: true,
category: {
isActive: true,
},
},
include: {
category: true,
seo: true,
},
orderBy: [{ sortOrder: 'asc' }, { createdAt: 'desc' }],
});
return res.status(200).json({
success: true,
data: packages,
});
} catch (error) {
console.error(error);
return res.status(500).json({
success: false,
message: 'Failed to fetch featured packages',
});
}
};
@@ -1,203 +0,0 @@
import prisma from '../prisma/client.js';
export const createHomepageBanner = async (req, res) => {
try {
const {
title,
subtitle,
mediaType,
desktopMediaUrl,
mobileMediaUrl,
buttonText,
buttonLink,
openInNewTab,
textAlignment,
sortOrder,
isActive,
} = req.body;
if (!mediaType || !desktopMediaUrl) {
return res.status(400).json({
success: false,
message: 'Media type and desktop media URL are required',
});
}
const banner = await prisma.homepageBanner.create({
data: {
title,
subtitle,
mediaType,
desktopMediaUrl,
mobileMediaUrl,
buttonText,
buttonLink,
openInNewTab,
textAlignment,
sortOrder,
isActive,
},
});
res.status(201).json({
success: true,
data: banner,
message: 'Homepage banner created successfully',
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to create homepage banner',
});
}
};
export const getHomepageBanners = async (req, res) => {
try {
const banners = await prisma.homepageBanner.findMany({
orderBy: {
sortOrder: 'asc',
},
});
res.json({
success: true,
data: banners,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to fetch homepage banners',
});
}
};
export const getActiveHomepageBanners = async (req, res) => {
try {
const banners = await prisma.homepageBanner.findMany({
where: {
isActive: true,
},
orderBy: {
sortOrder: 'asc',
},
});
res.json({
success: true,
data: banners,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to fetch active homepage banners',
});
}
};
export const getHomepageBanner = async (req, res) => {
try {
const { id } = req.params;
const banner = await prisma.homepageBanner.findUnique({
where: {
id: Number(id),
},
});
if (!banner) {
return res.status(404).json({
success: false,
message: 'Homepage banner not found',
});
}
res.json({
success: true,
data: banner,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to fetch homepage banner',
});
}
};
export const updateHomepageBanner = async (req, res) => {
try {
const { id } = req.params;
const {
title,
subtitle,
mediaType,
desktopMediaUrl,
mobileMediaUrl,
buttonText,
buttonLink,
openInNewTab,
textAlignment,
sortOrder,
isActive,
} = req.body;
const banner = await prisma.homepageBanner.update({
where: {
id: Number(id),
},
data: {
title,
subtitle,
mediaType,
desktopMediaUrl,
mobileMediaUrl,
buttonText,
buttonLink,
openInNewTab,
textAlignment,
sortOrder,
isActive,
},
});
res.json({
success: true,
data: banner,
message: 'Homepage banner updated successfully',
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to update homepage banner',
});
}
};
export const deleteHomepageBanner = async (req, res) => {
try {
const { id } = req.params;
await prisma.homepageBanner.delete({
where: {
id: Number(id),
},
});
res.json({
success: true,
message: 'Homepage banner deleted successfully',
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to delete homepage banner',
});
}
};
-292
View File
@@ -1,292 +0,0 @@
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 doctorName = row.Doctor?.toString();
const departmentName = row['Department Id']?.toString();
const doctor = await prisma.doctor.findFirst({
where: { name: doctorName },
});
const department = await prisma.department.findFirst({
where: { name: departmentName },
});
const parseDate = (value) => {
if (!value) return new Date();
// Excel numeric date
if (typeof value === 'number') {
return new Date((value - 25569) * 86400 * 1000);
}
if (typeof value === 'string') {
const v = value.trim();
// Handle DD/MM/YYYY
const ddmmyyyy = /^(\d{2})\/(\d{2})\/(\d{4})$/;
const match = v.match(ddmmyyyy);
if (match) {
const [_, dd, mm, yyyy] = match;
return new Date(`${yyyy}-${mm}-${dd}`);
}
// Fallback (ISO or other valid formats)
const d = new Date(v);
if (!isNaN(d.getTime()) && d.getFullYear() < 2100) {
return d;
}
}
console.warn('⚠️ Invalid date, using current date:', value);
return new Date();
};
if (doctor && department) {
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: parseDate(row.Date),
doctorId: doctor.doctorId,
departmentId: department.departmentId,
},
});
}
}
}
// 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 });
}
};
+10 -87
View File
@@ -1,7 +1,4 @@
import prisma from '../prisma/client.js';
import { sendEmail } from '../utils/sendEmail.js';
import { getEmailsByType } from '../utils/getEmailByTypes.js';
import prisma from "../prisma/client.js";
/* CREATE INQUIRY */
export const createInquiry = async (req, res) => {
@@ -11,7 +8,7 @@ export const createInquiry = async (req, res) => {
if (!fullName || !number) {
return res.status(400).json({
success: false,
message: 'Full name and number are required',
message: "Full name and number are required",
});
}
@@ -24,92 +21,18 @@ export const createInquiry = async (req, res) => {
message,
},
});
try {
const emailList = await getEmailsByType('INQUIRY');
if (emailList && emailList.length > 0) {
await sendEmail({
to: emailList,
subject: 'New Inquiry Received',
html: `
<div style="font-family: Arial, sans-serif; background-color: #f4f6f8; padding: 20px;">
<div style="max-width: 600px; margin: auto; background: #ffffff; border-radius: 10px; overflow: hidden; box-shadow: 0 4px 10px rgba(0,0,0,0.05);">
<!-- Header -->
<div style="background-color: #0d6efd; color: #ffffff; padding: 20px;">
<h2 style="margin: 0;">GG Hospital</h2>
<p style="margin: 5px 0 0; font-size: 14px;">New Inquiry Received</p>
</div>
<!-- Body -->
<div style="padding: 20px; color: #333;">
<h3 style="margin-top: 0;">Contact Details</h3>
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 8px 0;"><b>Name:</b></td>
<td style="padding: 8px 0;">${fullName}</td>
</tr>
<tr>
<td style="padding: 8px 0;"><b>Phone:</b></td>
<td style="padding: 8px 0;">${number}</td>
</tr>
<tr>
<td style="padding: 8px 0;"><b>Email:</b></td>
<td style="padding: 8px 0;">${emailId}</td>
</tr>
<tr>
<td style="padding: 8px 0;"><b>Subject:</b></td>
<td style="padding: 8px 0;">${subject}</td>
</tr>
</table>
<!-- Message Box -->
<div style="margin-top: 20px;">
<h3>Message</h3>
<div style="
background: #f8f9fa;
padding: 15px;
border-radius: 6px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
overflow-wrap: anywhere;
">
${message ? message.replace(/\n/g, '<br/>') : '-'}
</div>
</div>
</div>
<!-- Footer -->
<div style="background: #f1f1f1; padding: 15px; text-align: center; font-size: 12px; color: #666;">
This message was sent from the GG Hospital website contact form.
</div>
</div>
</div>
`,
});
}
} catch (err) {
console.error('Inquiry email failed:', err);
}
res.status(200).json({
success: true,
status: 200,
data: inquiry,
message: 'Inquiry added successfully',
message: "Inquiry added successfully",
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to add inquiry',
message: "Failed to add inquiry",
});
}
};
@@ -119,7 +42,7 @@ export const getInquiries = async (req, res) => {
try {
const inquiries = await prisma.inquiry.findMany({
orderBy: {
createdAt: 'desc',
createdAt: "desc",
},
});
@@ -130,7 +53,7 @@ export const getInquiries = async (req, res) => {
} catch (error) {
res.status(500).json({
success: false,
message: 'Failed to fetch inquiries',
message: "Failed to fetch inquiries",
});
}
};
@@ -147,7 +70,7 @@ export const getInquiry = async (req, res) => {
if (!inquiry) {
return res.status(404).json({
success: false,
message: 'Inquiry not found',
message: "Inquiry not found",
});
}
@@ -158,7 +81,7 @@ export const getInquiry = async (req, res) => {
} catch (error) {
res.status(500).json({
success: false,
message: 'Failed to fetch inquiry',
message: "Failed to fetch inquiry",
});
}
};
@@ -174,12 +97,12 @@ export const deleteInquiry = async (req, res) => {
res.json({
success: true,
message: 'Inquiry deleted successfully',
message: "Inquiry deleted successfully",
});
} catch (error) {
res.status(500).json({
success: false,
message: 'Failed to delete inquiry',
message: "Failed to delete inquiry",
});
}
};
@@ -1,173 +0,0 @@
import prisma from '../prisma/client.js';
export const createInsurancePartner = async (req, res) => {
try {
const { name, logo, websiteUrl, sortOrder, isActive } = req.body;
if (!name || !logo) {
return res.status(400).json({
success: false,
message: 'Name and logo are required',
});
}
const partner = await prisma.insurancePartner.create({
data: {
name,
logo,
websiteUrl,
sortOrder,
isActive,
},
});
res.status(201).json({
success: true,
data: partner,
message: 'Insurance partner created successfully',
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to create insurance partner',
});
}
};
export const getInsurancePartners = async (req, res) => {
try {
const partners = await prisma.insurancePartner.findMany({
orderBy: {
sortOrder: 'asc',
},
});
res.json({
success: true,
data: partners,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to fetch insurance partners',
});
}
};
export const getActiveInsurancePartners = async (req, res) => {
try {
const partners = await prisma.insurancePartner.findMany({
where: {
isActive: true,
},
orderBy: {
sortOrder: 'asc',
},
});
res.json({
success: true,
data: partners,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to fetch insurance partners',
});
}
};
export const getInsurancePartner = async (req, res) => {
try {
const { id } = req.params;
const partner = await prisma.insurancePartner.findUnique({
where: {
id: Number(id),
},
});
if (!partner) {
return res.status(404).json({
success: false,
message: 'Insurance partner not found',
});
}
res.json({
success: true,
data: partner,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to fetch insurance partner',
});
}
};
export const updateInsurancePartner = async (req, res) => {
try {
const { id } = req.params;
const { name, logo, websiteUrl, sortOrder, isActive } = req.body;
const partner = await prisma.insurancePartner.update({
where: {
id: Number(id),
},
data: {
name,
logo,
websiteUrl,
sortOrder,
isActive,
},
});
res.json({
success: true,
data: partner,
message: 'Insurance partner updated successfully',
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to update insurance partner',
});
}
};
export const deleteInsurancePartner = async (req, res) => {
try {
const { id } = req.params;
await prisma.insurancePartner.delete({
where: {
id: Number(id),
},
});
res.json({
success: true,
message: 'Insurance partner deleted successfully',
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to delete insurance partner',
});
}
};
+44 -64
View File
@@ -1,44 +1,46 @@
import prisma from '../prisma/client.js';
import prisma from "../prisma/client.js";
// GET ALL NEWS
export const getAllNews = async (req, res) => {
try {
const page = req.query.page ? parseInt(req.query.page) : null;
const limit = req.query.limit ? parseInt(req.query.limit) : null;
const search = req.query.search?.trim() || '';
const page = parseInt(req.query.page);
const limit = parseInt(req.query.limit);
const includeImages = {
images: true,
};
if (!page && !limit) {
const news = await prisma.newsMedia.findMany({
orderBy: { createdAt: "desc" },
});
const searchFilter = search
? {
headline: {
contains: search,
mode: 'insensitive',
},
const response = news.map((n) => ({
Id: n.id.toString(),
Headline: n.headline,
Content: n.content,
FirstPara: n.firstPara,
SecondPara: n.secondPara,
Date: n.date,
Author: n.author,
}));
return res.status(200).json({
success: true,
data: response,
meta: null,
});
}
: {};
const whereCondition = {
...searchFilter,
};
const currentPage = page || 1;
const currentLimit = limit || 10;
const skip = page && limit ? (page - 1) * limit : undefined;
const take = limit ? limit : undefined;
const skip = (currentPage - 1) * currentLimit;
const [news, total] = await Promise.all([
prisma.newsMedia.findMany({
where: whereCondition,
include: includeImages,
orderBy: { createdAt: 'desc' },
orderBy: { createdAt: "desc" },
skip,
take,
}),
prisma.newsMedia.count({
where: whereCondition,
take: currentLimit,
}),
prisma.newsMedia.count(),
]);
const response = news.map((n) => ({
@@ -49,10 +51,6 @@ export const getAllNews = async (req, res) => {
SecondPara: n.secondPara,
Date: n.date,
Author: n.author,
Images: n.images.map((img) => ({
id: img.id,
image: img.url,
})),
}));
return res.status(200).json({
@@ -60,19 +58,20 @@ export const getAllNews = async (req, res) => {
data: response,
meta: {
total,
page: page || 1,
limit: limit || total,
totalPages: limit ? Math.ceil(total / limit) : 1,
page: currentPage,
limit: currentLimit,
totalPages: Math.ceil(total / currentLimit),
},
});
} catch (error) {
console.error(error);
return res.status(500).json({
success: false,
message: 'Failed to fetch news',
message: "Failed to fetch news",
});
}
};
// GET NEWS BY ID
export const getNewsById = async (req, res) => {
@@ -81,13 +80,12 @@ export const getNewsById = async (req, res) => {
const n = await prisma.newsMedia.findUnique({
where: { id: Number(id) },
include: { images: true },
});
if (!n) {
return res.status(404).json({
success: false,
message: 'News not found',
message: "News not found",
});
}
@@ -99,10 +97,6 @@ export const getNewsById = async (req, res) => {
SecondPara: n.secondPara,
Date: n.date,
Author: n.author,
Images: n.images.map((img) => ({
id: img.id,
image: img.url,
})),
};
return res.status(200).json({
@@ -113,7 +107,7 @@ export const getNewsById = async (req, res) => {
console.error(error);
return res.status(500).json({
success: false,
message: 'Failed to fetch news',
message: "Failed to fetch news",
});
}
};
@@ -122,12 +116,12 @@ export const getNewsById = async (req, res) => {
export const createNews = async (req, res) => {
try {
const { headline, content, firstPara, secondPara, date, author, imageUrls } = req.body;
const { headline, content, firstPara, secondPara, date, author } = req.body;
if (!headline) {
return res.status(400).json({
success: false,
message: 'Headline is required',
message: "Headline is required",
});
}
@@ -139,25 +133,19 @@ export const createNews = async (req, res) => {
secondPara,
date: date ? new Date(date) : null,
author,
images: imageUrls
? {
create: imageUrls.map((url) => ({ url })),
}
: undefined,
},
include: { images: true },
});
return res.status(201).json({
success: true,
message: 'News created successfully',
message: "News created successfully",
data: news,
});
} catch (error) {
console.error(error);
return res.status(500).json({
success: false,
message: 'Failed to create news',
message: "Failed to create news",
});
}
};
@@ -167,33 +155,25 @@ export const createNews = async (req, res) => {
export const updateNews = async (req, res) => {
try {
const { id } = req.params;
const { imageUrls, ...otherData } = req.body;
const news = await prisma.newsMedia.update({
where: { id: Number(id) },
data: {
...otherData,
...req.body,
date: req.body.date ? new Date(req.body.date) : undefined,
images: imageUrls
? {
deleteMany: {},
create: imageUrls.map((url) => ({ url })),
}
: undefined,
},
include: { images: true },
});
return res.status(200).json({
success: true,
message: 'News updated successfully',
message: "News updated successfully",
data: news,
});
} catch (error) {
console.error(error);
return res.status(500).json({
success: false,
message: 'Failed to update news',
message: "Failed to update news",
});
}
};
@@ -210,13 +190,13 @@ export const deleteNews = async (req, res) => {
return res.status(200).json({
success: true,
message: 'News deleted successfully',
message: "News deleted successfully",
});
} catch (error) {
console.error(error);
return res.status(500).json({
success: false,
message: 'Failed to delete news',
message: "Failed to delete news",
});
}
};
+3 -3
View File
@@ -1,9 +1,9 @@
import multer from 'multer';
import path from 'path';
import multer from "multer";
import path from "path";
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, 'uploads/blog');
cb(null, "uploads/blog");
},
filename: function (req, file, cb) {
+5 -5
View File
@@ -1,19 +1,19 @@
import { verifyToken } from '../utils/jwt.js';
import {verifyToken} from "../utils/jwt.js";
export default function jwtAuthMiddleware(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return res.status(401).json({error: "No token provided"});
}
const token = authHeader.split(' ')[1];
const token = authHeader.split(" ")[1];
try {
const user = verifyToken(token);
req.user = user;
next();
} catch (err) {
return res.status(401).json({ error: 'Invalid or expired token' });
return res.status(401).json({error: "Invalid or expired token"});
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { PrismaClient } from '@prisma/client';
import {PrismaClient} from "@prisma/client";
const prisma = new PrismaClient();
@@ -1,18 +1,18 @@
import express from 'express';
import express from "express";
import {
createAcademicsResearch,
getAcademicsResearch,
getSingleAcademicsResearch,
deleteAcademicsResearch,
} from '../controllers/academicsResearch.controller.js';
} from "../controllers/academicsResearch.controller.js";
import jwtAuthMiddleware from '../middleware/auth.js';
import jwtAuthMiddleware from "../middleware/auth.js";
const router = express.Router();
router.post('/', createAcademicsResearch);
router.get('/getAll', jwtAuthMiddleware, getAcademicsResearch);
router.get('/:id', jwtAuthMiddleware, getSingleAcademicsResearch);
router.delete('/:id', jwtAuthMiddleware, deleteAcademicsResearch);
router.post("/", createAcademicsResearch);
router.get("/getAll", getAcademicsResearch);
router.get("/:id", getSingleAcademicsResearch);
router.delete("/:id", jwtAuthMiddleware, deleteAcademicsResearch);
export default router;
@@ -1,24 +0,0 @@
import express from 'express';
import {
createAccreditation,
getAccreditations,
getActiveAccreditations,
getAccreditation,
updateAccreditation,
deleteAccreditation,
} from '../controllers/accreditation.controller.js';
import jwtAuthMiddleware from '../middleware/auth.js';
const router = express.Router();
router.get('/active', getActiveAccreditations);
router.post('/', jwtAuthMiddleware, createAccreditation);
router.get('/getAll', jwtAuthMiddleware, getAccreditations);
router.get('/:id', jwtAuthMiddleware, getAccreditation);
router.put('/:id', jwtAuthMiddleware, updateAccreditation);
router.delete('/:id', jwtAuthMiddleware, deleteAccreditation);
export default router;
+8 -8
View File
@@ -1,23 +1,23 @@
import express from 'express';
import express from "express";
import {
createAppointment,
getAppointments,
getAppointment,
updateAppointment,
deleteAppointment,
} from '../controllers/appointment.controller.js';
} from "../controllers/appointment.controller.js";
import jwtAuthMiddleware from '../middleware/auth.js';
import jwtAuthMiddleware from "../middleware/auth.js";
const router = express.Router();
/* PUBLIC */
router.get('/getall', jwtAuthMiddleware, getAppointments);
router.post('/', createAppointment);
router.get("/getall", getAppointments);
router.post("/", createAppointment);
router.get('/:id', jwtAuthMiddleware, getAppointment);
router.patch('/:id', jwtAuthMiddleware, updateAppointment);
router.delete('/:id', jwtAuthMiddleware, deleteAppointment);
router.get("/:id", getAppointment);
router.patch("/:id", updateAppointment);
router.delete("/:id", jwtAuthMiddleware, deleteAppointment);
export default router;
+4 -3
View File
@@ -1,8 +1,9 @@
import express from 'express';
import { login } from '../controllers/auth.controller.js';
import express from "express";
import {register, login} from "../controllers/auth.controller.js";
const router = express.Router();
router.post('/login', login);
router.post("/register", register);
router.post("/login", login);
export default router;
+9 -13
View File
@@ -1,4 +1,4 @@
import express from 'express';
import express from "express";
import {
createBlog,
getBlogs,
@@ -6,26 +6,22 @@ import {
updateBlog,
deleteBlog,
getAllBlogs,
getBlogForAdmin,
} from '../controllers/blog.controller.js';
} from "../controllers/blog.controller.js";
import jwtAuthMiddleware from '../middleware/auth.js';
import jwtAuthMiddleware from "../middleware/auth.js";
const router = express.Router();
/* PUBLIC */
router.get('/', getBlogs);
router.get('/:slug', getBlog);
router.get("/", getBlogs);
router.get("/:id", getBlog);
// Protected
router.get('/admin/all', jwtAuthMiddleware, getAllBlogs);
router.get('/admin/:id', jwtAuthMiddleware, getBlogForAdmin);
router.post('/', jwtAuthMiddleware, createBlog);
router.put('/:id', jwtAuthMiddleware, updateBlog);
router.delete('/:id', jwtAuthMiddleware, deleteBlog);
router.get("/admin/all", jwtAuthMiddleware, getAllBlogs);
router.post("/", jwtAuthMiddleware, createBlog);
router.put("/:id", jwtAuthMiddleware, updateBlog);
router.delete("/:id", jwtAuthMiddleware, deleteBlog);
export default router;
+9 -9
View File
@@ -1,4 +1,4 @@
import express from 'express';
import express from "express";
import {
createCandidate,
getCandidates,
@@ -6,20 +6,20 @@ import {
getCandidatesByCareer,
updateCandidate,
deleteCandidate,
} from '../controllers/candidate.controller.js';
} from "../controllers/candidate.controller.js";
import jwtAuthMiddleware from '../middleware/auth.js';
import jwtAuthMiddleware from "../middleware/auth.js";
const router = express.Router();
/* PUBLIC */
router.post('/', createCandidate);
router.get('/getAll', jwtAuthMiddleware, getCandidates);
router.get('/:id', jwtAuthMiddleware, getCandidate);
router.get('/career/:careerId', jwtAuthMiddleware, getCandidatesByCareer);
router.get("/getAll", getCandidates);
router.get("/:id", getCandidate);
router.get("/career/:careerId", getCandidatesByCareer);
router.patch('/:id', jwtAuthMiddleware, updateCandidate);
router.delete('/:id', jwtAuthMiddleware, deleteCandidate);
router.post("/", createCandidate);
router.patch("/:id", updateCandidate);
router.delete("/:id", jwtAuthMiddleware, deleteCandidate);
export default router;
+11 -8
View File
@@ -1,14 +1,17 @@
import express from 'express';
import { getAllCareers, createCareer, updateCareer, deleteCareer } from '../controllers/career.controller.js';
import jwtAuthMiddleware from '../middleware/auth.js';
import express from "express";
import {
getAllCareers,
createCareer,
updateCareer,
deleteCareer,
} from "../controllers/career.controller.js";
const router = express.Router();
router.get('/getAll', getAllCareers);
router.get("/getAll", getAllCareers);
router.post('/', jwtAuthMiddleware, createCareer);
router.patch('/:id', jwtAuthMiddleware, updateCareer);
router.delete('/:id', jwtAuthMiddleware, deleteCareer);
router.post("/", createCareer);
router.patch("/:id", updateCareer);
router.delete("/:id", deleteCareer);
export default router;
+7 -9
View File
@@ -1,22 +1,20 @@
import express from 'express';
import express from "express";
import {
getAllDepartments,
getDepartmentByName,
createDepartment,
updateDepartment,
deleteDepartment,
} from '../controllers/department.controller.js';
import jwtAuthMiddleware from '../middleware/auth.js';
} from "../controllers/department.controller.js";
import jwtAuthMiddleware from "../middleware/auth.js";
const router = express.Router();
// Public
router.get('/getAll', getAllDepartments);
router.get('/search', getDepartmentByName);
router.get("/getAll", getAllDepartments);
// Protected
router.post('/', jwtAuthMiddleware, createDepartment);
router.put('/:departmentId', jwtAuthMiddleware, updateDepartment);
router.delete('/:departmentId', jwtAuthMiddleware, deleteDepartment);
router.post("/", jwtAuthMiddleware, createDepartment);
router.put("/:departmentId", jwtAuthMiddleware, updateDepartment);
router.delete("/:departmentId", jwtAuthMiddleware, deleteDepartment);
export default router;
+10 -14
View File
@@ -1,4 +1,4 @@
import express from 'express';
import express from "express";
import {
getAllDoctors,
createDoctor,
@@ -7,23 +7,19 @@ import {
getDoctorTimings,
getDoctorTimingById,
getDoctorByDoctorId,
getDoctorsByDepartmentId,
getFeaturedDoctors,
} from '../controllers/doctor.controller.js';
} from "../controllers/doctor.controller.js";
import jwtAuthMiddleware from '../middleware/auth.js';
import jwtAuthMiddleware from "../middleware/auth.js";
const router = express.Router();
router.get('/getAll', getAllDoctors);
router.get('/search', getDoctorsByDepartmentId);
router.get('/getTimings', getDoctorTimings);
router.get('/getTimings/:doctorId', getDoctorTimingById);
router.get('/featured', getFeaturedDoctors);
router.get('/:doctorId', getDoctorByDoctorId);
router.get("/getAll", getAllDoctors);
router.get("/:doctorId", getDoctorByDoctorId);
router.get("/getTimings", getDoctorTimings);
router.get("/getTimings/:doctorId", getDoctorTimingById);
router.post('/', jwtAuthMiddleware, createDoctor);
router.patch('/:doctorId/:action', jwtAuthMiddleware, updateDoctor);
router.delete('/:doctorId', jwtAuthMiddleware, deleteDoctor);
router.post("/", jwtAuthMiddleware, createDoctor);
router.patch("/:doctorId", jwtAuthMiddleware, updateDoctor);
router.delete("/:doctorId", jwtAuthMiddleware, deleteDoctor);
export default router;
+7 -7
View File
@@ -1,19 +1,19 @@
import express from 'express';
import express from "express";
import {
getEmailConfigs,
createEmailConfig,
updateEmailConfig,
deleteEmailConfig,
} from '../controllers/emailConfig.controller.js';
} from "../controllers/emailConfig.controller.js";
import jwtAuthMiddleware from '../middleware/auth.js';
import jwtAuthMiddleware from "../middleware/auth.js";
const router = express.Router();
router.get('/getAll', getEmailConfigs);
router.get("/getAll", getEmailConfigs);
router.post('/', jwtAuthMiddleware, createEmailConfig);
router.patch('/:id', jwtAuthMiddleware, updateEmailConfig);
router.delete('/:id', jwtAuthMiddleware, deleteEmailConfig);
router.post("/", jwtAuthMiddleware, createEmailConfig);
router.patch("/:id", jwtAuthMiddleware, updateEmailConfig);
router.delete("/:id", jwtAuthMiddleware, deleteEmailConfig);
export default router;
-41
View File
@@ -1,41 +0,0 @@
import express from 'express';
import {
// Categories
getAllCategories,
getPackageBySlug,
createCategory,
updateCategory,
deleteCategory,
// Packages
getAllPackages,
createPackage,
updatePackage,
deletePackage,
getFeaturedPackages,
// Inquiries
createPackageInquiry,
getAllInquiries,
} from '../controllers/healthCheck.controller.js';
import jwtAuthMiddleware from '../middleware/auth.js';
const router = express.Router();
router.get('/packages', getAllPackages);
router.get('/packages/:slug', getPackageBySlug);
router.get('/categories', getAllCategories);
router.post('/inquiry', createPackageInquiry);
router.get('/featured', getFeaturedPackages);
router.get('/inquiries', jwtAuthMiddleware, getAllInquiries);
router.post('/', jwtAuthMiddleware, createPackage);
router.patch('/:id', jwtAuthMiddleware, updatePackage);
router.delete('/:id', jwtAuthMiddleware, deletePackage);
router.post('/categories', jwtAuthMiddleware, createCategory);
router.patch('/categories/:id', jwtAuthMiddleware, updateCategory);
router.delete('/categories/:id', jwtAuthMiddleware, deleteCategory);
export default router;
@@ -1,27 +0,0 @@
import express from 'express';
import {
createHomepageBanner,
getHomepageBanners,
getActiveHomepageBanners,
getHomepageBanner,
updateHomepageBanner,
deleteHomepageBanner,
} from '../controllers/homepageBanner.controller.js';
import jwtAuthMiddleware from '../middleware/auth.js';
const router = express.Router();
router.get('/active', getActiveHomepageBanners);
router.post('/', jwtAuthMiddleware, createHomepageBanner);
router.get('/getAll', jwtAuthMiddleware, getHomepageBanners);
router.get('/:id', jwtAuthMiddleware, getHomepageBanner);
router.put('/:id', jwtAuthMiddleware, updateHomepageBanner);
router.delete('/:id', jwtAuthMiddleware, deleteHomepageBanner);
export default router;
-9
View File
@@ -1,9 +0,0 @@
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;
+12 -7
View File
@@ -1,14 +1,19 @@
import express from 'express';
import { createInquiry, getInquiries, getInquiry, deleteInquiry } from '../controllers/inquiry.controller.js';
import express from "express";
import {
createInquiry,
getInquiries,
getInquiry,
deleteInquiry,
} from "../controllers/inquiry.controller.js";
import jwtAuthMiddleware from '../middleware/auth.js';
import jwtAuthMiddleware from "../middleware/auth.js";
const router = express.Router();
router.post('/', createInquiry);
router.post("/", createInquiry);
router.get('/getAll', jwtAuthMiddleware, getInquiries);
router.get('/:id', jwtAuthMiddleware, getInquiry);
router.delete('/:id', jwtAuthMiddleware, deleteInquiry);
router.get("/getAll", getInquiries);
router.get("/:id", getInquiry);
router.delete("/:id", jwtAuthMiddleware, deleteInquiry);
export default router;
@@ -1,28 +0,0 @@
import express from 'express';
import {
createInsurancePartner,
getInsurancePartners,
getActiveInsurancePartners,
getInsurancePartner,
updateInsurancePartner,
deleteInsurancePartner,
} from '../controllers/insurancePartner.controller.js';
import jwtAuthMiddleware from '../middleware/auth.js';
const router = express.Router();
router.get('/active', getActiveInsurancePartners);
router.post('/', jwtAuthMiddleware, createInsurancePartner);
router.get('/getAll', jwtAuthMiddleware, getInsurancePartners);
router.get('/:id', jwtAuthMiddleware, getInsurancePartner);
router.put('/:id', jwtAuthMiddleware, updateInsurancePartner);
router.delete('/:id', jwtAuthMiddleware, deleteInsurancePartner);
export default router;
+14 -8
View File
@@ -1,17 +1,23 @@
import express from 'express';
import { createNews, getAllNews, getNewsById, updateNews, deleteNews } from '../controllers/newsMedia.controller.js';
import express from "express";
import {
createNews,
getAllNews,
getNewsById,
updateNews,
deleteNews,
} from "../controllers/newsMedia.controller.js";
import jwtAuthMiddleware from '../middleware/auth.js';
import jwtAuthMiddleware from "../middleware/auth.js";
const router = express.Router();
// PUBLIC ROUTES
router.get('/getAll', getAllNews);
router.get('/:id', getNewsById);
router.get("/getAll", getAllNews);
router.get("/:id", getNewsById);
// PROTECTED ROUTES
router.post('/', jwtAuthMiddleware, createNews);
router.patch('/:id', jwtAuthMiddleware, updateNews);
router.delete('/:id', jwtAuthMiddleware, deleteNews);
router.post("/", jwtAuthMiddleware, createNews);
router.patch("/:id", jwtAuthMiddleware, updateNews);
router.delete("/:id", jwtAuthMiddleware, deleteNews);
export default router;
+7 -27
View File
@@ -1,35 +1,15 @@
import express from 'express';
import * as Bytescale from '@bytescale/sdk';
import multer from 'multer';
import express from "express";
import {upload} from "../controllers/upload.controller.js";
const router = express.Router();
const uploadManager = new Bytescale.UploadManager({
apiKey: process.env.BYTESCALE_SECRET_API_KEY,
});
const storage = multer.memoryStorage();
const upload = multer({ storage });
router.post('/', upload.single('file'), async (req, res) => {
try {
const file = req.file;
const { folderPath } = req.body;
const result = await uploadManager.upload({
data: file.buffer,
name: file.originalname,
mime: file.mimetype,
path: {
folderPath: folderPath || '/general',
router.post("/image", upload.single("image"), (req, res) => {
res.json({
success: 1,
file: {
url: `http://localhost:3000/uploads/blog/${req.file.filename}`,
},
});
res.json({ fileUrl: result.fileUrl });
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Upload failed' });
}
});
export default router;
-45
View File
@@ -1,45 +0,0 @@
import prisma from '../prisma/client.js';
import { hashPassword } from './password.js';
async function main() {
const username = process.argv[2];
const password = process.argv[3];
const role = process.argv[4] || 'admin';
if (!username || !password) {
console.log('Usage: node scripts/createUser.js <username> <password> [role]');
process.exit(1);
}
const existingUser = await prisma.user.findUnique({
where: { username },
});
if (existingUser) {
console.log('User already exists');
process.exit(1);
}
const hashedPassword = await hashPassword(password);
const user = await prisma.user.create({
data: {
username,
password: hashedPassword,
role,
},
});
console.log('User created:', {
id: user.id,
username: user.username,
role: user.role,
});
}
main()
.catch((e) => {
console.error(e);
})
.finally(async () => {
await prisma.$disconnect();
});
+4 -4
View File
@@ -1,4 +1,4 @@
import prisma from '../prisma/client.js';
import prisma from "../prisma/client.js";
export const getEmailsByType = async (type) => {
try {
@@ -9,9 +9,9 @@ export const getEmailsByType = async (type) => {
},
});
return emails.map((e) => e.email).join(',');
return emails.map((e) => e.email).join(",");
} catch (error) {
console.error('Fetch email config error:', error);
return '';
console.error("Fetch email config error:", error);
return "";
}
};
+3 -3
View File
@@ -1,10 +1,10 @@
import jwt from 'jsonwebtoken';
import 'dotenv/config';
import jwt from "jsonwebtoken";
import "dotenv/config";
const SECRET = process.env.JWT_SECRET;
export function generateToken(payload) {
return jwt.sign(payload, SECRET, { expiresIn: '24h' });
return jwt.sign(payload, SECRET, {expiresIn: "24h"});
}
export function verifyToken(token) {
+1 -1
View File
@@ -1,4 +1,4 @@
import bcrypt from 'bcryptjs';
import bcrypt from "bcryptjs";
export async function hashPassword(password) {
return bcrypt.hash(password, 10);
+4 -4
View File
@@ -1,4 +1,4 @@
import postmark from 'postmark';
import postmark from "postmark";
const client = new postmark.ServerClient(process.env.POSTMARK_API_KEY);
@@ -9,10 +9,10 @@ export const sendEmail = async ({ to, subject, html, text }) => {
To: to,
Subject: subject,
HtmlBody: html,
TextBody: text || '',
MessageStream: 'outbound',
TextBody: text || "",
MessageStream: "outbound",
});
} catch (error) {
console.error('Email send error:', error);
console.error("Email send error:", error);
}
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

-45
View File
@@ -1,45 +0,0 @@
version: '3.8'
services:
backend:
build:
context: .
dockerfile: docker/dev/Dockerfile.main
ports:
- '127.0.0.1:5008:5008'
env_file:
- ./backend/.env
depends_on:
db:
condition: service_healthy
restart: unless-stopped
frontend:
build:
context: .
dockerfile: docker/dev/Dockerfile.frontend
ports:
- '127.0.0.1:3008:3000'
env_file:
- ./frontend/.env
restart: unless-stopped
db:
image: postgres:16-alpine
environment:
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB']
interval: 5s
timeout: 5s
retries: 5
restart: unless-stopped
volumes:
postgres_data:
external: true
name: gg-backend_postgres_data
-41
View File
@@ -1,41 +0,0 @@
services:
backend:
build:
context: .
dockerfile: docker/dev/Dockerfile.main
ports:
- '127.0.0.1:5008:5008'
env_file:
- ./backend/.env
depends_on:
db:
condition: service_healthy
restart: always
frontend:
build:
context: .
dockerfile: docker/dev/Dockerfile.frontend
ports:
- '127.0.0.1:3008:3000'
env_file:
- ./frontend/.env
restart: always
db:
image: postgres:16-alpine
environment:
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB']
interval: 5s
timeout: 5s
retries: 5
restart: always
volumes:
postgres_data:
-19
View File
@@ -1,19 +0,0 @@
ARG NODE_VERSION=24.15.0
FROM node:${NODE_VERSION}-alpine
WORKDIR /usr/src/app
COPY ./frontend/package*.json ./
RUN npm ci
COPY ./frontend .
# Build the app
RUN npm run build
RUN npm install -g serve
EXPOSE 3000
# Serve built app
CMD ["serve", "-s", "dist"]
-24
View File
@@ -1,24 +0,0 @@
ARG NODE_VERSION=24.15.0
FROM node:${NODE_VERSION}-alpine
WORKDIR /usr/src/app
# Use cache mounts for faster installs
RUN --mount=type=bind,source=backend/package.json,target=package.json \
--mount=type=bind,source=backend/package-lock.json,target=package-lock.json \
--mount=type=cache,target=/root/.npm \
npm ci
# Copy the backend source
COPY ./backend .
# Copy and setup entrypoint
COPY ./docker/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
EXPOSE 5008
ENTRYPOINT [ "entrypoint.sh" ]
# This '$@' will be replaced by the CMD
CMD ["npm", "start"]
-12
View File
@@ -1,12 +0,0 @@
#!/bin/sh
set -e # Exit immediately if a command exits with a non-zero status
echo "Generating Prisma Client..."
npx prisma generate
echo "Running migrate..."
npx prisma migrate deploy
echo "Executing command: $@"
exec "$@"
-5
View File
@@ -22,8 +22,3 @@ dist-ssr
*.njsproj
*.sln
*.sw?
#env files
.env*
.env.*.local
+62 -36
View File
@@ -1,47 +1,73 @@
**GG-Dashboard**
# React + TypeScript + Vite
## Tech Stack
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
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
Currently, two official plugins are available:
## Project Structure
- [@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
- [@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
frontend/
├── src/
│ ├── api/
│ ├── assets/
│ ├── components/
│ ├── context/
│ ├── lib/
│ ├── layout/
│ ├── pages/
│ ├── services/
│ ├── utils/
│ └── App.tsx
├── .env
├── index.html
└── package.json
## React Compiler
## Installation & Setup
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).
**1. Prerequisites**
Node.js (v20+)
## Expanding the ESLint configuration
**2. Environment Variables**
VITE_API_URL="http://localhost:5008/api"
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
**3. Install Dependencies**
npm install
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
**4. Development**
npm run dev
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
## Scripts
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
npm run dev: Starts the Vite development server with Hot Module Replacement.
npm run build: Compiles TypeScript and builds the production-ready assets.
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:
```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...
},
},
])
```
+7 -7
View File
@@ -1,9 +1,9 @@
import js from '@eslint/js';
import globals from 'globals';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import tseslint from 'typescript-eslint';
import { defineConfig, globalIgnores } from 'eslint/config';
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
@@ -20,4 +20,4 @@ export default defineConfig([
globals: globals.browser,
},
},
]);
])
+2 -2
View File
@@ -2,9 +2,9 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GG Admin Dashboard</title>
<title>frontend</title>
</head>
<body>
<div id="root"></div>
+19 -28
View File
@@ -28,7 +28,6 @@
"radix-ui": "^1.4.3",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-hot-toast": "^2.6.0",
"react-router-dom": "^7.13.1",
"shadcn": "^4.0.5",
"tailwind-merge": "^3.5.0",
@@ -116,6 +115,7 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
@@ -1760,6 +1760,7 @@
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz",
"integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
"license": "MIT",
"peer": true,
"engines": {
"node": "^14.21.3 || >=16"
},
@@ -4092,6 +4093,7 @@
"integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@@ -4102,6 +4104,7 @@
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -4112,6 +4115,7 @@
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
@@ -4173,6 +4177,7 @@
"integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.57.0",
"@typescript-eslint/types": "8.57.0",
@@ -4463,6 +4468,7 @@
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -4722,6 +4728,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -5188,6 +5195,7 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"devOptional": true,
"license": "MIT"
},
"node_modules/data-uri-to-buffer": {
@@ -5550,6 +5558,7 @@
"integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -5802,6 +5811,7 @@
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
"license": "MIT",
"peer": true,
"dependencies": {
"accepts": "^2.0.0",
"body-parser": "^2.2.1",
@@ -6352,15 +6362,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/goober": {
"version": "2.1.18",
"resolved": "https://registry.npmjs.org/goober/-/goober-2.1.18.tgz",
"integrity": "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==",
"license": "MIT",
"peerDependencies": {
"csstype": "^3.0.10"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -6465,6 +6466,7 @@
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.7.tgz",
"integrity": "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=16.9.0"
}
@@ -8129,6 +8131,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -8138,6 +8141,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -8145,23 +8149,6 @@
"react": "^19.2.4"
}
},
"node_modules/react-hot-toast": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz",
"integrity": "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==",
"license": "MIT",
"dependencies": {
"csstype": "^3.1.3",
"goober": "^2.1.16"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": ">=16",
"react-dom": ">=16"
}
},
"node_modules/react-refresh": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
@@ -8918,7 +8905,8 @@
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz",
"integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/tailwindcss-animate": {
"version": "1.0.7",
@@ -9125,6 +9113,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -9325,6 +9314,7 @@
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@@ -9669,6 +9659,7 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
-1
View File
@@ -30,7 +30,6 @@
"radix-ui": "^1.4.3",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-hot-toast": "^2.6.0",
"react-router-dom": "^7.13.1",
"shadcn": "^4.0.5",
"tailwind-merge": "^3.5.0",
+1 -1
View File
@@ -1,5 +1,5 @@
export default {
plugins: {
'@tailwindcss/postcss': {},
"@tailwindcss/postcss": {},
},
};
Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+17 -32
View File
@@ -1,38 +1,29 @@
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { Toaster } from 'react-hot-toast';
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import Login from '@/pages/Login';
import Login from "@/pages/Login";
import DashboardLayout from './layouts/DashboardLayout';
import DashboardLayout from "./layouts/DashboardLayout";
// import ProtectedRoute from "./components/ProtectedRoutes/ProtectedRoutes";
import ProtectedRoute from './auth/ProtectedRoute';
import PublicRoute from './auth/PublicRoute';
import { AuthProvider } from './context/AuthContext';
import Department from './pages/Department';
import Doctor from './pages/Doctor';
import Blog from './pages/Blog';
import BlogEditorPage from './pages/BlogEditor';
import Appointment from './pages/Appointment';
import EmailPage from './pages/email';
import CareerPage from './pages/Career';
import CandidatePage from './pages/candidates';
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';
import HealthPackagePage from './pages/HealthPackagePage';
import HomepageBanner from './pages/HomepageBannerPage';
import InsurancePartnerPage from './pages/InsurancePartner';
import AccreditationPage from './pages/Accreditation';
import ProtectedRoute from "./auth/ProtectedRoute";
import PublicRoute from "./auth/PublicRoute";
import { AuthProvider } from "./context/AuthContext";
import Department from "./pages/Department";
import Doctor from "./pages/Doctor";
import Blog from "./pages/Blog";
import BlogEditorPage from "./pages/BlogEditor";
import Appointment from "./pages/Appointment";
import EmailPage from "./pages/email";
import CareerPage from "./pages/Career";
import CandidatePage from "./pages/candidates";
import InquiryPage from "./pages/inquiry";
import AcademicsPage from "./pages/Academics";
import NewsPage from "./pages/newsMedia";
export default function App() {
return (
<BrowserRouter>
<Toaster position="top-right" />
<AuthProvider>
<Routes>
<Route element={<PublicRoute />}>
@@ -44,7 +35,6 @@ export default function App() {
<Route path="/department" element={<Department />} />
<Route path="/doctor" element={<Doctor />} />
<Route path="/blog" element={<Blog />} />
<Route path="/blog/:id" element={<BlogDetail />} />
<Route path="/blog/create" element={<BlogEditorPage />} />
<Route path="/blog/edit/:id" element={<BlogEditorPage />} />
<Route path="/appointment" element={<Appointment />} />
@@ -54,11 +44,6 @@ export default function App() {
<Route path="/inquiry" element={<InquiryPage />} />
<Route path="/academics" element={<AcademicsPage />} />
<Route path="/news" element={<NewsPage />} />
<Route path="/import" element={<ImportData />} />
<Route path="/health-check" element={<HealthPackagePage />} />
<Route path="/homepage-banner" element={<HomepageBanner />} />
<Route path="/insurance-partner" element={<InsurancePartnerPage />} />
<Route path="/accreditation" element={<AccreditationPage />} />
</Route>
</Route>
+2 -2
View File
@@ -1,7 +1,7 @@
import apiClient from '@/api/client';
import apiClient from "@/api/client";
export const getAcademicsApi = async () => {
const res = await apiClient.get('/academics/getAll');
const res = await apiClient.get("/academics/getAll");
return res.data;
};

Some files were not shown because too many files have changed in this diff Show More