Compare commits

..

4 Commits

10 changed files with 197 additions and 19 deletions
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Doctor" ADD COLUMN "isFeatured" BOOLEAN NOT NULL DEFAULT false;
+1
View File
@@ -27,6 +27,7 @@ model Doctor {
workingStatus String? workingStatus String?
qualification String? qualification String?
isActive Boolean @default(true) isActive Boolean @default(true)
isFeatured Boolean @default(false)
globalSortOrder Int @default(1000) globalSortOrder Int @default(1000)
specializations DoctorSpecialization[] specializations DoctorSpecialization[]
professionalSummary String? @db.Text professionalSummary String? @db.Text
+69 -1
View File
@@ -34,6 +34,7 @@ export const getAllDoctors = async (req, res) => {
workingStatus: doc.workingStatus, workingStatus: doc.workingStatus,
qualification: doc.qualification, qualification: doc.qualification,
isActive: doc.isActive, isActive: doc.isActive,
isFeatured: doc.isFeatured,
experience: doc.experience, experience: doc.experience,
professionalSummary: doc.professionalSummary, professionalSummary: doc.professionalSummary,
globalSortOrder: doc.globalSortOrder, globalSortOrder: doc.globalSortOrder,
@@ -129,6 +130,7 @@ export const getDoctorByDoctorId = async (req, res) => {
experience: doctor.experience, experience: doctor.experience,
professionalSummary: doctor.professionalSummary, professionalSummary: doctor.professionalSummary,
isActive: doctor.isActive, isActive: doctor.isActive,
isFeatured: doctor.isFeatured,
seo: { seo: {
seoTitle: doctor.seo?.seoTitle ?? '', seoTitle: doctor.seo?.seoTitle ?? '',
metaDescription: doctor.seo?.metaDescription ?? '', metaDescription: doctor.seo?.metaDescription ?? '',
@@ -240,6 +242,7 @@ export const createDoctor = async (req, res) => {
workingStatus, workingStatus,
qualification, qualification,
isActive, isActive,
isFeatured,
globalSortOrder, globalSortOrder,
departments, departments,
experience, experience,
@@ -297,6 +300,7 @@ export const createDoctor = async (req, res) => {
professionalSummary, professionalSummary,
seoId: seo.id, seoId: seo.id,
isActive: isActive !== undefined ? isActive : true, isActive: isActive !== undefined ? isActive : true,
isFeatured: isFeatured !== undefined ? isFeatured : false,
globalSortOrder: globalSortOrder !== undefined ? Number(globalSortOrder) : 0, globalSortOrder: globalSortOrder !== undefined ? Number(globalSortOrder) : 0,
}, },
}); });
@@ -361,6 +365,7 @@ export const updateDoctor = async (req, res) => {
workingStatus, workingStatus,
qualification, qualification,
isActive, isActive,
isFeatured,
globalSortOrder, globalSortOrder,
departments, departments,
experience, experience,
@@ -397,6 +402,19 @@ export const updateDoctor = async (req, res) => {
message: `Doctor has been ${doctor.isActive ? 'deactivated' : 'activated'} successfully`, message: `Doctor has been ${doctor.isActive ? 'deactivated' : 'activated'} successfully`,
}); });
} }
if (action === 'toggleFeatured') {
await prisma.doctor.update({
where: { id: doctor.id },
data: {
isFeatured: !doctor.isFeatured,
},
});
return res.status(200).json({
success: true,
message: `Doctor has been ${doctor.isFeatured ? 'removed from featured' : 'marked as featured'} successfully`,
});
}
const messages = []; const messages = [];
if (!doctorId) messages.push('Doctor ID is required'); if (!doctorId) messages.push('Doctor ID is required');
@@ -423,7 +441,8 @@ export const updateDoctor = async (req, res) => {
image, image,
workingStatus, workingStatus,
qualification, qualification,
isActive, isActive: isActive !== undefined ? isActive : undefined,
isFeatured: isFeatured !== undefined ? isFeatured : undefined,
experience: experience ? Number(experience) : null, experience: experience ? Number(experience) : null,
professionalSummary, professionalSummary,
globalSortOrder: globalSortOrder !== undefined ? Number(globalSortOrder) : undefined, globalSortOrder: globalSortOrder !== undefined ? Number(globalSortOrder) : undefined,
@@ -700,3 +719,52 @@ export const getDoctorTimingById = async (req, res) => {
}); });
} }
}; };
export const getFeaturedDoctors = async (req, res) => {
try {
const doctors = await prisma.doctor.findMany({
where: {
isActive: true,
isFeatured: true,
},
include: {
seo: {
select: {
slug: true,
},
},
departments: {
include: {
department: true,
},
},
},
orderBy: [{ globalSortOrder: 'asc' }, { name: 'asc' }],
});
const data = doctors.map((doc) => ({
doctorId: doc.doctorId,
name: doc.name,
image: doc.image ?? '',
designation: doc.designation,
qualification: doc.qualification,
experience: doc.experience,
slug: doc.seo?.slug ?? '',
departments: doc.departments.map((d) => ({
departmentId: d.department.departmentId,
departmentName: d.department.name,
})),
}));
res.status(200).json({
success: true,
data,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to fetch featured doctors',
});
}
};
@@ -486,3 +486,33 @@ export const getAllInquiries = async (req, res) => {
return res.status(500).json({ success: false, message: 'Failed to fetch inquiries' }); return res.status(500).json({ success: false, message: 'Failed to fetch inquiries' });
} }
}; };
export const getFeaturedPackages = async (req, res) => {
try {
const packages = await prisma.healthPackage.findMany({
where: {
isActive: true,
isFeatured: true,
category: {
isActive: true,
},
},
include: {
category: true,
seo: true,
},
orderBy: [{ sortOrder: 'asc' }, { createdAt: 'desc' }],
});
return res.status(200).json({
success: true,
data: packages,
});
} catch (error) {
console.error(error);
return res.status(500).json({
success: false,
message: 'Failed to fetch featured packages',
});
}
};
+2
View File
@@ -8,6 +8,7 @@ import {
getDoctorTimingById, getDoctorTimingById,
getDoctorByDoctorId, getDoctorByDoctorId,
getDoctorsByDepartmentId, getDoctorsByDepartmentId,
getFeaturedDoctors,
} from '../controllers/doctor.controller.js'; } from '../controllers/doctor.controller.js';
import jwtAuthMiddleware from '../middleware/auth.js'; import jwtAuthMiddleware from '../middleware/auth.js';
@@ -18,6 +19,7 @@ router.get('/getAll', getAllDoctors);
router.get('/search', getDoctorsByDepartmentId); router.get('/search', getDoctorsByDepartmentId);
router.get('/getTimings', getDoctorTimings); router.get('/getTimings', getDoctorTimings);
router.get('/getTimings/:doctorId', getDoctorTimingById); router.get('/getTimings/:doctorId', getDoctorTimingById);
router.get('/featured', getFeaturedDoctors);
router.get('/:doctorId', getDoctorByDoctorId); router.get('/:doctorId', getDoctorByDoctorId);
router.post('/', jwtAuthMiddleware, createDoctor); router.post('/', jwtAuthMiddleware, createDoctor);
+2
View File
@@ -12,6 +12,7 @@ import {
createPackage, createPackage,
updatePackage, updatePackage,
deletePackage, deletePackage,
getFeaturedPackages,
// Inquiries // Inquiries
createPackageInquiry, createPackageInquiry,
@@ -26,6 +27,7 @@ router.get('/packages', getAllPackages);
router.get('/packages/:slug', getPackageBySlug); router.get('/packages/:slug', getPackageBySlug);
router.get('/categories', getAllCategories); router.get('/categories', getAllCategories);
router.post('/inquiry', createPackageInquiry); router.post('/inquiry', createPackageInquiry);
router.get('/featured', getFeaturedPackages);
router.get('/inquiries', jwtAuthMiddleware, getAllInquiries); router.get('/inquiries', jwtAuthMiddleware, getAllInquiries);
router.post('/', jwtAuthMiddleware, createPackage); router.post('/', jwtAuthMiddleware, createPackage);
+2 -1
View File
@@ -9,6 +9,7 @@ export interface Doctor {
workingStatus?: string; workingStatus?: string;
qualification?: string; qualification?: string;
isActive: boolean; isActive: boolean;
isFeatured: boolean;
globalSortOrder: number; globalSortOrder: number;
departments?: { departments?: {
@@ -53,7 +54,7 @@ export const createDoctorApi = async (data: Doctor) => {
export const updateDoctorApi = async ( export const updateDoctorApi = async (
doctorId: string, doctorId: string,
data: Partial<Doctor>, data: Partial<Doctor>,
action: 'toggleStatus' | 'updateDetails' = 'updateDetails' action: 'toggleStatus' | 'toggleFeatured' | 'updateDetails' = 'updateDetails'
) => { ) => {
try { try {
const res = await apiClient.patch(`/doctors/${doctorId}/${action}`, data); const res = await apiClient.patch(`/doctors/${doctorId}/${action}`, data);
@@ -120,22 +120,40 @@ export default function HealthPackageModal({
/> />
</div> </div>
<div className="flex items-center justify-between border rounded-xl p-4 bg-muted/30"> <div className="grid grid-cols-2 gap-4">
<div> <div className="flex items-center justify-between border rounded-xl p-4 bg-muted/30">
<p className="font-semibold">Active Visibility</p> <div>
<p className="font-semibold">Active</p>
<p className="text-sm text-muted-foreground">Show publicly</p>
</div>
<p className="text-sm text-muted-foreground">Show this package publicly</p> <Switch
checked={pkgForm.isActive}
onCheckedChange={(val) =>
setPkgForm({
...pkgForm,
isActive: val,
})
}
/>
</div> </div>
<Switch <div className="flex items-center justify-between border rounded-xl p-4 bg-muted/30">
checked={pkgForm.isActive} <div>
onCheckedChange={(val) => <p className="font-semibold">Featured</p>
setPkgForm({ <p className="text-sm text-muted-foreground">Show on homepage</p>
...pkgForm, </div>
isActive: val,
}) <Switch
} checked={pkgForm.isFeatured || false}
/> onCheckedChange={(val) =>
setPkgForm({
...pkgForm,
isFeatured: val,
})
}
/>
</div>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+44 -4
View File
@@ -51,6 +51,7 @@ export default function DoctorPage() {
workingStatus: '', workingStatus: '',
qualification: '', qualification: '',
isActive: true, isActive: true,
isFeatured: false,
globalSortOrder: 0, globalSortOrder: 0,
departments: [], departments: [],
professionalSummary: '', professionalSummary: '',
@@ -156,6 +157,22 @@ export default function DoctorPage() {
} }
}; };
const handleToggleFeatured = async (doc: any) => {
try {
const newFeaturedStatus = !doc.isFeatured;
const payload = {
isFeatured: newFeaturedStatus,
};
await updateDoctorApi(doc.doctorId, payload, 'toggleFeatured');
fetchAll();
} catch (err) {
console.error('Failed to update featured 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) {
@@ -201,6 +218,7 @@ export default function DoctorPage() {
experience: '', experience: '',
professionalSummary: '', professionalSummary: '',
isActive: true, isActive: true,
isFeatured: false,
globalSortOrder: 0, globalSortOrder: 0,
specializations: [ specializations: [
{ {
@@ -235,6 +253,7 @@ export default function DoctorPage() {
workingStatus: doc.workingStatus, workingStatus: doc.workingStatus,
qualification: doc.qualification, qualification: doc.qualification,
isActive: doc.isActive ?? true, isActive: doc.isActive ?? true,
isFeatured: doc.isFeatured ?? false,
globalSortOrder: doc.globalSortOrder ?? 0, globalSortOrder: doc.globalSortOrder ?? 0,
experience: doc.experience || '', experience: doc.experience || '',
professionalSummary: doc.professionalSummary || '', professionalSummary: doc.professionalSummary || '',
@@ -353,14 +372,15 @@ 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-[1100px] table-fixed border-separate border-spacing-0"> <Table className="w-full min-w-[1200px] 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-[80px] bg-background text-sm font-bold">Priority </TableHead> <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-[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-[150px] bg-background text-sm font-bold">Designation</TableHead>
<TableHead className="w-[220px] bg-background text-sm font-bold">Departments (Hierarchy)</TableHead> <TableHead className="w-[220px] bg-background text-sm font-bold">Departments (Hierarchy)</TableHead>
<TableHead className="w-[80px] bg-background text-sm font-bold">Status (Active)</TableHead> <TableHead className="w-[100px] bg-background text-sm font-bold">Status (Active)</TableHead>
<TableHead className="w-[100px] bg-background text-sm font-bold">Featured</TableHead>
<TableHead className="w-[80px] bg-background text-right text-sm font-bold">Actions</TableHead> <TableHead className="w-[80px] bg-background text-right text-sm font-bold">Actions</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
@@ -368,13 +388,13 @@ export default function DoctorPage() {
<TableBody> <TableBody>
{loading ? ( {loading ? (
<TableRow> <TableRow>
<TableCell colSpan={6} className="text-center py-10"> <TableCell colSpan={7} 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 colSpan={6} className="text-center text-muted-foreground py-10 text-base"> <TableCell colSpan={7} className="text-center text-muted-foreground py-10 text-base">
No doctors found No doctors found
</TableCell> </TableCell>
</TableRow> </TableRow>
@@ -423,6 +443,15 @@ export default function DoctorPage() {
</div> </div>
</TableCell> </TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Switch checked={doc.isFeatured || false} onCheckedChange={() => handleToggleFeatured(doc)} />
<Badge variant={doc.isFeatured ? 'default' : 'secondary'}>
{doc.isFeatured ? 'Featured' : 'Hidden'}
</Badge>
</div>
</TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<Button size="icon" variant="ghost" className="h-9 w-9" onClick={() => handlePreview(doc)}> <Button size="icon" variant="ghost" className="h-9 w-9" onClick={() => handlePreview(doc)}>
@@ -508,6 +537,17 @@ export default function DoctorPage() {
/> />
</div> </div>
<div className="flex items-center justify-between p-3 border rounded-md bg-muted/30">
<Label htmlFor="isFeatured" className="text-base font-semibold cursor-pointer">
Featured
</Label>
<Switch
id="isFeatured"
checked={form.isFeatured}
onCheckedChange={(val) => setForm({ ...form, isFeatured: val })}
/>
</div>
<div className="space-y-1"> <div className="space-y-1">
<Label htmlFor="globalSortOrder" className="text-sm font-semibold"> <Label htmlFor="globalSortOrder" className="text-sm font-semibold">
Sort Priority (Lower numbers show first) Sort Priority (Lower numbers show first)
+14
View File
@@ -62,6 +62,7 @@ export default function HealthPackagePage() {
discountedPrice: undefined, discountedPrice: undefined,
categoryId: 0, categoryId: 0,
isActive: true, isActive: true,
isFeatured: false,
sortOrder: 1000, sortOrder: 1000,
seo: { seo: {
seoTitle: '', seoTitle: '',
@@ -167,6 +168,7 @@ export default function HealthPackagePage() {
discountedPrice: undefined, discountedPrice: undefined,
categoryId: categories[0]?.id || 0, categoryId: categories[0]?.id || 0,
isActive: true, isActive: true,
isFeatured: false,
sortOrder: 1000, sortOrder: 1000,
seo: { seo: {
seoTitle: '', seoTitle: '',
@@ -366,6 +368,7 @@ export default function HealthPackagePage() {
<TableHead className="w-[150px] bg-background text-sm font-bold">Category</TableHead> <TableHead className="w-[150px] bg-background text-sm font-bold">Category</TableHead>
<TableHead className="w-[150px] bg-background text-sm font-bold">Pricing</TableHead> <TableHead className="w-[150px] bg-background text-sm font-bold">Pricing</TableHead>
<TableHead className="w-[120px] bg-background text-sm font-bold">Status</TableHead> <TableHead className="w-[120px] bg-background text-sm font-bold">Status</TableHead>
<TableHead className="w-[100px] bg-background text-sm font-bold">Featured</TableHead>
<TableHead className="w-[120px] bg-background text-right text-sm font-bold">Actions</TableHead> <TableHead className="w-[120px] bg-background text-right text-sm font-bold">Actions</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
@@ -418,6 +421,17 @@ export default function HealthPackagePage() {
</Badge> </Badge>
</div> </div>
</TableCell> </TableCell>
<TableCell>
<Switch
checked={pkg.isFeatured}
onCheckedChange={async () => {
await updateHealthPackageApi(pkg.id!, {
isFeatured: !pkg.isFeatured,
});
fetchData();
}}
/>
</TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<Button <Button