diff --git a/backend/prisma/migrations/20260622101915_accreditation/migration.sql b/backend/prisma/migrations/20260622101915_accreditation/migration.sql new file mode 100644 index 0000000..0c21349 --- /dev/null +++ b/backend/prisma/migrations/20260622101915_accreditation/migration.sql @@ -0,0 +1,18 @@ +-- CreateEnum +CREATE TYPE "AccreditationType" AS ENUM ('ACCREDITATION', 'CERTIFICATION'); + +-- CreateTable +CREATE TABLE "Accreditation" ( + "id" SERIAL NOT NULL, + "title" TEXT NOT NULL, + "type" "AccreditationType" NOT NULL, + "logo" TEXT, + "image" TEXT, + "description" 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 "Accreditation_pkey" PRIMARY KEY ("id") +); diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 0baea7d..993825d 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -356,4 +356,25 @@ model InsurancePartner { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt +} + +model Accreditation { + id Int @id @default(autoincrement()) + + title String + type AccreditationType + logo String? + image String? + description String? @db.Text + + sortOrder Int @default(1000) + isActive Boolean @default(true) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +enum AccreditationType { + ACCREDITATION + CERTIFICATION } \ No newline at end of file diff --git a/backend/src/app.js b/backend/src/app.js index 4e6257b..98da273 100644 --- a/backend/src/app.js +++ b/backend/src/app.js @@ -18,6 +18,7 @@ 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'; +import accreditationRoutes from './routes/accreditation.routes.js'; dotenv.config(); @@ -61,6 +62,7 @@ app.use('/api/import', importRoutes); app.use('/api/health-check', healthCheckRoutes); app.use('/api/homepage-banners', homepageBannerRoutes); app.use('/api/insurance-partners', insurancePartnerRoutes); +app.use('/api/accreditation', accreditationRoutes); const PORT = process.env.PORT || 5008; app.listen(PORT, () => { diff --git a/backend/src/controllers/accreditation.controller.js b/backend/src/controllers/accreditation.controller.js new file mode 100644 index 0000000..0f6fffe --- /dev/null +++ b/backend/src/controllers/accreditation.controller.js @@ -0,0 +1,189 @@ +import prisma from '../prisma/client.js'; + +export const createAccreditation = async (req, res) => { + try { + const { title, type, logo, image, description, sortOrder, isActive } = req.body; + + if (!title || !type) { + return res.status(400).json({ + success: false, + message: 'Title and type are required', + }); + } + + const accreditation = await prisma.accreditation.create({ + data: { + title, + type, + logo, + image, + description, + sortOrder: sortOrder ? Number(sortOrder) : 1000, + isActive: isActive ?? true, + }, + }); + + res.status(201).json({ + success: true, + data: accreditation, + message: 'Accreditation created successfully', + }); + } catch (error) { + console.error('PRISMA TRANSACTION ERROR:', error); + + res.status(500).json({ + success: false, + message: 'Failed to create accreditation', + }); + } +}; + +export const getAccreditations = async (req, res) => { + try { + const { type } = req.query; + + const accreditations = await prisma.accreditation.findMany({ + where: type + ? { + type, + } + : undefined, + orderBy: { + sortOrder: 'asc', + }, + }); + + res.json({ + success: true, + data: accreditations, + }); + } catch (error) { + console.error(error); + + res.status(500).json({ + success: false, + message: 'Failed to fetch accreditations', + }); + } +}; + +export const getActiveAccreditations = async (req, res) => { + try { + const { type } = req.query; + + const accreditations = await prisma.accreditation.findMany({ + where: { + isActive: true, + ...(type && { type }), + }, + orderBy: { + sortOrder: 'asc', + }, + }); + + res.json({ + success: true, + data: accreditations, + }); + } catch (error) { + console.error(error); + + res.status(500).json({ + success: false, + message: 'Failed to fetch accreditations', + }); + } +}; + +export const getAccreditation = async (req, res) => { + try { + const { id } = req.params; + + const accreditation = await prisma.accreditation.findUnique({ + where: { + id: Number(id), + }, + }); + + if (!accreditation) { + return res.status(404).json({ + success: false, + message: 'Accreditation not found', + }); + } + + res.json({ + success: true, + data: accreditation, + }); + } catch (error) { + console.error(error); + + res.status(500).json({ + success: false, + message: 'Failed to fetch accreditation', + }); + } +}; + +export const updateAccreditation = async (req, res) => { + try { + const { id } = req.params; + + const { title, type, logo, image, description, sortOrder, isActive } = req.body; + + const updateData = {}; + + if (title !== undefined) updateData.title = title; + if (type !== undefined) updateData.type = type; + if (logo !== undefined) updateData.logo = logo; + if (image !== undefined) updateData.image = image; + if (description !== undefined) updateData.description = description; + if (sortOrder !== undefined) updateData.sortOrder = Number(sortOrder); + if (isActive !== undefined) updateData.isActive = isActive; + + const accreditation = await prisma.accreditation.update({ + where: { + id: Number(id), + }, + data: updateData, + }); + + res.json({ + success: true, + data: accreditation, + message: 'Accreditation updated successfully', + }); + } catch (error) { + console.error('PRISMA TRANSACTION ERROR:', error); + + res.status(500).json({ + success: false, + message: 'Failed to update accreditation', + }); + } +}; + +export const deleteAccreditation = async (req, res) => { + try { + const { id } = req.params; + + await prisma.accreditation.delete({ + where: { + id: Number(id), + }, + }); + + res.json({ + success: true, + message: 'Accreditation deleted successfully', + }); + } catch (error) { + console.error(error); + + res.status(500).json({ + success: false, + message: 'Failed to delete accreditation', + }); + } +}; diff --git a/backend/src/routes/accreditation.routes.js b/backend/src/routes/accreditation.routes.js new file mode 100644 index 0000000..11499e2 --- /dev/null +++ b/backend/src/routes/accreditation.routes.js @@ -0,0 +1,24 @@ +import express from 'express'; + +import { + createAccreditation, + getAccreditations, + getActiveAccreditations, + getAccreditation, + updateAccreditation, + deleteAccreditation, +} from '../controllers/accreditation.controller.js'; + +import jwtAuthMiddleware from '../middleware/auth.js'; + +const router = express.Router(); + +router.get('/active', getActiveAccreditations); + +router.post('/', jwtAuthMiddleware, createAccreditation); +router.get('/getAll', jwtAuthMiddleware, getAccreditations); +router.get('/:id', jwtAuthMiddleware, getAccreditation); +router.put('/:id', jwtAuthMiddleware, updateAccreditation); +router.delete('/:id', jwtAuthMiddleware, deleteAccreditation); + +export default router; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 5f0d0ba..71001d3 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -26,6 +26,7 @@ import ImportData from './pages/ImportData'; import HealthPackagePage from './pages/HealthPackagePage'; import HomepageBanner from './pages/HomepageBannerPage'; import InsurancePartnerPage from './pages/InsurancePartner'; +import AccreditationPage from './pages/Accreditation'; export default function App() { return ( @@ -57,6 +58,7 @@ export default function App() { } /> } /> } /> + } /> diff --git a/frontend/src/api/accreditation.ts b/frontend/src/api/accreditation.ts new file mode 100644 index 0000000..0af4c36 --- /dev/null +++ b/frontend/src/api/accreditation.ts @@ -0,0 +1,85 @@ +import apiClient from '@/api/client'; +import toast from 'react-hot-toast'; + +export type AccreditationType = 'ACCREDITATION' | 'CERTIFICATION'; + +export interface Accreditation { + id?: number; + + title: string; + + type: AccreditationType; + + logo?: string; + image?: string; + + description?: string; + + sortOrder: number; + isActive: boolean; + + createdAt?: string; + updatedAt?: string; +} + +export const getAccreditationsApi = async (type?: AccreditationType) => { + const query = type ? `?type=${type}` : ''; + const res = await apiClient.get(`/accreditation/getAll${query}`); + + return res.data; +}; + +export const getAccreditationApi = async (id: number) => { + const res = await apiClient.get(`/accreditation/${id}`); + + return res.data; +}; + +export const getActiveAccreditationsApi = async (type?: AccreditationType) => { + const query = type ? `?type=${type}` : ''; + const res = await apiClient.get(`/accreditation/active${query}`); + + return res.data; +}; + +export const createAccreditationApi = async (data: Partial) => { + try { + const res = await apiClient.post('/accreditation', data); + + toast.success('Accreditation created successfully'); + + return res.data; + } catch (error: any) { + toast.error(error?.response?.data?.message || 'Failed to create accreditation'); + + throw error; + } +}; + +export const updateAccreditationApi = async (id: number, data: Partial) => { + try { + const res = await apiClient.put(`/accreditation/${id}`, data); + + toast.success('Accreditation updated successfully'); + + return res.data; + } catch (error: any) { + toast.error(error?.response?.data?.message || 'Failed to update accreditation'); + + throw error; + } +}; + +export const deleteAccreditationApi = async (id: number) => { + try { + const res = await apiClient.delete(`/accreditation/${id}`); + + toast.success('Accreditation deleted successfully'); + + return res.data; + } catch (error: any) { + toast.error(error?.response?.data?.message || 'Failed to delete accreditation'); + + throw error; + } +}; diff --git a/frontend/src/components/AccreditationModal/AccreditationModal.tsx b/frontend/src/components/AccreditationModal/AccreditationModal.tsx new file mode 100644 index 0000000..5bc901a --- /dev/null +++ b/frontend/src/components/AccreditationModal/AccreditationModal.tsx @@ -0,0 +1,209 @@ +import { BytescaleUploader } from '@/components/BytescaleUploader/BytescaleUploader'; + +import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; + +import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; +import { Button } from '@/components/ui/button'; +import { Label } from '@/components/ui/label'; +import { Switch } from '@/components/ui/switch'; + +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; + +interface Props { + open: boolean; + onOpenChange: (open: boolean) => void; + + editingAccreditation: any; + + accreditationForm: any; + setAccreditationForm: any; + + onSave: () => void; +} + +export default function AccreditationModal({ + open, + onOpenChange, + editingAccreditation, + accreditationForm, + setAccreditationForm, + onSave, +}: Props) { + return ( + + + {/* Header */} + + + {editingAccreditation ? 'Edit Accreditation / Certification' : 'Create Accreditation / Certification'} + + + + {/* Body */} +
+ {/* Basic Information */} +
+
+

Basic Information

+

Configure accreditation or certification details.

+
+ +
+
+ + + +
+ +
+ + + + setAccreditationForm({ + ...accreditationForm, + title: e.target.value, + }) + } + /> +
+
+ +
+ + +