Compare commits

..

5 Commits

Author SHA1 Message Date
kailasdevdas daf0178330 Merge pull request '[1.0.4]' (#29) from dev into main
Reviewed-on: #29
2026-05-14 03:53:43 +00:00
kailasdevdas 7e39683aa2 Merge pull request '[1.0.3]' (#25) from dev into main
Reviewed-on: #25
2026-05-12 05:38:48 +00:00
ashir f60dbe14e6 Merge pull request '1.0.2' (#22) from dev into main
Reviewed-on: #22
2026-05-05 12:29:11 +00:00
ashir 71da0243be Merge pull request '[1.0.1]' (#20) from dev into main
Reviewed-on: #20
2026-04-30 19:20:58 +00:00
ashir 86d41f6c2f Merge pull request '[1.0.0]' (#19) from dev into main
Reviewed-on: #19
2026-04-30 18:37:18 +00:00
136 changed files with 13310 additions and 16538 deletions
-1
View File
@@ -1,2 +1 @@
.env .env
node_modules
-14
View File
@@ -1,14 +0,0 @@
node_modules
dist
build
coverage
.next
out
*.log
backend/node_modules
backend/dist
frontend/node_modules
frontend/build
frontend/dist
-13
View File
@@ -1,13 +0,0 @@
{
"printWidth": 120,
"useTabs": true,
"tabWidth": 2,
"trailingComma": "es5",
"semi": true,
"singleQuote": true,
"bracketSpacing": true,
"arrowParens": "always",
"jsxSingleQuote": false,
"bracketSameLine": false,
"endOfLine": "lf"
}
-4
View File
@@ -1,4 +0,0 @@
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
}
-3
View File
@@ -35,7 +35,6 @@ npm run create-user <username> <password> [role] # role defaults to "admin"
``` ```
In Docker, create an admin via: In Docker, create an admin via:
```bash ```bash
docker exec -it gg-backend-api-backend-1 node src/utils/createUser.js <name> <password> <role> docker exec -it gg-backend-api-backend-1 node src/utils/createUser.js <name> <password> <role>
``` ```
@@ -87,7 +86,6 @@ Note the natural-key relations: `Appointment.doctorId` references `Doctor.doctor
## Environment variables ## Environment variables
`backend/.env`: `backend/.env`:
``` ```
DATABASE_URL=postgresql://user:password@db:5432/mydb DATABASE_URL=postgresql://user:password@db:5432/mydb
PORT=5008 PORT=5008
@@ -99,7 +97,6 @@ EMAIL_FROM=admin@example.com
``` ```
`frontend/.env`: `frontend/.env`:
``` ```
VITE_API_URL=http://localhost:5008/api VITE_API_URL=http://localhost:5008/api
``` ```
+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"],
}, },
}); });
@@ -1,63 +0,0 @@
-- CreateTable
CREATE TABLE "HealthCheckCategory" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"description" TEXT,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"sortOrder" INTEGER NOT NULL DEFAULT 1000,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "HealthCheckCategory_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "HealthPackage" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"description" TEXT,
"price" DECIMAL(10,2),
"discountedPrice" DECIMAL(10,2),
"inclusions" JSONB NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"isFeatured" BOOLEAN NOT NULL DEFAULT false,
"sortOrder" INTEGER NOT NULL DEFAULT 1000,
"categoryId" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "HealthPackage_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "HealthPackageInquiry" (
"id" SERIAL NOT NULL,
"fullName" TEXT NOT NULL,
"mobileNumber" TEXT NOT NULL,
"email" TEXT,
"preferredDate" TIMESTAMP(3),
"message" TEXT,
"packageId" INTEGER NOT NULL,
"status" TEXT NOT NULL DEFAULT 'pending',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "HealthPackageInquiry_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "HealthCheckCategory_name_key" ON "HealthCheckCategory"("name");
-- CreateIndex
CREATE UNIQUE INDEX "HealthCheckCategory_slug_key" ON "HealthCheckCategory"("slug");
-- CreateIndex
CREATE UNIQUE INDEX "HealthPackage_slug_key" ON "HealthPackage"("slug");
-- AddForeignKey
ALTER TABLE "HealthPackage" ADD CONSTRAINT "HealthPackage_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "HealthCheckCategory"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "HealthPackageInquiry" ADD CONSTRAINT "HealthPackageInquiry_packageId_fkey" FOREIGN KEY ("packageId") REFERENCES "HealthPackage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -1,3 +0,0 @@
-- AlterTable
ALTER TABLE "HealthPackageInquiry" ADD COLUMN "age" INTEGER,
ADD COLUMN "gender" TEXT;
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "HealthPackage" ALTER COLUMN "inclusions" SET DEFAULT '{}';
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "HealthCheckCategory" ALTER COLUMN "slug" DROP NOT NULL;
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "HealthPackage" ADD COLUMN "image" TEXT;
@@ -1,50 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[seoId]` on the table `Doctor` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable
ALTER TABLE "Doctor" ADD COLUMN "professionalSummary" TEXT,
ADD COLUMN "seoId" INTEGER;
-- CreateTable
CREATE TABLE "DoctorSpecialization" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"doctorId" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "DoctorSpecialization_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Seo" (
"id" SERIAL NOT NULL,
"seoTitle" TEXT,
"metaDescription" TEXT,
"focusKeyphrase" TEXT,
"slug" TEXT,
"tags" TEXT[],
"ogTitle" TEXT,
"ogDescription" TEXT,
"ogImage" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Seo_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Seo_slug_key" ON "Seo"("slug");
-- CreateIndex
CREATE UNIQUE INDEX "Doctor_seoId_key" ON "Doctor"("seoId");
-- AddForeignKey
ALTER TABLE "Doctor" ADD CONSTRAINT "Doctor_seoId_fkey" FOREIGN KEY ("seoId") REFERENCES "Seo"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "DoctorSpecialization" ADD CONSTRAINT "DoctorSpecialization_doctorId_fkey" FOREIGN KEY ("doctorId") REFERENCES "Doctor"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "Doctor" ADD COLUMN "experience" INTEGER;
@@ -1,14 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[seoId]` on the table `HealthPackage` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable
ALTER TABLE "HealthPackage" ADD COLUMN "seoId" INTEGER;
-- CreateIndex
CREATE UNIQUE INDEX "HealthPackage_seoId_key" ON "HealthPackage"("seoId");
-- AddForeignKey
ALTER TABLE "HealthPackage" ADD CONSTRAINT "HealthPackage_seoId_fkey" FOREIGN KEY ("seoId") REFERENCES "Seo"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -1,22 +0,0 @@
-- CreateEnum
CREATE TYPE "BannerMediaType" AS ENUM ('IMAGE', 'VIDEO');
-- CreateTable
CREATE TABLE "HomepageBanner" (
"id" SERIAL NOT NULL,
"title" TEXT,
"subtitle" TEXT,
"mediaType" "BannerMediaType" NOT NULL,
"desktopMediaUrl" TEXT NOT NULL,
"mobileMediaUrl" TEXT,
"buttonText" TEXT,
"buttonLink" TEXT,
"openInNewTab" BOOLEAN NOT NULL DEFAULT false,
"textAlignment" TEXT DEFAULT 'left',
"sortOrder" INTEGER NOT NULL DEFAULT 1000,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "HomepageBanner_pkey" PRIMARY KEY ("id")
);
+1 -125
View File
@@ -23,15 +23,11 @@ model Doctor {
name String name String
image String? image String?
designation String? designation String?
experience Int?
workingStatus String? workingStatus String?
qualification String? qualification String?
isActive Boolean @default(true) isActive Boolean @default(true)
globalSortOrder Int @default(1000) 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[]
@@ -224,123 +220,3 @@ model NewsImage {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
} }
model HealthCheckCategory {
id Int @id @default(autoincrement())
name String @unique
slug String? @unique
description String?
isActive Boolean @default(true)
sortOrder Int @default(1000)
packages HealthPackage[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model HealthPackage {
id Int @id @default(autoincrement())
name String
slug String @unique
description String?
price Decimal? @db.Decimal(10, 2)
image String?
discountedPrice Decimal? @db.Decimal(10, 2)
inclusions Json @default("{}")
isActive Boolean @default(true)
isFeatured Boolean @default(false)
sortOrder Int @default(1000)
categoryId Int
category HealthCheckCategory @relation(fields: [categoryId], references: [id])
inquiries HealthPackageInquiry[]
seoId Int? @unique
seo Seo? @relation(fields: [seoId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model HealthPackageInquiry {
id Int @id @default(autoincrement())
fullName String
mobileNumber String
email String?
age Int?
gender String?
preferredDate DateTime?
message String?
packageId Int
healthPackage HealthPackage @relation(fields: [packageId], references: [id])
status String @default("pending")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model DoctorSpecialization {
id Int @id @default(autoincrement())
name String
description String? @db.Text
doctorId Int
doctor Doctor @relation(fields: [doctorId],references: [id],onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Seo {
id Int @id @default(autoincrement())
doctor Doctor?
healthPackage HealthPackage?
seoTitle String?
metaDescription String? @db.Text
focusKeyphrase String?
slug String? @unique
tags String[]
ogTitle String?
ogDescription String? @db.Text
ogImage String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model HomepageBanner {
id Int @id @default(autoincrement())
title String?
subtitle String?
mediaType BannerMediaType
desktopMediaUrl String
mobileMediaUrl String?
buttonText String?
buttonLink String?
openInNewTab Boolean @default(false)
textAlignment String? @default("left")
sortOrder Int @default(1000)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
enum BannerMediaType {
IMAGE
VIDEO
}
+37 -41
View File
@@ -1,64 +1,60 @@
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 importRoutes from "./routes/importRoutes.js";
import healthCheckRoutes from './routes/healthCheck.route.js';
import homepageBannerRoutes from './routes/homepageBanner.routes.js';
dotenv.config(); dotenv.config();
const app = express(); const app = express();
app.use(express.json({ limit: '50mb' })); app.use(express.json({ limit: "50mb" }));
app.use(express.urlencoded({ limit: '50mb', extended: true })); 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(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/import", importRoutes);
app.use('/api/health-check', healthCheckRoutes);
app.use('/api/homepage-banners', homepageBannerRoutes);
const PORT = process.env.PORT || 5008; const PORT = process.env.PORT || 5008;
app.listen(PORT, () => { app.listen(PORT, () => {
@@ -1,17 +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 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 } = req.body; const { fullName, number, emailId, subject, courseName, message } =
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",
}); });
} }
@@ -27,12 +28,12 @@ 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: `
<div style="font-family: Arial, sans-serif; background-color: #f4f6f8; padding: 20px;"> <div style="font-family: Arial, sans-serif; background-color: #f4f6f8; padding: 20px;">
@@ -62,15 +63,15 @@ export const createAcademicsResearch = async (req, res) => {
</tr> </tr>
<tr> <tr>
<td style="padding: 8px 0;"><b>Email:</b></td> <td style="padding: 8px 0;"><b>Email:</b></td>
<td style="padding: 8px 0;">${emailId || '-'}</td> <td style="padding: 8px 0;">${emailId || "-"}</td>
</tr> </tr>
<tr> <tr>
<td style="padding: 8px 0;"><b>Course:</b></td> <td style="padding: 8px 0;"><b>Course:</b></td>
<td style="padding: 8px 0;">${courseName || '-'}</td> <td style="padding: 8px 0;">${courseName || "-"}</td>
</tr> </tr>
<tr> <tr>
<td style="padding: 8px 0;"><b>Subject:</b></td> <td style="padding: 8px 0;"><b>Subject:</b></td>
<td style="padding: 8px 0;">${subject || '-'}</td> <td style="padding: 8px 0;">${subject || "-"}</td>
</tr> </tr>
</table> </table>
@@ -86,7 +87,7 @@ export const createAcademicsResearch = async (req, res) => {
word-break: break-word; word-break: break-word;
overflow-wrap: anywhere; overflow-wrap: anywhere;
"> ">
${message ? message.replace(/\n/g, '<br/>') : '-'} ${message ? message.replace(/\n/g, "<br/>") : "-"}
</div> </div>
</div> </div>
@@ -104,20 +105,20 @@ export const createAcademicsResearch = async (req, res) => {
}); });
} }
} 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",
}); });
} }
}; };
@@ -128,7 +129,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",
}, },
}); });
@@ -139,7 +140,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",
}); });
} }
}; };
@@ -159,7 +160,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",
}); });
} }
@@ -170,7 +171,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",
}); });
} }
}; };
@@ -189,12 +190,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",
}); });
} }
}; };
@@ -1,15 +1,16 @@
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 } = req.body; const { name, mobileNumber, email, message, date, doctorId, departmentId } =
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",
}); });
} }
@@ -30,12 +31,12 @@ 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: `
<div style="font-family: Arial, sans-serif; background-color: #f4f6f8; padding: 20px;"> <div style="font-family: Arial, sans-serif; background-color: #f4f6f8; padding: 20px;">
@@ -65,7 +66,7 @@ export const createAppointment = async (req, res) => {
</tr> </tr>
<tr> <tr>
<td style="padding: 8px 0;"><b>Email:</b></td> <td style="padding: 8px 0;"><b>Email:</b></td>
<td style="padding: 8px 0;">${email || '-'}</td> <td style="padding: 8px 0;">${email || "-"}</td>
</tr> </tr>
</table> </table>
@@ -74,19 +75,19 @@ export const createAppointment = async (req, res) => {
<table style="width: 100%; border-collapse: collapse;"> <table style="width: 100%; border-collapse: collapse;">
<tr> <tr>
<td style="padding: 8px 0;"><b>Doctor:</b></td> <td style="padding: 8px 0;"><b>Doctor:</b></td>
<td style="padding: 8px 0;">${appointment.doctor?.name || '-'}</td> <td style="padding: 8px 0;">${appointment.doctor?.name || "-"}</td>
</tr> </tr>
<tr> <tr>
<td style="padding: 8px 0;"><b>Department:</b></td> <td style="padding: 8px 0;"><b>Department:</b></td>
<td style="padding: 8px 0;">${appointment.department?.name || '-'}</td> <td style="padding: 8px 0;">${appointment.department?.name || "-"}</td>
</tr> </tr>
<tr> <tr>
<td style="padding: 8px 0;"><b>Date:</b></td> <td style="padding: 8px 0;"><b>Date:</b></td>
<td style="padding: 8px 0;"> <td style="padding: 8px 0;">
${new Date(date).toLocaleDateString('en-GB', { ${new Date(date).toLocaleDateString("en-GB", {
day: '2-digit', day: "2-digit",
month: 'long', month: "long",
year: 'numeric', year: "numeric",
})} })}
</td> </td>
</tr> </tr>
@@ -104,7 +105,7 @@ export const createAppointment = async (req, res) => {
word-break: break-word; word-break: break-word;
overflow-wrap: anywhere; overflow-wrap: anywhere;
"> ">
${message ? message.replace(/\n/g, '<br/>') : '-'} ${message ? message.replace(/\n/g, "<br/>") : "-"}
</div> </div>
</div> </div>
@@ -122,19 +123,19 @@ export const createAppointment = async (req, res) => {
}); });
} }
} 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",
}); });
} }
}; };
@@ -151,9 +152,11 @@ export const getAppointments = async (req, res) => {
const where = {}; const where = {};
const hasSingleDate = date && date.trim() !== ''; const hasSingleDate = date && date.trim() !== "";
const hasRange = (startDate && startDate.trim() !== '') || (endDate && endDate.trim() !== ''); const hasRange =
(startDate && startDate.trim() !== "") ||
(endDate && endDate.trim() !== "");
if (hasSingleDate) { if (hasSingleDate) {
const start = new Date(date); const start = new Date(date);
@@ -171,14 +174,14 @@ export const getAppointments = async (req, res) => {
if (!hasSingleDate && hasRange) { if (!hasSingleDate && hasRange) {
const dateFilter = {}; const dateFilter = {};
if (startDate && startDate.trim() !== '') { if (startDate && startDate.trim() !== "") {
const start = new Date(startDate); const start = new Date(startDate);
start.setHours(0, 0, 0, 0); start.setHours(0, 0, 0, 0);
dateFilter.gte = start; dateFilter.gte = start;
} }
if (endDate && endDate.trim() !== '') { if (endDate && endDate.trim() !== "") {
const end = new Date(endDate); const end = new Date(endDate);
end.setHours(23, 59, 59, 999); end.setHours(23, 59, 59, 999);
@@ -188,11 +191,11 @@ export const getAppointments = async (req, res) => {
where.date = dateFilter; where.date = dateFilter;
} }
if (search && search.trim() !== '') { if (search && search.trim() !== "") {
where.OR = [ where.OR = [
{ name: { contains: search, mode: 'insensitive' } }, { name: { contains: search, mode: "insensitive" } },
{ mobileNumber: { contains: search } }, { mobileNumber: { contains: search } },
{ email: { contains: search, mode: 'insensitive' } }, { email: { contains: search, mode: "insensitive" } },
]; ];
} }
@@ -204,7 +207,7 @@ export const getAppointments = async (req, res) => {
department: true, department: true,
}, },
orderBy: { orderBy: {
createdAt: 'desc', createdAt: "desc",
}, },
skip, skip,
take: limit, take: limit,
@@ -230,7 +233,7 @@ export const getAppointments = async (req, res) => {
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: 'Failed to fetch appointments', message: "Failed to fetch appointments",
}); });
} }
}; };
@@ -254,7 +257,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",
}); });
} }
@@ -266,7 +269,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",
}); });
} }
}; };
@@ -286,7 +289,7 @@ export const getAppointmentsByDoctor = async (req, res) => {
department: true, department: true,
}, },
orderBy: { orderBy: {
date: 'asc', date: "asc",
}, },
}); });
@@ -298,7 +301,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",
}); });
} }
}; };
@@ -327,7 +330,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",
}); });
} }
}; };
@@ -351,14 +354,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",
}); });
} }
}; };
@@ -377,13 +380,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",
}); });
} }
}; };
+15 -15
View File
@@ -1,24 +1,24 @@
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
* POST /api/auth/register * POST /api/auth/register
*/ */
export async function register(req, res) { 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({
where: { username }, where: {username},
}); });
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,
@@ -46,24 +46,24 @@ export async function register(req, res) {
* POST /api/auth/login * POST /api/auth/login
*/ */
export async function login(req, res) { 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({
where: { username }, where: {username},
}); });
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({
@@ -72,5 +72,5 @@ export async function login(req, res) {
role: user.role, role: user.role,
}); });
res.json({ token }); res.json({token});
} }
+21 -21
View File
@@ -1,10 +1,10 @@
import prisma from '../prisma/client.js'; import prisma from "../prisma/client.js";
import slugify from 'slugify'; import slugify from "slugify";
/* CREATE BLOG */ /* CREATE BLOG */
export async function createBlog(req, res) { export async function createBlog(req, res) {
const { title, writer, image, content, isActive } = req.body; const {title, writer, image, content, isActive} = req.body;
try { try {
const blog = await prisma.blog.create({ const blog = await prisma.blog.create({
@@ -20,7 +20,7 @@ export async function createBlog(req, res) {
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"});
} }
} }
@@ -29,13 +29,13 @@ export async function createBlog(req, res) {
export async function getBlogs(req, res) { 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);
} catch (error) { } catch (error) {
res.status(500).json({ error: error.message }); res.status(500).json({error: error.message});
} }
} }
@@ -44,12 +44,12 @@ 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);
} catch (error) { } catch (error) {
res.status(500).json({ error: error.message }); res.status(500).json({error: error.message});
} }
} }
@@ -60,16 +60,16 @@ export async function getBlog(req, res) {
const slug = req.params.slug; const slug = req.params.slug;
const blog = await prisma.blog.findUnique({ const blog = await prisma.blog.findUnique({
where: { slug }, where: {slug},
}); });
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);
} catch (error) { } catch (error) {
res.status(500).json({ error: error.message }); res.status(500).json({error: error.message});
} }
} }
@@ -80,16 +80,16 @@ export async function getBlogForAdmin(req, res) {
const id = Number(req.params.id); const id = Number(req.params.id);
const blog = await prisma.blog.findUnique({ const blog = await prisma.blog.findUnique({
where: { id }, where: {id},
}); });
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);
} catch (error) { } catch (error) {
res.status(500).json({ error: error.message }); res.status(500).json({error: error.message});
} }
} }
@@ -97,10 +97,10 @@ export async function getBlogForAdmin(req, res) {
export async function updateBlog(req, res) { export async function updateBlog(req, res) {
try { try {
const { title, writer, image, content } = req.body; const {title, writer, image, content} = req.body;
const blog = await prisma.blog.update({ const blog = await prisma.blog.update({
where: { id: Number(req.params.id) }, where: {id: Number(req.params.id)},
data: { data: {
title, title,
writer, writer,
@@ -111,7 +111,7 @@ export async function updateBlog(req, res) {
res.json(blog); res.json(blog);
} catch (error) { } catch (error) {
res.status(500).json({ error: error.message }); res.status(500).json({error: error.message});
} }
} }
@@ -122,11 +122,11 @@ export async function deleteBlog(req, res) {
const id = Number(req.params.id); const id = Number(req.params.id);
await prisma.blog.delete({ await prisma.blog.delete({
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});
} }
} }
+25 -24
View File
@@ -1,18 +1,19 @@
import prisma from '../prisma/client.js'; import prisma from "../prisma/client.js";
import { sendEmail } from '../utils/sendEmail.js'; import { 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 } = req.body; const { fullName, mobile, email, subject, coverLetter, careerId } =
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",
}); });
} }
@@ -31,12 +32,12 @@ 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: `
<div style="font-family: Arial, sans-serif; background-color: #f4f6f8; padding: 20px;"> <div style="font-family: Arial, sans-serif; background-color: #f4f6f8; padding: 20px;">
@@ -75,15 +76,15 @@ export const createCandidate = async (req, res) => {
<table style="width: 100%; border-collapse: collapse;"> <table style="width: 100%; border-collapse: collapse;">
<tr> <tr>
<td style="padding: 8px 0;"><b>Applied For:</b></td> <td style="padding: 8px 0;"><b>Applied For:</b></td>
<td style="padding: 8px 0;">${candidate.career?.post || '-'}</td> <td style="padding: 8px 0;">${candidate.career?.post || "-"}</td>
</tr> </tr>
<tr> <tr>
<td style="padding: 8px 0;"><b>Designation:</b></td> <td style="padding: 8px 0;"><b>Designation:</b></td>
<td style="padding: 8px 0;">${candidate.career?.designation || '-'}</td> <td style="padding: 8px 0;">${candidate.career?.designation || "-"}</td>
</tr> </tr>
<tr> <tr>
<td style="padding: 8px 0;"><b>Subject:</b></td> <td style="padding: 8px 0;"><b>Subject:</b></td>
<td style="padding: 8px 0;">${subject || '-'}</td> <td style="padding: 8px 0;">${subject || "-"}</td>
</tr> </tr>
</table> </table>
@@ -99,7 +100,7 @@ export const createCandidate = async (req, res) => {
word-break: break-word; word-break: break-word;
overflow-wrap: anywhere; overflow-wrap: anywhere;
"> ">
${coverLetter ? coverLetter.replace(/\n/g, '<br/>') : '-'} ${coverLetter ? coverLetter.replace(/\n/g, "<br/>") : "-"}
</div> </div>
</div> </div>
@@ -117,19 +118,19 @@ export const createCandidate = async (req, res) => {
}); });
} }
} 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",
}); });
} }
}; };
@@ -143,7 +144,7 @@ export const getCandidates = async (req, res) => {
career: true, career: true,
}, },
orderBy: { orderBy: {
createdAt: 'desc', createdAt: "desc",
}, },
}); });
@@ -155,7 +156,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",
}); });
} }
}; };
@@ -178,7 +179,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",
}); });
} }
@@ -190,7 +191,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",
}); });
} }
}; };
@@ -209,7 +210,7 @@ export const getCandidatesByCareer = async (req, res) => {
career: true, career: true,
}, },
orderBy: { orderBy: {
createdAt: 'desc', createdAt: "desc",
}, },
}); });
@@ -221,7 +222,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",
}); });
} }
}; };
@@ -241,14 +242,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",
}); });
} }
}; };
@@ -267,13 +268,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",
}); });
} }
}; };
+22 -12
View File
@@ -1,4 +1,4 @@
import prisma from '../prisma/client.js'; import prisma from "../prisma/client.js";
// GET ALL CAREERS // GET ALL CAREERS
@@ -7,8 +7,8 @@ export const getAllCareers = async (req, res) => {
const { admin } = req.query; const { admin } = req.query;
const careers = await prisma.career.findMany({ const careers = await prisma.career.findMany({
where: admin === 'true' ? {} : { isActive: true }, where: admin === "true" ? {} : { isActive: true },
orderBy: [{ sortOrder: 'asc' }, { createdAt: 'desc' }], orderBy: [{ sortOrder: "asc" }, { createdAt: "desc" }],
}); });
const response = careers.map((c) => ({ const response = careers.map((c) => ({
@@ -32,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",
}); });
} }
}; };
@@ -41,12 +41,22 @@ export const getAllCareers = async (req, res) => {
export const createCareer = async (req, res) => { export const createCareer = async (req, res) => {
try { try {
const { post, designation, qualification, experienceNeed, email, number, status, isActive, sortOrder } = req.body; const {
post,
designation,
qualification,
experienceNeed,
email,
number,
status,
isActive,
sortOrder,
} = 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",
}); });
} }
@@ -66,14 +76,14 @@ export const createCareer = async (req, res) => {
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",
}); });
} }
}; };
@@ -96,14 +106,14 @@ export const updateCareer = async (req, res) => {
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",
}); });
} }
}; };
@@ -120,13 +130,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,23 +1,23 @@
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 {admin} = req.query;
const departments = await prisma.department.findMany({ const departments = await prisma.department.findMany({
where: admin === 'true' ? {} : { isActive: true }, where: admin === "true" ? {} : {isActive: true},
orderBy: [{ sortOrder: 'asc' }, { name: 'asc' }], 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,
image: dep.image ?? '', image: dep.image ?? "",
para1: dep.para1 ?? '', para1: dep.para1 ?? "",
para2: dep.para2 ?? '', para2: dep.para2 ?? "",
para3: dep.para3 ?? '', para3: dep.para3 ?? "",
facilities: dep.facilities ?? '', facilities: dep.facilities ?? "",
services: dep.services ?? '', services: dep.services ?? "",
isActive: dep.isActive, isActive: dep.isActive,
sortOrder: dep.sortOrder, sortOrder: dep.sortOrder,
})); }));
@@ -30,19 +30,19 @@ 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) => { export const getDepartmentByName = async (req, res) => {
try { try {
const { name } = req.query; const {name} = req.query;
if (!name) { if (!name) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: 'Department name is required', message: "Department name is required",
}); });
} }
@@ -56,19 +56,19 @@ export const getDepartmentByName = async (req, res) => {
if (!department) { if (!department) {
return res.status(404).json({ return res.status(404).json({
success: false, success: false,
message: 'Department not found or inactive', message: "Department not found or inactive",
}); });
} }
const response = { const response = {
departmentId: department.departmentId, departmentId: department.departmentId,
name: department.name, name: department.name,
image: department.image ?? '', image: department.image ?? "",
para1: department.para1 ?? '', para1: department.para1 ?? "",
para2: department.para2 ?? '', para2: department.para2 ?? "",
para3: department.para3 ?? '', para3: department.para3 ?? "",
facilities: department.facilities ?? '', facilities: department.facilities ?? "",
services: department.services ?? '', services: department.services ?? "",
isActive: department.isActive, isActive: department.isActive,
sortOrder: department.sortOrder, sortOrder: department.sortOrder,
}; };
@@ -81,17 +81,30 @@ export const getDepartmentByName = 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 department', message: "Failed to fetch department",
}); });
} }
}; };
export async function createDepartment(req, res) { export async function createDepartment(req, res) {
try { try {
const { departmentId, name, image, para1, para2, para3, facilities, services, isActive, sortOrder } = req.body; const {
departmentId,
name,
image,
para1,
para2,
para3,
facilities,
services,
isActive,
sortOrder,
} = req.body;
if (!departmentId || !name) { if (!departmentId || !name) {
return res.status(400).json({ error: 'departmentId and name are required' }); return res
.status(400)
.json({error: "departmentId and name are required"});
} }
const department = await prisma.department.create({ const department = await prisma.department.create({
@@ -110,63 +123,63 @@ export async function createDepartment(req, res) {
}); });
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); 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 updateData = {...req.body};
if (updateData.sortOrder !== undefined) { if (updateData.sortOrder !== undefined) {
updateData.sortOrder = Number(updateData.sortOrder); updateData.sortOrder = Number(updateData.sortOrder);
} }
const department = await prisma.department.update({ const department = await prisma.department.update({
where: { departmentId }, where: {departmentId},
data: updateData, data: updateData,
}); });
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",
}); });
} }
}; };
export const deleteDepartment = async (req, res) => { export const deleteDepartment = async (req, res) => {
try { try {
const { departmentId } = req.params; const {departmentId} = req.params;
await prisma.department.delete({ await prisma.department.delete({
where: { departmentId }, where: {departmentId},
}); });
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",
}); });
} }
}; };
+78 -306
View File
@@ -1,58 +1,34 @@
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 {admin} = req.query;
const doctors = await prisma.doctor.findMany({ const doctors = await prisma.doctor.findMany({
where: admin === 'true' ? {} : { isActive: true }, 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: [{globalSortOrder: "asc"}, {name: "asc"}],
},
orderBy: [{ globalSortOrder: 'asc' }, { name: 'asc' }],
}); });
const formatted = doctors.map((doc, index) => ({ const formatted = doctors.map((doc, index) => ({
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 ?? '', image: doc.image ?? "",
designation: doc.designation, designation: doc.designation,
workingStatus: doc.workingStatus, workingStatus: doc.workingStatus,
qualification: doc.qualification, qualification: doc.qualification,
isActive: doc.isActive, isActive: doc.isActive,
experience: doc.experience,
professionalSummary: doc.professionalSummary,
globalSortOrder: doc.globalSortOrder, 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 = [
@@ -69,7 +45,7 @@ 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, deptSortOrder: d.sortOrder,
}; };
}), }),
@@ -83,7 +59,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",
}); });
} }
}; };
@@ -92,13 +68,11 @@ export const getAllDoctors = async (req, res) => {
export const getDoctorByDoctorId = async (req, res) => { export const getDoctorByDoctorId = async (req, res) => {
try { try {
const { doctorId } = req.params; const {doctorId} = req.params;
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,
@@ -111,35 +85,17 @@ 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 ?? '', 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,
@@ -155,7 +111,7 @@ 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",
}); });
} }
}; };
@@ -163,52 +119,43 @@ export const getDoctorByDoctorId = async (req, res) => {
// get doctors by department // get doctors by department
export const getDoctorsByDepartmentId = async (req, res) => { export const getDoctorsByDepartmentId = async (req, res) => {
try { try {
const { Department_ID } = req.query; const {Department_ID} = req.query;
if (!Department_ID) { if (!Department_ID) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: 'Department_ID is required', message: "Department_ID is required",
}); });
} }
const department = await prisma.department.findUnique({ const department = await prisma.department.findUnique({
where: { departmentId: Department_ID }, where: {departmentId: Department_ID},
}); });
if (!department) { if (!department) {
return res.status(404).json({ return res.status(404).json({
success: false, success: false,
message: 'Department not found', message: "Department not found",
}); });
} }
const doctorsInDept = await prisma.doctorDepartment.findMany({ const doctorsInDept = await prisma.doctorDepartment.findMany({
where: { where: {
departmentId: department.id, departmentId: department.id,
doctor: { isActive: true }, doctor: {isActive: true},
}, },
include: { include: {
doctor: { doctor: true,
include: {
seo: {
select: {
slug: true,
}, },
}, orderBy: {sortOrder: "asc"},
},
},
},
orderBy: { sortOrder: 'asc' },
}); });
const result = doctorsInDept.map((d) => ({ const result = doctorsInDept.map((d) => ({
GG_ID: d.doctor.doctorId, GG_ID: d.doctor.doctorId,
Name: d.doctor.name, Name: d.doctor.name,
image: d.doctor.image ?? '', image: d.doctor.image ?? "",
designation: d.doctor.designation, designation: d.doctor.designation,
hierarchyOrder: d.sortOrder, hierarchyOrder: d.sortOrder,
slug: d.doctor.seo?.slug ?? '',
})); }));
res.status(200).json({ res.status(200).json({
@@ -219,7 +166,7 @@ export const getDoctorsByDepartmentId = 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",
}); });
} }
}; };
@@ -237,48 +184,7 @@ export const createDoctor = async (req, res) => {
isActive, isActive,
globalSortOrder, 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: {
@@ -288,17 +194,15 @@ export const createDoctor = async (req, res) => {
designation, designation,
workingStatus, workingStatus,
qualification, qualification,
experience: experience ? Number(experience) : null,
professionalSummary,
seoId: seo.id,
isActive: isActive !== undefined ? isActive : true, isActive: isActive !== undefined ? isActive : true,
globalSortOrder: globalSortOrder !== undefined ? Number(globalSortOrder) : 0, globalSortOrder:
globalSortOrder !== undefined ? Number(globalSortOrder) : 0,
}, },
}); });
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;
@@ -320,27 +224,16 @@ 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",
}); });
} }
}; };
@@ -348,7 +241,7 @@ 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, action } = req.params; const {doctorId} = req.params;
const { const {
name, name,
designation, designation,
@@ -358,60 +251,16 @@ export const updateDoctor = async (req, res) => {
isActive, isActive,
globalSortOrder, globalSortOrder,
departments, departments,
experience,
professionalSummary,
seoTitle,
metaDescription,
ogTitle,
ogDescription,
focusKeyphrase,
slug,
tags,
ogImage,
specializations,
} = req.body; } = req.body;
if (!doctorId) { const doctor = await prisma.doctor.findUnique({where: {doctorId}});
return res.status(400).json({ if (!doctor)
success: false, return res
message: 'Doctor ID is required', .status(404)
}); .json({success: false, message: "Doctor not found"});
}
const doctor = await prisma.doctor.findUnique({ where: { doctorId } });
if (!doctor) return res.status(404).json({ success: false, message: 'Doctor not found' });
if (action === 'toggleStatus') {
await prisma.doctor.update({
where: { id: doctor.id },
data: {
isActive: !doctor.isActive,
},
});
return res.status(200).json({
success: true,
message: `Doctor has been ${doctor.isActive ? 'deactivated' : 'activated'} successfully`,
});
}
const messages = [];
if (!doctorId) messages.push('Doctor ID is required');
if (!name?.trim()) messages.push('Doctor name is required');
if (!qualification?.trim()) messages.push('Qualification is required');
if (!designation?.trim()) messages.push('Designation is required');
if (!departments || departments.length === 0) {
messages.push('At least one department is required');
}
if (messages.length > 0) {
return res.status(400).json({
success: false,
message: messages.join(', '),
});
}
await prisma.doctor.update({ await prisma.doctor.update({
where: { id: doctor.id }, where: {id: doctor.id},
data: { data: {
name, name,
designation, designation,
@@ -419,183 +268,106 @@ export const updateDoctor = async (req, res) => {
workingStatus, workingStatus,
qualification, qualification,
isActive, isActive,
experience: experience ? Number(experience) : null, globalSortOrder:
professionalSummary, globalSortOrder !== undefined ? Number(globalSortOrder) : undefined,
globalSortOrder: globalSortOrder !== undefined ? Number(globalSortOrder) : undefined,
}, },
}); });
if (doctor.seoId) { const hasTimingData = departments?.some(
await prisma.seo.update({ (dep) => dep.timing && Object.keys(dep.timing).length > 0,
where: { );
id: doctor.seoId,
},
data: {
seoTitle,
metaDescription,
ogTitle,
ogDescription,
ogImage,
focusKeyphrase,
slug: slug ? slug : null,
tags: tags || [],
},
});
} else {
const seo = await prisma.seo.create({
data: {
ogImage,
metaDescription,
seoTitle,
ogDescription,
ogTitle,
focusKeyphrase,
slug: slug ? slug : null,
tags: tags || [],
},
});
await prisma.doctor.update({ if (departments && Array.isArray(departments) && hasTimingData) {
where: {
id: doctor.id,
},
data: {
seoId: seo.id,
},
});
}
// Update Departments & Timings
if (Array.isArray(departments)) {
const oldRelations = await prisma.doctorDepartment.findMany({ const oldRelations = await prisma.doctorDepartment.findMany({
where: { where: {doctorId: doctor.id},
doctorId: doctor.id,
},
include: {
timing: true,
},
}); });
// Delete old timings
for (const rel of oldRelations) { for (const rel of oldRelations) {
if (rel.timing) {
await prisma.doctorTiming.deleteMany({ await prisma.doctorTiming.deleteMany({
where: { where: {doctorDepartmentId: rel.id},
doctorDepartmentId: rel.id,
},
}); });
} }
}
// Delete old departments
await prisma.doctorDepartment.deleteMany({ await prisma.doctorDepartment.deleteMany({
where: { where: {doctorId: doctor.id},
doctorId: doctor.id,
},
}); });
// Recreate departments + timings
for (const dep of departments) { for (const dep of departments) {
const department = await prisma.department.findUnique({ const targetDept = await prisma.department.findUnique({
where: { where: {departmentId: dep.departmentId},
departmentId: dep.departmentId,
},
}); });
if (!targetDept) continue;
if (!department) continue; const newDD = await prisma.doctorDepartment.create({
const doctorDepartment = await prisma.doctorDepartment.create({
data: { data: {
doctorId: doctor.id, doctorId: doctor.id,
departmentId: department.id, departmentId: targetDept.id,
sortOrder: dep.sortOrder !== undefined ? Number(dep.sortOrder) : 0, sortOrder: dep.sortOrder !== undefined ? Number(dep.sortOrder) : 0,
}, },
}); });
if (dep.timing && Object.keys(dep.timing).length > 0) { if (dep.timing) {
const { id, doctorDepartmentId, createdAt, updatedAt, ...cleanTiming } = dep.timing; const {id, doctorDepartmentId, createdAt, updatedAt, ...cleanTiming} =
dep.timing;
await prisma.doctorTiming.create({ await prisma.doctorTiming.create({
data: { data: {doctorDepartmentId: newDD.id, ...cleanTiming},
doctorDepartmentId: doctorDepartment.id,
...cleanTiming,
},
}); });
} }
} }
} }
// Update Specializations res
if (Array.isArray(specializations)) { .status(200)
await prisma.doctorSpecialization.deleteMany({ .json({success: true, message: "Doctor updated successfully"});
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('Update Error:', error); console.error("Update Error:", error);
res.status(500).json({ success: false, message: 'Failed to update doctor' }); res.status(500).json({success: false, message: "Failed to update doctor"});
} }
}; };
//delete doctor //delete doctor
export const deleteDoctor = async (req, res) => { export const deleteDoctor = async (req, res) => {
try { try {
const { doctorId } = req.params; const {doctorId} = req.params;
const doctor = await prisma.doctor.findUnique({ const doctor = await prisma.doctor.findUnique({
where: { doctorId }, where: {doctorId},
}); });
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 doctorDepartments = await prisma.doctorDepartment.findMany({ const doctorDepartments = await prisma.doctorDepartment.findMany({
where: { doctorId: doctor.id }, where: {doctorId: doctor.id},
}); });
for (const dd of doctorDepartments) { for (const dd of doctorDepartments) {
await prisma.doctorTiming.deleteMany({ await prisma.doctorTiming.deleteMany({
where: { doctorDepartmentId: dd.id }, where: {doctorDepartmentId: dd.id},
}); });
} }
await prisma.doctorDepartment.deleteMany({ await prisma.doctorDepartment.deleteMany({
where: { doctorId: doctor.id }, where: {doctorId: doctor.id},
}); });
await prisma.doctor.delete({ await prisma.doctor.delete({
where: { id: doctor.id }, where: {id: doctor.id},
}); });
res.status(200).json({ res.status(200).json({
success: true, success: true,
message: 'Doctor 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",
}); });
} }
}; };
@@ -620,14 +392,14 @@ export const getDoctorTimings = async (req, res) => {
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 || "",
}; };
}); });
@@ -639,7 +411,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",
}); });
} }
}; };
@@ -647,10 +419,10 @@ export const getDoctorTimings = async (req, res) => {
export const getDoctorTimingById = async (req, res) => { export const getDoctorTimingById = async (req, res) => {
try { try {
const { doctorId } = req.params; const {doctorId} = req.params;
const doctor = await prisma.doctor.findUnique({ const doctor = await prisma.doctor.findUnique({
where: { doctorId }, where: {doctorId},
include: { include: {
departments: { departments: {
include: { include: {
@@ -664,7 +436,7 @@ 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",
}); });
} }
@@ -687,7 +459,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,14 +1,14 @@
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) => {
try { try {
const { name, email, type, isActive } = req.body; const {name, email, type, isActive} = req.body;
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",
}); });
} }
}; };
@@ -60,7 +60,7 @@ export const getEmailConfigs = async (req, res) => {
// GET SINGLE // GET SINGLE
export const getEmailConfig = async (req, res) => { export const getEmailConfig = async (req, res) => {
try { try {
const { id } = req.params; const {id} = req.params;
const email = await prisma.emailConfig.findUnique({ const email = await prisma.emailConfig.findUnique({
where: { where: {
@@ -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",
}); });
} }
}; };
@@ -91,7 +91,7 @@ export const getEmailConfig = async (req, res) => {
// UPDATE // UPDATE
export const updateEmailConfig = async (req, res) => { export const updateEmailConfig = async (req, res) => {
try { try {
const { id } = req.params; const {id} = req.params;
const updated = await prisma.emailConfig.update({ const updated = await prisma.emailConfig.update({
where: { where: {
@@ -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",
}); });
} }
}; };
@@ -117,7 +117,7 @@ export const updateEmailConfig = async (req, res) => {
// DELETE // DELETE
export const deleteEmailConfig = async (req, res) => { export const deleteEmailConfig = async (req, res) => {
try { try {
const { id } = req.params; const {id} = req.params;
await prisma.emailConfig.delete({ await prisma.emailConfig.delete({
where: { where: {
@@ -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",
}); });
} }
}; };
@@ -1,488 +0,0 @@
import prisma from '../prisma/client.js';
import { sendEmail } from '../utils/sendEmail.js';
import { getEmailsByType } from '../utils/getEmailByTypes.js';
export const getAllCategories = async (req, res) => {
try {
const { admin } = req.query;
const categories = await prisma.healthCheckCategory.findMany({
where: admin === 'true' ? {} : { isActive: true },
orderBy: { sortOrder: 'asc' },
include: {
_count: { select: { packages: true } },
},
});
return res.status(200).json({ success: true, data: categories });
} catch (error) {
return res.status(500).json({ success: false, message: 'Failed to fetch categories' });
}
};
export const createCategory = async (req, res) => {
try {
const { name, slug, description, isActive, sortOrder } = req.body;
const category = await prisma.healthCheckCategory.create({
data: {
name,
slug: slug || null,
description,
isActive: isActive ?? true,
sortOrder: sortOrder ? Number(sortOrder) : 1000,
},
});
return res.status(201).json({ success: true, message: 'Category created', data: category });
} catch (error) {
console.error(error);
return res.status(500).json({ success: false, message: 'Failed to create category' });
}
};
export const updateCategory = async (req, res) => {
try {
const { id } = req.params;
const data = { ...req.body };
delete data.id;
delete data._count;
delete data.createdAt;
delete data.updatedAt;
if (data.sortOrder !== undefined) data.sortOrder = Number(data.sortOrder);
if (data.slug === '') data.slug = null;
const updatedCategory = await prisma.$transaction(async (tx) => {
const category = await tx.healthCheckCategory.update({
where: { id: Number(id) },
data,
});
if (data.isActive === false) {
await tx.healthPackage.updateMany({
where: { categoryId: Number(id) },
data: { isActive: false },
});
}
return category;
});
return res.status(200).json({
success: true,
message: 'Category updated',
data: updatedCategory,
});
} catch (error) {
console.error(error);
return res.status(500).json({ success: false, message: 'Failed to update category' });
}
};
export const deleteCategory = async (req, res) => {
try {
const { id } = req.params;
await prisma.healthCheckCategory.delete({
where: { id: Number(id) },
});
return res.status(200).json({ success: true, message: 'Category deleted successfully' });
} catch (error) {
console.error(error);
return res.status(500).json({
success: false,
message: 'Failed to delete category. Ensure no packages are linked to it.',
});
}
};
export const getAllPackages = async (req, res) => {
try {
const { admin, categorySlug } = req.query;
const packages = await prisma.healthPackage.findMany({
where: {
AND: [admin === 'true' ? {} : { isActive: true }, categorySlug ? { category: { slug: categorySlug } } : {}],
},
include: {
category: true,
seo: true,
},
orderBy: [{ sortOrder: 'asc' }, { createdAt: 'desc' }],
});
return res.status(200).json({ success: true, data: packages });
} catch (error) {
console.error(error);
return res.status(500).json({ success: false, message: 'Failed to fetch packages' });
}
};
export const createPackage = async (req, res) => {
try {
const {
name,
slug,
description,
price,
image,
discountedPrice,
inclusions,
categoryId,
isActive,
isFeatured,
sortOrder,
seo,
} = req.body;
const healthPackage = await prisma.healthPackage.create({
data: {
name,
slug,
description,
price,
image,
discountedPrice,
inclusions,
categoryId: Number(categoryId),
isActive: isActive ?? true,
isFeatured: isFeatured ?? false,
sortOrder: sortOrder ? Number(sortOrder) : 1000,
...(seo && {
seo: {
create: {
seoTitle: seo.seoTitle,
metaDescription: seo.metaDescription,
focusKeyphrase: seo.focusKeyphrase,
slug: slug,
tags: seo.tags || [],
ogTitle: seo.ogTitle,
ogDescription: seo.ogDescription,
ogImage: seo.ogImage,
},
},
}),
},
include: {
category: true,
seo: true,
},
});
return res.status(201).json({ success: true, message: 'Package created', data: healthPackage });
} catch (error) {
console.error(error);
return res.status(500).json({ success: false, message: 'Failed to create package' });
}
};
export const updatePackage = async (req, res) => {
try {
const { id } = req.params;
const data = { ...req.body };
delete data.id;
delete data.category;
delete data.createdAt;
delete data.updatedAt;
delete data.seoId;
if (data.categoryId) data.categoryId = Number(data.categoryId);
if (data.sortOrder) data.sortOrder = Number(data.sortOrder);
const existingPackage = await prisma.healthPackage.findUnique({
where: { id: Number(id) },
select: { slug: true },
});
const seoSlug = data.slug || existingPackage.slug;
const updated = await prisma.healthPackage.update({
where: { id: Number(id) },
data: {
...data,
seo: data.seo
? {
upsert: {
create: {
seoTitle: data.seo.seoTitle,
metaDescription: data.seo.metaDescription,
focusKeyphrase: data.seo.focusKeyphrase,
slug: seoSlug,
tags: data.seo.tags || [],
ogTitle: data.seo.ogTitle,
ogDescription: data.seo.ogDescription,
ogImage: data.seo.ogImage,
},
update: {
seoTitle: data.seo.seoTitle,
metaDescription: data.seo.metaDescription,
focusKeyphrase: data.seo.focusKeyphrase,
slug: seoSlug,
tags: data.seo.tags || [],
ogTitle: data.seo.ogTitle,
ogDescription: data.seo.ogDescription,
ogImage: data.seo.ogImage,
},
},
}
: undefined,
},
include: {
category: true,
seo: true,
},
});
return res.status(200).json({ success: true, message: 'Package updated', data: updated });
} catch (error) {
console.error(error);
return res.status(500).json({ success: false, message: 'Update failed' });
}
};
export const deletePackage = async (req, res) => {
try {
const { id } = req.params;
await prisma.healthPackage.delete({
where: { id: Number(id) },
});
return res.status(200).json({
success: true,
message: 'Package deleted',
});
} catch (error) {
console.error(error);
return res.status(500).json({
success: false,
message: 'Delete failed',
});
}
};
export const createPackageInquiry = async (req, res) => {
try {
const { fullName, mobileNumber, email, age, gender, preferredDate, packageId, message } = req.body;
const inquiry = await prisma.healthPackageInquiry.create({
data: {
fullName,
mobileNumber,
email,
age: age ? Number(age) : null,
gender,
preferredDate: preferredDate ? new Date(preferredDate) : null,
message,
packageId: Number(packageId),
},
include: {
healthPackage: true,
},
});
try {
const emailList = await getEmailsByType('HCINQUIRY');
if (emailList) {
await sendEmail({
to: emailList,
subject: 'New Health Checkup Package Inquiry',
html: `
<div style="font-family: Arial, sans-serif; background-color: #f4f6f8; padding: 20px;">
<div style="max-width: 600px; margin: auto; background: #ffffff; border-radius: 10px; overflow: hidden; box-shadow: 0 4px 10px rgba(0,0,0,0.05);">
<!-- Header -->
<div style="background-color: #0d6efd; color: #ffffff; padding: 20px;">
<h2 style="margin: 0;">GG Hospital</h2>
<p style="margin: 5px 0 0; font-size: 14px;">
New Health Checkup Package Inquiry
</p>
</div>
<!-- Body -->
<div style="padding: 20px; color: #333;">
<h3 style="margin-top: 0;">Inquirer Details</h3>
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 8px 0; width: 35%;"><b>Name:</b></td>
<td style="padding: 8px 0;">${fullName}</td>
</tr>
<tr>
<td style="padding: 8px 0;"><b>Phone:</b></td>
<td style="padding: 8px 0;">${mobileNumber}</td>
</tr>
<tr>
<td style="padding: 8px 0;"><b>Email:</b></td>
<td style="padding: 8px 0;">${email || '-'}</td>
</tr>
<tr>
<td style="padding: 8px 0;"><b>Age:</b></td>
<td style="padding: 8px 0;">${age || '-'}</td>
</tr>
<tr>
<td style="padding: 8px 0;"><b>Gender:</b></td>
<td style="padding: 8px 0;">${gender || '-'}</td>
</tr>
</table>
<h3 style="margin-top: 20px;">Package Details</h3>
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 8px 0; width: 35%;"><b>Package Name:</b></td>
<td style="padding: 8px 0;">${inquiry.healthPackage?.name || 'Unknown Package'}</td>
</tr>
<tr>
<td style="padding: 8px 0;"><b>Preferred Date:</b></td>
<td style="padding: 8px 0;">
${
preferredDate
? new Date(preferredDate).toLocaleDateString('en-GB', {
day: '2-digit',
month: 'long',
year: 'numeric',
})
: 'Not specified'
}
</td>
</tr>
</table>
<!-- Message Box -->
<div style="margin-top: 20px;">
<h3>Message</h3>
<div style="
background: #f8f9fa;
padding: 15px;
border-radius: 6px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
overflow-wrap: anywhere;
">
${message ? message.replace(/\n/g, '<br/>') : '-'}
</div>
</div>
</div>
<!-- Footer -->
<div style="background: #f1f1f1; padding: 15px; text-align: center; font-size: 12px; color: #666;">
This inquiry was submitted via the GG Hospital website.
</div>
</div>
</div>
`,
});
}
} catch (err) {
console.error('Email failed:', err);
}
return res.status(201).json({
success: true,
message: 'Booking inquiry sent successfully',
data: inquiry,
});
} catch (error) {
console.error(error);
return res.status(500).json({ success: false, message: 'Failed to submit inquiry' });
}
};
export const getPackageBySlug = async (req, res) => {
try {
const { slug } = req.params;
const healthPackage = await prisma.healthPackage.findFirst({
where: { slug, isActive: true },
include: {
category: true,
seo: true,
},
});
if (!healthPackage) {
return res.status(404).json({ success: false, message: 'Package not found' });
}
return res.status(200).json({
success: true,
data: healthPackage,
});
} catch (error) {
console.error(error);
return res.status(500).json({ success: false, message: 'Failed to fetch package' });
}
};
export const getAllInquiries = async (req, res) => {
try {
const { page = 1, limit = 10, filterDate, startDate, endDate } = req.query;
const queryPage = parseInt(page);
const queryLimit = parseInt(limit);
const skip = (queryPage - 1) * queryLimit;
let where = {};
if (filterDate) {
where.preferredDate = {
gte: new Date(`${filterDate}T00:00:00.000Z`),
lte: new Date(`${filterDate}T23:59:59.999Z`),
};
} else if (startDate || endDate) {
where.preferredDate = {};
if (startDate) {
where.preferredDate.gte = new Date(`${startDate}T00:00:00.000Z`);
}
if (endDate) {
where.preferredDate.lte = new Date(`${endDate}T23:59:59.999Z`);
}
}
const [total, inquiries] = await prisma.$transaction([
prisma.healthPackageInquiry.count({ where }),
prisma.healthPackageInquiry.findMany({
where,
skip,
take: queryLimit,
include: {
healthPackage: {
include: {
category: true,
},
},
},
orderBy: { createdAt: 'desc' },
}),
]);
return res.status(200).json({
success: true,
data: inquiries,
pagination: {
total,
page: queryPage,
limit: queryLimit,
totalPages: Math.ceil(total / queryLimit),
},
});
} catch (error) {
console.error(error);
return res.status(500).json({ success: false, message: 'Failed to fetch inquiries' });
}
};
@@ -1,203 +0,0 @@
import prisma from '../prisma/client.js';
export const createHomepageBanner = async (req, res) => {
try {
const {
title,
subtitle,
mediaType,
desktopMediaUrl,
mobileMediaUrl,
buttonText,
buttonLink,
openInNewTab,
textAlignment,
sortOrder,
isActive,
} = req.body;
if (!mediaType || !desktopMediaUrl) {
return res.status(400).json({
success: false,
message: 'Media type and desktop media URL are required',
});
}
const banner = await prisma.homepageBanner.create({
data: {
title,
subtitle,
mediaType,
desktopMediaUrl,
mobileMediaUrl,
buttonText,
buttonLink,
openInNewTab,
textAlignment,
sortOrder,
isActive,
},
});
res.status(201).json({
success: true,
data: banner,
message: 'Homepage banner created successfully',
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to create homepage banner',
});
}
};
export const getHomepageBanners = async (req, res) => {
try {
const banners = await prisma.homepageBanner.findMany({
orderBy: {
sortOrder: 'asc',
},
});
res.json({
success: true,
data: banners,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to fetch homepage banners',
});
}
};
export const getActiveHomepageBanners = async (req, res) => {
try {
const banners = await prisma.homepageBanner.findMany({
where: {
isActive: true,
},
orderBy: {
sortOrder: 'asc',
},
});
res.json({
success: true,
data: banners,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to fetch active homepage banners',
});
}
};
export const getHomepageBanner = async (req, res) => {
try {
const { id } = req.params;
const banner = await prisma.homepageBanner.findUnique({
where: {
id: Number(id),
},
});
if (!banner) {
return res.status(404).json({
success: false,
message: 'Homepage banner not found',
});
}
res.json({
success: true,
data: banner,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to fetch homepage banner',
});
}
};
export const updateHomepageBanner = async (req, res) => {
try {
const { id } = req.params;
const {
title,
subtitle,
mediaType,
desktopMediaUrl,
mobileMediaUrl,
buttonText,
buttonLink,
openInNewTab,
textAlignment,
sortOrder,
isActive,
} = req.body;
const banner = await prisma.homepageBanner.update({
where: {
id: Number(id),
},
data: {
title,
subtitle,
mediaType,
desktopMediaUrl,
mobileMediaUrl,
buttonText,
buttonLink,
openInNewTab,
textAlignment,
sortOrder,
isActive,
},
});
res.json({
success: true,
data: banner,
message: 'Homepage banner updated successfully',
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to update homepage banner',
});
}
};
export const deleteHomepageBanner = async (req, res) => {
try {
const { id } = req.params;
await prisma.homepageBanner.delete({
where: {
id: Number(id),
},
});
res.json({
success: true,
message: 'Homepage banner deleted successfully',
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to delete homepage banner',
});
}
};
+35 -23
View File
@@ -1,12 +1,22 @@
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient(); const prisma = new PrismaClient();
export const bulkImportExcelData = async (req, res) => { export const bulkImportExcelData = async (req, res) => {
try { try {
const { departments, doctors, timings, careers, inquiries, academics, appointments, candidates, news } = req.body; const {
departments,
doctors,
timings,
careers,
inquiries,
academics,
appointments,
candidates,
news,
} = req.body;
console.log('🚀 Starting Robust Data Import...'); console.log("🚀 Starting Robust Data Import...");
// 1. DEPARTMENTS // 1. DEPARTMENTS
if (departments) { if (departments) {
@@ -44,14 +54,14 @@ export const bulkImportExcelData = async (req, res) => {
update: { update: {
name: row.Name?.toString(), name: row.Name?.toString(),
designation: row.Designation?.toString() || null, designation: row.Designation?.toString() || null,
workingStatus: row['Working Status']?.toString() || null, workingStatus: row["Working Status"]?.toString() || null,
qualification: row.Qualification?.toString() || null, qualification: row.Qualification?.toString() || null,
}, },
create: { create: {
doctorId: row.GG_ID.toString(), doctorId: row.GG_ID.toString(),
name: row.Name?.toString(), name: row.Name?.toString(),
designation: row.Designation?.toString() || null, designation: row.Designation?.toString() || null,
workingStatus: row['Working Status']?.toString() || null, workingStatus: row["Working Status"]?.toString() || null,
qualification: row.Qualification?.toString() || null, qualification: row.Qualification?.toString() || null,
}, },
}); });
@@ -90,8 +100,8 @@ export const bulkImportExcelData = async (req, res) => {
if (doctor && doctor.departments.length > 0) { if (doctor && doctor.departments.length > 0) {
const doctorDeptId = doctor.departments[0].id; const doctorDeptId = doctor.departments[0].id;
const rawAdd = row.Additional?.toString() || ''; const rawAdd = row.Additional?.toString() || "";
const rawMon = row.Monday?.toString() || ''; const rawMon = row.Monday?.toString() || "";
const isAppt = (val) => /appointment|basis|on call/i.test(val); const isAppt = (val) => /appointment|basis|on call/i.test(val);
let finalAdd = rawAdd; let finalAdd = rawAdd;
@@ -137,7 +147,7 @@ export const bulkImportExcelData = async (req, res) => {
experienceNeed: row.ExperienceNeed?.toString() || null, experienceNeed: row.ExperienceNeed?.toString() || null,
email: row.HiringEmail?.toString() || null, email: row.HiringEmail?.toString() || null,
number: row.Number?.toString() || null, number: row.Number?.toString() || null,
status: row.Status?.toString() || 'new', status: row.Status?.toString() || "new",
}; };
if (cId) { if (cId) {
await prisma.career.upsert({ await prisma.career.upsert({
@@ -158,7 +168,7 @@ export const bulkImportExcelData = async (req, res) => {
await prisma.inquiry.create({ await prisma.inquiry.create({
data: { data: {
fullName: row.FullName.toString(), fullName: row.FullName.toString(),
number: row.Number?.toString() || '', number: row.Number?.toString() || "",
emailId: row.EmailId?.toString() || null, emailId: row.EmailId?.toString() || null,
subject: row.Subject?.toString() || null, subject: row.Subject?.toString() || null,
message: row.Message?.toString() || null, message: row.Message?.toString() || null,
@@ -175,10 +185,10 @@ export const bulkImportExcelData = async (req, res) => {
await prisma.academicsResearch.create({ await prisma.academicsResearch.create({
data: { data: {
fullName: row.FullName.toString(), fullName: row.FullName.toString(),
number: row.Number?.toString() || '', number: row.Number?.toString() || "",
emailId: row.EmailId?.toString() || null, emailId: row.EmailId?.toString() || null,
subject: row.Subject?.toString() || null, // Force String subject: row.Subject?.toString() || null, // Force String
courseName: row['Course Name']?.toString() || null, courseName: row["Course Name"]?.toString() || null,
message: row.Message?.toString() || null, message: row.Message?.toString() || null,
createdAt: row.Date ? new Date(row.Date) : new Date(), createdAt: row.Date ? new Date(row.Date) : new Date(),
}, },
@@ -191,7 +201,7 @@ export const bulkImportExcelData = async (req, res) => {
for (const row of appointments) { for (const row of appointments) {
if (!row.FullName) continue; if (!row.FullName) continue;
const doctorName = row.Doctor?.toString(); const doctorName = row.Doctor?.toString();
const departmentName = row['Department Id']?.toString(); const departmentName = row["Department Id"]?.toString();
const doctor = await prisma.doctor.findFirst({ const doctor = await prisma.doctor.findFirst({
where: { name: doctorName }, where: { name: doctorName },
@@ -204,11 +214,11 @@ export const bulkImportExcelData = async (req, res) => {
if (!value) return new Date(); if (!value) return new Date();
// Excel numeric date // Excel numeric date
if (typeof value === 'number') { if (typeof value === "number") {
return new Date((value - 25569) * 86400 * 1000); return new Date((value - 25569) * 86400 * 1000);
} }
if (typeof value === 'string') { if (typeof value === "string") {
const v = value.trim(); const v = value.trim();
// Handle DD/MM/YYYY // Handle DD/MM/YYYY
@@ -228,15 +238,15 @@ export const bulkImportExcelData = async (req, res) => {
} }
} }
console.warn('⚠️ Invalid date, using current date:', value); console.warn("⚠️ Invalid date, using current date:", value);
return new Date(); return new Date();
}; };
if (doctor && department) { if (doctor && department) {
await prisma.appointment.create({ await prisma.appointment.create({
data: { data: {
name: row.FullName.toString(), name: row.FullName.toString(),
mobileNumber: row.Number?.toString() || '', mobileNumber: row.Number?.toString() || "",
email: row['Email Id']?.toString() || null, email: row["Email Id"]?.toString() || null,
message: row.Message?.toString() || null, message: row.Message?.toString() || null,
date: parseDate(row.Date), date: parseDate(row.Date),
doctorId: doctor.doctorId, doctorId: doctor.doctorId,
@@ -255,10 +265,10 @@ export const bulkImportExcelData = async (req, res) => {
.create({ .create({
data: { data: {
fullName: row.FullName.toString(), fullName: row.FullName.toString(),
mobile: row.Number?.toString() || '', mobile: row.Number?.toString() || "",
email: row.EmailId?.toString() || '', email: row.EmailId?.toString() || "",
subject: row.Subject?.toString() || '', subject: row.Subject?.toString() || "",
coverLetter: row['Cover Letter']?.toString() || '', coverLetter: row["Cover Letter"]?.toString() || "",
careerId: parseInt(row.CareerId), careerId: parseInt(row.CareerId),
createdAt: row.Date ? new Date(row.Date) : new Date(), createdAt: row.Date ? new Date(row.Date) : new Date(),
}, },
@@ -284,9 +294,11 @@ export const bulkImportExcelData = async (req, res) => {
} }
} }
res.status(200).json({ success: true, message: '✅ Import completed successfully!' }); res
.status(200)
.json({ success: true, message: "✅ Import completed successfully!" });
} catch (error) { } catch (error) {
console.error('❌ Bulk Import Error:', error); console.error("❌ Bulk Import Error:", error);
res.status(500).json({ success: false, error: error.message }); res.status(500).json({ success: false, error: error.message });
} }
}; };
+21 -21
View File
@@ -1,17 +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 INQUIRY */ /* CREATE INQUIRY */
export const createInquiry = async (req, res) => { export const createInquiry = async (req, res) => {
try { try {
const { fullName, number, emailId, subject, message } = req.body; const {fullName, number, emailId, subject, message} = 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",
}); });
} }
@@ -25,12 +25,12 @@ export const createInquiry = async (req, res) => {
}, },
}); });
try { try {
const emailList = await getEmailsByType('INQUIRY'); const emailList = await getEmailsByType("INQUIRY");
if (emailList && emailList.length > 0) { if (emailList && emailList.length > 0) {
await sendEmail({ await sendEmail({
to: emailList, to: emailList,
subject: 'New Inquiry Received', subject: "New Inquiry Received",
html: ` html: `
<div style="font-family: Arial, sans-serif; background-color: #f4f6f8; padding: 20px;"> <div style="font-family: Arial, sans-serif; background-color: #f4f6f8; padding: 20px;">
@@ -78,7 +78,7 @@ export const createInquiry = async (req, res) => {
word-break: break-word; word-break: break-word;
overflow-wrap: anywhere; overflow-wrap: anywhere;
"> ">
${message ? message.replace(/\n/g, '<br/>') : '-'} ${message ? message.replace(/\n/g, "<br/>") : "-"}
</div> </div>
</div> </div>
@@ -96,20 +96,20 @@ export const createInquiry = async (req, res) => {
}); });
} }
} catch (err) { } catch (err) {
console.error('Inquiry email failed:', 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",
}); });
} }
}; };
@@ -119,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",
}, },
}); });
@@ -130,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",
}); });
} }
}; };
@@ -138,16 +138,16 @@ export const getInquiries = async (req, res) => {
/* GET SINGLE */ /* GET SINGLE */
export const getInquiry = async (req, res) => { export const getInquiry = async (req, res) => {
try { try {
const { id } = req.params; const {id} = req.params;
const inquiry = await prisma.inquiry.findUnique({ const inquiry = await prisma.inquiry.findUnique({
where: { id: Number(id) }, where: {id: Number(id)},
}); });
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",
}); });
} }
@@ -158,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",
}); });
} }
}; };
@@ -166,20 +166,20 @@ export const getInquiry = async (req, res) => {
/* DELETE */ /* DELETE */
export const deleteInquiry = async (req, res) => { export const deleteInquiry = async (req, res) => {
try { try {
const { id } = req.params; const {id} = req.params;
await prisma.inquiry.delete({ await prisma.inquiry.delete({
where: { id: Number(id) }, where: {id: Number(id)},
}); });
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",
}); });
} }
}; };
+23 -15
View File
@@ -1,4 +1,4 @@
import prisma from '../prisma/client.js'; import prisma from "../prisma/client.js";
// GET ALL NEWS // GET ALL NEWS
@@ -6,7 +6,7 @@ export const getAllNews = async (req, res) => {
try { try {
const page = req.query.page ? parseInt(req.query.page) : null; const page = req.query.page ? parseInt(req.query.page) : null;
const limit = req.query.limit ? parseInt(req.query.limit) : null; const limit = req.query.limit ? parseInt(req.query.limit) : null;
const search = req.query.search?.trim() || ''; const search = req.query.search?.trim() || "";
const includeImages = { const includeImages = {
images: true, images: true,
@@ -16,7 +16,7 @@ export const getAllNews = async (req, res) => {
? { ? {
headline: { headline: {
contains: search, contains: search,
mode: 'insensitive', mode: "insensitive",
}, },
} }
: {}; : {};
@@ -32,7 +32,7 @@ export const getAllNews = async (req, res) => {
prisma.newsMedia.findMany({ prisma.newsMedia.findMany({
where: whereCondition, where: whereCondition,
include: includeImages, include: includeImages,
orderBy: { createdAt: 'desc' }, orderBy: { createdAt: "desc" },
skip, skip,
take, take,
}), }),
@@ -69,7 +69,7 @@ export const getAllNews = 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",
}); });
} }
}; };
@@ -87,7 +87,7 @@ export const getNewsById = async (req, res) => {
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",
}); });
} }
@@ -113,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",
}); });
} }
}; };
@@ -122,12 +122,20 @@ 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, imageUrls } = 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",
}); });
} }
@@ -150,14 +158,14 @@ export const createNews = async (req, res) => {
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",
}); });
} }
}; };
@@ -186,14 +194,14 @@ export const updateNews = async (req, res) => {
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",
}); });
} }
}; };
@@ -210,13 +218,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",
}); });
} }
}; };
+4 -4
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) {
@@ -12,4 +12,4 @@ const storage = multer.diskStorage({
}, },
}); });
export const upload = multer({ storage }); export const upload = multer({storage});
+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', jwtAuthMiddleware, getAcademicsResearch); router.get("/getAll", jwtAuthMiddleware, getAcademicsResearch);
router.get('/:id', jwtAuthMiddleware, 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', jwtAuthMiddleware, getAppointments); router.get("/getall", jwtAuthMiddleware, getAppointments);
router.post('/', createAppointment); router.post("/", createAppointment);
router.get('/:id', jwtAuthMiddleware, getAppointment); router.get("/:id", jwtAuthMiddleware, getAppointment);
router.patch('/:id', jwtAuthMiddleware, updateAppointment); router.patch("/:id", jwtAuthMiddleware, updateAppointment);
router.delete('/:id', jwtAuthMiddleware, deleteAppointment); router.delete("/:id", jwtAuthMiddleware, deleteAppointment);
export default router; export default router;
+3 -3
View File
@@ -1,8 +1,8 @@
import express from 'express'; import express from "express";
import { login } from '../controllers/auth.controller.js'; import { login } from "../controllers/auth.controller.js";
const router = express.Router(); const router = express.Router();
router.post('/login', login); router.post("/login", login);
export default router; export default router;
+10 -10
View File
@@ -1,4 +1,4 @@
import express from 'express'; import express from "express";
import { import {
createBlog, createBlog,
getBlogs, getBlogs,
@@ -7,25 +7,25 @@ import {
deleteBlog, deleteBlog,
getAllBlogs, getAllBlogs,
getBlogForAdmin, getBlogForAdmin,
} from '../controllers/blog.controller.js'; } from "../controllers/blog.controller.js";
import jwtAuthMiddleware from '../middleware/auth.js'; import jwtAuthMiddleware from "../middleware/auth.js";
const router = express.Router(); const router = express.Router();
/* PUBLIC */ /* PUBLIC */
router.get('/', getBlogs); router.get("/", getBlogs);
router.get('/:slug', getBlog); router.get("/:slug", getBlog);
// Protected // Protected
router.get('/admin/all', jwtAuthMiddleware, getAllBlogs); router.get("/admin/all", jwtAuthMiddleware, getAllBlogs);
router.get('/admin/:id', jwtAuthMiddleware, getBlogForAdmin); router.get("/admin/:id", jwtAuthMiddleware, getBlogForAdmin);
router.post('/', jwtAuthMiddleware, createBlog); router.post("/", jwtAuthMiddleware, createBlog);
router.put('/:id', jwtAuthMiddleware, updateBlog); router.put("/:id", jwtAuthMiddleware, updateBlog);
router.delete('/:id', jwtAuthMiddleware, deleteBlog); 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.post("/", createCandidate);
router.get('/getAll', jwtAuthMiddleware, getCandidates); router.get("/getAll", jwtAuthMiddleware, getCandidates);
router.get('/:id', jwtAuthMiddleware, getCandidate); router.get("/:id", jwtAuthMiddleware, getCandidate);
router.get('/career/:careerId', jwtAuthMiddleware, getCandidatesByCareer); router.get("/career/:careerId", jwtAuthMiddleware, getCandidatesByCareer);
router.patch('/:id', jwtAuthMiddleware, updateCandidate); router.patch("/:id", jwtAuthMiddleware, updateCandidate);
router.delete('/:id', jwtAuthMiddleware, deleteCandidate); router.delete("/:id", jwtAuthMiddleware, deleteCandidate);
export default router; export default router;
+12 -7
View File
@@ -1,14 +1,19 @@
import express from 'express'; import express from "express";
import { getAllCareers, createCareer, updateCareer, deleteCareer } from '../controllers/career.controller.js'; import {
getAllCareers,
createCareer,
updateCareer,
deleteCareer,
} from "../controllers/career.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', getAllCareers); router.get("/getAll", getAllCareers);
router.post('/', jwtAuthMiddleware, createCareer); router.post("/", jwtAuthMiddleware, createCareer);
router.patch('/:id', jwtAuthMiddleware, updateCareer); router.patch("/:id", jwtAuthMiddleware, updateCareer);
router.delete('/:id', jwtAuthMiddleware, deleteCareer); router.delete("/:id", jwtAuthMiddleware, deleteCareer);
export default router; export default router;
+8 -8
View File
@@ -1,22 +1,22 @@
import express from 'express'; import express from "express";
import { import {
getAllDepartments, getAllDepartments,
getDepartmentByName, 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); 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;
+11 -11
View File
@@ -1,4 +1,4 @@
import express from 'express'; import express from "express";
import { import {
getAllDoctors, getAllDoctors,
createDoctor, createDoctor,
@@ -8,20 +8,20 @@ import {
getDoctorTimingById, getDoctorTimingById,
getDoctorByDoctorId, getDoctorByDoctorId,
getDoctorsByDepartmentId, getDoctorsByDepartmentId,
} from '../controllers/doctor.controller.js'; } from "../controllers/doctor.controller.js";
import jwtAuthMiddleware from '../middleware/auth.js'; import jwtAuthMiddleware from "../middleware/auth.js";
const router = express.Router(); const router = express.Router();
router.get('/getAll', getAllDoctors); router.get("/getAll", getAllDoctors);
router.get('/search', getDoctorsByDepartmentId); 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.get("/:doctorId", getDoctorByDoctorId);
router.post('/', jwtAuthMiddleware, createDoctor); router.post("/", jwtAuthMiddleware, createDoctor);
router.patch('/:doctorId/:action', jwtAuthMiddleware, updateDoctor); router.patch("/:doctorId", 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
@@ -1,39 +0,0 @@
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;
@@ -1,27 +0,0 @@
import express from 'express';
import {
createHomepageBanner,
getHomepageBanners,
getActiveHomepageBanners,
getHomepageBanner,
updateHomepageBanner,
deleteHomepageBanner,
} from '../controllers/homepageBanner.controller.js';
import jwtAuthMiddleware from '../middleware/auth.js';
const router = express.Router();
router.get('/active', getActiveHomepageBanners);
router.post('/', jwtAuthMiddleware, createHomepageBanner);
router.get('/getAll', jwtAuthMiddleware, getHomepageBanners);
router.get('/:id', jwtAuthMiddleware, getHomepageBanner);
router.put('/:id', jwtAuthMiddleware, updateHomepageBanner);
router.delete('/:id', jwtAuthMiddleware, deleteHomepageBanner);
export default router;
+4 -4
View File
@@ -1,9 +1,9 @@
import express from 'express'; import express from "express";
import { bulkImportExcelData } from '../controllers/importController.js'; import { bulkImportExcelData } from "../controllers/importController.js";
import jwtAuthMiddleware from '../middleware/auth.js'; import jwtAuthMiddleware from "../middleware/auth.js";
const router = express.Router(); const router = express.Router();
router.post('/bulk', jwtAuthMiddleware, bulkImportExcelData); router.post("/bulk", jwtAuthMiddleware, bulkImportExcelData);
export default router; export default router;
+12 -7
View File
@@ -1,14 +1,19 @@
import express from 'express'; import express from "express";
import { createInquiry, getInquiries, getInquiry, deleteInquiry } from '../controllers/inquiry.controller.js'; import {
createInquiry,
getInquiries,
getInquiry,
deleteInquiry,
} from "../controllers/inquiry.controller.js";
import jwtAuthMiddleware from '../middleware/auth.js'; import jwtAuthMiddleware from "../middleware/auth.js";
const router = express.Router(); const router = express.Router();
router.post('/', createInquiry); router.post("/", createInquiry);
router.get('/getAll', jwtAuthMiddleware, getInquiries); router.get("/getAll", jwtAuthMiddleware, getInquiries);
router.get('/:id', jwtAuthMiddleware, getInquiry); router.get("/:id", jwtAuthMiddleware, getInquiry);
router.delete('/:id', jwtAuthMiddleware, deleteInquiry); router.delete("/:id", jwtAuthMiddleware, deleteInquiry);
export default router; export default router;
+14 -8
View File
@@ -1,17 +1,23 @@
import express from 'express'; import express from "express";
import { createNews, getAllNews, getNewsById, updateNews, deleteNews } from '../controllers/newsMedia.controller.js'; import {
createNews,
getAllNews,
getNewsById,
updateNews,
deleteNews,
} from "../controllers/newsMedia.controller.js";
import jwtAuthMiddleware from '../middleware/auth.js'; import jwtAuthMiddleware from "../middleware/auth.js";
const router = express.Router(); 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;
+9 -9
View File
@@ -1,6 +1,6 @@
import express from 'express'; import express from "express";
import * as Bytescale from '@bytescale/sdk'; import * as Bytescale from "@bytescale/sdk";
import multer from 'multer'; import multer from "multer";
const router = express.Router(); const router = express.Router();
@@ -9,26 +9,26 @@ const uploadManager = new Bytescale.UploadManager({
}); });
const storage = multer.memoryStorage(); const storage = multer.memoryStorage();
const upload = multer({ storage }); const upload = multer({storage});
router.post('/', upload.single('file'), async (req, res) => { router.post("/", upload.single("file"), async (req, res) => {
try { try {
const file = req.file; const file = req.file;
const { folderPath } = req.body; const {folderPath} = req.body;
const result = await uploadManager.upload({ const result = await uploadManager.upload({
data: file.buffer, data: file.buffer,
name: file.originalname, name: file.originalname,
mime: file.mimetype, mime: file.mimetype,
path: { path: {
folderPath: folderPath || '/general', folderPath: folderPath || "/general",
}, },
}); });
res.json({ fileUrl: result.fileUrl }); res.json({fileUrl: result.fileUrl});
} catch (error) { } catch (error) {
console.error(error); console.error(error);
res.status(500).json({ error: 'Upload failed' }); res.status(500).json({error: "Upload failed"});
} }
}); });
+8 -6
View File
@@ -1,12 +1,14 @@
import prisma from '../prisma/client.js'; import prisma from "../prisma/client.js";
import { hashPassword } from './password.js'; import { hashPassword } from "./password.js";
async function main() { async function main() {
const username = process.argv[2]; const username = process.argv[2];
const password = process.argv[3]; const password = process.argv[3];
const role = process.argv[4] || 'admin'; const role = process.argv[4] || "admin";
if (!username || !password) { if (!username || !password) {
console.log('Usage: node scripts/createUser.js <username> <password> [role]'); console.log(
"Usage: node scripts/createUser.js <username> <password> [role]",
);
process.exit(1); process.exit(1);
} }
@@ -15,7 +17,7 @@ async function main() {
}); });
if (existingUser) { if (existingUser) {
console.log('User already exists'); console.log("User already exists");
process.exit(1); process.exit(1);
} }
@@ -29,7 +31,7 @@ async function main() {
}, },
}); });
console.log('User created:', { console.log("User created:", {
id: user.id, id: user.id,
username: user.username, username: user.username,
role: user.role, role: user.role,
+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);
+5 -5
View File
@@ -1,18 +1,18 @@
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);
export const sendEmail = async ({ to, subject, html, text }) => { export const sendEmail = async ({to, subject, html, text}) => {
try { try {
await client.sendEmail({ await client.sendEmail({
From: process.env.EMAIL_FROM, From: process.env.EMAIL_FROM,
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);
} }
}; };
+4 -4
View File
@@ -1,4 +1,4 @@
version: '3.8' version: "3.8"
services: services:
backend: backend:
@@ -6,7 +6,7 @@ services:
context: . context: .
dockerfile: docker/dev/Dockerfile.main dockerfile: docker/dev/Dockerfile.main
ports: ports:
- '127.0.0.1:5008:5008' - "127.0.0.1:5008:5008"
env_file: env_file:
- ./backend/.env - ./backend/.env
depends_on: depends_on:
@@ -19,7 +19,7 @@ services:
context: . context: .
dockerfile: docker/dev/Dockerfile.frontend dockerfile: docker/dev/Dockerfile.frontend
ports: ports:
- '127.0.0.1:3008:3000' - "127.0.0.1:3008:3000"
env_file: env_file:
- ./frontend/.env - ./frontend/.env
restart: unless-stopped restart: unless-stopped
@@ -33,7 +33,7 @@ services:
volumes: volumes:
- postgres_data:/var/lib/postgresql/data - postgres_data:/var/lib/postgresql/data
healthcheck: healthcheck:
test: ['CMD-SHELL', 'pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB'] test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
interval: 5s interval: 5s
timeout: 5s timeout: 5s
retries: 5 retries: 5
+3 -3
View File
@@ -4,7 +4,7 @@ services:
context: . context: .
dockerfile: docker/dev/Dockerfile.main dockerfile: docker/dev/Dockerfile.main
ports: ports:
- '127.0.0.1:5008:5008' - "127.0.0.1:5008:5008"
env_file: env_file:
- ./backend/.env - ./backend/.env
depends_on: depends_on:
@@ -17,7 +17,7 @@ services:
context: . context: .
dockerfile: docker/dev/Dockerfile.frontend dockerfile: docker/dev/Dockerfile.frontend
ports: ports:
- '127.0.0.1:3008:3000' - "127.0.0.1:3008:3000"
env_file: env_file:
- ./frontend/.env - ./frontend/.env
restart: always restart: always
@@ -31,7 +31,7 @@ services:
volumes: volumes:
- postgres_data:/var/lib/postgresql/data - postgres_data:/var/lib/postgresql/data
healthcheck: healthcheck:
test: ['CMD-SHELL', 'pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB'] test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
interval: 5s interval: 5s
timeout: 5s timeout: 5s
retries: 5 retries: 5
+4 -2
View File
@@ -4,8 +4,10 @@ set -e # Exit immediately if a command exits with a non-zero status
echo "Generating Prisma Client..." echo "Generating Prisma Client..."
npx prisma generate npx prisma generate
echo "Running migrate..." # echo "Running migrate..."
npx prisma migrate deploy # npx prisma migrate deploy
echo "Running PUSH..."
npx prisma db push
echo "Executing command: $@" echo "Executing command: $@"
exec "$@" exec "$@"
+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,
}, },
}, },
]); ])
+1 -1
View File
@@ -1,5 +1,5 @@
export default { export default {
plugins: { plugins: {
'@tailwindcss/postcss': {}, "@tailwindcss/postcss": {},
}, },
}; };
+20 -24
View File
@@ -1,30 +1,28 @@
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 {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 BlogDetail from "./pages/BlogDetails";
import ImportData from './pages/ImportData'; import ImportData from "./pages/ImportData";
import HealthPackagePage from './pages/HealthPackagePage';
import HomepageBanner from './pages/HomepageBannerPage';
export default function App() { export default function App() {
return ( return (
@@ -53,8 +51,6 @@ export default function App() {
<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="/import" element={<ImportData />} />
<Route path="/health-check" element={<HealthPackagePage />} />
<Route path="/homepage-banner" element={<HomepageBanner />} />
</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;
}; };
+5 -5
View File
@@ -1,12 +1,12 @@
import apiClient from '@/api/client'; import apiClient from "@/api/client";
export const getAppointmentsApi = async ( export const getAppointmentsApi = async (
page = 1, page = 1,
limit = 10, limit = 10,
date = '', date = "",
startDate = '', startDate = "",
endDate = '', endDate = "",
search = '' search = "",
) => { ) => {
const params = new URLSearchParams({ const params = new URLSearchParams({
page: String(page), page: String(page),
+6 -3
View File
@@ -1,7 +1,10 @@
import apiClient from './client'; import apiClient from "./client";
export const loginApi = async (username: string, password: string): Promise<any> => { export const loginApi = async (
const response = await apiClient.post('/auth/login/', { username: string,
password: string,
): Promise<any> => {
const response = await apiClient.post("/auth/login/", {
username, username,
password, password,
}); });
+6 -6
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,7 +9,7 @@ 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;
}; };
@@ -19,7 +19,7 @@ export const getBlogByIdApi = async (id: number) => {
}; };
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;
}; };
+10 -10
View File
@@ -1,20 +1,20 @@
import apiClient from '@/api/client'; import apiClient from "@/api/client";
import toast from 'react-hot-toast'; import toast from "react-hot-toast";
export const getCareersApi = async () => { export const getCareersApi = async () => {
const res = await apiClient.get('/careers/getAll?admin=true'); const res = await apiClient.get("/careers/getAll?admin=true");
return res.data; return res.data;
}; };
export const createCareerApi = async (data: any) => { export const createCareerApi = async (data: any) => {
try { try {
const res = await apiClient.post('/careers', data); const res = await apiClient.post("/careers", data);
toast.success('Career created successfully'); toast.success("Career created successfully");
return res.data; return res.data;
} catch (error: any) { } catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to create career'); toast.error(error?.response?.data?.message || "Failed to create career");
throw error; throw error;
} }
@@ -24,11 +24,11 @@ export const updateCareerApi = async (id: number, data: any) => {
try { try {
const res = await apiClient.patch(`/careers/${id}`, data); const res = await apiClient.patch(`/careers/${id}`, data);
toast.success('Career updated successfully'); toast.success("Career updated successfully");
return res.data; return res.data;
} catch (error: any) { } catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to update career'); toast.error(error?.response?.data?.message || "Failed to update career");
throw error; throw error;
} }
@@ -38,11 +38,11 @@ export const deleteCareerApi = async (id: number) => {
try { try {
const res = await apiClient.delete(`/careers/${id}`); const res = await apiClient.delete(`/careers/${id}`);
toast.success('Career deleted successfully'); toast.success("Career deleted successfully");
return res.data; return res.data;
} catch (error: any) { } catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to delete career'); toast.error(error?.response?.data?.message || "Failed to delete career");
throw error; throw error;
} }
+12 -12
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 baseURL: string = import.meta.env.VITE_API_URL; const baseURL: string = import.meta.env.VITE_API_URL;
const apiClient = axios.create({ const apiClient = axios.create({
baseURL: baseURL, 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;
+17 -11
View File
@@ -1,5 +1,5 @@
import apiClient from '@/api/client'; import apiClient from "@/api/client";
import toast from 'react-hot-toast'; import toast from "react-hot-toast";
export interface Department { export interface Department {
departmentId: string; departmentId: string;
@@ -15,7 +15,7 @@ export interface Department {
} }
export const getDepartmentsApi = async () => { export const getDepartmentsApi = async () => {
const res = await apiClient.get('/departments/getAll?admin=true'); const res = await apiClient.get("/departments/getAll?admin=true");
return res.data; return res.data;
}; };
@@ -29,13 +29,15 @@ export const createDepartmentApi = async (data: {
services?: string; services?: string;
}) => { }) => {
try { try {
const res = await apiClient.post('/departments', data); const res = await apiClient.post("/departments", data);
toast.success('Department created successfully'); toast.success("Department created successfully");
return res.data; return res.data;
} catch (error: any) { } catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to create department'); toast.error(
error?.response?.data?.message || "Failed to create department",
);
throw error; throw error;
} }
@@ -50,16 +52,18 @@ export const updateDepartmentApi = async (
para3?: string; para3?: string;
facilities?: string; facilities?: string;
services?: string; services?: string;
} },
) => { ) => {
try { try {
const res = await apiClient.put(`/departments/${departmentId}`, data); const res = await apiClient.put(`/departments/${departmentId}`, data);
toast.success('Department updated successfully'); toast.success("Department updated successfully");
return res.data; return res.data;
} catch (error: any) { } catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to update department'); toast.error(
error?.response?.data?.message || "Failed to update department",
);
throw error; throw error;
} }
@@ -69,11 +73,13 @@ export const deleteDepartmentApi = async (departmentId: string) => {
try { try {
const res = await apiClient.delete(`/departments/${departmentId}`); const res = await apiClient.delete(`/departments/${departmentId}`);
toast.success('Department deleted successfully'); toast.success("Department deleted successfully");
return res.data; return res.data;
} catch (error: any) { } catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to delete department'); toast.error(
error?.response?.data?.message || "Failed to delete department",
);
throw error; throw error;
} }
+11 -12
View File
@@ -1,5 +1,5 @@
import apiClient from '@/api/client'; import apiClient from "@/api/client";
import toast from 'react-hot-toast'; import toast from "react-hot-toast";
export interface Doctor { export interface Doctor {
doctorId: string; doctorId: string;
@@ -27,7 +27,7 @@ export interface Doctor {
} }
export const getDoctorsApi = async () => { export const getDoctorsApi = async () => {
const res = await apiClient.get('/doctors/getAll?admin=true'); const res = await apiClient.get("/doctors/getAll?admin=true");
return res.data; return res.data;
}; };
@@ -38,13 +38,13 @@ export const getDoctorByIdApi = async (doctorId: string) => {
export const createDoctorApi = async (data: Doctor) => { export const createDoctorApi = async (data: Doctor) => {
try { try {
const res = await apiClient.post('/doctors', data); const res = await apiClient.post("/doctors", data);
toast.success('Doctor created successfully'); toast.success("Doctor created successfully");
return res.data; return res.data;
} catch (error: any) { } catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to create doctor'); toast.error(error?.response?.data?.message || "Failed to create doctor");
throw error; throw error;
} }
@@ -53,16 +53,15 @@ export const createDoctorApi = async (data: Doctor) => {
export const updateDoctorApi = async ( export const updateDoctorApi = async (
doctorId: string, doctorId: string,
data: Partial<Doctor>, data: Partial<Doctor>,
action: 'toggleStatus' | 'updateDetails' = 'updateDetails'
) => { ) => {
try { try {
const res = await apiClient.patch(`/doctors/${doctorId}/${action}`, data); const res = await apiClient.patch(`/doctors/${doctorId}`, data);
toast.success('Doctor updated successfully'); toast.success("Doctor updated successfully");
return res.data; return res.data;
} catch (error: any) { } catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to update doctor'); toast.error(error?.response?.data?.message || "Failed to update doctor");
throw error; throw error;
} }
@@ -72,11 +71,11 @@ export const deleteDoctorApi = async (doctorId: string) => {
try { try {
const res = await apiClient.delete(`/doctors/${doctorId}`); const res = await apiClient.delete(`/doctors/${doctorId}`);
toast.success('Doctor deleted successfully'); toast.success("Doctor deleted successfully");
return res.data; return res.data;
} catch (error: any) { } catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to delete doctor'); toast.error(error?.response?.data?.message || "Failed to delete doctor");
throw error; throw error;
} }
+7 -4
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,18 +10,21 @@ 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 (id: number, data: Partial<EmailConfig>) => { export const updateEmailConfigApi = async (
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
@@ -1,155 +0,0 @@
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;
};
-83
View File
@@ -1,83 +0,0 @@
import apiClient from '@/api/client';
import toast from 'react-hot-toast';
export type BannerMediaType = 'IMAGE' | 'VIDEO';
export interface HomepageBanner {
id?: number;
title?: string;
subtitle?: string;
mediaType: BannerMediaType;
desktopMediaUrl: string;
mobileMediaUrl?: string;
buttonText?: string;
buttonLink?: string;
openInNewTab: boolean;
textAlignment?: 'left' | 'center' | 'right';
sortOrder: number;
isActive: boolean;
createdAt?: string;
updatedAt?: string;
}
export const getHomepageBannersApi = async () => {
const res = await apiClient.get('/homepage-banners/getAll');
return res.data;
};
export const getHomepageBannerApi = async (id: number) => {
const res = await apiClient.get(`/homepage-banners/${id}`);
return res.data;
};
export const getActiveHomepageBannersApi = async () => {
const res = await apiClient.get('/homepage-banners/active');
return res.data;
};
export const createHomepageBannerApi = async (data: Partial<HomepageBanner>) => {
try {
const res = await apiClient.post('/homepage-banners', data);
toast.success('Banner created successfully');
return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to create banner');
throw error;
}
};
export const updateHomepageBannerApi = async (id: number, data: Partial<HomepageBanner>) => {
try {
const res = await apiClient.put(`/homepage-banners/${id}`, data);
toast.success('Banner updated successfully');
return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to update banner');
throw error;
}
};
export const deleteHomepageBannerApi = async (id: number) => {
try {
const res = await apiClient.delete(`/homepage-banners/${id}`);
toast.success('Banner deleted successfully');
return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to delete banner');
throw error;
}
};
+2 -2
View File
@@ -1,7 +1,7 @@
import apiClient from '@/api/client'; import apiClient from "@/api/client";
export const getInquiriesApi = async () => { export const getInquiriesApi = async () => {
const res = await apiClient.get('/inquiry/getAll'); const res = await apiClient.get("/inquiry/getAll");
return res.data; return res.data;
}; };
+6 -4
View File
@@ -1,12 +1,14 @@
import apiClient from '@/api/client'; import apiClient from "@/api/client";
export const getNewsApi = async (page = 1, limit = 10, search = '') => { export const getNewsApi = async (page = 1, limit = 10, search = "") => {
const res = await apiClient.get(`/newsMedia/getAll?page=${page}&limit=${limit}&search=${search}`); const res = await apiClient.get(
`/newsMedia/getAll?page=${page}&limit=${limit}&search=${search}`,
);
return res.data; return res.data;
}; };
export const createNewsApi = async (data: any) => { export const createNewsApi = async (data: any) => {
const res = await apiClient.post('/newsMedia', data); const res = await apiClient.post("/newsMedia", data);
return res.data; return res.data;
}; };
+3 -3
View File
@@ -1,7 +1,7 @@
import { Navigate, Outlet } from 'react-router-dom'; import {Navigate, Outlet} from "react-router-dom";
import { useAuth } from '@/context/AuthContext'; import {useAuth} from "@/context/AuthContext";
export default function ProtectedRoute() { export default function ProtectedRoute() {
const { token } = useAuth(); const {token} = useAuth();
return token ? <Outlet /> : <Navigate to="/" replace />; return token ? <Outlet /> : <Navigate to="/" replace />;
} }
+3 -3
View File
@@ -1,7 +1,7 @@
import { Navigate, Outlet } from 'react-router-dom'; import {Navigate, Outlet} from "react-router-dom";
import { useAuth } from '@/context/AuthContext'; import {useAuth} from "@/context/AuthContext";
export default function PublicRoute() { export default function PublicRoute() {
const { token } = useAuth(); const {token} = useAuth();
return token ? <Navigate to="/dashboard" replace /> : <Outlet />; return token ? <Navigate to="/dashboard" replace /> : <Outlet />;
} }
@@ -1,263 +0,0 @@
import { BytescaleUploader } from '@/components/BytescaleUploader/BytescaleUploader';
import { useEffect } from 'react';
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
interface Props {
open: boolean;
onOpenChange: (open: boolean) => void;
editingBanner: any;
bannerForm: any;
setBannerForm: any;
onSave: () => void;
}
export default function HomepageBannerModal({
open,
onOpenChange,
editingBanner,
bannerForm,
setBannerForm,
onSave,
}: Props) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="w-full !max-w-4xl h-[90vh] flex flex-col p-0 overflow-hidden">
<DialogHeader className="px-6 py-5 border-b bg-background sticky top-0 z-20">
<DialogTitle className="text-2xl font-bold">
{editingBanner ? 'Edit Homepage Banner' : 'Create Homepage Banner'}
</DialogTitle>
</DialogHeader>
<div className="flex-1 overflow-y-auto p-6 space-y-8">
<div className="space-y-5">
<div className="border-b pb-2">
<h3 className="text-lg font-bold">Media Configuration</h3>
<p className="text-sm text-muted-foreground">Manage your banner files for desktop and mobile layouts</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label className="font-semibold">Media Type</Label>
<Select
value={bannerForm.mediaType}
onValueChange={(v) =>
setBannerForm({
...bannerForm,
mediaType: v,
})
}
>
<SelectTrigger>
<SelectValue placeholder="Select media type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="IMAGE">Image Asset</SelectItem>
<SelectItem value="VIDEO">Video Loop</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center justify-between border rounded-xl p-4 bg-muted/30">
<div>
<p className="font-semibold">Active Visibility</p>
<p className="text-sm text-muted-foreground">Publish this banner live on the homepage</p>
</div>
<Switch
checked={bannerForm.isActive}
onCheckedChange={(val) =>
setBannerForm({
...bannerForm,
isActive: val,
})
}
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label className="font-semibold">Desktop Media URL</Label>
<p className="text-xs text-muted-foreground">
{bannerForm.mediaType === 'VIDEO'
? 'Recommended: 1920 × 650 MP4 Format '
: 'Recommended: 1920 × 650 Widescreen'}
</p>
<BytescaleUploader
value={bannerForm.desktopMediaUrl || ''}
folderPath="/homepage-banners"
onChange={(url) =>
setBannerForm({
...bannerForm,
desktopMediaUrl: url,
})
}
/>
</div>
<div className="space-y-2">
<Label className="font-semibold">Mobile Media URL (Optional)</Label>
<p className="text-xs text-muted-foreground">
{bannerForm.mediaType === 'VIDEO' ? 'Recommended: 340 × 390 MP4 Format' : 'Recommended: 340 × 390 '}
</p>
<BytescaleUploader
value={bannerForm.mobileMediaUrl || ''}
folderPath="/homepage-banners"
onChange={(url) =>
setBannerForm({
...bannerForm,
mobileMediaUrl: url,
})
}
/>
</div>
</div>
</div>
<div className="space-y-5">
<div className="border-b pb-2">
<h3 className="text-lg font-bold">Banner Copy & Styles</h3>
<p className="text-sm text-muted-foreground">Modify text details and text alignment configurations</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="font-semibold">Main Heading / Title</Label>
<Input
value={bannerForm.title || ''}
placeholder="e.g., Advanced Healthcare, Exceptional Compassion"
onChange={(e) =>
setBannerForm({
...bannerForm,
title: e.target.value,
})
}
/>
</div>
<div className="space-y-2">
<Label className="font-semibold">Sub-Heading / Subtitle</Label>
<Input
value={bannerForm.subtitle || ''}
placeholder="e.g., Book appointments online with top multi-specialty doctors."
onChange={(e) =>
setBannerForm({
...bannerForm,
subtitle: e.target.value,
})
}
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="font-semibold">Text Alignment Alignment</Label>
<Select
value={bannerForm.textAlignment || 'left'}
onValueChange={(v) =>
setBannerForm({
...bannerForm,
textAlignment: v,
})
}
>
<SelectTrigger>
<SelectValue placeholder="Select text position" />
</SelectTrigger>
<SelectContent>
<SelectItem value="left">Left Aligned</SelectItem>
<SelectItem value="center">Center Aligned</SelectItem>
<SelectItem value="right">Right Aligned</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label className="font-semibold">Display Priority Order</Label>
<Input
type="number"
value={bannerForm.sortOrder}
onChange={(e) =>
setBannerForm({
...bannerForm,
sortOrder: Number(e.target.value),
})
}
/>
</div>
</div>
</div>
<div className="space-y-5">
<div className="border-b pb-2">
<h3 className="text-lg font-bold">Call To Action (CTA Button)</h3>
<p className="text-sm text-muted-foreground">Hyperlinks for optional button element overlays</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="font-semibold">Button Label</Label>
<Input
value={bannerForm.buttonText || ''}
placeholder="e.g., Find a Doctor"
onChange={(e) =>
setBannerForm({
...bannerForm,
buttonText: e.target.value,
})
}
/>
</div>
<div className="space-y-2">
<Label className="font-semibold">Button Redirect Link</Label>
<Input
value={bannerForm.buttonLink || ''}
placeholder="e.g., /doctors or https://..."
onChange={(e) =>
setBannerForm({
...bannerForm,
buttonLink: e.target.value,
})
}
/>
</div>
</div>
<div className="flex items-center justify-between border rounded-xl p-4 bg-muted/30">
<div>
<p className="font-semibold">Target Tab Redirection</p>
<p className="text-sm text-muted-foreground">
Force-launch the button link target into a completely new browser tab window
</p>
</div>
<Switch
checked={bannerForm.openInNewTab}
onCheckedChange={(val) =>
setBannerForm({
...bannerForm,
openInNewTab: val,
})
}
/>
</div>
</div>
</div>
<DialogFooter className="p-6 border-t bg-background sticky bottom-0 z-20">
<Button variant="ghost" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button className="px-10" onClick={onSave}>
{editingBanner ? 'Save Changes' : 'Create Banner'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
@@ -1,68 +1,55 @@
import { useState, useRef } from 'react'; import {useState, useRef} from "react";
import { Button } from '@/components/ui/button'; import {Button} from "@/components/ui/button";
import { User, X, Loader2, Video } from 'lucide-react'; import {User, X, Loader2} from "lucide-react";
import axios from 'axios'; import axios from "axios";
interface BytescaleUploaderProps { interface BytescaleUploaderProps {
value: string; value: string;
onChange: (url: string) => void; onChange: (url: string) => void;
folderPath: folderPath: "/doctors" | "/departments" | "/news" | "/blog";
| '/health-packages'
| '/seo'
| '/doctors'
| '/departments'
| '/news'
| '/blog'
| '/doctor-og'
| '/homepage-banners';
} }
export function BytescaleUploader({ value, onChange, folderPath }: BytescaleUploaderProps) { export function BytescaleUploader({
value,
onChange,
folderPath,
}: BytescaleUploaderProps) {
const baseURL = import.meta.env.VITE_API_URL; const baseURL = import.meta.env.VITE_API_URL;
const [isUploading, setIsUploading] = useState(false); const [isUploading, setIsUploading] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const isVideo = (url: string) => {
return /\.(mp4|webm|ogg)$/i.test(url);
};
const onFileSelected = async (event: React.ChangeEvent<HTMLInputElement>) => { const onFileSelected = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]; const file = event.target.files?.[0];
if (!file) return; if (!file) return;
const maxSize = file.type.startsWith('video/') ? 10 * 1024 * 1024 : 5 * 1024 * 1024; if (file.size > 5 * 1024 * 1024) {
alert("File is too large (Max 5MB)");
if (file.size > maxSize) {
alert(file.type.startsWith('video/') ? 'Video is too large (Max 10MB)' : 'Image is too large (Max 5MB)');
return; return;
} }
setIsUploading(true); setIsUploading(true);
const formData = new FormData(); const formData = new FormData();
formData.append('file', file); formData.append("file", file);
formData.append('folderPath', folderPath); formData.append("folderPath", folderPath);
try { try {
const response = await axios.post(`${baseURL}/upload`, formData, { const response = await axios.post(`${baseURL}/upload`, formData, {
headers: { headers: {
'Content-Type': 'multipart/form-data', "Content-Type": "multipart/form-data",
}, },
}); });
const { fileUrl } = response.data; const {fileUrl} = response.data;
onChange(fileUrl); onChange(fileUrl);
} catch (e: any) { } catch (e: any) {
console.error('Upload Error:', e); console.error("Upload Error:", e);
const errorMessage = e.response?.data?.error || e.message || 'Upload failed'; const errorMessage =
e.response?.data?.error || e.message || "Upload failed";
alert(`Upload Error: ${errorMessage}`); alert(`Upload Error: ${errorMessage}`);
} finally { } finally {
setIsUploading(false); setIsUploading(false);
if (fileInputRef.current) fileInputRef.current.value = "";
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
} }
}; };
@@ -72,19 +59,14 @@ export function BytescaleUploader({ value, onChange, folderPath }: BytescaleUplo
<div className="relative"> <div className="relative">
{value ? ( {value ? (
<> <>
{isVideo(value) ? (
<video src={value} className="w-20 h-20 rounded-md object-cover border-2 border-primary/20" controls />
) : (
<img <img
src={value} src={value}
className="w-16 h-16 rounded-full object-cover border-2 border-primary/20" className="w-16 h-16 rounded-full object-cover border-2 border-primary/20"
alt="Preview" alt="Preview"
/> />
)}
<button <button
type="button" type="button"
onClick={() => onChange('')} onClick={() => onChange("")}
className="absolute -top-1 -right-1 bg-destructive text-white rounded-full p-0.5 shadow-sm hover:scale-110 transition-transform" className="absolute -top-1 -right-1 bg-destructive text-white rounded-full p-0.5 shadow-sm hover:scale-110 transition-transform"
> >
<X className="w-3 h-3" /> <X className="w-3 h-3" />
@@ -105,7 +87,7 @@ export function BytescaleUploader({ value, onChange, folderPath }: BytescaleUplo
type="file" type="file"
ref={fileInputRef} ref={fileInputRef}
onChange={onFileSelected} onChange={onFileSelected}
accept="image/jpeg,image/png,image/webp,video/mp4,video/webm,video/ogg" accept="image/jpeg,image/png,image/webp"
className="hidden" className="hidden"
/> />
@@ -122,16 +104,17 @@ export function BytescaleUploader({ value, onChange, folderPath }: BytescaleUplo
Uploading... Uploading...
</> </>
) : value ? ( ) : value ? (
'Change File' "Change Photo"
) : ( ) : (
'Upload Image / Video' "Upload Photo"
)} )}
</Button> </Button>
</div> </div>
{value && ( {value && (
<p className="text-xs text-amber-600 pl-[72px]"> <p className="text-xs text-amber-600 pl-[72px]">
Make sure to save the changes by clicking the "Save Changes" button. Make sure to save the changes by clicking the "Save Changes"
button.
</p> </p>
)} )}
</div> </div>
@@ -1,379 +0,0 @@
import { BytescaleUploader } from '@/components/BytescaleUploader/BytescaleUploader';
import SeoFields from '@/components/SeoFields/SeoFields';
import { useEffect } from 'react';
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Badge } from '@/components/ui/badge';
import { Switch } from '@/components/ui/switch';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion';
import { Plus, Trash2 } from 'lucide-react';
interface Props {
open: boolean;
onOpenChange: (open: boolean) => void;
editingPackage: any;
pkgForm: any;
setPkgForm: any;
inclusionsList: any[];
setInclusionsList: any;
categories: any[];
onSave: () => void;
}
export default function HealthPackageModal({
open,
onOpenChange,
editingPackage,
pkgForm,
setPkgForm,
inclusionsList,
setInclusionsList,
categories,
onSave,
}: Props) {
useEffect(() => {
if (!editingPackage && pkgForm.name) {
setPkgForm((prev: any) => ({
...prev,
slug: prev.slug
? prev.slug
: pkgForm.name
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, ''),
}));
}
}, [pkgForm.name]);
const handleAddInclusionField = () => {
setInclusionsList([
...inclusionsList,
{
id: Date.now(),
category: '',
items: '',
},
]);
};
const handleRemoveInclusionField = (id: number) => {
setInclusionsList(inclusionsList.filter((item) => item.id !== id));
};
const handleUpdateInclusionField = (id: number, field: string, value: string) => {
setInclusionsList(
inclusionsList.map((item) =>
item.id === id
? {
...item,
[field]: value,
}
: item
)
);
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="w-full !max-w-7xl h-[92vh] flex flex-col p-0 overflow-hidden">
<DialogHeader className="px-6 py-5 border-b bg-background sticky top-0 z-20">
<DialogTitle className="text-2xl font-bold">
{editingPackage ? 'Edit Health Package' : 'Create Health Package'}
</DialogTitle>
</DialogHeader>
<div className="flex-1 overflow-y-auto">
<div className="grid grid-cols-1 xl:grid-cols-[1.2fr_0.8fr] gap-8 p-6">
{/* LEFT COLUMN */}
<div className="space-y-8">
<div className="space-y-5">
<div className="sticky top-0 bg-background z-10 pb-2">
<h3 className="text-lg font-bold">Profile & Pricing</h3>
<p className="text-sm text-muted-foreground">Main package information</p>
</div>
<div className="space-y-5">
<div className="space-y-2">
<Label className="font-semibold">Package Image</Label>
<p className="text-xs text-muted-foreground">Recommended size: 650 × 250</p>
<BytescaleUploader
value={pkgForm.image || ''}
folderPath="/health-packages"
onChange={(url) =>
setPkgForm({
...pkgForm,
image: url,
})
}
/>
</div>
<div className="flex items-center justify-between border rounded-xl p-4 bg-muted/30">
<div>
<p className="font-semibold">Active Visibility</p>
<p className="text-sm text-muted-foreground">Show this package publicly</p>
</div>
<Switch
checked={pkgForm.isActive}
onCheckedChange={(val) =>
setPkgForm({
...pkgForm,
isActive: val,
})
}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="font-semibold">Package Name</Label>
<Input
value={pkgForm.name}
onChange={(e) =>
setPkgForm({
...pkgForm,
name: e.target.value,
})
}
/>
</div>
<div className="space-y-2">
<Label className="font-semibold">URL Slug</Label>
<Input
value={pkgForm.slug}
onChange={(e) =>
setPkgForm({
...pkgForm,
slug: e.target.value,
})
}
/>
</div>
</div>
<div className="space-y-2">
<Label className="font-semibold">Category</Label>
<Select
value={pkgForm.categoryId?.toString()}
onValueChange={(v) =>
setPkgForm({
...pkgForm,
categoryId: Number(v),
})
}
>
<SelectTrigger>
<SelectValue placeholder="Select category" />
</SelectTrigger>
<SelectContent>
{categories.map((c) => (
<SelectItem key={c.id} value={c.id.toString()}>
{c.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2">
<Label className="font-semibold">Regular Price ()</Label>
<Input
type="number"
value={pkgForm.price || ''}
onChange={(e) => {
const value = e.target.value ? Number(e.target.value) : undefined;
setPkgForm({
...pkgForm,
price: value,
});
}}
/>
</div>
<div className="space-y-2">
<Label className="font-semibold">Discounted Price ()</Label>
<Input
type="number"
disabled={!pkgForm.price}
value={pkgForm.discountedPrice || ''}
onChange={(e) =>
setPkgForm({
...pkgForm,
discountedPrice: e.target.value ? Number(e.target.value) : undefined,
})
}
/>
</div>
<div className="space-y-2">
<Label className="font-semibold">Sort Priority</Label>
<Input
type="number"
value={pkgForm.sortOrder}
onChange={(e) =>
setPkgForm({
...pkgForm,
sortOrder: Number(e.target.value),
})
}
/>
</div>
</div>
<div className="space-y-2">
<Label className="font-semibold">Description</Label>
<Textarea
rows={5}
value={pkgForm.description}
onChange={(e) =>
setPkgForm({
...pkgForm,
description: e.target.value,
})
}
/>
</div>
</div>
</div>
{/* INCLUSIONS */}
<div className="space-y-5">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-bold">Tests & Inclusions</h3>
<p className="text-sm text-muted-foreground">Group tests into categories</p>
</div>
<Badge variant="outline">{inclusionsList.length} Groups</Badge>
</div>
<Accordion type="multiple" className="space-y-4">
{inclusionsList.map((inc, index) => {
const testCount = inc.items?.split(',').filter(Boolean).length;
return (
<AccordionItem
key={inc.id}
value={inc.id.toString()}
className="border rounded-xl bg-background px-5 shadow-sm"
>
<AccordionTrigger className="hover:no-underline w-full">
<div className="flex w-full items-center justify-between">
<div className="flex flex-col items-start text-left">
<p className="font-semibold">{inc.category || `Group ${index + 1}`}</p>
<p className="text-xs text-muted-foreground">{testCount || 0} tests included</p>
</div>
<Button
variant="ghost"
size="sm"
className="text-red-500 hover:text-red-600"
onClick={() => handleRemoveInclusionField(inc.id)}
>
<Trash2 className="h-4 w-4 mr-1" />
Remove
</Button>
</div>
</AccordionTrigger>
<AccordionContent className="pt-4">
<div className="space-y-4">
<div className="space-y-2">
<Label>Category Title</Label>
<Input
placeholder="Routine Blood Tests"
value={inc.category}
onChange={(e) => handleUpdateInclusionField(inc.id, 'category', e.target.value)}
/>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label>Included Tests</Label>
</div>
<Textarea
rows={4}
placeholder="CBC, LFT, RFT, TSH"
value={inc.items}
onChange={(e) => handleUpdateInclusionField(inc.id, 'items', e.target.value)}
/>
<p className="text-xs text-muted-foreground">Separate each test using commas</p>
</div>
</div>
</AccordionContent>
</AccordionItem>
);
})}
</Accordion>
<Button
variant="outline"
className="w-full border-dashed border-2 h-12"
onClick={handleAddInclusionField}
>
<Plus className="h-4 w-4 mr-2" />
Add New Inclusion Group
</Button>
</div>
</div>
{/* RIGHT COLUMN */}
<div className="space-y-6">
<SeoFields
value={pkgForm.seo}
slug={pkgForm.slug}
folderPath="/seo"
onChange={(seo) =>
setPkgForm({
...pkgForm,
seo,
})
}
/>
</div>
</div>
</div>
<DialogFooter className="p-6 border-t bg-background sticky bottom-0 z-20">
<Button variant="ghost" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button className="px-10" onClick={onSave}>
{editingPackage ? 'Save Changes' : 'Create Package'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
@@ -1,219 +0,0 @@
import { useState, useEffect, useCallback } from 'react';
import { getAllInquiriesApi, HealthInquiry } from '@/api/healthCheck';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { Loader2, RefreshCw, ChevronLeft, ChevronRight } from 'lucide-react';
export default function PackageInquiriesTab() {
const [inquiries, setInquiries] = useState<HealthInquiry[]>([]);
const [loading, setLoading] = useState(true);
const [filterDate, setFilterDate] = useState('');
const [startDate, setStartDate] = useState('');
const [endDate, setEndDate] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(10);
const [totalItems, setTotalItems] = useState(0);
const [totalPages, setTotalPages] = useState(1);
const fetchInquiries = useCallback(async () => {
setLoading(true);
try {
const res = await getAllInquiriesApi(currentPage, itemsPerPage, filterDate, startDate, endDate);
setInquiries(res.data || []);
setTotalItems(res.pagination?.total || 0);
setTotalPages(res.pagination?.totalPages || 1);
} catch (err) {
console.error('Failed to fetch inquiries', err);
} finally {
setLoading(false);
}
}, [currentPage, itemsPerPage, filterDate, startDate, endDate]);
useEffect(() => {
fetchInquiries();
}, [fetchInquiries]);
const handleFilterChange = (setter: React.Dispatch<React.SetStateAction<string>>, value: string) => {
setter(value);
setCurrentPage(1);
};
const indexOfFirstItem = (currentPage - 1) * itemsPerPage;
const indexOfLastItem = Math.min(currentPage * itemsPerPage, totalItems);
return (
<Card>
<CardHeader className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4">
<CardTitle className="text-xl">Package Inquiries</CardTitle>
<div className="flex flex-wrap items-end gap-3">
<div className="flex flex-col gap-1">
<label className="text-xs font-medium text-muted-foreground">Specific Date</label>
<Input
type="date"
value={filterDate}
onChange={(e) => handleFilterChange(setFilterDate, e.target.value)}
className="w-[140px] text-sm"
disabled={!!startDate || !!endDate}
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-xs font-medium text-muted-foreground">From</label>
<Input
type="date"
value={startDate}
onChange={(e) => handleFilterChange(setStartDate, e.target.value)}
className="w-[140px] text-sm"
disabled={!!filterDate}
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-xs font-medium text-muted-foreground">To</label>
<Input
type="date"
value={endDate}
onChange={(e) => handleFilterChange(setEndDate, e.target.value)}
className="w-[140px] text-sm"
disabled={!!filterDate}
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-xs font-medium text-muted-foreground">Rows</label>
<select
value={itemsPerPage}
onChange={(e) => {
setItemsPerPage(Number(e.target.value));
setCurrentPage(1);
}}
className="flex h-10 rounded-md border border-input bg-background px-3 py-2 text-sm focus:ring-2 focus:ring-primary"
>
<option value={5}>5 / page</option>
<option value={10}>10 / page</option>
<option value={20}>20 / page</option>
<option value={50}>50 / page</option>
</select>
</div>
<Button variant="outline" onClick={fetchInquiries} disabled={loading}>
<RefreshCw className={`mr-2 h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
Refresh
</Button>
</div>
</CardHeader>
<CardContent className="p-0 sm:p-6 sm:pt-0">
<div className="rounded-md border overflow-x-auto overflow-y-auto max-h-[650px] relative">
<Table className="w-full min-w-[1000px] table-fixed border-separate border-spacing-0">
<TableHeader className="sticky top-0 z-20 bg-background shadow-sm">
<TableRow>
<TableHead className="w-[150px] font-bold bg-background">Requested Date</TableHead>
<TableHead className="w-[220px] font-bold bg-background">Patient Details</TableHead>
<TableHead className="w-[250px] font-bold bg-background">Requested Package</TableHead>
<TableHead className="w-[120px] font-bold bg-background">Age/Gender</TableHead>
<TableHead className="w-[250px] font-bold bg-background">Message</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={5} className="text-center py-10">
<Loader2 className="h-8 w-8 animate-spin mx-auto" />
</TableCell>
</TableRow>
) : inquiries.length === 0 ? (
<TableRow>
<TableCell colSpan={5} className="text-center text-muted-foreground py-10">
No inquiries found for the selected criteria
</TableCell>
</TableRow>
) : (
inquiries.map((inq) => (
<TableRow key={inq.id} className="hover:bg-muted/50">
<TableCell>
<div className="font-semibold text-primary">
{new Date(inq.preferredDate).toLocaleDateString()}
</div>
<div className="text-[11px] text-muted-foreground mt-1">
Submitted: {new Date(inq.createdAt).toLocaleDateString()}
</div>
</TableCell>
<TableCell>
<div className="font-semibold text-base">{inq.fullName}</div>
<div className="text-sm">{inq.mobileNumber}</div>
<div className="text-xs text-muted-foreground">{inq.email || '-'}</div>
</TableCell>
<TableCell>
<div className="font-semibold text-sm truncate">{inq.healthPackage?.name || 'N/A'}</div>
</TableCell>
<TableCell>
<div className="font-medium">
{inq.age} yrs / {inq.gender}
</div>
</TableCell>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="text-sm italic line-clamp-3 text-muted-foreground whitespace-pre-wrap cursor-pointer">
{inq.message || 'No message provided.'}
</div>
</TooltipTrigger>
<TooltipContent className="max-w-md whitespace-pre-wrap">
{inq.message || 'No message provided.'}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
{!loading && totalItems > 0 && (
<div className="flex flex-col sm:flex-row items-center justify-between px-2 py-4 border-t gap-4 mt-2">
<div className="text-sm text-muted-foreground">
Showing <span className="font-semibold">{indexOfFirstItem + 1}</span> to{' '}
<span className="font-semibold">{indexOfLastItem}</span> of{' '}
<span className="font-semibold">{totalItems}</span> inquiries
</div>
<div className="flex items-center gap-6">
<div className="text-sm font-semibold">
Page {currentPage} of {totalPages || 1}
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="icon"
className="h-9 w-9"
onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
disabled={currentPage === 1}
>
<ChevronLeft className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="icon"
className="h-9 w-9"
onClick={() => setCurrentPage((prev) => Math.min(prev + 1, totalPages))}
disabled={currentPage === totalPages || totalPages === 0}
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
</div>
)}
</CardContent>
</Card>
);
}
@@ -1,8 +1,8 @@
import { Navigate } from 'react-router-dom'; import {Navigate} from "react-router-dom";
import { useAuth } from '@/context/AuthContext'; import {useAuth} from "@/context/AuthContext";
export default function ProtectedRoute({ children }: any) { export default function ProtectedRoute({children}: any) {
const { token } = useAuth(); const {token} = useAuth();
if (!token) { if (!token) {
return <Navigate to="/" />; return <Navigate to="/" />;
@@ -1,186 +0,0 @@
import { BytescaleUploader } from '@/components/BytescaleUploader/BytescaleUploader';
import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { X } from 'lucide-react';
interface SeoData {
seoTitle?: string;
metaDescription?: string;
focusKeyphrase?: string;
tags?: string[];
ogTitle?: string;
ogDescription?: string;
ogImage?: string;
}
interface SeoFieldsProps {
value?: SeoData;
onChange: (seo: SeoData) => void;
slug?: string;
folderPath?: '/seo';
}
export default function SeoFields({ value, onChange, slug, folderPath = '/seo' }: SeoFieldsProps) {
const seo = value || {};
const updateSeo = (field: keyof SeoData, fieldValue: any) => {
onChange({
...seo,
[field]: fieldValue,
});
};
const removeTag = (index: number) => {
updateSeo(
'tags',
(seo.tags || []).filter((_, i) => i !== index)
);
};
return (
<div className="space-y-5 p-5 border rounded-xl bg-muted/20">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-bold">SEO Settings</h3>
<p className="text-sm text-muted-foreground">Optimize for Google & social sharing</p>
</div>
<Badge variant="secondary">Optional</Badge>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-sm font-semibold">SEO Title</Label>
<span className="text-xs text-muted-foreground">{seo.seoTitle?.length || 0}/60</span>
</div>
<Input
placeholder="Best Health Checkup Package in Kochi"
value={seo.seoTitle || ''}
onChange={(e) => updateSeo('seoTitle', e.target.value)}
/>
<p className="text-xs text-muted-foreground">Recommended: 5060 characters</p>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-sm font-semibold">Meta Description</Label>
<span className="text-xs text-muted-foreground">{seo.metaDescription?.length || 0}/160</span>
</div>
<Textarea
rows={4}
placeholder="Short description shown in Google search results"
value={seo.metaDescription || ''}
onChange={(e) => updateSeo('metaDescription', e.target.value)}
/>
<p className="text-xs text-muted-foreground">Recommended: 150160 characters</p>
</div>
<div className="space-y-2">
<Label className="text-sm font-semibold">Focus Keyphrase</Label>
<Input
placeholder="health checkup package kochi"
value={seo.focusKeyphrase || ''}
onChange={(e) => updateSeo('focusKeyphrase', e.target.value)}
/>
</div>
<div className="space-y-2">
<Label className="text-sm font-semibold">Tags / Keywords</Label>
<div className="flex flex-wrap gap-2 border rounded-md p-3 min-h-[48px] bg-background">
{(seo.tags || []).map((tag, index) => (
<div
key={index}
className="bg-primary/10 text-primary px-3 py-1 rounded-full text-sm flex items-center gap-2"
>
<span>{tag}</span>
<button type="button" onClick={() => removeTag(index)} className="hover:text-red-500 transition-colors">
<X className="h-3 w-3" />
</button>
</div>
))}
<Input
placeholder="Type keyword and press Enter"
className="border-0 shadow-none focus-visible:ring-0 min-w-[220px] flex-1"
onKeyDown={(e) => {
if (e.key === 'Enter' && e.currentTarget.value.trim()) {
e.preventDefault();
const newTag = e.currentTarget.value.trim();
if (!(seo.tags || []).includes(newTag)) {
updateSeo('tags', [...(seo.tags || []), newTag]);
}
e.currentTarget.value = '';
}
}}
/>
</div>
<p className="text-xs text-muted-foreground">Press Enter to add tags</p>
</div>
<div className="border-t pt-5 space-y-5">
<div className="flex items-center justify-between">
<div>
<h4 className="font-bold">Open Graph (Social Preview)</h4>
<p className="text-sm text-muted-foreground">Facebook, WhatsApp & Twitter sharing</p>
</div>
<Badge variant="secondary">Optional</Badge>
</div>
<div className="space-y-2">
<Label className="text-sm font-semibold">OG Title</Label>
<Input
placeholder="Title for social sharing"
value={seo.ogTitle || ''}
onChange={(e) => updateSeo('ogTitle', e.target.value)}
/>
<p className="text-xs text-muted-foreground">If empty, SEO title will be used</p>
</div>
<div className="space-y-2">
<Label className="text-sm font-semibold">OG Description</Label>
<Textarea
rows={4}
placeholder="Description for social sharing"
value={seo.ogDescription || ''}
onChange={(e) => updateSeo('ogDescription', e.target.value)}
/>
<p className="text-xs text-muted-foreground">If empty, meta description will be used</p>
</div>
<div className="space-y-2">
<Label className="text-sm font-semibold">OG Image</Label>
<BytescaleUploader
value={seo.ogImage || ''}
folderPath={folderPath}
onChange={(url) => updateSeo('ogImage', url)}
/>
</div>
</div>
</div>
);
}
@@ -1,113 +0,0 @@
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
interface SeoPreviewData {
seo?: {
ogImage?: string;
ogTitle?: string;
seoTitle?: string;
ogDescription?: string;
metaDescription?: string;
slug?: string;
};
doctorId?: string;
name?: string;
}
interface SeoPreviewProps {
open: boolean;
onOpenChange: (open: boolean) => void;
previewData?: SeoPreviewData | null;
url?: string;
title?: string;
}
export default function SeoPreview({ open, onOpenChange, previewData, url, title = 'SEO Preview' }: SeoPreviewProps) {
const previewUrl = url || 'https://www.gg-hospital.com';
const hasSeoData =
!!previewData?.seo &&
!!(
previewData.seo.ogImage ||
previewData.seo.ogTitle ||
previewData.seo.seoTitle ||
previewData.seo.ogDescription ||
previewData.seo.metaDescription
);
const imageUrl = previewData?.seo?.ogImage || 'https://placehold.co/1200x630?text=GG+Hospital';
const ogTitle = previewData?.seo?.ogTitle || previewData?.seo?.seoTitle || 'GG Hospital';
const ogDescription =
previewData?.seo?.ogDescription || previewData?.seo?.metaDescription || 'No description available';
const searchTitle = previewData?.seo?.seoTitle || previewData?.seo?.ogTitle || 'SEO title preview';
const searchDescription =
previewData?.seo?.metaDescription || previewData?.seo?.ogDescription || 'No meta description available';
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:!max-w-4xl overflow-hidden">
<DialogHeader>
<DialogTitle className="text-xl">{title}</DialogTitle>
</DialogHeader>
{hasSeoData ? (
<div className="space-y-10 py-2">
{/* Social Preview */}
<div>
<p className="mb-4 text-sm font-semibold text-muted-foreground">
Social Media Preview (WhatsApp / Facebook)
</p>
<a
href={previewUrl}
target="_blank"
rel="noopener noreferrer"
className="block max-w-[560px] overflow-hidden rounded-xl border bg-white shadow-sm transition hover:shadow-md"
>
<div className="aspect-[1.91/1] overflow-hidden bg-muted">
<img src={imageUrl} alt="OG Preview" className="h-full w-full object-cover" />
</div>
<div className="border-t bg-[#f0f2f5] px-4 py-3">
<h3 className="mt-1 line-clamp-2 text-[18px] font-semibold leading-snug text-[#1c1e21]">{ogTitle}</h3>
<p className="mt-1 line-clamp-2 text-[14px] text-[#65676b]">{ogDescription}</p>
<p className="mt-1 truncate text-[11px] tracking-wide text-[#65676b]">{previewUrl}</p>
</div>
</a>
</div>
{/* Google Preview */}
<div>
<p className="mb-4 text-sm font-semibold text-muted-foreground">Google Search Preview</p>
<div className="rounded-xl border bg-white p-6">
<a href={previewUrl} target="_blank" rel="noopener noreferrer" className="block">
<p className="truncate text-[14px] text-[#202124] hover:underline">{previewUrl}</p>
<h3 className="mt-1 text-[22px] leading-tight text-[#1a0dab] hover:underline">{searchTitle}</h3>
</a>
<p className="mt-2 line-clamp-3 text-[14px] leading-6 text-[#4d5156]">{searchDescription}</p>
</div>
</div>
</div>
) : (
<div className="flex items-center justify-center py-16 text-sm text-muted-foreground">
No preview data available.
</div>
)}
<DialogFooter className="mt-0 border-t bg-background p-6">
<Button variant="outline" onClick={() => onOpenChange(false)}>
Close
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
+8 -8
View File
@@ -1,16 +1,16 @@
import { useState, useEffect } from 'react'; import {useState, useEffect} from "react";
import { useAuth } from '@/context/AuthContext'; import {useAuth} from "@/context/AuthContext";
import { Button } from '@/components/ui/button'; import {Button} from "@/components/ui/button";
import { Switch } from '@/components/ui/switch'; import {Switch} from "@/components/ui/switch";
import { log } from 'console'; import {log} from "console";
export default function Header() { export default function Header() {
const { user, logout } = useAuth(); const {user, logout} = useAuth();
const [darkMode, setDarkMode] = useState<boolean>(false); const [darkMode, setDarkMode] = useState<boolean>(false);
useEffect(() => { useEffect(() => {
if (darkMode) document.documentElement.classList.add('dark'); if (darkMode) document.documentElement.classList.add("dark");
else document.documentElement.classList.remove('dark'); else document.documentElement.classList.remove("dark");
}, [darkMode]); }, [darkMode]);
return ( return (
+26 -68
View File
@@ -1,66 +1,51 @@
import { Link, useLocation } from 'react-router-dom'; import { Link, useLocation } from "react-router-dom";
import { Button } from '@/components/ui/button'; import { Button } from "@/components/ui/button";
import { Separator } from '@/components/ui/separator'; import { Separator } from "@/components/ui/separator";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import { ChevronDown } from 'lucide-react';
export default function Sidebar() { export default function Sidebar() {
const location = useLocation(); const location = useLocation();
const navItems = [ const navItems = [
{ {
name: 'Department', name: "Department",
path: '/department', path: "/department",
}, },
{ {
name: 'Doctor', name: "Doctor",
path: '/doctor', path: "/doctor",
}, },
{ {
name: 'Health Check', name: "Appointments",
path: '/health-check', path: "/appointment",
}, },
{ {
name: 'Appointments', name: "Career",
path: '/appointment', path: "/career",
}, },
{ {
name: 'Career', name: "Candidates",
path: '/career', path: "/candidate",
}, },
{ {
name: 'Candidates', name: "Inquiry",
path: '/candidate', path: "/inquiry",
}, },
{ {
name: 'Inquiry', name: "Academics & Research",
path: '/inquiry', path: "/academics",
}, },
{ {
name: 'Academics & Research', name: "News & Media",
path: '/academics', path: "/news",
}, },
{ {
name: 'News & Media', name: "Email",
path: '/news', path: "/email",
}, },
{ {
name: 'Email', name: "Blog",
path: '/email', path: "/blog",
},
{
name: 'Blog',
path: '/blog',
},
{
name: 'Homepage Content',
children: [
{
name: 'Homepage Banner',
path: '/homepage-banner',
},
],
}, },
]; ];
@@ -74,40 +59,13 @@ export default function Sidebar() {
<nav className="p-4 space-y-2"> <nav className="p-4 space-y-2">
{navItems.map((item) => { {navItems.map((item) => {
if ('children' in item) {
const hasActiveChild = item.children.some((child) => location.pathname === child.path);
return (
<Collapsible key={item.name} defaultOpen={hasActiveChild}>
<CollapsibleTrigger asChild>
<Button variant={hasActiveChild ? 'secondary' : 'ghost'} className="w-full justify-between">
<span>{item.name}</span>
<ChevronDown className="h-4 w-4" />
</Button>
</CollapsibleTrigger>
<CollapsibleContent className="mt-1 space-y-1">
{item.children.map((child) => {
const active = location.pathname === child.path;
return (
<Link key={child.path} to={child.path}>
<Button variant={active ? 'secondary' : 'ghost'} className="w-full justify-start pl-8">
{child.name}
</Button>
</Link>
);
})}
</CollapsibleContent>
</Collapsible>
);
}
const active = location.pathname === item.path; const active = location.pathname === item.path;
return ( return (
<Link key={item.path} to={item.path}> <Link key={item.path} to={item.path}>
<Button variant={active ? 'secondary' : 'ghost'} className="w-full justify-start"> <Button
variant={active ? "secondary" : "ghost"}
className="w-full justify-start">
{item.name} {item.name}
</Button> </Button>
</Link> </Link>
-61
View File
@@ -1,61 +0,0 @@
import * as React from 'react';
import { Accordion as AccordionPrimitive } from 'radix-ui';
import { cn } from '@/lib/utils';
import { ChevronDownIcon, ChevronUpIcon } from 'lucide-react';
function Accordion({ className, ...props }: React.ComponentProps<typeof AccordionPrimitive.Root>) {
return <AccordionPrimitive.Root data-slot="accordion" className={cn('flex w-full flex-col', className)} {...props} />;
}
function AccordionItem({ className, ...props }: React.ComponentProps<typeof AccordionPrimitive.Item>) {
return (
<AccordionPrimitive.Item data-slot="accordion-item" className={cn('not-last:border-b', className)} {...props} />
);
}
function AccordionTrigger({ className, children, ...props }: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
return (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
className={cn(
'group/accordion-trigger relative flex flex-1 items-start justify-between rounded-lg border border-transparent py-2.5 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:after:border-ring disabled:pointer-events-none disabled:opacity-50 **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-4 **:data-[slot=accordion-trigger-icon]:text-muted-foreground',
className
)}
{...props}
>
{children}
<ChevronDownIcon
data-slot="accordion-trigger-icon"
className="pointer-events-none shrink-0 group-aria-expanded/accordion-trigger:hidden"
/>
<ChevronUpIcon
data-slot="accordion-trigger-icon"
className="pointer-events-none hidden shrink-0 group-aria-expanded/accordion-trigger:inline"
/>
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
);
}
function AccordionContent({ className, children, ...props }: React.ComponentProps<typeof AccordionPrimitive.Content>) {
return (
<AccordionPrimitive.Content
data-slot="accordion-content"
className="overflow-hidden text-sm data-open:animate-accordion-down data-closed:animate-accordion-up"
{...props}
>
<div
className={cn(
'h-(--radix-accordion-content-height) pt-0 pb-2.5 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4',
className
)}
>
{children}
</div>
</AccordionPrimitive.Content>
);
}
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
+28 -19
View File
@@ -1,40 +1,49 @@
import * as React from 'react'; import * as React from "react"
import { cva, type VariantProps } from 'class-variance-authority'; import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from 'radix-ui'; import { Slot } from "radix-ui"
import { cn } from '@/lib/utils'; import { cn } from "@/lib/utils"
const badgeVariants = cva( const badgeVariants = cva(
'group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!', "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
{ {
variants: { variants: {
variant: { variant: {
default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80', default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
secondary: 'bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80', secondary:
"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
destructive: destructive:
'bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20', "bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
outline: 'border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground', outline:
ghost: 'hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50', "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
link: 'text-primary underline-offset-4 hover:underline', ghost:
"hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
link: "text-primary underline-offset-4 hover:underline",
}, },
}, },
defaultVariants: { defaultVariants: {
variant: 'default', variant: "default",
}, },
} }
); )
function Badge({ function Badge({
className, className,
variant = 'default', variant = "default",
asChild = false, asChild = false,
...props ...props
}: React.ComponentProps<'span'> & VariantProps<typeof badgeVariants> & { asChild?: boolean }) { }: React.ComponentProps<"span"> &
const Comp = asChild ? Slot.Root : 'span'; VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot.Root : "span"
return ( return (
<Comp data-slot="badge" data-variant={variant} className={cn(badgeVariants({ variant }), className)} {...props} /> <Comp
); data-slot="badge"
data-variant={variant}
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
} }
export { Badge, badgeVariants }; export { Badge, badgeVariants }
+29 -27
View File
@@ -1,55 +1,57 @@
import * as React from 'react'; import * as React from "react"
import { cva, type VariantProps } from 'class-variance-authority'; import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from 'radix-ui'; import { Slot } from "radix-ui"
import { cn } from '@/lib/utils'; import { cn } from "@/lib/utils"
const buttonVariants = cva( const buttonVariants = cva(
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{ {
variants: { variants: {
variant: { variant: {
default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80', default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
outline: outline:
'border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50', "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
secondary: secondary:
'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground', "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
ghost: ghost:
'hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50', "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
destructive: destructive:
'bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40', "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
link: 'text-primary underline-offset-4 hover:underline', link: "text-primary underline-offset-4 hover:underline",
}, },
size: { size: {
default: 'h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2', default:
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3", xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5", sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
lg: 'h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2', lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
icon: 'size-8', icon: "size-8",
'icon-xs': "icon-xs":
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3", "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
'icon-sm': 'size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg', "icon-sm":
'icon-lg': 'size-9', "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
"icon-lg": "size-9",
}, },
}, },
defaultVariants: { defaultVariants: {
variant: 'default', variant: "default",
size: 'default', size: "default",
}, },
} }
); )
function Button({ function Button({
className, className,
variant = 'default', variant = "default",
size = 'default', size = "default",
asChild = false, asChild = false,
...props ...props
}: React.ComponentProps<'button'> & }: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & { VariantProps<typeof buttonVariants> & {
asChild?: boolean; asChild?: boolean
}) { }) {
const Comp = asChild ? Slot.Root : 'button'; const Comp = asChild ? Slot.Root : "button"
return ( return (
<Comp <Comp
@@ -59,7 +61,7 @@ function Button({
className={cn(buttonVariants({ variant, size, className }))} className={cn(buttonVariants({ variant, size, className }))}
{...props} {...props}
/> />
); )
} }
export { Button, buttonVariants }; export { Button, buttonVariants }
+55 -22
View File
@@ -1,70 +1,103 @@
import * as React from 'react'; import * as React from "react"
import { cn } from '@/lib/utils'; import { cn } from "@/lib/utils"
function Card({ className, size = 'default', ...props }: React.ComponentProps<'div'> & { size?: 'default' | 'sm' }) { function Card({
className,
size = "default",
...props
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
return ( return (
<div <div
data-slot="card" data-slot="card"
data-size={size} data-size={size}
className={cn( className={cn(
'group/card flex flex-col gap-4 overflow-hidden rounded-xl bg-card py-4 text-sm text-card-foreground ring-1 ring-foreground/10 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl', "group/card flex flex-col gap-4 overflow-hidden rounded-xl bg-card py-4 text-sm text-card-foreground ring-1 ring-foreground/10 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
className className
)} )}
{...props} {...props}
/> />
); )
} }
function CardHeader({ className, ...props }: React.ComponentProps<'div'>) { function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="card-header" data-slot="card-header"
className={cn( className={cn(
'group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3', "group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3",
className className
)} )}
{...props} {...props}
/> />
); )
} }
function CardTitle({ className, ...props }: React.ComponentProps<'div'>) { function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="card-title" data-slot="card-title"
className={cn('text-base leading-snug font-medium group-data-[size=sm]/card:text-sm', className)} className={cn(
"text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
className
)}
{...props} {...props}
/> />
); )
} }
function CardDescription({ className, ...props }: React.ComponentProps<'div'>) { function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="card-description" className={cn('text-sm text-muted-foreground', className)} {...props} />; return (
<div
data-slot="card-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
} }
function CardAction({ className, ...props }: React.ComponentProps<'div'>) { function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="card-action" data-slot="card-action"
className={cn('col-start-2 row-span-2 row-start-1 self-start justify-self-end', className)} className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props} {...props}
/> />
); )
} }
function CardContent({ className, ...props }: React.ComponentProps<'div'>) { function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="card-content" className={cn('px-4 group-data-[size=sm]/card:px-3', className)} {...props} />; return (
<div
data-slot="card-content"
className={cn("px-4 group-data-[size=sm]/card:px-3", className)}
{...props}
/>
)
} }
function CardFooter({ className, ...props }: React.ComponentProps<'div'>) { function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="card-footer" data-slot="card-footer"
className={cn('flex items-center rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/card:p-3', className)} className={cn(
"flex items-center rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/card:p-3",
className
)}
{...props} {...props}
/> />
); )
} }
export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent }; export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}
@@ -1,31 +0,0 @@
import { Collapsible as CollapsiblePrimitive } from "radix-ui"
function Collapsible({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
}
function CollapsibleTrigger({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
return (
<CollapsiblePrimitive.CollapsibleTrigger
data-slot="collapsible-trigger"
{...props}
/>
)
}
function CollapsibleContent({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
return (
<CollapsiblePrimitive.CollapsibleContent
data-slot="collapsible-content"
{...props}
/>
)
}
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
+95 -52
View File
@@ -1,36 +1,48 @@
import * as React from 'react'; import * as React from "react"
import { Command as CommandPrimitive } from 'cmdk'; import { Command as CommandPrimitive } from "cmdk"
import { cn } from '@/lib/utils'; import { cn } from "@/lib/utils"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import {
import { InputGroup, InputGroupAddon } from '@/components/ui/input-group'; Dialog,
import { SearchIcon, CheckIcon } from 'lucide-react'; DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import {
InputGroup,
InputGroupAddon,
} from "@/components/ui/input-group"
import { SearchIcon, CheckIcon } from "lucide-react"
function Command({ className, ...props }: React.ComponentProps<typeof CommandPrimitive>) { function Command({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive>) {
return ( return (
<CommandPrimitive <CommandPrimitive
data-slot="command" data-slot="command"
className={cn( className={cn(
'flex size-full flex-col overflow-hidden rounded-xl! bg-popover p-1 text-popover-foreground', "flex size-full flex-col overflow-hidden rounded-xl! bg-popover p-1 text-popover-foreground",
className className
)} )}
{...props} {...props}
/> />
); )
} }
function CommandDialog({ function CommandDialog({
title = 'Command Palette', title = "Command Palette",
description = 'Search for a command to run...', description = "Search for a command to run...",
children, children,
className, className,
showCloseButton = false, showCloseButton = false,
...props ...props
}: React.ComponentProps<typeof Dialog> & { }: React.ComponentProps<typeof Dialog> & {
title?: string; title?: string
description?: string; description?: string
className?: string; className?: string
showCloseButton?: boolean; showCloseButton?: boolean
}) { }) {
return ( return (
<Dialog {...props}> <Dialog {...props}>
@@ -39,22 +51,31 @@ function CommandDialog({
<DialogDescription>{description}</DialogDescription> <DialogDescription>{description}</DialogDescription>
</DialogHeader> </DialogHeader>
<DialogContent <DialogContent
className={cn('top-1/3 translate-y-0 overflow-hidden rounded-xl! p-0', className)} className={cn(
"top-1/3 translate-y-0 overflow-hidden rounded-xl! p-0",
className
)}
showCloseButton={showCloseButton} showCloseButton={showCloseButton}
> >
{children} {children}
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); )
} }
function CommandInput({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Input>) { function CommandInput({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
return ( return (
<div data-slot="command-input-wrapper" className="p-1 pb-0"> <div data-slot="command-input-wrapper" className="p-1 pb-0">
<InputGroup className="h-8! rounded-lg! border-input/30 bg-input/30 shadow-none! *:data-[slot=input-group-addon]:pl-2!"> <InputGroup className="h-8! rounded-lg! border-input/30 bg-input/30 shadow-none! *:data-[slot=input-group-addon]:pl-2!">
<CommandPrimitive.Input <CommandPrimitive.Input
data-slot="command-input" data-slot="command-input"
className={cn('w-full text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50', className)} className={cn(
"w-full text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props} {...props}
/> />
<InputGroupAddon> <InputGroupAddon>
@@ -62,53 +83,72 @@ function CommandInput({ className, ...props }: React.ComponentProps<typeof Comma
</InputGroupAddon> </InputGroupAddon>
</InputGroup> </InputGroup>
</div> </div>
); )
} }
function CommandList({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.List>) { function CommandList({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.List>) {
return ( return (
<CommandPrimitive.List <CommandPrimitive.List
data-slot="command-list" data-slot="command-list"
className={cn('no-scrollbar max-h-72 scroll-py-1 overflow-x-hidden overflow-y-auto outline-none', className)}
{...props}
/>
);
}
function CommandEmpty({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Empty>) {
return (
<CommandPrimitive.Empty
data-slot="command-empty"
className={cn('py-6 text-center text-sm', className)}
{...props}
/>
);
}
function CommandGroup({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Group>) {
return (
<CommandPrimitive.Group
data-slot="command-group"
className={cn( className={cn(
'overflow-hidden p-1 text-foreground **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:py-1.5 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:font-medium **:[[cmdk-group-heading]]:text-muted-foreground', "no-scrollbar max-h-72 scroll-py-1 overflow-x-hidden overflow-y-auto outline-none",
className className
)} )}
{...props} {...props}
/> />
); )
} }
function CommandSeparator({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Separator>) { function CommandEmpty({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
return (
<CommandPrimitive.Empty
data-slot="command-empty"
className={cn("py-6 text-center text-sm", className)}
{...props}
/>
)
}
function CommandGroup({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
return (
<CommandPrimitive.Group
data-slot="command-group"
className={cn(
"overflow-hidden p-1 text-foreground **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:py-1.5 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:font-medium **:[[cmdk-group-heading]]:text-muted-foreground",
className
)}
{...props}
/>
)
}
function CommandSeparator({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
return ( return (
<CommandPrimitive.Separator <CommandPrimitive.Separator
data-slot="command-separator" data-slot="command-separator"
className={cn('-mx-1 h-px bg-border', className)} className={cn("-mx-1 h-px bg-border", className)}
{...props} {...props}
/> />
); )
} }
function CommandItem({ className, children, ...props }: React.ComponentProps<typeof CommandPrimitive.Item>) { function CommandItem({
className,
children,
...props
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
return ( return (
<CommandPrimitive.Item <CommandPrimitive.Item
data-slot="command-item" data-slot="command-item"
@@ -121,20 +161,23 @@ function CommandItem({ className, children, ...props }: React.ComponentProps<typ
{children} {children}
<CheckIcon className="ml-auto opacity-0 group-has-data-[slot=command-shortcut]/command-item:hidden group-data-[checked=true]/command-item:opacity-100" /> <CheckIcon className="ml-auto opacity-0 group-has-data-[slot=command-shortcut]/command-item:hidden group-data-[checked=true]/command-item:opacity-100" />
</CommandPrimitive.Item> </CommandPrimitive.Item>
); )
} }
function CommandShortcut({ className, ...props }: React.ComponentProps<'span'>) { function CommandShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return ( return (
<span <span
data-slot="command-shortcut" data-slot="command-shortcut"
className={cn( className={cn(
'ml-auto text-xs tracking-widest text-muted-foreground group-data-selected/command-item:text-foreground', "ml-auto text-xs tracking-widest text-muted-foreground group-data-selected/command-item:text-foreground",
className className
)} )}
{...props} {...props}
/> />
); )
} }
export { export {
@@ -147,4 +190,4 @@ export {
CommandItem, CommandItem,
CommandShortcut, CommandShortcut,
CommandSeparator, CommandSeparator,
}; }
+62 -34
View File
@@ -1,37 +1,48 @@
import * as React from 'react'; import * as React from "react"
import { Dialog as DialogPrimitive } from 'radix-ui'; import { Dialog as DialogPrimitive } from "radix-ui"
import { cn } from '@/lib/utils'; import { cn } from "@/lib/utils"
import { Button } from '@/components/ui/button'; import { Button } from "@/components/ui/button"
import { XIcon } from 'lucide-react'; import { XIcon } from "lucide-react"
function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) { function Dialog({
return <DialogPrimitive.Root data-slot="dialog" {...props} />; ...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
} }
function DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) { function DialogTrigger({
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />; ...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
} }
function DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) { function DialogPortal({
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />; ...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
} }
function DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) { function DialogClose({
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />; ...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
} }
function DialogOverlay({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Overlay>) { function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return ( return (
<DialogPrimitive.Overlay <DialogPrimitive.Overlay
data-slot="dialog-overlay" data-slot="dialog-overlay"
className={cn( className={cn(
'fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0', "fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
className className
)} )}
{...props} {...props}
/> />
); )
} }
function DialogContent({ function DialogContent({
@@ -40,7 +51,7 @@ function DialogContent({
showCloseButton = true, showCloseButton = true,
...props ...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & { }: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean; showCloseButton?: boolean
}) { }) {
return ( return (
<DialogPortal> <DialogPortal>
@@ -48,7 +59,7 @@ function DialogContent({
<DialogPrimitive.Content <DialogPrimitive.Content
data-slot="dialog-content" data-slot="dialog-content"
className={cn( className={cn(
'fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95', "fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-background p-4 text-sm ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className className
)} )}
{...props} {...props}
@@ -56,19 +67,30 @@ function DialogContent({
{children} {children}
{showCloseButton && ( {showCloseButton && (
<DialogPrimitive.Close data-slot="dialog-close" asChild> <DialogPrimitive.Close data-slot="dialog-close" asChild>
<Button variant="ghost" className="absolute top-2 right-2" size="icon-sm"> <Button
<XIcon /> variant="ghost"
className="absolute top-2 right-2"
size="icon-sm"
>
<XIcon
/>
<span className="sr-only">Close</span> <span className="sr-only">Close</span>
</Button> </Button>
</DialogPrimitive.Close> </DialogPrimitive.Close>
)} )}
</DialogPrimitive.Content> </DialogPrimitive.Content>
</DialogPortal> </DialogPortal>
); )
} }
function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) { function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="dialog-header" className={cn('flex flex-col gap-2', className)} {...props} />; return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2", className)}
{...props}
/>
)
} }
function DialogFooter({ function DialogFooter({
@@ -76,14 +98,14 @@ function DialogFooter({
showCloseButton = false, showCloseButton = false,
children, children,
...props ...props
}: React.ComponentProps<'div'> & { }: React.ComponentProps<"div"> & {
showCloseButton?: boolean; showCloseButton?: boolean
}) { }) {
return ( return (
<div <div
data-slot="dialog-footer" data-slot="dialog-footer"
className={cn( className={cn(
'-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 sm:flex-row sm:justify-end', "-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 sm:flex-row sm:justify-end",
className className
)} )}
{...props} {...props}
@@ -95,30 +117,36 @@ function DialogFooter({
</DialogPrimitive.Close> </DialogPrimitive.Close>
)} )}
</div> </div>
); )
} }
function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) { function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return ( return (
<DialogPrimitive.Title <DialogPrimitive.Title
data-slot="dialog-title" data-slot="dialog-title"
className={cn('text-base leading-none font-medium', className)} className={cn("text-base leading-none font-medium", className)}
{...props} {...props}
/> />
); )
} }
function DialogDescription({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Description>) { function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return ( return (
<DialogPrimitive.Description <DialogPrimitive.Description
data-slot="dialog-description" data-slot="dialog-description"
className={cn( className={cn(
'text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground', "text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
className className
)} )}
{...props} {...props}
/> />
); )
} }
export { export {
@@ -132,4 +160,4 @@ export {
DialogPortal, DialogPortal,
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
}; }
+64 -43
View File
@@ -1,25 +1,25 @@
'use client'; "use client"
import * as React from 'react'; import * as React from "react"
import { cva, type VariantProps } from 'class-variance-authority'; import { cva, type VariantProps } from "class-variance-authority"
import { cn } from '@/lib/utils'; import { cn } from "@/lib/utils"
import { Button } from '@/components/ui/button'; import { Button } from "@/components/ui/button"
import { Input } from '@/components/ui/input'; import { Input } from "@/components/ui/input"
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from "@/components/ui/textarea"
function InputGroup({ className, ...props }: React.ComponentProps<'div'>) { function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="input-group" data-slot="input-group"
role="group" role="group"
className={cn( className={cn(
'group/input-group relative flex h-8 w-full min-w-0 items-center rounded-lg border border-input transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:bg-input/30 dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5', "group/input-group relative flex h-8 w-full min-w-0 items-center rounded-lg border border-input transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:bg-input/30 dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5",
className className
)} )}
{...props} {...props}
/> />
); )
} }
const inputGroupAddonVariants = cva( const inputGroupAddonVariants = cva(
@@ -27,24 +27,27 @@ const inputGroupAddonVariants = cva(
{ {
variants: { variants: {
align: { align: {
'inline-start': 'order-first pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem]', "inline-start":
'inline-end': 'order-last pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem]', "order-first pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem]",
'block-start': "inline-end":
'order-first w-full justify-start px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2', "order-last pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem]",
'block-end': 'order-last w-full justify-start px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2', "block-start":
"order-first w-full justify-start px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2",
"block-end":
"order-last w-full justify-start px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2",
}, },
}, },
defaultVariants: { defaultVariants: {
align: 'inline-start', align: "inline-start",
}, },
} }
); )
function InputGroupAddon({ function InputGroupAddon({
className, className,
align = 'inline-start', align = "inline-start",
...props ...props
}: React.ComponentProps<'div'> & VariantProps<typeof inputGroupAddonVariants>) { }: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
return ( return (
<div <div
role="group" role="group"
@@ -52,37 +55,42 @@ function InputGroupAddon({
data-align={align} data-align={align}
className={cn(inputGroupAddonVariants({ align }), className)} className={cn(inputGroupAddonVariants({ align }), className)}
onClick={(e) => { onClick={(e) => {
if ((e.target as HTMLElement).closest('button')) { if ((e.target as HTMLElement).closest("button")) {
return; return
} }
e.currentTarget.parentElement?.querySelector('input')?.focus(); e.currentTarget.parentElement?.querySelector("input")?.focus()
}} }}
{...props} {...props}
/> />
); )
} }
const inputGroupButtonVariants = cva('flex items-center gap-2 text-sm shadow-none', { const inputGroupButtonVariants = cva(
"flex items-center gap-2 text-sm shadow-none",
{
variants: { variants: {
size: { size: {
xs: "h-6 gap-1 rounded-[calc(var(--radius)-3px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5", xs: "h-6 gap-1 rounded-[calc(var(--radius)-3px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5",
sm: '', sm: "",
'icon-xs': 'size-6 rounded-[calc(var(--radius)-3px)] p-0 has-[>svg]:p-0', "icon-xs":
'icon-sm': 'size-8 p-0 has-[>svg]:p-0', "size-6 rounded-[calc(var(--radius)-3px)] p-0 has-[>svg]:p-0",
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
}, },
}, },
defaultVariants: { defaultVariants: {
size: 'xs', size: "xs",
}, },
}); }
)
function InputGroupButton({ function InputGroupButton({
className, className,
type = 'button', type = "button",
variant = 'ghost', variant = "ghost",
size = 'xs', size = "xs",
...props ...props
}: Omit<React.ComponentProps<typeof Button>, 'size'> & VariantProps<typeof inputGroupButtonVariants>) { }: Omit<React.ComponentProps<typeof Button>, "size"> &
VariantProps<typeof inputGroupButtonVariants>) {
return ( return (
<Button <Button
type={type} type={type}
@@ -91,10 +99,10 @@ function InputGroupButton({
className={cn(inputGroupButtonVariants({ size }), className)} className={cn(inputGroupButtonVariants({ size }), className)}
{...props} {...props}
/> />
); )
} }
function InputGroupText({ className, ...props }: React.ComponentProps<'span'>) { function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
return ( return (
<span <span
className={cn( className={cn(
@@ -103,33 +111,46 @@ function InputGroupText({ className, ...props }: React.ComponentProps<'span'>) {
)} )}
{...props} {...props}
/> />
); )
} }
function InputGroupInput({ className, ...props }: React.ComponentProps<'input'>) { function InputGroupInput({
className,
...props
}: React.ComponentProps<"input">) {
return ( return (
<Input <Input
data-slot="input-group-control" data-slot="input-group-control"
className={cn( className={cn(
'flex-1 rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent', "flex-1 rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent",
className className
)} )}
{...props} {...props}
/> />
); )
} }
function InputGroupTextarea({ className, ...props }: React.ComponentProps<'textarea'>) { function InputGroupTextarea({
className,
...props
}: React.ComponentProps<"textarea">) {
return ( return (
<Textarea <Textarea
data-slot="input-group-control" data-slot="input-group-control"
className={cn( className={cn(
'flex-1 resize-none rounded-none border-0 bg-transparent py-2 shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent', "flex-1 resize-none rounded-none border-0 bg-transparent py-2 shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent",
className className
)} )}
{...props} {...props}
/> />
); )
} }
export { InputGroup, InputGroupAddon, InputGroupButton, InputGroupText, InputGroupInput, InputGroupTextarea }; export {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupText,
InputGroupInput,
InputGroupTextarea,
}
+6 -6
View File
@@ -1,19 +1,19 @@
import * as React from 'react'; import * as React from "react"
import { cn } from '@/lib/utils'; import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<'input'>) { function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return ( return (
<input <input
type={type} type={type}
data-slot="input" data-slot="input"
className={cn( className={cn(
'h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40', "h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
className className
)} )}
{...props} {...props}
/> />
); )
} }
export { Input }; export { Input }
+10 -7
View File
@@ -1,19 +1,22 @@
import * as React from 'react'; import * as React from "react"
import { Label as LabelPrimitive } from 'radix-ui'; import { Label as LabelPrimitive } from "radix-ui"
import { cn } from '@/lib/utils'; import { cn } from "@/lib/utils"
function Label({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) { function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return ( return (
<LabelPrimitive.Root <LabelPrimitive.Root
data-slot="label" data-slot="label"
className={cn( className={cn(
'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50', "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className className
)} )}
{...props} {...props}
/> />
); )
} }
export { Label }; export { Label }
+54 -19
View File
@@ -1,19 +1,23 @@
import * as React from 'react'; import * as React from "react"
import { Popover as PopoverPrimitive } from 'radix-ui'; import { Popover as PopoverPrimitive } from "radix-ui"
import { cn } from '@/lib/utils'; import { cn } from "@/lib/utils"
function Popover({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Root>) { function Popover({
return <PopoverPrimitive.Root data-slot="popover" {...props} />; ...props
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />
} }
function PopoverTrigger({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Trigger>) { function PopoverTrigger({
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />; ...props
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
} }
function PopoverContent({ function PopoverContent({
className, className,
align = 'center', align = "center",
sideOffset = 4, sideOffset = 4,
...props ...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) { }: React.ComponentProps<typeof PopoverPrimitive.Content>) {
@@ -24,29 +28,60 @@ function PopoverContent({
align={align} align={align}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
'z-50 flex w-72 origin-(--radix-popover-content-transform-origin) flex-col gap-2.5 rounded-lg bg-popover p-2.5 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95', "z-50 flex w-72 origin-(--radix-popover-content-transform-origin) flex-col gap-2.5 rounded-lg bg-popover p-2.5 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className className
)} )}
{...props} {...props}
/> />
</PopoverPrimitive.Portal> </PopoverPrimitive.Portal>
); )
} }
function PopoverAnchor({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Anchor>) { function PopoverAnchor({
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />; ...props
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
} }
function PopoverHeader({ className, ...props }: React.ComponentProps<'div'>) { function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="popover-header" className={cn('flex flex-col gap-0.5 text-sm', className)} {...props} />; return (
<div
data-slot="popover-header"
className={cn("flex flex-col gap-0.5 text-sm", className)}
{...props}
/>
)
} }
function PopoverTitle({ className, ...props }: React.ComponentProps<'h2'>) { function PopoverTitle({ className, ...props }: React.ComponentProps<"h2">) {
return <div data-slot="popover-title" className={cn('font-medium', className)} {...props} />; return (
<div
data-slot="popover-title"
className={cn("font-medium", className)}
{...props}
/>
)
} }
function PopoverDescription({ className, ...props }: React.ComponentProps<'p'>) { function PopoverDescription({
return <p data-slot="popover-description" className={cn('text-muted-foreground', className)} {...props} />; className,
...props
}: React.ComponentProps<"p">) {
return (
<p
data-slot="popover-description"
className={cn("text-muted-foreground", className)}
{...props}
/>
)
} }
export { Popover, PopoverAnchor, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger }; export {
Popover,
PopoverAnchor,
PopoverContent,
PopoverDescription,
PopoverHeader,
PopoverTitle,
PopoverTrigger,
}
+18 -10
View File
@@ -1,11 +1,19 @@
import * as React from 'react'; import * as React from "react"
import { ScrollArea as ScrollAreaPrimitive } from 'radix-ui'; import { ScrollArea as ScrollAreaPrimitive } from "radix-ui"
import { cn } from '@/lib/utils'; import { cn } from "@/lib/utils"
function ScrollArea({ className, children, ...props }: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) { function ScrollArea({
className,
children,
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return ( return (
<ScrollAreaPrimitive.Root data-slot="scroll-area" className={cn('relative', className)} {...props}> <ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn("relative", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport <ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport" data-slot="scroll-area-viewport"
className="size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1" className="size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1"
@@ -15,12 +23,12 @@ function ScrollArea({ className, children, ...props }: React.ComponentProps<type
<ScrollBar /> <ScrollBar />
<ScrollAreaPrimitive.Corner /> <ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root> </ScrollAreaPrimitive.Root>
); )
} }
function ScrollBar({ function ScrollBar({
className, className,
orientation = 'vertical', orientation = "vertical",
...props ...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) { }: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return ( return (
@@ -29,7 +37,7 @@ function ScrollBar({
data-orientation={orientation} data-orientation={orientation}
orientation={orientation} orientation={orientation}
className={cn( className={cn(
'flex touch-none p-px transition-colors select-none data-horizontal:h-2.5 data-horizontal:flex-col data-horizontal:border-t data-horizontal:border-t-transparent data-vertical:h-full data-vertical:w-2.5 data-vertical:border-l data-vertical:border-l-transparent', "flex touch-none p-px transition-colors select-none data-horizontal:h-2.5 data-horizontal:flex-col data-horizontal:border-t data-horizontal:border-t-transparent data-vertical:h-full data-vertical:w-2.5 data-vertical:border-l data-vertical:border-l-transparent",
className className
)} )}
{...props} {...props}
@@ -39,7 +47,7 @@ function ScrollBar({
className="relative flex-1 rounded-full bg-border" className="relative flex-1 rounded-full bg-border"
/> />
</ScrollAreaPrimitive.ScrollAreaScrollbar> </ScrollAreaPrimitive.ScrollAreaScrollbar>
); )
} }
export { ScrollArea, ScrollBar }; export { ScrollArea, ScrollBar }
-167
View File
@@ -1,167 +0,0 @@
import * as React from 'react';
import { Select as SelectPrimitive } from 'radix-ui';
import { cn } from '@/lib/utils';
import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from 'lucide-react';
function Select({ ...props }: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />;
}
function SelectGroup({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" className={cn('scroll-my-1 p-1', className)} {...props} />;
}
function SelectValue({ ...props }: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
}
function SelectTrigger({
className,
size = 'default',
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: 'sm' | 'default';
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"flex w-fit items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="pointer-events-none size-4 text-muted-foreground" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
);
}
function SelectContent({
className,
children,
position = 'item-aligned',
align = 'center',
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
data-align-trigger={position === 'item-aligned'}
className={cn(
'relative z-50 max-h-(--radix-select-content-available-height) min-w-36 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95',
position === 'popper' &&
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
className
)}
position={position}
align={align}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
data-position={position}
className={cn(
'data-[position=popper]:h-(--radix-select-trigger-height) data-[position=popper]:w-full data-[position=popper]:min-w-(--radix-select-trigger-width)',
position === 'popper' && ''
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
);
}
function SelectLabel({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn('px-1.5 py-1 text-xs text-muted-foreground', className)}
{...props}
/>
);
}
function SelectItem({ className, children, ...props }: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
)}
{...props}
>
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="pointer-events-none" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
);
}
function SelectSeparator({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn('pointer-events-none -mx-1 my-1 h-px bg-border', className)}
{...props}
/>
);
}
function SelectScrollUpButton({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<ChevronUpIcon />
</SelectPrimitive.ScrollUpButton>
);
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<ChevronDownIcon />
</SelectPrimitive.ScrollDownButton>
);
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
};
+7 -7
View File
@@ -1,11 +1,11 @@
import * as React from 'react'; import * as React from "react"
import { Separator as SeparatorPrimitive } from 'radix-ui'; import { Separator as SeparatorPrimitive } from "radix-ui"
import { cn } from '@/lib/utils'; import { cn } from "@/lib/utils"
function Separator({ function Separator({
className, className,
orientation = 'horizontal', orientation = "horizontal",
decorative = true, decorative = true,
...props ...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) { }: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
@@ -15,12 +15,12 @@ function Separator({
decorative={decorative} decorative={decorative}
orientation={orientation} orientation={orientation}
className={cn( className={cn(
'shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch', "shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
className className
)} )}
{...props} {...props}
/> />
); )
} }
export { Separator }; export { Separator }
+8 -8
View File
@@ -1,22 +1,22 @@
import * as React from 'react'; import * as React from "react";
import { Switch as SwitchPrimitive } from 'radix-ui'; import {Switch as SwitchPrimitive} from "radix-ui";
import { cn } from '@/lib/utils'; import {cn} from "@/lib/utils";
function Switch({ function Switch({
className, className,
size = 'default', size = "default",
...props ...props
}: React.ComponentProps<typeof SwitchPrimitive.Root> & { }: React.ComponentProps<typeof SwitchPrimitive.Root> & {
size?: 'sm' | 'default'; size?: "sm" | "default";
}) { }) {
return ( return (
<SwitchPrimitive.Root <SwitchPrimitive.Root
data-slot="switch" data-slot="switch"
data-size={size} data-size={size}
className={cn( className={cn(
'peer group/switch relative inline-flex shrink-0 items-center rounded-full border border-transparent transition-all outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:bg-primary data-unchecked:bg-input dark:data-unchecked:bg-input/80 data-disabled:cursor-not-allowed data-disabled:opacity-50', "peer group/switch relative inline-flex shrink-0 items-center rounded-full border border-transparent transition-all outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:bg-primary data-unchecked:bg-input dark:data-unchecked:bg-input/80 data-disabled:cursor-not-allowed data-disabled:opacity-50",
className className,
)} )}
{...props} {...props}
> >
@@ -28,4 +28,4 @@ function Switch({
); );
} }
export { Switch }; export {Switch};

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