bhai
This commit is contained in:
58
apps/admin/app/(main)/jobs/[jobId]/StatusSelect.tsx
Normal file
58
apps/admin/app/(main)/jobs/[jobId]/StatusSelect.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
@@ -99,7 +105,7 @@ export default async function JobDetailPage({ params }: JobPageProps) {
|
|||||||
</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'
|
||||||
@@ -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">
|
||||||
@@ -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
|
||||||
|
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'
|
job.allowDeadKT ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'
|
||||||
}`}>
|
}`}
|
||||||
<div className={`w-2 h-2 rounded-full ${
|
>
|
||||||
|
<div
|
||||||
|
className={`w-2 h-2 rounded-full ${
|
||||||
job.allowDeadKT ? 'bg-green-500' : 'bg-red-500'
|
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
|
||||||
|
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'
|
job.allowLiveKT ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'
|
||||||
}`}>
|
}`}
|
||||||
<div className={`w-2 h-2 rounded-full ${
|
>
|
||||||
|
<div
|
||||||
|
className={`w-2 h-2 rounded-full ${
|
||||||
job.allowLiveKT ? 'bg-green-500' : 'bg-red-500'
|
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>
|
||||||
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user