candidate data
This commit is contained in:
92
src/app/candidates/[id]/edit/page.tsx
Normal file
92
src/app/candidates/[id]/edit/page.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import { Suspense } from "react";
|
||||
import { notFound } from "next/navigation";
|
||||
import { Metadata } from "next";
|
||||
import MainLayout from "@/components/layout/MainLayout";
|
||||
import { CandidateForm } from "@/components/candidates/CandidateForm";
|
||||
import { requireAuth } from "@/lib/auth";
|
||||
import { connectToDatabase } from "@/lib/db";
|
||||
import { Candidate } from "@/models/Candidate";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Edit Candidate - Candidate Portal",
|
||||
description: "Edit candidate information",
|
||||
};
|
||||
|
||||
interface EditCandidatePageProps {
|
||||
params: {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
async function getCandidateById(id: string) {
|
||||
await connectToDatabase();
|
||||
const candidate = await Candidate.findById(id);
|
||||
|
||||
if (!candidate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return JSON.parse(JSON.stringify(candidate));
|
||||
}
|
||||
|
||||
function FormSkeleton() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-6 w-1/3" />
|
||||
<Skeleton className="h-4 w-1/2" />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<div key={i} className="space-y-2">
|
||||
<Skeleton className="h-4 w-20" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Skeleton className="h-32 w-full" />
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Skeleton className="h-10 w-24" />
|
||||
<Skeleton className="h-10 w-32" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default async function EditCandidatePage({
|
||||
params,
|
||||
}: EditCandidatePageProps) {
|
||||
// Ensure user is authenticated
|
||||
await requireAuth();
|
||||
|
||||
const candidateData = await getCandidateById(params.id);
|
||||
|
||||
if (!candidateData) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<MainLayout>
|
||||
<div className="flex flex-col space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Edit Candidate</CardTitle>
|
||||
<CardDescription>
|
||||
Update information for {candidateData.name}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Suspense fallback={<FormSkeleton />}>
|
||||
<CandidateForm
|
||||
initialData={candidateData}
|
||||
isEditing={true}
|
||||
/>
|
||||
</Suspense>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</MainLayout>
|
||||
);
|
||||
}
|
||||
306
src/app/candidates/[id]/page.tsx
Normal file
306
src/app/candidates/[id]/page.tsx
Normal file
@@ -0,0 +1,306 @@
|
||||
import { Suspense } from "react";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import MainLayout from "@/components/layout/MainLayout";
|
||||
import { requireAuth } from "@/lib/auth";
|
||||
import { connectToDatabase } from "@/lib/db";
|
||||
import { Candidate } from "@/models/Candidate";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle
|
||||
} from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { ArrowLeft, Calendar, Mail, MapPin, Phone, Edit, Trash2 } from "lucide-react";
|
||||
import { format } from "date-fns";
|
||||
import { DeleteCandidateButton } from "@/components/candidates/DeleteCandidateButton";
|
||||
|
||||
interface CandidateDetailsPageProps {
|
||||
params: {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
async function getCandidateById(id: string) {
|
||||
await connectToDatabase();
|
||||
const candidate = await Candidate.findById(id);
|
||||
|
||||
if (!candidate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return JSON.parse(JSON.stringify(candidate));
|
||||
}
|
||||
|
||||
function CandidateDetailsSkeleton() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<Skeleton className="h-8 w-64" />
|
||||
<Skeleton className="h-4 w-40" />
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Skeleton className="h-9 w-20" />
|
||||
<Skeleton className="h-9 w-20" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<Skeleton className="h-28 w-full" />
|
||||
<Skeleton className="h-28 w-full" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Skeleton className="h-10 w-80 mb-4" />
|
||||
<Skeleton className="h-64 w-full" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default async function CandidateDetailsPage({
|
||||
params,
|
||||
}: CandidateDetailsPageProps) {
|
||||
// Ensure user is authenticated
|
||||
await requireAuth();
|
||||
|
||||
const candidateData = await getCandidateById(params.id);
|
||||
|
||||
if (!candidateData) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
if (!dateString) return "N/A";
|
||||
try {
|
||||
return format(new Date(dateString), "PPP");
|
||||
} catch (error) {
|
||||
return "Invalid date";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<MainLayout>
|
||||
<div className="flex flex-col space-y-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<Link href="/candidates">
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
Back to Candidates
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Suspense fallback={<CandidateDetailsSkeleton />}>
|
||||
<div className="flex flex-col space-y-6">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">{candidateData.name}</h1>
|
||||
<p className="text-muted-foreground">
|
||||
{candidateData.technology || "Technology not specified"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<Link href={`/candidates/${params.id}/edit`}>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
Edit
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<DeleteCandidateButton id={params.id} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Contact Information</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Mail className="h-4 w-4 text-muted-foreground" />
|
||||
<span>{candidateData.email}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Phone className="h-4 w-4 text-muted-foreground" />
|
||||
<span>{candidateData.phone || "No phone number"}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<MapPin className="h-4 w-4 text-muted-foreground" />
|
||||
<span>{candidateData.location || "No location specified"}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Candidate Details</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p className="text-sm font-medium">Visa Status</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{candidateData.visaStatus || "Not specified"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">Current Status</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{candidateData.currentStatus || "Not specified"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">Experience</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{candidateData.experience || "Not specified"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">Open to Relocate</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{candidateData.openToRelocate ? "Yes" : "No"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="interviews" className="w-full">
|
||||
<TabsList>
|
||||
<TabsTrigger value="interviews">Interviews</TabsTrigger>
|
||||
<TabsTrigger value="assessments">Assessments</TabsTrigger>
|
||||
<TabsTrigger value="notes">Notes</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="interviews" className="pt-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Interview History</CardTitle>
|
||||
<CardDescription>
|
||||
All interviews scheduled for this candidate
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{candidateData.interviewing && candidateData.interviewing.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{candidateData.interviewing.map((interview: any, index: number) => (
|
||||
<div key={index} className="border rounded-md p-4">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h4 className="font-medium">{interview.company}</h4>
|
||||
<p className="text-sm text-muted-foreground">{interview.position}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Calendar className="h-4 w-4" />
|
||||
{formatDate(interview.date)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<span className="text-xs bg-muted px-2 py-1 rounded-md">
|
||||
{interview.status}
|
||||
</span>
|
||||
</div>
|
||||
{interview.feedback && (
|
||||
<div className="mt-4 border-t pt-2">
|
||||
<p className="text-sm font-medium">Feedback:</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{interview.feedback}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-muted-foreground">No interviews scheduled yet.</p>
|
||||
)}
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button size="sm" variant="outline" asChild>
|
||||
<Link href={`/candidates/${params.id}/interviews/new`}>
|
||||
Add Interview
|
||||
</Link>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="assessments" className="pt-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Assessment Results</CardTitle>
|
||||
<CardDescription>
|
||||
Technical and skill assessments
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{candidateData.assessment && candidateData.assessment.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{candidateData.assessment.map((assessment: any, index: number) => (
|
||||
<div key={index} className="border rounded-md p-4">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h4 className="font-medium">{assessment.type} Assessment</h4>
|
||||
<p className="text-sm">Score: {assessment.score || "Not scored"}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Calendar className="h-4 w-4" />
|
||||
{formatDate(assessment.date)}
|
||||
</div>
|
||||
</div>
|
||||
{assessment.notes && (
|
||||
<div className="mt-4 border-t pt-2">
|
||||
<p className="text-sm font-medium">Notes:</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{assessment.notes}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-muted-foreground">No assessments yet.</p>
|
||||
)}
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button size="sm" variant="outline" asChild>
|
||||
<Link href={`/candidates/${params.id}/assessments/new`}>
|
||||
Add Assessment
|
||||
</Link>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="notes" className="pt-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Additional Notes</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{candidateData.notes ? (
|
||||
<div className="whitespace-pre-wrap">{candidateData.notes}</div>
|
||||
) : (
|
||||
<p className="text-muted-foreground">No additional notes.</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</Suspense>
|
||||
</div>
|
||||
</MainLayout>
|
||||
);
|
||||
}
|
||||
61
src/app/candidates/new/page.tsx
Normal file
61
src/app/candidates/new/page.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { Suspense } from "react";
|
||||
import { Metadata } from "next";
|
||||
import MainLayout from "@/components/layout/MainLayout";
|
||||
import { CandidateForm } from "@/components/candidates/CandidateForm";
|
||||
import { requireAuth } from "@/lib/auth";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Add New Candidate - Candidate Portal",
|
||||
description: "Add a new candidate to the system",
|
||||
};
|
||||
|
||||
function FormSkeleton() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-6 w-1/3" />
|
||||
<Skeleton className="h-4 w-1/2" />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<div key={i} className="space-y-2">
|
||||
<Skeleton className="h-4 w-20" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Skeleton className="h-32 w-full" />
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Skeleton className="h-10 w-24" />
|
||||
<Skeleton className="h-10 w-32" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default async function NewCandidatePage() {
|
||||
// Ensure user is authenticated
|
||||
await requireAuth();
|
||||
|
||||
return (
|
||||
<MainLayout>
|
||||
<div className="flex flex-col space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Add New Candidate</CardTitle>
|
||||
<CardDescription>
|
||||
Fill in the details to add a new candidate to the system
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Suspense fallback={<FormSkeleton />}>
|
||||
<CandidateForm />
|
||||
</Suspense>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</MainLayout>
|
||||
);
|
||||
}
|
||||
599
src/app/candidates/page.tsx
Normal file
599
src/app/candidates/page.tsx
Normal file
@@ -0,0 +1,599 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import MainLayout from "@/components/layout/MainLayout";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle,} from "@/components/ui/card";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow,} from "@/components/ui/table";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from "@/components/ui/select";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { format } from "date-fns";
|
||||
import { toast } from "sonner";
|
||||
import { ChevronLeft, ChevronRight, Download, Filter, PlusCircle, Search, SlidersHorizontal,} from "lucide-react";
|
||||
|
||||
interface Candidate {
|
||||
_id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
technology: string;
|
||||
visaStatus: string;
|
||||
location: string;
|
||||
openToRelocate: boolean;
|
||||
experience: string;
|
||||
currentStatus: string;
|
||||
status: string;
|
||||
marketingStartDate: string;
|
||||
dayRecruiter: string;
|
||||
nightRecruiter: string;
|
||||
}
|
||||
|
||||
export default function CandidatesPage() {
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [candidates, setCandidates] = useState<Candidate[]>([]);
|
||||
const [pagination, setPagination] = useState({
|
||||
currentPage: 1,
|
||||
totalPages: 1,
|
||||
totalItems: 0,
|
||||
itemsPerPage: 10,
|
||||
});
|
||||
const [filters, setFilters] = useState({
|
||||
status: "",
|
||||
technology: "",
|
||||
visaStatus: "",
|
||||
openToRelocate: null as boolean | null,
|
||||
});
|
||||
const [showFilters, setShowFilters] = useState(false);
|
||||
|
||||
const fetchCandidates = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// Build the query string
|
||||
const queryParams = new URLSearchParams();
|
||||
queryParams.append("page", pagination.currentPage.toString());
|
||||
queryParams.append("limit", pagination.itemsPerPage.toString());
|
||||
|
||||
if (searchQuery) {
|
||||
queryParams.append("search", searchQuery);
|
||||
}
|
||||
|
||||
if (filters.status) {
|
||||
queryParams.append("status", filters.status);
|
||||
}
|
||||
|
||||
if (filters.technology) {
|
||||
queryParams.append("technology", filters.technology);
|
||||
}
|
||||
|
||||
if (filters.visaStatus) {
|
||||
queryParams.append("visaStatus", filters.visaStatus);
|
||||
}
|
||||
|
||||
if (filters.openToRelocate !== null) {
|
||||
queryParams.append("openToRelocate", filters.openToRelocate.toString());
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/candidates?${queryParams.toString()}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch candidates");
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('frontend response', data)
|
||||
// For this demo, we'll use sample data since we can't connect to a real DB
|
||||
// In a real app, we would use data.candidates and data.pagination
|
||||
const sampleCandidates: Candidate[] = [
|
||||
{
|
||||
_id: "1",
|
||||
name: "John Doe",
|
||||
email: "john.doe@example.com",
|
||||
phone: "123-456-7890",
|
||||
technology: "React, Node.js",
|
||||
visaStatus: "H1B",
|
||||
location: "New York, NY",
|
||||
openToRelocate: true,
|
||||
experience: "5 years",
|
||||
currentStatus: "In marketing",
|
||||
status: "Active",
|
||||
marketingStartDate: "2023-04-15",
|
||||
dayRecruiter: "Jane Smith",
|
||||
nightRecruiter: "Mike Johnson",
|
||||
},
|
||||
{
|
||||
_id: "2",
|
||||
name: "Jane Smith",
|
||||
email: "jane.smith@example.com",
|
||||
phone: "234-567-8901",
|
||||
technology: "Angular, Python",
|
||||
visaStatus: "Green Card",
|
||||
location: "San Francisco, CA",
|
||||
openToRelocate: false,
|
||||
experience: "3 years",
|
||||
currentStatus: "In marketing",
|
||||
status: "Active",
|
||||
marketingStartDate: "2023-04-10",
|
||||
dayRecruiter: "Bob Williams",
|
||||
nightRecruiter: "Alice Brown",
|
||||
},
|
||||
{
|
||||
_id: "3",
|
||||
name: "Mike Johnson",
|
||||
email: "mike.johnson@example.com",
|
||||
phone: "345-678-9012",
|
||||
technology: "Java, Spring",
|
||||
visaStatus: "OPT",
|
||||
location: "Austin, TX",
|
||||
openToRelocate: true,
|
||||
experience: "2 years",
|
||||
currentStatus: "In marketing",
|
||||
status: "Active",
|
||||
marketingStartDate: "2023-04-05",
|
||||
dayRecruiter: "Jane Smith",
|
||||
nightRecruiter: "Bob Williams",
|
||||
},
|
||||
{
|
||||
_id: "4",
|
||||
name: "Sarah Williams",
|
||||
email: "sarah.williams@example.com",
|
||||
phone: "456-789-0123",
|
||||
technology: "Python, Django",
|
||||
visaStatus: "H1B",
|
||||
location: "Chicago, IL",
|
||||
openToRelocate: true,
|
||||
experience: "4 years",
|
||||
currentStatus: "Hold",
|
||||
status: "Hold",
|
||||
marketingStartDate: "2023-03-25",
|
||||
dayRecruiter: "Mike Johnson",
|
||||
nightRecruiter: "Jane Smith",
|
||||
},
|
||||
{
|
||||
_id: "5",
|
||||
name: "David Brown",
|
||||
email: "david.brown@example.com",
|
||||
phone: "567-890-1234",
|
||||
technology: "React, Redux, TypeScript",
|
||||
visaStatus: "Citizen",
|
||||
location: "Seattle, WA",
|
||||
openToRelocate: false,
|
||||
experience: "6 years",
|
||||
currentStatus: "Placed",
|
||||
status: "Inactive",
|
||||
marketingStartDate: "2023-03-15",
|
||||
dayRecruiter: "Alice Brown",
|
||||
nightRecruiter: "Bob Williams",
|
||||
},
|
||||
];
|
||||
|
||||
// Filter samples based on our filters
|
||||
let filteredCandidates = sampleCandidates;
|
||||
|
||||
if (searchQuery) {
|
||||
const query = searchQuery.toLowerCase();
|
||||
filteredCandidates = filteredCandidates.filter(
|
||||
(candidate) =>
|
||||
candidate.name.toLowerCase().includes(query) ||
|
||||
candidate.email.toLowerCase().includes(query) ||
|
||||
candidate.technology.toLowerCase().includes(query)
|
||||
);
|
||||
}
|
||||
|
||||
if (filters.status) {
|
||||
filteredCandidates = filteredCandidates.filter(
|
||||
(candidate) => candidate.status === filters.status
|
||||
);
|
||||
}
|
||||
|
||||
if (filters.technology) {
|
||||
filteredCandidates = filteredCandidates.filter(
|
||||
(candidate) => candidate.technology.toLowerCase().includes(filters.technology.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
if (filters.visaStatus) {
|
||||
filteredCandidates = filteredCandidates.filter(
|
||||
(candidate) => candidate.visaStatus === filters.visaStatus
|
||||
);
|
||||
}
|
||||
|
||||
if (filters.openToRelocate !== null) {
|
||||
filteredCandidates = filteredCandidates.filter(
|
||||
(candidate) => candidate.openToRelocate === filters.openToRelocate
|
||||
);
|
||||
}
|
||||
|
||||
setCandidates(filteredCandidates);
|
||||
setPagination({
|
||||
currentPage: 1,
|
||||
totalPages: 1,
|
||||
totalItems: filteredCandidates.length,
|
||||
itemsPerPage: 10,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching candidates:", error);
|
||||
toast.error("Failed to load candidates");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [filters, pagination.currentPage, pagination.itemsPerPage, searchQuery]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCandidates();
|
||||
}, [fetchCandidates]);
|
||||
|
||||
const handlePageChange = (newPage: number) => {
|
||||
if (newPage > 0 && newPage <= pagination.totalPages) {
|
||||
setPagination((prev) => ({
|
||||
...prev,
|
||||
currentPage: newPage,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const handleExportCSV = () => {
|
||||
// Simple CSV export
|
||||
const headers = [
|
||||
"Name",
|
||||
"Email",
|
||||
"Phone",
|
||||
"Technology",
|
||||
"Visa Status",
|
||||
"Location",
|
||||
"Open To Relocate",
|
||||
"Experience",
|
||||
"Current Status",
|
||||
"Status",
|
||||
].join(",");
|
||||
|
||||
const csvRows = candidates.map((candidate) => {
|
||||
return [
|
||||
candidate.name,
|
||||
candidate.email,
|
||||
candidate.phone,
|
||||
candidate.technology,
|
||||
candidate.visaStatus,
|
||||
candidate.location,
|
||||
candidate.openToRelocate ? "Yes" : "No",
|
||||
candidate.experience,
|
||||
candidate.currentStatus,
|
||||
candidate.status,
|
||||
]
|
||||
.map((value) => `"${value}"`)
|
||||
.join(",");
|
||||
});
|
||||
|
||||
const csvContent = [headers, ...csvRows].join("\n");
|
||||
|
||||
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.setAttribute("href", url);
|
||||
link.setAttribute("download", "candidates.csv");
|
||||
link.style.visibility = "hidden";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
toast.success("Exported candidates to CSV successfully");
|
||||
};
|
||||
|
||||
const resetFilters = () => {
|
||||
setFilters({
|
||||
status: "",
|
||||
technology: "",
|
||||
visaStatus: "",
|
||||
openToRelocate: null,
|
||||
});
|
||||
setSearchQuery("");
|
||||
};
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
if (!dateStr) return "N/A";
|
||||
try {
|
||||
return format(new Date(dateStr), "MMM dd, yyyy");
|
||||
} catch (error) {
|
||||
return "Invalid date";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<MainLayout>
|
||||
<div className="flex flex-col space-y-6">
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">Candidates</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Manage and track all candidates in the system
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex items-center gap-2"
|
||||
onClick={handleExportCSV}
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
Export CSV
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => router.push("/candidates/new")}
|
||||
>
|
||||
<PlusCircle className="h-4 w-4" />
|
||||
Add Candidate
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<div>
|
||||
<CardTitle>Candidate List</CardTitle>
|
||||
<CardDescription>
|
||||
{pagination.totalItems} candidates found
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative w-full md:w-64">
|
||||
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search candidates..."
|
||||
className="pl-8"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setShowFilters(!showFilters)}
|
||||
>
|
||||
<Filter className="h-4 w-4 mr-2" />
|
||||
Filters
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
{showFilters && (
|
||||
<div className="px-6 py-3 border-b">
|
||||
<div className="text-sm font-medium mb-3">Filter Candidates</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm">Status</label>
|
||||
<Select
|
||||
value={filters.status}
|
||||
onValueChange={(value) =>
|
||||
setFilters({ ...filters, status: value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="All Statuses" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="">All Statuses</SelectItem>
|
||||
<SelectItem value="Active">Active</SelectItem>
|
||||
<SelectItem value="Hold">Hold</SelectItem>
|
||||
<SelectItem value="Inactive">Inactive</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm">Technology</label>
|
||||
<Input
|
||||
placeholder="e.g. React"
|
||||
value={filters.technology}
|
||||
onChange={(e) =>
|
||||
setFilters({ ...filters, technology: e.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm">Visa Status</label>
|
||||
<Select
|
||||
value={filters.visaStatus}
|
||||
onValueChange={(value) =>
|
||||
setFilters({ ...filters, visaStatus: value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="All Visa Statuses" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="">All Visa Statuses</SelectItem>
|
||||
<SelectItem value="H1B">H1B</SelectItem>
|
||||
<SelectItem value="Green Card">Green Card</SelectItem>
|
||||
<SelectItem value="OPT">OPT</SelectItem>
|
||||
<SelectItem value="CPT">CPT</SelectItem>
|
||||
<SelectItem value="Citizen">Citizen</SelectItem>
|
||||
<SelectItem value="Other">Other</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm">Open to Relocate</label>
|
||||
<Select
|
||||
value={
|
||||
filters.openToRelocate === null
|
||||
? ""
|
||||
: filters.openToRelocate
|
||||
? "true"
|
||||
: "false"
|
||||
}
|
||||
onValueChange={(value) =>
|
||||
setFilters({
|
||||
...filters,
|
||||
openToRelocate:
|
||||
value === ""
|
||||
? null
|
||||
: value === "true"
|
||||
? true
|
||||
: false,
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Any" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="">Any</SelectItem>
|
||||
<SelectItem value="true">Yes</SelectItem>
|
||||
<SelectItem value="false">No</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-2 mt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={resetFilters}
|
||||
>
|
||||
Reset Filters
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
fetchCandidates();
|
||||
setShowFilters(false);
|
||||
}}
|
||||
>
|
||||
Apply Filters
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CardContent className="p-0">
|
||||
<div className="relative w-full overflow-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Email</TableHead>
|
||||
<TableHead>Technology</TableHead>
|
||||
<TableHead>Visa Status</TableHead>
|
||||
<TableHead>Location</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Marketing Date</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
Array.from({ length: 5 }).map((_, i) => (
|
||||
<TableRow key={i}>
|
||||
<TableCell colSpan={8}>
|
||||
<Skeleton className="h-6 w-full" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : candidates.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} className="text-center py-10">
|
||||
No candidates found
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
candidates.map((candidate) => (
|
||||
<TableRow key={candidate._id}>
|
||||
<TableCell className="font-medium">
|
||||
<Link
|
||||
href={`/candidates/${candidate._id}`}
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
{candidate.name}
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell>{candidate.email}</TableCell>
|
||||
<TableCell>{candidate.technology}</TableCell>
|
||||
<TableCell>{candidate.visaStatus}</TableCell>
|
||||
<TableCell>{candidate.location}</TableCell>
|
||||
<TableCell>
|
||||
<span
|
||||
className={`px-2 py-1 rounded-full text-xs ${
|
||||
candidate.status === "Active"
|
||||
? "bg-green-100 text-green-800"
|
||||
: candidate.status === "Hold"
|
||||
? "bg-yellow-100 text-yellow-800"
|
||||
: "bg-gray-100 text-gray-800"
|
||||
}`}
|
||||
>
|
||||
{candidate.status}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{formatDate(candidate.marketingStartDate)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => router.push(`/candidates/${candidate._id}`)}
|
||||
>
|
||||
View
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => router.push(`/candidates/${candidate._id}/edit`)}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
{pagination.totalPages > 1 && (
|
||||
<div className="flex items-center justify-between px-6 py-4 border-t">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Showing {candidates.length} of {pagination.totalItems} results
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handlePageChange(pagination.currentPage - 1)}
|
||||
disabled={pagination.currentPage === 1}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
Previous
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handlePageChange(pagination.currentPage + 1)}
|
||||
disabled={pagination.currentPage === pagination.totalPages}
|
||||
>
|
||||
Next
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
</MainLayout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user