From 83fb3065f7f366cc02d1ed8272c95f5c2180c453 Mon Sep 17 00:00:00 2001 From: Albert Ying Date: Tue, 1 Sep 2026 19:23:47 -0400 Subject: [PATCH 1/5] remove redundant generateCSV block --- .../applications/ApplicationsTablePage.tsx | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/src/components/admin/applications/ApplicationsTablePage.tsx b/src/components/admin/applications/ApplicationsTablePage.tsx index 9a0f226..8f93fd1 100644 --- a/src/components/admin/applications/ApplicationsTablePage.tsx +++ b/src/components/admin/applications/ApplicationsTablePage.tsx @@ -47,33 +47,6 @@ const columns = [ }, ]; -const generateCSV = async ( - hexathonId: any, - status: any, - applicationBranch: any, - confirmationBranch: any -) => { - await axios - .get(apiUrl(Service.REGISTRATION, `applications/generate-csv`), { - params: { hexathon: hexathonId, status, applicationBranch, confirmationBranch }, - responseType: "blob", - }) - .then(response => { - const href = URL.createObjectURL(response.data); - - // create "a" HTML element with href to file & click - const link = document.createElement("a"); - link.href = href; - link.setAttribute("download", "Applications.csv"); - document.body.appendChild(link); - link.click(); - - // clean up "a" element & remove ObjectURL - document.body.removeChild(link); - URL.revokeObjectURL(href); - }); -}; - const ApplicationsTablePage: React.FC = () => { const { hexathonId } = useParams(); const [searchParams, setSearchParams] = useSearchParams(); From 6eb5017f42e60b616818add12653978e258ac1c9 Mon Sep 17 00:00:00 2001 From: Albert Ying Date: Tue, 1 Sep 2026 20:38:12 -0400 Subject: [PATCH 2/5] add final score column --- src/components/admin/applications/ApplicationsTablePage.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/admin/applications/ApplicationsTablePage.tsx b/src/components/admin/applications/ApplicationsTablePage.tsx index 8f93fd1..acb31bd 100644 --- a/src/components/admin/applications/ApplicationsTablePage.tsx +++ b/src/components/admin/applications/ApplicationsTablePage.tsx @@ -45,6 +45,11 @@ const columns = [ header: "Status", accessor: (row: any) => , }, + { + key: 4, + header: "Final Score", + accessor: (row: any) => row.finalScore ?? "N/A", + }, ]; const ApplicationsTablePage: React.FC = () => { @@ -71,6 +76,7 @@ const ApplicationsTablePage: React.FC = () => { confirmationBranch: searchParams.get("confirmationBranch")?.split(","), search: searchText, offset, + requireApplicationData: true, }, }); const [{ data: branches, loading: branchesLoading, error: branchesError }] = useAxios({ From badeedeeddfe66d03619b36df6aebc719e81c7c4 Mon Sep 17 00:00:00 2001 From: Albert Ying Date: Wed, 2 Sep 2026 20:07:33 -0400 Subject: [PATCH 3/5] filter top percent, csv modal pass current active filters --- .../applications/ApplicationCSVModal.tsx | 10 +++++-- .../applications/ApplicationsTablePage.tsx | 27 +++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/components/admin/applications/ApplicationCSVModal.tsx b/src/components/admin/applications/ApplicationCSVModal.tsx index a6af86e..c58e657 100644 --- a/src/components/admin/applications/ApplicationCSVModal.tsx +++ b/src/components/admin/applications/ApplicationCSVModal.tsx @@ -20,11 +20,13 @@ const generateCSV = async ( status: any, applicationBranch: any, confirmationBranch: any, + search: any, + topPercentage: any, rowLimit: any ) => { await axios .get(apiUrl(Service.REGISTRATION, `applications/generate-csv`), { - params: { hexathon: hexathonId, status, applicationBranch, confirmationBranch, limit: rowLimit }, + params: { hexathon: hexathonId, status, applicationBranch, confirmationBranch, search, topPercentage, limit: rowLimit }, responseType: "blob", }) .then(response => { @@ -51,10 +53,12 @@ interface ApplicationCSVModalProps { status: any; applicationBranch: any; confirmationBranch: any; + search: any; + topPercentage: any; totalApplicants: any; }; -const ApplicationCSVModal: React.FC = ({isOpen, onOpen, onClose, hexathonId, status, applicationBranch, confirmationBranch, totalApplicants}) => { +const ApplicationCSVModal: React.FC = ({isOpen, onOpen, onClose, hexathonId, status, applicationBranch, confirmationBranch, search, topPercentage, totalApplicants}) => { const [ rowLimit, setRowLimit ] = useState(totalApplicants); useEffect(() => { @@ -84,6 +88,8 @@ const ApplicationCSVModal: React.FC = ({isOpen, onOpen status, applicationBranch, confirmationBranch, + search, + topPercentage, rowLimit ) } diff --git a/src/components/admin/applications/ApplicationsTablePage.tsx b/src/components/admin/applications/ApplicationsTablePage.tsx index acb31bd..79cbe6a 100644 --- a/src/components/admin/applications/ApplicationsTablePage.tsx +++ b/src/components/admin/applications/ApplicationsTablePage.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useMemo, useState } from "react"; import { Box, Heading, + Input, Link as ChakraLink, Stack, Text, @@ -66,6 +67,9 @@ const ApplicationsTablePage: React.FC = () => { ); const { isOpen, onOpen, onClose } = useDisclosure(); + const [topPercentageInput, setTopPercentageInput] = useState(""); + const [topPercentage, setTopPercentage] = useState(undefined); + const [{ data, error }] = useAxios({ method: "GET", url: apiUrl(Service.REGISTRATION, "/applications"), @@ -75,6 +79,7 @@ const ApplicationsTablePage: React.FC = () => { applicationBranch: searchParams.get("applicationBranch")?.split(","), confirmationBranch: searchParams.get("confirmationBranch")?.split(","), search: searchText, + topPercentage, offset, requireApplicationData: true, }, @@ -161,6 +166,12 @@ const ApplicationsTablePage: React.FC = () => { ); }, [searchParams, statusOptions, applicationBranchOptions, confirmationBranchOptions]); + const applyTopPercentage = () => { + const parsed = parseInt(topPercentageInput); + setTopPercentage(!isNaN(parsed) && parsed >= 1 && parsed <= 100 ? parsed : undefined); + setOffset(0); + }; + const onPreviousClicked = () => { setOffset(offset - limit); }; @@ -302,6 +313,20 @@ const ApplicationsTablePage: React.FC = () => { }} /> + + Top X% by Score + setTopPercentageInput(e.target.value)} + onBlur={applyTopPercentage} + onKeyDown={(e: React.KeyboardEvent) => e.key === "Enter" && applyTopPercentage()} + /> +
@@ -313,6 +338,8 @@ const ApplicationsTablePage: React.FC = () => { status={searchParams.get("status")?.split(",")} applicationBranch={searchParams.get("applicationBranch")?.split(",")} confirmationBranch={searchParams.get("confirmationBranch")?.split(",")} + search={searchText || undefined} + topPercentage={topPercentage} totalApplicants={data?.total} />
From 2b786cb707a5a61f0fd76fc97f9d8260baed292c Mon Sep 17 00:00:00 2001 From: Albert Ying Date: Thu, 3 Sep 2026 09:07:33 -0400 Subject: [PATCH 4/5] add bulk assign panel --- .../applications/ApplicationsTablePage.tsx | 102 +++++++++++++++++- 1 file changed, 101 insertions(+), 1 deletion(-) diff --git a/src/components/admin/applications/ApplicationsTablePage.tsx b/src/components/admin/applications/ApplicationsTablePage.tsx index 79cbe6a..fcf8ffe 100644 --- a/src/components/admin/applications/ApplicationsTablePage.tsx +++ b/src/components/admin/applications/ApplicationsTablePage.tsx @@ -8,8 +8,9 @@ import { Text, Button, useDisclosure, + useToast, } from "@chakra-ui/react"; -import { apiUrl, ErrorScreen, SearchableTable, Service } from "@hex-labs/core"; +import { apiUrl, ErrorScreen, SearchableTable, Service, useAuth } from "@hex-labs/core"; import useAxios from "axios-hooks"; import { createSearchParams, Link, useParams, useSearchParams } from "react-router-dom"; import { GroupBase, OptionBase, Select } from "chakra-react-select"; @@ -66,6 +67,20 @@ const ApplicationsTablePage: React.FC = () => { [] ); const { isOpen, onOpen, onClose } = useDisclosure(); + const toast = useToast(); + const { user } = useAuth(); + const [role, setRole] = useState({ member: false, exec: false, admin: false }); + + useEffect(() => { + if (user?.uid) { + axios + .get(apiUrl(Service.USERS, `/users/${user.uid}`)) + .then(res => setRole({ ...res.data.roles })); + } + }, [user?.uid]); + + const [targetConfirmationBranch, setTargetConfirmationBranch] = useState(null); + const [isBulkAssigning, setIsBulkAssigning] = useState(false); const [topPercentageInput, setTopPercentageInput] = useState(""); const [topPercentage, setTopPercentage] = useState(undefined); @@ -166,6 +181,63 @@ const ApplicationsTablePage: React.FC = () => { ); }, [searchParams, statusOptions, applicationBranchOptions, confirmationBranchOptions]); + const handleBulkAssign = async () => { + if (!targetConfirmationBranch) return; + setIsBulkAssigning(true); + try { + const total = data?.total ?? 0; + const pages = Math.ceil(total / limit); + const responses = await Promise.all( + Array.from({ length: pages }, (_, i) => + axios.get(apiUrl(Service.REGISTRATION, "/applications"), { + params: { + hexathon: hexathonId, + status: searchParams.get("status")?.split(","), + applicationBranch: searchParams.get("applicationBranch")?.split(","), + confirmationBranch: searchParams.get("confirmationBranch")?.split(","), + search: searchText || undefined, + topPercentage, + limit, + offset: i * limit, + }, + }) + ) + ); + const allApps = responses.flatMap(r => r.data.applications); + + await Promise.all( + allApps.map((app: any) => + axios.post( + apiUrl(Service.REGISTRATION, `/applications/${app.id}/actions/update-application`), + { + applicationBranch: app.applicationBranch.id, + status: "ACCEPTED", + confirmationBranch: targetConfirmationBranch.value, + } + ) + ) + ); + + toast({ + title: "Success", + description: `Assigned ${allApps.length} applicants to "${targetConfirmationBranch.label}".`, + status: "success", + duration: 5000, + isClosable: true, + }); + } catch (e: any) { + toast({ + title: "Error", + description: e?.response?.data?.message ?? "Bulk assign failed. Please try again.", + status: "error", + duration: 5000, + isClosable: true, + }); + } finally { + setIsBulkAssigning(false); + } + }; + const applyTopPercentage = () => { const parsed = parseInt(topPercentageInput); setTopPercentage(!isNaN(parsed) && parsed >= 1 && parsed <= 100 ? parsed : undefined); @@ -344,6 +416,34 @@ const ApplicationsTablePage: React.FC = () => { /> + + {role.admin && ( + + + Bulk Actions: + + > + options={confirmationBranchOptions} + placeholder="Assign to confirmation branch..." + value={targetConfirmationBranch} + isLoading={branchesLoading} + size="sm" + onChange={(e: GroupOption | null) => setTargetConfirmationBranch(e)} + isClearable + /> + + + )} + Date: Thu, 3 Sep 2026 09:14:47 -0400 Subject: [PATCH 5/5] refetch after bulk assign --- src/components/admin/applications/ApplicationsTablePage.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/admin/applications/ApplicationsTablePage.tsx b/src/components/admin/applications/ApplicationsTablePage.tsx index fcf8ffe..b886846 100644 --- a/src/components/admin/applications/ApplicationsTablePage.tsx +++ b/src/components/admin/applications/ApplicationsTablePage.tsx @@ -85,7 +85,7 @@ const ApplicationsTablePage: React.FC = () => { const [topPercentageInput, setTopPercentageInput] = useState(""); const [topPercentage, setTopPercentage] = useState(undefined); - const [{ data, error }] = useAxios({ + const [{ data, error }, refetch] = useAxios({ method: "GET", url: apiUrl(Service.REGISTRATION, "/applications"), params: { @@ -225,6 +225,7 @@ const ApplicationsTablePage: React.FC = () => { duration: 5000, isClosable: true, }); + refetch(); } catch (e: any) { toast({ title: "Error",