Merge pull request 'feat: insurance crud' (#48) from feat/insurance-crud into dev

Reviewed-on: #48
This commit was merged in pull request #48.
This commit is contained in:
2026-06-22 06:53:46 +00:00
12 changed files with 715 additions and 4 deletions
@@ -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")
);
+13
View File
@@ -344,3 +344,16 @@ enum BannerMediaType {
IMAGE IMAGE
VIDEO 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
}
+2
View File
@@ -17,6 +17,7 @@ import newsMediaRoutes from './routes/newsMedia.routes.js';
import importRoutes from './routes/importRoutes.js'; import importRoutes from './routes/importRoutes.js';
import healthCheckRoutes from './routes/healthCheck.route.js'; import healthCheckRoutes from './routes/healthCheck.route.js';
import homepageBannerRoutes from './routes/homepageBanner.routes.js'; import homepageBannerRoutes from './routes/homepageBanner.routes.js';
import insurancePartnerRoutes from './routes/insurancePartner.routes.js';
dotenv.config(); dotenv.config();
@@ -59,6 +60,7 @@ app.use('/api/newsMedia', newsMediaRoutes);
app.use('/api/import', importRoutes); app.use('/api/import', importRoutes);
app.use('/api/health-check', healthCheckRoutes); app.use('/api/health-check', healthCheckRoutes);
app.use('/api/homepage-banners', homepageBannerRoutes); app.use('/api/homepage-banners', homepageBannerRoutes);
app.use('/api/insurance-partners', insurancePartnerRoutes);
const PORT = process.env.PORT || 5008; const PORT = process.env.PORT || 5008;
app.listen(PORT, () => { app.listen(PORT, () => {
@@ -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',
});
}
};
@@ -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;
+2
View File
@@ -25,6 +25,7 @@ import BlogDetail from './pages/BlogDetails';
import ImportData from './pages/ImportData'; import ImportData from './pages/ImportData';
import HealthPackagePage from './pages/HealthPackagePage'; import HealthPackagePage from './pages/HealthPackagePage';
import HomepageBanner from './pages/HomepageBannerPage'; import HomepageBanner from './pages/HomepageBannerPage';
import InsurancePartnerPage from './pages/InsurancePartner';
export default function App() { export default function App() {
return ( return (
@@ -55,6 +56,7 @@ export default function App() {
<Route path="/import" element={<ImportData />} /> <Route path="/import" element={<ImportData />} />
<Route path="/health-check" element={<HealthPackagePage />} /> <Route path="/health-check" element={<HealthPackagePage />} />
<Route path="/homepage-banner" element={<HomepageBanner />} /> <Route path="/homepage-banner" element={<HomepageBanner />} />
<Route path="/insurance-partner" element={<InsurancePartnerPage />} />
</Route> </Route>
</Route> </Route>
+61
View File
@@ -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<InsurancePartner>) => {
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<InsurancePartner>) => {
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;
}
};
@@ -14,7 +14,8 @@ interface BytescaleUploaderProps {
| '/news' | '/news'
| '/blog' | '/blog'
| '/doctor-og' | '/doctor-og'
| '/homepage-banners'; | '/homepage-banners'
| '/insurance-partners';
} }
export function BytescaleUploader({ value, onChange, folderPath }: BytescaleUploaderProps) { export function BytescaleUploader({ value, onChange, folderPath }: BytescaleUploaderProps) {
@@ -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 (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="w-full !max-w-2xl h-auto max-h-[90vh] flex flex-col p-0 overflow-hidden">
<DialogHeader className="px-6 py-5 border-b bg-background sticky top-0 z-20">
<DialogTitle className="text-2xl font-bold">
{editingPartner ? 'Edit Insurance Partner' : 'Create Insurance Partner'}
</DialogTitle>
</DialogHeader>
<div className="flex-1 overflow-y-auto p-6 space-y-6">
<div className="space-y-4">
<div className="border-b pb-2">
<h3 className="text-lg font-bold">Company Information</h3>
<p className="text-sm text-muted-foreground">Setup profile configurations for target insurance brand</p>
</div>
<div className="space-y-2">
<Label className="font-semibold">Company / Provider Name</Label>
<Input
value={partnerForm.name || ''}
placeholder="e.g., National Insurance, Star Health Care"
onChange={(e) =>
setPartnerForm({
...partnerForm,
name: e.target.value,
})
}
/>
</div>
<div className="space-y-2">
<Label className="font-semibold">Claim Portal Redirect Link (Optional)</Label>
<Input
value={partnerForm.websiteUrl || ''}
placeholder="e.g., https://corporate-claims-portal.com"
onChange={(e) =>
setPartnerForm({
...partnerForm,
websiteUrl: e.target.value,
})
}
/>
</div>
</div>
<div className="space-y-4">
<div className="space-y-2">
<Label className="font-semibold">Brand Logo Image</Label>
<p className="text-xs text-muted-foreground">
Recommended: Clear layout, transparent background (PNG or SVG preferred)
</p>
<BytescaleUploader
value={partnerForm.logo || ''}
folderPath="/insurance-partners"
onChange={(url) =>
setPartnerForm({
...partnerForm,
logo: url,
})
}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-2">
<div className="space-y-2">
<Label className="font-semibold">Grid Layout Sorting Rank</Label>
<Input
type="number"
value={partnerForm.sortOrder}
onChange={(e) =>
setPartnerForm({
...partnerForm,
sortOrder: Number(e.target.value),
})
}
/>
</div>
<div className="flex items-center justify-between border rounded-xl p-4 bg-muted/30">
<div>
<p className="font-semibold">Active Visibility</p>
</div>
<Switch
checked={partnerForm.isActive}
onCheckedChange={(val) =>
setPartnerForm({
...partnerForm,
isActive: val,
})
}
/>
</div>
</div>
</div>
</div>
<DialogFooter className="p-6 border-t bg-background sticky bottom-0 z-20">
<Button variant="ghost" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button className="px-10" onClick={onSave}>
{editingPartner ? 'Save Changes' : 'Add Partner'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
@@ -60,6 +60,10 @@ export default function Sidebar() {
name: 'Homepage Banner', name: 'Homepage Banner',
path: '/homepage-banner', path: '/homepage-banner',
}, },
{
name: 'Insurance Partner',
path: '/insurance-partner',
},
], ],
}, },
]; ];
@@ -172,9 +172,6 @@ export default function HomepageBannerPage() {
<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">
<div> <div>
<h1 className="text-3xl font-bold">Homepage Banners</h1> <h1 className="text-3xl font-bold">Homepage Banners</h1>
<p className="text-sm text-muted-foreground">
Manage sliding heroes, background loops, video graphics, and dynamic contextual landing URLs.
</p>
</div> </div>
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
+285
View File
@@ -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<InsurancePartner[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [partnerModal, setPartnerModal] = useState(false);
const [editingPartner, setEditingPartner] = useState<InsurancePartner | null>(null);
const [searchText, setSearchText] = useState('');
const [partnerForm, setPartnerForm] = useState<Partial<InsurancePartner>>({
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<string, any> = {};
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 (
<div className="p-6 space-y-6">
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-4">
<div>
<h1 className="text-3xl font-bold">Insurance Partners</h1>
</div>
<div className="flex flex-wrap gap-3">
<Input
placeholder="Search partners by name..."
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
className="w-[260px] text-base"
/>
<Button variant="outline" onClick={fetchData} disabled={loading} className="text-base">
<RefreshCw className="mr-2 h-5 w-5" />
Refresh
</Button>
<Button onClick={openAddPartner} className="text-base">
<Plus className="mr-2 h-5 w-5" />
Add Partner
</Button>
</div>
</div>
{error && <div className="p-4 text-red-600 bg-red-50 border rounded-md text-base">{error}</div>}
<Card>
<CardHeader>
<CardTitle className="text-xl">Network Panels Sequence Directory</CardTitle>
</CardHeader>
<CardContent className="p-0 sm:p-6">
<div className="rounded-md border overflow-x-auto overflow-y-auto max-h-[680px] relative">
<Table className="w-full min-w-[800px] table-fixed border-separate border-spacing-0">
<TableHeader className="sticky top-0 z-20 bg-background shadow-sm">
<TableRow>
<TableHead className="w-[80px] bg-background text-sm font-bold">Order</TableHead>
<TableHead className="w-[160px] bg-background text-sm font-bold">Logo Preview</TableHead>
<TableHead className="w-[260px] bg-background text-sm font-bold">Company Identity</TableHead>
<TableHead className="w-[240px] bg-background text-sm font-bold">Portal Website</TableHead>
<TableHead className="w-[120px] bg-background text-sm font-bold">Status</TableHead>
<TableHead className="w-[120px] bg-background text-right text-sm font-bold">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={6} className="text-center py-10">
<Loader2 className="h-8 w-8 animate-spin mx-auto" />
</TableCell>
</TableRow>
) : partners.length === 0 ? (
<TableRow>
<TableCell colSpan={6} className="text-center text-muted-foreground py-10 text-base">
No active healthcare insurance tie-ups found matching criteria.
</TableCell>
</TableRow>
) : (
partners.map((partner) => (
<TableRow key={partner.id} className="hover:bg-muted/50">
<TableCell className="font-mono text-sm">{partner.sortOrder}</TableCell>
<TableCell>
<div className="w-28 h-14 rounded-md overflow-hidden bg-white p-1 relative border flex items-center justify-center">
<img
src={partner.logo}
alt={`${partner.name} logo`}
className="w-full h-full object-contain"
/>
</div>
</TableCell>
<TableCell>
<div className="font-semibold text-base truncate" title={partner.name}>
{partner.name}
</div>
</TableCell>
<TableCell>
{partner.websiteUrl ? (
<a
href={partner.websiteUrl}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-sky-600 hover:underline flex items-center gap-1.5 truncate max-w-[220px]"
title={partner.websiteUrl}
>
<Link2 className="h-3.5 w-3.5 flex-shrink-0" />
{partner.websiteUrl}
</a>
) : (
<span className="text-xs text-muted-foreground italic">No external URL linked</span>
)}
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Switch checked={partner.isActive} onCheckedChange={() => handleToggleStatus(partner)} />
<Badge variant={partner.isActive ? 'default' : 'secondary'}>
{partner.isActive ? 'Active' : 'Disabled'}
</Badge>
</div>
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<Button
size="icon"
variant="ghost"
className="h-9 w-9 text-muted-foreground hover:text-foreground"
onClick={() => openEditPartner(partner)}
>
<Pencil className="h-4 w-4" />
</Button>
<Button
size="icon"
variant="ghost"
className="h-9 w-9 text-red-500 hover:text-red-600 hover:bg-red-50"
onClick={() => partner.id && handleDeletePartner(partner.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
<InsurancePartnerModal
open={partnerModal}
onOpenChange={setPartnerModal}
editingPartner={editingPartner}
partnerForm={partnerForm}
setPartnerForm={setPartnerForm}
onSave={savePartner}
/>
</div>
);
}