From c38d2d0c66fdb820aa1027af13b2ae91d8a79238 Mon Sep 17 00:00:00 2001
From: Unchanted
Date: Tue, 2 Sep 2025 17:38:19 +0530
Subject: [PATCH] feat(tpo): added bulk edit of status
---
.../(main)/jobs/[jobId]/ApplicationsTable.tsx | 168 ++++++++++++++++++
.../app/(main)/jobs/[jobId]/StatusSelect.tsx | 7 +-
apps/admin/app/(main)/jobs/[jobId]/page.tsx | 31 +---
3 files changed, 176 insertions(+), 30 deletions(-)
create mode 100644 apps/admin/app/(main)/jobs/[jobId]/ApplicationsTable.tsx
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 && (
+
+ )}
+
+
+
+
+
+ );
+}
+
+
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}
-
-
-
-
- ))}
-
-
-
+
)}