14 Commits

Author SHA1 Message Date
ARJUN S THAMPI
1bbf7f9c1c feat: add blog page 2026-03-18 14:25:08 +05:30
ARJUN S THAMPI
2584539fb0 feat: add search in department 2026-03-17 17:28:18 +05:30
ARJUN S THAMPI
101c235855 feat: doctor search filter functionality added 2026-03-17 17:08:05 +05:30
ARJUN S THAMPI
b89b2b1ba5 feat: department wise timing 2026-03-17 16:22:37 +05:30
ARJUN S THAMPI
c11a3f9a7d fix: apis 2026-03-17 13:56:46 +05:30
ARJUN S THAMPI
763b887d65 feat:add doctor page 2026-03-17 13:11:00 +05:30
ARJUN S THAMPI
db8cee836a feat: change dept name 2026-03-16 17:55:55 +05:30
ARJUN S THAMPI
46bbd8106b feat: add department dashboard 2026-03-16 17:55:33 +05:30
ARJUN S THAMPI
aaa62ae3f5 feat : add academics api 2026-03-16 12:39:41 +05:30
ARJUN S THAMPI
9ae190754a feat : add appointment apis 2026-03-16 10:16:27 +05:30
ARJUN S THAMPI
9faa512c0b feat:add candidate apis 2026-03-13 16:26:06 +05:30
ARJUN S THAMPI
7955465be4 feat : add doctor & career api 2026-03-13 14:54:47 +05:30
3ac50d4132 Merge pull request 'feat : add front-end using shad cn' (#1) from feat/-addshadcn- into dev
Reviewed-on: #1
2026-03-13 04:34:14 +00:00
ARJUN S THAMPI
1206e51f6d feat :add package.json 2026-03-12 17:43:38 +05:30
60 changed files with 15796 additions and 9590 deletions

2076
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,15 @@
-- CreateTable
CREATE TABLE "Career" (
"id" SERIAL NOT NULL,
"post" TEXT NOT NULL,
"designation" TEXT,
"qualification" TEXT,
"experienceNeed" TEXT,
"email" TEXT,
"number" TEXT,
"status" TEXT NOT NULL DEFAULT 'new',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Career_pkey" PRIMARY KEY ("id")
);

View File

@@ -0,0 +1,17 @@
-- CreateTable
CREATE TABLE "Candidate" (
"id" SERIAL NOT NULL,
"fullName" TEXT NOT NULL,
"mobile" TEXT NOT NULL,
"email" TEXT NOT NULL,
"subject" TEXT NOT NULL,
"coverLetter" TEXT NOT NULL,
"careerId" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Candidate_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "Candidate" ADD CONSTRAINT "Candidate_careerId_fkey" FOREIGN KEY ("careerId") REFERENCES "Career"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -0,0 +1,21 @@
-- CreateTable
CREATE TABLE "Appointment" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"mobileNumber" TEXT NOT NULL,
"email" TEXT,
"message" TEXT,
"date" TIMESTAMP(3) NOT NULL,
"doctorId" INTEGER NOT NULL,
"departmentId" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Appointment_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "Appointment" ADD CONSTRAINT "Appointment_doctorId_fkey" FOREIGN KEY ("doctorId") REFERENCES "Doctor"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Appointment" ADD CONSTRAINT "Appointment_departmentId_fkey" FOREIGN KEY ("departmentId") REFERENCES "Department"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -0,0 +1,15 @@
-- DropForeignKey
ALTER TABLE "Appointment" DROP CONSTRAINT "Appointment_departmentId_fkey";
-- DropForeignKey
ALTER TABLE "Appointment" DROP CONSTRAINT "Appointment_doctorId_fkey";
-- AlterTable
ALTER TABLE "Appointment" ALTER COLUMN "doctorId" SET DATA TYPE TEXT,
ALTER COLUMN "departmentId" SET DATA TYPE TEXT;
-- AddForeignKey
ALTER TABLE "Appointment" ADD CONSTRAINT "Appointment_doctorId_fkey" FOREIGN KEY ("doctorId") REFERENCES "Doctor"("doctorId") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Appointment" ADD CONSTRAINT "Appointment_departmentId_fkey" FOREIGN KEY ("departmentId") REFERENCES "Department"("departmentId") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -0,0 +1,13 @@
-- CreateTable
CREATE TABLE "Inquiry" (
"id" SERIAL NOT NULL,
"fullName" TEXT NOT NULL,
"number" TEXT NOT NULL,
"emailId" TEXT,
"subject" TEXT,
"message" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Inquiry_pkey" PRIMARY KEY ("id")
);

View File

@@ -0,0 +1,14 @@
-- CreateTable
CREATE TABLE "AcademicsResearch" (
"id" SERIAL NOT NULL,
"fullName" TEXT NOT NULL,
"number" TEXT NOT NULL,
"emailId" TEXT,
"subject" TEXT,
"courseName" TEXT,
"message" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "AcademicsResearch_pkey" PRIMARY KEY ("id")
);

View File

@@ -1,4 +1,3 @@
generator client { generator client {
provider = "prisma-client-js" provider = "prisma-client-js"
} }
@@ -9,92 +8,169 @@ datasource db {
} }
model User { model User {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
username String @unique username String @unique
password String password String
role String? @default("admin") role String? @default("admin")
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
model Doctor { model Doctor {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
doctorId String @unique doctorId String @unique
name String name String
designation String? designation String?
workingStatus String? workingStatus String?
qualification String? qualification String?
departments DoctorDepartment[] departments DoctorDepartment[]
appointments Appointment[]
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
model Department { model Department {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
departmentId String @unique departmentId String @unique
name String name String
para1 String? para1 String?
para2 String? para2 String?
para3 String? para3 String?
facilities String? facilities String?
services String? services String?
doctors DoctorDepartment[] doctors DoctorDepartment[]
appointments Appointment[]
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
model DoctorDepartment { model DoctorDepartment {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
doctorId Int doctorId Int
departmentId Int departmentId Int
doctor Doctor @relation(fields: [doctorId], references: [id]) doctor Doctor @relation(fields: [doctorId], references: [id])
department Department @relation(fields: [departmentId], references: [id]) department Department @relation(fields: [departmentId], references: [id])
timing DoctorTiming? timing DoctorTiming?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
@@unique([doctorId, departmentId]) @@unique([doctorId, departmentId])
} }
model DoctorTiming { model DoctorTiming {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
doctorDepartmentId Int @unique doctorDepartmentId Int @unique
doctorDepartment DoctorDepartment @relation(fields: [doctorDepartmentId], references: [id]) doctorDepartment DoctorDepartment @relation(fields: [doctorDepartmentId], references: [id])
monday String? monday String?
tuesday String? tuesday String?
wednesday String? wednesday String?
thursday String? thursday String?
friday String? friday String?
saturday String? saturday String?
sunday String? sunday String?
additional String? additional String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Blog {
id Int @id @default(autoincrement())
title String
writer String?
image String?
content Json
isActive Boolean @default(true)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
}
model Blog {
id Int @id @default(autoincrement())
title String
writer String?
image String?
content Json
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Career {
id Int @id @default(autoincrement())
post String
designation String?
qualification String?
experienceNeed String?
email String?
number String?
status String @default("new")
candidates Candidate[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Candidate {
id Int @id @default(autoincrement())
fullName String
mobile String
email String
subject String
coverLetter String
careerId Int
career Career @relation(fields: [careerId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Appointment {
id Int @id @default(autoincrement())
name String
mobileNumber String
email String?
message String?
date DateTime
doctorId String
departmentId String
doctor Doctor @relation(fields: [doctorId], references: [doctorId])
department Department @relation(fields: [departmentId], references: [departmentId])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Inquiry {
id Int @id @default(autoincrement())
fullName String
number String
emailId String?
subject String?
message String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model AcademicsResearch {
id Int @id @default(autoincrement())
fullName String
number String
emailId String?
subject String?
courseName String?
message String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
} }

View File

@@ -6,6 +6,12 @@ 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 careerRoutes from "./routes/career.routes.js";
import candidateRoutes from "./routes/candidate.routes.js";
import appointmentRoutes from "./routes/appointment.routes.js";
import inquiryRoutes from "./routes/inquiry.routes.js";
import academicsResearchRoutes from "./routes/academicsResearch.routes.js";
dotenv.config(); dotenv.config();
@@ -35,6 +41,12 @@ 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/careers", careerRoutes);
app.use("/api/candidates", candidateRoutes);
app.use("/api/appointments", appointmentRoutes);
app.use("/api/inquiry", inquiryRoutes);
app.use("/api/academics", academicsResearchRoutes);
const PORT = process.env.PORT || 3000; const PORT = process.env.PORT || 3000;
app.listen(PORT, () => { app.listen(PORT, () => {

View File

@@ -0,0 +1,117 @@
import prisma from "../prisma/client.js";
// CREATE ACADEMICS & RESEARCH
export const createAcademicsResearch = async (req, res) => {
try {
const {fullName, number, emailId, subject, courseName, message} = req.body;
if (!fullName || !number) {
return res.status(400).json({
success: false,
message: "Full name and number are required",
});
}
const data = await prisma.academicsResearch.create({
data: {
fullName,
number,
emailId,
subject,
courseName,
message,
},
});
res.status(200).json({
success: true,
status: 200,
data,
message: "Academics & Research added successfully",
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to add Academics & Research inquiry",
});
}
};
// GET ALL
export const getAcademicsResearch = async (req, res) => {
try {
const data = await prisma.academicsResearch.findMany({
orderBy: {
createdAt: "desc",
},
});
res.json({
success: true,
data,
});
} catch (error) {
res.status(500).json({
success: false,
message: "Failed to fetch records",
});
}
};
// GET SINGLE
export const getSingleAcademicsResearch = async (req, res) => {
try {
const {id} = req.params;
const data = await prisma.academicsResearch.findUnique({
where: {
id: Number(id),
},
});
if (!data) {
return res.status(404).json({
success: false,
message: "Record not found",
});
}
res.json({
success: true,
data,
});
} catch (error) {
res.status(500).json({
success: false,
message: "Failed to fetch record",
});
}
};
// DELETE
export const deleteAcademicsResearch = async (req, res) => {
try {
const {id} = req.params;
await prisma.academicsResearch.delete({
where: {
id: Number(id),
},
});
res.json({
success: true,
message: "Record deleted successfully",
});
} catch (error) {
res.status(500).json({
success: false,
message: "Failed to delete record",
});
}
};

View File

@@ -0,0 +1,224 @@
import prisma from "../prisma/client.js";
//CREATE APPOINTMENT
export const createAppointment = async (req, res) => {
try {
const {name, mobileNumber, email, message, date, doctorId, departmentId} =
req.body;
if (!name || !mobileNumber || !doctorId || !departmentId || !date) {
return res.status(400).json({
success: false,
message: "Required fields missing",
});
}
const appointment = await prisma.appointment.create({
data: {
name,
mobileNumber,
email,
message,
date: new Date(date),
doctorId,
departmentId,
},
include: {
doctor: true,
department: true,
},
});
res.status(201).json({
success: true,
message: "Appointment booked successfully",
data: appointment,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to create appointment",
});
}
};
// GET ALL APPOINTMENTS
export const getAppointments = async (req, res) => {
try {
const appointments = await prisma.appointment.findMany({
include: {
doctor: true,
department: true,
},
orderBy: {
createdAt: "desc",
},
});
res.status(200).json({
success: true,
data: appointments,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to fetch appointments",
});
}
};
// GET SINGLE APPOINTMENT
export const getAppointment = async (req, res) => {
try {
const {id} = req.params;
const appointment = await prisma.appointment.findUnique({
where: {
id: Number(id),
},
include: {
doctor: true,
department: true,
},
});
if (!appointment) {
return res.status(404).json({
success: false,
message: "Appointment not found",
});
}
res.status(200).json({
success: true,
data: appointment,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to fetch appointment",
});
}
};
// GET APPOINTMENTS BY DOCTOR
export const getAppointmentsByDoctor = async (req, res) => {
try {
const {doctorId} = req.params;
const appointments = await prisma.appointment.findMany({
where: {
doctorId,
},
include: {
doctor: true,
department: true,
},
orderBy: {
date: "asc",
},
});
res.status(200).json({
success: true,
data: appointments,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to fetch doctor appointments",
});
}
};
// GET APPOINTMENTS BY DEPARTMENT
export const getAppointmentsByDepartment = async (req, res) => {
try {
const {departmentId} = req.params;
const appointments = await prisma.appointment.findMany({
where: {
departmentId,
},
include: {
doctor: true,
department: true,
},
});
res.status(200).json({
success: true,
data: appointments,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to fetch department appointments",
});
}
};
// UPDATE APPOINTMENT
export const updateAppointment = async (req, res) => {
try {
const {id} = req.params;
const appointment = await prisma.appointment.update({
where: {
id: Number(id),
},
data: req.body,
include: {
doctor: true,
department: true,
},
});
res.status(200).json({
success: true,
message: "Appointment updated successfully",
data: appointment,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to update appointment",
});
}
};
//DELETE APPOINTMENT
export const deleteAppointment = async (req, res) => {
try {
const {id} = req.params;
await prisma.appointment.delete({
where: {
id: Number(id),
},
});
res.status(200).json({
success: true,
message: "Appointment deleted successfully",
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to delete appointment",
});
}
};

View File

@@ -0,0 +1,183 @@
import prisma from "../prisma/client.js";
// CREATE CANDIDATE
export const createCandidate = async (req, res) => {
try {
const {fullName, mobile, email, subject, coverLetter, careerId} = req.body;
if (!fullName || !mobile || !email || !careerId) {
return res.status(400).json({
success: false,
message: "Required fields missing",
});
}
const candidate = await prisma.candidate.create({
data: {
fullName,
mobile,
email,
subject,
coverLetter,
careerId: Number(careerId),
},
});
res.status(201).json({
success: true,
message: "Application submitted successfully",
data: candidate,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to create candidate",
});
}
};
// GET ALL CANDIDATES
export const getCandidates = async (req, res) => {
try {
const candidates = await prisma.candidate.findMany({
include: {
career: true,
},
orderBy: {
createdAt: "desc",
},
});
res.status(200).json({
success: true,
data: candidates,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to fetch candidates",
});
}
};
// GET SINGLE CANDIDATE
export const getCandidate = async (req, res) => {
try {
const {id} = req.params;
const candidate = await prisma.candidate.findUnique({
where: {
id: Number(id),
},
include: {
career: true,
},
});
if (!candidate) {
return res.status(404).json({
success: false,
message: "Candidate not found",
});
}
res.status(200).json({
success: true,
data: candidate,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to fetch candidate",
});
}
};
// GET CANDIDATES BY CAREER
export const getCandidatesByCareer = async (req, res) => {
try {
const {careerId} = req.params;
const candidates = await prisma.candidate.findMany({
where: {
careerId: Number(careerId),
},
include: {
career: true,
},
orderBy: {
createdAt: "desc",
},
});
res.status(200).json({
success: true,
data: candidates,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to fetch candidates",
});
}
};
// UPDATE CANDIDATE
export const updateCandidate = async (req, res) => {
try {
const {id} = req.params;
const candidate = await prisma.candidate.update({
where: {
id: Number(id),
},
data: req.body,
});
res.status(200).json({
success: true,
message: "Candidate updated successfully",
data: candidate,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to update candidate",
});
}
};
// DELETE CANDIDATE
export const deleteCandidate = async (req, res) => {
try {
const {id} = req.params;
await prisma.candidate.delete({
where: {
id: Number(id),
},
});
res.status(200).json({
success: true,
message: "Candidate deleted successfully",
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to delete candidate",
});
}
};

View File

@@ -0,0 +1,128 @@
import prisma from "../prisma/client.js";
// GET ALL CAREERS
export const getAllCareers = async (req, res) => {
try {
const careers = await prisma.career.findMany({
orderBy: {createdAt: "desc"},
});
const response = careers.map((c) => ({
id: c.id,
post: c.post,
designation: c.designation,
qualification: c.qualification,
experienceNeed: c.experienceNeed,
email: c.email,
number: c.number,
status: c.status,
}));
return res.status(200).json({
success: true,
data: response,
});
} catch (error) {
console.error(error);
return res.status(500).json({
success: false,
message: "Failed to fetch careers",
});
}
};
// CREATE CAREER
export const createCareer = async (req, res) => {
try {
const {
post,
designation,
qualification,
experienceNeed,
email,
number,
status,
} = req.body;
if (!post || !designation) {
return res.status(400).json({
success: false,
message: "Post and designation are required",
});
}
const career = await prisma.career.create({
data: {
post,
designation,
qualification,
experienceNeed,
email,
number,
status,
},
});
return res.status(201).json({
success: true,
message: "Career created successfully",
data: career,
});
} catch (error) {
console.error(error);
return res.status(500).json({
success: false,
message: "Failed to create career",
});
}
};
// UPDATE CAREER (PATCH)
export const updateCareer = async (req, res) => {
try {
const {id} = req.params;
const career = await prisma.career.update({
where: {id: Number(id)},
data: req.body,
});
return res.status(200).json({
success: true,
message: "Career updated successfully",
data: career,
});
} catch (error) {
console.error(error);
return res.status(500).json({
success: false,
message: "Failed to update career",
});
}
};
// DELETE CAREER
export const deleteCareer = async (req, res) => {
try {
const {id} = req.params;
await prisma.career.delete({
where: {id: Number(id)},
});
return res.status(200).json({
success: true,
message: "Career deleted successfully",
});
} catch (error) {
console.error(error);
return res.status(500).json({
success: false,
message: "Failed to delete career",
});
}
};

View File

@@ -8,7 +8,7 @@ export const getAllDepartments = async (req, res) => {
const response = departments.map((dep) => ({ const response = departments.map((dep) => ({
departmentId: dep.departmentId, departmentId: dep.departmentId,
Department: dep.name, name: dep.name,
para1: dep.para1 ?? "", para1: dep.para1 ?? "",
para2: dep.para2 ?? "", para2: dep.para2 ?? "",
para3: dep.para3 ?? "", para3: dep.para3 ?? "",
@@ -64,3 +64,56 @@ export async function createDepartment(req, res) {
res.status(500).json({error: "Failed to create department"}); res.status(500).json({error: "Failed to create department"});
} }
} }
export const updateDepartment = async (req, res) => {
try {
const {departmentId} = req.params;
const {name, para1, para2, para3, facilities, services} = req.body;
const department = await prisma.department.update({
where: {departmentId},
data: {
name,
para1,
para2,
para3,
facilities,
services,
},
});
return res.status(200).json({
success: true,
message: "Department updated successfully",
data: department,
});
} catch (error) {
console.error(error);
return res.status(500).json({
success: false,
message: "Failed to update department",
});
}
};
export const deleteDepartment = async (req, res) => {
try {
const {departmentId} = req.params;
await prisma.department.delete({
where: {departmentId},
});
return res.status(200).json({
success: true,
message: "Department deleted successfully",
});
} catch (error) {
console.error(error);
return res.status(500).json({
success: false,
message: "Failed to delete department",
});
}
};

View File

@@ -0,0 +1,416 @@
import prisma from "../prisma/client.js";
// get doctors
export const getAllDoctors = async (req, res) => {
try {
const doctors = await prisma.doctor.findMany({
include: {
departments: {
include: {
department: true,
timing: true,
},
},
},
orderBy: {name: "asc"},
});
const formatted = doctors.map((doc, index) => {
return {
SL_NO: String(index + 1),
doctorId: doc.doctorId,
name: doc.name,
designation: doc.designation,
workingStatus: doc.workingStatus,
qualification: doc.qualification,
departments: doc.departments.map((d) => {
const t = d.timing || {};
const timingArray = [
t.monday && `Monday ${t.monday}`,
t.tuesday && `Tuesday ${t.tuesday}`,
t.wednesday && `Wednesday ${t.wednesday}`,
t.thursday && `Thursday ${t.thursday}`,
t.friday && `Friday ${t.friday}`,
t.saturday && `Saturday ${t.saturday}`,
t.sunday && `Sunday ${t.sunday}`,
t.additional && t.additional,
].filter(Boolean);
return {
departmentId: d.department.departmentId,
departmentName: d.department.name,
timing: timingArray.join(" & "),
};
}),
};
});
res.status(200).json({
success: true,
data: formatted,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to fetch doctors",
});
}
};
// get doctor by id
export const getDoctorByDoctorId = async (req, res) => {
try {
const {doctorId} = req.params;
const doctor = await prisma.doctor.findUnique({
where: {doctorId},
include: {
departments: {
include: {
department: true,
timing: true,
},
},
},
});
if (!doctor) {
return res.status(404).json({
success: false,
message: "Doctor not found",
});
}
const response = {
doctorId: doctor.doctorId,
name: doctor.name,
designation: doctor.designation,
workingStatus: doctor.workingStatus,
qualification: doctor.qualification,
departments: doctor.departments.map((d) => ({
departmentId: d.department.departmentId,
departmentName: d.department.name,
timing: d.timing || {},
})),
};
res.status(200).json({
success: true,
data: response,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to fetch doctor",
});
}
};
// add doctors
export const createDoctor = async (req, res) => {
try {
const {
doctorId,
name,
designation,
workingStatus,
qualification,
departments,
} = req.body;
const doctor = await prisma.doctor.create({
data: {
doctorId,
name,
designation,
workingStatus,
qualification,
},
});
for (const dep of departments) {
const department = await prisma.department.findUnique({
where: {departmentId: dep.departmentId},
});
if (!department) continue;
const doctorDepartment = await prisma.doctorDepartment.create({
data: {
doctorId: doctor.id,
departmentId: department.id,
},
});
if (dep.timing) {
await prisma.doctorTiming.create({
data: {
doctorDepartmentId: doctorDepartment.id,
...dep.timing,
},
});
}
}
res.status(201).json({
success: true,
message: "Doctor created successfully",
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to create doctor",
});
}
};
//update doctors
export const updateDoctor = async (req, res) => {
try {
const {doctorId} = req.params;
const {name, designation, workingStatus, qualification, departments} =
req.body;
const doctor = await prisma.doctor.findUnique({
where: {doctorId},
});
if (!doctor) {
return res.status(404).json({
success: false,
message: "Doctor not found",
});
}
await prisma.doctor.update({
where: {id: doctor.id},
data: {
name,
designation,
workingStatus,
qualification,
},
});
const oldRelations = await prisma.doctorDepartment.findMany({
where: {doctorId: doctor.id},
});
for (const rel of oldRelations) {
await prisma.doctorTiming.deleteMany({
where: {doctorDepartmentId: rel.id},
});
}
await prisma.doctorDepartment.deleteMany({
where: {doctorId: doctor.id},
});
for (const dep of departments) {
const department = await prisma.department.findUnique({
where: {departmentId: dep.departmentId},
});
if (!department) continue;
const doctorDepartment = await prisma.doctorDepartment.create({
data: {
doctorId: doctor.id,
departmentId: department.id,
},
});
if (dep.timing) {
await prisma.doctorTiming.create({
data: {
doctorDepartmentId: doctorDepartment.id,
...dep.timing,
},
});
}
}
res.status(200).json({
success: true,
message: "Doctor updated successfully",
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to update doctor",
});
}
};
//delete doctor
export const deleteDoctor = async (req, res) => {
try {
const {doctorId} = req.params;
if (!doctorId) {
return res.status(400).json({
success: false,
message: "Doctor ID is required",
});
}
const doctor = await prisma.doctor.findUnique({
where: {doctorId},
});
if (!doctor) {
return res.status(404).json({
success: false,
message: `Doctor with ID ${doctorId} not found`,
});
}
const doctorDepartments = await prisma.doctorDepartment.findMany({
where: {doctorId: doctor.id},
});
for (const dd of doctorDepartments) {
await prisma.doctorTiming.deleteMany({
where: {doctorDepartmentId: dd.id},
});
}
await prisma.doctorDepartment.deleteMany({
where: {doctorId: doctor.id},
});
await prisma.doctor.delete({
where: {id: doctor.id},
});
res.status(200).json({
success: true,
message: `Doctor ${doctorId} deleted successfully`,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to delete doctor",
});
}
};
// get doctor timings
export const getDoctorTimings = async (req, res) => {
try {
const doctors = await prisma.doctor.findMany({
include: {
departments: {
include: {
timing: true,
},
},
},
});
const result = doctors.map((doc) => {
let timing = {};
if (doc.departments.length > 0) {
timing = doc.departments[0].timing ?? {};
}
return {
Doctor_ID: doc.doctorId,
Doctor: doc.name,
Monday: timing?.monday ?? "",
Tuesday: timing?.tuesday ?? "",
Wednesday: timing?.wednesday ?? "",
Thursday: timing?.thursday ?? "",
Friday: timing?.friday ?? "",
Saturday: timing?.saturday ?? "",
Sunday: timing?.sunday ?? "",
Additional: timing?.additional ?? "",
};
});
res.status(200).json({
success: true,
data: result,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to fetch doctor timings",
});
}
};
// get doctor timings by id
export const getDoctorTimingById = async (req, res) => {
try {
const {doctorId} = req.params;
const doctor = await prisma.doctor.findUnique({
where: {doctorId},
include: {
departments: {
include: {
department: true,
timing: true,
},
},
},
});
if (!doctor) {
return res.status(404).json({
success: false,
message: "Doctor not found",
});
}
const result = {
doctorId: doctor.doctorId,
doctorName: doctor.name,
departments: doctor.departments.map((d) => {
const t = d.timing || {};
return {
departmentId: d.department.departmentId,
departmentName: d.department.name,
timing: {
monday: t.monday || "",
tuesday: t.tuesday || "",
wednesday: t.wednesday || "",
thursday: t.thursday || "",
friday: t.friday || "",
saturday: t.saturday || "",
sunday: t.sunday || "",
additional: t.additional || "",
},
};
}),
};
res.status(200).json({
success: true,
data: result,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to fetch doctor timing",
});
}
};

View File

@@ -0,0 +1,108 @@
import prisma from "../prisma/client.js";
/* CREATE INQUIRY */
export const createInquiry = async (req, res) => {
try {
const {fullName, number, emailId, subject, message} = req.body;
if (!fullName || !number) {
return res.status(400).json({
success: false,
message: "Full name and number are required",
});
}
const inquiry = await prisma.inquiry.create({
data: {
fullName,
number,
emailId,
subject,
message,
},
});
res.status(200).json({
success: true,
status: 200,
data: inquiry,
message: "Inquiry added successfully",
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to add inquiry",
});
}
};
/* GET ALL */
export const getInquiries = async (req, res) => {
try {
const inquiries = await prisma.inquiry.findMany({
orderBy: {
createdAt: "desc",
},
});
res.json({
success: true,
data: inquiries,
});
} catch (error) {
res.status(500).json({
success: false,
message: "Failed to fetch inquiries",
});
}
};
/* GET SINGLE */
export const getInquiry = async (req, res) => {
try {
const {id} = req.params;
const inquiry = await prisma.inquiry.findUnique({
where: {id: Number(id)},
});
if (!inquiry) {
return res.status(404).json({
success: false,
message: "Inquiry not found",
});
}
res.json({
success: true,
data: inquiry,
});
} catch (error) {
res.status(500).json({
success: false,
message: "Failed to fetch inquiry",
});
}
};
/* DELETE */
export const deleteInquiry = async (req, res) => {
try {
const {id} = req.params;
await prisma.inquiry.delete({
where: {id: Number(id)},
});
res.json({
success: true,
message: "Inquiry deleted successfully",
});
} catch (error) {
res.status(500).json({
success: false,
message: "Failed to delete inquiry",
});
}
};

View File

@@ -0,0 +1,18 @@
import express from "express";
import {
createAcademicsResearch,
getAcademicsResearch,
getSingleAcademicsResearch,
deleteAcademicsResearch,
} from "../controllers/academicsResearch.controller.js";
import jwtAuthMiddleware from "../middleware/auth.js";
const router = express.Router();
router.post("/", createAcademicsResearch);
router.get("/getAll", getAcademicsResearch);
router.get("/:id", getSingleAcademicsResearch);
router.delete("/:id", jwtAuthMiddleware, deleteAcademicsResearch);
export default router;

View File

@@ -0,0 +1,23 @@
import express from "express";
import {
createAppointment,
getAppointments,
getAppointment,
updateAppointment,
deleteAppointment,
} from "../controllers/appointment.controller.js";
import jwtAuthMiddleware from "../middleware/auth.js";
const router = express.Router();
/* PUBLIC */
router.get("/getall", getAppointments);
router.post("/", createAppointment);
router.get("/:id", getAppointment);
router.patch("/:id", updateAppointment);
router.delete("/:id", jwtAuthMiddleware, deleteAppointment);
export default router;

View File

@@ -0,0 +1,25 @@
import express from "express";
import {
createCandidate,
getCandidates,
getCandidate,
getCandidatesByCareer,
updateCandidate,
deleteCandidate,
} from "../controllers/candidate.controller.js";
import jwtAuthMiddleware from "../middleware/auth.js";
const router = express.Router();
/* PUBLIC */
router.get("/getAll", getCandidates);
router.get("/:id", getCandidate);
router.get("/career/:careerId", getCandidatesByCareer);
router.post("/", createCandidate);
router.patch("/:id", updateCandidate);
router.delete("/:id", jwtAuthMiddleware, deleteCandidate);
export default router;

View File

@@ -0,0 +1,17 @@
import express from "express";
import {
getAllCareers,
createCareer,
updateCareer,
deleteCareer,
} from "../controllers/career.controller.js";
const router = express.Router();
router.get("/getAll", getAllCareers);
router.post("/", createCareer);
router.patch("/:id", updateCareer);
router.delete("/:id", deleteCareer);
export default router;

View File

@@ -2,6 +2,8 @@ import express from "express";
import { import {
getAllDepartments, getAllDepartments,
createDepartment, createDepartment,
updateDepartment,
deleteDepartment,
} from "../controllers/department.controller.js"; } from "../controllers/department.controller.js";
import jwtAuthMiddleware from "../middleware/auth.js"; import jwtAuthMiddleware from "../middleware/auth.js";
@@ -12,5 +14,7 @@ router.get("/getAll", getAllDepartments);
// Protected // Protected
router.post("/", jwtAuthMiddleware, createDepartment); router.post("/", jwtAuthMiddleware, createDepartment);
router.put("/:departmentId", jwtAuthMiddleware, updateDepartment);
router.delete("/:departmentId", jwtAuthMiddleware, deleteDepartment);
export default router; export default router;

View File

@@ -0,0 +1,25 @@
import express from "express";
import {
getAllDoctors,
createDoctor,
updateDoctor,
deleteDoctor,
getDoctorTimings,
getDoctorTimingById,
getDoctorByDoctorId,
} from "../controllers/doctor.controller.js";
import jwtAuthMiddleware from "../middleware/auth.js";
const router = express.Router();
router.get("/getAll", getAllDoctors);
router.get("/:doctorId", getDoctorByDoctorId);
router.get("/getTimings", getDoctorTimings);
router.get("/getTimings/:doctorId", getDoctorTimingById);
router.post("/", jwtAuthMiddleware, createDoctor);
router.patch("/:doctorId", jwtAuthMiddleware, updateDoctor);
router.delete("/:doctorId", jwtAuthMiddleware, deleteDoctor);
export default router;

View File

@@ -0,0 +1,19 @@
import express from "express";
import {
createInquiry,
getInquiries,
getInquiry,
deleteInquiry,
} from "../controllers/inquiry.controller.js";
import jwtAuthMiddleware from "../middleware/auth.js";
const router = express.Router();
router.post("/", createInquiry);
router.get("/getAll", getInquiries);
router.get("/:id", getInquiry);
router.delete("/:id", jwtAuthMiddleware, deleteInquiry);
export default router;

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

18980
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,10 +10,21 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@editorjs/code": "^2.9.4",
"@editorjs/delimiter": "^1.4.2",
"@editorjs/editorjs": "^2.31.5",
"@editorjs/embed": "^2.8.0",
"@editorjs/header": "^2.8.8",
"@editorjs/image": "^2.10.3",
"@editorjs/list": "^2.0.9",
"@editorjs/quote": "^2.7.6",
"@editorjs/table": "^2.4.5",
"@fontsource-variable/geist": "^5.2.8", "@fontsource-variable/geist": "^5.2.8",
"@tailwindcss/postcss": "^4.2.1",
"axios": "^1.13.6", "axios": "^1.13.6",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1",
"lucide-react": "^0.577.0", "lucide-react": "^0.577.0",
"radix-ui": "^1.4.3", "radix-ui": "^1.4.3",
"react": "^19.2.0", "react": "^19.2.0",
@@ -21,21 +32,23 @@
"react-router-dom": "^7.13.1", "react-router-dom": "^7.13.1",
"shadcn": "^4.0.5", "shadcn": "^4.0.5",
"tailwind-merge": "^3.5.0", "tailwind-merge": "^3.5.0",
"tailwindcss": "^4.2.1",
"tailwindcss-animate": "^1.0.7",
"tw-animate-css": "^1.4.0" "tw-animate-css": "^1.4.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.1", "@eslint/js": "^9.39.1",
"@types/estree": "^1.0.8",
"@types/json-schema": "^7.0.15",
"@types/node": "^24.12.0", "@types/node": "^24.12.0",
"@types/react": "^19.2.7", "@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.4", "@vitejs/plugin-react": "^5.1.4",
"autoprefixer": "^10.4.27",
"eslint": "^9.39.1", "eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24", "eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0", "globals": "^16.5.0",
"postcss": "^8.5.8", "postcss": "^8.5.8",
"tailwindcss": "^3.4.19",
"typescript": "~5.9.3", "typescript": "~5.9.3",
"typescript-eslint": "^8.48.0", "typescript-eslint": "^8.48.0",
"vite": "^7.3.1" "vite": "^7.3.1"

View File

@@ -1,6 +1,5 @@
export default { export default {
plugins: { plugins: {
tailwindcss: {}, "@tailwindcss/postcss": {},
autoprefixer: {}, },
}, };
}

View File

@@ -1,42 +1,42 @@
#root { #root {
max-width: 1280px; max-width: 1280px;
margin: 0 auto; margin: 0 auto;
padding: 2rem; padding: 2rem;
text-align: center; text-align: center;
} }
.logo { .logo {
height: 6em; height: 6em;
padding: 1.5em; padding: 1.5em;
will-change: filter; will-change: filter;
transition: filter 300ms; transition: filter 300ms;
} }
.logo:hover { .logo:hover {
filter: drop-shadow(0 0 2em #646cffaa); filter: drop-shadow(0 0 2em #646cffaa);
} }
.logo.react:hover { .logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa); filter: drop-shadow(0 0 2em #61dafbaa);
} }
@keyframes logo-spin { @keyframes logo-spin {
from { from {
transform: rotate(0deg); transform: rotate(0deg);
} }
to { to {
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
@media (prefers-reduced-motion: no-preference) { @media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo { a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear; animation: logo-spin infinite 20s linear;
} }
} }
.card { .card {
padding: 2em; padding: 2em;
} }
.read-the-docs { .read-the-docs {
color: #888; color: #888;
} }

View File

@@ -1,45 +1,41 @@
import {BrowserRouter, Routes, Route} from "react-router-dom"; import {BrowserRouter, Routes, Route, Navigate} from "react-router-dom";
import Login from "@/pages/Login"; import Login from "@/pages/Login";
import Dashboard from "@/pages/Dashboard";
import Blog from "@/pages/Blog";
import Department from "@/pages/Department";
import ProtectedRoute from "./components/ProtectedRoutes/ProtectedRoutes"; import DashboardLayout from "./layouts/DashboardLayout";
// import ProtectedRoute from "./components/ProtectedRoutes/ProtectedRoutes";
import ProtectedRoute from "./auth/ProtectedRoute";
import PublicRoute from "./auth/PublicRoute";
import {AuthProvider} from "./context/AuthContext";
import Department from "./pages/Department";
import Doctor from "./pages/Doctor";
import Blog from "./pages/Blog";
import BlogEditorPage from "./pages/BlogEditor";
export default function App() { export default function App() {
return ( return (
<BrowserRouter> <BrowserRouter>
<Routes> <AuthProvider>
<Route path="/" element={<Login />} /> <Routes>
<Route element={<PublicRoute />}>
<Route path="/" element={<Login />} />
</Route>
<Route <Route element={<ProtectedRoute />}>
path="/dashboard" <Route element={<DashboardLayout />}>
element={ <Route path="/department" element={<Department />} />
<ProtectedRoute> <Route path="/doctor" element={<Doctor />} />
<Dashboard /> <Route path="/blog" element={<Blog />} />
</ProtectedRoute> <Route path="/blog/create" element={<BlogEditorPage />} />
} <Route path="/blog/edit/:id" element={<BlogEditorPage />} />
/> </Route>
</Route>
<Route <Route path="*" element={<Navigate to="/department" replace />} />
path="/blog" </Routes>
element={ </AuthProvider>
<ProtectedRoute>
<Blog />
</ProtectedRoute>
}
/>
<Route
path="/department"
element={
<ProtectedRoute>
<Department />
</ProtectedRoute>
}
/>
</Routes>
</BrowserRouter> </BrowserRouter>
); );
} }

12
frontend/src/api/auth.ts Normal file
View File

@@ -0,0 +1,12 @@
import apiClient from "./client";
export const loginApi = async (
username: string,
password: string,
): Promise<any> => {
const response = await apiClient.post("/auth/login/", {
username,
password,
});
return response.data;
};

48
frontend/src/api/blog.ts Normal file
View File

@@ -0,0 +1,48 @@
import apiClient from "@/api/client";
export interface Blog {
id?: number;
title: string;
writer: string;
image?: string;
content: any;
}
export const getAllBlogsApi = async () => {
const res = await apiClient.get("/blogs");
return res.data;
};
export const getBlogByIdApi = async (id: number) => {
const res = await apiClient.get(`/blogs/${id}`);
return res.data;
};
export const createBlogApi = async (data: Blog) => {
const res = await apiClient.post("/blogs", data);
return res.data;
};
export const updateBlogApi = async (id: number, data: Blog) => {
const res = await apiClient.put(`/blogs/${id}`, data);
return res.data;
};
export const deleteBlogApi = async (id: number) => {
const res = await apiClient.delete(`/blogs/${id}`);
return res.data;
};
/* IMAGE UPLOAD */
export const uploadImageApi = async (file: File) => {
const formData = new FormData();
formData.append("image", file);
const res = await apiClient.post("/upload/image", formData, {
headers: {
"Content-Type": "multipart/form-data",
},
});
return res.data;
};

View File

@@ -0,0 +1,48 @@
import axios from "axios";
import type {InternalAxiosRequestConfig} from "axios";
const BASE_URL: string = "http://localhost:3000/api";
const apiClient = axios.create({
baseURL: BASE_URL,
headers: {
"Content-Type": "application/json",
},
});
export const setAxiosAuthToken = (token: string | null): void => {
if (token) {
apiClient.defaults.headers.common["Authorization"] = `Bearer ${token}`;
} else {
delete apiClient.defaults.headers.common["Authorization"];
}
};
apiClient.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const token = localStorage.getItem("token");
if (token && config.headers) {
config.headers["Authorization"] = `Bearer ${token}`;
}
return config;
},
(error: any) => Promise.reject(error),
);
apiClient.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401) {
console.error("Unauthorized - token missing or invalid");
localStorage.removeItem("token");
window.location.href = "/login";
}
return Promise.reject(error);
},
);
export default apiClient;

View File

@@ -0,0 +1,49 @@
import apiClient from "@/api/client";
export interface Department {
departmentId: string;
name: string;
para1: string;
para2: string;
para3: string;
facilities: string;
services: string;
}
export const getDepartmentsApi = async () => {
const res = await apiClient.get("/departments/getAll");
return res.data;
};
export const createDepartmentApi = async (data: {
departmentId: string;
name: string;
para1?: string;
para2?: string;
para3?: string;
facilities?: string;
services?: string;
}) => {
const res = await apiClient.post("/departments", data);
return res.data;
};
export const updateDepartmentApi = async (
departmentId: string,
data: {
name?: string;
para1?: string;
para2?: string;
para3?: string;
facilities?: string;
services?: string;
},
) => {
const res = await apiClient.put(`/departments/${departmentId}`, data);
return res.data;
};
export const deleteDepartmentApi = async (departmentId: string) => {
const res = await apiClient.delete(`/departments/${departmentId}`);
return res.data;
};

View File

@@ -0,0 +1,56 @@
import apiClient from "@/api/client";
export interface Doctor {
doctorId: string;
name: string;
designation?: string;
workingStatus?: string;
qualification?: string;
departments: {
departmentId: string;
timing?: {
monday?: string;
tuesday?: string;
wednesday?: string;
thursday?: string;
friday?: string;
saturday?: string;
sunday?: string;
additional?: string;
};
}[];
}
export const getDoctorsApi = async () => {
const res = await apiClient.get("/doctors/getAll");
return res.data;
};
export const getDoctorByIdApi = async (doctorId: string) => {
const res = await apiClient.get(`/doctors/${doctorId}`);
return res.data;
};
export const createDoctorApi = async (data: Doctor) => {
const res = await apiClient.post("/doctors", data);
return res.data;
};
export const updateDoctorApi = async (
doctorId: string,
data: Partial<Doctor>,
) => {
const res = await apiClient.patch(`/doctors/${doctorId}`, data);
return res.data;
};
export const deleteDoctorApi = async (doctorId: string) => {
const res = await apiClient.delete(`/doctors/${doctorId}`);
return res.data;
};
export const getDoctorTimingApi = async (doctorId: string) => {
const res = await apiClient.get(`/doctors/getTimings/${doctorId}`);
return res.data;
};

View File

@@ -0,0 +1,7 @@
import {Navigate, Outlet} from "react-router-dom";
import {useAuth} from "@/context/AuthContext";
export default function ProtectedRoute() {
const {token} = useAuth();
return token ? <Outlet /> : <Navigate to="/" replace />;
}

View File

@@ -0,0 +1,7 @@
import {Navigate, Outlet} from "react-router-dom";
import {useAuth} from "@/context/AuthContext";
export default function PublicRoute() {
const {token} = useAuth();
return token ? <Navigate to="/dashboard" replace /> : <Outlet />;
}

View File

@@ -1,20 +0,0 @@
import Sidebar from "./Sidebar"
export default function DashboardLayout({children}:{children:React.ReactNode}){
return(
<div className="flex">
<Sidebar/>
<div className="flex-1 p-6 bg-slate-50 min-h-screen">
{children}
</div>
</div>
)
}

View File

@@ -0,0 +1,35 @@
import {useState, useEffect} from "react";
import {useAuth} from "@/context/AuthContext";
import {Button} from "@/components/ui/button";
import {Switch} from "@/components/ui/switch";
import {log} from "console";
export default function Header() {
const {user, logout} = useAuth();
const [darkMode, setDarkMode] = useState<boolean>(false);
useEffect(() => {
if (darkMode) document.documentElement.classList.add("dark");
else document.documentElement.classList.remove("dark");
}, [darkMode]);
return (
<header className="border-b bg-card">
<div className="flex items-center justify-between px-6 h-16">
<div>
<p className="text-sm text-muted-foreground">Welcome back</p>
<p className="font-semibold">{user?.username}</p>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<span className="text-sm">Dark</span>
<Switch checked={darkMode} onCheckedChange={setDarkMode} />
</div>
<Button variant="destructive" onClick={logout}>
Logout
</Button>
</div>
</div>
</header>
);
}

View File

@@ -1,17 +1,50 @@
import {Link} from "react-router-dom"; import {Link, useLocation} from "react-router-dom";
import {Button} from "@/components/ui/button";
import {Separator} from "@/components/ui/separator";
export default function Sidebar() { export default function Sidebar() {
const location = useLocation();
const navItems = [
{
name: "Department",
path: "/department",
},
{
name: "Doctor",
path: "/doctor",
},
{
name: "Blog",
path: "/blog",
},
];
return ( return (
<div className="w-[220px] h-screen border-r bg-white p-4"> <div className="w-64 border-r bg-card">
<h2 className="text-lg font-semibold mb-6">Admin</h2> <div className="p-6">
<h2 className="text-xl font-bold">GG Dashboard</h2>
<div className="space-y-3">
<Link to="/dashboard">Dashboard</Link>
<Link to="/blog">Blog</Link>
<Link to="/department">Department</Link>
</div> </div>
<Separator />
<nav className="p-4 space-y-2">
{navItems.map((item) => {
const active = location.pathname === item.path;
return (
<Link key={item.path} to={item.path}>
<Button
variant={active ? "secondary" : "ghost"}
className="w-full justify-start"
>
{item.name}
</Button>
</Link>
);
})}
</nav>
</div> </div>
); );
} }

View File

@@ -0,0 +1,193 @@
import * as React from "react"
import { Command as CommandPrimitive } from "cmdk"
import { cn } from "@/lib/utils"
import {
Dialog,
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>) {
return (
<CommandPrimitive
data-slot="command"
className={cn(
"flex size-full flex-col overflow-hidden rounded-xl! bg-popover p-1 text-popover-foreground",
className
)}
{...props}
/>
)
}
function CommandDialog({
title = "Command Palette",
description = "Search for a command to run...",
children,
className,
showCloseButton = false,
...props
}: React.ComponentProps<typeof Dialog> & {
title?: string
description?: string
className?: string
showCloseButton?: boolean
}) {
return (
<Dialog {...props}>
<DialogHeader className="sr-only">
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogContent
className={cn(
"top-1/3 translate-y-0 overflow-hidden rounded-xl! p-0",
className
)}
showCloseButton={showCloseButton}
>
{children}
</DialogContent>
</Dialog>
)
}
function CommandInput({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
return (
<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!">
<CommandPrimitive.Input
data-slot="command-input"
className={cn(
"w-full text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
<InputGroupAddon>
<SearchIcon className="size-4 shrink-0 opacity-50" />
</InputGroupAddon>
</InputGroup>
</div>
)
}
function CommandList({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.List>) {
return (
<CommandPrimitive.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(
"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 (
<CommandPrimitive.Separator
data-slot="command-separator"
className={cn("-mx-1 h-px bg-border", className)}
{...props}
/>
)
}
function CommandItem({
className,
children,
...props
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
return (
<CommandPrimitive.Item
data-slot="command-item"
className={cn(
"group/command-item relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none in-data-[slot=dialog-content]:rounded-lg! data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-selected:bg-muted data-selected:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-selected:*:[svg]:text-foreground",
className
)}
{...props}
>
{children}
<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>
)
}
function CommandShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="command-shortcut"
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground group-data-selected/command-item:text-foreground",
className
)}
{...props}
/>
)
}
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
}

View File

@@ -0,0 +1,163 @@
import * as React from "react"
import { Dialog as DialogPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { XIcon } from "lucide-react"
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
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",
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
return (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
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-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
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close data-slot="dialog-close" asChild>
<Button
variant="ghost"
className="absolute top-2 right-2"
size="icon-sm"
>
<XIcon
/>
<span className="sr-only">Close</span>
</Button>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2", className)}
{...props}
/>
)
}
function DialogFooter({
className,
showCloseButton = false,
children,
...props
}: React.ComponentProps<"div"> & {
showCloseButton?: boolean
}) {
return (
<div
data-slot="dialog-footer"
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",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close asChild>
<Button variant="outline">Close</Button>
</DialogPrimitive.Close>
)}
</div>
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-base leading-none font-medium", className)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn(
"text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
className
)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}

View File

@@ -0,0 +1,156 @@
"use client"
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="input-group"
role="group"
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",
className
)}
{...props}
/>
)
}
const inputGroupAddonVariants = cva(
"flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
{
variants: {
align: {
"inline-start":
"order-first pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem]",
"inline-end":
"order-last pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem]",
"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: {
align: "inline-start",
},
}
)
function InputGroupAddon({
className,
align = "inline-start",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
return (
<div
role="group"
data-slot="input-group-addon"
data-align={align}
className={cn(inputGroupAddonVariants({ align }), className)}
onClick={(e) => {
if ((e.target as HTMLElement).closest("button")) {
return
}
e.currentTarget.parentElement?.querySelector("input")?.focus()
}}
{...props}
/>
)
}
const inputGroupButtonVariants = cva(
"flex items-center gap-2 text-sm shadow-none",
{
variants: {
size: {
xs: "h-6 gap-1 rounded-[calc(var(--radius)-3px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5",
sm: "",
"icon-xs":
"size-6 rounded-[calc(var(--radius)-3px)] p-0 has-[>svg]:p-0",
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
},
},
defaultVariants: {
size: "xs",
},
}
)
function InputGroupButton({
className,
type = "button",
variant = "ghost",
size = "xs",
...props
}: Omit<React.ComponentProps<typeof Button>, "size"> &
VariantProps<typeof inputGroupButtonVariants>) {
return (
<Button
type={type}
data-size={size}
variant={variant}
className={cn(inputGroupButtonVariants({ size }), className)}
{...props}
/>
)
}
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
className={cn(
"flex items-center gap-2 text-sm text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function InputGroupInput({
className,
...props
}: React.ComponentProps<"input">) {
return (
<Input
data-slot="input-group-control"
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",
className
)}
{...props}
/>
)
}
function InputGroupTextarea({
className,
...props
}: React.ComponentProps<"textarea">) {
return (
<Textarea
data-slot="input-group-control"
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",
className
)}
{...props}
/>
)
}
export {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupText,
InputGroupInput,
InputGroupTextarea,
}

View File

@@ -0,0 +1,87 @@
import * as React from "react"
import { Popover as PopoverPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function Popover({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />
}
function PopoverTrigger({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
}
function PopoverContent({
className,
align = "center",
sideOffset = 4,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
data-slot="popover-content"
align={align}
sideOffset={sideOffset}
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",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
)
}
function PopoverAnchor({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
}
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}
/>
)
}
function PopoverTitle({ className, ...props }: React.ComponentProps<"h2">) {
return (
<div
data-slot="popover-title"
className={cn("font-medium", className)}
{...props}
/>
)
}
function PopoverDescription({
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,
}

View File

@@ -0,0 +1,53 @@
import * as React from "react"
import { ScrollArea as ScrollAreaPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function ScrollArea({
className,
children,
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn("relative", className)}
{...props}
>
<ScrollAreaPrimitive.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"
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
)
}
function ScrollBar({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
data-slot="scroll-area-scrollbar"
data-orientation={orientation}
orientation={orientation}
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",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="relative flex-1 rounded-full bg-border"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
)
}
export { ScrollArea, ScrollBar }

View File

@@ -0,0 +1,31 @@
import * as React from "react";
import {Switch as SwitchPrimitive} from "radix-ui";
import {cn} from "@/lib/utils";
function Switch({
className,
size = "default",
...props
}: React.ComponentProps<typeof SwitchPrimitive.Root> & {
size?: "sm" | "default";
}) {
return (
<SwitchPrimitive.Root
data-slot="switch"
data-size={size}
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",
className,
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className="pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] dark:data-checked:bg-primary-foreground group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0 dark:data-unchecked:bg-foreground"
/>
</SwitchPrimitive.Root>
);
}
export {Switch};

View File

@@ -0,0 +1,114 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Table({ className, ...props }: React.ComponentProps<"table">) {
return (
<div
data-slot="table-container"
className="relative w-full overflow-x-auto"
>
<table
data-slot="table"
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
)
}
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
return (
<thead
data-slot="table-header"
className={cn("[&_tr]:border-b", className)}
{...props}
/>
)
}
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
return (
<tbody
data-slot="table-body"
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
)
}
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
return (
<tfoot
data-slot="table-footer"
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
)
}
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
return (
<tr
data-slot="table-row"
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props}
/>
)
}
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
return (
<th
data-slot="table-head"
className={cn(
"h-10 px-2 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0",
className
)}
{...props}
/>
)
}
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
return (
<td
data-slot="table-cell"
className={cn(
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0",
className
)}
{...props}
/>
)
}
function TableCaption({
className,
...props
}: React.ComponentProps<"caption">) {
return (
<caption
data-slot="table-caption"
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
)
}
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

View File

@@ -0,0 +1,18 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={cn(
"flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 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
)}
{...props}
/>
)
}
export { Textarea }

View File

@@ -1,3 +1,124 @@
@tailwind base; @import "tailwindcss";
@tailwind components; @import "tw-animate-css";
@tailwind utilities; @import "shadcn/tailwind.css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px);
--radius-3xl: calc(var(--radius) + 12px);
--radius-4xl: calc(var(--radius) + 16px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823);
--card: oklch(1 0 0);
--card-foreground: oklch(0.141 0.005 285.823);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823);
--primary: oklch(0.21 0.006 285.885);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938);
--accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.21 0.006 285.885);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32);
--ring: oklch(0.705 0.015 286.067);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.141 0.005 285.823);
--sidebar-primary: oklch(0.21 0.006 285.885);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.967 0.001 286.375);
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
--sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.705 0.015 286.067);
}
.dark {
--background: oklch(0.141 0.005 285.823);
--foreground: oklch(0.985 0 0);
--card: oklch(0.21 0.006 285.885);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.21 0.006 285.885);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.92 0.004 286.32);
--primary-foreground: oklch(0.21 0.006 285.885);
--secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.274 0.006 286.033);
--muted-foreground: oklch(0.705 0.015 286.067);
--accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.552 0.016 285.938);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.21 0.006 285.885);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.274 0.006 286.033);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.552 0.016 285.938);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

