15 Commits
main ... dev

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
ded04fca7f feat : add front-end using shad cn 2026-03-12 17:56:52 +05:30
ARJUN S THAMPI
1206e51f6d feat :add package.json 2026-03-12 17:43:38 +05:30
79 changed files with 16565 additions and 55 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 {
provider = "prisma-client-js"
}
@@ -9,92 +8,169 @@ datasource db {
}
model User {
id Int @id @default(autoincrement())
username String @unique
password String
role String? @default("admin")
id Int @id @default(autoincrement())
username String @unique
password String
role String? @default("admin")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Doctor {
id Int @id @default(autoincrement())
doctorId String @unique
id Int @id @default(autoincrement())
doctorId String @unique
name String
designation String?
workingStatus String?
qualification String?
departments DoctorDepartment[]
departments DoctorDepartment[]
appointments Appointment[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Department {
id Int @id @default(autoincrement())
departmentId String @unique
name String
id Int @id @default(autoincrement())
departmentId String @unique
name String
para1 String?
para2 String?
para3 String?
facilities String?
services String?
para1 String?
para2 String?
para3 String?
facilities String?
services String?
doctors DoctorDepartment[]
doctors DoctorDepartment[]
appointments Appointment[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model DoctorDepartment {
id Int @id @default(autoincrement())
id Int @id @default(autoincrement())
doctorId Int
departmentId Int
doctorId Int
departmentId Int
doctor Doctor @relation(fields: [doctorId], references: [id])
department Department @relation(fields: [departmentId], references: [id])
doctor Doctor @relation(fields: [doctorId], references: [id])
department Department @relation(fields: [departmentId], references: [id])
timing DoctorTiming?
timing DoctorTiming?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([doctorId, departmentId])
}
model DoctorTiming {
id Int @id @default(autoincrement())
id Int @id @default(autoincrement())
doctorDepartmentId Int @unique
doctorDepartment DoctorDepartment @relation(fields: [doctorDepartmentId], references: [id])
doctorDepartmentId Int @unique
doctorDepartment DoctorDepartment @relation(fields: [doctorDepartmentId], references: [id])
monday String?
tuesday String?
wednesday String?
thursday String?
friday String?
saturday String?
sunday 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)
monday String?
tuesday String?
wednesday String?
thursday String?
friday String?
saturday String?
sunday 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())
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 blogRoutes from "./routes/blog.routes.js";
import uploadRoutes from "./routes/upload.routes.js";
import doctorRoutes from "./routes/doctor.routes.js";
import careerRoutes from "./routes/career.routes.js";
import candidateRoutes from "./routes/candidate.routes.js";
import appointmentRoutes from "./routes/appointment.routes.js";
import inquiryRoutes from "./routes/inquiry.routes.js";
import academicsResearchRoutes from "./routes/academicsResearch.routes.js";
dotenv.config();
@@ -35,6 +41,12 @@ app.use("/api/auth", authRoutes);
app.use("/api/blogs", blogRoutes);
app.use("/uploads", express.static("uploads"));
app.use("/api/upload", uploadRoutes);
app.use("/api/doctors", doctorRoutes);
app.use("/api/careers", careerRoutes);
app.use("/api/candidates", candidateRoutes);
app.use("/api/appointments", appointmentRoutes);
app.use("/api/inquiry", inquiryRoutes);
app.use("/api/academics", academicsResearchRoutes);
const PORT = process.env.PORT || 3000;
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) => ({
departmentId: dep.departmentId,
Department: dep.name,
name: dep.name,
para1: dep.para1 ?? "",
para2: dep.para2 ?? "",
para3: dep.para3 ?? "",
@@ -64,3 +64,56 @@ export async function createDepartment(req, res) {
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 {
getAllDepartments,
createDepartment,
updateDepartment,
deleteDepartment,
} from "../controllers/department.controller.js";
import jwtAuthMiddleware from "../middleware/auth.js";
@@ -12,5 +14,7 @@ router.get("/getAll", getAllDepartments);
// Protected
router.post("/", jwtAuthMiddleware, createDepartment);
router.put("/:departmentId", jwtAuthMiddleware, updateDepartment);
router.delete("/:departmentId", jwtAuthMiddleware, deleteDepartment);
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

24
frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

73
frontend/README.md Normal file
View File

@@ -0,0 +1,73 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```

25
frontend/components.json Normal file
View File

@@ -0,0 +1,25 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "radix-nova",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"rtl": false,
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"menuColor": "default",
"menuAccent": "subtle",
"registries": {}
}

23
frontend/eslint.config.js Normal file
View File

@@ -0,0 +1,23 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])

13
frontend/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>frontend</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

9579
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

56
frontend/package.json Normal file
View File

@@ -0,0 +1,56 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"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",
"@tailwindcss/postcss": "^4.2.1",
"axios": "^1.13.6",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"lucide-react": "^0.577.0",
"radix-ui": "^1.4.3",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^7.13.1",
"shadcn": "^4.0.5",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.2.1",
"tailwindcss-animate": "^1.0.7",
"tw-animate-css": "^1.4.0"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@types/estree": "^1.0.8",
"@types/json-schema": "^7.0.15",
"@types/node": "^24.12.0",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.4",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"postcss": "^8.5.8",
"typescript": "~5.9.3",
"typescript-eslint": "^8.48.0",
"vite": "^7.3.1"
}
}

View File

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

1
frontend/public/vite.svg Normal file
View File

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

After

Width:  |  Height:  |  Size: 1.5 KiB

42
frontend/src/App.css Normal file
View File

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

41
frontend/src/App.tsx Normal file
View File

@@ -0,0 +1,41 @@
import {BrowserRouter, Routes, Route, Navigate} from "react-router-dom";
import Login from "@/pages/Login";
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() {
return (
<BrowserRouter>
<AuthProvider>
<Routes>
<Route element={<PublicRoute />}>
<Route path="/" element={<Login />} />
</Route>
<Route element={<ProtectedRoute />}>
<Route element={<DashboardLayout />}>
<Route path="/department" element={<Department />} />
<Route path="/doctor" element={<Doctor />} />
<Route path="/blog" element={<Blog />} />
<Route path="/blog/create" element={<BlogEditorPage />} />
<Route path="/blog/edit/:id" element={<BlogEditorPage />} />
</Route>
</Route>
<Route path="*" element={<Navigate to="/department" replace />} />
</Routes>
</AuthProvider>
</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 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

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

@@ -0,0 +1,12 @@
import {Navigate} from "react-router-dom";
import {useAuth} from "@/context/AuthContext";
export default function ProtectedRoute({children}: any) {
const {token} = useAuth();
if (!token) {
return <Navigate to="/" />;
}
return children;
}

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

@@ -0,0 +1,50 @@
import {Link, useLocation} from "react-router-dom";
import {Button} from "@/components/ui/button";
import {Separator} from "@/components/ui/separator";
export default function Sidebar() {
const location = useLocation();
const navItems = [
{
name: "Department",
path: "/department",
},
{
name: "Doctor",
path: "/doctor",
},
{
name: "Blog",
path: "/blog",
},
];
return (
<div className="w-64 border-r bg-card">
<div className="p-6">
<h2 className="text-xl font-bold">GG Dashboard</h2>
</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>
);
}

View File

@@ -0,0 +1,67 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
outline:
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
ghost:
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
destructive:
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default:
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
icon: "size-8",
"icon-xs":
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
"icon-sm":
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
"icon-lg": "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant = "default",
size = "default",
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot.Root : "button"
return (
<Comp
data-slot="button"
data-variant={variant}
data-size={size}
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

View File

@@ -0,0 +1,103 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({
className,
size = "default",
...props
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
return (
<div
data-slot="card"
data-size={size}
className={cn(
"group/card flex flex-col gap-4 overflow-hidden rounded-xl bg-card py-4 text-sm text-card-foreground ring-1 ring-foreground/10 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn(
"text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
className
)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-4 group-data-[size=sm]/card:px-3", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn(
"flex items-center rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/card:p-3",
className
)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

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,19 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
className
)}
{...props}
/>
)
}
export { Input }

View File

@@ -0,0 +1,22 @@
import * as React from "react"
import { Label as LabelPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...props}
/>
)
}
export { Label }

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,26 @@
import * as React from "react"
import { Separator as SeparatorPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
data-slot="separator"
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
className
)}
{...props}
/>
)
}
export { Separator }

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

@@ -0,0 +1,62 @@
import {createContext, useContext, useState} from "react";
import api from "@/services/api";
type AuthContextType = {
user: any;
token: string | null;
login: (username: string, password: string) => Promise<void>;
logout: () => void;
};
const AuthContext = createContext<AuthContextType | null>(null);
export function AuthProvider({children}: {children: React.ReactNode}) {
const [token, setToken] = useState<string | null>(
localStorage.getItem("token"),
);
const [user, setUser] = useState(null);
async function login(username: string, password: string) {
const response = await api.post("/auth/login", {
username,
password,
});
const token = response.data.token;
localStorage.setItem("token", token);
setToken(token);
}
function logout() {
localStorage.removeItem("token");
setToken(null);
setUser(null);
}
return (
<AuthContext.Provider
value={{
user,
token,
login,
logout,
}}
>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used inside AuthProvider");
}
return context;
}

124
frontend/src/index.css Normal file
View File

@@ -0,0 +1,124 @@
@import "tailwindcss";
@import "tw-animate-css";
@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

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

14
frontend/src/main.tsx Normal file
View File

@@ -0,0 +1,14 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./index.css";
import {AuthProvider} from "./context/AuthContext";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<AuthProvider>
<App />
</AuthProvider>
</React.StrictMode>,
);

182
frontend/src/pages/Blog.tsx Normal file
View File

@@ -0,0 +1,182 @@
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);
}
}
return (
<div className="p-6 space-y-6">
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-3">
<h1 className="text-2xl font-bold">Blogs</h1>
<div className="flex flex-wrap gap-3">
<Input
placeholder="Search blog..."
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

@@ -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

@@ -0,0 +1,92 @@
import {useState, ChangeEvent} from "react";
import {useNavigate} from "react-router-dom";
import {useAuth} from "@/context/AuthContext";
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
import {Input} from "@/components/ui/input";
import {Label} from "@/components/ui/label";
import {Button} from "@/components/ui/button";
export default function Login() {
const navigate = useNavigate();
const {login} = useAuth();
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
async function handleLogin() {
try {
setLoading(true);
setError("");
await login(username, password);
navigate("/dashboard");
} catch (err) {
setError("Invalid credentials");
}
setLoading(false);
}
return (
<div className="flex min-h-screen items-center justify-center bg-slate-100">
<Card className="w-[420px] shadow-xl border">
<CardHeader className="space-y-1 text-center">
<CardTitle className="text-2xl font-semibold">Admin Login</CardTitle>
<p className="text-sm text-muted-foreground">
Enter your credentials to access the dashboard
</p>
</CardHeader>
<CardContent className="space-y-5">
{error && (
<div className="p-3 text-sm text-red-600 bg-red-100 rounded-md">
{error}
</div>
)}
<div className="space-y-2">
<Label htmlFor="username">Username</Label>
<Input
id="username"
placeholder="Enter username"
value={username}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setUsername(e.target.value)
}
disabled={loading}
/>
</div>
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<Input
id="password"
type="password"
placeholder="Enter password"
value={password}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setPassword(e.target.value)
}
disabled={loading}
/>
</div>
<Button
onClick={handleLogin}
className="w-full"
disabled={loading || !username || !password}
>
{loading ? "Logging in..." : "Login"}
</Button>
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,17 @@
import axios from "axios";
const api = axios.create({
baseURL: "http://localhost:3000/api",
});
api.interceptors.request.use((config) => {
const token = localStorage.getItem("token");
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
export default api;

View File

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

View File

@@ -0,0 +1,28 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"types": ["vite/client"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

26
frontend/tsconfig.json Normal file
View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": false,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src"],
"references": [{"path": "./tsconfig.node.json"}]
}

View File

@@ -0,0 +1,9 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

13
frontend/vite.config.ts Normal file
View File

@@ -0,0 +1,13 @@
import {defineConfig} from "vite";
import react from "@vitejs/plugin-react";
import path from "path";
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
});