diff --git a/backend/src/controllers/candidate.controller.js b/backend/src/controllers/candidate.controller.js index bdc6b6c..b87f053 100644 --- a/backend/src/controllers/candidate.controller.js +++ b/backend/src/controllers/candidate.controller.js @@ -1,10 +1,14 @@ import prisma from "../prisma/client.js"; +import { sendEmail } from "../utils/sendEmail.js"; +import { getEmailsByType } from "../utils/getEmailByTypes.js"; + // CREATE CANDIDATE export const createCandidate = async (req, res) => { try { - const {fullName, mobile, email, subject, coverLetter, careerId} = req.body; + const { fullName, mobile, email, subject, coverLetter, careerId } = + req.body; if (!fullName || !mobile || !email || !careerId) { return res.status(400).json({ @@ -22,8 +26,38 @@ export const createCandidate = async (req, res) => { coverLetter, careerId: Number(careerId), }, + include: { + career: true, + }, }); + try { + const emailList = await getEmailsByType("CANDIDATE"); + + if (emailList && emailList.length > 0) { + await sendEmail({ + to: emailList, + subject: "New Job Application Received", + html: ` +

New Candidate Application

+ +

Name: ${fullName}

+

Phone: ${mobile}

+

Email: ${email}

+ +

Applied For: ${candidate.career?.post || "-"}

+

Designation: ${candidate.career?.designation || "-"}

+ +

Subject: ${subject || "-"}

+

Cover Letter:

+

${coverLetter || "-"}

+ `, + }); + } + } catch (err) { + console.error("Candidate email failed:", err); + } + res.status(201).json({ success: true, message: "Application submitted successfully", @@ -68,7 +102,7 @@ export const getCandidates = async (req, res) => { export const getCandidate = async (req, res) => { try { - const {id} = req.params; + const { id } = req.params; const candidate = await prisma.candidate.findUnique({ where: { @@ -103,7 +137,7 @@ export const getCandidate = async (req, res) => { export const getCandidatesByCareer = async (req, res) => { try { - const {careerId} = req.params; + const { careerId } = req.params; const candidates = await prisma.candidate.findMany({ where: { @@ -134,7 +168,7 @@ export const getCandidatesByCareer = async (req, res) => { export const updateCandidate = async (req, res) => { try { - const {id} = req.params; + const { id } = req.params; const candidate = await prisma.candidate.update({ where: { @@ -161,7 +195,7 @@ export const updateCandidate = async (req, res) => { export const deleteCandidate = async (req, res) => { try { - const {id} = req.params; + const { id } = req.params; await prisma.candidate.delete({ where: { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d8a1b4d..f2a027b 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -16,6 +16,7 @@ import BlogEditorPage from "./pages/BlogEditor"; import Appointment from "./pages/Appointment"; import EmailPage from "./pages/email"; import CareerPage from "./pages/Career"; +import CandidatePage from "./pages/candidates"; export default function App() { return ( @@ -36,6 +37,7 @@ export default function App() { } /> } /> } /> + } /> diff --git a/frontend/src/api/candidates.ts b/frontend/src/api/candidates.ts new file mode 100644 index 0000000..fa81dc6 --- /dev/null +++ b/frontend/src/api/candidates.ts @@ -0,0 +1,11 @@ +import apiClient from "@/api/client"; + +export const getCandidatesApi = async () => { + const res = await apiClient.get("/candidates/getAll"); + return res.data; +}; + +export const deleteCandidateApi = async (id: number) => { + const res = await apiClient.delete(`/candidates/${id}`); + return res.data; +}; diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index a3e1acd..d434faa 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -23,6 +23,10 @@ export default function Sidebar() { name: "Career", path: "/career", }, + { + name: "Candidates", + path: "/candidate", + }, { name: "Email", path: "/email", @@ -49,8 +53,7 @@ export default function Sidebar() { diff --git a/frontend/src/pages/candidates.tsx b/frontend/src/pages/candidates.tsx new file mode 100644 index 0000000..45a8ea7 --- /dev/null +++ b/frontend/src/pages/candidates.tsx @@ -0,0 +1,187 @@ +import { useState, useEffect, useCallback } from "react"; + +import { getCandidatesApi, deleteCandidateApi } from "@/api/candidates"; +import { exportToExcel } from "@/utils/exportToExcel"; + +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 { Input } from "@/components/ui/input"; + +import { Loader2, Trash, RefreshCw, Download } from "lucide-react"; + +export default function CandidatePage() { + const [candidates, setCandidates] = useState([]); + const [loading, setLoading] = useState(true); + + const [searchText, setSearchText] = useState(""); + const [filterCareer, setFilterCareer] = useState(""); + + const fetchAll = useCallback(async () => { + setLoading(true); + try { + const res = await getCandidatesApi(); + setCandidates(res?.data || []); + } catch (err) { + console.error(err); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchAll(); + }, [fetchAll]); + + const filteredCandidates = candidates.filter((item) => { + const matchesSearch = + item.fullName?.toLowerCase().includes(searchText.toLowerCase()) || + item.mobile?.includes(searchText) || + item.email?.toLowerCase().includes(searchText.toLowerCase()); + + const matchesCareer = filterCareer + ? item.career?.post?.toLowerCase().includes(filterCareer.toLowerCase()) + : true; + + return matchesSearch && matchesCareer; + }); + + async function handleDelete(id: number) { + if (!confirm("Delete candidate?")) return; + await deleteCandidateApi(id); + fetchAll(); + } + + const handleExport = () => { + const exportData = filteredCandidates.map((item) => ({ + ID: item.id, + Name: item.fullName, + Phone: item.mobile, + Email: item.email, + Career: item.career?.post, + Designation: item.career?.designation, + Subject: item.subject, + CoverLetter: item.coverLetter, + Date: new Date(item.createdAt).toLocaleDateString(), + })); + + exportToExcel(exportData, "candidates"); + }; + + return ( +
+
+

Candidates

+ +
+ setSearchText(e.target.value)} + className="w-[220px]" + /> + + setFilterCareer(e.target.value)} + className="w-[200px]" + /> + + + + +
+
+ + + + Candidate List + + + +
+ + + + ID + Name + Phone + Email + Career + Designation + Subject + Cover Letter + Applied On + Actions + + + + + {loading ? ( + + + + + + ) : filteredCandidates.length === 0 ? ( + + + No candidates found + + + ) : ( + filteredCandidates.map((item) => ( + + {item.id} + {item.fullName} + {item.mobile} + {item.email} + + {item.career?.post} + {item.career?.designation} + + {item.subject} + + + {item.coverLetter} + + + + {new Date(item.createdAt).toLocaleDateString()} + + + + + + + )) + )} + +
+
+
+
+
+ ); +}