diff --git a/apps/admin/app/(main)/jobs/[jobId]/ApplicationsTable.tsx b/apps/admin/app/(main)/jobs/[jobId]/ApplicationsTable.tsx new file mode 100644 index 0000000..277a8b4 --- /dev/null +++ b/apps/admin/app/(main)/jobs/[jobId]/ApplicationsTable.tsx @@ -0,0 +1,168 @@ +'use client'; + +import { useMemo, useState, useTransition } from 'react'; +import { Table, TableBody, TableHead, TableHeader, TableRow, TableCell } from '@workspace/ui/components/table'; +import { Input } from '@workspace/ui/components/input'; +import { Button } from '@workspace/ui/components/button'; +import { + Select, + SelectTrigger, + SelectContent, + SelectItem, + SelectValue, +} from '@workspace/ui/components/select'; +import StatusSelect from './StatusSelect'; + +type Applicant = { + applicationId: number; + status: string; + firstName: string | null; + lastName: string | null; + email: string | null; + studentId: number | null; +}; + +const STATUS_OPTIONS = [ + 'in review', + 'Online Assessment', + 'Interview round', + 'offer given', + 'accepted', + 'rejected', +]; + +export default function ApplicationsTable({ applicants }: { applicants: Applicant[] }) { + const [query, setQuery] = useState(''); + const [selected, setSelected] = useState([]); + const [bulkStatus, setBulkStatus] = useState(''); + const [rows, setRows] = useState(applicants); + const [isPending, startTransition] = useTransition(); + + const filtered = useMemo(() => { + if (!query.trim()) return rows; + const q = query.toLowerCase(); + return rows.filter((a) => { + const name = `${a.firstName ?? ''} ${a.lastName ?? ''}`.toLowerCase(); + const email = (a.email ?? '').toLowerCase(); + return name.includes(q) || email.includes(q); + }); + }, [rows, query]); + + const toggleAll = (checked: boolean) => { + setSelected(checked ? filtered.map((a) => a.applicationId) : []); + }; + + const toggleOne = (id: number, checked: boolean) => { + setSelected((prev) => (checked ? [...new Set([...prev, id])] : prev.filter((x) => x !== id))); + }; + + const onBulkUpdate = () => { + if (!bulkStatus || selected.length === 0) return; + const targets = new Set(selected); + startTransition(async () => { + const updates = rows.filter((r) => targets.has(r.applicationId)); + for (const r of updates) { + await fetch(`/api/applications/${r.applicationId}/status`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ status: bulkStatus, studentId: r.studentId ?? 0 }), + }); + } + setRows((prev) => + prev.map((r) => (targets.has(r.applicationId) ? { ...r, status: bulkStatus } : r)), + ); + setSelected([]); + }); + }; + + const allSelected = filtered.length > 0 && selected.length === filtered.length; + const someSelected = selected.length > 0 && selected.length < filtered.length; + + return ( +
+
+
+ setQuery(e.target.value)} + className="w-72" + /> + {filtered.length} results +
+
+ + + {selected.length > 0 && ( + + )} +
+
+ +
+ + + + + { + if (el) el.indeterminate = someSelected; + }} + onChange={(e) => toggleAll(e.target.checked)} + /> + + Name + Email + Status + + + + {filtered.map((applicant) => ( + + + toggleOne(applicant.applicationId, e.target.checked)} + /> + + + {`${applicant.firstName ?? ''} ${applicant.lastName ?? ''}`.trim() || 'Unknown'} + + {applicant.email} + + + + + ))} + +
+
+
+ ); +} + + diff --git a/apps/admin/app/(main)/jobs/[jobId]/StatusSelect.tsx b/apps/admin/app/(main)/jobs/[jobId]/StatusSelect.tsx index 26d027b..5dc6659 100644 --- a/apps/admin/app/(main)/jobs/[jobId]/StatusSelect.tsx +++ b/apps/admin/app/(main)/jobs/[jobId]/StatusSelect.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState, useTransition } from 'react'; +import { useEffect, useState, useTransition } from 'react'; import { Select, SelectTrigger, @@ -32,6 +32,11 @@ export default function StatusSelect({ const [status, setStatus] = useState(initialStatus); const [isPending, startTransition] = useTransition(); + // Sync local state when parent updates the initialStatus (e.g., after bulk update) + useEffect(() => { + setStatus(initialStatus); + }, [initialStatus]); + const handleChange = (value: string) => { setStatus(value); // Optimistic update startTransition(async () => { diff --git a/apps/admin/app/(main)/jobs/[jobId]/page.tsx b/apps/admin/app/(main)/jobs/[jobId]/page.tsx index 5f1339f..a97fefc 100644 --- a/apps/admin/app/(main)/jobs/[jobId]/page.tsx +++ b/apps/admin/app/(main)/jobs/[jobId]/page.tsx @@ -19,6 +19,7 @@ import { } from 'lucide-react'; import Link from 'next/link'; import StatusSelect from './StatusSelect'; +import ApplicationsTable from './ApplicationsTable'; export const dynamic = 'force-dynamic'; @@ -321,35 +322,7 @@ export default async function JobDetailPage({ params }: { params: Promise<{ jobI

) : ( -
- - - - Name - Email - Status - - - - {applicants.map((applicant) => ( - - - {`${applicant.firstName ?? ''} ${applicant.lastName ?? ''}`.trim() || - 'Unknown'} - - {applicant.email} - - - - - ))} - -
-
+ )}