Compare commits

..

8 Commits

7 changed files with 287 additions and 128 deletions
+92 -115
View File
@@ -340,7 +340,7 @@ export const createDoctor = async (req, res) => {
//update doctors //update doctors
export const updateDoctor = async (req, res) => { export const updateDoctor = async (req, res) => {
try { try {
const {doctorId} = req.params; const {doctorId, action} = req.params;
const { const {
name, name,
designation, designation,
@@ -357,8 +357,37 @@ export const updateDoctor = async (req, res) => {
focusKeyphrase, focusKeyphrase,
slug, slug,
tags, tags,
ogImage,
specializations, specializations,
} = req.body; } = req.body;
if (!doctorId) {
return res.status(400).json({
success: false,
message: "Doctor ID is required",
});
}
const doctor = await prisma.doctor.findUnique({where: {doctorId}});
if (!doctor)
return res
.status(404)
.json({success: false, message: "Doctor not found"});
if (action === "toggleStatus") {
await prisma.doctor.update({
where: {id: doctor.id},
data: {
isActive: !doctor.isActive,
},
});
return res.status(200).json({
success: true,
message: `Doctor has been ${
doctor.isActive ? "deactivated" : "activated"
} successfully`,
});
}
const messages = []; const messages = [];
if (!doctorId) messages.push("Doctor ID is required"); if (!doctorId) messages.push("Doctor ID is required");
if (!name?.trim()) messages.push("Doctor name is required"); if (!name?.trim()) messages.push("Doctor name is required");
@@ -375,11 +404,6 @@ export const updateDoctor = async (req, res) => {
message: messages.join(", "), message: messages.join(", "),
}); });
} }
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({ await prisma.doctor.update({
where: {id: doctor.id}, where: {id: doctor.id},
@@ -396,74 +420,7 @@ export const updateDoctor = async (req, res) => {
globalSortOrder !== undefined ? Number(globalSortOrder) : undefined, globalSortOrder !== undefined ? Number(globalSortOrder) : undefined,
}, },
}); });
const existingDepartments = await prisma.doctorDepartment.findMany({
where: {
doctorId: doctor.id,
},
include: {
timing: true,
},
});
for (const dep of departments) {
const department = await prisma.department.findUnique({
where: {departmentId: dep.departmentId},
});
if (!department) continue;
const existing = existingDepartments.find(
(d) => d.departmentId === department.id,
);
const newSortOrder =
dep.sortOrder !== undefined ? Number(dep.sortOrder) : 0;
const isSameDepartment = existing && existing.sortOrder === newSortOrder;
const isSameTiming =
JSON.stringify(existing?.timing || {}) ===
JSON.stringify(dep.timing || {});
if (isSameDepartment && isSameTiming) {
continue;
}
let doctorDepartment = existing;
if (!existing) {
doctorDepartment = await prisma.doctorDepartment.create({
data: {
doctorId: doctor.id,
departmentId: department.id,
sortOrder: newSortOrder,
},
});
} else if (existing.sortOrder !== newSortOrder) {
doctorDepartment = await prisma.doctorDepartment.update({
where: {id: existing.id},
data: {
sortOrder: newSortOrder,
},
});
}
if (dep.timing) {
if (existing?.timing) {
await prisma.doctorTiming.update({
where: {id: existing.timing.id},
data: dep.timing,
});
} else {
await prisma.doctorTiming.create({
data: {
doctorDepartmentId: doctorDepartment.id,
...dep.timing,
},
});
}
}
}
if (doctor.seoId) { if (doctor.seoId) {
await prisma.seo.update({ await prisma.seo.update({
where: { where: {
@@ -472,6 +429,7 @@ export const updateDoctor = async (req, res) => {
data: { data: {
seoTitle, seoTitle,
metaDescription, metaDescription,
ogImage,
focusKeyphrase, focusKeyphrase,
slug: slug ? slug : null, slug: slug ? slug : null,
tags: tags || [], tags: tags || [],
@@ -480,6 +438,7 @@ export const updateDoctor = async (req, res) => {
} else { } else {
const seo = await prisma.seo.create({ const seo = await prisma.seo.create({
data: { data: {
ogImage,
seoTitle, seoTitle,
metaDescription, metaDescription,
focusKeyphrase, focusKeyphrase,
@@ -498,9 +457,66 @@ export const updateDoctor = async (req, res) => {
}); });
} }
const hasTimingData = departments?.some( // Update Departments & Timings
(dep) => dep.timing && Object.keys(dep.timing).length > 0, if (Array.isArray(departments)) {
); const oldRelations = await prisma.doctorDepartment.findMany({
where: {
doctorId: doctor.id,
},
include: {
timing: true,
},
});
// Delete old timings
for (const rel of oldRelations) {
if (rel.timing) {
await prisma.doctorTiming.deleteMany({
where: {
doctorDepartmentId: rel.id,
},
});
}
}
// Delete old departments
await prisma.doctorDepartment.deleteMany({
where: {
doctorId: doctor.id,
},
});
// Recreate departments + timings
for (const dep of departments) {
const department = await prisma.department.findUnique({
where: {
departmentId: dep.departmentId,
},
});
if (!department) continue;
const doctorDepartment = await prisma.doctorDepartment.create({
data: {
doctorId: doctor.id,
departmentId: department.id,
sortOrder: dep.sortOrder !== undefined ? Number(dep.sortOrder) : 0,
},
});
if (dep.timing && Object.keys(dep.timing).length > 0) {
const {id, doctorDepartmentId, createdAt, updatedAt, ...cleanTiming} =
dep.timing;
await prisma.doctorTiming.create({
data: {
doctorDepartmentId: doctorDepartment.id,
...cleanTiming,
},
});
}
}
}
// Update Specializations // Update Specializations
if (Array.isArray(specializations)) { if (Array.isArray(specializations)) {
@@ -523,45 +539,6 @@ export const updateDoctor = async (req, res) => {
} }
} }
if (departments && Array.isArray(departments) && hasTimingData) {
const oldRelations = await prisma.doctorDepartment.findMany({
where: {doctorId: doctor.id},
});
for (const rel of oldRelations) {
await prisma.doctorTiming.deleteMany({
where: {doctorDepartmentId: rel.id},
});
}
await prisma.doctorDepartment.deleteMany({
where: {doctorId: doctor.id},
});
for (const dep of departments) {
const targetDept = await prisma.department.findUnique({
where: {departmentId: dep.departmentId},
});
if (!targetDept) continue;
const newDD = await prisma.doctorDepartment.create({
data: {
doctorId: doctor.id,
departmentId: targetDept.id,
sortOrder: dep.sortOrder !== undefined ? Number(dep.sortOrder) : 0,
},
});
if (dep.timing) {
const {id, doctorDepartmentId, createdAt, updatedAt, ...cleanTiming} =
dep.timing;
await prisma.doctorTiming.create({
data: {doctorDepartmentId: newDD.id, ...cleanTiming},
});
}
}
}
res res
.status(200) .status(200)
.json({success: true, message: "Doctor updated successfully"}); .json({success: true, message: "Doctor updated successfully"});
+1 -1
View File
@@ -21,7 +21,7 @@ router.get("/getTimings/:doctorId", getDoctorTimingById);
router.get("/:doctorId", getDoctorByDoctorId); router.get("/:doctorId", getDoctorByDoctorId);
router.post("/", jwtAuthMiddleware, createDoctor); router.post("/", jwtAuthMiddleware, createDoctor);
router.patch("/:doctorId", jwtAuthMiddleware, updateDoctor); router.patch("/:doctorId/:action", jwtAuthMiddleware, updateDoctor);
router.delete("/:doctorId", jwtAuthMiddleware, deleteDoctor); router.delete("/:doctorId", jwtAuthMiddleware, deleteDoctor);
export default router; export default router;
+2 -1
View File
@@ -53,9 +53,10 @@ 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",
) => { ) => {
try { try {
const res = await apiClient.patch(`/doctors/${doctorId}`, data); const res = await apiClient.patch(`/doctors/${doctorId}/${action}`, data);
toast.success("Doctor updated successfully"); toast.success("Doctor updated successfully");
+1 -1
View File
@@ -6,7 +6,7 @@ export interface HealthPackage {
name: string; name: string;
slug: string; slug: string;
description?: string; description?: string;
price: number; price?: number;
image?: string; image?: string;
discountedPrice?: number; discountedPrice?: number;
inclusions: Record<string, string[]>; inclusions: Record<string, string[]>;
@@ -0,0 +1,131 @@
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
interface SeoPreviewData {
seo?: {
ogImage?: string;
ogTitle?: string;
seoTitle?: string;
ogDescription?: string;
metaDescription?: string;
slug?: string;
};
doctorId?: string;
name?: string;
}
interface SeoPreviewProps {
open: boolean;
onOpenChange: (open: boolean) => void;
previewData?: SeoPreviewData | null;
url?: string;
title?: string;
}
export default function SeoPreview({
open,
onOpenChange,
previewData,
url,
title = "SEO Preview",
}: SeoPreviewProps) {
const previewUrl = url || "#";
const imageUrl =
previewData?.seo?.ogImage || "https://placehold.co/1200x630?text=GG+Hospital";
const ogTitle =
previewData?.seo?.ogTitle || previewData?.seo?.seoTitle || "GG Hospital";
const ogDescription =
previewData?.seo?.ogDescription || previewData?.seo?.metaDescription ||
"No description available";
const searchTitle =
previewData?.seo?.seoTitle || previewData?.seo?.ogTitle || "SEO title preview";
const searchDescription =
previewData?.seo?.metaDescription || previewData?.seo?.ogDescription ||
"No meta description available";
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:!max-w-4xl overflow-hidden">
<DialogHeader>
<DialogTitle className="text-xl">{title}</DialogTitle>
</DialogHeader>
{previewData ? (
<div className="space-y-10 py-2">
<div>
<p className="mb-4 text-sm font-semibold text-muted-foreground">
Social Media Preview (WhatsApp / Facebook)
</p>
<a
href={previewUrl}
target="_blank"
rel="noopener noreferrer"
className="block max-w-[560px] overflow-hidden rounded-xl border bg-white shadow-sm transition hover:shadow-md"
>
<div className="aspect-[1.91/1] overflow-hidden bg-muted">
<img
src={imageUrl}
alt="OG Preview"
className="h-full w-full object-cover"
/>
</div>
<div className="border-t bg-[#f0f2f5] px-4 py-3">
<p className="truncate text-[11px] uppercase tracking-wide text-[#65676b]">
gg-hospital.com
</p>
<h3 className="mt-1 line-clamp-2 text-[18px] font-semibold leading-snug text-[#1c1e21]">
{ogTitle}
</h3>
<p className="mt-1 line-clamp-2 text-[14px] text-[#65676b]">
{ogDescription}
</p>
</div>
</a>
</div>
<div>
<p className="mb-4 text-sm font-semibold text-muted-foreground">
Google Search Preview
</p>
<div className="rounded-xl border bg-white p-6">
<a
href={previewUrl}
target="_blank"
rel="noopener noreferrer"
className="block"
>
<p className="truncate text-[14px] text-[#202124] hover:underline">
{previewUrl}
</p>
<h3 className="mt-1 text-[22px] leading-tight text-[#1a0dab] hover:underline">
{searchTitle}
</h3>
</a>
<p className="mt-2 line-clamp-3 text-[14px] leading-6 text-[#4d5156]">
{searchDescription}
</p>
</div>
</div>
</div>
) : (
<div className="p-6 text-sm text-muted-foreground">
No preview data available.
</div>
)}
<DialogFooter className="p-6 border-t bg-background z-10 mt-0">
<Button variant="outline" onClick={() => onOpenChange(false)}>
Close
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
+43 -3
View File
@@ -1,6 +1,6 @@
import { useState, useEffect, useCallback } from "react"; import { useState, useEffect, useCallback } from "react";
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import { Eye } from "lucide-react";
import { BytescaleUploader } from "@/components/BytescaleUploader/BytescaleUploader"; import { BytescaleUploader } from "@/components/BytescaleUploader/BytescaleUploader";
import { import {
@@ -28,6 +28,7 @@ import {
DialogTitle, DialogTitle,
DialogFooter, DialogFooter,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import SeoPreview from "@/components/SeoPreview/SeoPreview";
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 { Switch } from "@/components/ui/switch";
@@ -60,6 +61,8 @@ const DAYS = [
]; ];
export default function DoctorPage() { export default function DoctorPage() {
const WEBSITE_URL = import.meta.env.VITE_WEBSITE_URL;
const [doctors, setDoctors] = useState<any[]>([]); const [doctors, setDoctors] = useState<any[]>([]);
const [departments, setDepartments] = useState<Department[]>([]); const [departments, setDepartments] = useState<Department[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -100,6 +103,8 @@ export default function DoctorPage() {
slug: "", slug: "",
tags: [], tags: [],
}); });
const [openOgPreview, setOpenOgPreview] = useState(false);
const [previewDoctor, setPreviewDoctor] = useState<any>(null);
const fetchAll = useCallback(async () => { const fetchAll = useCallback(async () => {
setLoading(true); setLoading(true);
@@ -188,7 +193,7 @@ export default function DoctorPage() {
isActive: newStatus, isActive: newStatus,
}; };
await updateDoctorApi(doc.doctorId, payload); await updateDoctorApi(doc.doctorId, payload, "toggleStatus");
fetchAll(); fetchAll();
} catch (err) { } catch (err) {
@@ -316,7 +321,10 @@ export default function DoctorPage() {
} }
} }
console.log("Current form state:", form); // Debug log to check form state function handlePreview(doc: any) {
setPreviewDoctor(doc);
setOpenOgPreview(true);
}
async function handleSubmit() { async function handleSubmit() {
try { try {
@@ -332,6 +340,24 @@ export default function DoctorPage() {
} }
} }
const createSlug = (text: string) => {
if (!text) return "";
return text
.toString()
.toLowerCase()
.trim()
.replace(/\s+/g, "-")
.replace(/[^\w-]+/g, "")
.replace(/--+/g, "-");
};
const getDoctorUrl = (doctor: any) => {
const slug = doctor?.seo?.slug || createSlug(doctor?.name);
return `${WEBSITE_URL}/${doctor?.doctorId}/${slug}`;
};
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">
@@ -492,6 +518,14 @@ export default function DoctorPage() {
<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)}
>
<Eye className="h-4 w-4" />
</Button>
<Button <Button
size="icon" size="icon"
variant="ghost" variant="ghost"
@@ -1126,6 +1160,12 @@ export default function DoctorPage() {
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
<SeoPreview
open={openOgPreview}
onOpenChange={setOpenOgPreview}
previewData={previewDoctor}
url={getDoctorUrl(previewDoctor)}
/>
</div> </div>
); );
} }
+17 -7
View File
@@ -270,10 +270,6 @@ export default function HealthPackagePage() {
if (!pkgForm.slug?.trim()) return toast.error("URL Slug is required."); if (!pkgForm.slug?.trim()) return toast.error("URL Slug is required.");
if (!pkgForm.categoryId) if (!pkgForm.categoryId)
return toast.error("Please select a valid category."); return toast.error("Please select a valid category.");
if (pkgForm.price === undefined || pkgForm.price <= 0)
return toast.error(
"Regular Price must be a valid amount greater than 0.",
);
if (!pkgForm.description?.trim()) if (!pkgForm.description?.trim())
return toast.error("Description is required."); return toast.error("Description is required.");
@@ -299,8 +295,18 @@ export default function HealthPackagePage() {
} }
}); });
const finalData = { ...pkgForm, inclusions: parsedInclusions }; const finalData: Partial<HealthPackage> = {
...pkgForm,
inclusions: parsedInclusions,
};
if (!finalData.price) {
delete finalData.price;
}
if (!finalData.discountedPrice) {
delete finalData.discountedPrice;
}
if (editingPackage?.id) { if (editingPackage?.id) {
const changedFields: Record<string, any> = {}; const changedFields: Record<string, any> = {};
Object.keys(finalData).forEach((key) => { Object.keys(finalData).forEach((key) => {
@@ -836,7 +842,9 @@ export default function HealthPackagePage() {
onChange={(e) => onChange={(e) =>
setPkgForm({ setPkgForm({
...pkgForm, ...pkgForm,
price: Number(e.target.value), price: e.target.value
? Number(e.target.value)
: undefined,
}) })
} }
className="text-base" className="text-base"
@@ -852,7 +860,9 @@ export default function HealthPackagePage() {
onChange={(e) => onChange={(e) =>
setPkgForm({ setPkgForm({
...pkgForm, ...pkgForm,
discountedPrice: Number(e.target.value), discountedPrice: e.target.value
? Number(e.target.value)
: undefined,
}) })
} }
className="text-base" className="text-base"