feat(tpo): added bulk edit of status
This commit is contained in:
168
apps/admin/app/(main)/jobs/[jobId]/ApplicationsTable.tsx
Normal file
168
apps/admin/app/(main)/jobs/[jobId]/ApplicationsTable.tsx
Normal file
@@ -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<number[]>([]);
|
||||
const [bulkStatus, setBulkStatus] = useState<string>('');
|
||||
const [rows, setRows] = useState<Applicant[]>(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 (
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col sm:flex-row gap-3 sm:items-center sm:justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
placeholder="Search by name or email"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
className="w-72"
|
||||
/>
|
||||
<span className="text-sm text-gray-500">{filtered.length} results</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Select value={bulkStatus} onValueChange={setBulkStatus}>
|
||||
<SelectTrigger className="min-w-[200px]">
|
||||
<SelectValue placeholder="Bulk update status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{STATUS_OPTIONS.map((s) => (
|
||||
<SelectItem key={s} value={s}>
|
||||
{s}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button onClick={onBulkUpdate} disabled={!bulkStatus || selected.length === 0 || isPending}>
|
||||
Update {selected.length || ''}
|
||||
</Button>
|
||||
{selected.length > 0 && (
|
||||
<Button variant="outline" onClick={() => setSelected([])} disabled={isPending}>
|
||||
Clear Selection
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-gray-200 overflow-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableHead className="w-10">
|
||||
<input
|
||||
type="checkbox"
|
||||
aria-label="Select all"
|
||||
checked={allSelected}
|
||||
ref={(el) => {
|
||||
if (el) el.indeterminate = someSelected;
|
||||
}}
|
||||
onChange={(e) => toggleAll(e.target.checked)}
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead className="font-medium text-gray-700">Name</TableHead>
|
||||
<TableHead className="font-medium text-gray-700">Email</TableHead>
|
||||
<TableHead className="font-medium text-gray-700">Status</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filtered.map((applicant) => (
|
||||
<TableRow key={applicant.applicationId} className="hover:bg-gray-50">
|
||||
<TableCell>
|
||||
<input
|
||||
type="checkbox"
|
||||
aria-label={`Select ${applicant.email ?? ''}`}
|
||||
checked={selected.includes(applicant.applicationId)}
|
||||
onChange={(e) => toggleOne(applicant.applicationId, e.target.checked)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
{`${applicant.firstName ?? ''} ${applicant.lastName ?? ''}`.trim() || 'Unknown'}
|
||||
</TableCell>
|
||||
<TableCell className="text-gray-600">{applicant.email}</TableCell>
|
||||
<TableCell>
|
||||
<StatusSelect
|
||||
applicationId={applicant.applicationId}
|
||||
initialStatus={applicant.status}
|
||||
studentId={applicant.studentId ?? 0}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg border border-gray-200 overflow-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableHead className="font-medium text-gray-700">Name</TableHead>
|
||||
<TableHead className="font-medium text-gray-700">Email</TableHead>
|
||||
<TableHead className="font-medium text-gray-700">Status</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{applicants.map((applicant) => (
|
||||
<TableRow key={applicant.applicationId} className="hover:bg-gray-50">
|
||||
<TableCell className="font-medium">
|
||||
{`${applicant.firstName ?? ''} ${applicant.lastName ?? ''}`.trim() ||
|
||||
'Unknown'}
|
||||
</TableCell>
|
||||
<TableCell className="text-gray-600">{applicant.email}</TableCell>
|
||||
<TableCell>
|
||||
<StatusSelect
|
||||
applicationId={applicant.applicationId}
|
||||
initialStatus={applicant.status}
|
||||
studentId={applicant.studentId ?? 0}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<ApplicationsTable applicants={applicants} />
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user