feat:add candidate pages

This commit is contained in:
ARJUN S THAMPI
2026-03-25 10:10:15 +05:30
parent 8277641077
commit de854ed538
5 changed files with 244 additions and 7 deletions
@@ -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: `
<h2>New Candidate Application</h2>
<p><b>Name:</b> ${fullName}</p>
<p><b>Phone:</b> ${mobile}</p>
<p><b>Email:</b> ${email}</p>
<p><b>Applied For:</b> ${candidate.career?.post || "-"}</p>
<p><b>Designation:</b> ${candidate.career?.designation || "-"}</p>
<p><b>Subject:</b> ${subject || "-"}</p>
<p><b>Cover Letter:</b></p>
<p>${coverLetter || "-"}</p>
`,
});
}
} catch (err) {
console.error("Candidate email failed:", err);
}
res.status(201).json({
success: true,
message: "Application submitted successfully",
+2
View File
@@ -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() {
<Route path="/appointment" element={<Appointment />} />
<Route path="/email" element={<EmailPage />} />
<Route path="/career" element={<CareerPage />} />
<Route path="/candidate" element={<CandidatePage />} />
</Route>
</Route>
+11
View File
@@ -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;
};
+5 -2
View File
@@ -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() {
<Link key={item.path} to={item.path}>
<Button
variant={active ? "secondary" : "ghost"}
className="w-full justify-start"
>
className="w-full justify-start">
{item.name}
</Button>
</Link>
+187
View File
@@ -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<any[]>([]);
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 (
<div className="p-6 space-y-6">
<div className="flex justify-between items-center gap-3 flex-wrap">
<h1 className="text-2xl font-bold">Candidates</h1>
<div className="flex flex-wrap gap-2">
<Input
placeholder="Search name / phone / email..."
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
className="w-[220px]"
/>
<Input
placeholder="Filter Career"
value={filterCareer}
onChange={(e) => setFilterCareer(e.target.value)}
className="w-[200px]"
/>
<Button variant="outline" onClick={fetchAll} disabled={loading}>
<RefreshCw className="mr-2 h-4 w-4" />
Refresh
</Button>
<Button variant="outline" onClick={handleExport}>
<Download className="mr-2 h-4 w-4" />
Export
</Button>
</div>
</div>
<Card>
<CardHeader>
<CardTitle>Candidate List</CardTitle>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<Table className="min-w-[900px]">
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead>Name</TableHead>
<TableHead>Phone</TableHead>
<TableHead>Email</TableHead>
<TableHead>Career</TableHead>
<TableHead>Designation</TableHead>
<TableHead>Subject</TableHead>
<TableHead>Cover Letter</TableHead>
<TableHead>Applied On</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={10} className="text-center">
<Loader2 className="h-6 w-6 animate-spin mx-auto" />
</TableCell>
</TableRow>
) : filteredCandidates.length === 0 ? (
<TableRow>
<TableCell colSpan={10} className="text-center">
No candidates found
</TableCell>
</TableRow>
) : (
filteredCandidates.map((item) => (
<TableRow key={item.id}>
<TableCell>{item.id}</TableCell>
<TableCell>{item.fullName}</TableCell>
<TableCell>{item.mobile}</TableCell>
<TableCell>{item.email}</TableCell>
<TableCell>{item.career?.post}</TableCell>
<TableCell>{item.career?.designation}</TableCell>
<TableCell>{item.subject}</TableCell>
<TableCell className="max-w-[250px] whitespace-normal">
{item.coverLetter}
</TableCell>
<TableCell>
{new Date(item.createdAt).toLocaleDateString()}
</TableCell>
<TableCell>
<Button
size="sm"
variant="destructive"
onClick={() => handleDelete(item.id)}>
<Trash className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
</div>
);
}