Merge pull request 'feat: add featured doctors and health packages APIs' (#50) from feat/featured-doctors-packages-api into dev
Reviewed-on: #50
This commit was merged in pull request #50.
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Doctor" ADD COLUMN "isFeatured" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -27,6 +27,7 @@ model Doctor {
|
||||
workingStatus String?
|
||||
qualification String?
|
||||
isActive Boolean @default(true)
|
||||
isFeatured Boolean @default(false)
|
||||
globalSortOrder Int @default(1000)
|
||||
specializations DoctorSpecialization[]
|
||||
professionalSummary String? @db.Text
|
||||
|
||||
@@ -34,6 +34,7 @@ export const getAllDoctors = async (req, res) => {
|
||||
workingStatus: doc.workingStatus,
|
||||
qualification: doc.qualification,
|
||||
isActive: doc.isActive,
|
||||
isFeatured: doc.isFeatured,
|
||||
experience: doc.experience,
|
||||
professionalSummary: doc.professionalSummary,
|
||||
globalSortOrder: doc.globalSortOrder,
|
||||
@@ -129,6 +130,7 @@ export const getDoctorByDoctorId = async (req, res) => {
|
||||
experience: doctor.experience,
|
||||
professionalSummary: doctor.professionalSummary,
|
||||
isActive: doctor.isActive,
|
||||
isFeatured: doctor.isFeatured,
|
||||
seo: {
|
||||
seoTitle: doctor.seo?.seoTitle ?? '',
|
||||
metaDescription: doctor.seo?.metaDescription ?? '',
|
||||
@@ -240,6 +242,7 @@ export const createDoctor = async (req, res) => {
|
||||
workingStatus,
|
||||
qualification,
|
||||
isActive,
|
||||
isFeatured,
|
||||
globalSortOrder,
|
||||
departments,
|
||||
experience,
|
||||
@@ -297,6 +300,7 @@ export const createDoctor = async (req, res) => {
|
||||
professionalSummary,
|
||||
seoId: seo.id,
|
||||
isActive: isActive !== undefined ? isActive : true,
|
||||
isFeatured: isFeatured !== undefined ? isFeatured : false,
|
||||
globalSortOrder: globalSortOrder !== undefined ? Number(globalSortOrder) : 0,
|
||||
},
|
||||
});
|
||||
@@ -361,6 +365,7 @@ export const updateDoctor = async (req, res) => {
|
||||
workingStatus,
|
||||
qualification,
|
||||
isActive,
|
||||
isFeatured,
|
||||
globalSortOrder,
|
||||
departments,
|
||||
experience,
|
||||
@@ -397,6 +402,19 @@ export const updateDoctor = async (req, res) => {
|
||||
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 = [];
|
||||
if (!doctorId) messages.push('Doctor ID is required');
|
||||
@@ -423,7 +441,8 @@ export const updateDoctor = async (req, res) => {
|
||||
image,
|
||||
workingStatus,
|
||||
qualification,
|
||||
isActive,
|
||||
isActive: isActive !== undefined ? isActive : undefined,
|
||||
isFeatured: isFeatured !== undefined ? isFeatured : undefined,
|
||||
experience: experience ? Number(experience) : null,
|
||||
professionalSummary,
|
||||
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' });
|
||||
}
|
||||
};
|
||||
|
||||
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',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
getDoctorTimingById,
|
||||
getDoctorByDoctorId,
|
||||
getDoctorsByDepartmentId,
|
||||
getFeaturedDoctors,
|
||||
} from '../controllers/doctor.controller.js';
|
||||
|
||||
import jwtAuthMiddleware from '../middleware/auth.js';
|
||||
@@ -18,6 +19,7 @@ router.get('/getAll', getAllDoctors);
|
||||
router.get('/search', getDoctorsByDepartmentId);
|
||||
router.get('/getTimings', getDoctorTimings);
|
||||
router.get('/getTimings/:doctorId', getDoctorTimingById);
|
||||
router.get('/featured', getFeaturedDoctors);
|
||||
router.get('/:doctorId', getDoctorByDoctorId);
|
||||
|
||||
router.post('/', jwtAuthMiddleware, createDoctor);
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
createPackage,
|
||||
updatePackage,
|
||||
deletePackage,
|
||||
getFeaturedPackages,
|
||||
|
||||
// Inquiries
|
||||
createPackageInquiry,
|
||||
@@ -26,6 +27,7 @@ router.get('/packages', getAllPackages);
|
||||
router.get('/packages/:slug', getPackageBySlug);
|
||||
router.get('/categories', getAllCategories);
|
||||
router.post('/inquiry', createPackageInquiry);
|
||||
router.get('/featured', getFeaturedPackages);
|
||||
|
||||
router.get('/inquiries', jwtAuthMiddleware, getAllInquiries);
|
||||
router.post('/', jwtAuthMiddleware, createPackage);
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface Doctor {
|
||||
workingStatus?: string;
|
||||
qualification?: string;
|
||||
isActive: boolean;
|
||||
isFeatured: boolean;
|
||||
globalSortOrder: number;
|
||||
|
||||
departments?: {
|
||||
@@ -53,7 +54,7 @@ export const createDoctorApi = async (data: Doctor) => {
|
||||
export const updateDoctorApi = async (
|
||||
doctorId: string,
|
||||
data: Partial<Doctor>,
|
||||
action: 'toggleStatus' | 'updateDetails' = 'updateDetails'
|
||||
action: 'toggleStatus' | 'toggleFeatured' | 'updateDetails' = 'updateDetails'
|
||||
) => {
|
||||
try {
|
||||
const res = await apiClient.patch(`/doctors/${doctorId}/${action}`, data);
|
||||
|
||||
@@ -120,22 +120,40 @@ export default function HealthPackageModal({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between border rounded-xl p-4 bg-muted/30">
|
||||
<div>
|
||||
<p className="font-semibold">Active Visibility</p>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="flex items-center justify-between border rounded-xl p-4 bg-muted/30">
|
||||
<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>
|
||||
|
||||
<Switch
|
||||
checked={pkgForm.isActive}
|
||||
onCheckedChange={(val) =>
|
||||
setPkgForm({
|
||||
...pkgForm,
|
||||
isActive: val,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<div className="flex items-center justify-between border rounded-xl p-4 bg-muted/30">
|
||||
<div>
|
||||
<p className="font-semibold">Featured</p>
|
||||
<p className="text-sm text-muted-foreground">Show on homepage</p>
|
||||
</div>
|
||||
|
||||
<Switch
|
||||
checked={pkgForm.isFeatured || false}
|
||||
onCheckedChange={(val) =>
|
||||
setPkgForm({
|
||||
...pkgForm,
|
||||
isFeatured: val,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
|
||||
@@ -51,6 +51,7 @@ export default function DoctorPage() {
|
||||
workingStatus: '',
|
||||
qualification: '',
|
||||
isActive: true,
|
||||
isFeatured: false,
|
||||
globalSortOrder: 0,
|
||||
departments: [],
|
||||
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) {
|
||||
const exists = form.departments.find((d: any) => d.departmentId === depId);
|
||||
if (exists) {
|
||||
@@ -201,6 +218,7 @@ export default function DoctorPage() {
|
||||
experience: '',
|
||||
professionalSummary: '',
|
||||
isActive: true,
|
||||
isFeatured: false,
|
||||
globalSortOrder: 0,
|
||||
specializations: [
|
||||
{
|
||||
@@ -235,6 +253,7 @@ export default function DoctorPage() {
|
||||
workingStatus: doc.workingStatus,
|
||||
qualification: doc.qualification,
|
||||
isActive: doc.isActive ?? true,
|
||||
isFeatured: doc.isFeatured ?? false,
|
||||
globalSortOrder: doc.globalSortOrder ?? 0,
|
||||
experience: doc.experience || '',
|
||||
professionalSummary: doc.professionalSummary || '',
|
||||
@@ -353,14 +372,15 @@ 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-[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">
|
||||
<TableRow>
|
||||
<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-[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>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -368,13 +388,13 @@ export default function DoctorPage() {
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<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" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : currentItems.length === 0 ? (
|
||||
<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
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -423,6 +443,15 @@ export default function DoctorPage() {
|
||||
</div>
|
||||
</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">
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button size="icon" variant="ghost" className="h-9 w-9" onClick={() => handlePreview(doc)}>
|
||||
@@ -508,6 +537,17 @@ export default function DoctorPage() {
|
||||
/>
|
||||
</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">
|
||||
<Label htmlFor="globalSortOrder" className="text-sm font-semibold">
|
||||
Sort Priority (Lower numbers show first)
|
||||
|
||||
@@ -62,6 +62,7 @@ export default function HealthPackagePage() {
|
||||
discountedPrice: undefined,
|
||||
categoryId: 0,
|
||||
isActive: true,
|
||||
isFeatured: false,
|
||||
sortOrder: 1000,
|
||||
seo: {
|
||||
seoTitle: '',
|
||||
@@ -167,6 +168,7 @@ export default function HealthPackagePage() {
|
||||
discountedPrice: undefined,
|
||||
categoryId: categories[0]?.id || 0,
|
||||
isActive: true,
|
||||
isFeatured: false,
|
||||
sortOrder: 1000,
|
||||
seo: {
|
||||
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">Pricing</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>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -418,6 +421,17 @@ export default function HealthPackagePage() {
|
||||
</Badge>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Switch
|
||||
checked={pkg.isFeatured}
|
||||
onCheckedChange={async () => {
|
||||
await updateHealthPackageApi(pkg.id!, {
|
||||
isFeatured: !pkg.isFeatured,
|
||||
});
|
||||
fetchData();
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
|
||||
Reference in New Issue
Block a user