Compare commits

..

113 Commits

Author SHA1 Message Date
Kailasdevdas 78e2618a29 chore: file formatting 2026-05-26 15:48:01 +05:30
Kailasdevdas 8a21e0bf38 chore: setup prettier 2026-05-26 15:23:53 +05:30
kailasdevdas 6c5e529017 Merge pull request 'feat: seo preview' (#44) from feat/healthcheck-seo into dev
Reviewed-on: #44
2026-05-26 07:33:40 +00:00
Kailasdevdas 8b563e45a2 feat: health check preview url 2026-05-26 12:58:13 +05:30
Kailasdevdas e6d77b72b4 feat: health check seo preview 2026-05-26 12:41:43 +05:30
Kailasdevdas 3d7b8eef6c Merge branch 'feat/seo-preview' into feat/healthcheck-seo 2026-05-26 12:39:17 +05:30
rishalkv 5aae2824ef fix: edge case creation of og 2026-05-26 12:36:11 +05:30
Kailasdevdas f3cb4aee91 Merge branch 'feat/seo-preview' into feat/healthcheck-seo 2026-05-26 12:34:21 +05:30
rishalkv 3af6401429 fix: og title description 2026-05-26 12:33:51 +05:30
Kailasdevdas 2e63106439 Merge branch 'feat/seo-preview' into feat/healthcheck-seo 2026-05-26 11:59:23 +05:30
rishalkv c2b54725fe fix: og image update 2026-05-26 11:57:10 +05:30
Kailasdevdas 4d73da5ddd feat: health check seo 2026-05-26 11:56:22 +05:30
rishalkv fa06126219 chore: add seo reusable component 2026-05-26 11:38:34 +05:30
rishalkv fc491f4050 feat: seo preview 2026-05-25 16:20:47 +05:30
kailasdevdas 31c0e50177 Merge pull request 'fix: handle empty package pricing fields correctly' (#42) from fix/optional-pricing into dev
Reviewed-on: #42
2026-05-25 07:37:12 +00:00
Kailasdevdas 8f813ed7c4 fix: handle empty package pricing fields correctly 2026-05-25 12:59:44 +05:30
kailasdevdas 9a14965a54 Merge pull request 'fix: remove duplicate toasts' (#41) from fix/optional-pricing into dev
Reviewed-on: #41
2026-05-25 07:00:27 +00:00
kailasdevdas 2fc57a1ae9 Merge pull request 'feat: add dynamic slug' (#40) from feat/dynamic-slug into dev
Reviewed-on: #40
2026-05-25 06:57:42 +00:00
Kailasdevdas d76011d301 fix: remove duplicate toasts 2026-05-25 12:19:09 +05:30
rishalkv 6d5e243e06 feat: add dynamic slug 2026-05-25 12:04:14 +05:30
kailasdevdas 9210621d67 Merge pull request 'fix: optional price fields' (#39) from fix/optional-pricing into dev
Reviewed-on: #39
2026-05-25 06:11:31 +00:00
Kailasdevdas cefaf3a850 fix: optional price fields 2026-05-25 11:37:51 +05:30
kailasdevdas 120ff12fef Merge pull request 'fix: add toggle action update controller' (#38) from fix/doc-update-controller into dev
Reviewed-on: #38
2026-05-22 11:40:52 +00:00
rishalkv 12d9f2a4cb fix: add toggle action update controller 2026-05-22 16:34:37 +05:30
kailasdevdas 5eecc5092d Merge pull request 'fix: use migrate deploy' (#37) from fix/prisma-migrate into dev
Reviewed-on: #37
2026-05-22 09:20:03 +00:00
Kailasdevdas f11c8ae8dc fix: use migrate deploy 2026-05-22 14:21:25 +05:30
kailasdevdas 558ab12e1f Merge pull request 'fix: bytescale type' (#36) from fix/bytescale-type into dev
Reviewed-on: #36
2026-05-21 09:12:43 +00:00
rishalkv 0f839c7f84 fix: bytescale type 2026-05-21 14:36:17 +05:30
kailasdevdas 9271ea9b38 Merge pull request 'feat:add seo and more about doctors' (#34) from feat/doc-seo-content-enhacement into dev
Reviewed-on: #34
2026-05-21 08:47:32 +00:00
rishalkv eb68d0acc4 Merge pull request 'fix:added validations for api' (#35) from fix/doc-validations into feat/doc-seo-content-enhacement
Reviewed-on: #35
2026-05-21 06:06:30 +00:00
rishalkv 667e15513c fix:added validations for api 2026-05-21 11:20:09 +05:30
rishalkv 2a786ef118 fix:unwanted query exec on update 2026-05-20 10:43:05 +05:30
rishalkv da6587c83d fix:editing doctor dept 2026-05-20 10:28:46 +05:30
rishalkv 5fea2a306d feat:add seo and more about doctores 2026-05-20 10:15:53 +05:30
kailasdevdas 08b9c2647e Merge pull request 'chore: add toast show validation errors' (#32) from fix/health-checkup-validation into dev
Reviewed-on: #32
2026-05-18 11:21:57 +00:00
Kailasdevdas 98194283df chore: add toast show validation errors 2026-05-18 16:41:38 +05:30
kailasdevdas 1320ce6fe6 Merge pull request 'chore: show required image dimension' (#31) from feat/healthcheckup-crud into dev
Reviewed-on: #31
2026-05-18 07:47:06 +00:00
Kailasdevdas 3dbbb2e77e chore: show required image dimension 2026-05-18 13:15:00 +05:30
kailasdevdas 5b1d626661 Merge pull request 'feat: health checkup CRUD apis' (#30) from feat/healthcheckup-crud into dev
Reviewed-on: #30
2026-05-18 06:26:27 +00:00
Kailasdevdas 098fe12fd7 feat: add image upload for health package 2026-05-18 11:55:55 +05:30
Kailasdevdas 852a25269a feat: add toast 2026-05-18 10:58:24 +05:30
Kailasdevdas d92e0538bd fix: make category slug optional 2026-05-18 10:58:14 +05:30
Kailasdevdas 8d60afdc49 feat: health checkup page 2026-05-15 17:58:25 +05:30
Kailasdevdas 9bc0bf406a feat: health checkup CRUD apis 2026-05-15 17:46:52 +05:30
kailasdevdas 3140d72e28 Merge pull request 'fix:blog editor image upload' (#28) from fix/blog-text-editor-image-uploader into dev
Reviewed-on: #28
2026-05-13 11:49:49 +00:00
rishalkv 6117805467 fix:blog editor image upload 2026-05-13 17:16:25 +05:30
kailasdevdas b002c053ae Merge pull request 'fix: doctor toggle logic' (#27) from feat/appointment-date-filter into dev
Reviewed-on: #27
2026-05-13 09:12:35 +00:00
kailasdevdas e6044518d2 Merge pull request 'feat: update date format in mail' (#26) from feat/email-date-format into dev
Reviewed-on: #26
2026-05-13 09:03:56 +00:00
Kailasdevdas 6889137164 feat: add appointment date range filter 2026-05-13 14:20:51 +05:30
Kailasdevdas 988fbd28f1 fix: doctor toggle logic 2026-05-13 14:19:42 +05:30
Kailasdevdas fa2b02ad23 feat: update date format in mail 2026-05-13 11:57:33 +05:30
kailasdevdas 199797fdf4 Merge pull request 'feat:sorting according to the prio of dept' (#24) from feat/department-internal-sort into dev
Reviewed-on: #24
2026-05-11 11:37:53 +00:00
rishalkv 5efe049fbd feat:sorting according to the prio of dept 2026-05-11 16:27:20 +05:30
kailasdevdas a008f09923 Merge pull request 'feat: implement sorting and visibility changes' (#23) from feat/inactive-field into dev
Reviewed-on: #23
2026-05-11 05:35:39 +00:00
Kailasdevdas 903a541e35 chore: correct lablel 2026-05-11 10:57:52 +05:30
Kailasdevdas 4808e99ae5 feat: remove delete action and add status toggle to tables 2026-05-11 10:52:30 +05:30
Kailasdevdas 2c6da93dfb feat: add toast 2026-05-11 10:51:34 +05:30
Kailasdevdas 1717507555 feat: implement sorting and visibility changes 2026-05-11 00:04:22 +05:30
kailasdevdas 9aaf1879a8 Merge pull request 'fix: edit form fields and update form submission logic' (#21) from fix/news-media-UI into dev
Reviewed-on: #21
2026-05-05 06:44:06 +00:00
Kailasdevdas d847789564 fix: edit form fields and update form submission logic 2026-05-05 10:48:18 +05:30
Ashir Ali eab661bb4a refactor: email updates 2026-05-01 01:31:41 +05:30
Ashir Ali 60ace745f3 feat: disableConcurrentBuilds 2026-05-01 01:27:33 +05:30
Ashir Ali 5fec4dd37d feat: email template added, jenkins file bug fix 2026-05-01 00:50:31 +05:30
Ashir Ali bb42581c17 feat: added email recepient 2026-05-01 00:47:17 +05:30
Ashir Ali 621bc8efcd feat: Prod docker compose and Jenkinsfile added 2026-05-01 00:06:27 +05:30
Ashir Ali e32a005340 fix: update docker compose to reuse old data 2026-04-30 21:28:20 +05:30
Ashir Ali 1e98bb4115 fix: agent 6 for jenkins 2026-04-30 21:04:15 +05:30
Ashir Ali c4ef2af9df fix: PAT update 2026-04-30 20:51:24 +05:30
Ashir Ali dda6b75dce feat: Jenkinsfile added 2026-04-30 20:33:42 +05:30
kailasdevdas 2c37cd042f Merge pull request 'feat: improve email template' (#18) from feat/ui-enhancements into dev
Reviewed-on: #18
2026-04-29 04:28:58 +00:00
Kailasdevdas 082e227111 feat: improve email template 2026-04-27 17:29:33 +05:30
kailasdevdas a0dbf32cf3 Merge pull request 'fix/news-media-pagination' (#17) from fix/news-media-pagination into dev
Reviewed-on: #17
2026-04-27 05:39:47 +00:00
rishalkv c6fbd0dc30 refactor:change in the image path 2026-04-24 17:27:15 +05:30
rishalkv 6001a2db64 fix:search pagination issue 2026-04-24 17:25:47 +05:30
kailasdevdas 67fe087906 Merge pull request 'feat: server-side pagination, search, and date filter for appointments' (#16) from feat/appointment-pagination into dev
Reviewed-on: #16
2026-04-24 09:37:15 +00:00
Kailasdevdas 65e6413129 feat: server-side pagination, search, and date filter for appointments 2026-04-24 11:40:14 +05:30
Ashir Ali 51d604d6ee fix: docker compose .env, gitignore updates 2026-04-23 15:00:57 +05:30
Kailasdevdas bb98ddf514 chore: update backend port to 5008 and frontend to 3008 2026-04-23 13:30:11 +05:30
Kailasdevdas 39e5590dd8 doc: update readme 2026-04-23 13:07:40 +05:30
Kailasdevdas 876c966b17 chore: add dockerignore 2026-04-21 17:04:24 +05:30
Kailasdevdas 284854c33a chore: update Node.js and PostgreSQL in Docker 2026-04-21 16:41:54 +05:30
Kailasdevdas f32978ec1c fix: appointment import mapping and date validation 2026-04-21 16:40:18 +05:30
kailasdevdas cac3685c01 Merge pull request 'feat/create-user-script' (#15) from feat/create-user-script into dev
Reviewed-on: #15
2026-04-21 06:24:27 +00:00
Kailasdevdas dd5d46c58d feat: update favicon and title 2026-04-21 11:51:14 +05:30
Kailasdevdas 64d720143e feat: add create user script 2026-04-21 11:50:48 +05:30
Kailasdevdas 69d6d5a8f3 fix: align ports and docker config 2026-04-20 16:05:38 +05:30
kailasdevdas 3fc4100a72 Merge pull request 'feat: add bulk excel data import functionality' (#14) from feat/import-excel-data into dev
Reviewed-on: #14
2026-04-20 10:02:06 +00:00
kailasdevdas ddb87d6789 Merge pull request 'feat/docker-setup' (#13) from feat/docker-setup into dev
Reviewed-on: #13
2026-04-20 10:01:35 +00:00
Kailasdevdas d0686b67aa feat: add bulk excel data import functionality 2026-04-20 15:29:46 +05:30
Kailasdevdas 671a3c4e3a feat: docker dev setup 2026-04-20 14:39:29 +05:30
Kailasdevdas e356aa8fd9 chore: remove unused package 2026-04-20 12:35:38 +05:30
Kailasdevdas 740631d376 docs: update readme 2026-04-16 19:50:11 +05:30
Kailasdevdas 39e162f65c feat: use API base URL from env 2026-04-16 19:49:06 +05:30
kailasdevdas 959440e1c6 Merge pull request 'fix:fix in the blog editor' (#12) from fix/blog-view into dev
Reviewed-on: #12
2026-04-16 11:17:23 +00:00
rishalkv 809a0a4798 refactor:remove unwanted images 2026-04-16 16:45:20 +05:30
rishalkv 5cf73a6351 fix:fix in the blog editor 2026-04-16 16:38:16 +05:30
kailasdevdas 7eab5fe3ff Merge pull request 'refactor: move Bytescale upload logic to backend for security' (#10) from feat/backend-bytescale-uploader into dev
Reviewed-on: #10
2026-04-16 10:04:19 +00:00
Kailasdevdas 16cf582e2c refactor: move Bytescale upload logic to backend for security 2026-04-16 15:33:22 +05:30
Kailasdevdas 5b4aacda04 Merge branch 'feat/bytescale-integration' into dev 2026-04-16 14:25:56 +05:30
Kailasdevdas dc3228a07a feat: email on inquiry 2026-04-16 14:03:50 +05:30
kailasdevdas fd60419c26 Merge pull request 'feat/blog' (#8) from feat/blog into dev
Reviewed-on: #8
2026-04-16 08:31:38 +00:00
Kailasdevdas c21ab02c2a feat: add doctor image in the response 2026-04-16 11:17:42 +05:30
Kailasdevdas c282b1825e feat: add Bytescale image uploads 2026-04-14 17:33:21 +05:30
rishalkv e74a5b09c2 feat/add blog 2026-04-14 16:04:44 +05:30
rishalkv 86afb86d3c feat/blig edit 2026-04-14 15:25:20 +05:30
Kailasdevdas 0fddd7a656 fix: add missing import 2026-04-14 12:56:47 +05:30
kailasdevdas de53008e2d Merge pull request 'fix/frontend-ui' (#6) from fix/frontend-ui into dev
Reviewed-on: #6
2026-04-14 07:19:58 +00:00
kailasdevdas 0f6b34487e Merge pull request 'fix/backend' (#7) from fix/backend into dev
Reviewed-on: #7
2026-04-14 07:19:45 +00:00
Kailasdevdas fb298cb846 fix: add JWT middleware to private API routes 2026-04-08 16:44:41 +05:30
Kailasdevdas 9c44c66b22 feat: get doctors by department 2026-04-08 16:40:42 +05:30
Kailasdevdas 29d2ed6b96 feat: get department by name 2026-04-08 16:39:29 +05:30
Kailasdevdas c4ebd19c15 fix: maintain same ui across all the pages 2026-04-08 16:30:50 +05:30
Kailasdevdas 1d55cfc4b8 fix: doctors page ui 2026-04-06 17:46:31 +05:30
160 changed files with 19278 additions and 13107 deletions
+34
View File
@@ -0,0 +1,34 @@
# 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
@@ -0,0 +1,2 @@
.env
node_modules
+14
View File
@@ -0,0 +1,14 @@
node_modules
dist
build
coverage
.next
out
*.log
backend/node_modules
backend/dist
frontend/node_modules
frontend/build
frontend/dist
+13
View File
@@ -0,0 +1,13 @@
{
"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
@@ -0,0 +1,4 @@
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
}
+105
View File
@@ -0,0 +1,105 @@
# 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
@@ -0,0 +1,221 @@
// 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
@@ -0,0 +1,224 @@
// 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
@@ -0,0 +1,94 @@
# 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 node_modules
# Keep environment variables out of version control # Keep environment variables out of version control
.env .env*
/src/generated/prisma /src/generated/prisma
+59
View File
@@ -0,0 +1,59 @@
**GG-Node-Backend**
## Tech Stack
Runtime: Node.js (ES Modules)
Framework: Express.js (v5.x)
ORM: Prisma (PostgreSQL)
Storage: Bytescale (Image uploads)
Auth: JSON Web Tokens (JWT) & Bcrypt
Email: Postmark
## Project Structure
backend/
├── prisma/
│ └── schema.prisma
├──── src/
│ ├── app.js
│ ├── controllers/
│ ├── middlewares/
│ ├── routes/
│ ├── prisma/
│ └── utils/
├── .env
└── package.json
## Installation & Setup
**1. Prerequisites**
Node.js (v18+)
PostgreSQL Database
**2. Environment Variables**
DATABASE_URL=""
PORT=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.
+110 -2
View File
@@ -9,6 +9,7 @@
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@bytescale/sdk": "^3.53.0",
"@editorjs/editorjs": "^2.31.4", "@editorjs/editorjs": "^2.31.4",
"@editorjs/header": "^2.8.8", "@editorjs/header": "^2.8.8",
"@editorjs/list": "^2.0.9", "@editorjs/list": "^2.0.9",
@@ -21,13 +22,21 @@
"express-session": "^1.19.0", "express-session": "^1.19.0",
"jsonwebtoken": "^9.0.3", "jsonwebtoken": "^9.0.3",
"multer": "^2.1.1", "multer": "^2.1.1",
"node-fetch": "^3.3.2",
"postmark": "^4.0.7", "postmark": "^4.0.7",
"prisma": "^6.19.2" "prisma": "^6.19.2",
"slugify": "^1.6.9"
}, },
"devDependencies": { "devDependencies": {
"nodemon": "^3.1.11" "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": { "node_modules/@codexteam/icons": {
"version": "0.0.5", "version": "0.0.5",
"resolved": "https://registry.npmjs.org/@codexteam/icons/-/icons-0.0.5.tgz", "resolved": "https://registry.npmjs.org/@codexteam/icons/-/icons-0.0.5.tgz",
@@ -599,6 +608,15 @@
"url": "https://opencollective.com/express" "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": { "node_modules/debug": {
"version": "4.4.3", "version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -899,6 +917,29 @@
"node": ">=8.0.0" "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": { "node_modules/fill-range": {
"version": "7.1.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -990,6 +1031,18 @@
"node": ">= 0.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": { "node_modules/forwarded": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -1521,6 +1574,44 @@
"node": "^18 || ^20 || >= 21" "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": { "node_modules/node-fetch-native": {
"version": "1.6.7", "version": "1.6.7",
"resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz",
@@ -1727,7 +1818,6 @@
"integrity": "sha512-XTKeKxtQElcq3U9/jHyxSPgiRgeYDKxWTPOf6NkXA0dNj5j40MfEsZkMbyNpwDWCUv7YBFUl7I2VK/6ALbmhEg==", "integrity": "sha512-XTKeKxtQElcq3U9/jHyxSPgiRgeYDKxWTPOf6NkXA0dNj5j40MfEsZkMbyNpwDWCUv7YBFUl7I2VK/6ALbmhEg==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"@prisma/config": "6.19.2", "@prisma/config": "6.19.2",
"@prisma/engines": "6.19.2" "@prisma/engines": "6.19.2"
@@ -2064,6 +2154,15 @@
"node": ">=10" "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": { "node_modules/statuses": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
@@ -2207,6 +2306,15 @@
"node": ">= 0.8" "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": { "node_modules/wrappy": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+6 -2
View File
@@ -8,13 +8,15 @@
"start": "node src/app.js", "start": "node src/app.js",
"prisma": "prisma", "prisma": "prisma",
"migrate": "npx prisma migrate dev", "migrate": "npx prisma migrate dev",
"generate": "npx prisma generate" "generate": "npx prisma generate",
"create-user": "node src/utils/createUser.js"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@bytescale/sdk": "^3.53.0",
"@editorjs/editorjs": "^2.31.4", "@editorjs/editorjs": "^2.31.4",
"@editorjs/header": "^2.8.8", "@editorjs/header": "^2.8.8",
"@editorjs/list": "^2.0.9", "@editorjs/list": "^2.0.9",
@@ -27,8 +29,10 @@
"express-session": "^1.19.0", "express-session": "^1.19.0",
"jsonwebtoken": "^9.0.3", "jsonwebtoken": "^9.0.3",
"multer": "^2.1.1", "multer": "^2.1.1",
"node-fetch": "^3.3.2",
"postmark": "^4.0.7", "postmark": "^4.0.7",
"prisma": "^6.19.2" "prisma": "^6.19.2",
"slugify": "^1.6.9"
}, },
"devDependencies": { "devDependencies": {
"nodemon": "^3.1.11" "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: // This file was generated by Prisma, and assumes you have installed the following:
// npm install --save-dev prisma dotenv // npm install --save-dev prisma dotenv
import "dotenv/config"; import 'dotenv/config';
import {defineConfig} from "prisma/config"; import { defineConfig } from 'prisma/config';
export default defineConfig({ export default defineConfig({
schema: "prisma/schema.prisma", schema: 'prisma/schema.prisma',
migrations: { migrations: {
path: "prisma/migrations", path: 'prisma/migrations',
}, },
datasource: { datasource: {
url: process.env["DATABASE_URL"], url: process.env['DATABASE_URL'],
}, },
}); });
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Doctor" ADD COLUMN "image" TEXT;
@@ -0,0 +1,8 @@
/*
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;
@@ -0,0 +1,8 @@
/*
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");
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Department" ADD COLUMN "image" TEXT;
@@ -0,0 +1,12 @@
-- 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;
@@ -0,0 +1,14 @@
-- 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;
@@ -0,0 +1,11 @@
-- 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;
@@ -0,0 +1,63 @@
-- 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;
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "HealthPackageInquiry" ADD COLUMN "age" INTEGER,
ADD COLUMN "gender" TEXT;
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "HealthPackage" ALTER COLUMN "inclusions" SET DEFAULT '{}';
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "HealthCheckCategory" ALTER COLUMN "slug" DROP NOT NULL;
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "HealthPackage" ADD COLUMN "image" TEXT;
@@ -0,0 +1,50 @@
/*
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;
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Doctor" ADD COLUMN "experience" INTEGER;
@@ -0,0 +1,14 @@
/*
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;
+116 -4
View File
@@ -21,10 +21,17 @@ model Doctor {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
doctorId String @unique doctorId String @unique
name String name String
image String?
designation String? designation String?
experience Int?
workingStatus String? workingStatus String?
qualification String? qualification String?
isActive Boolean @default(true)
globalSortOrder Int @default(1000)
specializations DoctorSpecialization[]
professionalSummary String? @db.Text
seoId Int? @unique
seo Seo? @relation(fields: [seoId], references: [id])
departments DoctorDepartment[] departments DoctorDepartment[]
appointments Appointment[] appointments Appointment[]
@@ -36,6 +43,8 @@ model Department {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
departmentId String @unique departmentId String @unique
name String name String
image String?
para1 String? para1 String?
para2 String? para2 String?
@@ -43,6 +52,9 @@ model Department {
facilities String? facilities String?
services String? services String?
isActive Boolean @default(true)
sortOrder Int @default(1000)
doctors DoctorDepartment[] doctors DoctorDepartment[]
appointments Appointment[] appointments Appointment[]
@@ -58,7 +70,7 @@ model DoctorDepartment {
doctor Doctor @relation(fields: [doctorId], references: [id]) doctor Doctor @relation(fields: [doctorId], references: [id])
department Department @relation(fields: [departmentId], references: [id]) department Department @relation(fields: [departmentId], references: [id])
sortOrder Int @default(1000)
timing DoctorTiming? timing DoctorTiming?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
@@ -93,6 +105,7 @@ model Blog {
image String? image String?
content Json content Json
isActive Boolean @default(true) isActive Boolean @default(true)
slug String @unique
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
@@ -107,7 +120,8 @@ model Career {
email String? email String?
number String? number String?
status String @default("new") status String @default("new")
isActive Boolean @default(true)
sortOrder Int @default(1000)
candidates Candidate[] candidates Candidate[]
createdAt DateTime @default(now()) createdAt DateTime @default(now())
@@ -175,7 +189,6 @@ model AcademicsResearch {
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
model EmailConfig { model EmailConfig {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
name String name String
@@ -196,8 +209,107 @@ model NewsMedia {
secondPara String? secondPara String?
author String? author String?
date DateTime? date DateTime?
images NewsImage[]
isActive Boolean @default(true) 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()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
+41 -35
View File
@@ -1,58 +1,64 @@
import express from "express"; import express from 'express';
import dotenv from "dotenv"; import dotenv from 'dotenv';
import cors from "cors"; import cors from 'cors';
import departmentRoutes from "./routes/department.routes.js"; import departmentRoutes from './routes/department.routes.js';
import authRoutes from "./routes/auth.routes.js"; import authRoutes from './routes/auth.routes.js';
import blogRoutes from "./routes/blog.routes.js"; import blogRoutes from './routes/blog.routes.js';
import uploadRoutes from "./routes/upload.routes.js"; import uploadRoutes from './routes/upload.routes.js';
import doctorRoutes from "./routes/doctor.routes.js"; import doctorRoutes from './routes/doctor.routes.js';
import careerRoutes from "./routes/career.routes.js"; import careerRoutes from './routes/career.routes.js';
import candidateRoutes from "./routes/candidate.routes.js"; import candidateRoutes from './routes/candidate.routes.js';
import appointmentRoutes from "./routes/appointment.routes.js"; import appointmentRoutes from './routes/appointment.routes.js';
import inquiryRoutes from "./routes/inquiry.routes.js"; import inquiryRoutes from './routes/inquiry.routes.js';
import academicsResearchRoutes from "./routes/academicsResearch.routes.js"; import academicsResearchRoutes from './routes/academicsResearch.routes.js';
import emailConfigRoutes from "./routes/emailConfig.routes.js"; import emailConfigRoutes from './routes/emailConfig.routes.js';
import newsMediaRoutes from "./routes/newsMedia.routes.js"; import newsMediaRoutes from './routes/newsMedia.routes.js';
import importRoutes from './routes/importRoutes.js';
import healthCheckRoutes from './routes/healthCheck.route.js';
dotenv.config(); dotenv.config();
const app = express(); const app = express();
app.use(express.json({ limit: '50mb' }));
app.use(express.urlencoded({ limit: '50mb', extended: true }));
const allowedOrigins = process.env.CORS_ALLOWED_ORIGINS const allowedOrigins = process.env.CORS_ALLOWED_ORIGINS
? process.env.CORS_ALLOWED_ORIGINS.split(" ") ? process.env.CORS_ALLOWED_ORIGINS.split(' ')
: ["http://localhost:3001"]; : ['http://localhost:3001'];
const corsOptions = { const corsOptions = {
origin: function (origin, callback) { origin: function (origin, callback) {
if (!origin || allowedOrigins.includes(origin)) { if (!origin || allowedOrigins.includes(origin)) {
callback(null, true); callback(null, true);
} else { } else {
callback(new Error("Not allowed by CORS")); callback(new Error('Not allowed by CORS'));
} }
}, },
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"], methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: "*", allowedHeaders: '*',
}; };
app.use(express.json());
app.use(cors(corsOptions)); app.use(cors(corsOptions));
app.use("/api/departments", departmentRoutes); app.use('/api/departments', departmentRoutes);
app.use("/api/auth", authRoutes); app.use('/api/auth', authRoutes);
app.use("/api/blogs", blogRoutes); app.use('/api/blogs', blogRoutes);
app.use("/uploads", express.static("uploads")); app.use('/uploads', express.static('uploads'));
app.use("/api/upload", uploadRoutes); app.use('/api/upload', uploadRoutes);
app.use("/api/doctors", doctorRoutes); app.use('/api/doctors', doctorRoutes);
app.use("/api/careers", careerRoutes); app.use('/api/careers', careerRoutes);
app.use("/api/candidates", candidateRoutes); app.use('/api/candidates', candidateRoutes);
app.use("/api/appointments", appointmentRoutes); app.use('/api/appointments', appointmentRoutes);
app.use("/api/inquiry", inquiryRoutes); app.use('/api/inquiry', inquiryRoutes);
app.use("/api/academics", academicsResearchRoutes); app.use('/api/academics', academicsResearchRoutes);
app.use("/api/email", emailConfigRoutes); app.use('/api/email', emailConfigRoutes);
app.use("/api/newsMedia", newsMediaRoutes); app.use('/api/newsMedia', newsMediaRoutes);
app.use('/api/import', importRoutes);
app.use('/api/health-check', healthCheckRoutes);
const PORT = process.env.PORT || 3000; const PORT = process.env.PORT || 5008;
app.listen(PORT, () => { app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`); console.log(`Server running on port ${PORT}`);
}); });
@@ -1,18 +1,17 @@
import prisma from "../prisma/client.js"; import prisma from '../prisma/client.js';
import { sendEmail } from "../utils/sendEmail.js"; import { sendEmail } from '../utils/sendEmail.js';
import { getEmailsByType } from "../utils/getEmailByTypes.js"; import { getEmailsByType } from '../utils/getEmailByTypes.js';
// CREATE ACADEMICS & RESEARCH // CREATE ACADEMICS & RESEARCH
export const createAcademicsResearch = async (req, res) => { export const createAcademicsResearch = async (req, res) => {
try { try {
const { fullName, number, emailId, subject, courseName, message } = const { fullName, number, emailId, subject, courseName, message } = req.body;
req.body;
if (!fullName || !number) { if (!fullName || !number) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: "Full name and number are required", message: 'Full name and number are required',
}); });
} }
@@ -28,42 +27,97 @@ export const createAcademicsResearch = async (req, res) => {
}); });
try { try {
const emailList = await getEmailsByType("ACADEMICS"); const emailList = await getEmailsByType('ACADEMICS');
if (emailList && emailList.length > 0) { if (emailList && emailList.length > 0) {
await sendEmail({ await sendEmail({
to: emailList, to: emailList,
subject: "New Academics & Research Inquiry", subject: 'New Academics & Research Inquiry',
html: ` html: `
<h2>New Academics & Research Inquiry</h2> <div style="font-family: Arial, sans-serif; background-color: #f4f6f8; padding: 20px;">
<p><b>Name:</b> ${fullName}</p> <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>Phone:</b> ${number}</p>
<p><b>Email:</b> ${emailId || "-"}</p>
<p><b>Course:</b> ${courseName || "-"}</p> <!-- Header -->
<p><b>Subject:</b> ${subject || "-"}</p> <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>Message:</b></p> <!-- Body -->
<p>${message || "-"}</p> <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>
`, `,
}); });
} }
} catch (err) { } catch (err) {
console.error("Academics email failed:", err); console.error('Academics email failed:', err);
} }
res.status(200).json({ res.status(200).json({
success: true, success: true,
status: 200, status: 200,
data, data,
message: "Academics & Research added successfully", message: 'Academics & Research added successfully',
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: "Failed to add Academics & Research inquiry", message: 'Failed to add Academics & Research inquiry',
}); });
} }
}; };
@@ -74,7 +128,7 @@ export const getAcademicsResearch = async (req, res) => {
try { try {
const data = await prisma.academicsResearch.findMany({ const data = await prisma.academicsResearch.findMany({
orderBy: { orderBy: {
createdAt: "desc", createdAt: 'desc',
}, },
}); });
@@ -85,7 +139,7 @@ export const getAcademicsResearch = async (req, res) => {
} catch (error) { } catch (error) {
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: "Failed to fetch records", message: 'Failed to fetch records',
}); });
} }
}; };
@@ -105,7 +159,7 @@ export const getSingleAcademicsResearch = async (req, res) => {
if (!data) { if (!data) {
return res.status(404).json({ return res.status(404).json({
success: false, success: false,
message: "Record not found", message: 'Record not found',
}); });
} }
@@ -116,7 +170,7 @@ export const getSingleAcademicsResearch = async (req, res) => {
} catch (error) { } catch (error) {
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: "Failed to fetch record", message: 'Failed to fetch record',
}); });
} }
}; };
@@ -135,12 +189,12 @@ export const deleteAcademicsResearch = async (req, res) => {
res.json({ res.json({
success: true, success: true,
message: "Record deleted successfully", message: 'Record deleted successfully',
}); });
} catch (error) { } catch (error) {
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: "Failed to delete record", message: 'Failed to delete record',
}); });
} }
}; };
+159 -97
View File
@@ -1,16 +1,15 @@
import prisma from "../prisma/client.js"; import prisma from '../prisma/client.js';
import { sendEmail } from "../utils/sendEmail.js"; import { sendEmail } from '../utils/sendEmail.js';
import { getEmailsByType } from "../utils/getEmailByTypes.js"; import { getEmailsByType } from '../utils/getEmailByTypes.js';
export const createAppointment = async (req, res) => { export const createAppointment = async (req, res) => {
try { try {
const { name, mobileNumber, email, message, date, doctorId, departmentId } = const { name, mobileNumber, email, message, date, doctorId, departmentId } = req.body;
req.body;
if (!name || !mobileNumber || !doctorId || !departmentId || !date) { if (!name || !mobileNumber || !doctorId || !departmentId || !date) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: "Required fields missing", message: 'Required fields missing',
}); });
} }
@@ -31,38 +30,111 @@ export const createAppointment = async (req, res) => {
}); });
try { try {
const emailList = await getEmailsByType("APPOINTMENT"); const emailList = await getEmailsByType('APPOINTMENT');
if (emailList) { if (emailList) {
await sendEmail({ await sendEmail({
to: emailList, to: emailList,
subject: "New Appointment Booked", subject: 'New Appointment Booked',
html: ` html: `
<h2>New Appointment Booked</h2> <div style="font-family: Arial, sans-serif; background-color: #f4f6f8; padding: 20px;">
<p><b>Name:</b> ${name}</p>
<p><b>Phone:</b> ${mobileNumber}</p> <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>Email:</b> ${email || "-"}</p>
<p><b>Doctor:</b> ${appointment.doctor?.name}</p> <!-- Header -->
<p><b>Department:</b> ${appointment.department?.name}</p> <div style="background-color: #0d6efd; color: #ffffff; padding: 20px;">
<p><b>Date:</b> ${new Date(date).toLocaleDateString()}</p> <h2 style="margin: 0;">GG Hospital</h2>
<p><b>Message:</b> ${message || "-"}</p> <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>
`, `,
}); });
} }
} catch (err) { } catch (err) {
console.error("Email failed:", err); console.error('Email failed:', err);
} }
res.status(201).json({ res.status(201).json({
success: true, success: true,
message: "Appointment booked successfully", message: 'Appointment booked successfully',
data: appointment, data: appointment,
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: "Failed to create appointment", message: 'Failed to create appointment',
}); });
} }
}; };
@@ -71,74 +143,58 @@ export const createAppointment = async (req, res) => {
export const getAppointments = async (req, res) => { export const getAppointments = async (req, res) => {
try { try {
const page = parseInt(req.query.page); const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit); const limit = parseInt(req.query.limit) || 10;
const skip = (page - 1) * limit;
const search = req.query.search || ""; const { date, startDate, endDate, search } = req.query;
const doctor = req.query.doctor || "";
const department = req.query.department || "";
const date = req.query.date || "";
if (!page && !limit) { const where = {};
const appointments = await prisma.appointment.findMany({
include: {
doctor: true,
department: true,
},
orderBy: { createdAt: "desc" },
});
return res.status(200).json({ const hasSingleDate = date && date.trim() !== '';
success: true,
data: appointments,
meta: null,
});
}
const currentPage = page || 1; const hasRange = (startDate && startDate.trim() !== '') || (endDate && endDate.trim() !== '');
const currentLimit = limit || 10;
const skip = (currentPage - 1) * currentLimit;
const where = { if (hasSingleDate) {
AND: [ const start = new Date(date);
search start.setHours(0, 0, 0, 0);
? {
OR: [
{ name: { contains: search, mode: "insensitive" } },
{ mobileNumber: { contains: search } },
{ email: { contains: search, mode: "insensitive" } },
],
}
: {},
doctor const end = new Date(date);
? { end.setHours(23, 59, 59, 999);
doctor: {
name: { contains: doctor, mode: "insensitive" },
},
}
: {},
department where.date = {
? { gte: start,
department: { lte: end,
name: { contains: department, mode: "insensitive" },
},
}
: {},
date
? {
date: {
gte: new Date(date),
lt: new Date(
new Date(date).setDate(new Date(date).getDate() + 1),
),
},
}
: {},
],
}; };
}
if (!hasSingleDate && hasRange) {
const dateFilter = {};
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' } },
{ mobileNumber: { contains: search } },
{ email: { contains: search, mode: 'insensitive' } },
];
}
const [appointments, total] = await Promise.all([ const [appointments, total] = await Promise.all([
prisma.appointment.findMany({ prisma.appointment.findMany({
@@ -147,28 +203,34 @@ export const getAppointments = async (req, res) => {
doctor: true, doctor: true,
department: true, department: true,
}, },
orderBy: { createdAt: "desc" }, orderBy: {
createdAt: 'desc',
},
skip, skip,
take: currentLimit, take: limit,
}),
prisma.appointment.count({
where,
}), }),
prisma.appointment.count({ where }),
]); ]);
return res.status(200).json({ res.status(200).json({
success: true, success: true,
data: appointments, data: appointments,
meta: { pagination: {
total, total,
page: currentPage, page,
limit: currentLimit, limit,
totalPages: Math.ceil(total / currentLimit), totalPages: Math.ceil(total / limit),
}, },
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: "Failed to fetch appointments", message: 'Failed to fetch appointments',
}); });
} }
}; };
@@ -192,7 +254,7 @@ export const getAppointment = async (req, res) => {
if (!appointment) { if (!appointment) {
return res.status(404).json({ return res.status(404).json({
success: false, success: false,
message: "Appointment not found", message: 'Appointment not found',
}); });
} }
@@ -204,7 +266,7 @@ export const getAppointment = async (req, res) => {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: "Failed to fetch appointment", message: 'Failed to fetch appointment',
}); });
} }
}; };
@@ -224,7 +286,7 @@ export const getAppointmentsByDoctor = async (req, res) => {
department: true, department: true,
}, },
orderBy: { orderBy: {
date: "asc", date: 'asc',
}, },
}); });
@@ -236,7 +298,7 @@ export const getAppointmentsByDoctor = async (req, res) => {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: "Failed to fetch doctor appointments", message: 'Failed to fetch doctor appointments',
}); });
} }
}; };
@@ -265,7 +327,7 @@ export const getAppointmentsByDepartment = async (req, res) => {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: "Failed to fetch department appointments", message: 'Failed to fetch department appointments',
}); });
} }
}; };
@@ -289,14 +351,14 @@ export const updateAppointment = async (req, res) => {
res.status(200).json({ res.status(200).json({
success: true, success: true,
message: "Appointment updated successfully", message: 'Appointment updated successfully',
data: appointment, data: appointment,
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: "Failed to update appointment", message: 'Failed to update appointment',
}); });
} }
}; };
@@ -315,13 +377,13 @@ export const deleteAppointment = async (req, res) => {
res.status(200).json({ res.status(200).json({
success: true, success: true,
message: "Appointment deleted successfully", message: 'Appointment deleted successfully',
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, 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 prisma from '../prisma/client.js';
import {generateToken} from "../utils/jwt.js"; import { generateToken } from '../utils/jwt.js';
import {hashPassword, comparePassword} from "../utils/password.js"; import { hashPassword, comparePassword } from '../utils/password.js';
/** /**
* REGISTER * REGISTER
@@ -10,7 +10,7 @@ export async function register(req, res) {
const { username, password, role } = req.body; const { username, password, role } = req.body;
if (!username || !password) { 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({ const existingUser = await prisma.user.findUnique({
@@ -18,7 +18,7 @@ export async function register(req, res) {
}); });
if (existingUser) { 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); const hashedPassword = await hashPassword(password);
@@ -27,12 +27,12 @@ export async function register(req, res) {
data: { data: {
username, username,
password: hashedPassword, password: hashedPassword,
role: role || "admin", role: role || 'admin',
}, },
}); });
res.status(201).json({ res.status(201).json({
message: "User registered successfully", message: 'User registered successfully',
user: { user: {
id: user.id, id: user.id,
username: user.username, username: user.username,
@@ -49,7 +49,7 @@ export async function login(req, res) {
const { username, password } = req.body; const { username, password } = req.body;
if (!username || !password) { 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({ const user = await prisma.user.findUnique({
@@ -57,13 +57,13 @@ export async function login(req, res) {
}); });
if (!user) { 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); const isValid = await comparePassword(password, user.password);
if (!isValid) { if (!isValid) {
return res.status(401).json({error: "Invalid credentials"}); return res.status(401).json({ error: 'Invalid credentials' });
} }
const token = generateToken({ const token = generateToken({
+28 -6
View File
@@ -1,4 +1,5 @@
import prisma from "../prisma/client.js"; import prisma from '../prisma/client.js';
import slugify from 'slugify';
/* CREATE BLOG */ /* CREATE BLOG */
@@ -13,12 +14,13 @@ export async function createBlog(req, res) {
image, image,
content, content,
isActive, isActive,
slug: slugify(title),
}, },
}); });
res.json(blog); res.json(blog);
} catch (error) { } catch (error) {
res.status(500).json({error: "Blog creation failed"}); res.status(500).json({ error: 'Blog creation failed' });
} }
} }
@@ -28,7 +30,7 @@ export async function getBlogs(req, res) {
try { try {
const blogs = await prisma.blog.findMany({ const blogs = await prisma.blog.findMany({
where: { isActive: true }, where: { isActive: true },
orderBy: {createdAt: "desc"}, orderBy: { createdAt: 'desc' },
}); });
res.json(blogs); res.json(blogs);
@@ -42,7 +44,7 @@ export async function getBlogs(req, res) {
export async function getAllBlogs(req, res) { export async function getAllBlogs(req, res) {
try { try {
const blogs = await prisma.blog.findMany({ const blogs = await prisma.blog.findMany({
orderBy: {createdAt: "desc"}, orderBy: { createdAt: 'desc' },
}); });
res.json(blogs); res.json(blogs);
@@ -54,6 +56,26 @@ export async function getAllBlogs(req, res) {
/* GET SINGLE BLOG */ /* GET SINGLE BLOG */
export async function getBlog(req, res) { 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 { try {
const id = Number(req.params.id); const id = Number(req.params.id);
@@ -62,7 +84,7 @@ export async function getBlog(req, res) {
}); });
if (!blog) { if (!blog) {
return res.status(404).json({error: "Blog not found"}); return res.status(404).json({ error: 'Blog not found' });
} }
res.json(blog); res.json(blog);
@@ -103,7 +125,7 @@ export async function deleteBlog(req, res) {
where: { id }, where: { id },
}); });
res.json({message: "Blog deleted successfully"}); res.json({ message: 'Blog deleted successfully' });
} catch (error) { } catch (error) {
res.status(500).json({ error: error.message }); res.status(500).json({ error: error.message });
} }
+92 -30
View File
@@ -1,19 +1,18 @@
import prisma from "../prisma/client.js"; import prisma from '../prisma/client.js';
import { sendEmail } from "../utils/sendEmail.js"; import { sendEmail } from '../utils/sendEmail.js';
import { getEmailsByType } from "../utils/getEmailByTypes.js"; import { getEmailsByType } from '../utils/getEmailByTypes.js';
// CREATE CANDIDATE // CREATE CANDIDATE
export const createCandidate = async (req, res) => { export const createCandidate = async (req, res) => {
try { try {
const { fullName, mobile, email, subject, coverLetter, careerId } = const { fullName, mobile, email, subject, coverLetter, careerId } = req.body;
req.body;
if (!fullName || !mobile || !email || !careerId) { if (!fullName || !mobile || !email || !careerId) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: "Required fields missing", message: 'Required fields missing',
}); });
} }
@@ -32,42 +31,105 @@ export const createCandidate = async (req, res) => {
}); });
try { try {
const emailList = await getEmailsByType("CANDIDATE"); const emailList = await getEmailsByType('CANDIDATE');
if (emailList && emailList.length > 0) { if (emailList && emailList.length > 0) {
await sendEmail({ await sendEmail({
to: emailList, to: emailList,
subject: "New Job Application Received", subject: 'New Job Application Received',
html: ` html: `
<h2>New Candidate Application</h2> <div style="font-family: Arial, sans-serif; background-color: #f4f6f8; padding: 20px;">
<p><b>Name:</b> ${fullName}</p> <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>Phone:</b> ${mobile}</p>
<p><b>Email:</b> ${email}</p>
<p><b>Applied For:</b> ${candidate.career?.post || "-"}</p> <!-- Header -->
<p><b>Designation:</b> ${candidate.career?.designation || "-"}</p> <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>Subject:</b> ${subject || "-"}</p> <!-- Body -->
<p><b>Cover Letter:</b></p> <div style="padding: 20px; color: #333;">
<p>${coverLetter || "-"}</p>
<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>
`, `,
}); });
} }
} catch (err) { } catch (err) {
console.error("Candidate email failed:", err); console.error('Candidate email failed:', err);
} }
res.status(201).json({ res.status(201).json({
success: true, success: true,
message: "Application submitted successfully", message: 'Application submitted successfully',
data: candidate, data: candidate,
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: "Failed to create candidate", message: 'Failed to create candidate',
}); });
} }
}; };
@@ -81,7 +143,7 @@ export const getCandidates = async (req, res) => {
career: true, career: true,
}, },
orderBy: { orderBy: {
createdAt: "desc", createdAt: 'desc',
}, },
}); });
@@ -93,7 +155,7 @@ export const getCandidates = async (req, res) => {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: "Failed to fetch candidates", message: 'Failed to fetch candidates',
}); });
} }
}; };
@@ -116,7 +178,7 @@ export const getCandidate = async (req, res) => {
if (!candidate) { if (!candidate) {
return res.status(404).json({ return res.status(404).json({
success: false, success: false,
message: "Candidate not found", message: 'Candidate not found',
}); });
} }
@@ -128,7 +190,7 @@ export const getCandidate = async (req, res) => {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: "Failed to fetch candidate", message: 'Failed to fetch candidate',
}); });
} }
}; };
@@ -147,7 +209,7 @@ export const getCandidatesByCareer = async (req, res) => {
career: true, career: true,
}, },
orderBy: { orderBy: {
createdAt: "desc", createdAt: 'desc',
}, },
}); });
@@ -159,7 +221,7 @@ export const getCandidatesByCareer = async (req, res) => {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: "Failed to fetch candidates", message: 'Failed to fetch candidates',
}); });
} }
}; };
@@ -179,14 +241,14 @@ export const updateCandidate = async (req, res) => {
res.status(200).json({ res.status(200).json({
success: true, success: true,
message: "Candidate updated successfully", message: 'Candidate updated successfully',
data: candidate, data: candidate,
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: "Failed to update candidate", message: 'Failed to update candidate',
}); });
} }
}; };
@@ -205,13 +267,13 @@ export const deleteCandidate = async (req, res) => {
res.status(200).json({ res.status(200).json({
success: true, success: true,
message: "Candidate deleted successfully", message: 'Candidate deleted successfully',
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: "Failed to delete candidate", message: 'Failed to delete candidate',
}); });
} }
}; };
+24 -20
View File
@@ -1,11 +1,14 @@
import prisma from "../prisma/client.js"; import prisma from '../prisma/client.js';
// GET ALL CAREERS // GET ALL CAREERS
export const getAllCareers = async (req, res) => { export const getAllCareers = async (req, res) => {
try { try {
const { admin } = req.query;
const careers = await prisma.career.findMany({ const careers = await prisma.career.findMany({
orderBy: {createdAt: "desc"}, where: admin === 'true' ? {} : { isActive: true },
orderBy: [{ sortOrder: 'asc' }, { createdAt: 'desc' }],
}); });
const response = careers.map((c) => ({ const response = careers.map((c) => ({
@@ -17,6 +20,8 @@ export const getAllCareers = async (req, res) => {
email: c.email, email: c.email,
number: c.number, number: c.number,
status: c.status, status: c.status,
isActive: c.isActive,
sortOrder: c.sortOrder,
})); }));
return res.status(200).json({ return res.status(200).json({
@@ -27,7 +32,7 @@ export const getAllCareers = async (req, res) => {
console.error(error); console.error(error);
return res.status(500).json({ return res.status(500).json({
success: false, success: false,
message: "Failed to fetch careers", message: 'Failed to fetch careers',
}); });
} }
}; };
@@ -36,20 +41,12 @@ export const getAllCareers = async (req, res) => {
export const createCareer = async (req, res) => { export const createCareer = async (req, res) => {
try { try {
const { const { post, designation, qualification, experienceNeed, email, number, status, isActive, sortOrder } = req.body;
post,
designation,
qualification,
experienceNeed,
email,
number,
status,
} = req.body;
if (!post || !designation) { if (!post || !designation) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: "Post and designation are required", message: 'Post and designation are required',
}); });
} }
@@ -62,19 +59,21 @@ export const createCareer = async (req, res) => {
email, email,
number, number,
status, status,
isActive: isActive !== undefined ? isActive : true,
sortOrder: sortOrder !== undefined ? Number(sortOrder) : 0,
}, },
}); });
return res.status(201).json({ return res.status(201).json({
success: true, success: true,
message: "Career created successfully", message: 'Career created successfully',
data: career, data: career,
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return res.status(500).json({ return res.status(500).json({
success: false, success: false,
message: "Failed to create career", message: 'Failed to create career',
}); });
} }
}; };
@@ -84,22 +83,27 @@ export const createCareer = async (req, res) => {
export const updateCareer = async (req, res) => { export const updateCareer = async (req, res) => {
try { try {
const { id } = req.params; const { id } = req.params;
const updateData = { ...req.body };
if (updateData.sortOrder !== undefined) {
updateData.sortOrder = Number(updateData.sortOrder);
}
const career = await prisma.career.update({ const career = await prisma.career.update({
where: { id: Number(id) }, where: { id: Number(id) },
data: req.body, data: updateData,
}); });
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
message: "Career updated successfully", message: 'Career updated successfully',
data: career, data: career,
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return res.status(500).json({ return res.status(500).json({
success: false, success: false,
message: "Failed to update career", message: 'Failed to update career',
}); });
} }
}; };
@@ -116,13 +120,13 @@ export const deleteCareer = async (req, res) => {
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
message: "Career deleted successfully", message: 'Career deleted successfully',
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return res.status(500).json({ return res.status(500).json({
success: false, success: false,
message: "Failed to delete career", message: 'Failed to delete career',
}); });
} }
}; };
@@ -1,19 +1,25 @@
import prisma from "../prisma/client.js"; import prisma from '../prisma/client.js';
export const getAllDepartments = async (req, res) => { export const getAllDepartments = async (req, res) => {
try { try {
const { admin } = req.query;
const departments = await prisma.department.findMany({ const departments = await prisma.department.findMany({
orderBy: {name: "asc"}, where: admin === 'true' ? {} : { isActive: true },
orderBy: [{ sortOrder: 'asc' }, { name: 'asc' }],
}); });
const response = departments.map((dep) => ({ const response = departments.map((dep) => ({
departmentId: dep.departmentId, departmentId: dep.departmentId,
name: dep.name, name: dep.name,
para1: dep.para1 ?? "", image: dep.image ?? '',
para2: dep.para2 ?? "", para1: dep.para1 ?? '',
para3: dep.para3 ?? "", para2: dep.para2 ?? '',
facilities: dep.facilities ?? "", para3: dep.para3 ?? '',
services: dep.services ?? "", facilities: dep.facilities ?? '',
services: dep.services ?? '',
isActive: dep.isActive,
sortOrder: dep.sortOrder,
})); }));
return res.status(200).json({ return res.status(200).json({
@@ -24,75 +30,122 @@ export const getAllDepartments = async (req, res) => {
console.error(error); console.error(error);
return res.status(500).json({ return res.status(500).json({
success: false, success: false,
message: "Failed to fetch departments", 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',
}); });
} }
}; };
export async function createDepartment(req, res) { export async function createDepartment(req, res) {
try { try {
const {departmentId, name, para1, para2, para3, facilities, services} = const { departmentId, name, image, para1, para2, para3, facilities, services, isActive, sortOrder } = req.body;
req.body;
if (!departmentId || !name) { if (!departmentId || !name) {
return res return res.status(400).json({ error: 'departmentId and name are required' });
.status(400)
.json({error: "departmentId and name are required"});
} }
const department = await prisma.department.create({ const department = await prisma.department.create({
data: { data: {
departmentId, departmentId,
name, name,
image,
para1, para1,
para2, para2,
para3, para3,
facilities, facilities,
services, services,
isActive: isActive !== undefined ? isActive : true,
sortOrder: sortOrder !== undefined ? Number(sortOrder) : 0,
}, },
}); });
res.status(201).json({ res.status(201).json({
message: "Department created successfully", message: 'Department created successfully',
data: department, data: department,
}); });
} catch (error) { } catch (error) {
if (error.code === "P2002") { if (error.code === 'P2002') {
return res.status(409).json({error: "Department already exists"}); 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) => { export const updateDepartment = async (req, res) => {
try { try {
const { departmentId } = req.params; const { departmentId } = req.params;
const updateData = { ...req.body };
const {name, para1, para2, para3, facilities, services} = req.body; if (updateData.sortOrder !== undefined) {
updateData.sortOrder = Number(updateData.sortOrder);
}
const department = await prisma.department.update({ const department = await prisma.department.update({
where: { departmentId }, where: { departmentId },
data: { data: updateData,
name,
para1,
para2,
para3,
facilities,
services,
},
}); });
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
message: "Department updated successfully", message: 'Department updated successfully',
data: department, data: department,
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return res.status(500).json({ return res.status(500).json({
success: false, success: false,
message: "Failed to update department", message: 'Failed to update department',
}); });
} }
}; };
@@ -107,13 +160,13 @@ export const deleteDepartment = async (req, res) => {
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
message: "Department deleted successfully", message: 'Department deleted successfully',
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return res.status(500).json({ return res.status(500).json({
success: false, success: false,
message: "Failed to delete department", message: 'Failed to delete department',
}); });
} }
}; };
+361 -84
View File
@@ -1,33 +1,60 @@
import prisma from "../prisma/client.js"; import prisma from '../prisma/client.js';
// get doctors // get doctors
export const getAllDoctors = async (req, res) => { export const getAllDoctors = async (req, res) => {
try { try {
const { admin } = req.query;
const doctors = await prisma.doctor.findMany({ const doctors = await prisma.doctor.findMany({
where: admin === 'true' ? {} : { isActive: true },
include: { include: {
seo: true,
departments: { departments: {
include: { include: {
department: true, department: true,
timing: true, timing: true,
}, },
}, },
specializations: {
orderBy: {
createdAt: 'asc',
}, },
orderBy: {name: "asc"}, },
},
orderBy: [{ globalSortOrder: 'asc' }, { name: 'asc' }],
}); });
const formatted = doctors.map((doc, index) => { const formatted = doctors.map((doc, index) => ({
return {
SL_NO: String(index + 1), SL_NO: String(index + 1),
doctorId: doc.doctorId, doctorId: doc.doctorId,
name: doc.name, name: doc.name,
image: doc.image ?? '',
designation: doc.designation, designation: doc.designation,
workingStatus: doc.workingStatus, workingStatus: doc.workingStatus,
qualification: doc.qualification, qualification: doc.qualification,
isActive: doc.isActive,
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) => { departments: doc.departments.map((d) => {
const t = d.timing || {}; const t = d.timing || {};
const timingArray = [ const timingArray = [
t.monday && `Monday ${t.monday}`, t.monday && `Monday ${t.monday}`,
t.tuesday && `Tuesday ${t.tuesday}`, t.tuesday && `Tuesday ${t.tuesday}`,
@@ -42,12 +69,11 @@ export const getAllDoctors = async (req, res) => {
return { return {
departmentId: d.department.departmentId, departmentId: d.department.departmentId,
departmentName: d.department.name, departmentName: d.department.name,
timing: timingArray.join(' & '),
timing: timingArray.join(" & "), deptSortOrder: d.sortOrder,
}; };
}), }),
}; }));
});
res.status(200).json({ res.status(200).json({
success: true, success: true,
@@ -57,7 +83,7 @@ export const getAllDoctors = async (req, res) => {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: "Failed to fetch doctors", message: 'Failed to fetch doctors',
}); });
} }
}; };
@@ -71,6 +97,8 @@ export const getDoctorByDoctorId = async (req, res) => {
const doctor = await prisma.doctor.findUnique({ const doctor = await prisma.doctor.findUnique({
where: { doctorId }, where: { doctorId },
include: { include: {
seo: true,
specializations: true,
departments: { departments: {
include: { include: {
department: true, department: true,
@@ -83,16 +111,35 @@ export const getDoctorByDoctorId = async (req, res) => {
if (!doctor) { if (!doctor) {
return res.status(404).json({ return res.status(404).json({
success: false, success: false,
message: "Doctor not found", message: 'Doctor not found',
}); });
} }
const response = { const response = {
doctorId: doctor.doctorId, doctorId: doctor.doctorId,
name: doctor.name, name: doctor.name,
image: doctor.image ?? '',
designation: doctor.designation, designation: doctor.designation,
workingStatus: doctor.workingStatus, workingStatus: doctor.workingStatus,
qualification: doctor.qualification, qualification: doctor.qualification,
experience: doctor.experience,
professionalSummary: doctor.professionalSummary,
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) => ({ departments: doctor.departments.map((d) => ({
departmentId: d.department.departmentId, departmentId: d.department.departmentId,
departmentName: d.department.name, departmentName: d.department.name,
@@ -108,7 +155,71 @@ export const getDoctorByDoctorId = async (req, res) => {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: "Failed to fetch doctor", 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',
}); });
} }
}; };
@@ -119,19 +230,69 @@ export const createDoctor = async (req, res) => {
const { const {
doctorId, doctorId,
name, name,
image,
designation, designation,
workingStatus, workingStatus,
qualification, qualification,
isActive,
globalSortOrder,
departments, departments,
experience,
professionalSummary,
seoTitle,
metaDescription,
focusKeyphrase,
slug,
tags,
specializations,
ogTitle,
ogDescription,
ogImage,
} = req.body; } = 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({ const doctor = await prisma.doctor.create({
data: { data: {
doctorId, doctorId,
name, name,
image,
designation, designation,
workingStatus, workingStatus,
qualification, qualification,
experience: experience ? Number(experience) : null,
professionalSummary,
seoId: seo.id,
isActive: isActive !== undefined ? isActive : true,
globalSortOrder: globalSortOrder !== undefined ? Number(globalSortOrder) : 0,
}, },
}); });
@@ -146,6 +307,7 @@ export const createDoctor = async (req, res) => {
data: { data: {
doctorId: doctor.id, doctorId: doctor.id,
departmentId: department.id, departmentId: department.id,
sortOrder: dep.sortOrder !== undefined ? Number(dep.sortOrder) : 0,
}, },
}); });
@@ -158,16 +320,27 @@ 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({ res.status(201).json({
success: true, success: true,
message: "Doctor created successfully", message: 'Doctor created successfully',
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: "Failed to create doctor", message: 'Failed to create doctor',
}); });
} }
}; };
@@ -175,18 +348,65 @@ export const createDoctor = async (req, res) => {
//update doctors //update doctors
export const updateDoctor = async (req, res) => { export const updateDoctor = async (req, res) => {
try { try {
const {doctorId} = req.params; const { doctorId, action } = req.params;
const {name, designation, workingStatus, qualification, departments} = const {
req.body; name,
designation,
image,
workingStatus,
qualification,
isActive,
globalSortOrder,
departments,
experience,
professionalSummary,
seoTitle,
metaDescription,
ogTitle,
ogDescription,
focusKeyphrase,
slug,
tags,
ogImage,
specializations,
} = req.body;
const doctor = await prisma.doctor.findUnique({ if (!doctorId) {
where: {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,
},
}); });
if (!doctor) { return res.status(200).json({
return res.status(404).json({ success: true,
message: `Doctor has been ${doctor.isActive ? 'deactivated' : 'activated'} 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, success: false,
message: "Doctor not found", message: messages.join(', '),
}); });
} }
@@ -195,28 +415,91 @@ export const updateDoctor = async (req, res) => {
data: { data: {
name, name,
designation, designation,
image,
workingStatus, workingStatus,
qualification, qualification,
isActive,
experience: experience ? Number(experience) : null,
professionalSummary,
globalSortOrder: globalSortOrder !== undefined ? Number(globalSortOrder) : undefined,
}, },
}); });
const oldRelations = await prisma.doctorDepartment.findMany({ if (doctor.seoId) {
where: {doctorId: doctor.id}, 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 || [],
},
}); });
for (const rel of oldRelations) { await prisma.doctor.update({
await prisma.doctorTiming.deleteMany({ where: {
where: {doctorDepartmentId: rel.id}, id: doctor.id,
},
data: {
seoId: seo.id,
},
}); });
} }
await prisma.doctorDepartment.deleteMany({ // Update Departments & Timings
where: {doctorId: doctor.id}, 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) { for (const dep of departments) {
const department = await prisma.department.findUnique({ const department = await prisma.department.findUnique({
where: {departmentId: dep.departmentId}, where: {
departmentId: dep.departmentId,
},
}); });
if (!department) continue; if (!department) continue;
@@ -225,29 +508,48 @@ export const updateDoctor = async (req, res) => {
data: { data: {
doctorId: doctor.id, doctorId: doctor.id,
departmentId: department.id, departmentId: department.id,
sortOrder: dep.sortOrder !== undefined ? Number(dep.sortOrder) : 0,
}, },
}); });
if (dep.timing) { if (dep.timing && Object.keys(dep.timing).length > 0) {
const { id, doctorDepartmentId, createdAt, updatedAt, ...cleanTiming } = dep.timing;
await prisma.doctorTiming.create({ await prisma.doctorTiming.create({
data: { data: {
doctorDepartmentId: doctorDepartment.id, doctorDepartmentId: doctorDepartment.id,
...dep.timing, ...cleanTiming,
}, },
}); });
} }
} }
}
res.status(200).json({ // Update Specializations
success: true, if (Array.isArray(specializations)) {
message: "Doctor updated successfully", 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) { } catch (error) {
console.error(error); console.error('Update Error:', error);
res.status(500).json({ res.status(500).json({ success: false, message: 'Failed to update doctor' });
success: false,
message: "Failed to update doctor",
});
} }
}; };
//delete doctor //delete doctor
@@ -256,13 +558,6 @@ export const deleteDoctor = async (req, res) => {
try { try {
const { doctorId } = req.params; const { doctorId } = req.params;
if (!doctorId) {
return res.status(400).json({
success: false,
message: "Doctor ID is required",
});
}
const doctor = await prisma.doctor.findUnique({ const doctor = await prisma.doctor.findUnique({
where: { doctorId }, where: { doctorId },
}); });
@@ -270,7 +565,7 @@ export const deleteDoctor = async (req, res) => {
if (!doctor) { if (!doctor) {
return res.status(404).json({ return res.status(404).json({
success: false, success: false,
message: `Doctor with ID ${doctorId} not found`, message: 'Doctor not found',
}); });
} }
@@ -294,13 +589,13 @@ export const deleteDoctor = async (req, res) => {
res.status(200).json({ res.status(200).json({
success: true, success: true,
message: `Doctor ${doctorId} deleted successfully`, message: 'Doctor deleted successfully',
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: "Failed to delete doctor", message: 'Failed to delete doctor',
}); });
} }
}; };
@@ -320,23 +615,19 @@ export const getDoctorTimings = async (req, res) => {
}); });
const result = doctors.map((doc) => { const result = doctors.map((doc) => {
let timing = {}; const timing = doc.departments[0]?.timing || {};
if (doc.departments.length > 0) {
timing = doc.departments[0].timing ?? {};
}
return { return {
Doctor_ID: doc.doctorId, Doctor_ID: doc.doctorId,
Doctor: doc.name, Doctor: doc.name,
Monday: timing?.monday ?? "", Monday: timing.monday || '',
Tuesday: timing?.tuesday ?? "", Tuesday: timing.tuesday || '',
Wednesday: timing?.wednesday ?? "", Wednesday: timing.wednesday || '',
Thursday: timing?.thursday ?? "", Thursday: timing.thursday || '',
Friday: timing?.friday ?? "", Friday: timing.friday || '',
Saturday: timing?.saturday ?? "", Saturday: timing.saturday || '',
Sunday: timing?.sunday ?? "", Sunday: timing.sunday || '',
Additional: timing?.additional ?? "", Additional: timing.additional || '',
}; };
}); });
@@ -348,7 +639,7 @@ export const getDoctorTimings = async (req, res) => {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: "Failed to fetch doctor timings", message: 'Failed to fetch doctor timings',
}); });
} }
}; };
@@ -373,33 +664,19 @@ export const getDoctorTimingById = async (req, res) => {
if (!doctor) { if (!doctor) {
return res.status(404).json({ return res.status(404).json({
success: false, success: false,
message: "Doctor not found", message: 'Doctor not found',
}); });
} }
const result = { const result = {
doctorId: doctor.doctorId, doctorId: doctor.doctorId,
doctorName: doctor.name, doctorName: doctor.name,
departments: doctor.departments.map((d) => ({
departments: doctor.departments.map((d) => {
const t = d.timing || {};
return {
departmentId: d.department.departmentId, departmentId: d.department.departmentId,
departmentName: d.department.name, departmentName: d.department.name,
deptSortOrder: d.sortOrder,
timing: { timing: d.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({ res.status(200).json({
@@ -410,7 +687,7 @@ export const getDoctorTimingById = async (req, res) => {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: "Failed to fetch doctor timing", message: 'Failed to fetch doctor timing',
}); });
} }
}; };
@@ -1,4 +1,4 @@
import prisma from "../prisma/client.js"; import prisma from '../prisma/client.js';
// CREATE // CREATE
export const createEmailConfig = async (req, res) => { export const createEmailConfig = async (req, res) => {
@@ -8,7 +8,7 @@ export const createEmailConfig = async (req, res) => {
if (!name || !email || !type) { if (!name || !email || !type) {
return res.status(400).json({ return res.status(400).json({
success: false, 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({ res.status(201).json({
success: true, success: true,
message: "Email config created", message: 'Email config created',
data: newEmail, data: newEmail,
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, 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 { try {
const emails = await prisma.emailConfig.findMany({ const emails = await prisma.emailConfig.findMany({
orderBy: { orderBy: {
createdAt: "desc", createdAt: 'desc',
}, },
}); });
@@ -52,7 +52,7 @@ export const getEmailConfigs = async (req, res) => {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, 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) { if (!email) {
return res.status(404).json({ return res.status(404).json({
success: false, 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); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, 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({ res.status(200).json({
success: true, success: true,
message: "Email config updated", message: 'Email config updated',
data: updated, data: updated,
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, 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({ res.status(200).json({
success: true, success: true,
message: "Email config deleted", message: 'Email config deleted',
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: "Failed to delete email config", message: 'Failed to delete email config',
}); });
} }
}; };
@@ -0,0 +1,488 @@
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' });
}
};
+292
View File
@@ -0,0 +1,292 @@
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 });
}
};
+87 -10
View File
@@ -1,4 +1,7 @@
import prisma from "../prisma/client.js"; import prisma from '../prisma/client.js';
import { sendEmail } from '../utils/sendEmail.js';
import { getEmailsByType } from '../utils/getEmailByTypes.js';
/* CREATE INQUIRY */ /* CREATE INQUIRY */
export const createInquiry = async (req, res) => { export const createInquiry = async (req, res) => {
@@ -8,7 +11,7 @@ export const createInquiry = async (req, res) => {
if (!fullName || !number) { if (!fullName || !number) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: "Full name and number are required", message: 'Full name and number are required',
}); });
} }
@@ -21,18 +24,92 @@ export const createInquiry = async (req, res) => {
message, 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({ res.status(200).json({
success: true, success: true,
status: 200, status: 200,
data: inquiry, data: inquiry,
message: "Inquiry added successfully", message: 'Inquiry added successfully',
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: "Failed to add inquiry", message: 'Failed to add inquiry',
}); });
} }
}; };
@@ -42,7 +119,7 @@ export const getInquiries = async (req, res) => {
try { try {
const inquiries = await prisma.inquiry.findMany({ const inquiries = await prisma.inquiry.findMany({
orderBy: { orderBy: {
createdAt: "desc", createdAt: 'desc',
}, },
}); });
@@ -53,7 +130,7 @@ export const getInquiries = async (req, res) => {
} catch (error) { } catch (error) {
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: "Failed to fetch inquiries", message: 'Failed to fetch inquiries',
}); });
} }
}; };
@@ -70,7 +147,7 @@ export const getInquiry = async (req, res) => {
if (!inquiry) { if (!inquiry) {
return res.status(404).json({ return res.status(404).json({
success: false, success: false,
message: "Inquiry not found", message: 'Inquiry not found',
}); });
} }
@@ -81,7 +158,7 @@ export const getInquiry = async (req, res) => {
} catch (error) { } catch (error) {
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: "Failed to fetch inquiry", message: 'Failed to fetch inquiry',
}); });
} }
}; };
@@ -97,12 +174,12 @@ export const deleteInquiry = async (req, res) => {
res.json({ res.json({
success: true, success: true,
message: "Inquiry deleted successfully", message: 'Inquiry deleted successfully',
}); });
} catch (error) { } catch (error) {
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: "Failed to delete inquiry", message: 'Failed to delete inquiry',
}); });
} }
}; };
+64 -44
View File
@@ -1,46 +1,44 @@
import prisma from "../prisma/client.js"; import prisma from '../prisma/client.js';
// GET ALL NEWS // GET ALL NEWS
export const getAllNews = async (req, res) => { export const getAllNews = async (req, res) => {
try { try {
const page = parseInt(req.query.page); const page = req.query.page ? parseInt(req.query.page) : null;
const limit = parseInt(req.query.limit); const limit = req.query.limit ? parseInt(req.query.limit) : null;
const search = req.query.search?.trim() || '';
if (!page && !limit) { const includeImages = {
const news = await prisma.newsMedia.findMany({ images: true,
orderBy: { createdAt: "desc" }, };
});
const response = news.map((n) => ({ const searchFilter = search
Id: n.id.toString(), ? {
Headline: n.headline, headline: {
Content: n.content, contains: search,
FirstPara: n.firstPara, mode: 'insensitive',
SecondPara: n.secondPara, },
Date: n.date,
Author: n.author,
}));
return res.status(200).json({
success: true,
data: response,
meta: null,
});
} }
: {};
const currentPage = page || 1; const whereCondition = {
const currentLimit = limit || 10; ...searchFilter,
};
const skip = (currentPage - 1) * currentLimit; const skip = page && limit ? (page - 1) * limit : undefined;
const take = limit ? limit : undefined;
const [news, total] = await Promise.all([ const [news, total] = await Promise.all([
prisma.newsMedia.findMany({ prisma.newsMedia.findMany({
orderBy: { createdAt: "desc" }, where: whereCondition,
include: includeImages,
orderBy: { createdAt: 'desc' },
skip, skip,
take: currentLimit, take,
}),
prisma.newsMedia.count({
where: whereCondition,
}), }),
prisma.newsMedia.count(),
]); ]);
const response = news.map((n) => ({ const response = news.map((n) => ({
@@ -51,6 +49,10 @@ export const getAllNews = async (req, res) => {
SecondPara: n.secondPara, SecondPara: n.secondPara,
Date: n.date, Date: n.date,
Author: n.author, Author: n.author,
Images: n.images.map((img) => ({
id: img.id,
image: img.url,
})),
})); }));
return res.status(200).json({ return res.status(200).json({
@@ -58,20 +60,19 @@ export const getAllNews = async (req, res) => {
data: response, data: response,
meta: { meta: {
total, total,
page: currentPage, page: page || 1,
limit: currentLimit, limit: limit || total,
totalPages: Math.ceil(total / currentLimit), totalPages: limit ? Math.ceil(total / limit) : 1,
}, },
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return res.status(500).json({ return res.status(500).json({
success: false, success: false,
message: "Failed to fetch news", message: 'Failed to fetch news',
}); });
} }
}; };
// GET NEWS BY ID // GET NEWS BY ID
export const getNewsById = async (req, res) => { export const getNewsById = async (req, res) => {
@@ -80,12 +81,13 @@ export const getNewsById = async (req, res) => {
const n = await prisma.newsMedia.findUnique({ const n = await prisma.newsMedia.findUnique({
where: { id: Number(id) }, where: { id: Number(id) },
include: { images: true },
}); });
if (!n) { if (!n) {
return res.status(404).json({ return res.status(404).json({
success: false, success: false,
message: "News not found", message: 'News not found',
}); });
} }
@@ -97,6 +99,10 @@ export const getNewsById = async (req, res) => {
SecondPara: n.secondPara, SecondPara: n.secondPara,
Date: n.date, Date: n.date,
Author: n.author, Author: n.author,
Images: n.images.map((img) => ({
id: img.id,
image: img.url,
})),
}; };
return res.status(200).json({ return res.status(200).json({
@@ -107,7 +113,7 @@ export const getNewsById = async (req, res) => {
console.error(error); console.error(error);
return res.status(500).json({ return res.status(500).json({
success: false, success: false,
message: "Failed to fetch news", message: 'Failed to fetch news',
}); });
} }
}; };
@@ -116,12 +122,12 @@ export const getNewsById = async (req, res) => {
export const createNews = async (req, res) => { export const createNews = async (req, res) => {
try { try {
const { headline, content, firstPara, secondPara, date, author } = req.body; const { headline, content, firstPara, secondPara, date, author, imageUrls } = req.body;
if (!headline) { if (!headline) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: "Headline is required", message: 'Headline is required',
}); });
} }
@@ -133,19 +139,25 @@ export const createNews = async (req, res) => {
secondPara, secondPara,
date: date ? new Date(date) : null, date: date ? new Date(date) : null,
author, author,
images: imageUrls
? {
create: imageUrls.map((url) => ({ url })),
}
: undefined,
}, },
include: { images: true },
}); });
return res.status(201).json({ return res.status(201).json({
success: true, success: true,
message: "News created successfully", message: 'News created successfully',
data: news, data: news,
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return res.status(500).json({ return res.status(500).json({
success: false, success: false,
message: "Failed to create news", message: 'Failed to create news',
}); });
} }
}; };
@@ -155,25 +167,33 @@ export const createNews = async (req, res) => {
export const updateNews = async (req, res) => { export const updateNews = async (req, res) => {
try { try {
const { id } = req.params; const { id } = req.params;
const { imageUrls, ...otherData } = req.body;
const news = await prisma.newsMedia.update({ const news = await prisma.newsMedia.update({
where: { id: Number(id) }, where: { id: Number(id) },
data: { data: {
...req.body, ...otherData,
date: req.body.date ? new Date(req.body.date) : undefined, 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({ return res.status(200).json({
success: true, success: true,
message: "News updated successfully", message: 'News updated successfully',
data: news, data: news,
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return res.status(500).json({ return res.status(500).json({
success: false, success: false,
message: "Failed to update news", message: 'Failed to update news',
}); });
} }
}; };
@@ -190,13 +210,13 @@ export const deleteNews = async (req, res) => {
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
message: "News deleted successfully", message: 'News deleted successfully',
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return res.status(500).json({ return res.status(500).json({
success: false, 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 multer from 'multer';
import path from "path"; import path from 'path';
const storage = multer.diskStorage({ const storage = multer.diskStorage({
destination: function (req, file, cb) { destination: function (req, file, cb) {
cb(null, "uploads/blog"); cb(null, 'uploads/blog');
}, },
filename: function (req, file, cb) { 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) { export default function jwtAuthMiddleware(req, res, next) {
const authHeader = req.headers.authorization; const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith("Bearer ")) { if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({error: "No token provided"}); return res.status(401).json({ error: 'No token provided' });
} }
const token = authHeader.split(" ")[1]; const token = authHeader.split(' ')[1];
try { try {
const user = verifyToken(token); const user = verifyToken(token);
req.user = user; req.user = user;
next(); next();
} catch (err) { } 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(); const prisma = new PrismaClient();
@@ -1,18 +1,18 @@
import express from "express"; import express from 'express';
import { import {
createAcademicsResearch, createAcademicsResearch,
getAcademicsResearch, getAcademicsResearch,
getSingleAcademicsResearch, getSingleAcademicsResearch,
deleteAcademicsResearch, 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(); const router = express.Router();
router.post("/", createAcademicsResearch); router.post('/', createAcademicsResearch);
router.get("/getAll", getAcademicsResearch); router.get('/getAll', jwtAuthMiddleware, getAcademicsResearch);
router.get("/:id", getSingleAcademicsResearch); router.get('/:id', jwtAuthMiddleware, getSingleAcademicsResearch);
router.delete("/:id", jwtAuthMiddleware, deleteAcademicsResearch); router.delete('/:id', jwtAuthMiddleware, deleteAcademicsResearch);
export default router; export default router;
+8 -8
View File
@@ -1,23 +1,23 @@
import express from "express"; import express from 'express';
import { import {
createAppointment, createAppointment,
getAppointments, getAppointments,
getAppointment, getAppointment,
updateAppointment, updateAppointment,
deleteAppointment, 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(); const router = express.Router();
/* PUBLIC */ /* PUBLIC */
router.get("/getall", getAppointments); router.get('/getall', jwtAuthMiddleware, getAppointments);
router.post("/", createAppointment); router.post('/', createAppointment);
router.get("/:id", getAppointment); router.get('/:id', jwtAuthMiddleware, getAppointment);
router.patch("/:id", updateAppointment); router.patch('/:id', jwtAuthMiddleware, updateAppointment);
router.delete("/:id", jwtAuthMiddleware, deleteAppointment); router.delete('/:id', jwtAuthMiddleware, deleteAppointment);
export default router; export default router;
+3 -4
View File
@@ -1,9 +1,8 @@
import express from "express"; import express from 'express';
import {register, login} from "../controllers/auth.controller.js"; import { login } from '../controllers/auth.controller.js';
const router = express.Router(); const router = express.Router();
router.post("/register", register); router.post('/login', login);
router.post("/login", login);
export default router; export default router;
+13 -9
View File
@@ -1,4 +1,4 @@
import express from "express"; import express from 'express';
import { import {
createBlog, createBlog,
getBlogs, getBlogs,
@@ -6,22 +6,26 @@ import {
updateBlog, updateBlog,
deleteBlog, deleteBlog,
getAllBlogs, getAllBlogs,
} from "../controllers/blog.controller.js"; getBlogForAdmin,
} from '../controllers/blog.controller.js';
import jwtAuthMiddleware from "../middleware/auth.js"; import jwtAuthMiddleware from '../middleware/auth.js';
const router = express.Router(); const router = express.Router();
/* PUBLIC */ /* PUBLIC */
router.get("/", getBlogs); router.get('/', getBlogs);
router.get("/:id", getBlog); router.get('/:slug', getBlog);
// Protected // Protected
router.get("/admin/all", jwtAuthMiddleware, getAllBlogs); router.get('/admin/all', jwtAuthMiddleware, getAllBlogs);
router.post("/", jwtAuthMiddleware, createBlog);
router.put("/:id", jwtAuthMiddleware, updateBlog); router.get('/admin/:id', jwtAuthMiddleware, getBlogForAdmin);
router.delete("/:id", jwtAuthMiddleware, deleteBlog);
router.post('/', jwtAuthMiddleware, createBlog);
router.put('/:id', jwtAuthMiddleware, updateBlog);
router.delete('/:id', jwtAuthMiddleware, deleteBlog);
export default router; export default router;
+9 -9
View File
@@ -1,4 +1,4 @@
import express from "express"; import express from 'express';
import { import {
createCandidate, createCandidate,
getCandidates, getCandidates,
@@ -6,20 +6,20 @@ import {
getCandidatesByCareer, getCandidatesByCareer,
updateCandidate, updateCandidate,
deleteCandidate, 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(); const router = express.Router();
/* PUBLIC */ /* PUBLIC */
router.post('/', createCandidate);
router.get("/getAll", getCandidates); router.get('/getAll', jwtAuthMiddleware, getCandidates);
router.get("/:id", getCandidate); router.get('/:id', jwtAuthMiddleware, getCandidate);
router.get("/career/:careerId", getCandidatesByCareer); router.get('/career/:careerId', jwtAuthMiddleware, getCandidatesByCareer);
router.post("/", createCandidate); router.patch('/:id', jwtAuthMiddleware, updateCandidate);
router.patch("/:id", updateCandidate); router.delete('/:id', jwtAuthMiddleware, deleteCandidate);
router.delete("/:id", jwtAuthMiddleware, deleteCandidate);
export default router; export default router;
+8 -11
View File
@@ -1,17 +1,14 @@
import express from "express"; import express from 'express';
import { import { getAllCareers, createCareer, updateCareer, deleteCareer } from '../controllers/career.controller.js';
getAllCareers,
createCareer, import jwtAuthMiddleware from '../middleware/auth.js';
updateCareer,
deleteCareer,
} from "../controllers/career.controller.js";
const router = express.Router(); const router = express.Router();
router.get("/getAll", getAllCareers); router.get('/getAll', getAllCareers);
router.post("/", createCareer); router.post('/', jwtAuthMiddleware, createCareer);
router.patch("/:id", updateCareer); router.patch('/:id', jwtAuthMiddleware, updateCareer);
router.delete("/:id", deleteCareer); router.delete('/:id', jwtAuthMiddleware, deleteCareer);
export default router; export default router;
+9 -7
View File
@@ -1,20 +1,22 @@
import express from "express"; import express from 'express';
import { import {
getAllDepartments, getAllDepartments,
getDepartmentByName,
createDepartment, createDepartment,
updateDepartment, updateDepartment,
deleteDepartment, deleteDepartment,
} from "../controllers/department.controller.js"; } from '../controllers/department.controller.js';
import jwtAuthMiddleware from "../middleware/auth.js"; import jwtAuthMiddleware from '../middleware/auth.js';
const router = express.Router(); const router = express.Router();
// Public // Public
router.get("/getAll", getAllDepartments); router.get('/getAll', getAllDepartments);
router.get('/search', getDepartmentByName);
// Protected // Protected
router.post("/", jwtAuthMiddleware, createDepartment); router.post('/', jwtAuthMiddleware, createDepartment);
router.put("/:departmentId", jwtAuthMiddleware, updateDepartment); router.put('/:departmentId', jwtAuthMiddleware, updateDepartment);
router.delete("/:departmentId", jwtAuthMiddleware, deleteDepartment); router.delete('/:departmentId', jwtAuthMiddleware, deleteDepartment);
export default router; export default router;
+12 -10
View File
@@ -1,4 +1,4 @@
import express from "express"; import express from 'express';
import { import {
getAllDoctors, getAllDoctors,
createDoctor, createDoctor,
@@ -7,19 +7,21 @@ import {
getDoctorTimings, getDoctorTimings,
getDoctorTimingById, getDoctorTimingById,
getDoctorByDoctorId, getDoctorByDoctorId,
} from "../controllers/doctor.controller.js"; getDoctorsByDepartmentId,
} from '../controllers/doctor.controller.js';
import jwtAuthMiddleware from "../middleware/auth.js"; import jwtAuthMiddleware from '../middleware/auth.js';
const router = express.Router(); const router = express.Router();
router.get("/getAll", getAllDoctors); router.get('/getAll', getAllDoctors);
router.get("/:doctorId", getDoctorByDoctorId); router.get('/search', getDoctorsByDepartmentId);
router.get("/getTimings", getDoctorTimings); router.get('/getTimings', getDoctorTimings);
router.get("/getTimings/:doctorId", getDoctorTimingById); router.get('/getTimings/:doctorId', getDoctorTimingById);
router.get('/:doctorId', getDoctorByDoctorId);
router.post("/", jwtAuthMiddleware, createDoctor); router.post('/', jwtAuthMiddleware, createDoctor);
router.patch("/:doctorId", jwtAuthMiddleware, updateDoctor); router.patch('/:doctorId/:action', jwtAuthMiddleware, updateDoctor);
router.delete("/:doctorId", jwtAuthMiddleware, deleteDoctor); router.delete('/:doctorId', jwtAuthMiddleware, deleteDoctor);
export default router; export default router;
+7 -7
View File
@@ -1,19 +1,19 @@
import express from "express"; import express from 'express';
import { import {
getEmailConfigs, getEmailConfigs,
createEmailConfig, createEmailConfig,
updateEmailConfig, updateEmailConfig,
deleteEmailConfig, 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(); const router = express.Router();
router.get("/getAll", getEmailConfigs); router.get('/getAll', getEmailConfigs);
router.post("/", jwtAuthMiddleware, createEmailConfig); router.post('/', jwtAuthMiddleware, createEmailConfig);
router.patch("/:id", jwtAuthMiddleware, updateEmailConfig); router.patch('/:id', jwtAuthMiddleware, updateEmailConfig);
router.delete("/:id", jwtAuthMiddleware, deleteEmailConfig); router.delete('/:id', jwtAuthMiddleware, deleteEmailConfig);
export default router; export default router;
+39
View File
@@ -0,0 +1,39 @@
import express from 'express';
import {
// Categories
getAllCategories,
getPackageBySlug,
createCategory,
updateCategory,
deleteCategory,
// Packages
getAllPackages,
createPackage,
updatePackage,
deletePackage,
// 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('/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;
+9
View File
@@ -0,0 +1,9 @@
import express from 'express';
import { bulkImportExcelData } from '../controllers/importController.js';
import jwtAuthMiddleware from '../middleware/auth.js';
const router = express.Router();
router.post('/bulk', jwtAuthMiddleware, bulkImportExcelData);
export default router;
+7 -12
View File
@@ -1,19 +1,14 @@
import express from "express"; import express from 'express';
import { import { createInquiry, getInquiries, getInquiry, deleteInquiry } from '../controllers/inquiry.controller.js';
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(); const router = express.Router();
router.post("/", createInquiry); router.post('/', createInquiry);
router.get("/getAll", getInquiries); router.get('/getAll', jwtAuthMiddleware, getInquiries);
router.get("/:id", getInquiry); router.get('/:id', jwtAuthMiddleware, getInquiry);
router.delete("/:id", jwtAuthMiddleware, deleteInquiry); router.delete('/:id', jwtAuthMiddleware, deleteInquiry);
export default router; export default router;
+8 -14
View File
@@ -1,23 +1,17 @@
import express from "express"; import express from 'express';
import { import { createNews, getAllNews, getNewsById, updateNews, deleteNews } from '../controllers/newsMedia.controller.js';
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(); const router = express.Router();
// PUBLIC ROUTES // PUBLIC ROUTES
router.get("/getAll", getAllNews); router.get('/getAll', getAllNews);
router.get("/:id", getNewsById); router.get('/:id', getNewsById);
// PROTECTED ROUTES // PROTECTED ROUTES
router.post("/", jwtAuthMiddleware, createNews); router.post('/', jwtAuthMiddleware, createNews);
router.patch("/:id", jwtAuthMiddleware, updateNews); router.patch('/:id', jwtAuthMiddleware, updateNews);
router.delete("/:id", jwtAuthMiddleware, deleteNews); router.delete('/:id', jwtAuthMiddleware, deleteNews);
export default router; export default router;
+27 -7
View File
@@ -1,15 +1,35 @@
import express from "express"; import express from 'express';
import {upload} from "../controllers/upload.controller.js"; import * as Bytescale from '@bytescale/sdk';
import multer from 'multer';
const router = express.Router(); const router = express.Router();
router.post("/image", upload.single("image"), (req, res) => { const uploadManager = new Bytescale.UploadManager({
res.json({ apiKey: process.env.BYTESCALE_SECRET_API_KEY,
success: 1, });
file: {
url: `http://localhost:3000/uploads/blog/${req.file.filename}`, 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',
}, },
}); });
res.json({ fileUrl: result.fileUrl });
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Upload failed' });
}
}); });
export default router; export default router;
+45
View File
@@ -0,0 +1,45 @@
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) => { export const getEmailsByType = async (type) => {
try { 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) { } catch (error) {
console.error("Fetch email config error:", error); console.error('Fetch email config error:', error);
return ""; return '';
} }
}; };
+3 -3
View File
@@ -1,10 +1,10 @@
import jwt from "jsonwebtoken"; import jwt from 'jsonwebtoken';
import "dotenv/config"; import 'dotenv/config';
const SECRET = process.env.JWT_SECRET; const SECRET = process.env.JWT_SECRET;
export function generateToken(payload) { export function generateToken(payload) {
return jwt.sign(payload, SECRET, {expiresIn: "24h"}); return jwt.sign(payload, SECRET, { expiresIn: '24h' });
} }
export function verifyToken(token) { 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) { export async function hashPassword(password) {
return bcrypt.hash(password, 10); 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); const client = new postmark.ServerClient(process.env.POSTMARK_API_KEY);
@@ -9,10 +9,10 @@ export const sendEmail = async ({to, subject, html, text}) => {
To: to, To: to,
Subject: subject, Subject: subject,
HtmlBody: html, HtmlBody: html,
TextBody: text || "", TextBody: text || '',
MessageStream: "outbound", MessageStream: 'outbound',
}); });
} catch (error) { } catch (error) {
console.error("Email send error:", error); console.error('Email send error:', error);
} }
}; };
Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

+45
View File
@@ -0,0 +1,45 @@
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
@@ -0,0 +1,41 @@
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
@@ -0,0 +1,19 @@
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
@@ -0,0 +1,24 @@
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
@@ -0,0 +1,12 @@
#!/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,3 +22,8 @@ dist-ssr
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
#env files
.env*
.env.*.local
+36 -62
View File
@@ -1,73 +1,47 @@
# React + TypeScript + Vite **GG-Dashboard**
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. ## Tech Stack
Currently, two official plugins are available: Framework: React 19
Build Tool: Vite + TypeScript
Styling: Tailwind CSS 4 + shadcn/ui
Rich Text: Editor.js
State/Fetch: Axios + React Hooks
Export: XLSX + File-saver
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh ## Project Structure
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## React Compiler frontend/
├── src/
│ ├── api/
│ ├── assets/
│ ├── components/
│ ├── context/
│ ├── lib/
│ ├── layout/
│ ├── pages/
│ ├── services/
│ ├── utils/
│ └── App.tsx
├── .env
├── index.html
└── package.json
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). ## Installation & Setup
## Expanding the ESLint configuration **1. Prerequisites**
Node.js (v20+)
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: **2. Environment Variables**
VITE_API_URL="http://localhost:5008/api"
```js **3. Install Dependencies**
export default defineConfig([ npm install
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this **4. Development**
tseslint.configs.recommendedTypeChecked, npm run dev
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs... ## Scripts
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: npm run dev: Starts the Vite development server with Hot Module Replacement.
npm run build: Compiles TypeScript and builds the production-ready assets.
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
+7 -7
View File
@@ -1,9 +1,9 @@
import js from '@eslint/js' import js from '@eslint/js';
import globals from 'globals' import globals from 'globals';
import reactHooks from 'eslint-plugin-react-hooks' import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh' import reactRefresh from 'eslint-plugin-react-refresh';
import tseslint from 'typescript-eslint' import tseslint from 'typescript-eslint';
import { defineConfig, globalIgnores } from 'eslint/config' import { defineConfig, globalIgnores } from 'eslint/config';
export default defineConfig([ export default defineConfig([
globalIgnores(['dist']), globalIgnores(['dist']),
@@ -20,4 +20,4 @@ export default defineConfig([
globals: globals.browser, globals: globals.browser,
}, },
}, },
]) ]);
+2 -2
View File
@@ -2,9 +2,9 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>frontend</title> <title>GG Admin Dashboard</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+28 -19
View File
@@ -28,6 +28,7 @@
"radix-ui": "^1.4.3", "radix-ui": "^1.4.3",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-hot-toast": "^2.6.0",
"react-router-dom": "^7.13.1", "react-router-dom": "^7.13.1",
"shadcn": "^4.0.5", "shadcn": "^4.0.5",
"tailwind-merge": "^3.5.0", "tailwind-merge": "^3.5.0",
@@ -115,7 +116,6 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.29.0", "@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0", "@babel/generator": "^7.29.0",
@@ -1760,7 +1760,6 @@
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz",
"integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": "^14.21.3 || >=16" "node": "^14.21.3 || >=16"
}, },
@@ -4093,7 +4092,6 @@
"integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~7.16.0" "undici-types": "~7.16.0"
} }
@@ -4104,7 +4102,6 @@
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.2.2" "csstype": "^3.2.2"
} }
@@ -4115,7 +4112,6 @@
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"peerDependencies": { "peerDependencies": {
"@types/react": "^19.2.0" "@types/react": "^19.2.0"
} }
@@ -4177,7 +4173,6 @@
"integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==", "integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/scope-manager": "8.57.0",
"@typescript-eslint/types": "8.57.0", "@typescript-eslint/types": "8.57.0",
@@ -4468,7 +4463,6 @@
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@@ -4728,7 +4722,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.9.0", "baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759", "caniuse-lite": "^1.0.30001759",
@@ -5195,7 +5188,6 @@
"version": "3.2.3", "version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/data-uri-to-buffer": { "node_modules/data-uri-to-buffer": {
@@ -5558,7 +5550,6 @@
"integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
@@ -5811,7 +5802,6 @@
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"accepts": "^2.0.0", "accepts": "^2.0.0",
"body-parser": "^2.2.1", "body-parser": "^2.2.1",
@@ -6362,6 +6352,15 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/gopd": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -6466,7 +6465,6 @@
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.7.tgz", "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.7.tgz",
"integrity": "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw==", "integrity": "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=16.9.0" "node": ">=16.9.0"
} }
@@ -8131,7 +8129,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -8141,7 +8138,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.27.0" "scheduler": "^0.27.0"
}, },
@@ -8149,6 +8145,23 @@
"react": "^19.2.4" "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": { "node_modules/react-refresh": {
"version": "0.18.0", "version": "0.18.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
@@ -8905,8 +8918,7 @@
"version": "4.2.1", "version": "4.2.1",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz",
"integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==", "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/tailwindcss-animate": { "node_modules/tailwindcss-animate": {
"version": "1.0.7", "version": "1.0.7",
@@ -9113,7 +9125,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -9314,7 +9325,6 @@
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.27.0", "esbuild": "^0.27.0",
"fdir": "^6.5.0", "fdir": "^6.5.0",
@@ -9659,7 +9669,6 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }
+1
View File
@@ -30,6 +30,7 @@
"radix-ui": "^1.4.3", "radix-ui": "^1.4.3",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-hot-toast": "^2.6.0",
"react-router-dom": "^7.13.1", "react-router-dom": "^7.13.1",
"shadcn": "^4.0.5", "shadcn": "^4.0.5",
"tailwind-merge": "^3.5.0", "tailwind-merge": "^3.5.0",
+1 -1
View File
@@ -1,5 +1,5 @@
export default { export default {
plugins: { plugins: {
"@tailwindcss/postcss": {}, '@tailwindcss/postcss': {},
}, },
}; };
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

-1
View File
@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 1.5 KiB

+26 -17
View File
@@ -1,29 +1,35 @@
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { Toaster } from 'react-hot-toast';
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 "./components/ProtectedRoutes/ProtectedRoutes";
import ProtectedRoute from "./auth/ProtectedRoute"; import ProtectedRoute from './auth/ProtectedRoute';
import PublicRoute from "./auth/PublicRoute"; import PublicRoute from './auth/PublicRoute';
import { AuthProvider } from "./context/AuthContext"; import { AuthProvider } from './context/AuthContext';
import Department from "./pages/Department"; import Department from './pages/Department';
import Doctor from "./pages/Doctor"; import Doctor from './pages/Doctor';
import Blog from "./pages/Blog"; import Blog from './pages/Blog';
import BlogEditorPage from "./pages/BlogEditor"; import BlogEditorPage from './pages/BlogEditor';
import Appointment from "./pages/Appointment"; import Appointment from './pages/Appointment';
import EmailPage from "./pages/email"; import EmailPage from './pages/email';
import CareerPage from "./pages/Career"; import CareerPage from './pages/Career';
import CandidatePage from "./pages/candidates"; import CandidatePage from './pages/candidates';
import InquiryPage from "./pages/inquiry"; import InquiryPage from './pages/inquiry';
import AcademicsPage from "./pages/Academics"; import AcademicsPage from './pages/Academics';
import NewsPage from "./pages/newsMedia"; import NewsPage from './pages/newsMedia';
import BlogDetail from './pages/BlogDetails';
import ImportData from './pages/ImportData';
import HealthPackagePage from './pages/HealthPackagePage';
export default function App() { export default function App() {
return ( return (
<BrowserRouter> <BrowserRouter>
<Toaster position="top-right" />
<AuthProvider> <AuthProvider>
<Routes> <Routes>
<Route element={<PublicRoute />}> <Route element={<PublicRoute />}>
@@ -35,6 +41,7 @@ export default function App() {
<Route path="/department" element={<Department />} /> <Route path="/department" element={<Department />} />
<Route path="/doctor" element={<Doctor />} /> <Route path="/doctor" element={<Doctor />} />
<Route path="/blog" element={<Blog />} /> <Route path="/blog" element={<Blog />} />
<Route path="/blog/:id" element={<BlogDetail />} />
<Route path="/blog/create" element={<BlogEditorPage />} /> <Route path="/blog/create" element={<BlogEditorPage />} />
<Route path="/blog/edit/:id" element={<BlogEditorPage />} /> <Route path="/blog/edit/:id" element={<BlogEditorPage />} />
<Route path="/appointment" element={<Appointment />} /> <Route path="/appointment" element={<Appointment />} />
@@ -44,6 +51,8 @@ export default function App() {
<Route path="/inquiry" element={<InquiryPage />} /> <Route path="/inquiry" element={<InquiryPage />} />
<Route path="/academics" element={<AcademicsPage />} /> <Route path="/academics" element={<AcademicsPage />} />
<Route path="/news" element={<NewsPage />} /> <Route path="/news" element={<NewsPage />} />
<Route path="/import" element={<ImportData />} />
<Route path="/health-check" element={<HealthPackagePage />} />
</Route> </Route>
</Route> </Route>
+2 -2
View File
@@ -1,7 +1,7 @@
import apiClient from "@/api/client"; import apiClient from '@/api/client';
export const getAcademicsApi = async () => { export const getAcademicsApi = async () => {
const res = await apiClient.get("/academics/getAll"); const res = await apiClient.get('/academics/getAll');
return res.data; return res.data;
}; };
+16 -16
View File
@@ -1,22 +1,22 @@
import apiClient from "@/api/client"; import apiClient from '@/api/client';
export const getAppointmentsApi = async ( export const getAppointmentsApi = async (
page?: number, page = 1,
limit?: number, limit = 10,
search?: string, date = '',
doctor?: string, startDate = '',
department?: string, endDate = '',
date?: string, search = ''
) => { ) => {
let url = "/appointments/getAll"; const params = new URLSearchParams({
page: String(page),
if (page && limit) { limit: String(limit),
url += `?page=${page}&limit=${limit}&search=${search || ""}&doctor=${ ...(date && { date }),
doctor || "" ...(startDate && { startDate }),
}&department=${department || ""}&date=${date || ""}`; ...(endDate && { endDate }),
} ...(search && { search }),
});
const res = await apiClient.get(url); const res = await apiClient.get(`/appointments/getall?${params}`);
return res.data; return res.data;
}; };
+3 -6
View File
@@ -1,10 +1,7 @@
import apiClient from "./client"; import apiClient from './client';
export const loginApi = async ( export const loginApi = async (username: string, password: string): Promise<any> => {
username: string, const response = await apiClient.post('/auth/login/', {
password: string,
): Promise<any> => {
const response = await apiClient.post("/auth/login/", {
username, username,
password, password,
}); });
+7 -7
View File
@@ -1,4 +1,4 @@
import apiClient from "@/api/client"; import apiClient from '@/api/client';
export interface Blog { export interface Blog {
id?: number; id?: number;
@@ -9,17 +9,17 @@ export interface Blog {
} }
export const getAllBlogsApi = async () => { export const getAllBlogsApi = async () => {
const res = await apiClient.get("/blogs"); const res = await apiClient.get('/blogs');
return res.data; return res.data;
}; };
export const getBlogByIdApi = async (id: number) => { export const getBlogByIdApi = async (id: number) => {
const res = await apiClient.get(`/blogs/${id}`); const res = await apiClient.get(`/blogs/admin/${id}`);
return res.data; return res.data;
}; };
export const createBlogApi = async (data: Blog) => { export const createBlogApi = async (data: Blog) => {
const res = await apiClient.post("/blogs", data); const res = await apiClient.post('/blogs', data);
return res.data; return res.data;
}; };
@@ -36,11 +36,11 @@ export const deleteBlogApi = async (id: number) => {
/* IMAGE UPLOAD */ /* IMAGE UPLOAD */
export const uploadImageApi = async (file: File) => { export const uploadImageApi = async (file: File) => {
const formData = new FormData(); const formData = new FormData();
formData.append("image", file); formData.append('image', file);
const res = await apiClient.post("/upload/image", formData, { const res = await apiClient.post('/upload/image', formData, {
headers: { headers: {
"Content-Type": "multipart/form-data", 'Content-Type': 'multipart/form-data',
}, },
}); });
+2 -2
View File
@@ -1,7 +1,7 @@
import apiClient from "@/api/client"; import apiClient from '@/api/client';
export const getCandidatesApi = async () => { export const getCandidatesApi = async () => {
const res = await apiClient.get("/candidates/getAll"); const res = await apiClient.get('/candidates/getAll');
return res.data; return res.data;
}; };
+40 -2
View File
@@ -1,11 +1,49 @@
import apiClient from "@/api/client"; import apiClient from '@/api/client';
import toast from 'react-hot-toast';
export const getCareersApi = async () => { export const getCareersApi = async () => {
const res = await apiClient.get("/careers/getAll"); const res = await apiClient.get('/careers/getAll?admin=true');
return res.data; return res.data;
}; };
export const createCareerApi = async (data: any) => {
try {
const res = await apiClient.post('/careers', data);
toast.success('Career created successfully');
return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to create career');
throw error;
}
};
export const updateCareerApi = async (id: number, data: any) => {
try {
const res = await apiClient.patch(`/careers/${id}`, data);
toast.success('Career updated successfully');
return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to update career');
throw error;
}
};
export const deleteCareerApi = async (id: number) => { export const deleteCareerApi = async (id: number) => {
try {
const res = await apiClient.delete(`/careers/${id}`); const res = await apiClient.delete(`/careers/${id}`);
toast.success('Career deleted successfully');
return res.data; return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to delete career');
throw error;
}
}; };
+14 -14
View File
@@ -1,48 +1,48 @@
import axios from "axios"; import axios from 'axios';
import type {InternalAxiosRequestConfig} from "axios"; import type { InternalAxiosRequestConfig } from 'axios';
const BASE_URL: string = "http://localhost:3000/api"; const baseURL: string = import.meta.env.VITE_API_URL;
const apiClient = axios.create({ const apiClient = axios.create({
baseURL: BASE_URL, baseURL: baseURL,
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
}, },
}); });
export const setAxiosAuthToken = (token: string | null): void => { export const setAxiosAuthToken = (token: string | null): void => {
if (token) { if (token) {
apiClient.defaults.headers.common["Authorization"] = `Bearer ${token}`; apiClient.defaults.headers.common['Authorization'] = `Bearer ${token}`;
} else { } else {
delete apiClient.defaults.headers.common["Authorization"]; delete apiClient.defaults.headers.common['Authorization'];
} }
}; };
apiClient.interceptors.request.use( apiClient.interceptors.request.use(
(config: InternalAxiosRequestConfig) => { (config: InternalAxiosRequestConfig) => {
const token = localStorage.getItem("token"); const token = localStorage.getItem('token');
if (token && config.headers) { if (token && config.headers) {
config.headers["Authorization"] = `Bearer ${token}`; config.headers['Authorization'] = `Bearer ${token}`;
} }
return config; return config;
}, },
(error: any) => Promise.reject(error), (error: any) => Promise.reject(error)
); );
apiClient.interceptors.response.use( apiClient.interceptors.response.use(
(response) => response, (response) => response,
async (error) => { async (error) => {
if (error.response?.status === 401) { if (error.response?.status === 401) {
console.error("Unauthorized - token missing or invalid"); console.error('Unauthorized - token missing or invalid');
localStorage.removeItem("token"); localStorage.removeItem('token');
window.location.href = "/login"; window.location.href = '/login';
} }
return Promise.reject(error); return Promise.reject(error);
}, }
); );
export default apiClient; export default apiClient;
+35 -4
View File
@@ -1,17 +1,21 @@
import apiClient from "@/api/client"; import apiClient from '@/api/client';
import toast from 'react-hot-toast';
export interface Department { export interface Department {
departmentId: string; departmentId: string;
name: string; name: string;
image?: string;
para1: string; para1: string;
para2: string; para2: string;
para3: string; para3: string;
facilities: string; facilities: string;
services: string; services: string;
isActive?: boolean;
sortOrder?: number;
} }
export const getDepartmentsApi = async () => { export const getDepartmentsApi = async () => {
const res = await apiClient.get("/departments/getAll"); const res = await apiClient.get('/departments/getAll?admin=true');
return res.data; return res.data;
}; };
@@ -24,8 +28,17 @@ export const createDepartmentApi = async (data: {
facilities?: string; facilities?: string;
services?: string; services?: string;
}) => { }) => {
const res = await apiClient.post("/departments", data); try {
const res = await apiClient.post('/departments', data);
toast.success('Department created successfully');
return res.data; return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to create department');
throw error;
}
}; };
export const updateDepartmentApi = async ( export const updateDepartmentApi = async (
@@ -37,13 +50,31 @@ export const updateDepartmentApi = async (
para3?: string; para3?: string;
facilities?: string; facilities?: string;
services?: string; services?: string;
}, }
) => { ) => {
try {
const res = await apiClient.put(`/departments/${departmentId}`, data); const res = await apiClient.put(`/departments/${departmentId}`, data);
toast.success('Department updated successfully');
return res.data; return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to update department');
throw error;
}
}; };
export const deleteDepartmentApi = async (departmentId: string) => { export const deleteDepartmentApi = async (departmentId: string) => {
try {
const res = await apiClient.delete(`/departments/${departmentId}`); const res = await apiClient.delete(`/departments/${departmentId}`);
toast.success('Department deleted successfully');
return res.data; return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to delete department');
throw error;
}
}; };
+37 -5
View File
@@ -1,13 +1,17 @@
import apiClient from "@/api/client"; import apiClient from '@/api/client';
import toast from 'react-hot-toast';
export interface Doctor { export interface Doctor {
doctorId: string; doctorId: string;
name: string; name: string;
image?: string;
designation?: string; designation?: string;
workingStatus?: string; workingStatus?: string;
qualification?: string; qualification?: string;
isActive: boolean;
globalSortOrder: number;
departments: { departments?: {
departmentId: string; departmentId: string;
timing?: { timing?: {
monday?: string; monday?: string;
@@ -23,7 +27,7 @@ export interface Doctor {
} }
export const getDoctorsApi = async () => { export const getDoctorsApi = async () => {
const res = await apiClient.get("/doctors/getAll"); const res = await apiClient.get('/doctors/getAll?admin=true');
return res.data; return res.data;
}; };
@@ -33,21 +37,49 @@ export const getDoctorByIdApi = async (doctorId: string) => {
}; };
export const createDoctorApi = async (data: Doctor) => { export const createDoctorApi = async (data: Doctor) => {
const res = await apiClient.post("/doctors", data); try {
const res = await apiClient.post('/doctors', data);
toast.success('Doctor created successfully');
return res.data; return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to create doctor');
throw error;
}
}; };
export const updateDoctorApi = async ( export const updateDoctorApi = async (
doctorId: string, doctorId: string,
data: Partial<Doctor>, data: Partial<Doctor>,
action: 'toggleStatus' | 'updateDetails' = 'updateDetails'
) => { ) => {
const res = await apiClient.patch(`/doctors/${doctorId}`, data); try {
const res = await apiClient.patch(`/doctors/${doctorId}/${action}`, data);
toast.success('Doctor updated successfully');
return res.data; return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to update doctor');
throw error;
}
}; };
export const deleteDoctorApi = async (doctorId: string) => { export const deleteDoctorApi = async (doctorId: string) => {
try {
const res = await apiClient.delete(`/doctors/${doctorId}`); const res = await apiClient.delete(`/doctors/${doctorId}`);
toast.success('Doctor deleted successfully');
return res.data; return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to delete doctor');
throw error;
}
}; };
export const getDoctorTimingApi = async (doctorId: string) => { export const getDoctorTimingApi = async (doctorId: string) => {
+4 -7
View File
@@ -1,4 +1,4 @@
import apiClient from "@/api/client"; import apiClient from '@/api/client';
export interface EmailConfig { export interface EmailConfig {
id?: number; id?: number;
@@ -10,21 +10,18 @@ export interface EmailConfig {
// GET ALL // GET ALL
export const getEmailConfigsApi = async () => { export const getEmailConfigsApi = async () => {
const res = await apiClient.get("/email/getAll"); const res = await apiClient.get('/email/getAll');
return res.data; return res.data;
}; };
// CREATE // CREATE
export const createEmailConfigApi = async (data: EmailConfig) => { export const createEmailConfigApi = async (data: EmailConfig) => {
const res = await apiClient.post("/email", data); const res = await apiClient.post('/email', data);
return res.data; return res.data;
}; };
// UPDATE // UPDATE
export const updateEmailConfigApi = async ( export const updateEmailConfigApi = async (id: number, data: Partial<EmailConfig>) => {
id: number,
data: Partial<EmailConfig>,
) => {
const res = await apiClient.patch(`/email/${id}`, data); const res = await apiClient.patch(`/email/${id}`, data);
return res.data; return res.data;
}; };
+155
View File
@@ -0,0 +1,155 @@
import apiClient from '@/api/client';
import toast from 'react-hot-toast';
export interface SeoData {
seoTitle?: string;
metaDescription?: string;
focusKeyphrase?: string;
tags?: string[];
ogTitle?: string;
ogDescription?: string;
ogImage?: string;
}
export interface HealthPackage {
id?: number;
name: string;
slug: string;
description?: string;
price?: number;
image?: string;
discountedPrice?: number;
inclusions: Record<string, string[]>;
categoryId: number;
isActive: boolean;
isFeatured: boolean;
sortOrder: number;
seo?: SeoData | null;
category?: {
name: string;
};
}
export interface HealthCategory {
id: number;
name: string;
slug: string;
sortOrder: number;
isActive: boolean;
}
export interface HealthInquiry {
id: number;
fullName: string;
mobileNumber: string;
email?: string;
age: string;
gender: string;
preferredDate: string;
message?: string;
createdAt: string;
healthPackage?: {
name: string;
category?: {
name: string;
};
};
}
export const getHealthCategoriesApi = async () => {
const res = await apiClient.get('/health-check/categories?admin=true');
return res.data;
};
export const getHealthPackagesApi = async () => {
const res = await apiClient.get('/health-check/packages?admin=true');
return res.data;
};
export const createHealthPackageApi = async (data: Partial<HealthPackage>) => {
try {
const res = await apiClient.post('/health-check', data);
toast.success('Package created successfully');
return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to create package');
throw error;
}
};
export const updateHealthPackageApi = async (id: number, data: Partial<HealthPackage>) => {
try {
const res = await apiClient.patch(`/health-check/${id}`, data);
toast.success('Package updated successfully');
return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to update package');
throw error;
}
};
export const deleteHealthPackageApi = async (id: number) => {
try {
const res = await apiClient.delete(`/health-check/${id}`);
toast.success('Package deleted successfully');
return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to delete package');
throw error;
}
};
export const createCategoryApi = async (data: { name: string; slug: string; sortOrder: number }) => {
try {
const res = await apiClient.post('/health-check/categories', data);
toast.success('Category created successfully');
return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to create category');
throw error;
}
};
export const updateCategoryApi = async (id: number, data: any) => {
try {
const res = await apiClient.patch(`/health-check/categories/${id}`, data);
toast.success('Category updated successfully');
return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to update category');
throw error;
}
};
export const deleteCategoryApi = async (id: number) => {
try {
const res = await apiClient.delete(`/health-check/categories/${id}`);
toast.success('Category deleted successfully');
return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to delete category');
throw error;
}
};
export const getAllInquiriesApi = async (page = 1, limit = 10, filterDate = '', startDate = '', endDate = '') => {
const params = new URLSearchParams({
page: page.toString(),
limit: limit.toString(),
});
if (filterDate) params.append('filterDate', filterDate);
if (startDate) params.append('startDate', startDate);
if (endDate) params.append('endDate', endDate);
const res = await apiClient.get(`/health-check/inquiries?${params.toString()}`);
return res.data;
};

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