[1.0.3] #25

Merged
kailasdevdas merged 7 commits from dev into main 2026-05-12 05:38:48 +00:00
15 changed files with 582 additions and 240 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?
workingStatus String?
qualification String?
isActive Boolean @default(true)
globalSortOrder Int @default(1000)
departments DoctorDepartment[]
appointments Appointment[]
@@ -46,6 +48,9 @@ model Department {
facilities String?
services String?
isActive Boolean @default(true)
sortOrder Int @default(1000)
doctors DoctorDepartment[]
appointments Appointment[]
@@ -61,7 +66,7 @@ model DoctorDepartment {
doctor Doctor @relation(fields: [doctorId], references: [id])
department Department @relation(fields: [departmentId], references: [id])
sortOrder Int @default(1000)
timing DoctorTiming?
createdAt DateTime @default(now())
@@ -111,7 +116,8 @@ model Career {
email String?
number String?
status String @default("new")
isActive Boolean @default(true)
sortOrder Int @default(1000)
candidates Candidate[]
createdAt DateTime @default(now())
+16 -2
View File
@@ -4,8 +4,11 @@ import prisma from "../prisma/client.js";
export const getAllCareers = async (req, res) => {
try {
const { admin } = req.query;
const careers = await prisma.career.findMany({
orderBy: {createdAt: "desc"},
where: admin === "true" ? {} : { isActive: true },
orderBy: [{ sortOrder: "asc" }, { createdAt: "desc" }],
});
const response = careers.map((c) => ({
@@ -17,6 +20,8 @@ export const getAllCareers = async (req, res) => {
email: c.email,
number: c.number,
status: c.status,
isActive: c.isActive,
sortOrder: c.sortOrder,
}));
return res.status(200).json({
@@ -44,6 +49,8 @@ export const createCareer = async (req, res) => {
email,
number,
status,
isActive,
sortOrder,
} = req.body;
if (!post || !designation) {
@@ -62,6 +69,8 @@ export const createCareer = async (req, res) => {
email,
number,
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) => {
try {
const { id } = req.params;
const updateData = { ...req.body };
if (updateData.sortOrder !== undefined) {
updateData.sortOrder = Number(updateData.sortOrder);
}
const career = await prisma.career.update({
where: { id: Number(id) },
data: req.body,
data: updateData,
});
return res.status(200).json({
@@ -2,8 +2,11 @@ import prisma from "../prisma/client.js";
export const getAllDepartments = async (req, res) => {
try {
const {admin} = req.query;
const departments = await prisma.department.findMany({
orderBy: {name: "asc"},
where: admin === "true" ? {} : {isActive: true},
orderBy: [{sortOrder: "asc"}, {name: "asc"}],
});
const response = departments.map((dep) => ({
@@ -15,6 +18,8 @@ export const getAllDepartments = async (req, res) => {
para3: dep.para3 ?? "",
facilities: dep.facilities ?? "",
services: dep.services ?? "",
isActive: dep.isActive,
sortOrder: dep.sortOrder,
}));
return res.status(200).json({
@@ -44,13 +49,14 @@ export const getDepartmentByName = async (req, res) => {
const department = await prisma.department.findFirst({
where: {
name: name,
isActive: true,
},
});
if (!department) {
return res.status(404).json({
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 ?? "",
facilities: department.facilities ?? "",
services: department.services ?? "",
isActive: department.isActive,
sortOrder: department.sortOrder,
};
return res.status(200).json({
@@ -89,6 +97,8 @@ export async function createDepartment(req, res) {
para3,
facilities,
services,
isActive,
sortOrder,
} = req.body;
if (!departmentId || !name) {
@@ -107,6 +117,8 @@ export async function createDepartment(req, res) {
para3,
facilities,
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") {
return res.status(409).json({error: "Department already exists"});
}
console.error(error);
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) => {
try {
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({
where: {departmentId},
data: {
name,
image,
para1,
para2,
para3,
facilities,
services,
},
data: updateData,
});
return res.status(200).json({
+43 -22
View File
@@ -4,7 +4,10 @@ import prisma from "../prisma/client.js";
export const getAllDoctors = async (req, res) => {
try {
const {admin} = req.query;
const doctors = await prisma.doctor.findMany({
where: admin === "true" ? {} : {isActive: true},
include: {
departments: {
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) => ({
@@ -24,10 +27,10 @@ export const getAllDoctors = async (req, res) => {
designation: doc.designation,
workingStatus: doc.workingStatus,
qualification: doc.qualification,
isActive: doc.isActive,
globalSortOrder: doc.globalSortOrder,
departments: doc.departments.map((d) => {
const t = d.timing || {};
const timingArray = [
t.monday && `Monday ${t.monday}`,
t.tuesday && `Tuesday ${t.tuesday}`,
@@ -43,6 +46,7 @@ export const getAllDoctors = async (req, res) => {
departmentId: d.department.departmentId,
departmentName: d.department.name,
timing: timingArray.join(" & "),
deptSortOrder: d.sortOrder,
};
}),
}));
@@ -135,17 +139,23 @@ export const getDoctorsByDepartmentId = async (req, res) => {
});
}
const doctors = await prisma.doctorDepartment.findMany({
where: {departmentId: department.id},
const doctorsInDept = await prisma.doctorDepartment.findMany({
where: {
departmentId: department.id,
doctor: {isActive: true},
},
include: {
doctor: true,
},
orderBy: {sortOrder: "asc"},
});
const result = doctors.map((d) => ({
const result = doctorsInDept.map((d) => ({
GG_ID: d.doctor.doctorId,
Name: d.doctor.name,
image: d.doctor.image ?? "",
designation: d.doctor.designation,
hierarchyOrder: d.sortOrder,
}));
res.status(200).json({
@@ -171,6 +181,8 @@ export const createDoctor = async (req, res) => {
designation,
workingStatus,
qualification,
isActive,
globalSortOrder,
departments,
} = req.body;
@@ -182,6 +194,9 @@ export const createDoctor = async (req, res) => {
designation,
workingStatus,
qualification,
isActive: isActive !== undefined ? isActive : true,
globalSortOrder:
globalSortOrder !== undefined ? Number(globalSortOrder) : 0,
},
});
@@ -196,6 +211,7 @@ export const createDoctor = async (req, res) => {
data: {
doctorId: doctor.id,
departmentId: department.id,
sortOrder: dep.sortOrder !== undefined ? Number(dep.sortOrder) : 0,
},
});
@@ -232,22 +248,29 @@ export const updateDoctor = async (req, res) => {
image,
workingStatus,
qualification,
isActive,
globalSortOrder,
departments,
} = req.body;
const doctor = await prisma.doctor.findUnique({
where: {doctorId},
});
if (!doctor) {
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, image, workingStatus, qualification},
data: {
name,
designation,
image,
workingStatus,
qualification,
isActive,
globalSortOrder:
globalSortOrder !== undefined ? Number(globalSortOrder) : undefined,
},
});
const oldRelations = await prisma.doctorDepartment.findMany({
@@ -265,16 +288,16 @@ export const updateDoctor = async (req, res) => {
});
for (const dep of departments) {
const department = await prisma.department.findUnique({
const targetDept = await prisma.department.findUnique({
where: {departmentId: dep.departmentId},
});
if (!targetDept) continue;
if (!department) continue;
const doctorDepartment = await prisma.doctorDepartment.create({
const newDD = await prisma.doctorDepartment.create({
data: {
doctorId: doctor.id,
departmentId: department.id,
departmentId: targetDept.id,
sortOrder: dep.sortOrder !== undefined ? Number(dep.sortOrder) : 0,
},
});
@@ -283,10 +306,7 @@ export const updateDoctor = async (req, res) => {
dep.timing;
await prisma.doctorTiming.create({
data: {
doctorDepartmentId: doctorDepartment.id,
...cleanTiming,
},
data: {doctorDepartmentId: newDD.id, ...cleanTiming},
});
}
}
@@ -421,6 +441,7 @@ export const getDoctorTimingById = async (req, res) => {
departments: doctor.departments.map((d) => ({
departmentId: d.department.departmentId,
departmentName: d.department.name,
deptSortOrder: d.sortOrder,
timing: d.timing || {},
})),
};
+27 -1
View File
@@ -28,6 +28,7 @@
"radix-ui": "^1.4.3",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-hot-toast": "^2.6.0",
"react-router-dom": "^7.13.1",
"shadcn": "^4.0.5",
"tailwind-merge": "^3.5.0",
@@ -5187,7 +5188,6 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"devOptional": true,
"license": "MIT"
},
"node_modules/data-uri-to-buffer": {
@@ -6352,6 +6352,15 @@
"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": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -8136,6 +8145,23 @@
"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": {
"version": "0.18.0",
"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",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-hot-toast": "^2.6.0",
"react-router-dom": "^7.13.1",
"shadcn": "^4.0.5",
"tailwind-merge": "^3.5.0",
+3
View File
@@ -1,4 +1,5 @@
import {BrowserRouter, Routes, Route, Navigate} from "react-router-dom";
import {Toaster} from "react-hot-toast";
import Login from "@/pages/Login";
@@ -26,6 +27,8 @@ import ImportData from "./pages/ImportData";
export default function App() {
return (
<BrowserRouter>
<Toaster position="top-right" />
<AuthProvider>
<Routes>
<Route element={<PublicRoute />}>
+39 -1
View File
@@ -1,11 +1,49 @@
import apiClient from "@/api/client";
import toast from "react-hot-toast";
export const getCareersApi = async () => {
const res = await apiClient.get("/careers/getAll");
const res = await apiClient.get("/careers/getAll?admin=true");
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) => {
try {
const res = await apiClient.delete(`/careers/${id}`);
toast.success("Career deleted successfully");
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 toast from "react-hot-toast";
export interface Department {
departmentId: string;
@@ -9,10 +10,12 @@ export interface Department {
para3: string;
facilities: string;
services: string;
isActive?: boolean;
sortOrder?: number;
}
export const getDepartmentsApi = async () => {
const res = await apiClient.get("/departments/getAll");
const res = await apiClient.get("/departments/getAll?admin=true");
return res.data;
};
@@ -25,8 +28,19 @@ export const createDepartmentApi = async (data: {
facilities?: string;
services?: string;
}) => {
try {
const res = await apiClient.post("/departments", data);
toast.success("Department created successfully");
return res.data;
} catch (error: any) {
toast.error(
error?.response?.data?.message || "Failed to create department",
);
throw error;
}
};
export const updateDepartmentApi = async (
@@ -40,11 +54,33 @@ export const updateDepartmentApi = async (
services?: string;
},
) => {
try {
const res = await apiClient.put(`/departments/${departmentId}`, data);
toast.success("Department updated successfully");
return res.data;
} catch (error: any) {
toast.error(
error?.response?.data?.message || "Failed to update department",
);
throw error;
}
};
export const deleteDepartmentApi = async (departmentId: string) => {
try {
const res = await apiClient.delete(`/departments/${departmentId}`);
toast.success("Department deleted successfully");
return res.data;
} catch (error: any) {
toast.error(
error?.response?.data?.message || "Failed to delete department",
);
throw error;
}
};
+29 -1
View File
@@ -1,4 +1,5 @@
import apiClient from "@/api/client";
import toast from "react-hot-toast";
export interface Doctor {
doctorId: string;
@@ -24,7 +25,7 @@ export interface Doctor {
}
export const getDoctorsApi = async () => {
const res = await apiClient.get("/doctors/getAll");
const res = await apiClient.get("/doctors/getAll?admin=true");
return res.data;
};
@@ -34,21 +35,48 @@ export const getDoctorByIdApi = async (doctorId: string) => {
};
export const createDoctorApi = async (data: Doctor) => {
try {
const res = await apiClient.post("/doctors", data);
toast.success("Doctor created successfully");
return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || "Failed to create doctor");
throw error;
}
};
export const updateDoctorApi = async (
doctorId: string,
data: Partial<Doctor>,
) => {
try {
const res = await apiClient.patch(`/doctors/${doctorId}`, data);
toast.success("Doctor updated successfully");
return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || "Failed to update doctor");
throw error;
}
};
export const deleteDoctorApi = async (doctorId: string) => {
try {
const res = await apiClient.delete(`/doctors/${doctorId}`);
toast.success("Doctor deleted successfully");
return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || "Failed to delete doctor");
throw error;
}
};
export const getDoctorTimingApi = async (doctorId: string) => {
+66 -43
View File
@@ -1,7 +1,6 @@
import { useState, useEffect, useCallback } from "react";
import { getCareersApi, deleteCareerApi } from "@/api/career";
import apiClient from "@/api/client";
import { getCareersApi, updateCareerApi, createCareerApi } from "@/api/career";
import {
Table,
@@ -24,12 +23,13 @@ import {
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import {
Loader2,
Plus,
Pencil,
Trash,
RefreshCw,
ChevronLeft,
ChevronRight,
@@ -55,6 +55,8 @@ export default function CareerPage() {
email: "",
number: "",
status: "new",
isActive: true,
sortOrder: 0,
});
const fetchAll = useCallback(async () => {
@@ -92,6 +94,18 @@ export default function CareerPage() {
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() {
setEditing(null);
setForm({
@@ -102,6 +116,8 @@ export default function CareerPage() {
email: "",
number: "",
status: "new",
isActive: true,
sortOrder: 0,
});
setOpenModal(true);
}
@@ -116,6 +132,8 @@ export default function CareerPage() {
email: item.email || "",
number: item.number || "",
status: item.status || "new",
isActive: item.isActive ?? true,
sortOrder: item.sortOrder ?? 0,
});
setOpenModal(true);
}
@@ -123,10 +141,11 @@ export default function CareerPage() {
async function handleSubmit() {
try {
if (editing) {
await apiClient.patch(`/careers/${editing.id}`, form);
await updateCareerApi(editing.id, form);
} else {
await apiClient.post("/careers", form);
await createCareerApi(form);
}
setOpenModal(false);
fetchAll();
} 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 (
<div className="p-6 space-y-6">
<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">
<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">
<TableRow>
<TableHead className="w-[60px] bg-background font-bold text-sm">
ID
<TableHead className="w-[80px] bg-background font-bold text-sm">
Priority
</TableHead>
<TableHead className="w-[200px] bg-background font-bold text-sm">
<TableHead className="w-[250px] bg-background font-bold text-sm">
Post & Designation
</TableHead>
<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">
Experience
</TableHead>
<TableHead className="w-[200px] bg-background font-bold text-sm">
Contact Info
<TableHead className="w-[80px] bg-background font-bold text-sm">
Status (Active)
</TableHead>
<TableHead className="w-[100px] bg-background font-bold text-sm">
Status
</TableHead>
<TableHead className="w-[120px] bg-background font-bold text-right text-sm">
<TableHead className="w-[80px] bg-background font-bold text-right text-sm">
Actions
</TableHead>
</TableRow>
@@ -207,14 +217,14 @@ export default function CareerPage() {
<TableBody>
{loading ? (
<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" />
</TableCell>
</TableRow>
) : currentItems.length === 0 ? (
<TableRow>
<TableCell
colSpan={7}
colSpan={6}
className="text-center text-muted-foreground py-10 text-base"
>
No careers found
@@ -224,7 +234,7 @@ export default function CareerPage() {
currentItems.map((item) => (
<TableRow key={item.id} className="hover:bg-muted/50">
<TableCell className="font-mono text-xs">
{item.id}
{item.sortOrder}
</TableCell>
<TableCell>
<div className="font-semibold text-base truncate">
@@ -243,20 +253,18 @@ export default function CareerPage() {
{item.experienceNeed}
</TableCell>
<TableCell>
<div className="text-sm font-medium">{item.email}</div>
<div className="text-xs text-muted-foreground">
{item.number}
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Switch
checked={item.isActive}
onCheckedChange={() => handleToggleStatus(item)}
/>
<Badge
variant={
item.status === "active" ? "default" : "secondary"
}
variant={item.isActive ? "default" : "secondary"}
className="capitalize"
>
{item.status}
{item.isActive ? "Active" : "Hidden"}
</Badge>
</div>
</TableCell>
<TableCell className="text-right">
@@ -269,15 +277,6 @@ export default function CareerPage() {
>
<Pencil className="h-4 w-4" />
</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>
</TableCell>
</TableRow>
@@ -393,6 +392,30 @@ export default function CareerPage() {
onChange={handleChange}
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>
+85 -58
View File
@@ -6,7 +6,6 @@ import {
getDepartmentsApi,
createDepartmentApi,
updateDepartmentApi,
deleteDepartmentApi,
} from "@/api/department";
import {
@@ -31,13 +30,15 @@ import {
import {Input} from "@/components/ui/input";
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 {
Loader2,
RefreshCw,
Plus,
Pencil,
Trash,
Eye,
ChevronLeft,
ChevronRight,
@@ -52,6 +53,8 @@ interface Department {
para3: string;
facilities: string;
services: string;
isActive: boolean;
sortOrder: number;
}
export default function DepartmentPage() {
@@ -79,6 +82,8 @@ export default function DepartmentPage() {
para3: "",
facilities: "",
services: "",
isActive: true,
sortOrder: 0,
});
const fetchDepartments = useCallback(async () => {
@@ -122,13 +127,23 @@ export default function DepartmentPage() {
);
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) {
if (!text) return "-";
return text.length > limit ? text.substring(0, limit) + "..." : text;
const handleToggleStatus = async (dep: Department) => {
try {
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() {
setEditing(null);
@@ -141,13 +156,19 @@ export default function DepartmentPage() {
para3: "",
facilities: "",
services: "",
isActive: true,
sortOrder: 0,
});
setOpenModal(true);
}
function openEdit(dep: Department) {
setEditing(dep);
setForm(dep);
setForm({
...dep,
isActive: dep.isActive ?? true,
sortOrder: dep.sortOrder ?? 0,
});
setOpenModal(true);
}
@@ -160,7 +181,7 @@ export default function DepartmentPage() {
try {
if (editing) {
const {departmentId, ...updateData} = form;
await updateDepartmentApi(editing.departmentId, updateData);
await updateDepartmentApi(editing.departmentId, form as any);
} else {
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 (
<div className="p-6 space-y-6">
<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">
<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">
<TableRow>
<TableHead className="w-[100px] bg-background text-sm font-bold">
ID
Priority
</TableHead>
<TableHead className="w-[200px] bg-background text-sm font-bold">
<TableHead className="w-[300px] bg-background text-sm font-bold">
Name
</TableHead>
<TableHead className="w-[250px] bg-background text-sm font-bold">
Para 1
<TableHead className="w-[80px] bg-background text-sm font-bold">
Status (Active)
</TableHead>
<TableHead className="w-[220px] bg-background 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">
<TableHead className="w-[80px] bg-background text-right text-sm font-bold">
Actions
</TableHead>
</TableRow>
@@ -253,14 +257,14 @@ export default function DepartmentPage() {
<TableBody>
{loading ? (
<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" />
</TableCell>
</TableRow>
) : currentItems.length === 0 ? (
<TableRow>
<TableCell
colSpan={6}
colSpan={4}
className="text-center text-muted-foreground py-10 text-base"
>
No departments found
@@ -272,8 +276,8 @@ export default function DepartmentPage() {
key={dep.departmentId}
className="hover:bg-muted/50"
>
<TableCell className="font-mono text-xs">
{dep.departmentId}
<TableCell className="font-mono text-sm">
{dep.sortOrder}
</TableCell>
<TableCell>
@@ -283,23 +287,21 @@ export default function DepartmentPage() {
>
{dep.name}
</div>
</TableCell>
<TableCell>
<div className="text-sm break-words whitespace-normal">
{truncate(dep.para1)}
<div className="text-xs text-muted-foreground">
{dep.departmentId}
</div>
</TableCell>
<TableCell>
<div className="text-sm break-words whitespace-normal">
{truncate(dep.facilities)}
</div>
</TableCell>
<TableCell>
<div className="text-sm break-words whitespace-normal">
{truncate(dep.services)}
<div className="flex items-center gap-2">
<Switch
checked={dep.isActive}
onCheckedChange={() => handleToggleStatus(dep)}
/>
<Badge
variant={dep.isActive ? "default" : "secondary"}
>
{dep.isActive ? "Active" : "Hidden"}
</Badge>
</div>
</TableCell>
@@ -322,15 +324,6 @@ export default function DepartmentPage() {
>
<Pencil className="h-4 w-4" />
</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>
</TableCell>
</TableRow>
@@ -450,6 +443,32 @@ export default function DepartmentPage() {
onChange={handleChange}
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>
<DialogFooter>
@@ -470,9 +489,17 @@ export default function DepartmentPage() {
</DialogHeader>
{viewData && (
<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>
<b>ID:</b> {viewData.departmentId}
</p>
</div>
<p>
<b>Name:</b> {viewData.name}
</p>
+148 -61
View File
@@ -7,7 +7,6 @@ import {
getDoctorsApi,
createDoctorApi,
updateDoctorApi,
deleteDoctorApi,
getDoctorTimingApi,
} from "@/api/doctor";
import { getDepartmentsApi } from "@/api/department";
@@ -31,15 +30,16 @@ import {
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import {
Loader2,
RefreshCw,
Plus,
Pencil,
Trash,
ChevronLeft,
ChevronRight,
User,
} from "lucide-react";
interface Department {
@@ -80,6 +80,8 @@ export default function DoctorPage() {
designation: "",
workingStatus: "",
qualification: "",
isActive: true,
globalSortOrder: 0,
departments: [],
});
@@ -108,7 +110,8 @@ export default function DoctorPage() {
fetchAll();
}, [fetchAll]);
const filteredDoctors = doctors.filter((doc) => {
const filteredDoctors = doctors
.filter((doc) => {
const matchesSearch =
doc.name.toLowerCase().includes(searchText.toLowerCase()) ||
doc.doctorId.toLowerCase().includes(searchText.toLowerCase());
@@ -118,6 +121,24 @@ export default function DoctorPage() {
: true;
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(() => {
@@ -130,9 +151,21 @@ export default function DoctorPage() {
const currentItems = filteredDoctors.slice(indexOfFirstItem, indexOfLastItem);
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 updatedDoc = { ...doc, isActive: !doc.isActive };
await updateDoctorApi(doc.doctorId, updatedDoc);
fetchAll();
} catch (err) {
console.error("Failed to update status", err);
}
};
function handleDepartmentToggle(depId: string) {
const exists = form.departments.find((d: any) => d.departmentId === depId);
if (exists) {
@@ -145,11 +178,23 @@ export default function DoctorPage() {
} else {
setForm({
...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) {
setForm({
...form,
@@ -170,6 +215,8 @@ export default function DoctorPage() {
designation: "",
workingStatus: "",
qualification: "",
isActive: true,
globalSortOrder: 0,
departments: [],
});
setOpenModal(true);
@@ -180,6 +227,7 @@ export default function DoctorPage() {
try {
const timingRes = await getDoctorTimingApi(doc.doctorId);
const timingData = timingRes?.data?.departments || [];
setForm({
doctorId: doc.doctorId,
name: doc.name,
@@ -187,14 +235,17 @@ export default function DoctorPage() {
designation: doc.designation,
workingStatus: doc.workingStatus,
qualification: doc.qualification,
isActive: doc.isActive ?? true,
globalSortOrder: doc.globalSortOrder ?? 0,
departments: timingData.map((d: any) => ({
departmentId: d.departmentId,
sortOrder: d.deptSortOrder ?? 0,
timing: d.timing || {},
})),
});
setOpenModal(true);
} catch (err) {
console.error(err);
console.error("Error fetching doctor details:", err);
}
}
@@ -212,16 +263,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 (
<div className="p-6 space-y-6">
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-4">
@@ -278,25 +319,25 @@ export default function DoctorPage() {
<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">
<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">
<TableRow>
<TableHead className="w-[100px] bg-background text-sm font-bold">
ID
</TableHead>
<TableHead className="w-[200px] bg-background text-sm font-bold">
Name
<TableHead className="w-[80px] bg-background text-sm font-bold">
Priority{" "}
</TableHead>
<TableHead className="w-[180px] bg-background text-sm font-bold">
Doctor Info
</TableHead>
<TableHead className="w-[150px] bg-background text-sm font-bold">
Designation
</TableHead>
<TableHead className="w-[180px] bg-background text-sm font-bold">
Qualification
</TableHead>
<TableHead className="w-[220px] bg-background text-sm font-bold">
Departments
Departments (Hierarchy)
</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
</TableHead>
</TableRow>
@@ -321,8 +362,8 @@ export default function DoctorPage() {
) : (
currentItems.map((doc) => (
<TableRow key={doc.doctorId} className="hover:bg-muted/50">
<TableCell className="truncate font-mono text-xs">
{doc.doctorId}
<TableCell className="font-mono text-sm">
{doc.globalSortOrder}
</TableCell>
<TableCell>
@@ -332,26 +373,20 @@ export default function DoctorPage() {
>
{doc.name}
</div>
<div className="text-xs text-muted-foreground truncate italic">
{doc.workingStatus}
<div className="text-xs text-muted-foreground truncate font-mono">
{doc.doctorId}
</div>
</TableCell>
<TableCell>
<div
className="truncate text-sm"
className="truncate text-sm font-medium"
title={doc.designation}
>
{doc.designation || "-"}
</div>
</TableCell>
<TableCell>
<div
className="truncate text-sm"
title={doc.qualification}
>
{doc.qualification || "-"}
<div className="text-xs italic text-muted-foreground truncate">
{doc.workingStatus}
</div>
</TableCell>
@@ -361,14 +396,28 @@ export default function DoctorPage() {
<Badge
key={d.departmentId}
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}
<span className="bg-primary text-primary-foreground px-1 rounded-full text-[10px]">
{d.deptSortOrder}
</span>
</Badge>
))}
{doc.departments?.length === 0 && (
<span className="text-muted-foreground">-</span>
)}
</div>
</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>
</TableCell>
@@ -382,14 +431,6 @@ export default function DoctorPage() {
>
<Pencil className="h-4 w-4" />
</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>
</TableCell>
</TableRow>
@@ -457,7 +498,7 @@ export default function DoctorPage() {
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div className="space-y-6">
<h3 className="font-bold text-base border-b pb-2">
Basic Information
Profile & Visibility
</h3>
<div className="space-y-4">
<div className="space-y-2">
@@ -471,6 +512,39 @@ export default function DoctorPage() {
/>
</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">
<label className="text-sm font-semibold">Doctor ID</label>
<Input
@@ -556,11 +630,11 @@ export default function DoctorPage() {
<div className="space-y-6">
<h3 className="font-bold text-base border-b pb-2">
Working Hours / Timing
Department Hierarchy & Timing
</h3>
{form.departments.length === 0 ? (
<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 className="space-y-8">
@@ -571,15 +645,28 @@ export default function DoctorPage() {
return (
<div
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">
{depName}
</p>
<Badge variant="outline" className="text-xs">
Timing Slot
</Badge>
<div className="flex items-center gap-2">
<Label className="text-xs font-bold">
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 className="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-3">
{DAYS.map((day) => (