This commit is contained in:
Om Lanke
2025-07-08 16:23:01 +05:30
parent e60e0cded7
commit 900b18da1a
3 changed files with 140 additions and 43 deletions

View File

@@ -0,0 +1,58 @@
'use client';
import { useState, useTransition } from 'react';
import {
Select,
SelectTrigger,
SelectContent,
SelectItem,
SelectValue,
} from '@workspace/ui/components/select';
const STATUS_OPTIONS = [
'in review',
'Online Assessment',
'Interview round',
'offer given',
'accepted',
'rejected',
];
interface StatusSelectProps {
applicationId: number;
initialStatus: string;
studentId: number;
}
export default function StatusSelect({
applicationId,
initialStatus,
studentId,
}: StatusSelectProps) {
const [status, setStatus] = useState(initialStatus);
const [isPending, startTransition] = useTransition();
const handleChange = (value: string) => {
setStatus(value); // Optimistic update
startTransition(async () => {
await fetch(`/api/applications/${applicationId}/status`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: value, studentId }),
});
});
};
return (
<Select value={status} onValueChange={handleChange} disabled={isPending}>
<SelectTrigger className="min-w-[160px]" />
<SelectContent>
{STATUS_OPTIONS.map((option) => (
<SelectItem key={option} value={option}>
{option}
</SelectItem>
))}
</SelectContent>
</Select>
);
}

View File

