From c077574cbb87f37bcafcdb0083425f11aa760b56 Mon Sep 17 00:00:00 2001 From: Kailasdevdas Date: Wed, 17 Jun 2026 14:36:07 +0530 Subject: [PATCH] feat: insurance crud --- .../migration.sql | 13 + backend/prisma/schema.prisma | 13 + backend/src/app.js | 2 + .../insurancePartner.controller.js | 173 +++++++++++ backend/src/routes/insurancePartner.routes.js | 28 ++ frontend/src/App.tsx | 2 + frontend/src/api/insurancePartner.ts | 61 ++++ .../BytescaleUploader/BytescaleUploader.tsx | 3 +- .../InsurancePartnerModal.tsx | 132 ++++++++ frontend/src/components/layout/Sidebar.tsx | 4 + frontend/src/pages/HomepageBannerPage.tsx | 3 - frontend/src/pages/InsurancePartner.tsx | 285 ++++++++++++++++++ 12 files changed, 715 insertions(+), 4 deletions(-) create mode 100644 backend/prisma/migrations/20260617053419_add_insurance_partner/migration.sql create mode 100644 backend/src/controllers/insurancePartner.controller.js create mode 100644 backend/src/routes/insurancePartner.routes.js create mode 100644 frontend/src/api/insurancePartner.ts create mode 100644 frontend/src/components/InsurancePartnerModal/InsurancePartnerModal.tsx create mode 100644 frontend/src/pages/InsurancePartner.tsx diff --git a/backend/prisma/migrations/20260617053419_add_insurance_partner/migration.sql b/backend/prisma/migrations/20260617053419_add_insurance_partner/migration.sql new file mode 100644 index 0000000..249ec89 --- /dev/null +++ b/backend/prisma/migrations/20260617053419_add_insurance_partner/migration.sql @@ -0,0 +1,13 @@ +-- CreateTable +CREATE TABLE "InsurancePartner" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "logo" TEXT NOT NULL, + "websiteUrl" TEXT, + "sortOrder" INTEGER NOT NULL DEFAULT 1000, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "InsurancePartner_pkey" PRIMARY KEY ("id") +); diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 9bbc928..0baea7d 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -343,4 +343,17 @@ model HomepageBanner { enum BannerMediaType { IMAGE VIDEO +} + +model InsurancePartner { + id Int @id @default(autoincrement()) + + name String + logo String + websiteUrl String? + sortOrder Int @default(1000) + isActive Boolean @default(true) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } \ No newline at end of file diff --git a/backend/src/app.js b/backend/src/app.js index e343468..4e6257b 100644 --- a/backend/src/app.js +++ b/backend/src/app.js @@ -17,6 +17,7 @@ import newsMediaRoutes from './routes/newsMedia.routes.js'; import importRoutes from './routes/importRoutes.js'; import healthCheckRoutes from './routes/healthCheck.route.js'; import homepageBannerRoutes from './routes/homepageBanner.routes.js'; +import insurancePartnerRoutes from './routes/insurancePartner.routes.js'; dotenv.config(); @@ -59,6 +60,7 @@ app.use('/api/newsMedia', newsMediaRoutes); app.use('/api/import', importRoutes); app.use('/api/health-check', healthCheckRoutes); app.use('/api/homepage-banners', homepageBannerRoutes); +app.use('/api/insurance-partners', insurancePartnerRoutes); const PORT = process.env.PORT || 5008; app.listen(PORT, () => { diff --git a/backend/src/controllers/insurancePartner.controller.js b/backend/src/controllers/insurancePartner.controller.js new file mode 100644 index 0000000..f0ed62a --- /dev/null +++ b/backend/src/controllers/insurancePartner.controller.js @@ -0,0 +1,173 @@ +import prisma from '../prisma/client.js'; + +export const createInsurancePartner = async (req, res) => { + try { + const { name, logo, websiteUrl, sortOrder, isActive } = req.body; + + if (!name || !logo) { + return res.status(400).json({ + success: false, + message: 'Name and logo are required', + }); + } + + const partner = await prisma.insurancePartner.create({ + data: { + name, + logo, + websiteUrl, + sortOrder, + isActive, + }, + }); + + res.status(201).json({ + success: true, + data: partner, + message: 'Insurance partner created successfully', + }); + } catch (error) { + console.error(error); + + res.status(500).json({ + success: false, + message: 'Failed to create insurance partner', + }); + } +}; + +export const getInsurancePartners = async (req, res) => { + try { + const partners = await prisma.insurancePartner.findMany({ + orderBy: { + sortOrder: 'asc', + }, + }); + + res.json({ + success: true, + data: partners, + }); + } catch (error) { + console.error(error); + + res.status(500).json({ + success: false, + message: 'Failed to fetch insurance partners', + }); + } +}; + +export const getActiveInsurancePartners = async (req, res) => { + try { + const partners = await prisma.insurancePartner.findMany({ + where: { + isActive: true, + }, + orderBy: { + sortOrder: 'asc', + }, + }); + + res.json({ + success: true, + data: partners, + }); + } catch (error) { + console.error(error); + + res.status(500).json({ + success: false, + message: 'Failed to fetch insurance partners', + }); + } +}; + +export const getInsurancePartner = async (req, res) => { + try { + const { id } = req.params; + + const partner = await prisma.insurancePartner.findUnique({ + where: { + id: Number(id), + }, + }); + + if (!partner) { + return res.status(404).json({ + success: false, + message: 'Insurance partner not found', + }); + } + + res.json({ + success: true, + data: partner, + }); + } catch (error) { + console.error(error); + + res.status(500).json({ + success: false, + message: 'Failed to fetch insurance partner', + }); + } +}; + +export const updateInsurancePartner = async (req, res) => { + try { + const { id } = req.params; + + const { name, logo, websiteUrl, sortOrder, isActive } = req.body; + + const partner = await prisma.insurancePartner.update({ + where: { + id: Number(id), + }, + data: { + name, + logo, + websiteUrl, + sortOrder, + isActive, + }, + }); + + res.json({ + success: true, + data: partner, + message: 'Insurance partner updated successfully', + }); + } catch (error) { + console.error(error); + + res.status(500).json({ + success: false, + message: 'Failed to update insurance partner', + }); + } +}; + +export const deleteInsurancePartner = async (req, res) => { + try { + const { id } = req.params; + + await prisma.insurancePartner.delete({ + where: { + id: Number(id), + }, + }); + + res.json({ + success: true, + message: 'Insurance partner deleted successfully', + }); + } catch (error) { + console.error(error); + + res.status(500).json({ + success: false, + message: 'Failed to delete insurance partner', + }); + } +}; diff --git a/backend/src/routes/insurancePartner.routes.js b/backend/src/routes/insurancePartner.routes.js new file mode 100644 index 0000000..a11d112 --- /dev/null +++ b/backend/src/routes/insurancePartner.routes.js @@ -0,0 +1,28 @@ +import express from 'express'; + +import { + createInsurancePartner, + getInsurancePartners, + getActiveInsurancePartners, + getInsurancePartner, + updateInsurancePartner, + deleteInsurancePartner, +} from '../controllers/insurancePartner.controller.js'; + +import jwtAuthMiddleware from '../middleware/auth.js'; + +const router = express.Router(); + +router.get('/active', getActiveInsurancePartners); + +router.post('/', jwtAuthMiddleware, createInsurancePartner); + +router.get('/getAll', jwtAuthMiddleware, getInsurancePartners); + +router.get('/:id', jwtAuthMiddleware, getInsurancePartner); + +router.put('/:id', jwtAuthMiddleware, updateInsurancePartner); + +router.delete('/:id', jwtAuthMiddleware, deleteInsurancePartner); + +export default router; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index aaa2f45..5f0d0ba 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -25,6 +25,7 @@ import BlogDetail from './pages/BlogDetails'; import ImportData from './pages/ImportData'; import HealthPackagePage from './pages/HealthPackagePage'; import HomepageBanner from './pages/HomepageBannerPage'; +import InsurancePartnerPage from './pages/InsurancePartner'; export default function App() { return ( @@ -55,6 +56,7 @@ export default function App() { } /> } /> } /> + } /> diff --git a/frontend/src/api/insurancePartner.ts b/frontend/src/api/insurancePartner.ts new file mode 100644 index 0000000..9d1d194 --- /dev/null +++ b/frontend/src/api/insurancePartner.ts @@ -0,0 +1,61 @@ +import apiClient from '@/api/client'; +import toast from 'react-hot-toast'; + +export interface InsurancePartner { + id?: number; + name: string; + logo: string; + websiteUrl?: string; + sortOrder: number; + isActive: boolean; + createdAt?: string; + updatedAt?: string; +} + +export const getInsurancePartnersApi = async () => { + const res = await apiClient.get('/insurance-partners/getAll'); + return res.data; +}; + +export const getInsurancePartnerApi = async (id: number) => { + const res = await apiClient.get(`/insurance-partners/${id}`); + return res.data; +}; + +export const getActiveInsurancePartnersApi = async () => { + const res = await apiClient.get('/insurance-partners/active'); + return res.data; +}; + +export const createInsurancePartnerApi = async (data: Partial) => { + try { + const res = await apiClient.post('/insurance-partners', data); + toast.success('Insurance partner created successfully'); + return res.data; + } catch (error: any) { + toast.error(error?.response?.data?.message || 'Failed to create insurance partner'); + throw error; + } +}; + +export const updateInsurancePartnerApi = async (id: number, data: Partial) => { + try { + const res = await apiClient.put(`/insurance-partners/${id}`, data); + toast.success('Insurance partner updated successfully'); + return res.data; + } catch (error: any) { + toast.error(error?.response?.data?.message || 'Failed to update insurance partner'); + throw error; + } +}; + +export const deleteInsurancePartnerApi = async (id: number) => { + try { + const res = await apiClient.delete(`/insurance-partners/${id}`); + toast.success('Insurance partner deleted successfully'); + return res.data; + } catch (error: any) { + toast.error(error?.response?.data?.message || 'Failed to delete insurance partner'); + throw error; + } +}; diff --git a/frontend/src/components/BytescaleUploader/BytescaleUploader.tsx b/frontend/src/components/BytescaleUploader/BytescaleUploader.tsx index 72bf160..bb230cf 100644 --- a/frontend/src/components/BytescaleUploader/BytescaleUploader.tsx +++ b/frontend/src/components/BytescaleUploader/BytescaleUploader.tsx @@ -14,7 +14,8 @@ interface BytescaleUploaderProps { | '/news' | '/blog' | '/doctor-og' - | '/homepage-banners'; + | '/homepage-banners' + | '/insurance-partners'; } export function BytescaleUploader({ value, onChange, folderPath }: BytescaleUploaderProps) { diff --git a/frontend/src/components/InsurancePartnerModal/InsurancePartnerModal.tsx b/frontend/src/components/InsurancePartnerModal/InsurancePartnerModal.tsx new file mode 100644 index 0000000..3eb62cb --- /dev/null +++ b/frontend/src/components/InsurancePartnerModal/InsurancePartnerModal.tsx @@ -0,0 +1,132 @@ +import { BytescaleUploader } from '@/components/BytescaleUploader/BytescaleUploader'; +import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { Label } from '@/components/ui/label'; +import { Switch } from '@/components/ui/switch'; + +interface Props { + open: boolean; + onOpenChange: (open: boolean) => void; + editingPartner: any; + partnerForm: any; + setPartnerForm: any; + onSave: () => void; +} + +export default function InsurancePartnerModal({ + open, + onOpenChange, + editingPartner, + partnerForm, + setPartnerForm, + onSave, +}: Props) { + return ( + + + + + {editingPartner ? 'Edit Insurance Partner' : 'Create Insurance Partner'} + + + +
+
+
+

Company Information

+

Setup profile configurations for target insurance brand

+
+ +
+ + + setPartnerForm({ + ...partnerForm, + name: e.target.value, + }) + } + /> +
+ +
+ + + setPartnerForm({ + ...partnerForm, + websiteUrl: e.target.value, + }) + } + /> +
+
+ +
+
+ +

+ Recommended: Clear layout, transparent background (PNG or SVG preferred) +

+ + setPartnerForm({ + ...partnerForm, + logo: url, + }) + } + /> +
+ +
+
+ + + setPartnerForm({ + ...partnerForm, + sortOrder: Number(e.target.value), + }) + } + /> +
+ +
+
+

Active Visibility

+
+ + setPartnerForm({ + ...partnerForm, + isActive: val, + }) + } + /> +
+
+
+
+ + + + + +
+
+ ); +} diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index ee8c80b..9165057 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -60,6 +60,10 @@ export default function Sidebar() { name: 'Homepage Banner', path: '/homepage-banner', }, + { + name: 'Insurance Partner', + path: '/insurance-partner', + }, ], }, ]; diff --git a/frontend/src/pages/HomepageBannerPage.tsx b/frontend/src/pages/HomepageBannerPage.tsx index 0934513..00d224a 100644 --- a/frontend/src/pages/HomepageBannerPage.tsx +++ b/frontend/src/pages/HomepageBannerPage.tsx @@ -172,9 +172,6 @@ export default function HomepageBannerPage() {

Homepage Banners

-

- Manage sliding heroes, background loops, video graphics, and dynamic contextual landing URLs. -

diff --git a/frontend/src/pages/InsurancePartner.tsx b/frontend/src/pages/InsurancePartner.tsx new file mode 100644 index 0000000..3faabec --- /dev/null +++ b/frontend/src/pages/InsurancePartner.tsx @@ -0,0 +1,285 @@ +import { useState, useEffect, useCallback, useMemo } from 'react'; +import toast from 'react-hot-toast'; +import { AxiosError } from 'axios'; + +import { + getInsurancePartnersApi, + createInsurancePartnerApi, + updateInsurancePartnerApi, + deleteInsurancePartnerApi, + InsurancePartner, +} from '@/api/insurancePartner'; + +import InsurancePartnerModal from '@/components/InsurancePartnerModal/InsurancePartnerModal'; + +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Switch } from '@/components/ui/switch'; +import { Input } from '@/components/ui/input'; + +import { Loader2, RefreshCw, Plus, Pencil, Trash2, Link2 } from 'lucide-react'; + +export default function InsurancePartnerPage() { + const [partners, setPartners] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + + const [partnerModal, setPartnerModal] = useState(false); + const [editingPartner, setEditingPartner] = useState(null); + + const [searchText, setSearchText] = useState(''); + + const [partnerForm, setPartnerForm] = useState>({ + name: '', + logo: '', + websiteUrl: '', + sortOrder: 1000, + isActive: true, + }); + + const fetchData = useCallback(async () => { + setLoading(true); + setError(''); + try { + const res = await getInsurancePartnersApi(); + setPartners(res.data || []); + } catch (err) { + if (err instanceof AxiosError) { + setError(err.response?.data?.message || 'Failed to sync insurance directory records.'); + } else { + setError('An unhandled database communication error occurred.'); + } + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + const handleToggleStatus = async (partner: InsurancePartner) => { + if (!partner.id) return; + try { + await updateInsurancePartnerApi(partner.id, { isActive: !partner.isActive }); + toast.success(`Partner status updated successfully`); + fetchData(); + } catch (err) { + console.error(err); + toast.error('Could not overwrite corporate status configurations.'); + } + }; + + const handleDeletePartner = async (id: number) => { + const confirmDelete = window.confirm( + 'Are you completely sure you want to remove this insurance partner record? This step is irreversible.' + ); + if (!confirmDelete) return; + + try { + await deleteInsurancePartnerApi(id); + fetchData(); + } catch (err) { + console.error(err); + } + }; + + const openAddPartner = () => { + setEditingPartner(null); + setPartnerForm({ + name: '', + logo: '', + websiteUrl: '', + sortOrder: 1000, + isActive: true, + }); + setPartnerModal(true); + }; + + const openEditPartner = (partner: InsurancePartner) => { + setEditingPartner(partner); + setPartnerForm({ ...partner }); + setPartnerModal(true); + }; + + const savePartner = async () => { + if (!partnerForm.name) return toast.error('Insurance Partner Name is required.'); + if (!partnerForm.logo) return toast.error('Partner Corporate Logo Asset is required.'); + + try { + const finalData = { ...partnerForm }; + + if (editingPartner?.id) { + const changedFields: Record = {}; + Object.keys(finalData).forEach((key) => { + const k = key as keyof InsurancePartner; + if (JSON.stringify(finalData[k]) !== JSON.stringify(editingPartner[k])) { + changedFields[k] = finalData[k]; + } + }); + + delete changedFields.id; + delete changedFields.createdAt; + delete changedFields.updatedAt; + + if (Object.keys(changedFields).length === 0) { + setPartnerModal(false); + return; + } + + await updateInsurancePartnerApi(editingPartner.id, changedFields); + } else { + await createInsurancePartnerApi(finalData); + } + + setPartnerModal(false); + fetchData(); + } catch (err) { + console.error(err); + toast.error('An unexpected service exception halted corporate resource persistence runtime.'); + } + }; + + return ( +
+
+
+

Insurance Partners

+
+ +
+ setSearchText(e.target.value)} + className="w-[260px] text-base" + /> + + + + +
+
+ + {error &&
{error}
} + + + + Network Panels Sequence Directory + + +
+ + + + Order + Logo Preview + Company Identity + Portal Website + Status + Actions + + + + {loading ? ( + + + + + + ) : partners.length === 0 ? ( + + + No active healthcare insurance tie-ups found matching criteria. + + + ) : ( + partners.map((partner) => ( + + {partner.sortOrder} + +
+ {`${partner.name} +
+
+ +
+ {partner.name} +
+
+ + {partner.websiteUrl ? ( + + + {partner.websiteUrl} + + ) : ( + No external URL linked + )} + + +
+ handleToggleStatus(partner)} /> + + {partner.isActive ? 'Active' : 'Disabled'} + +
+
+ +
+ + +
+
+
+ )) + )} +
+
+
+
+
+ + +
+ ); +}