feat(tpo): added bulk edit of status

This commit is contained in:
Unchanted
2025-09-02 17:38:19 +05:30
parent 797fdf23af
commit c38d2d0c66
3 changed files with 176 additions and 30 deletions

View 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>
);
}

View File

@@ -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 () => {

View File

@@ -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>