Compare commits

..

8 Commits

14 changed files with 218 additions and 183 deletions
@@ -1,37 +0,0 @@
/*
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;
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "Doctor" ADD COLUMN "professionalSummary" TEXT;
@@ -1,7 +0,0 @@
-- 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[];
@@ -1,4 +0,0 @@
-- AlterTable
ALTER TABLE "Doctor" ADD COLUMN "ogDescription" TEXT,
ADD COLUMN "ogImage" TEXT,
ADD COLUMN "ogTitle" TEXT;
@@ -1,19 +0,0 @@
-- 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");
@@ -1,30 +0,0 @@
/*
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;
@@ -1,66 +0,0 @@
-- CreateTable
CREATE TABLE "HealthCheckCategory" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"slug" TEXT,
"description" TEXT,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"sortOrder" INTEGER NOT NULL DEFAULT 1000,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "HealthCheckCategory_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "HealthPackage" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"description" TEXT,
"price" DECIMAL(10,2),
"image" TEXT,
"discountedPrice" DECIMAL(10,2),
"inclusions" JSONB NOT NULL DEFAULT '{}',
"isActive" BOOLEAN NOT NULL DEFAULT true,
"isFeatured" BOOLEAN NOT NULL DEFAULT false,
"sortOrder" INTEGER NOT NULL DEFAULT 1000,
"categoryId" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "HealthPackage_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "HealthPackageInquiry" (
"id" SERIAL NOT NULL,
"fullName" TEXT NOT NULL,
"mobileNumber" TEXT NOT NULL,
"email" TEXT,
"age" INTEGER,
"gender" TEXT,
"preferredDate" TIMESTAMP(3),
"message" TEXT,
"packageId" INTEGER NOT NULL,
"status" TEXT NOT NULL DEFAULT 'pending',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "HealthPackageInquiry_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "HealthCheckCategory_name_key" ON "HealthCheckCategory"("name");
-- CreateIndex
CREATE UNIQUE INDEX "HealthCheckCategory_slug_key" ON "HealthCheckCategory"("slug");
-- CreateIndex
CREATE UNIQUE INDEX "HealthPackage_slug_key" ON "HealthPackage"("slug");
-- AddForeignKey
ALTER TABLE "HealthPackage" ADD CONSTRAINT "HealthPackage_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "HealthCheckCategory"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "HealthPackageInquiry" ADD CONSTRAINT "HealthPackageInquiry_packageId_fkey" FOREIGN KEY ("packageId") REFERENCES "HealthPackage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1,50 @@
/*
Warnings:
- 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" ADD COLUMN "professionalSummary" TEXT,
ADD COLUMN "seoId" INTEGER;
-- 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")
);
-- 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");
-- 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;
-- AddForeignKey
ALTER TABLE "DoctorSpecialization" ADD CONSTRAINT "DoctorSpecialization_doctorId_fkey" FOREIGN KEY ("doctorId") REFERENCES "Doctor"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Doctor" ADD COLUMN "experience" INTEGER;
+6 -2
View File
@@ -28,12 +28,13 @@ model Doctor {
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])
departments DoctorDepartment[]
appointments Appointment[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
@@ -223,6 +224,7 @@ model NewsImage {
createdAt DateTime @default(now())
}
model HealthCheckCategory {
id Int @id @default(autoincrement())
name String @unique
@@ -278,6 +280,8 @@ model HealthPackageInquiry {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model DoctorSpecialization {
id Int @id @default(autoincrement())
name String
@@ -240,6 +240,22 @@ export const createDoctor = async (req, res) => {
ogDescription,
ogImage,
} = req.body;
const messages = [];
if (!doctorId) messages.push("Doctor ID is required");
if (!name?.trim()) messages.push("Doctor name is required");
if (!designation?.trim()) messages.push("Designation is required");
if (!qualification?.trim()) messages.push("Qualification is required");
if (!departments || departments.length === 0) {
messages.push("At least one department is required");
}
if (messages.length > 0) {
return res.status(400).json({
success: false,
message: messages.join(", "),
});
}
const seo = await prisma.seo.create({
data: {
seoTitle,
@@ -343,7 +359,22 @@ export const updateDoctor = async (req, res) => {
tags,
specializations,
} = req.body;
const messages = [];
if (!doctorId) messages.push("Doctor ID is required");
if (!name?.trim()) messages.push("Doctor name is required");
if (!qualification?.trim()) messages.push("Qualification is required");
if (!designation?.trim()) messages.push("Designation is required");
if (!departments || departments.length === 0) {
messages.push("At least one department is required");
}
if (messages.length > 0) {
return res.status(400).json({
success: false,
message: messages.join(", "),
});
}
const doctor = await prisma.doctor.findUnique({where: {doctorId}});
if (!doctor)
return res
@@ -365,6 +396,74 @@ export const updateDoctor = async (req, res) => {
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) {
await prisma.seo.update({
where: {
@@ -11,8 +11,7 @@ interface BytescaleUploaderProps {
| "/departments"
| "/news"
| "/blog"
| "/health-packages"
| "/doctor-og";
| "/health-packages";
}
export function BytescaleUploader({
+9 -1
View File
@@ -167,8 +167,16 @@ export default function DoctorPage() {
const currentItems = filteredDoctors.slice(indexOfFirstItem, indexOfLastItem);
function handleChange(e: any) {
const value =
let value =
e.target.type === "number" ? Number(e.target.value) : e.target.value;
if (e.target.name === "slug") {
value = value
.toLowerCase()
.replace(/\s+/g, "-") // replace spaces with -
.replace(/[^\w-]+/g, "") // remove special chars
.replace(/--+/g, "-"); // remove duplicate -
}
setForm({ ...form, [e.target.name]: value });
}
+51 -13
View File
@@ -1,4 +1,5 @@
import { useState, useEffect, useCallback, useMemo } from "react";
import toast from "react-hot-toast";
import { AxiosError } from "axios";
import { BytescaleUploader } from "@/components/BytescaleUploader/BytescaleUploader";
@@ -164,9 +165,11 @@ export default function HealthPackagePage() {
if (!pkg.id) return;
try {
await updateHealthPackageApi(pkg.id, { isActive: !pkg.isActive });
toast.success(`Package ${pkg.isActive ? "hidden" : "activated"}`);
fetchData();
} catch (err) {
console.error("Failed to update status", err);
toast.error("Failed to update status");
}
};
const handleToggleCategoryStatus = async (cat: HealthCategory) => {
@@ -180,13 +183,21 @@ export default function HealthPackagePage() {
}
await updateCategoryApi(cat.id, { isActive: !cat.isActive });
toast.success(`Category ${cat.isActive ? "hidden" : "activated"}`);
fetchData();
} catch (err) {
console.error("Failed to update category status", err);
toast.error("Failed to update category status");
}
};
const openAddPackage = () => {
if (categories.length === 0) {
toast.error(
"Please create at least one category before attempting to add a health package.",
);
return;
}
setEditingPackage(null);
setPkgForm({
name: "",
@@ -254,6 +265,27 @@ export default function HealthPackagePage() {
};
const savePackage = async () => {
if (!pkgForm.image) return toast.error("Package image is required.");
if (!pkgForm.name?.trim()) return toast.error("Package Name is required.");
if (!pkgForm.slug?.trim()) return toast.error("URL Slug is required.");
if (!pkgForm.categoryId)
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())
return toast.error("Description is required.");
const structureFilled = inclusionsList.some(
(item) => item.category.trim() !== "" && item.items.trim() !== "",
);
if (!structureFilled) {
return toast.error(
"Please provide at least one valid Category Group with tests inside it.",
);
}
try {
// Convert the dynamic array back into the required JSON object format
const parsedInclusions: Record<string, string[]> = {};
@@ -289,18 +321,25 @@ export default function HealthPackagePage() {
}
await updateHealthPackageApi(editingPackage.id, changedFields);
toast.success("Package updated successfully!");
} else {
await createHealthPackageApi(finalData);
toast.success("Package created successfully!");
}
setPackageModal(false);
fetchData();
} catch (err) {
console.error(err);
toast.error(
"An unexpected system error occurred while trying to save the package.",
);
}
};
const saveCategory = async () => {
if (!catForm.name?.trim()) return toast.error("Category Name is required.");
try {
if (editingCategory?.id) {
const changedFields: Record<string, any> = {};
@@ -324,21 +363,30 @@ export default function HealthPackagePage() {
editingCategory.id,
changedFields as Partial<HealthCategory>,
);
toast.success("Category updated successfully!");
} else {
await createCategoryApi(catForm as any);
toast.success("Category created successfully!");
}
setCategoryModal(false);
fetchData();
} catch (err) {
console.error(err);
toast.error("An error occurred while saving the category.");
}
};
const deleteCategory = async (id: number) => {
if (confirm("Delete this category? Ensure no packages are linked to it.")) {
await deleteCategoryApi(id);
fetchData();
try {
await deleteCategoryApi(id);
toast.success("Category deleted successfully!");
fetchData();
} catch (err) {
console.error(err);
toast.error("Failed to delete category.");
}
}
};
@@ -689,7 +737,7 @@ export default function HealthPackagePage() {
<div className="space-y-4">
<div className="space-y-2">
<Label className="text-sm font-semibold">
Package Image
Package Image(Dimensions: 650w x 250h)
</Label>
<BytescaleUploader
@@ -949,16 +997,6 @@ export default function HealthPackagePage() {
className="text-base"
/>
</div>
{/* <div className="space-y-1">
<Label className="text-sm font-semibold">URL Slug</Label>
<Input
value={catForm.slug}
onChange={(e) =>
setCatForm({ ...catForm, slug: e.target.value })
}
className="text-base"
/>
</div> */}
<div className="space-y-1">
<Label className="text-sm font-semibold">Sort Order</Label>