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 9a0f226..b886846 100644 --- a/src/components/admin/applications/ApplicationsTablePage.tsx +++ b/src/components/admin/applications/ApplicationsTablePage.tsx @@ -2,13 +2,15 @@ import React, { useEffect, useMemo, useState } from "react"; import { Box, Heading, + Input, Link as ChakraLink, Stack, 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"; @@ -45,35 +47,13 @@ const columns = [ header: "Status", accessor: (row: any) => , }, + { + key: 4, + header: "Final Score", + accessor: (row: any) => row.finalScore ?? "N/A", + }, ]; -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(); @@ -87,8 +67,25 @@ 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 [{ data, error }] = useAxios({ + const [topPercentageInput, setTopPercentageInput] = useState(""); + const [topPercentage, setTopPercentage] = useState(undefined); + + const [{ data, error }, refetch] = useAxios({ method: "GET", url: apiUrl(Service.REGISTRATION, "/applications"), params: { @@ -97,7 +94,9 @@ const ApplicationsTablePage: React.FC = () => { applicationBranch: searchParams.get("applicationBranch")?.split(","), confirmationBranch: searchParams.get("confirmationBranch")?.split(","), search: searchText, + topPercentage, offset, + requireApplicationData: true, }, }); const [{ data: branches, loading: branchesLoading, error: branchesError }] = useAxios({ @@ -182,6 +181,70 @@ 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, + }); + refetch(); + } 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); + setOffset(0); + }; + const onPreviousClicked = () => { setOffset(offset - limit); }; @@ -323,6 +386,20 @@ const ApplicationsTablePage: React.FC = () => { }} /> + + Top X% by Score + setTopPercentageInput(e.target.value)} + onBlur={applyTopPercentage} + onKeyDown={(e: React.KeyboardEvent) => e.key === "Enter" && applyTopPercentage()} + /> +
@@ -334,10 +411,40 @@ 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} />
+ + {role.admin && ( + + + Bulk Actions: + + > + options={confirmationBranchOptions} + placeholder="Assign to confirmation branch..." + value={targetConfirmationBranch} + isLoading={branchesLoading} + size="sm" + onChange={(e: GroupOption | null) => setTargetConfirmationBranch(e)} + isClearable + /> + + + )} +