View File

@@ -0,0 +1,18 @@
import {Outlet} from "react-router-dom";
import Sidebar from "@/components/layout/Sidebar";
import Header from "@/components/layout/Header";
export default function DashboardLayout() {
return (
<div className="flex min-h-screen bg-background">
<Sidebar />
<div className="flex flex-col flex-1">
<Header />
<main className="flex-1 p-6">
<Outlet />
</main>
</div>
</div>
);
}

View File

@@ -1,13 +1,182 @@
import DashboardLayout from "@/components/layout/DashboardLayout"; import {useState, useEffect, useCallback} from "react";
import {AxiosError} from "axios";
import {useNavigate} from "react-router-dom";
import {getAllBlogsApi, deleteBlogApi} from "@/api/blog";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
import {Button} from "@/components/ui/button";
import {Input} from "@/components/ui/input";
import {Loader2, RefreshCw, Plus, Pencil, Trash} from "lucide-react";
interface Blog {
id: number;
title: string;
writer: string;
image: string | null;
}
export default function BlogPage() {
const [blogs, setBlogs] = useState<Blog[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [searchText, setSearchText] = useState("");
const navigate = useNavigate();
const fetchBlogs = useCallback(async () => {
setLoading(true);
setError("");
try {
const res = await getAllBlogsApi();
if (Array.isArray(res)) {
setBlogs(res);
} else {
setBlogs([]);
}
} catch (err) {
if (err instanceof AxiosError) {
setError(err.response?.data?.message || "Failed to load blogs");
} else {
setError("Something went wrong");
}
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchBlogs();
}, [fetchBlogs]);
const filteredBlogs = blogs.filter((b) => {
const text = searchText.toLowerCase();
return (
b.title?.toLowerCase().includes(text) ||
b.writer?.toLowerCase().includes(text)
);
});
async function handleDelete(id: number) {
const confirmDelete = confirm("Delete this blog?");
if (!confirmDelete) return;
try {
await deleteBlogApi(id);
fetchBlogs();
} catch (error) {
console.error(error);
}
}
export default function Blog() {
return ( return (
<DashboardLayout> <div className="p-6 space-y-6">
<h1 className="text-xl font-semibold mb-4">Blog Management</h1> <div className="flex flex-col md:flex-row md:justify-between md:items-center gap-3">
<h1 className="text-2xl font-bold">Blogs</h1>
<button className="px-4 py-2 bg-black text-white rounded"> <div className="flex flex-wrap gap-3">
Create Blog <Input
</button> placeholder="Search blog..."
</DashboardLayout> value={searchText}
onChange={(e) => setSearchText(e.target.value)}
className="w-[220px]"
/>
<Button variant="outline" onClick={fetchBlogs} disabled={loading}>
<RefreshCw className="mr-2 h-4 w-4" />
Refresh
</Button>
<Button onClick={() => navigate("/blog/create")}>
<Plus className="mr-2 h-4 w-4" />
Add Blog
</Button>
</div>
</div>
{error && (
<div className="p-4 text-red-600 bg-red-50 border rounded-md">
{error}
</div>
)}
<Card>
<CardHeader>
<CardTitle>Blog List</CardTitle>
</CardHeader>
<CardContent>
<div className="border rounded-md overflow-x-auto max-w-full">
<Table>
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead>Title</TableHead>
<TableHead>Writer</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={4} className="text-center">
<Loader2 className="h-6 w-6 animate-spin mx-auto" />
</TableCell>
</TableRow>
) : filteredBlogs.length === 0 ? (
<TableRow>
<TableCell colSpan={4} className="text-center">
No blogs found
</TableCell>
</TableRow>
) : (
filteredBlogs.map((blog) => (
<TableRow key={blog.id}>
<TableCell>{blog.id}</TableCell>
<TableCell>{blog.title}</TableCell>
<TableCell>{blog.writer}</TableCell>
<TableCell className="flex gap-2">
<Button
size="sm"
variant="outline"
onClick={() => navigate(`/blog/edit/${blog.id}`)}
>
<Pencil className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="destructive"
onClick={() => handleDelete(blog.id)}
>
<Trash className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
</div>
); );
} }

View File

@@ -0,0 +1,208 @@
import {useEffect, useRef, useState} from "react";
import {useNavigate, useParams} from "react-router-dom";
import EditorJS, {OutputData} from "@editorjs/editorjs";
import Header from "@editorjs/header";
import List from "@editorjs/list";
import ImageTool from "@editorjs/image";
import Quote from "@editorjs/quote";
import Table from "@editorjs/table";
import CodeTool from "@editorjs/code";
import Embed from "@editorjs/embed";
import Delimiter from "@editorjs/delimiter";
import {
createBlogApi,
updateBlogApi,
getBlogByIdApi,
uploadImageApi,
} from "@/api/blog";
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
import {Input} from "@/components/ui/input";
import {Button} from "@/components/ui/button";
export default function BlogEditorPage() {
const {id} = useParams();
const navigate = useNavigate();
const editorRef = useRef<EditorJS | null>(null);
const hasInitialized = useRef(false);
const hasRenderedContent = useRef(false);
const [title, setTitle] = useState("");
const [writer, setWriter] = useState("");
const [coverImage, setCoverImage] = useState("");
const [loading, setLoading] = useState(false);
const isEdit = Boolean(id);
useEffect(() => {
if (hasInitialized.current) return;
hasInitialized.current = true;
let editor: EditorJS;
const initEditor = async () => {
editor = new EditorJS({
holder: "editorjs",
placeholder: "Write blog content...",
tools: {
header: {
class: Header,
inlineToolbar: true,
config: {
placeholder: "Enter heading",
levels: [1, 2, 3, 4],
defaultLevel: 2,
},
},
list: {
class: List,
inlineToolbar: true,
config: {
defaultStyle: "unordered",
},
},
quote: Quote,
table: Table,
code: CodeTool,
embed: Embed,
delimiter: Delimiter,
image: {
class: ImageTool,
config: {
uploader: {
uploadByFile: async (file: File) => {
const res = await uploadImageApi(file);
return {
success: 1,
file: {url: res.file.url},
};
},
},
},
},
},
});
await editor.isReady;
editorRef.current = editor;
if (isEdit && id && !hasRenderedContent.current) {
try {
const res = await getBlogByIdApi(Number(id));
setTitle(res.title);
setWriter(res.writer);
setCoverImage(res.image || "");
if (res.content) {
await editor.blocks.clear();
await editor.render(res.content);
hasRenderedContent.current = true;
}
} catch (err) {
console.error(err);
}
}
};
initEditor();
}, [id, isEdit]);
const handleCoverUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
try {
const res = await uploadImageApi(file);
setCoverImage(res.file.url);
} catch (err) {
console.error(err);
}
};
const handleSave = async () => {
if (!editorRef.current) return;
setLoading(true);
try {
const content: OutputData = await editorRef.current.save();
const payload = {
title,
writer,
image: coverImage,
content,
isActive: true,
};
if (isEdit) {
await updateBlogApi(Number(id), payload);
} else {
await createBlogApi(payload);
}
navigate("/blog");
} catch (err) {
console.error(err);
} finally {
setLoading(false);
}
};
return (
<div className="p-6 max-w-5xl mx-auto space-y-6">
<Card>
<CardHeader>
<CardTitle>{isEdit ? "Edit Blog" : "Create Blog"}</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<Input
placeholder="Blog Title"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<Input
placeholder="Writer Name"
value={writer}
onChange={(e) => setWriter(e.target.value)}
/>
<div className="space-y-2">
<label className="text-sm font-medium">Cover Image</label>
<Input type="file" onChange={handleCoverUpload} />
{coverImage && (
<img
src={coverImage}
alt="cover"
className="w-full max-h-[250px] object-cover rounded-md"
/>
)}
</div>
<div
id="editorjs"
className="border rounded-md p-4 bg-white min-h-[300px]"
/>
<Button onClick={handleSave} disabled={loading}>
{loading ? "Saving..." : "Save Blog"}
</Button>
</CardContent>
</Card>
</div>
);
}

View File

@@ -1,17 +0,0 @@
import DashboardLayout from "@/components/layout/DashboardLayout";
export default function Dashboard() {
return (
<DashboardLayout>
<h1 className="text-2xl font-bold mb-6">Dashboard</h1>
<div className="grid grid-cols-3 gap-6">
<div className="p-6 bg-white shadow rounded">Blogs</div>
<div className="p-6 bg-white shadow rounded">Departments</div>
<div className="p-6 bg-white shadow rounded">Users</div>
</div>
</DashboardLayout>
);
}

View File

@@ -1,9 +0,0 @@
import DashboardLayout from "@/components/layout/DashboardLayout";
export default function Department() {
return (
<DashboardLayout>
<h1 className="text-xl font-semibold mb-4">Departments</h1>
</DashboardLayout>
);
}

View File

@@ -0,0 +1,358 @@
import {useState, useEffect, useCallback} from "react";
import {AxiosError} from "axios";
import {
getDepartmentsApi,
createDepartmentApi,
updateDepartmentApi,
deleteDepartmentApi,
} from "@/api/department";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
import {Button} from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import {Input} from "@/components/ui/input";
import {Textarea} from "@/components/ui/textarea";
import {Loader2, RefreshCw, Plus, Pencil, Trash} from "lucide-react";
interface Department {
departmentId: string;
name: string;
para1: string;
para2: string;
para3: string;
facilities: string;
services: string;
}
export default function DepartmentPage() {
const [departments, setDepartments] = useState<Department[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [openModal, setOpenModal] = useState(false);
const [editing, setEditing] = useState<Department | null>(null);
const [searchText, setSearchText] = useState("");
const [form, setForm] = useState<Department>({
departmentId: "",
name: "",
para1: "",
para2: "",
para3: "",
facilities: "",
services: "",
});
const fetchDepartments = useCallback(async () => {
setLoading(true);
setError("");
try {
const res = await getDepartmentsApi();
setDepartments(res?.data || []);
} catch (err) {
if (err instanceof AxiosError) {
setError(err.response?.data?.message || "Failed to load departments");
} else {
setError("Something went wrong");
}
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchDepartments();
}, [fetchDepartments]);
const filteredDepartments = departments.filter((dep) => {
const text = searchText.toLowerCase();
return (
dep.name.toLowerCase().includes(text) ||
dep.departmentId.toLowerCase().includes(text) ||
dep.para1.toLowerCase().includes(text) ||
dep.para2.toLowerCase().includes(text) ||
dep.para3.toLowerCase().includes(text) ||
dep.facilities.toLowerCase().includes(text) ||
dep.services.toLowerCase().includes(text)
);
});
function handleChange(
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
) {
setForm({
...form,
[e.target.name]: e.target.value,
});
}
function openAdd() {
setEditing(null);
setForm({
departmentId: "",
name: "",
para1: "",
para2: "",
para3: "",
facilities: "",
services: "",
});
setOpenModal(true);
}
function openEdit(dep: Department) {
setEditing(dep);
setForm(dep);
setOpenModal(true);
}
async function handleSubmit() {
try {
if (editing) {
const {departmentId, ...updateData} = form;
await updateDepartmentApi(editing.departmentId, updateData);
} else {
await createDepartmentApi(form);
}
setOpenModal(false);
fetchDepartments();
} catch (error) {
console.error(error);
}
}
async function handleDelete(departmentId: string) {
const confirmDelete = confirm("Delete this department?");
if (!confirmDelete) return;
try {
await deleteDepartmentApi(departmentId);
fetchDepartments();
} catch (error) {
console.error(error);
}
}
return (
<div className="p-6 space-y-6">
{/* HEADER */}
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-3">
<h1 className="text-2xl font-bold">Departments</h1>
<div className="flex flex-wrap gap-3">
<Input
placeholder="Search department..."
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
className="w-[220px]"
/>
<Button
variant="outline"
onClick={fetchDepartments}
disabled={loading}
>
<RefreshCw className="mr-2 h-4 w-4" />
Refresh
</Button>
<Button onClick={openAdd}>
<Plus className="mr-2 h-4 w-4" />
Add Department
</Button>
</div>
</div>
{error && (
<div className="p-4 text-red-600 bg-red-50 border rounded-md">
{error}
</div>
)}
<Card>
<CardHeader>
<CardTitle>Department List</CardTitle>
</CardHeader>
<CardContent>
<div className="border rounded-md overflow-x-auto max-w-full">
<Table>
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead>Name</TableHead>
<TableHead>Para1</TableHead>
<TableHead>Para2</TableHead>
<TableHead>Para3</TableHead>
<TableHead>Facilities</TableHead>
<TableHead>Services</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={8} className="text-center">
<Loader2 className="h-6 w-6 animate-spin mx-auto" />
</TableCell>
</TableRow>
) : filteredDepartments.length === 0 ? (
<TableRow>
<TableCell colSpan={8} className="text-center">
No departments found
</TableCell>
</TableRow>
) : (
filteredDepartments.map((dep) => (
<TableRow key={dep.departmentId}>
<TableCell>{dep.departmentId}</TableCell>
<TableCell>{dep.name}</TableCell>
<TableCell className="max-w-[300px] whitespace-normal break-words">
{dep.para1}
</TableCell>
<TableCell className="max-w-[300px] whitespace-normal break-words">
{dep.para2}
</TableCell>
<TableCell className="max-w-[300px] whitespace-normal break-words">
{dep.para3}
</TableCell>
<TableCell className="max-w-[300px] whitespace-normal break-words">
{dep.facilities}
</TableCell>
<TableCell className="max-w-[300px] whitespace-normal break-words">
{dep.services}
</TableCell>
<TableCell className="flex gap-2">
<Button
size="sm"
variant="outline"
onClick={() => openEdit(dep)}
>
<Pencil className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="destructive"
onClick={() => handleDelete(dep.departmentId)}
>
<Trash className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
{/* MODAL */}
<Dialog open={openModal} onOpenChange={setOpenModal}>
<DialogContent className="max-w-4xl">
<DialogHeader>
<DialogTitle>
{editing ? "Edit Department" : "Add Department"}
</DialogTitle>
</DialogHeader>
<div className="space-y-4 max-h-[70vh] overflow-y-auto pr-2">
<Input
name="departmentId"
placeholder="Department ID"
value={form.departmentId}
onChange={handleChange}
disabled={!!editing}
/>
<Input
name="name"
placeholder="Department Name"
value={form.name}
onChange={handleChange}
/>
<Textarea
name="para1"
placeholder="Paragraph 1"
value={form.para1}
onChange={handleChange}
/>
<Textarea
name="para2"
placeholder="Paragraph 2"
value={form.para2}
onChange={handleChange}
/>
<Textarea
name="para3"
placeholder="Paragraph 3"
value={form.para3}
onChange={handleChange}
/>
<Textarea
name="facilities"
placeholder="Facilities"
value={form.facilities}
onChange={handleChange}
/>
<Textarea
name="services"
placeholder="Services"
value={form.services}
onChange={handleChange}
/>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setOpenModal(false)}>
Cancel
</Button>
<Button onClick={handleSubmit}>
{editing ? "Update" : "Create"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

View File

@@ -0,0 +1,471 @@
import {useState, useEffect, useCallback} from "react";
import {
getDoctorsApi,
createDoctorApi,
updateDoctorApi,
deleteDoctorApi,
getDoctorTimingApi,
} from "@/api/doctor";
import {getDepartmentsApi} from "@/api/department";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
import {Button} from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import {Popover, PopoverContent, PopoverTrigger} from "@/components/ui/popover";
import {
Command,
CommandGroup,
CommandItem,
CommandInput,
} from "@/components/ui/command";
import {Input} from "@/components/ui/input";
import {Loader2, Plus, Pencil, Trash, RefreshCw} from "lucide-react";
interface Department {
departmentId: string;
name: string;
}
export default function DoctorPage() {
const [doctors, setDoctors] = useState<any[]>([]);
const [departments, setDepartments] = useState<Department[]>([]);
const [loading, setLoading] = useState(true);
const [openModal, setOpenModal] = useState(false);
const [editing, setEditing] = useState<any>(null);
const [searchText, setSearchText] = useState("");
const [filterDepartment, setFilterDepartment] = useState("");
const [form, setForm] = useState<any>({
doctorId: "",
name: "",
designation: "",
workingStatus: "",
qualification: "",
departments: [],
});
const fetchAll = useCallback(async () => {
setLoading(true);
try {
const [docRes, depRes] = await Promise.all([
getDoctorsApi(),
getDepartmentsApi(),
]);
setDoctors(docRes?.data || []);
setDepartments(depRes?.data || []);
} catch (err) {
console.error(err);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchAll();
}, [fetchAll]);
const filteredDoctors = doctors.filter((doc) => {
const matchesSearch =
doc.name.toLowerCase().includes(searchText.toLowerCase()) ||
doc.doctorId.toLowerCase().includes(searchText.toLowerCase());
const matchesDepartment = filterDepartment
? doc.departments?.some((d: any) => d.departmentId === filterDepartment)
: true;
return matchesSearch && matchesDepartment;
});
function handleChange(e: any) {
setForm({...form, [e.target.name]: e.target.value});
}
function handleDepartmentChange(depId: string) {
const exists = form.departments.find((d: any) => d.departmentId === depId);
if (exists) {
setForm({
...form,
departments: form.departments.filter(
(d: any) => d.departmentId !== depId,
),
});
} else {
setForm({
...form,
departments: [...form.departments, {departmentId: depId, timing: {}}],
});
}
}
function handleTimingChange(depId: string, day: string, value: string) {
setForm({
...form,
departments: form.departments.map((d: any) =>
d.departmentId === depId
? {
...d,
timing: {...d.timing, [day]: value},
}
: d,
),
});
}
function openAdd() {
setEditing(null);
setForm({
doctorId: "",
name: "",
designation: "",
workingStatus: "",
qualification: "",
departments: [],
});
setOpenModal(true);
}
async function openEdit(doc: any) {
setEditing(doc);
try {
const timingRes = await getDoctorTimingApi(doc.doctorId);
const timingData = timingRes?.data?.departments || [];
const mappedDepartments = timingData.map((d: any) => ({
departmentId: d.departmentId,
timing: d.timing || {},
}));
setForm({
doctorId: doc.doctorId,
name: doc.name,
designation: doc.designation,
workingStatus: doc.workingStatus,
qualification: doc.qualification,
departments: mappedDepartments,
});
setOpenModal(true);
} catch (err) {
console.error(err);
}
}
async function handleSubmit() {
try {
if (editing) {
await updateDoctorApi(editing.doctorId, form);
} else {
await createDoctorApi(form);
}
setOpenModal(false);
fetchAll();
} catch (err) {
console.error(err);
}
}
async function handleDelete(id: string) {
if (!confirm("Delete doctor?")) return;
await deleteDoctorApi(id);
fetchAll();
}
return (
<div className="p-6 space-y-6">
{/* HEADER */}
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-3">
<h1 className="text-2xl font-bold">Doctors</h1>
<div className="flex flex-wrap gap-2">
<Input
placeholder="Search doctor..."
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
className="w-[200px]"
/>
<select
value={filterDepartment}
onChange={(e) => setFilterDepartment(e.target.value)}
className="border rounded px-2 py-1"
>
<option value="">All Departments</option>
{departments.map((dep) => (
<option key={dep.departmentId} value={dep.departmentId}>
{dep.name}
</option>
))}
</select>
<Button variant="outline" onClick={fetchAll} disabled={loading}>
<RefreshCw className="mr-2 h-4 w-4" />
Refresh
</Button>
<Button onClick={openAdd}>
<Plus className="mr-2 h-4 w-4" />
Add Doctor
</Button>
</div>
</div>
{/* TABLE */}
<Card>
<CardHeader>
<CardTitle>Doctor List</CardTitle>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<Table className="min-w-[1000px]">
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead>Name</TableHead>
<TableHead>Designation</TableHead>
<TableHead>Status</TableHead>
<TableHead>Qualification</TableHead>
<TableHead>Departments</TableHead>
<TableHead>Timing</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={8} className="text-center">
<Loader2 className="h-6 w-6 animate-spin mx-auto" />
</TableCell>
</TableRow>
) : filteredDoctors.length === 0 ? (
<TableRow>
<TableCell colSpan={8} className="text-center">
No doctors found
</TableCell>
</TableRow>
) : (
filteredDoctors.map((doc) => (
<TableRow key={doc.doctorId}>
<TableCell>{doc.doctorId}</TableCell>
<TableCell>{doc.name}</TableCell>
<TableCell>{doc.designation}</TableCell>
<TableCell>{doc.workingStatus}</TableCell>
<TableCell>{doc.qualification}</TableCell>
<TableCell>
{doc.departments
?.map((d: any) => d.departmentName)
.join(", ")}
</TableCell>
<TableCell className="max-w-[250px] whitespace-normal">
{doc.departments?.map((d: any) => (
<div key={d.departmentId}>
<b>{d.departmentName}:</b>{" "}
{JSON.stringify(d.timing)}
</div>
))}
</TableCell>
<TableCell className="flex gap-2">
<Button
size="sm"
variant="outline"
onClick={() => openEdit(doc)}
>
<Pencil className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="destructive"
onClick={() => handleDelete(doc.doctorId)}
>
<Trash className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
{/* MODAL */}
{/* MODAL */}
<Dialog open={openModal} onOpenChange={setOpenModal}>
<DialogContent className="overflow-y-auto max-h-[80vh] ">
<DialogHeader>
<DialogTitle>{editing ? "Edit Doctor" : "Add Doctor"}</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<Input
name="doctorId"
placeholder="Doctor ID"
value={form.doctorId}
onChange={handleChange}
disabled={!!editing}
/>
<Input
name="name"
placeholder="Name"
value={form.name}
onChange={handleChange}
/>
<Input
name="designation"
placeholder="Designation"
value={form.designation}
onChange={handleChange}
/>
<Input
name="workingStatus"
placeholder="Working Status"
value={form.workingStatus}
onChange={handleChange}
/>
<Input
name="qualification"
placeholder="Qualification"
value={form.qualification}
onChange={handleChange}
/>
{/* Departments */}
<div>
<p className="font-medium mb-2">Departments</p>
<Popover>
<PopoverTrigger asChild>
<Button className="w-full justify-between h-auto min-h-[40px]">
{form.departments.length > 0 ? (
<div className="flex flex-col items-start gap-1 text-left">
{form.departments.map((d: any) => {
const name = departments.find(
(dep) => dep.departmentId === d.departmentId,
)?.name;
return (
<span key={d.departmentId} className="text-sm">
{name}
</span>
);
})}
</div>
) : (
<span>Select Departments</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-[300px] p-0">
<Command>
<CommandInput placeholder="Search department..." />
<CommandGroup className="max-h-[250px] overflow-y-auto">
{departments.map((dep) => {
const selected = form.departments.some(
(d: any) => d.departmentId === dep.departmentId,
);
return (
<CommandItem
key={dep.departmentId}
className="flex justify-between"
onSelect={() =>
handleDepartmentChange(dep.departmentId)
}
>
<span>{dep.name}</span>
{selected && <span></span>}
</CommandItem>
);
})}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
</div>
{form.departments.map((dep: any) => {
const depName = departments.find(
(d) => d.departmentId === dep.departmentId,
)?.name;
return (
<div
key={dep.departmentId}
className="tw-border tw-p-3 tw-rounded"
>
<p className="tw-font-semibold">{depName}</p>
{[
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
"sunday",
"additional",
].map((day) => (
<Input
key={day}
placeholder={day}
value={dep.timing?.[day] || ""}
onChange={(e) =>
handleTimingChange(
dep.departmentId,
day,
e.target.value,
)
}
/>
))}
</div>
);
})}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setOpenModal(false)}>
Cancel
</Button>
<Button onClick={handleSubmit}>
{editing ? "Update" : "Create"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

View File

@@ -1,8 +1,4 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
export default { export default {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {},
},
plugins: [],
}; };