Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions src/components/admin/applications/ApplicationCSVModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand All @@ -51,10 +53,12 @@ interface ApplicationCSVModalProps {
status: any;
applicationBranch: any;
confirmationBranch: any;
search: any;
topPercentage: any;
totalApplicants: any;
};

const ApplicationCSVModal: React.FC<ApplicationCSVModalProps> = ({isOpen, onOpen, onClose, hexathonId, status, applicationBranch, confirmationBranch, totalApplicants}) => {
const ApplicationCSVModal: React.FC<ApplicationCSVModalProps> = ({isOpen, onOpen, onClose, hexathonId, status, applicationBranch, confirmationBranch, search, topPercentage, totalApplicants}) => {
const [ rowLimit, setRowLimit ] = useState(totalApplicants);

useEffect(() => {
Expand Down Expand Up @@ -84,6 +88,8 @@ const ApplicationCSVModal: React.FC<ApplicationCSVModalProps> = ({isOpen, onOpen
status,
applicationBranch,
confirmationBranch,
search,
topPercentage,
rowLimit
)
}
Expand Down
165 changes: 136 additions & 29 deletions src/components/admin/applications/ApplicationsTablePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -45,35 +47,13 @@ const columns = [
header: "Status",
accessor: (row: any) => <ApplicationStatusTag status={row.status} includeColor />,
},
{
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();
Expand All @@ -87,8 +67,25 @@ const ApplicationsTablePage: React.FC = () => {
[]
);
const { isOpen, onOpen, onClose } = useDisclosure();
const toast = useToast();
const { user } = useAuth();
const [role, setRole] = useState<any>({ 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<GroupOption | null>(null);
const [isBulkAssigning, setIsBulkAssigning] = useState(false);

const [{ data, error }] = useAxios({
const [topPercentageInput, setTopPercentageInput] = useState("");
const [topPercentage, setTopPercentage] = useState<number | undefined>(undefined);

const [{ data, error }, refetch] = useAxios({
method: "GET",
url: apiUrl(Service.REGISTRATION, "/applications"),
params: {
Expand All @@ -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({
Expand Down Expand Up @@ -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(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might not be a great idea to promise.all and post hundreds of applications.... we should be using an updateMany on the api side instead.

i actually was working on one for bulk status updates last week (+confirmation branch assigning), check /bulk/decide-applications route in the bulk-application-decisions branch. Kinda forgot if it's done or not but I definitely haven't gotten to testing it yet. Will try to finalize it by the end of this week, and I think it should be used here instead

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);
};
Expand Down Expand Up @@ -323,6 +386,20 @@ const ApplicationsTablePage: React.FC = () => {
}}
/>
</Box>
<Box p={4} w="52">
<Text size="xs">Top X% by Score</Text>
<Input
type="number"
min={1}
max={100}
size="sm"
placeholder="e.g. 25"
value={topPercentageInput}
onChange={e => setTopPercentageInput(e.target.value)}
onBlur={applyTopPercentage}
onKeyDown={(e: React.KeyboardEvent) => e.key === "Enter" && applyTopPercentage()}
/>
</Box>
<Box p={4} w="80">
<br />
<Button onClick={onOpen}>Generate CSV</Button>
Expand All @@ -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}
/>
</Box>
</Stack>

{role.admin && (
<Box marginLeft={6} my={3} w="80">
<Heading as="h5" size="sm" mb={2} mt={4}>
Bulk Actions:
</Heading>
<Select<GroupOption, false, GroupBase<GroupOption>>
options={confirmationBranchOptions}
placeholder="Assign to confirmation branch..."
value={targetConfirmationBranch}
isLoading={branchesLoading}
size="sm"
onChange={(e: GroupOption | null) => setTargetConfirmationBranch(e)}
isClearable
/>
<Button
mt={2}
colorScheme="blue"
size="sm"
isDisabled={!targetConfirmationBranch}
isLoading={isBulkAssigning}
onClick={handleBulkAssign}
>
{`Accept & assign ${data?.total ?? 0} applicant${data?.total !== 1 ? "s" : ""}`}
</Button>
</Box>
)}

<SearchableTable
title="Applications"
data={data?.applications}
Expand Down