@@ -18,6 +18,7 @@ import {
Clock Clock
} from 'lucide-react'; } from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
import StatusSelect from './StatusSelect';
interface JobPageProps { interface JobPageProps {
params: { jobId: string }; params: { jobId: string };
@@ -33,7 +34,11 @@ export default async function JobDetailPage({ params }: JobPageProps) {
if (jobRes.length === 0 || !jobRes[0]) notFound(); if (jobRes.length === 0 || !jobRes[0]) notFound();
const job = jobRes[0]; const job = jobRes[0];
const companyRes = await db.select().from(companies).where(eq(companies.id, job.companyId)).limit(1); const companyRes = await db
.select()
.from(companies)
.where(eq(companies.id, job.companyId))
.limit(1);
const company = companyRes[0]; const company = companyRes[0];
const applicants = await db const applicants = await db
@@ -43,6 +48,7 @@ export default async function JobDetailPage({ params }: JobPageProps) {
firstName: students.firstName, firstName: students.firstName,
lastName: students.lastName, lastName: students.lastName,
email: students.email, email: students.email,
studentId: students.id,
}) })
.from(applications) .from(applications)
.leftJoin(students, eq(applications.studentId, students.id)) .leftJoin(students, eq(applications.studentId, students.id))
@@ -80,9 +86,9 @@ export default async function JobDetailPage({ params }: JobPageProps) {
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="w-16 h-16 rounded-xl bg-gray-100 flex items-center justify-center overflow-hidden"> <div className="w-16 h-16 rounded-xl bg-gray-100 flex items-center justify-center overflow-hidden">
{company?.imageURL ? ( {company?.imageURL ? (
<img <img
src={company.imageURL} src={company.imageURL}
alt={company.name} alt={company.name}
className="w-full h-full object-cover" className="w-full h-full object-cover"
/> />
) : ( ) : (
@@ -98,11 +104,11 @@ export default async function JobDetailPage({ params }: JobPageProps) {
</CardDescription> </CardDescription>
</div> </div>
</div> </div>
<Badge <Badge
variant={job.active ? "default" : "secondary"} variant={job.active ? 'default' : 'secondary'}
className={`${ className={`${
job.active job.active
? 'bg-green-100 text-green-700 hover:bg-green-200' ? 'bg-green-100 text-green-700 hover:bg-green-200'
: 'bg-red-100 text-red-700 hover:bg-red-200' : 'bg-red-100 text-red-700 hover:bg-red-200'
}`} }`}
> >
@@ -131,7 +137,9 @@ export default async function JobDetailPage({ params }: JobPageProps) {
<Calendar className="w-5 h-5 text-gray-500" /> <Calendar className="w-5 h-5 text-gray-500" />
<div> <div>
<p className="text-sm font-medium text-gray-700">Application Deadline</p> <p className="text-sm font-medium text-gray-700">Application Deadline</p>
<p className="text-sm text-gray-600">{job.applicationDeadline.toLocaleDateString()}</p> <p className="text-sm text-gray-600">
{job.applicationDeadline.toLocaleDateString()}
</p>
</div> </div>
</div> </div>
<div className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg"> <div className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg">
@@ -145,10 +153,10 @@ export default async function JobDetailPage({ params }: JobPageProps) {
{/* Job Link */} {/* Job Link */}
<div className="pt-4 border-t border-gray-100"> <div className="pt-4 border-t border-gray-100">
<a <a
href={job.link} href={job.link}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="inline-flex items-center gap-2 text-blue-700 hover:text-red-600 transition-colors font-medium" className="inline-flex items-center gap-2 text-blue-700 hover:text-red-600 transition-colors font-medium"
> >
<ExternalLink className="w-4 h-4" /> <ExternalLink className="w-4 h-4" />
@@ -199,22 +207,30 @@ export default async function JobDetailPage({ params }: JobPageProps) {
</div> </div>
</div> </div>
<div className="mt-4 flex gap-4"> <div className="mt-4 flex gap-4">
<div className={`flex items-center gap-2 px-3 py-2 rounded-lg ${ <div
job.allowDeadKT ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700' className={`flex items-center gap-2 px-3 py-2 rounded-lg ${
}`}> job.allowDeadKT ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'
<div className={`w-2 h-2 rounded-full ${ }`}
job.allowDeadKT ? 'bg-green-500' : 'bg-red-500' >
}`} /> <div
className={`w-2 h-2 rounded-full ${
job.allowDeadKT ? 'bg-green-500' : 'bg-red-500'
}`}
/>
<span className="text-sm font-medium"> <span className="text-sm font-medium">
Dead KT: {job.allowDeadKT ? 'Allowed' : 'Not Allowed'} Dead KT: {job.allowDeadKT ? 'Allowed' : 'Not Allowed'}
</span> </span>
</div> </div>
<div className={`flex items-center gap-2 px-3 py-2 rounded-lg ${ <div
job.allowLiveKT ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700' className={`flex items-center gap-2 px-3 py-2 rounded-lg ${
}`}> job.allowLiveKT ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'
<div className={`w-2 h-2 rounded-full ${ }`}
job.allowLiveKT ? 'bg-green-500' : 'bg-red-500' >
}`} /> <div
className={`w-2 h-2 rounded-full ${
job.allowLiveKT ? 'bg-green-500' : 'bg-red-500'
}`}
/>
<span className="text-sm font-medium"> <span className="text-sm font-medium">
Live KT: {job.allowLiveKT ? 'Allowed' : 'Not Allowed'} Live KT: {job.allowLiveKT ? 'Allowed' : 'Not Allowed'}
</span> </span>
@@ -238,9 +254,9 @@ export default async function JobDetailPage({ params }: JobPageProps) {
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-lg bg-gray-100 flex items-center justify-center overflow-hidden"> <div className="w-12 h-12 rounded-lg bg-gray-100 flex items-center justify-center overflow-hidden">
{company?.imageURL ? ( {company?.imageURL ? (
<img <img
src={company.imageURL} src={company.imageURL}
alt={company.name} alt={company.name}
className="w-full h-full object-cover" className="w-full h-full object-cover"
/> />
) : ( ) : (
@@ -277,7 +293,9 @@ export default async function JobDetailPage({ params }: JobPageProps) {
</div> </div>
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className="text-gray-600">Application Deadline</span> <span className="text-gray-600">Application Deadline</span>
<span className="font-medium">{job.applicationDeadline.toLocaleDateString()}</span> <span className="font-medium">
{job.applicationDeadline.toLocaleDateString()}
</span>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@@ -301,7 +319,9 @@ export default async function JobDetailPage({ params }: JobPageProps) {
<div className="text-center py-12"> <div className="text-center py-12">
<Users className="w-12 h-12 text-gray-400 mx-auto mb-4" /> <Users className="w-12 h-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">No applications yet</h3> <h3 className="text-lg font-medium text-gray-900 mb-2">No applications yet</h3>
<p className="text-gray-600">Students will appear here once they apply for this job.</p> <p className="text-gray-600">
Students will appear here once they apply for this job.
</p>
</div> </div>
) : ( ) : (
<div className="rounded-lg border border-gray-200 overflow-hidden"> <div className="rounded-lg border border-gray-200 overflow-hidden">
@@ -317,22 +337,16 @@ export default async function JobDetailPage({ params }: JobPageProps) {
{applicants.map((applicant) => ( {applicants.map((applicant) => (
<TableRow key={applicant.applicationId} className="hover:bg-gray-50"> <TableRow key={applicant.applicationId} className="hover:bg-gray-50">
<TableCell className="font-medium"> <TableCell className="font-medium">
{`${applicant.firstName ?? ''} ${applicant.lastName ?? ''}`.trim() || 'Unknown'} {`${applicant.firstName ?? ''} ${applicant.lastName ?? ''}`.trim() ||
'Unknown'}
</TableCell> </TableCell>
<TableCell className="text-gray-600">{applicant.email}</TableCell> <TableCell className="text-gray-600">{applicant.email}</TableCell>
<TableCell> <TableCell>
<Badge <StatusSelect
variant="outline" applicationId={applicant.applicationId}
className={`${ initialStatus={applicant.status}
applicant.status === 'approved' studentId={applicant.studentId ?? 0}
? 'border-green-200 text-green-700 bg-green-50' />
: applicant.status === 'rejected'
? 'border-red-200 text-red-700 bg-red-50'
: 'border-yellow-200 text-yellow-700 bg-yellow-50'
}`}
>
{applicant.status.charAt(0).toUpperCase() + applicant.status.slice(1)}
</Badge>
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}

View File

@@ -0,0 +1,25 @@
import { db, applications } from '@workspace/db';
import { eq } from '@workspace/db/drizzle';
import { NextRequest, NextResponse } from 'next/server';
export async function PATCH(req: NextRequest, { params }: { params: { applicationId: string } }) {
const applicationId = Number(params.applicationId);
if (isNaN(applicationId)) {
return NextResponse.json({ error: 'Invalid applicationId' }, { status: 400 });
}
const { status } = await req.json();
if (!status) {
return NextResponse.json({ error: 'Missing status' }, { status: 400 });
}
const result = await db.update(applications)
.set({ status })
.where(eq(applications.id, applicationId));
if (result.rowCount === 0) {
return NextResponse.json({ error: 'Application not found' }, { status: 404 });
}
return NextResponse.json({ success: true });
}