From 475039036855343264b3ac838c43cad1deea171a Mon Sep 17 00:00:00 2001 From: rishalkv Date: Tue, 19 May 2026 15:53:31 +0530 Subject: [PATCH] feat:add doc seo and content --- .../migration.sql | 37 ++ .../migration.sql | 2 + .../migration.sql | 7 + .../migration.sql | 4 + .../migration.sql | 19 + .../migration.sql | 30 ++ backend/prisma/schema.prisma | 80 ++-- backend/src/controllers/doctor.controller.js | 147 +++++++ .../BytescaleUploader/BytescaleUploader.tsx | 3 +- frontend/src/pages/Doctor.tsx | 400 ++++++++++++++++++ 10 files changed, 675 insertions(+), 54 deletions(-) create mode 100644 backend/prisma/migrations/20260518070619_add_doctor_specialization/migration.sql create mode 100644 backend/prisma/migrations/20260518093623_add_professional_bio/migration.sql create mode 100644 backend/prisma/migrations/20260518100644_add_doctor_experience/migration.sql create mode 100644 backend/prisma/migrations/20260519042007_add_open_graph_fields_to_doctor/migration.sql create mode 100644 backend/prisma/migrations/20260519062600_add_seo_table_relation/migration.sql create mode 100644 backend/prisma/migrations/20260519064224_add_seo_relation_to_doctor/migration.sql diff --git a/backend/prisma/migrations/20260518070619_add_doctor_specialization/migration.sql b/backend/prisma/migrations/20260518070619_add_doctor_specialization/migration.sql new file mode 100644 index 0000000..8c12154 --- /dev/null +++ b/backend/prisma/migrations/20260518070619_add_doctor_specialization/migration.sql @@ -0,0 +1,37 @@ +/* + Warnings: + + - You are about to drop the `HealthCheckCategory` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `HealthPackage` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `HealthPackageInquiry` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "HealthPackage" DROP CONSTRAINT "HealthPackage_categoryId_fkey"; + +-- DropForeignKey +ALTER TABLE "HealthPackageInquiry" DROP CONSTRAINT "HealthPackageInquiry_packageId_fkey"; + +-- DropTable +DROP TABLE "HealthCheckCategory"; + +-- DropTable +DROP TABLE "HealthPackage"; + +-- DropTable +DROP TABLE "HealthPackageInquiry"; + +-- CreateTable +CREATE TABLE "DoctorSpecialization" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "doctorId" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "DoctorSpecialization_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "DoctorSpecialization" ADD CONSTRAINT "DoctorSpecialization_doctorId_fkey" FOREIGN KEY ("doctorId") REFERENCES "Doctor"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/backend/prisma/migrations/20260518093623_add_professional_bio/migration.sql b/backend/prisma/migrations/20260518093623_add_professional_bio/migration.sql new file mode 100644 index 0000000..733006b --- /dev/null +++ b/backend/prisma/migrations/20260518093623_add_professional_bio/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Doctor" ADD COLUMN "professionalSummary" TEXT; diff --git a/backend/prisma/migrations/20260518100644_add_doctor_experience/migration.sql b/backend/prisma/migrations/20260518100644_add_doctor_experience/migration.sql new file mode 100644 index 0000000..0f8f004 --- /dev/null +++ b/backend/prisma/migrations/20260518100644_add_doctor_experience/migration.sql @@ -0,0 +1,7 @@ +-- AlterTable +ALTER TABLE "Doctor" ADD COLUMN "experience" INTEGER, +ADD COLUMN "focusKeyphrase" TEXT, +ADD COLUMN "metaDescription" TEXT, +ADD COLUMN "seoTitle" TEXT, +ADD COLUMN "slug" TEXT, +ADD COLUMN "tags" TEXT[]; diff --git a/backend/prisma/migrations/20260519042007_add_open_graph_fields_to_doctor/migration.sql b/backend/prisma/migrations/20260519042007_add_open_graph_fields_to_doctor/migration.sql new file mode 100644 index 0000000..def77dc --- /dev/null +++ b/backend/prisma/migrations/20260519042007_add_open_graph_fields_to_doctor/migration.sql @@ -0,0 +1,4 @@ +-- AlterTable +ALTER TABLE "Doctor" ADD COLUMN "ogDescription" TEXT, +ADD COLUMN "ogImage" TEXT, +ADD COLUMN "ogTitle" TEXT; diff --git a/backend/prisma/migrations/20260519062600_add_seo_table_relation/migration.sql b/backend/prisma/migrations/20260519062600_add_seo_table_relation/migration.sql new file mode 100644 index 0000000..103ca37 --- /dev/null +++ b/backend/prisma/migrations/20260519062600_add_seo_table_relation/migration.sql @@ -0,0 +1,19 @@ +-- CreateTable +CREATE TABLE "Seo" ( + "id" SERIAL NOT NULL, + "seoTitle" TEXT, + "metaDescription" TEXT, + "focusKeyphrase" TEXT, + "slug" TEXT, + "tags" TEXT[], + "ogTitle" TEXT, + "ogDescription" TEXT, + "ogImage" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Seo_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Seo_slug_key" ON "Seo"("slug"); diff --git a/backend/prisma/migrations/20260519064224_add_seo_relation_to_doctor/migration.sql b/backend/prisma/migrations/20260519064224_add_seo_relation_to_doctor/migration.sql new file mode 100644 index 0000000..1e5fdbc --- /dev/null +++ b/backend/prisma/migrations/20260519064224_add_seo_relation_to_doctor/migration.sql @@ -0,0 +1,30 @@ +/* + Warnings: + + - You are about to drop the column `focusKeyphrase` on the `Doctor` table. All the data in the column will be lost. + - You are about to drop the column `metaDescription` on the `Doctor` table. All the data in the column will be lost. + - You are about to drop the column `ogDescription` on the `Doctor` table. All the data in the column will be lost. + - You are about to drop the column `ogImage` on the `Doctor` table. All the data in the column will be lost. + - You are about to drop the column `ogTitle` on the `Doctor` table. All the data in the column will be lost. + - You are about to drop the column `seoTitle` on the `Doctor` table. All the data in the column will be lost. + - You are about to drop the column `slug` on the `Doctor` table. All the data in the column will be lost. + - You are about to drop the column `tags` on the `Doctor` table. All the data in the column will be lost. + - A unique constraint covering the columns `[seoId]` on the table `Doctor` will be added. If there are existing duplicate values, this will fail. + +*/ +-- AlterTable +ALTER TABLE "Doctor" DROP COLUMN "focusKeyphrase", +DROP COLUMN "metaDescription", +DROP COLUMN "ogDescription", +DROP COLUMN "ogImage", +DROP COLUMN "ogTitle", +DROP COLUMN "seoTitle", +DROP COLUMN "slug", +DROP COLUMN "tags", +ADD COLUMN "seoId" INTEGER; + +-- CreateIndex +CREATE UNIQUE INDEX "Doctor_seoId_key" ON "Doctor"("seoId"); + +-- AddForeignKey +ALTER TABLE "Doctor" ADD CONSTRAINT "Doctor_seoId_fkey" FOREIGN KEY ("seoId") REFERENCES "Seo"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index f31c375..d212f41 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -23,14 +23,17 @@ model Doctor { name String image String? designation String? + experience Int? workingStatus String? qualification String? isActive Boolean @default(true) globalSortOrder Int @default(1000) - departments DoctorDepartment[] appointments Appointment[] - + specializations DoctorSpecialization[] + professionalSummary String? @db.Text + seoId Int? @unique + seo Seo? @relation(fields: [seoId], references: [id]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } @@ -220,59 +223,30 @@ model NewsImage { createdAt DateTime @default(now()) } - -model HealthCheckCategory { - id Int @id @default(autoincrement()) - name String @unique - slug String? @unique - description String? - isActive Boolean @default(true) - sortOrder Int @default(1000) - - packages HealthPackage[] - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt +model DoctorSpecialization { + id Int @id @default(autoincrement()) + name String + description String? @db.Text + doctorId Int + doctor Doctor @relation(fields: [doctorId],references: [id],onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } -model HealthPackage { - id Int @id @default(autoincrement()) - name String - slug String @unique - description String? - price Decimal? @db.Decimal(10, 2) - image String? - discountedPrice Decimal? @db.Decimal(10, 2) - - inclusions Json @default("{}") - - isActive Boolean @default(true) - isFeatured Boolean @default(false) - sortOrder Int @default(1000) +model Seo { + id Int @id @default(autoincrement()) + doctor Doctor? - categoryId Int - category HealthCheckCategory @relation(fields: [categoryId], references: [id]) - - inquiries HealthPackageInquiry[] + seoTitle String? + metaDescription String? @db.Text + focusKeyphrase String? + slug String? @unique + tags String[] - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt -} + ogTitle String? + ogDescription String? @db.Text + ogImage String? -model HealthPackageInquiry { - id Int @id @default(autoincrement()) - fullName String - mobileNumber String - email String? - age Int? - gender String? - preferredDate DateTime? - message String? - - packageId Int - healthPackage HealthPackage @relation(fields: [packageId], references: [id]) - - status String @default("pending") - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt -} + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} \ No newline at end of file diff --git a/backend/src/controllers/doctor.controller.js b/backend/src/controllers/doctor.controller.js index 750d3d7..da676da 100644 --- a/backend/src/controllers/doctor.controller.js +++ b/backend/src/controllers/doctor.controller.js @@ -9,12 +9,18 @@ export const getAllDoctors = async (req, res) => { const doctors = await prisma.doctor.findMany({ where: admin === "true" ? {} : {isActive: true}, include: { + seo: true, departments: { include: { department: true, timing: true, }, }, + specializations: { + orderBy: { + createdAt: "asc", + }, + }, }, orderBy: [{globalSortOrder: "asc"}, {name: "asc"}], }); @@ -28,7 +34,25 @@ export const getAllDoctors = async (req, res) => { workingStatus: doc.workingStatus, qualification: doc.qualification, isActive: doc.isActive, + experience: doc.experience, + professionalSummary: doc.professionalSummary, globalSortOrder: doc.globalSortOrder, + specializations: doc.specializations.map((item) => ({ + id: item.id, + name: item.name, + description: item.description, + })), + seo: { + seoTitle: doc.seo?.seoTitle ?? "", + metaDescription: doc.seo?.metaDescription ?? "", + focusKeyphrase: doc.seo?.focusKeyphrase ?? "", + slug: doc.seo?.slug ?? "", + tags: doc.seo?.tags ?? [], + + ogTitle: doc.seo?.ogTitle ?? "", + ogDescription: doc.seo?.ogDescription ?? "", + ogImage: doc.seo?.ogImage ?? "", + }, departments: doc.departments.map((d) => { const t = d.timing || {}; const timingArray = [ @@ -73,6 +97,8 @@ export const getDoctorByDoctorId = async (req, res) => { const doctor = await prisma.doctor.findUnique({ where: {doctorId}, include: { + seo: true, + specializations: true, departments: { include: { department: true, @@ -96,6 +122,24 @@ export const getDoctorByDoctorId = async (req, res) => { designation: doctor.designation, workingStatus: doctor.workingStatus, qualification: doctor.qualification, + experience: doctor.experience, + professionalSummary: doctor.professionalSummary, + seo: { + seoTitle: doctor.seo?.seoTitle ?? "", + metaDescription: doctor.seo?.metaDescription ?? "", + focusKeyphrase: doctor.seo?.focusKeyphrase ?? "", + slug: doctor.seo?.slug ?? "", + tags: doctor.seo?.tags ?? [], + ogTitle: doctor.seo?.ogTitle ?? "", + ogDescription: doctor.seo?.ogDescription ?? "", + ogImage: doctor.seo?.ogImage ?? "", + }, + specializations: + doctor.specializations?.map((item) => ({ + id: item.id, + name: item.name, + description: item.description, + })) ?? [], departments: doctor.departments.map((d) => ({ departmentId: d.department.departmentId, departmentName: d.department.name, @@ -184,7 +228,32 @@ export const createDoctor = async (req, res) => { isActive, globalSortOrder, departments, + experience, + professionalSummary, + seoTitle, + metaDescription, + focusKeyphrase, + slug, + tags, + specializations, + ogTitle, + ogDescription, + ogImage, } = req.body; + const seo = await prisma.seo.create({ + data: { + seoTitle, + metaDescription, + focusKeyphrase, + slug: slug ? slug : null, + tags: tags || [], + + // Open Graph + ogTitle, + ogDescription, + ogImage, + }, + }); const doctor = await prisma.doctor.create({ data: { @@ -194,6 +263,9 @@ export const createDoctor = async (req, res) => { designation, workingStatus, qualification, + experience: experience ? Number(experience) : null, + professionalSummary, + seoId: seo.id, isActive: isActive !== undefined ? isActive : true, globalSortOrder: globalSortOrder !== undefined ? Number(globalSortOrder) : 0, @@ -224,6 +296,17 @@ export const createDoctor = async (req, res) => { }); } } + if (specializations?.length) { + await prisma.doctorSpecialization.createMany({ + data: specializations + .filter((item) => item.name?.trim()) + .map((item) => ({ + name: item.name.trim(), + description: item.description?.trim() || null, + doctorId: doctor.id, + })), + }); + } res.status(201).json({ success: true, @@ -251,6 +334,14 @@ export const updateDoctor = async (req, res) => { isActive, globalSortOrder, departments, + experience, + professionalSummary, + seoTitle, + metaDescription, + focusKeyphrase, + slug, + tags, + specializations, } = req.body; const doctor = await prisma.doctor.findUnique({where: {doctorId}}); @@ -268,15 +359,71 @@ export const updateDoctor = async (req, res) => { workingStatus, qualification, isActive, + experience: experience ? Number(experience) : null, + professionalSummary, globalSortOrder: globalSortOrder !== undefined ? Number(globalSortOrder) : undefined, }, }); + if (doctor.seoId) { + await prisma.seo.update({ + where: { + id: doctor.seoId, + }, + data: { + seoTitle, + metaDescription, + focusKeyphrase, + slug: slug ? slug : null, + tags: tags || [], + }, + }); + } else { + const seo = await prisma.seo.create({ + data: { + seoTitle, + metaDescription, + focusKeyphrase, + slug: slug ? slug : null, + tags: tags || [], + }, + }); + + await prisma.doctor.update({ + where: { + id: doctor.id, + }, + data: { + seoId: seo.id, + }, + }); + } const hasTimingData = departments?.some( (dep) => dep.timing && Object.keys(dep.timing).length > 0, ); + // Update Specializations + if (Array.isArray(specializations)) { + await prisma.doctorSpecialization.deleteMany({ + where: { + doctorId: doctor.id, + }, + }); + + if (specializations.length) { + await prisma.doctorSpecialization.createMany({ + data: specializations + .filter((item) => item.name?.trim()) + .map((item) => ({ + name: item.name.trim(), + description: item.description?.trim() || null, + doctorId: doctor.id, + })), + }); + } + } + if (departments && Array.isArray(departments) && hasTimingData) { const oldRelations = await prisma.doctorDepartment.findMany({ where: {doctorId: doctor.id}, diff --git a/frontend/src/components/BytescaleUploader/BytescaleUploader.tsx b/frontend/src/components/BytescaleUploader/BytescaleUploader.tsx index cc412f3..04b3f46 100644 --- a/frontend/src/components/BytescaleUploader/BytescaleUploader.tsx +++ b/frontend/src/components/BytescaleUploader/BytescaleUploader.tsx @@ -11,7 +11,8 @@ interface BytescaleUploaderProps { | "/departments" | "/news" | "/blog" - | "/health-packages"; + | "/health-packages" + | "/doctor-og"; } export function BytescaleUploader({ diff --git a/frontend/src/pages/Doctor.tsx b/frontend/src/pages/Doctor.tsx index 74621c7..3c6c985 100644 --- a/frontend/src/pages/Doctor.tsx +++ b/frontend/src/pages/Doctor.tsx @@ -41,6 +41,7 @@ import { ChevronLeft, ChevronRight, } from "lucide-react"; +import { Textarea } from "@/components/ui/textarea"; interface Department { departmentId: string; @@ -83,6 +84,21 @@ export default function DoctorPage() { isActive: true, globalSortOrder: 0, departments: [], + professionalSummary: "", + seoTitle: "", + metaDescription: "", + ogTitle: "", + ogDescription: "", + ogImage: "", + specializations: [ + { + name: "", + description: "", + }, + ], + focusKeyphrase: "", + slug: "", + tags: [], }); const fetchAll = useCallback(async () => { @@ -221,9 +237,25 @@ export default function DoctorPage() { designation: "", workingStatus: "", qualification: "", + experience: "", + professionalSummary: "", isActive: true, globalSortOrder: 0, + specializations: [ + { + name: "", + description: "", + }, + ], departments: [], + seoTitle: "", + metaDescription: "", + focusKeyphrase: "", + slug: "", + tags: [], + ogTitle: "", + ogDescription: "", + ogImage: "", }); setOpenModal(true); } @@ -243,6 +275,27 @@ export default function DoctorPage() { qualification: doc.qualification, isActive: doc.isActive ?? true, globalSortOrder: doc.globalSortOrder ?? 0, + experience: doc.experience || "", + professionalSummary: doc.professionalSummary || "", + seoTitle: doc.seo?.seoTitle || "", + metaDescription: doc.seo?.metaDescription || "", + focusKeyphrase: doc.seo?.focusKeyphrase || "", + slug: doc.seo?.slug || "", + tags: doc.seo?.tags || [], + ogTitle: doc.seo?.ogTitle || "", + ogDescription: doc.seo?.ogDescription || "", + ogImage: doc.seo?.ogImage || "", + specializations: doc.specializations?.length + ? doc.specializations.map((item: any) => ({ + name: item.name || "", + description: item.description || "", + })) + : [ + { + name: "", + description: "", + }, + ], departments: timingData.map((d: any) => ({ departmentId: d.departmentId, sortOrder: d.deptSortOrder ?? 0, @@ -255,6 +308,8 @@ export default function DoctorPage() { } } + console.log("Current form state:", form); // Debug log to check form state + async function handleSubmit() { try { if (editing) { @@ -594,6 +649,24 @@ export default function DoctorPage() { className="text-base" /> +
+ + +