feat: facility and google review crud

This commit is contained in:
Kailasdevdas
2026-06-25 16:59:11 +05:30
parent a88d2e3d8c
commit 99601f9f0d
17 changed files with 2293 additions and 2 deletions
@@ -0,0 +1,49 @@
-- CreateTable
CREATE TABLE "Facility" (
"id" SERIAL NOT NULL,
"facilityId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"shortDescription" TEXT,
"description" TEXT,
"videoUrl" TEXT,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"isFeatured" BOOLEAN NOT NULL DEFAULT false,
"sortOrder" INTEGER NOT NULL DEFAULT 1000,
"departmentId" INTEGER,
"seoId" INTEGER,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Facility_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "FacilityImage" (
"id" SERIAL NOT NULL,
"url" TEXT NOT NULL,
"altText" TEXT,
"description" TEXT,
"facilityId" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "FacilityImage_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Facility_facilityId_key" ON "Facility"("facilityId");
-- CreateIndex
CREATE UNIQUE INDEX "Facility_slug_key" ON "Facility"("slug");
-- CreateIndex
CREATE UNIQUE INDEX "Facility_seoId_key" ON "Facility"("seoId");
-- AddForeignKey
ALTER TABLE "Facility" ADD CONSTRAINT "Facility_departmentId_fkey" FOREIGN KEY ("departmentId") REFERENCES "Department"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Facility" ADD CONSTRAINT "Facility_seoId_fkey" FOREIGN KEY ("seoId") REFERENCES "Seo"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "FacilityImage" ADD CONSTRAINT "FacilityImage_facilityId_fkey" FOREIGN KEY ("facilityId") REFERENCES "Facility"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,17 @@
-- CreateTable
CREATE TABLE "GoogleReview" (
"id" SERIAL NOT NULL,
"reviewerName" TEXT NOT NULL,
"reviewerImage" TEXT,
"rating" INTEGER NOT NULL,
"review" TEXT NOT NULL,
"reviewDate" TIMESTAMP(3),
"googleReviewUrl" TEXT,
"isFeatured" BOOLEAN NOT NULL DEFAULT false,
"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 "GoogleReview_pkey" PRIMARY KEY ("id")
);
+59 -1
View File
@@ -46,12 +46,12 @@ model Department {
name String
image String?
para1 String?
para2 String?
para3 String?
facilities String?
services String?
facilitiesList Facility[]
isActive Boolean @default(true)
sortOrder Int @default(1000)
@@ -300,6 +300,7 @@ model Seo {
id Int @id @default(autoincrement())
doctor Doctor?
healthPackage HealthPackage?
facility Facility?
seoTitle String?
@@ -378,4 +379,61 @@ model Accreditation {
enum AccreditationType {
ACCREDITATION
CERTIFICATION
}
model Facility {
id Int @id @default(autoincrement())
facilityId String @unique
name String
slug String @unique
shortDescription String? @db.Text
description String? @db.Text
videoUrl String?
isActive Boolean @default(true)
isFeatured Boolean @default(false)
sortOrder Int @default(1000)
images FacilityImage[]
departmentId Int?
department Department? @relation(fields: [departmentId], references: [id], onDelete: SetNull)
seoId Int? @unique
seo Seo? @relation(fields: [seoId], references: [id], onDelete: SetNull)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model FacilityImage {
id Int @id @default(autoincrement())
url String
altText String?
description String?
facilityId Int
facility Facility @relation(fields: [facilityId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
}
model GoogleReview {
id Int @id @default(autoincrement())
reviewerName String
reviewerImage String?
rating Int
review String @db.Text
reviewDate DateTime?
googleReviewUrl String?
isFeatured Boolean @default(false)
isActive Boolean @default(true)
sortOrder Int @default(1000)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
+4
View File
@@ -19,6 +19,8 @@ import healthCheckRoutes from './routes/healthCheck.route.js';
import homepageBannerRoutes from './routes/homepageBanner.routes.js';
import insurancePartnerRoutes from './routes/insurancePartner.routes.js';
import accreditationRoutes from './routes/accreditation.routes.js';
import facilityRoutes from './routes/facility.routes.js';
import gReviewRoutes from './routes/googleReview.routes.js';
dotenv.config();
@@ -63,6 +65,8 @@ app.use('/api/health-check', healthCheckRoutes);
app.use('/api/homepage-banners', homepageBannerRoutes);
app.use('/api/insurance-partners', insurancePartnerRoutes);
app.use('/api/accreditation', accreditationRoutes);
app.use('/api/facilities', facilityRoutes);
app.use('/api/google-reviews', gReviewRoutes);
const PORT = process.env.PORT || 5008;
app.listen(PORT, () => {
@@ -0,0 +1,421 @@
import prisma from '../prisma/client.js';
export const getAllFacilities = async (req, res) => {
try {
const { admin } = req.query;
const facilities = await prisma.facility.findMany({
where: admin === 'true' ? {} : { isActive: true },
include: {
seo: true,
department: true,
images: true,
},
orderBy: [{ sortOrder: 'asc' }, { name: 'asc' }],
});
const formatted = facilities.map((fac, index) => ({
SL_NO: String(index + 1),
id: fac.id,
facilityId: fac.facilityId,
name: fac.name,
slug: fac.slug,
shortDescription: fac.shortDescription,
description: fac.description,
videoUrl: fac.videoUrl ?? '',
isActive: fac.isActive,
isFeatured: fac.isFeatured,
sortOrder: fac.sortOrder,
department: fac.department
? {
id: fac.department.id,
departmentId: fac.department.departmentId,
name: fac.department.name,
}
: null,
images: fac.images.map((img) => ({
id: img.id,
url: img.url,
altText: img.altText ?? '',
description: img.description ?? '',
})),
seo: {
seoTitle: fac.seo?.seoTitle ?? '',
metaDescription: fac.seo?.metaDescription ?? '',
focusKeyphrase: fac.seo?.focusKeyphrase ?? '',
slug: fac.seo?.slug ?? '',
tags: fac.seo?.tags ?? [],
ogTitle: fac.seo?.ogTitle ?? '',
ogDescription: fac.seo?.ogDescription ?? '',
ogImage: fac.seo?.ogImage ?? '',
},
}));
res.status(200).json({
success: true,
data: formatted,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to fetch facilities',
});
}
};
export const getFacilityByFacilityId = async (req, res) => {
try {
const { facilityId } = req.params;
const { admin } = req.query;
const facility = await prisma.facility.findFirst({
where: {
facilityId,
...(admin === 'true' ? {} : { isActive: true }),
},
include: {
seo: true,
department: true,
images: true,
},
});
if (!facility) {
return res.status(404).json({
success: false,
message: 'Facility not found',
});
}
const response = {
facilityId: facility.facilityId,
name: facility.name,
slug: facility.slug,
shortDescription: facility.shortDescription,
description: facility.description,
videoUrl: facility.videoUrl ?? '',
isActive: facility.isActive,
isFeatured: facility.isFeatured,
sortOrder: facility.sortOrder,
department: facility.department
? {
departmentId: facility.department.departmentId,
name: facility.department.name,
}
: null,
images: facility.images.map((img) => ({
id: img.id,
url: img.url,
altText: img.altText ?? '',
description: img.description ?? '',
})),
seo: {
seoTitle: facility.seo?.seoTitle ?? '',
metaDescription: facility.seo?.metaDescription ?? '',
focusKeyphrase: facility.seo?.focusKeyphrase ?? '',
slug: facility.seo?.slug ?? '',
tags: facility.seo?.tags ?? [],
ogTitle: facility.seo?.ogTitle ?? '',
ogDescription: facility.seo?.ogDescription ?? '',
ogImage: facility.seo?.ogImage ?? '',
},
};
res.status(200).json({
success: true,
data: response,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to fetch facility',
});
}
};
export const createFacility = async (req, res) => {
try {
const {
facilityId,
name,
slug,
shortDescription,
description,
videoUrl,
isActive,
isFeatured,
sortOrder,
departmentId,
images,
seoTitle,
metaDescription,
focusKeyphrase,
tags,
ogTitle,
ogDescription,
ogImage,
} = req.body;
const messages = [];
if (!facilityId) messages.push('Facility ID is required');
if (!name?.trim()) messages.push('Facility name is required');
if (!slug?.trim()) messages.push('Slug is required');
if (messages.length > 0) {
return res.status(400).json({
success: false,
message: messages.join(', '),
});
}
let dbDepartmentId = null;
if (departmentId) {
const targetDept = await prisma.department.findUnique({
where: { departmentId: departmentId },
});
if (targetDept) {
dbDepartmentId = targetDept.id;
}
}
const seo = await prisma.seo.create({
data: {
seoTitle,
metaDescription,
focusKeyphrase,
slug: slug ? slug : null,
tags: tags || [],
ogTitle,
ogDescription,
ogImage,
},
});
const facility = await prisma.facility.create({
data: {
facilityId,
name,
slug,
shortDescription,
description,
videoUrl,
isActive: isActive !== undefined ? isActive : true,
isFeatured: isFeatured !== undefined ? isFeatured : false,
sortOrder: sortOrder !== undefined ? Number(sortOrder) : 1000,
departmentId: dbDepartmentId,
seoId: seo.id,
},
});
if (images && images.length > 0) {
await prisma.facilityImage.createMany({
data: images.map((img) => ({
url: img.url,
altText: img.altText || null,
description: img.description || null,
facilityId: facility.id,
})),
});
}
res.status(201).json({
success: true,
message: 'Facility created successfully',
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to create facility',
});
}
};
export const updateFacility = async (req, res) => {
try {
const { facilityId, action } = req.params;
const {
name,
slug,
shortDescription,
description,
videoUrl,
isActive,
isFeatured,
sortOrder,
departmentId,
images,
seoTitle,
metaDescription,
focusKeyphrase,
tags,
ogTitle,
ogDescription,
ogImage,
} = req.body;
if (!facilityId) {
return res.status(400).json({ success: false, message: 'Facility ID is required' });
}
const facility = await prisma.facility.findUnique({ where: { facilityId } });
if (!facility) return res.status(404).json({ success: false, message: 'Facility not found' });
if (action === 'toggleStatus') {
await prisma.facility.update({
where: { id: facility.id },
data: { isActive: !facility.isActive },
});
return res.status(200).json({
success: true,
message: `Facility has been ${facility.isActive ? 'deactivated' : 'activated'} successfully`,
});
}
if (action === 'toggleFeatured') {
await prisma.facility.update({
where: { id: facility.id },
data: { isFeatured: !facility.isFeatured },
});
return res.status(200).json({
success: true,
message: `Facility has been ${facility.isFeatured ? 'removed from featured' : 'marked as featured'} successfully`,
});
}
const messages = [];
if (!name?.trim()) messages.push('Facility name is required');
if (!slug?.trim()) messages.push('Slug is required');
if (messages.length > 0) {
return res.status(400).json({ success: false, message: messages.join(', ') });
}
let dbDepartmentId = undefined;
if (departmentId !== undefined) {
if (departmentId) {
const targetDept = await prisma.department.findUnique({
where: { departmentId: departmentId },
});
dbDepartmentId = targetDept ? targetDept.id : null;
} else {
dbDepartmentId = null;
}
}
await prisma.facility.update({
where: { id: facility.id },
data: {
name,
slug,
shortDescription,
description,
videoUrl,
isActive: isActive !== undefined ? isActive : undefined,
isFeatured: isFeatured !== undefined ? isFeatured : undefined,
sortOrder: sortOrder !== undefined ? Number(sortOrder) : undefined,
departmentId: dbDepartmentId,
},
});
if (facility.seoId) {
await prisma.seo.update({
where: { id: facility.seoId },
data: {
seoTitle,
metaDescription,
focusKeyphrase,
slug: slug ? slug : null,
tags: tags || [],
ogTitle,
ogDescription,
ogImage,
},
});
} else {
const seo = await prisma.seo.create({
data: {
seoTitle,
metaDescription,
focusKeyphrase,
slug: slug ? slug : null,
tags: tags || [],
ogTitle,
ogDescription,
ogImage,
},
});
await prisma.facility.update({ where: { id: facility.id }, data: { seoId: seo.id } });
}
if (Array.isArray(images)) {
await prisma.facilityImage.deleteMany({ where: { facilityId: facility.id } });
if (images.length > 0) {
await prisma.facilityImage.createMany({
data: images.map((img) => ({
url: img.url,
altText: img.altText || null,
description: img.description || null,
facilityId: facility.id,
})),
});
}
}
res.status(200).json({ success: true, message: 'Facility updated successfully' });
} catch (error) {
console.error('Update Error:', error);
res.status(500).json({ success: false, message: 'Failed to update facility' });
}
};
export const deleteFacility = async (req, res) => {
try {
const { facilityId } = req.params;
const facility = await prisma.facility.findUnique({ where: { facilityId } });
if (!facility) {
return res.status(404).json({ success: false, message: 'Facility not found' });
}
await prisma.facilityImage.deleteMany({ where: { facilityId: facility.id } });
await prisma.facility.delete({ where: { id: facility.id } });
res.status(200).json({
success: true,
message: 'Facility deleted successfully',
});
} catch (error) {
console.error(error);
res.status(500).json({ success: false, message: 'Failed to delete facility' });
}
};
export const getFeaturedFacilities = async (req, res) => {
try {
const facilities = await prisma.facility.findMany({
where: { isActive: true, isFeatured: true },
include: {
images: true,
department: true,
},
orderBy: [{ sortOrder: 'asc' }, { name: 'asc' }],
});
const data = facilities.map((fac) => ({
facilityId: fac.facilityId,
name: fac.name,
slug: fac.slug,
shortDescription: fac.shortDescription,
image: fac.images[0]?.url ?? '',
departmentName: fac.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 facilities' });
}
};
@@ -0,0 +1,224 @@
import prisma from '../prisma/client.js';
export const getFeaturedGoogleReviews = async (req, res) => {
try {
const reviews = await prisma.googleReview.findMany({
where: {
isActive: true,
isFeatured: true,
},
orderBy: {
sortOrder: 'asc',
},
});
res.json({
success: true,
data: reviews,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to fetch featured Google reviews',
});
}
};
export const createGoogleReview = async (req, res) => {
try {
const {
reviewerName,
reviewerImage,
rating,
review,
reviewDate,
googleReviewUrl,
isFeatured,
isActive,
sortOrder,
} = req.body;
if (!reviewerName || !rating || !review) {
return res.status(400).json({
success: false,
message: 'Reviewer name, rating and review are required.',
});
}
const googleReview = await prisma.googleReview.create({
data: {
reviewerName,
reviewerImage,
rating: Number(rating),
review,
reviewDate: reviewDate ? new Date(reviewDate) : null,
googleReviewUrl,
isFeatured,
isActive,
sortOrder,
},
});
res.status(201).json({
success: true,
data: googleReview,
message: 'Google review created successfully',
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to create Google review',
});
}
};
export const getGoogleReviews = async (req, res) => {
try {
const reviews = await prisma.googleReview.findMany({
orderBy: {
sortOrder: 'asc',
},
});
res.json({
success: true,
data: reviews,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to fetch Google reviews',
});
}
};
export const getActiveGoogleReviews = async (req, res) => {
try {
const reviews = await prisma.googleReview.findMany({
where: {
isActive: true,
},
orderBy: {
sortOrder: 'asc',
},
});
res.json({
success: true,
data: reviews,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to fetch active Google reviews',
});
}
};
export const getGoogleReview = async (req, res) => {
try {
const { id } = req.params;
const review = await prisma.googleReview.findUnique({
where: {
id: Number(id),
},
});
if (!review) {
return res.status(404).json({
success: false,
message: 'Google review not found',
});
}
res.json({
success: true,
data: review,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to fetch Google review',
});
}
};
export const updateGoogleReview = async (req, res) => {
try {
const { id } = req.params;
const {
reviewerName,
reviewerImage,
rating,
review,
reviewDate,
googleReviewUrl,
isFeatured,
isActive,
sortOrder,
} = req.body;
const dataToUpdate = {};
if (reviewerName !== undefined) dataToUpdate.reviewerName = reviewerName;
if (reviewerImage !== undefined) dataToUpdate.reviewerImage = reviewerImage;
if (rating !== undefined) dataToUpdate.rating = Number(rating);
if (review !== undefined) dataToUpdate.review = review;
if (reviewDate !== undefined) {
dataToUpdate.reviewDate = reviewDate ? new Date(reviewDate) : null;
}
if (googleReviewUrl !== undefined) dataToUpdate.googleReviewUrl = googleReviewUrl;
if (isFeatured !== undefined) dataToUpdate.isFeatured = isFeatured;
if (isActive !== undefined) dataToUpdate.isActive = isActive;
if (sortOrder !== undefined) dataToUpdate.sortOrder = sortOrder;
const googleReview = await prisma.googleReview.update({
where: {
id: Number(id),
},
data: dataToUpdate,
});
res.json({
success: true,
data: googleReview,
message: 'Google review updated successfully',
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to update Google review',
});
}
};
export const deleteGoogleReview = async (req, res) => {
try {
const { id } = req.params;
await prisma.googleReview.delete({
where: {
id: Number(id),
},
});
res.json({
success: true,
message: 'Google review deleted successfully',
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: 'Failed to delete Google review',
});
}
};
+23
View File
@@ -0,0 +1,23 @@
import express from 'express';
import {
getAllFacilities,
getFacilityByFacilityId,
createFacility,
updateFacility,
deleteFacility,
getFeaturedFacilities,
} from '../controllers/facility.controller.js';
import jwtAuthMiddleware from '../middleware/auth.js';
const router = express.Router();
router.get('/getAll', getAllFacilities);
router.get('/featured', getFeaturedFacilities);
router.get('/:facilityId', getFacilityByFacilityId);
router.post('/', jwtAuthMiddleware, createFacility);
router.patch('/:facilityId/:action', jwtAuthMiddleware, updateFacility);
router.delete('/:facilityId', jwtAuthMiddleware, deleteFacility);
export default router;
+25
View File
@@ -0,0 +1,25 @@
import express from 'express';
import jwtAuthMiddleware from '../middleware/auth.js';
import {
createGoogleReview,
getGoogleReviews,
getActiveGoogleReviews,
getFeaturedGoogleReviews,
getGoogleReview,
updateGoogleReview,
deleteGoogleReview,
} from '../controllers/googleReview.controller.js';
const router = express.Router();
router.get('/active', getActiveGoogleReviews);
router.get('/featured', getFeaturedGoogleReviews);
router.post('/', jwtAuthMiddleware, createGoogleReview);
router.get('/getAll', jwtAuthMiddleware, getGoogleReviews);
router.get('/:id', jwtAuthMiddleware, getGoogleReview);
router.put('/:id', jwtAuthMiddleware, updateGoogleReview);
router.delete('/:id', jwtAuthMiddleware, deleteGoogleReview);
export default router;