feat(student): added eligible and non eligible tabs
This commit is contained in:
@@ -43,28 +43,33 @@ export type Job = InferSelectModel<typeof jobs> & {
|
|||||||
export type Resume = typeof resumes.$inferSelect;
|
export type Resume = typeof resumes.$inferSelect;
|
||||||
|
|
||||||
export default function JobsPage({
|
export default function JobsPage({
|
||||||
jobs,
|
eligibleJobs,
|
||||||
|
ineligibleJobs,
|
||||||
resumes,
|
resumes,
|
||||||
studentId,
|
studentId,
|
||||||
appliedJobIds = [],
|
appliedJobIds = [],
|
||||||
}: {
|
}: {
|
||||||
jobs: Job[];
|
eligibleJobs: Job[];
|
||||||
|
ineligibleJobs: Job[];
|
||||||
resumes: Resume[];
|
resumes: Resume[];
|
||||||
studentId: number;
|
studentId: number;
|
||||||
appliedJobIds?: number[];
|
appliedJobIds?: number[];
|
||||||
}) {
|
}) {
|
||||||
const [filteredJobs, setFilteredJobs] = useState<Job[]>([]);
|
const [filteredJobs, setFilteredJobs] = useState<Job[]>([]);
|
||||||
|
const [activeTab, setActiveTab] = useState<'eligible' | 'ineligible'>('eligible');
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [locationFilter, setLocationFilter] = useState('all');
|
const [locationFilter, setLocationFilter] = useState('all');
|
||||||
const [jobTypeFilter, setJobTypeFilter] = useState('all');
|
const [jobTypeFilter, setJobTypeFilter] = useState('all');
|
||||||
const [showLoadMore, setShowLoadMore] = useState(false);
|
const [showLoadMore, setShowLoadMore] = useState(false);
|
||||||
|
const allJobs = [...eligibleJobs, ...ineligibleJobs];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
filterJobs();
|
filterJobs();
|
||||||
}, [jobs, searchTerm, locationFilter, jobTypeFilter]);
|
}, [eligibleJobs, ineligibleJobs, activeTab, searchTerm, locationFilter, jobTypeFilter]);
|
||||||
|
|
||||||
const filterJobs = () => {
|
const filterJobs = () => {
|
||||||
let filtered = [...jobs];
|
let base = activeTab === 'eligible' ? eligibleJobs : ineligibleJobs;
|
||||||
|
let filtered = [...base];
|
||||||
|
|
||||||
// Search filter
|
// Search filter
|
||||||
if (searchTerm) {
|
if (searchTerm) {
|
||||||
@@ -180,7 +185,7 @@ export default function JobsPage({
|
|||||||
Clear Filters
|
Clear Filters
|
||||||
</Button>
|
</Button>
|
||||||
<span className="text-sm text-gray-500">
|
<span className="text-sm text-gray-500">
|
||||||
{filteredJobs.length} of {jobs.length} jobs
|
{filteredJobs.length} of {allJobs.length} jobs
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -194,7 +199,7 @@ export default function JobsPage({
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-gray-600">Total Jobs</p>
|
<p className="text-sm font-medium text-gray-600">Total Jobs</p>
|
||||||
<p className="text-3xl font-bold text-gray-800">{jobs.length}</p>
|
<p className="text-3xl font-bold text-gray-800">{allJobs.length}</p>
|
||||||
</div>
|
</div>
|
||||||
<Briefcase className="w-8 h-8 text-blue-600" />
|
<Briefcase className="w-8 h-8 text-blue-600" />
|
||||||
</div>
|
</div>
|
||||||
@@ -207,7 +212,7 @@ export default function JobsPage({
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-gray-600">Active Companies</p>
|
<p className="text-sm font-medium text-gray-600">Active Companies</p>
|
||||||
<p className="text-3xl font-bold text-gray-800">
|
<p className="text-3xl font-bold text-gray-800">
|
||||||
{new Set(jobs.map((job) => job.company.name)).size}
|
{new Set(allJobs.map((job) => job.company.name)).size}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Building2 className="w-8 h-8 text-green-600" />
|
<Building2 className="w-8 h-8 text-green-600" />
|
||||||
@@ -221,7 +226,7 @@ export default function JobsPage({
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-gray-600">Remote Jobs</p>
|
<p className="text-sm font-medium text-gray-600">Remote Jobs</p>
|
||||||
<p className="text-3xl font-bold text-gray-800">
|
<p className="text-3xl font-bold text-gray-800">
|
||||||
{jobs.filter((job) => job.location.toLowerCase().includes('remote')).length}
|
{allJobs.filter((job) => job.location.toLowerCase().includes('remote')).length}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Users className="w-8 h-8 text-purple-600" />
|
<Users className="w-8 h-8 text-purple-600" />
|
||||||
@@ -242,6 +247,42 @@ export default function JobsPage({
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Tabs */}
|
||||||
|
<Card className="bg-white shadow-sm mb-6">
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="text-center mb-3">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-800">Select a category to view jobs</h2>
|
||||||
|
<p className="text-sm text-gray-500">Switch between eligible and not eligible jobs based on your profile</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<div className="inline-flex rounded-full border border-gray-200 bg-gray-50 p-1">
|
||||||
|
<Button
|
||||||
|
variant={activeTab === 'eligible' ? 'default' : 'ghost'}
|
||||||
|
size="sm"
|
||||||
|
className={`rounded-full px-5 ${activeTab === 'eligible' ? '' : 'text-gray-700'}`}
|
||||||
|
onClick={() => setActiveTab('eligible')}
|
||||||
|
>
|
||||||
|
Eligible
|
||||||
|
<span className={`ml-2 inline-flex items-center justify-center rounded-full text-xs px-2 py-0.5 ${activeTab === 'eligible' ? 'bg-white text-gray-900' : 'bg-gray-200 text-gray-700'}`}>
|
||||||
|
{eligibleJobs.length}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={activeTab === 'ineligible' ? 'default' : 'ghost'}
|
||||||
|
size="sm"
|
||||||
|
className={`rounded-full px-5 ${activeTab === 'ineligible' ? '' : 'text-gray-700'}`}
|
||||||
|
onClick={() => setActiveTab('ineligible')}
|
||||||
|
>
|
||||||
|
Not Eligible
|
||||||
|
<span className={`ml-2 inline-flex items-center justify-center rounded-full text-xs px-2 py-0.5 ${activeTab === 'ineligible' ? 'bg-white text-gray-900' : 'bg-gray-200 text-gray-700'}`}>
|
||||||
|
{ineligibleJobs.length}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
{/* Jobs Grid */}
|
{/* Jobs Grid */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
{displayedJobs.map((job) => (
|
{displayedJobs.map((job) => (
|
||||||
@@ -298,13 +339,24 @@ export default function JobsPage({
|
|||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
|
{activeTab === 'eligible' ? (
|
||||||
<JobApplicationModal
|
<JobApplicationModal
|
||||||
job={{ ...job, minCGPA: Number(job.minCGPA) }}
|
job={{ ...job, minCGPA: Number(job.minCGPA) }}
|
||||||
studentId={studentId} // Mock student ID - in real app this would come from auth
|
studentId={studentId}
|
||||||
resumes={resumes}
|
resumes={resumes}
|
||||||
|
isApplied={appliedJobIds.includes(job.id)}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<Button size="sm" variant="outline" disabled>
|
||||||
|
Not Eligible
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{job.link && (
|
{job.link && (
|
||||||
<Button size="sm" variant="outline">
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => window.open(job.link as string, '_blank')}
|
||||||
|
>
|
||||||
<ExternalLink className="w-4 h-4 mr-2" />
|
<ExternalLink className="w-4 h-4 mr-2" />
|
||||||
View Details
|
View Details
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import JobsClient from './JobClient';
|
import JobsClient from './JobClient';
|
||||||
import { auth } from '@/auth';
|
import { auth } from '@/auth';
|
||||||
import { db, resumes } from '@workspace/db';
|
import { db, resumes, students } from '@workspace/db';
|
||||||
import { eq } from '@workspace/db/drizzle';
|
import { eq } from '@workspace/db/drizzle';
|
||||||
import { getStudentApplicationJobIds } from '../actions';
|
import { getStudentApplicationJobIds } from '../actions';
|
||||||
|
|
||||||
@@ -18,5 +18,49 @@ export default async function JobsPage() {
|
|||||||
const { success, appliedJobIds } = await getStudentApplicationJobIds(studentId);
|
const { success, appliedJobIds } = await getStudentApplicationJobIds(studentId);
|
||||||
const studentAppliedJobIds = success ? appliedJobIds : [];
|
const studentAppliedJobIds = success ? appliedJobIds : [];
|
||||||
|
|
||||||
return <JobsClient jobs={jobs} resumes={reusmes} studentId={studentId} appliedJobIds={studentAppliedJobIds} />;
|
// Fetch student with grades for eligibility computation
|
||||||
|
const student = await db.query.students.findFirst({
|
||||||
|
where: eq(students.id, studentId),
|
||||||
|
with: { grades: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const studentSSC = Number((student as any)?.ssc ?? 0);
|
||||||
|
const studentHSC = Number((student as any)?.hsc ?? 0);
|
||||||
|
const grades = (student?.grades || []).map((g) => ({
|
||||||
|
sem: g.sem,
|
||||||
|
sgpi: Number(g.sgpi),
|
||||||
|
isKT: Boolean(g.isKT),
|
||||||
|
deadKT: Boolean(g.deadKT),
|
||||||
|
}));
|
||||||
|
const hasLiveKT = grades.some((g) => g.isKT);
|
||||||
|
const hasDeadKT = grades.some((g) => g.deadKT);
|
||||||
|
const avgCGPA = grades.length > 0 ? Number((grades.reduce((a, b) => a + (b.sgpi || 0), 0) / grades.length).toFixed(2)) : 0;
|
||||||
|
|
||||||
|
const isEligible = (job: typeof jobs[number]) => {
|
||||||
|
const minCGPA = Number(job.minCGPA || 0);
|
||||||
|
const minSSC = Number(job.minSSC || 0);
|
||||||
|
const minHSC = Number(job.minHSC || 0);
|
||||||
|
const allowDeadKT = Boolean(job.allowDeadKT);
|
||||||
|
const allowLiveKT = Boolean(job.allowLiveKT);
|
||||||
|
|
||||||
|
if (avgCGPA < minCGPA) return false;
|
||||||
|
if (studentSSC < minSSC) return false;
|
||||||
|
if (studentHSC < minHSC) return false;
|
||||||
|
if (!allowLiveKT && hasLiveKT) return false;
|
||||||
|
if (!allowDeadKT && hasDeadKT) return false;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const eligibleJobs = jobs.filter(isEligible);
|
||||||
|
const ineligibleJobs = jobs.filter((j) => !isEligible(j));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<JobsClient
|
||||||
|
eligibleJobs={eligibleJobs as any}
|
||||||
|
ineligibleJobs={ineligibleJobs as any}
|
||||||
|
resumes={reusmes}
|
||||||
|
studentId={studentId}
|
||||||
|
appliedJobIds={studentAppliedJobIds}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,18 +69,30 @@ export default function JobApplicationModal({ job, studentId, resumes, isApplied
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isDeadlinePassed = new Date() > job.applicationDeadline;
|
const isDeadlinePassed = new Date() > new Date(job.applicationDeadline as any);
|
||||||
|
const cannotApplyReason = isApplied
|
||||||
|
? 'You have already applied to this job'
|
||||||
|
: resumes.length === 0
|
||||||
|
? 'No resumes found. Please upload a resume first.'
|
||||||
|
: isDeadlinePassed
|
||||||
|
? 'Application deadline has passed'
|
||||||
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
|
<div className="flex flex-col items-start">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
className="bg-blue-600 hover:bg-blue-700"
|
className="bg-blue-600 hover:bg-blue-700"
|
||||||
disabled={isDeadlinePassed}
|
disabled={Boolean(cannotApplyReason)}
|
||||||
>
|
>
|
||||||
Apply Now
|
{isApplied ? 'Applied' : 'Apply Now'}
|
||||||
</Button>
|
</Button>
|
||||||
|
{cannotApplyReason && (
|
||||||
|
<span className="mt-1 text-xs text-red-600">{cannotApplyReason}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@@ -180,7 +192,7 @@ export default function JobApplicationModal({ job, studentId, resumes, isApplied
|
|||||||
<div className="flex gap-3 pt-4">
|
<div className="flex gap-3 pt-4">
|
||||||
<Button
|
<Button
|
||||||
onClick={handleApply}
|
onClick={handleApply}
|
||||||
disabled={isApplying || resumes.length === 0}
|
disabled={isApplying || resumes.length === 0 || isApplied}
|
||||||
className="flex-1 bg-blue-600 hover:bg-blue-700"
|
className="flex-1 bg-blue-600 hover:bg-blue-700"
|
||||||
>
|
>
|
||||||
{isApplying ? 'Submitting...' : 'Submit Application'}
|
{isApplying ? 'Submitting...' : 'Submit Application'}
|
||||||
|
|||||||
Reference in New Issue
Block a user