Compare commits

...

14 Commits

Author SHA1 Message Date
rishalkv 6117805467 fix:blog editor image upload 2026-05-13 17:16:25 +05:30
kailasdevdas b002c053ae Merge pull request 'fix: doctor toggle logic' (#27) from feat/appointment-date-filter into dev
Reviewed-on: #27
2026-05-13 09:12:35 +00:00
kailasdevdas e6044518d2 Merge pull request 'feat: update date format in mail' (#26) from feat/email-date-format into dev
Reviewed-on: #26
2026-05-13 09:03:56 +00:00
Kailasdevdas 6889137164 feat: add appointment date range filter 2026-05-13 14:20:51 +05:30
Kailasdevdas 988fbd28f1 fix: doctor toggle logic 2026-05-13 14:19:42 +05:30
Kailasdevdas fa2b02ad23 feat: update date format in mail 2026-05-13 11:57:33 +05:30
kailasdevdas 199797fdf4 Merge pull request 'feat:sorting according to the prio of dept' (#24) from feat/department-internal-sort into dev
Reviewed-on: #24
2026-05-11 11:37:53 +00:00
rishalkv 5efe049fbd feat:sorting according to the prio of dept 2026-05-11 16:27:20 +05:30
kailasdevdas a008f09923 Merge pull request 'feat: implement sorting and visibility changes' (#23) from feat/inactive-field into dev
Reviewed-on: #23
2026-05-11 05:35:39 +00:00
Kailasdevdas 903a541e35 chore: correct lablel 2026-05-11 10:57:52 +05:30
Kailasdevdas 4808e99ae5 feat: remove delete action and add status toggle to tables 2026-05-11 10:52:30 +05:30
Kailasdevdas 2c6da93dfb feat: add toast 2026-05-11 10:51:34 +05:30
Kailasdevdas 1717507555 feat: implement sorting and visibility changes 2026-05-11 00:04:22 +05:30
kailasdevdas 9aaf1879a8 Merge pull request 'fix: edit form fields and update form submission logic' (#21) from fix/news-media-UI into dev
Reviewed-on: #21
2026-05-05 06:44:06 +00:00
19 changed files with 805 additions and 312 deletions
@@ -0,0 +1,14 @@
-- AlterTable
ALTER TABLE "Career" ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN "sortOrder" INTEGER NOT NULL DEFAULT 0;
-- AlterTable
ALTER TABLE "Department" ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN "sortOrder" INTEGER NOT NULL DEFAULT 0;
-- AlterTable
ALTER TABLE "Doctor" ADD COLUMN "globalSortOrder" INTEGER NOT NULL DEFAULT 0,
ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true;
-- AlterTable
ALTER TABLE "DoctorDepartment" ADD COLUMN "sortOrder" INTEGER NOT NULL DEFAULT 0;
@@ -0,0 +1,11 @@
-- AlterTable
ALTER TABLE "Career" ALTER COLUMN "sortOrder" SET DEFAULT 1000;
-- AlterTable
ALTER TABLE "Department" ALTER COLUMN "sortOrder" SET DEFAULT 1000;
-- AlterTable
ALTER TABLE "Doctor" ALTER COLUMN "globalSortOrder" SET DEFAULT 1000;
-- AlterTable
ALTER TABLE "DoctorDepartment" ALTER COLUMN "sortOrder" SET DEFAULT 1000;
+8 -2
View File
@@ -25,6 +25,8 @@ model Doctor {
designation String? designation String?
workingStatus String? workingStatus String?
qualification String? qualification String?
isActive Boolean @default(true)
globalSortOrder Int @default(1000)
departments DoctorDepartment[] departments DoctorDepartment[]
appointments Appointment[] appointments Appointment[]
@@ -46,6 +48,9 @@ model Department {
facilities String? facilities String?
services String? services String?
isActive Boolean @default(true)
sortOrder Int @default(1000)
doctors DoctorDepartment[] doctors DoctorDepartment[]
appointments Appointment[] appointments Appointment[]
@@ -61,7 +66,7 @@ model DoctorDepartment {
doctor Doctor @relation(fields: [doctorId], references: [id]) doctor Doctor @relation(fields: [doctorId], references: [id])
department Department @relation(fields: [departmentId], references: [id]) department Department @relation(fields: [departmentId], references: [id])
sortOrder Int @default(1000)
timing DoctorTiming? timing DoctorTiming?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
@@ -111,7 +116,8 @@ model Career {
email String? email String?
number String? number String?
status String @default("new") status String @default("new")
isActive Boolean @default(true)
sortOrder Int @default(1000)
candidates Candidate[] candidates Candidate[]
createdAt DateTime @default(now()) createdAt DateTime @default(now())
@@ -84,7 +84,11 @@ export const createAppointment = async (req, res) => {
<tr> <tr>
<td style="padding: 8px 0;"><b>Date:</b></td> <td style="padding: 8px 0;"><b>Date:</b></td>
<td style="padding: 8px 0;"> <td style="padding: 8px 0;">
${new Date(date).toLocaleDateString()} ${new Date(date).toLocaleDateString("en-GB", {
day: "2-digit",
month: "long",
year: "numeric",
})}
</td> </td>
</tr> </tr>
</table> </table>
@@ -143,18 +147,51 @@ export const getAppointments = async (req, res) => {
const page = parseInt(req.query.page) || 1; const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10; const limit = parseInt(req.query.limit) || 10;
const skip = (page - 1) * limit; const skip = (page - 1) * limit;
const { date, search } = req.query;
const { date, startDate, endDate, search } = req.query;
const where = {}; const where = {};
if (date) { const hasSingleDate = date && date.trim() !== "";
const hasRange =
(startDate && startDate.trim() !== "") ||
(endDate && endDate.trim() !== "");
if (hasSingleDate) {
const start = new Date(date); const start = new Date(date);
start.setHours(0, 0, 0, 0);
const end = new Date(date); const end = new Date(date);
end.setDate(end.getDate() + 1); end.setHours(23, 59, 59, 999);
where.date = { gte: start, lt: end };
where.date = {
gte: start,
lte: end,
};
} }
if (search) { if (!hasSingleDate && hasRange) {
const dateFilter = {};
if (startDate && startDate.trim() !== "") {
const start = new Date(startDate);
start.setHours(0, 0, 0, 0);
dateFilter.gte = start;
}
if (endDate && endDate.trim() !== "") {
const end = new Date(endDate);
end.setHours(23, 59, 59, 999);
dateFilter.lte = end;
}
where.date = dateFilter;
}
if (search && search.trim() !== "") {
where.OR = [ where.OR = [
{ name: { contains: search, mode: "insensitive" } }, { name: { contains: search, mode: "insensitive" } },
{ mobileNumber: { contains: search } }, { mobileNumber: { contains: search } },
@@ -165,24 +202,39 @@ export const getAppointments = async (req, res) => {
const [appointments, total] = await Promise.all([ const [appointments, total] = await Promise.all([
prisma.appointment.findMany({ prisma.appointment.findMany({
where, where,
include: { doctor: true, department: true }, include: {
orderBy: { createdAt: "desc" }, doctor: true,
department: true,
},
orderBy: {
createdAt: "desc",
},
skip, skip,
take: limit, take: limit,
}), }),
prisma.appointment.count({ where }),
prisma.appointment.count({
where,
}),
]); ]);
res.status(200).json({ res.status(200).json({
success: true, success: true,
data: appointments, data: appointments,
pagination: { total, page, limit, totalPages: Math.ceil(total / limit) }, pagination: {
total,
page,
limit,
totalPages: Math.ceil(total / limit),
},
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
res
.status(500) res.status(500).json({
.json({ success: false, message: "Failed to fetch appointments" }); success: false,
message: "Failed to fetch appointments",
});
} }
}; };
+16 -2
View File
@@ -4,8 +4,11 @@ import prisma from "../prisma/client.js";
export const getAllCareers = async (req, res) => { export const getAllCareers = async (req, res) => {
try { try {
const { admin } = req.query;
const careers = await prisma.career.findMany({ const careers = await prisma.career.findMany({
orderBy: {createdAt: "desc"}, where: admin === "true" ? {} : { isActive: true },
orderBy: [{ sortOrder: "asc" }, { createdAt: "desc" }],
}); });
const response = careers.map((c) => ({ const response = careers.map((c) => ({
@@ -17,6 +20,8 @@ export const getAllCareers = async (req, res) => {
email: c.email, email: c.email,
number: c.number, number: c.number,
status: c.status, status: c.status,
isActive: c.isActive,
sortOrder: c.sortOrder,
})); }));
return res.status(200).json({ return res.status(200).json({
@@ -44,6 +49,8 @@ export const createCareer = async (req, res) => {
email, email,
number, number,
status, status,
isActive,
sortOrder,
} = req.body; } = req.body;
if (!post || !designation) { if (!post || !designation) {
@@ -62,6 +69,8 @@ export const createCareer = async (req, res) => {
email, email,
number, number,
status, status,
isActive: isActive !== undefined ? isActive : true,
sortOrder: sortOrder !== undefined ? Number(sortOrder) : 0,
}, },
}); });
@@ -84,10 +93,15 @@ export const createCareer = async (req, res) => {
export const updateCareer = async (req, res) => { export const updateCareer = async (req, res) => {
try { try {
const { id } = req.params; const { id } = req.params;
const updateData = { ...req.body };
if (updateData.sortOrder !== undefined) {
updateData.sortOrder = Number(updateData.sortOrder);
}
const career = await prisma.career.update({ const career = await prisma.career.update({
where: { id: Number(id) }, where: { id: Number(id) },
data: req.body, data: updateData,
}); });
return res.status(200).json({ return res.status(200).json({
@@ -2,8 +2,11 @@ import prisma from "../prisma/client.js";
export const getAllDepartments = async (req, res) => { export const getAllDepartments = async (req, res) => {
try { try {
const {admin} = req.query;
const departments = await prisma.department.findMany({ const departments = await prisma.department.findMany({
orderBy: {name: "asc"}, where: admin === "true" ? {} : {isActive: true},
orderBy: [{sortOrder: "asc"}, {name: "asc"}],
}); });
const response = departments.map((dep) => ({ const response = departments.map((dep) => ({
@@ -15,6 +18,8 @@ export const getAllDepartments = async (req, res) => {
para3: dep.para3 ?? "", para3: dep.para3 ?? "",
facilities: dep.facilities ?? "", facilities: dep.facilities ?? "",
services: dep.services ?? "", services: dep.services ?? "",
isActive: dep.isActive,
sortOrder: dep.sortOrder,
})); }));
return res.status(200).json({ return res.status(200).json({
@@ -44,13 +49,14 @@ export const getDepartmentByName = async (req, res) => {
const department = await prisma.department.findFirst({ const department = await prisma.department.findFirst({
where: { where: {
name: name, name: name,
isActive: true,
}, },
}); });
if (!department) { if (!department) {
return res.status(404).json({ return res.status(404).json({
success: false, success: false,
message: "Department not found", message: "Department not found or inactive",
}); });
} }
@@ -63,6 +69,8 @@ export const getDepartmentByName = async (req, res) => {
para3: department.para3 ?? "", para3: department.para3 ?? "",
facilities: department.facilities ?? "", facilities: department.facilities ?? "",
services: department.services ?? "", services: department.services ?? "",
isActive: department.isActive,
sortOrder: department.sortOrder,
}; };
return res.status(200).json({ return res.status(200).json({
@@ -89,6 +97,8 @@ export async function createDepartment(req, res) {
para3, para3,
facilities, facilities,
services, services,
isActive,
sortOrder,
} = req.body; } = req.body;
if (!departmentId || !name) { if (!departmentId || !name) {
@@ -107,6 +117,8 @@ export async function createDepartment(req, res) {
para3, para3,
facilities, facilities,
services, services,
isActive: isActive !== undefined ? isActive : true,
sortOrder: sortOrder !== undefined ? Number(sortOrder) : 0,
}, },
}); });
@@ -118,7 +130,7 @@ export async function createDepartment(req, res) {
if (error.code === "P2002") { if (error.code === "P2002") {
return res.status(409).json({error: "Department already exists"}); return res.status(409).json({error: "Department already exists"});
} }
console.error(error);
res.status(500).json({error: "Failed to create department"}); res.status(500).json({error: "Failed to create department"});
} }
} }
@@ -126,20 +138,15 @@ export async function createDepartment(req, res) {
export const updateDepartment = async (req, res) => { export const updateDepartment = async (req, res) => {
try { try {
const {departmentId} = req.params; const {departmentId} = req.params;
const updateData = {...req.body};
const {name, image, para1, para2, para3, facilities, services} = req.body; if (updateData.sortOrder !== undefined) {
updateData.sortOrder = Number(updateData.sortOrder);
}
const department = await prisma.department.update({ const department = await prisma.department.update({
where: {departmentId}, where: {departmentId},
data: { data: updateData,
name,
image,
para1,
para2,
para3,
facilities,
services,
},
}); });
return res.status(200).json({ return res.status(200).json({
+49 -23
View File
@@ -4,7 +4,10 @@ import prisma from "../prisma/client.js";
export const getAllDoctors = async (req, res) => { export const getAllDoctors = async (req, res) => {
try { try {
const {admin} = req.query;
const doctors = await prisma.doctor.findMany({ const doctors = await prisma.doctor.findMany({
where: admin === "true" ? {} : {isActive: true},
include: { include: {
departments: { departments: {
include: { include: {
@@ -13,7 +16,7 @@ export const getAllDoctors = async (req, res) => {
}, },
}, },
}, },
orderBy: {name: "asc"}, orderBy: [{globalSortOrder: "asc"}, {name: "asc"}],
}); });
const formatted = doctors.map((doc, index) => ({ const formatted = doctors.map((doc, index) => ({
@@ -24,10 +27,10 @@ export const getAllDoctors = async (req, res) => {
designation: doc.designation, designation: doc.designation,
workingStatus: doc.workingStatus, workingStatus: doc.workingStatus,
qualification: doc.qualification, qualification: doc.qualification,
isActive: doc.isActive,
globalSortOrder: doc.globalSortOrder,
departments: doc.departments.map((d) => { departments: doc.departments.map((d) => {
const t = d.timing || {}; const t = d.timing || {};
const timingArray = [ const timingArray = [
t.monday && `Monday ${t.monday}`, t.monday && `Monday ${t.monday}`,
t.tuesday && `Tuesday ${t.tuesday}`, t.tuesday && `Tuesday ${t.tuesday}`,
@@ -43,6 +46,7 @@ export const getAllDoctors = async (req, res) => {
departmentId: d.department.departmentId, departmentId: d.department.departmentId,
departmentName: d.department.name, departmentName: d.department.name,
timing: timingArray.join(" & "), timing: timingArray.join(" & "),
deptSortOrder: d.sortOrder,
}; };
}), }),
})); }));
@@ -135,17 +139,23 @@ export const getDoctorsByDepartmentId = async (req, res) => {
}); });
} }
const doctors = await prisma.doctorDepartment.findMany({ const doctorsInDept = await prisma.doctorDepartment.findMany({
where: {departmentId: department.id}, where: {
departmentId: department.id,
doctor: {isActive: true},
},
include: { include: {
doctor: true, doctor: true,
}, },
orderBy: {sortOrder: "asc"},
}); });
const result = doctors.map((d) => ({ const result = doctorsInDept.map((d) => ({
GG_ID: d.doctor.doctorId, GG_ID: d.doctor.doctorId,
Name: d.doctor.name, Name: d.doctor.name,
image: d.doctor.image ?? "", image: d.doctor.image ?? "",
designation: d.doctor.designation,
hierarchyOrder: d.sortOrder,
})); }));
res.status(200).json({ res.status(200).json({
@@ -171,6 +181,8 @@ export const createDoctor = async (req, res) => {
designation, designation,
workingStatus, workingStatus,
qualification, qualification,
isActive,
globalSortOrder,
departments, departments,
} = req.body; } = req.body;
@@ -182,6 +194,9 @@ export const createDoctor = async (req, res) => {
designation, designation,
workingStatus, workingStatus,
qualification, qualification,
isActive: isActive !== undefined ? isActive : true,
globalSortOrder:
globalSortOrder !== undefined ? Number(globalSortOrder) : 0,
}, },
}); });
@@ -196,6 +211,7 @@ export const createDoctor = async (req, res) => {
data: { data: {
doctorId: doctor.id, doctorId: doctor.id,
departmentId: department.id, departmentId: department.id,
sortOrder: dep.sortOrder !== undefined ? Number(dep.sortOrder) : 0,
}, },
}); });
@@ -232,24 +248,36 @@ export const updateDoctor = async (req, res) => {
image, image,
workingStatus, workingStatus,
qualification, qualification,
isActive,
globalSortOrder,
departments, departments,
} = req.body; } = req.body;
const doctor = await prisma.doctor.findUnique({ const doctor = await prisma.doctor.findUnique({where: {doctorId}});
where: {doctorId}, if (!doctor)
});
if (!doctor) {
return res return res
.status(404) .status(404)
.json({success: false, message: "Doctor not found"}); .json({success: false, message: "Doctor not found"});
}
await prisma.doctor.update({ await prisma.doctor.update({
where: {id: doctor.id}, where: {id: doctor.id},
data: {name, designation, image, workingStatus, qualification}, data: {
name,
designation,
image,
workingStatus,
qualification,
isActive,
globalSortOrder:
globalSortOrder !== undefined ? Number(globalSortOrder) : undefined,
},
}); });
const hasTimingData = departments?.some(
(dep) => dep.timing && Object.keys(dep.timing).length > 0,
);
if (departments && Array.isArray(departments) && hasTimingData) {
const oldRelations = await prisma.doctorDepartment.findMany({ const oldRelations = await prisma.doctorDepartment.findMany({
where: {doctorId: doctor.id}, where: {doctorId: doctor.id},
}); });
@@ -265,31 +293,28 @@ export const updateDoctor = async (req, res) => {
}); });
for (const dep of departments) { for (const dep of departments) {
const department = await prisma.department.findUnique({ const targetDept = await prisma.department.findUnique({
where: {departmentId: dep.departmentId}, where: {departmentId: dep.departmentId},
}); });
if (!targetDept) continue;
if (!department) continue; const newDD = await prisma.doctorDepartment.create({
const doctorDepartment = await prisma.doctorDepartment.create({
data: { data: {
doctorId: doctor.id, doctorId: doctor.id,
departmentId: department.id, departmentId: targetDept.id,
sortOrder: dep.sortOrder !== undefined ? Number(dep.sortOrder) : 0,
}, },
}); });
if (dep.timing) { if (dep.timing) {
const {id, doctorDepartmentId, createdAt, updatedAt, ...cleanTiming} = const {id, doctorDepartmentId, createdAt, updatedAt, ...cleanTiming} =
dep.timing; dep.timing;
await prisma.doctorTiming.create({ await prisma.doctorTiming.create({
data: { data: {doctorDepartmentId: newDD.id, ...cleanTiming},
doctorDepartmentId: doctorDepartment.id,
...cleanTiming,
},
}); });
} }
} }
}
res res
.status(200) .status(200)
@@ -421,6 +446,7 @@ export const getDoctorTimingById = async (req, res) => {
departments: doctor.departments.map((d) => ({ departments: doctor.departments.map((d) => ({
departmentId: d.department.departmentId, departmentId: d.department.departmentId,
departmentName: d.department.name, departmentName: d.department.name,
deptSortOrder: d.sortOrder,
timing: d.timing || {}, timing: d.timing || {},
})), })),
}; };
+27 -1
View File
@@ -28,6 +28,7 @@
"radix-ui": "^1.4.3", "radix-ui": "^1.4.3",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-hot-toast": "^2.6.0",
"react-router-dom": "^7.13.1", "react-router-dom": "^7.13.1",
"shadcn": "^4.0.5", "shadcn": "^4.0.5",
"tailwind-merge": "^3.5.0", "tailwind-merge": "^3.5.0",
@@ -5187,7 +5188,6 @@
"version": "3.2.3", "version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/data-uri-to-buffer": { "node_modules/data-uri-to-buffer": {
@@ -6352,6 +6352,15 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/goober": {
"version": "2.1.18",
"resolved": "https://registry.npmjs.org/goober/-/goober-2.1.18.tgz",
"integrity": "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==",
"license": "MIT",
"peerDependencies": {
"csstype": "^3.0.10"
}
},
"node_modules/gopd": { "node_modules/gopd": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -8136,6 +8145,23 @@
"react": "^19.2.4" "react": "^19.2.4"
} }
}, },
"node_modules/react-hot-toast": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz",
"integrity": "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==",
"license": "MIT",
"dependencies": {
"csstype": "^3.1.3",
"goober": "^2.1.16"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": ">=16",
"react-dom": ">=16"
}
},
"node_modules/react-refresh": { "node_modules/react-refresh": {
"version": "0.18.0", "version": "0.18.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
+1
View File
@@ -30,6 +30,7 @@
"radix-ui": "^1.4.3", "radix-ui": "^1.4.3",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-hot-toast": "^2.6.0",
"react-router-dom": "^7.13.1", "react-router-dom": "^7.13.1",
"shadcn": "^4.0.5", "shadcn": "^4.0.5",
"tailwind-merge": "^3.5.0", "tailwind-merge": "^3.5.0",
+3
View File
@@ -1,4 +1,5 @@
import {BrowserRouter, Routes, Route, Navigate} from "react-router-dom"; import {BrowserRouter, Routes, Route, Navigate} from "react-router-dom";
import {Toaster} from "react-hot-toast";
import Login from "@/pages/Login"; import Login from "@/pages/Login";
@@ -26,6 +27,8 @@ import ImportData from "./pages/ImportData";
export default function App() { export default function App() {
return ( return (
<BrowserRouter> <BrowserRouter>
<Toaster position="top-right" />
<AuthProvider> <AuthProvider>
<Routes> <Routes>
<Route element={<PublicRoute />}> <Route element={<PublicRoute />}>
+4
View File
@@ -4,12 +4,16 @@ export const getAppointmentsApi = async (
page = 1, page = 1,
limit = 10, limit = 10,
date = "", date = "",
startDate = "",
endDate = "",
search = "", search = "",
) => { ) => {
const params = new URLSearchParams({ const params = new URLSearchParams({
page: String(page), page: String(page),
limit: String(limit), limit: String(limit),
...(date && { date }), ...(date && { date }),
...(startDate && { startDate }),
...(endDate && { endDate }),
...(search && { search }), ...(search && { search }),
}); });
const res = await apiClient.get(`/appointments/getall?${params}`); const res = await apiClient.get(`/appointments/getall?${params}`);
+39 -1
View File
@@ -1,11 +1,49 @@
import apiClient from "@/api/client"; import apiClient from "@/api/client";
import toast from "react-hot-toast";
export const getCareersApi = async () => { export const getCareersApi = async () => {
const res = await apiClient.get("/careers/getAll"); const res = await apiClient.get("/careers/getAll?admin=true");
return res.data; return res.data;
}; };
export const createCareerApi = async (data: any) => {
try {
const res = await apiClient.post("/careers", data);
toast.success("Career created successfully");
return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || "Failed to create career");
throw error;
}
};
export const updateCareerApi = async (id: number, data: any) => {
try {
const res = await apiClient.patch(`/careers/${id}`, data);
toast.success("Career updated successfully");
return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || "Failed to update career");
throw error;
}
};
export const deleteCareerApi = async (id: number) => { export const deleteCareerApi = async (id: number) => {
try {
const res = await apiClient.delete(`/careers/${id}`); const res = await apiClient.delete(`/careers/${id}`);
toast.success("Career deleted successfully");
return res.data; return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || "Failed to delete career");
throw error;
}
}; };
+37 -1
View File
@@ -1,4 +1,5 @@
import apiClient from "@/api/client"; import apiClient from "@/api/client";
import toast from "react-hot-toast";
export interface Department { export interface Department {
departmentId: string; departmentId: string;
@@ -9,10 +10,12 @@ export interface Department {
para3: string; para3: string;
facilities: string; facilities: string;
services: string; services: string;
isActive?: boolean;
sortOrder?: number;
} }
export const getDepartmentsApi = async () => { export const getDepartmentsApi = async () => {
const res = await apiClient.get("/departments/getAll"); const res = await apiClient.get("/departments/getAll?admin=true");
return res.data; return res.data;
}; };
@@ -25,8 +28,19 @@ export const createDepartmentApi = async (data: {
facilities?: string; facilities?: string;
services?: string; services?: string;
}) => { }) => {
try {
const res = await apiClient.post("/departments", data); const res = await apiClient.post("/departments", data);
toast.success("Department created successfully");
return res.data; return res.data;
} catch (error: any) {
toast.error(
error?.response?.data?.message || "Failed to create department",
);
throw error;
}
}; };
export const updateDepartmentApi = async ( export const updateDepartmentApi = async (
@@ -40,11 +54,33 @@ export const updateDepartmentApi = async (
services?: string; services?: string;
}, },
) => { ) => {
try {
const res = await apiClient.put(`/departments/${departmentId}`, data); const res = await apiClient.put(`/departments/${departmentId}`, data);
toast.success("Department updated successfully");
return res.data; return res.data;
} catch (error: any) {
toast.error(
error?.response?.data?.message || "Failed to update department",
);
throw error;
}
}; };
export const deleteDepartmentApi = async (departmentId: string) => { export const deleteDepartmentApi = async (departmentId: string) => {
try {
const res = await apiClient.delete(`/departments/${departmentId}`); const res = await apiClient.delete(`/departments/${departmentId}`);
toast.success("Department deleted successfully");
return res.data; return res.data;
} catch (error: any) {
toast.error(
error?.response?.data?.message || "Failed to delete department",
);
throw error;
}
}; };
+32 -2
View File
@@ -1,4 +1,5 @@
import apiClient from "@/api/client"; import apiClient from "@/api/client";
import toast from "react-hot-toast";
export interface Doctor { export interface Doctor {
doctorId: string; doctorId: string;
@@ -7,8 +8,10 @@ export interface Doctor {
designation?: string; designation?: string;
workingStatus?: string; workingStatus?: string;
qualification?: string; qualification?: string;
isActive: boolean;
globalSortOrder: number;
departments: { departments?: {
departmentId: string; departmentId: string;
timing?: { timing?: {
monday?: string; monday?: string;
@@ -24,7 +27,7 @@ export interface Doctor {
} }
export const getDoctorsApi = async () => { export const getDoctorsApi = async () => {
const res = await apiClient.get("/doctors/getAll"); const res = await apiClient.get("/doctors/getAll?admin=true");
return res.data; return res.data;
}; };
@@ -34,21 +37,48 @@ export const getDoctorByIdApi = async (doctorId: string) => {
}; };
export const createDoctorApi = async (data: Doctor) => { export const createDoctorApi = async (data: Doctor) => {
try {
const res = await apiClient.post("/doctors", data); const res = await apiClient.post("/doctors", data);
toast.success("Doctor created successfully");
return res.data; return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || "Failed to create doctor");
throw error;
}
}; };
export const updateDoctorApi = async ( export const updateDoctorApi = async (
doctorId: string, doctorId: string,
data: Partial<Doctor>, data: Partial<Doctor>,
) => { ) => {
try {
const res = await apiClient.patch(`/doctors/${doctorId}`, data); const res = await apiClient.patch(`/doctors/${doctorId}`, data);
toast.success("Doctor updated successfully");
return res.data; return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || "Failed to update doctor");
throw error;
}
}; };
export const deleteDoctorApi = async (doctorId: string) => { export const deleteDoctorApi = async (doctorId: string) => {
try {
const res = await apiClient.delete(`/doctors/${doctorId}`); const res = await apiClient.delete(`/doctors/${doctorId}`);
toast.success("Doctor deleted successfully");
return res.data; return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || "Failed to delete doctor");
throw error;
}
}; };
export const getDoctorTimingApi = async (doctorId: string) => { export const getDoctorTimingApi = async (doctorId: string) => {
+54 -3
View File
@@ -40,7 +40,8 @@ export default function AppointmentPage() {
const [searchText, setSearchText] = useState(""); const [searchText, setSearchText] = useState("");
const [filterDoctor, setFilterDoctor] = useState(""); const [filterDoctor, setFilterDoctor] = useState("");
const [filterDate, setFilterDate] = useState(""); const [filterDate, setFilterDate] = useState("");
const [startDate, setStartDate] = useState("");
const [endDate, setEndDate] = useState("");
const [viewOpen, setViewOpen] = useState(false); const [viewOpen, setViewOpen] = useState(false);
const [viewData, setViewData] = useState<any>(null); const [viewData, setViewData] = useState<any>(null);
@@ -56,6 +57,8 @@ export default function AppointmentPage() {
currentPage, currentPage,
itemsPerPage, itemsPerPage,
filterDate, filterDate,
startDate,
endDate,
searchText, searchText,
); );
setAppointments(res?.data || []); setAppointments(res?.data || []);
@@ -66,7 +69,7 @@ export default function AppointmentPage() {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [currentPage, itemsPerPage, filterDate, searchText]); }, [currentPage, itemsPerPage, filterDate, startDate, endDate, searchText]);
useEffect(() => { useEffect(() => {
fetchAll(); fetchAll();
@@ -116,7 +119,11 @@ export default function AppointmentPage() {
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-4"> <div className="flex flex-col md:flex-row md:justify-between md:items-center gap-4">
<h1 className="text-3xl font-bold">Appointments</h1> <h1 className="text-3xl font-bold">Appointments</h1>
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-4 items-end">
<div className="flex flex-col gap-1">
<label className="text-xs font-medium text-muted-foreground">
Search
</label>
<Input <Input
placeholder="Search name / phone..." placeholder="Search name / phone..."
value={searchText} value={searchText}
@@ -126,7 +133,12 @@ export default function AppointmentPage() {
}} }}
className="w-[220px] text-base" className="w-[220px] text-base"
/> />
</div>
<div className="flex flex-col gap-1">
<label className="text-xs font-medium text-muted-foreground">
Date
</label>
<Input <Input
type="date" type="date"
value={filterDate} value={filterDate}
@@ -135,8 +147,46 @@ export default function AppointmentPage() {
setCurrentPage(1); setCurrentPage(1);
}} }}
className="w-[160px] text-base" className="w-[160px] text-base"
disabled={!!startDate || !!endDate}
/> />
</div>
<div className="flex flex-col gap-1">
<label className="text-xs font-medium text-muted-foreground">
From
</label>
<Input
type="date"
value={startDate}
onChange={(e) => {
setStartDate(e.target.value);
setCurrentPage(1);
}}
className="w-[160px] text-base"
disabled={!!filterDate}
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-xs font-medium text-muted-foreground">
To
</label>
<Input
type="date"
value={endDate}
onChange={(e) => {
setEndDate(e.target.value);
setCurrentPage(1);
}}
className="w-[160px] text-base"
disabled={!!filterDate}
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-xs font-medium text-muted-foreground">
Rows
</label>
<select <select
value={itemsPerPage} value={itemsPerPage}
onChange={(e) => { onChange={(e) => {
@@ -149,6 +199,7 @@ export default function AppointmentPage() {
<option value={10}>10 / page</option> <option value={10}>10 / page</option>
<option value={20}>20 / page</option> <option value={20}>20 / page</option>
</select> </select>
</div>
<Button <Button
variant="outline" variant="outline"
+33 -2
View File
@@ -10,6 +10,7 @@ import Table from "@editorjs/table";
import CodeTool from "@editorjs/code"; import CodeTool from "@editorjs/code";
import Embed from "@editorjs/embed"; import Embed from "@editorjs/embed";
import Delimiter from "@editorjs/delimiter"; import Delimiter from "@editorjs/delimiter";
import axios from "axios";
import { import {
createBlogApi, createBlogApi,
@@ -23,6 +24,7 @@ import {Input} from "@/components/ui/input";
import {Button} from "@/components/ui/button"; import {Button} from "@/components/ui/button";
export default function BlogEditorPage() { export default function BlogEditorPage() {
const baseURL = import.meta.env.VITE_API_URL;
const {id} = useParams(); const {id} = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
@@ -79,12 +81,41 @@ export default function BlogEditorPage() {
config: { config: {
uploader: { uploader: {
uploadByFile: async (file: File) => { uploadByFile: async (file: File) => {
const res = await uploadImageApi(file); if (file.size > 5 * 1024 * 1024) {
alert("File is too large (Max 5MB)");
return {success: 0, file: {url: ""}};
}
const formData = new FormData();
formData.append("file", file);
formData.append("folderPath", "/blog");
try {
const response = await axios.post(
`${baseURL}/upload`,
formData,
{
headers: {
"Content-Type": "multipart/form-data",
},
},
);
return { return {
success: 1, success: 1,
file: {url: res.file.url}, file: {url: response.data.fileUrl},
}; };
} catch (e: any) {
console.error("EditorJS Image Upload Error:", e);
const errorMessage =
e.response?.data?.error || e.message || "Upload failed";
alert(`Upload Error: ${errorMessage}`);
return {
success: 0,
file: {url: ""},
};
}
}, },
}, },
}, },
+66 -43
View File
@@ -1,7 +1,6 @@
import { useState, useEffect, useCallback } from "react"; import { useState, useEffect, useCallback } from "react";
import { getCareersApi, deleteCareerApi } from "@/api/career"; import { getCareersApi, updateCareerApi, createCareerApi } from "@/api/career";
import apiClient from "@/api/client";
import { import {
Table, Table,
@@ -24,12 +23,13 @@ import {
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import { import {
Loader2, Loader2,
Plus, Plus,
Pencil, Pencil,
Trash,
RefreshCw, RefreshCw,
ChevronLeft, ChevronLeft,
ChevronRight, ChevronRight,
@@ -55,6 +55,8 @@ export default function CareerPage() {
email: "", email: "",
number: "", number: "",
status: "new", status: "new",
isActive: true,
sortOrder: 0,
}); });
const fetchAll = useCallback(async () => { const fetchAll = useCallback(async () => {
@@ -92,6 +94,18 @@ export default function CareerPage() {
setForm({ ...form, [e.target.name]: e.target.value }); setForm({ ...form, [e.target.name]: e.target.value });
} }
const handleToggleStatus = async (item: any) => {
try {
await updateCareerApi(item.id, {
...item,
isActive: !item.isActive,
} as any);
fetchAll();
} catch (error) {
console.error("Failed to toggle status", error);
}
};
function openAdd() { function openAdd() {
setEditing(null); setEditing(null);
setForm({ setForm({
@@ -102,6 +116,8 @@ export default function CareerPage() {
email: "", email: "",
number: "", number: "",
status: "new", status: "new",
isActive: true,
sortOrder: 0,
}); });
setOpenModal(true); setOpenModal(true);
} }
@@ -116,6 +132,8 @@ export default function CareerPage() {
email: item.email || "", email: item.email || "",
number: item.number || "", number: item.number || "",
status: item.status || "new", status: item.status || "new",
isActive: item.isActive ?? true,
sortOrder: item.sortOrder ?? 0,
}); });
setOpenModal(true); setOpenModal(true);
} }
@@ -123,10 +141,11 @@ export default function CareerPage() {
async function handleSubmit() { async function handleSubmit() {
try { try {
if (editing) { if (editing) {
await apiClient.patch(`/careers/${editing.id}`, form); await updateCareerApi(editing.id, form);
} else { } else {
await apiClient.post("/careers", form); await createCareerApi(form);
} }
setOpenModal(false); setOpenModal(false);
fetchAll(); fetchAll();
} catch (err) { } catch (err) {
@@ -134,12 +153,6 @@ export default function CareerPage() {
} }
} }
async function handleDelete(id: number) {
if (!confirm("Delete career?")) return;
await deleteCareerApi(id);
fetchAll();
}
return ( return (
<div className="p-6 space-y-6"> <div className="p-6 space-y-6">
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-4"> <div className="flex flex-col md:flex-row md:justify-between md:items-center gap-4">
@@ -177,13 +190,13 @@ export default function CareerPage() {
<CardContent className="p-0 sm:p-6 space-y-4"> <CardContent className="p-0 sm:p-6 space-y-4">
<div className="rounded-md border overflow-x-auto overflow-y-auto max-h-[650px] relative"> <div className="rounded-md border overflow-x-auto overflow-y-auto max-h-[650px] relative">
<Table className="w-full min-w-[1000px] table-fixed border-separate border-spacing-0"> <Table className="w-full min-w-[800px] table-fixed border-separate border-spacing-0">
<TableHeader className="sticky top-0 z-20 bg-background shadow-sm"> <TableHeader className="sticky top-0 z-20 bg-background shadow-sm">
<TableRow> <TableRow>
<TableHead className="w-[60px] bg-background font-bold text-sm"> <TableHead className="w-[80px] bg-background font-bold text-sm">
ID Priority
</TableHead> </TableHead>
<TableHead className="w-[200px] bg-background font-bold text-sm"> <TableHead className="w-[250px] bg-background font-bold text-sm">
Post & Designation Post & Designation
</TableHead> </TableHead>
<TableHead className="w-[200px] bg-background font-bold text-sm"> <TableHead className="w-[200px] bg-background font-bold text-sm">
@@ -192,13 +205,10 @@ export default function CareerPage() {
<TableHead className="w-[120px] bg-background font-bold text-sm"> <TableHead className="w-[120px] bg-background font-bold text-sm">
Experience Experience
</TableHead> </TableHead>
<TableHead className="w-[200px] bg-background font-bold text-sm"> <TableHead className="w-[80px] bg-background font-bold text-sm">
Contact Info Status (Active)
</TableHead> </TableHead>
<TableHead className="w-[100px] bg-background font-bold text-sm"> <TableHead className="w-[80px] bg-background font-bold text-right text-sm">
Status
</TableHead>
<TableHead className="w-[120px] bg-background font-bold text-right text-sm">
Actions Actions
</TableHead> </TableHead>
</TableRow> </TableRow>
@@ -207,14 +217,14 @@ export default function CareerPage() {
<TableBody> <TableBody>
{loading ? ( {loading ? (
<TableRow> <TableRow>
<TableCell colSpan={7} className="text-center py-10"> <TableCell colSpan={6} className="text-center py-10">
<Loader2 className="h-8 w-8 animate-spin mx-auto" /> <Loader2 className="h-8 w-8 animate-spin mx-auto" />
</TableCell> </TableCell>
</TableRow> </TableRow>
) : currentItems.length === 0 ? ( ) : currentItems.length === 0 ? (
<TableRow> <TableRow>
<TableCell <TableCell
colSpan={7} colSpan={6}
className="text-center text-muted-foreground py-10 text-base" className="text-center text-muted-foreground py-10 text-base"
> >
No careers found No careers found
@@ -224,7 +234,7 @@ export default function CareerPage() {
currentItems.map((item) => ( currentItems.map((item) => (
<TableRow key={item.id} className="hover:bg-muted/50"> <TableRow key={item.id} className="hover:bg-muted/50">
<TableCell className="font-mono text-xs"> <TableCell className="font-mono text-xs">
{item.id} {item.sortOrder}
</TableCell> </TableCell>
<TableCell> <TableCell>
<div className="font-semibold text-base truncate"> <div className="font-semibold text-base truncate">
@@ -243,20 +253,18 @@ export default function CareerPage() {
{item.experienceNeed} {item.experienceNeed}
</TableCell> </TableCell>
<TableCell> <TableCell>
<div className="text-sm font-medium">{item.email}</div> <div className="flex items-center gap-2">
<div className="text-xs text-muted-foreground"> <Switch
{item.number} checked={item.isActive}
</div> onCheckedChange={() => handleToggleStatus(item)}
</TableCell> />
<TableCell>
<Badge <Badge
variant={ variant={item.isActive ? "default" : "secondary"}
item.status === "active" ? "default" : "secondary"
}
className="capitalize" className="capitalize"
> >
{item.status} {item.isActive ? "Active" : "Hidden"}
</Badge> </Badge>
</div>
</TableCell> </TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
@@ -269,15 +277,6 @@ export default function CareerPage() {
> >
<Pencil className="h-4 w-4" /> <Pencil className="h-4 w-4" />
</Button> </Button>
<Button
size="icon"
variant="ghost"
className="h-9 w-9 text-destructive hover:text-destructive hover:bg-destructive/10"
onClick={() => handleDelete(item.id)}
>
<Trash className="h-4 w-4" />
</Button>
</div> </div>
</TableCell> </TableCell>
</TableRow> </TableRow>
@@ -393,6 +392,30 @@ export default function CareerPage() {
onChange={handleChange} onChange={handleChange}
className="text-base" className="text-base"
/> />
<div className="flex items-center justify-between p-2 border rounded-md">
<Label htmlFor="isActive" className="text-base">
Active
</Label>
<Switch
id="isActive"
checked={form.isActive}
onCheckedChange={(val) => setForm({ ...form, isActive: val })}
/>
</div>
<div className="space-y-1">
<Label htmlFor="sortOrder" className="text-sm">
Sort Priority (Lower numbers show first)
</Label>
<Input
id="sortOrder"
name="sortOrder"
type="number"
placeholder="Sort Order"
value={form.sortOrder}
onChange={handleChange}
className="text-base"
/>
</div>
</div> </div>
</div> </div>
+85 -58
View File
@@ -6,7 +6,6 @@ import {
getDepartmentsApi, getDepartmentsApi,
createDepartmentApi, createDepartmentApi,
updateDepartmentApi, updateDepartmentApi,
deleteDepartmentApi,
} from "@/api/department"; } from "@/api/department";
import { import {
@@ -31,13 +30,15 @@ import {
import {Input} from "@/components/ui/input"; import {Input} from "@/components/ui/input";
import {Textarea} from "@/components/ui/textarea"; import {Textarea} from "@/components/ui/textarea";
import {Switch} from "@/components/ui/switch";
import {Label} from "@/components/ui/label";
import {Badge} from "@/components/ui/badge";
import { import {
Loader2, Loader2,
RefreshCw, RefreshCw,
Plus, Plus,
Pencil, Pencil,
Trash,
Eye, Eye,
ChevronLeft, ChevronLeft,
ChevronRight, ChevronRight,
@@ -52,6 +53,8 @@ interface Department {
para3: string; para3: string;
facilities: string; facilities: string;
services: string; services: string;
isActive: boolean;
sortOrder: number;
} }
export default function DepartmentPage() { export default function DepartmentPage() {
@@ -79,6 +82,8 @@ export default function DepartmentPage() {
para3: "", para3: "",
facilities: "", facilities: "",
services: "", services: "",
isActive: true,
sortOrder: 0,
}); });
const fetchDepartments = useCallback(async () => { const fetchDepartments = useCallback(async () => {
@@ -122,13 +127,23 @@ export default function DepartmentPage() {
); );
function handleChange(e: any) { function handleChange(e: any) {
setForm({...form, [e.target.name]: e.target.value}); const value =
e.target.type === "number" ? Number(e.target.value) : e.target.value;
setForm({...form, [e.target.name]: value});
} }
function truncate(text: string, limit = 60) { const handleToggleStatus = async (dep: Department) => {
if (!text) return "-"; try {
return text.length > limit ? text.substring(0, limit) + "..." : text; const {departmentId, ...updateData} = dep;
await updateDepartmentApi(departmentId, {
...updateData,
isActive: !dep.isActive,
} as any);
fetchDepartments();
} catch (error) {
console.error("Failed to toggle status", error);
} }
};
function openAdd() { function openAdd() {
setEditing(null); setEditing(null);
@@ -141,13 +156,19 @@ export default function DepartmentPage() {
para3: "", para3: "",
facilities: "", facilities: "",
services: "", services: "",
isActive: true,
sortOrder: 0,
}); });
setOpenModal(true); setOpenModal(true);
} }
function openEdit(dep: Department) { function openEdit(dep: Department) {
setEditing(dep); setEditing(dep);
setForm(dep); setForm({
...dep,
isActive: dep.isActive ?? true,
sortOrder: dep.sortOrder ?? 0,
});
setOpenModal(true); setOpenModal(true);
} }
@@ -160,7 +181,7 @@ export default function DepartmentPage() {
try { try {
if (editing) { if (editing) {
const {departmentId, ...updateData} = form; const {departmentId, ...updateData} = form;
await updateDepartmentApi(editing.departmentId, updateData); await updateDepartmentApi(editing.departmentId, form as any);
} else { } else {
await createDepartmentApi(form); await createDepartmentApi(form);
} }
@@ -172,17 +193,6 @@ export default function DepartmentPage() {
} }
} }
async function handleDelete(id: string) {
if (!confirm("Delete this department?")) return;
try {
await deleteDepartmentApi(id);
fetchDepartments();
} catch (error) {
console.error(error);
}
}
return ( return (
<div className="p-6 space-y-6"> <div className="p-6 space-y-6">
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-4"> <div className="flex flex-col md:flex-row md:justify-between md:items-center gap-4">
@@ -226,25 +236,19 @@ export default function DepartmentPage() {
<CardContent className="p-0 sm:p-6 space-y-4"> <CardContent className="p-0 sm:p-6 space-y-4">
<div className="rounded-md border overflow-x-auto overflow-y-auto max-h-[650px] relative"> <div className="rounded-md border overflow-x-auto overflow-y-auto max-h-[650px] relative">
<Table className="w-full min-w-[900px] table-fixed border-separate border-spacing-0"> <Table className="w-full min-w-[700px] table-fixed border-separate border-spacing-0">
<TableHeader className="sticky top-0 z-20 bg-background shadow-sm"> <TableHeader className="sticky top-0 z-20 bg-background shadow-sm">
<TableRow> <TableRow>
<TableHead className="w-[100px] bg-background text-sm font-bold"> <TableHead className="w-[100px] bg-background text-sm font-bold">
ID Priority
</TableHead> </TableHead>
<TableHead className="w-[200px] bg-background text-sm font-bold"> <TableHead className="w-[300px] bg-background text-sm font-bold">
Name Name
</TableHead> </TableHead>
<TableHead className="w-[250px] bg-background text-sm font-bold"> <TableHead className="w-[80px] bg-background text-sm font-bold">
Para 1 Status (Active)
</TableHead> </TableHead>
<TableHead className="w-[220px] bg-background text-sm font-bold"> <TableHead className="w-[80px] bg-background text-right text-sm font-bold">
Facilities
</TableHead>
<TableHead className="w-[220px] bg-background text-sm font-bold">
Services
</TableHead>
<TableHead className="w-[140px] bg-background text-right text-sm font-bold">
Actions Actions
</TableHead> </TableHead>
</TableRow> </TableRow>
@@ -253,14 +257,14 @@ export default function DepartmentPage() {
<TableBody> <TableBody>
{loading ? ( {loading ? (
<TableRow> <TableRow>
<TableCell colSpan={6} className="text-center py-10"> <TableCell colSpan={4} className="text-center py-10">
<Loader2 className="h-8 w-8 animate-spin mx-auto" /> <Loader2 className="h-8 w-8 animate-spin mx-auto" />
</TableCell> </TableCell>
</TableRow> </TableRow>
) : currentItems.length === 0 ? ( ) : currentItems.length === 0 ? (
<TableRow> <TableRow>
<TableCell <TableCell
colSpan={6} colSpan={4}
className="text-center text-muted-foreground py-10 text-base" className="text-center text-muted-foreground py-10 text-base"
> >
No departments found No departments found
@@ -272,8 +276,8 @@ export default function DepartmentPage() {
key={dep.departmentId} key={dep.departmentId}
className="hover:bg-muted/50" className="hover:bg-muted/50"
> >
<TableCell className="font-mono text-xs"> <TableCell className="font-mono text-sm">
{dep.departmentId} {dep.sortOrder}
</TableCell> </TableCell>
<TableCell> <TableCell>
@@ -283,23 +287,21 @@ export default function DepartmentPage() {
> >
{dep.name} {dep.name}
</div> </div>
</TableCell> <div className="text-xs text-muted-foreground">
{dep.departmentId}
<TableCell>
<div className="text-sm break-words whitespace-normal">
{truncate(dep.para1)}
</div> </div>
</TableCell> </TableCell>
<TableCell> <TableCell>
<div className="text-sm break-words whitespace-normal"> <div className="flex items-center gap-2">
{truncate(dep.facilities)} <Switch
</div> checked={dep.isActive}
</TableCell> onCheckedChange={() => handleToggleStatus(dep)}
/>
<TableCell> <Badge
<div className="text-sm break-words whitespace-normal"> variant={dep.isActive ? "default" : "secondary"}
{truncate(dep.services)} >
{dep.isActive ? "Active" : "Hidden"}
</Badge>
</div> </div>
</TableCell> </TableCell>
@@ -322,15 +324,6 @@ export default function DepartmentPage() {
> >
<Pencil className="h-4 w-4" /> <Pencil className="h-4 w-4" />
</Button> </Button>
<Button
size="icon"
variant="ghost"
className="h-9 w-9 text-destructive hover:text-destructive hover:bg-destructive/10"
onClick={() => handleDelete(dep.departmentId)}
>
<Trash className="h-4 w-4" />
</Button>
</div> </div>
</TableCell> </TableCell>
</TableRow> </TableRow>
@@ -450,6 +443,32 @@ export default function DepartmentPage() {
onChange={handleChange} onChange={handleChange}
placeholder="Services" placeholder="Services"
/> />
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 border-t pt-4">
<div className="flex items-center justify-between p-3 border rounded-md">
<Label htmlFor="isActive" className="text-base cursor-pointer">
Active Visibility
</Label>
<Switch
id="isActive"
checked={form.isActive}
onCheckedChange={(val) => setForm({...form, isActive: val})}
/>
</div>
<div className="space-y-1">
<Label htmlFor="sortOrder">
Sort Priority (Lower numbers show first)
</Label>
<Input
id="sortOrder"
name="sortOrder"
type="number"
value={form.sortOrder}
onChange={handleChange}
placeholder="0"
/>
</div>
</div>
</div> </div>
<DialogFooter> <DialogFooter>
@@ -470,9 +489,17 @@ export default function DepartmentPage() {
</DialogHeader> </DialogHeader>
{viewData && ( {viewData && (
<div className="space-y-4 text-sm"> <div className="space-y-4 text-sm">
<div className="flex gap-4 items-center border-b pb-4">
<Badge variant={viewData.isActive ? "default" : "secondary"}>
{viewData.isActive ? "ACTIVE" : "HIDDEN"}
</Badge>
<p>
<b>Sort Order:</b> {viewData.sortOrder}
</p>
<p> <p>
<b>ID:</b> {viewData.departmentId} <b>ID:</b> {viewData.departmentId}
</p> </p>
</div>
<p> <p>
<b>Name:</b> {viewData.name} <b>Name:</b> {viewData.name}
</p> </p>
+154 -61
View File
@@ -7,7 +7,6 @@ import {
getDoctorsApi, getDoctorsApi,
createDoctorApi, createDoctorApi,
updateDoctorApi, updateDoctorApi,
deleteDoctorApi,
getDoctorTimingApi, getDoctorTimingApi,
} from "@/api/doctor"; } from "@/api/doctor";
import { getDepartmentsApi } from "@/api/department"; import { getDepartmentsApi } from "@/api/department";
@@ -31,15 +30,16 @@ import {
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import { import {
Loader2, Loader2,
RefreshCw, RefreshCw,
Plus, Plus,
Pencil, Pencil,
Trash,
ChevronLeft, ChevronLeft,
ChevronRight, ChevronRight,
User,
} from "lucide-react"; } from "lucide-react";
interface Department { interface Department {
@@ -80,6 +80,8 @@ export default function DoctorPage() {
designation: "", designation: "",
workingStatus: "", workingStatus: "",
qualification: "", qualification: "",
isActive: true,
globalSortOrder: 0,
departments: [], departments: [],
}); });
@@ -108,7 +110,8 @@ export default function DoctorPage() {
fetchAll(); fetchAll();
}, [fetchAll]); }, [fetchAll]);
const filteredDoctors = doctors.filter((doc) => { const filteredDoctors = doctors
.filter((doc) => {
const matchesSearch = const matchesSearch =
doc.name.toLowerCase().includes(searchText.toLowerCase()) || doc.name.toLowerCase().includes(searchText.toLowerCase()) ||
doc.doctorId.toLowerCase().includes(searchText.toLowerCase()); doc.doctorId.toLowerCase().includes(searchText.toLowerCase());
@@ -118,6 +121,24 @@ export default function DoctorPage() {
: true; : true;
return matchesSearch && matchesDepartment; return matchesSearch && matchesDepartment;
})
.sort((a, b) => {
if (!filterDepartment) {
return a.globalSortOrder - b.globalSortOrder;
}
const aDept = a.departments.find(
(d: any) => d.departmentId === filterDepartment,
);
const bDept = b.departments.find(
(d: any) => d.departmentId === filterDepartment,
);
return (
(aDept?.deptSortOrder ?? Number.MAX_SAFE_INTEGER) -
(bDept?.deptSortOrder ?? Number.MAX_SAFE_INTEGER)
);
}); });
useEffect(() => { useEffect(() => {
@@ -130,9 +151,27 @@ export default function DoctorPage() {
const currentItems = filteredDoctors.slice(indexOfFirstItem, indexOfLastItem); const currentItems = filteredDoctors.slice(indexOfFirstItem, indexOfLastItem);
function handleChange(e: any) { function handleChange(e: any) {
setForm({ ...form, [e.target.name]: e.target.value }); const value =
e.target.type === "number" ? Number(e.target.value) : e.target.value;
setForm({ ...form, [e.target.name]: value });
} }
const handleToggleStatus = async (doc: any) => {
try {
const newStatus = !doc.isActive;
const payload = {
isActive: newStatus,
};
await updateDoctorApi(doc.doctorId, payload);
fetchAll();
} catch (err) {
console.error("Failed to update status", err);
}
};
function handleDepartmentToggle(depId: string) { function handleDepartmentToggle(depId: string) {
const exists = form.departments.find((d: any) => d.departmentId === depId); const exists = form.departments.find((d: any) => d.departmentId === depId);
if (exists) { if (exists) {
@@ -145,11 +184,23 @@ export default function DoctorPage() {
} else { } else {
setForm({ setForm({
...form, ...form,
departments: [...form.departments, { departmentId: depId, timing: {} }], departments: [
...form.departments,
{ departmentId: depId, sortOrder: 0, timing: {} },
],
}); });
} }
} }
function handleDeptSortChange(depId: string, value: string) {
setForm({
...form,
departments: form.departments.map((d: any) =>
d.departmentId === depId ? { ...d, sortOrder: Number(value) } : d,
),
});
}
function handleTimingChange(depId: string, day: string, value: string) { function handleTimingChange(depId: string, day: string, value: string) {
setForm({ setForm({
...form, ...form,
@@ -170,6 +221,8 @@ export default function DoctorPage() {
designation: "", designation: "",
workingStatus: "", workingStatus: "",
qualification: "", qualification: "",
isActive: true,
globalSortOrder: 0,
departments: [], departments: [],
}); });
setOpenModal(true); setOpenModal(true);
@@ -180,6 +233,7 @@ export default function DoctorPage() {
try { try {
const timingRes = await getDoctorTimingApi(doc.doctorId); const timingRes = await getDoctorTimingApi(doc.doctorId);
const timingData = timingRes?.data?.departments || []; const timingData = timingRes?.data?.departments || [];
setForm({ setForm({
doctorId: doc.doctorId, doctorId: doc.doctorId,
name: doc.name, name: doc.name,
@@ -187,14 +241,17 @@ export default function DoctorPage() {
designation: doc.designation, designation: doc.designation,
workingStatus: doc.workingStatus, workingStatus: doc.workingStatus,
qualification: doc.qualification, qualification: doc.qualification,
isActive: doc.isActive ?? true,
globalSortOrder: doc.globalSortOrder ?? 0,
departments: timingData.map((d: any) => ({ departments: timingData.map((d: any) => ({
departmentId: d.departmentId, departmentId: d.departmentId,
sortOrder: d.deptSortOrder ?? 0,
timing: d.timing || {}, timing: d.timing || {},
})), })),
}); });
setOpenModal(true); setOpenModal(true);
} catch (err) { } catch (err) {
console.error(err); console.error("Error fetching doctor details:", err);
} }
} }
@@ -212,16 +269,6 @@ export default function DoctorPage() {
} }
} }
async function handleDelete(id: string) {
if (!confirm("Delete this doctor?")) return;
try {
await deleteDoctorApi(id);
fetchAll();
} catch (error) {
console.error(error);
}
}
return ( return (
<div className="p-6 space-y-6"> <div className="p-6 space-y-6">
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-4"> <div className="flex flex-col md:flex-row md:justify-between md:items-center gap-4">
@@ -278,25 +325,25 @@ export default function DoctorPage() {
<CardContent className="p-0 sm:p-6 space-y-4"> <CardContent className="p-0 sm:p-6 space-y-4">
<div className="rounded-md border overflow-x-auto overflow-y-auto max-h-[650px] relative"> <div className="rounded-md border overflow-x-auto overflow-y-auto max-h-[650px] relative">
<Table className="w-full min-w-[900px] table-fixed border-separate border-spacing-0"> <Table className="w-full min-w-[1100px] table-fixed border-separate border-spacing-0">
<TableHeader className="sticky top-0 z-20 bg-background shadow-sm"> <TableHeader className="sticky top-0 z-20 bg-background shadow-sm">
<TableRow> <TableRow>
<TableHead className="w-[100px] bg-background text-sm font-bold"> <TableHead className="w-[80px] bg-background text-sm font-bold">
ID Priority{" "}
</TableHead>
<TableHead className="w-[200px] bg-background text-sm font-bold">
Name
</TableHead> </TableHead>
<TableHead className="w-[180px] bg-background text-sm font-bold"> <TableHead className="w-[180px] bg-background text-sm font-bold">
Doctor Info
</TableHead>
<TableHead className="w-[150px] bg-background text-sm font-bold">
Designation Designation
</TableHead> </TableHead>
<TableHead className="w-[180px] bg-background text-sm font-bold">
Qualification
</TableHead>
<TableHead className="w-[220px] bg-background text-sm font-bold"> <TableHead className="w-[220px] bg-background text-sm font-bold">
Departments Departments (Hierarchy)
</TableHead> </TableHead>
<TableHead className="w-[120px] bg-background text-right text-sm font-bold"> <TableHead className="w-[80px] bg-background text-sm font-bold">
Status (Active)
</TableHead>
<TableHead className="w-[80px] bg-background text-right text-sm font-bold">
Actions Actions
</TableHead> </TableHead>
</TableRow> </TableRow>
@@ -321,8 +368,8 @@ export default function DoctorPage() {
) : ( ) : (
currentItems.map((doc) => ( currentItems.map((doc) => (
<TableRow key={doc.doctorId} className="hover:bg-muted/50"> <TableRow key={doc.doctorId} className="hover:bg-muted/50">
<TableCell className="truncate font-mono text-xs"> <TableCell className="font-mono text-sm">
{doc.doctorId} {doc.globalSortOrder}
</TableCell> </TableCell>
<TableCell> <TableCell>
@@ -332,26 +379,20 @@ export default function DoctorPage() {
> >
{doc.name} {doc.name}
</div> </div>
<div className="text-xs text-muted-foreground truncate italic"> <div className="text-xs text-muted-foreground truncate font-mono">
{doc.workingStatus} {doc.doctorId}
</div> </div>
</TableCell> </TableCell>
<TableCell> <TableCell>
<div <div
className="truncate text-sm" className="truncate text-sm font-medium"
title={doc.designation} title={doc.designation}
> >
{doc.designation || "-"} {doc.designation || "-"}
</div> </div>
</TableCell> <div className="text-xs italic text-muted-foreground truncate">
{doc.workingStatus}
<TableCell>
<div
className="truncate text-sm"
title={doc.qualification}
>
{doc.qualification || "-"}
</div> </div>
</TableCell> </TableCell>
@@ -361,14 +402,28 @@ export default function DoctorPage() {
<Badge <Badge
key={d.departmentId} key={d.departmentId}
variant="secondary" variant="secondary"
className="text-xs px-2 h-5 leading-none" className="text-xs px-2 h-6 leading-none flex items-center gap-1"
> >
{d.departmentName} {d.departmentName}
<span className="bg-primary text-primary-foreground px-1 rounded-full text-[10px]">
{d.deptSortOrder}
</span>
</Badge> </Badge>
))} ))}
{doc.departments?.length === 0 && ( </div>
<span className="text-muted-foreground">-</span> </TableCell>
)}
<TableCell>
<div className="flex items-center gap-2">
<Switch
checked={doc.isActive}
onCheckedChange={() => handleToggleStatus(doc)}
/>
<Badge
variant={doc.isActive ? "default" : "secondary"}
>
{doc.isActive ? "Active" : "Hidden"}
</Badge>
</div> </div>
</TableCell> </TableCell>
@@ -382,14 +437,6 @@ export default function DoctorPage() {
> >
<Pencil className="h-4 w-4" /> <Pencil className="h-4 w-4" />
</Button> </Button>
<Button
size="icon"
variant="ghost"
className="h-9 w-9 text-destructive hover:text-destructive hover:bg-destructive/10"
onClick={() => handleDelete(doc.doctorId)}
>
<Trash className="h-4 w-4" />
</Button>
</div> </div>
</TableCell> </TableCell>
</TableRow> </TableRow>
@@ -457,7 +504,7 @@ export default function DoctorPage() {
<div className="grid grid-cols-1 md:grid-cols-2 gap-8"> <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div className="space-y-6"> <div className="space-y-6">
<h3 className="font-bold text-base border-b pb-2"> <h3 className="font-bold text-base border-b pb-2">
Basic Information Profile & Visibility
</h3> </h3>
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
@@ -471,6 +518,39 @@ export default function DoctorPage() {
/> />
</div> </div>
<div className="flex items-center justify-between p-3 border rounded-md bg-muted/30">
<Label
htmlFor="isActive"
className="text-base font-semibold cursor-pointer"
>
Active
</Label>
<Switch
id="isActive"
checked={form.isActive}
onCheckedChange={(val) =>
setForm({ ...form, isActive: val })
}
/>
</div>
<div className="space-y-1">
<Label
htmlFor="globalSortOrder"
className="text-sm font-semibold"
>
Sort Priority (Lower numbers show first)
</Label>
<Input
id="globalSortOrder"
name="globalSortOrder"
type="number"
value={form.globalSortOrder}
onChange={handleChange}
className="text-base"
/>
</div>
<div className="space-y-1"> <div className="space-y-1">
<label className="text-sm font-semibold">Doctor ID</label> <label className="text-sm font-semibold">Doctor ID</label>
<Input <Input
@@ -556,11 +636,11 @@ export default function DoctorPage() {
<div className="space-y-6"> <div className="space-y-6">
<h3 className="font-bold text-base border-b pb-2"> <h3 className="font-bold text-base border-b pb-2">
Working Hours / Timing Department Hierarchy & Timing
</h3> </h3>
{form.departments.length === 0 ? ( {form.departments.length === 0 ? (
<div className="text-base text-muted-foreground italic py-24 text-center border-2 border-dashed rounded-lg"> <div className="text-base text-muted-foreground italic py-24 text-center border-2 border-dashed rounded-lg">
Select a department to configure timing slots Select a department to configure hierarchy and timing
</div> </div>
) : ( ) : (
<div className="space-y-8"> <div className="space-y-8">
@@ -571,15 +651,28 @@ export default function DoctorPage() {
return ( return (
<div <div
key={dep.departmentId} key={dep.departmentId}
className="space-y-4 p-5 border rounded-lg bg-background shadow-sm" className="space-y-4 p-5 border rounded-lg bg-background shadow-sm border-primary/20"
> >
<div className="flex items-center justify-between"> <div className="flex items-center justify-between border-b pb-2">
<p className="font-bold text-base text-primary"> <p className="font-bold text-base text-primary">
{depName} {depName}
</p> </p>
<Badge variant="outline" className="text-xs"> <div className="flex items-center gap-2">
Timing Slot <Label className="text-xs font-bold">
</Badge> Hierarchy Order:
</Label>
<Input
type="number"
className="w-20 h-8 text-sm"
value={dep.sortOrder}
onChange={(e) =>
handleDeptSortChange(
dep.departmentId,
e.target.value,
)
}
/>
</div>
</div> </div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-3"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-3">
{DAYS.map((day) => ( {DAYS.map((day) => (