Skip to content

Commit

Permalink
Feature/yoma 233 verifications csv export (#882)
Browse files Browse the repository at this point in the history
* add export button and dialog to verification. fixed paging issue on 'all' tab

* export button for mobile display
  • Loading branch information
jasondicker authored Jun 4, 2024
1 parent 57da300 commit 69c28b3
Show file tree
Hide file tree
Showing 2 changed files with 179 additions and 23 deletions.
36 changes: 35 additions & 1 deletion src/web/src/api/services/myOpportunities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ import type {
VerificationStatus,
} from "../models/myOpportunity";
import { objectToFormData } from "~/lib/utils";
import type { GetServerSidePropsContext } from "next/types";
import type {
GetServerSidePropsContext,
GetStaticPropsContext,
} from "next/types";
import ApiServer from "~/lib/axiosServer";

export const saveMyOpportunity = async (
Expand Down Expand Up @@ -154,3 +157,34 @@ export const performActionInstantVerificationManual = async (
const instance = context ? ApiServer(context) : await ApiClient;
await instance.put(`/myopportunity/action/link/${linkId}/verify`);
};

export const getMyOpportunitiesExportToCSV = async (
filter: MyOpportunitySearchFilterAdmin,

context?: GetServerSidePropsContext | GetStaticPropsContext,
): Promise<File> => {
const instance = context ? ApiServer(context) : await ApiClient;

const { data } = await instance.post(
`/myopportunity/search/admin/csv`,
filter,
{
responseType: "blob", // set responseType to 'blob' or 'arraybuffer'
},
);

// create the file name
const date = new Date();
const dateString = `${date.getFullYear()}-${
date.getMonth() + 1
}-${date.getDate()} ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`;
const fileName = `Verifications_${dateString}.csv`;

// create a new Blob object using the data
const blob = new Blob([data], { type: "text/csv" });

// create a new File object from the Blob
const file = new File([blob], fileName);

return file;
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
import { type GetServerSidePropsContext } from "next";
import { getServerSession } from "next-auth";
import Head from "next/head";
import Image from "next/image";
import { useRouter } from "next/router";
import { useCallback, type ReactElement, useState, useMemo } from "react";
import MainLayout from "~/components/Layout/Main";
Expand All @@ -20,6 +21,7 @@ import {
IoMdAlert,
IoMdCheckmark,
IoMdClose,
IoMdDownload,
IoMdFlame,
IoMdThumbsDown,
IoMdThumbsUp,
Expand All @@ -30,9 +32,11 @@ import {
GA_ACTION_OPPORTUNITY_COMPLETION_VERIFY,
GA_CATEGORY_OPPORTUNITY,
PAGE_SIZE,
PAGE_SIZE_MAXIMUM,
} from "~/lib/constants";
import { PaginationButtons } from "~/components/PaginationButtons";
import {
getMyOpportunitiesExportToCSV,
getOpportunitiesForVerification,
performActionVerifyBulk,
searchMyOpportunitiesAdmin,
Expand Down Expand Up @@ -68,6 +72,8 @@ import MobileCard from "~/components/Organisation/Verifications/MobileCard";
import { useDisableBodyScroll } from "~/hooks/useDisableBodyScroll";
import React from "react";
import { LoadingSkeleton } from "~/components/Status/LoadingSkeleton";
import FileSaver from "file-saver";
import iconBell from "public/images/icon-bell.webp";

interface IParams extends ParsedUrlQuery {
id: string;
Expand Down Expand Up @@ -200,6 +206,9 @@ const OpportunityVerifications: NextPageWithLayout<{
const [verificationResponse, setVerificationResponse] =
useState<MyOpportunityResponseVerifyFinalizeBatch | null>(null);

const [isExportButtonLoading, setIsExportButtonLoading] = useState(false);
const [exportDialogOpen, setExportDialogOpen] = useState(false);

// search filter state
const searchFilter = useMemo<MyOpportunitySearchFilterAdmin>(
() => ({
Expand Down Expand Up @@ -360,7 +369,8 @@ const OpportunityVerifications: NextPageWithLayout<{
if (
searchFilter?.verificationStatuses !== undefined &&
searchFilter?.verificationStatuses !== null &&
searchFilter?.verificationStatuses.length > 0
searchFilter?.verificationStatuses.length > 0 &&
searchFilter?.verificationStatuses.length !== 3 // hack to prevent all" statuses from being added to the query string
)
params.append(
"verificationStatus",
Expand Down Expand Up @@ -533,6 +543,26 @@ const OpportunityVerifications: NextPageWithLayout<{
},
[data, setSelectedRows],
);

const handleExportToCSV = useCallback(async () => {
setIsExportButtonLoading(true);

try {
const searchFilterCopy = JSON.parse(JSON.stringify(searchFilter)); // deep copy

searchFilterCopy.pageNumber = 1;
searchFilterCopy.pageSize = PAGE_SIZE_MAXIMUM;

const data = await getMyOpportunitiesExportToCSV(searchFilterCopy);
if (!data) return;

FileSaver.saveAs(data);

setExportDialogOpen(false);
} finally {
setIsExportButtonLoading(false);
}
}, [searchFilter, setIsExportButtonLoading, setExportDialogOpen]);
//#endregion Click Handlers

//#region Filter Handlers
Expand Down Expand Up @@ -576,6 +606,7 @@ const OpportunityVerifications: NextPageWithLayout<{
// 👇 prevent scrolling on the page when the dialogs are open
useDisableBodyScroll(modalVerifyVisible);
useDisableBodyScroll(modalVerificationResultVisible);
useDisableBodyScroll(exportDialogOpen);

if (error) {
if (error === 401) return <Unauthenticated />;
Expand Down Expand Up @@ -774,6 +805,68 @@ const OpportunityVerifications: NextPageWithLayout<{
</div>
</ReactModal>

{/* EXPORT DIALOG */}
<ReactModal
isOpen={exportDialogOpen}
shouldCloseOnOverlayClick={true}
onRequestClose={() => {
setExportDialogOpen(false);
}}
className={`fixed bottom-0 left-0 right-0 top-0 flex-grow overflow-hidden bg-white animate-in fade-in md:m-auto md:max-h-[480px] md:w-[600px] md:rounded-3xl`}
portalClassName={"fixed z-40"}
overlayClassName="fixed inset-0 bg-overlay"
>
<div className="flex flex-col gap-2">
<div className="flex h-20 flex-row bg-blue p-4 shadow-lg"></div>
<div className="flex flex-col items-center justify-center gap-4">
<div className="-mt-8 flex h-12 w-12 items-center justify-center rounded-full border-green-dark bg-white shadow-lg">
<Image
src={iconBell}
alt="Icon Bell"
width={28}
height={28}
sizes="100vw"
priority={true}
style={{ width: "28px", height: "28px" }}
/>
</div>

<div className="flex w-96 flex-col gap-4">
<h4>
Just a heads up, the result set is quite large and we can only
return a maximum of {PAGE_SIZE_MAXIMUM.toLocaleString()} rows
for each export.
</h4>
<h5>
To help manage this, consider applying search filters. This will
narrow down the size of your results and make your data more
manageable.
</h5>
<h5>When you&apos;re ready, click the button to continue.</h5>
</div>

<div className="mt-4 flex flex-grow gap-4">
<button
type="button"
className="btn bg-green normal-case text-white hover:bg-green hover:brightness-110 disabled:border-0 disabled:bg-green disabled:brightness-90 md:w-[250px]"
onClick={handleExportToCSV}
disabled={isExportButtonLoading}
>
{isExportButtonLoading && (
<p className="text-white">Exporting...</p>
)}
{!isExportButtonLoading && (
<>
<IoMdDownload className="h-5 w-5 text-white" />
<p className="text-white">Export to CSV</p>
</>
)}
</button>
</div>
</div>
</div>
</ReactModal>

{/* PAGE */}
<div className="container z-10 mt-14 max-w-7xl px-2 py-8 md:mt-[4.9rem]">
<div className="flex flex-col gap-4 py-4">
Expand Down Expand Up @@ -980,28 +1073,57 @@ const OpportunityVerifications: NextPageWithLayout<{
them, we encourage selecting from top to bottom, as that is the
order in which Youth applied.
</div>

{/* BUTTONS */}
{(!verificationStatus || verificationStatus === "Pending") &&
!isLoadingData &&
data &&
data.items?.length > 0 && (
<div className="flex w-full flex-row justify-around gap-2 md:w-fit md:justify-end">
<button
className="btn btn-sm flex-nowrap border-green bg-white text-green hover:bg-green hover:text-white"
onClick={() => onChangeBulkAction(true)}
>
<IoMdThumbsUp className="h-6 w-6" />
Approve
</button>
<button
className="btn btn-sm flex-nowrap border-red-500 bg-white text-red-500 hover:bg-red-500 hover:text-white"
onClick={() => onChangeBulkAction(false)}
>
<IoMdThumbsDown className="h-6 w-6" />
Reject
</button>
</div>
)}
<div className="flex w-full flex-row justify-around gap-2 md:w-fit md:justify-end">
<button
type="button"
className="btn btn-sm w-[150px] flex-nowrap border-green bg-green text-white hover:bg-green hover:text-white disabled:bg-green disabled:brightness-90"
onClick={() => {
// show dialog if the result set is too large
if ((data?.totalCount ?? 0) > PAGE_SIZE_MAXIMUM) {
setExportDialogOpen(true);
return;
}

handleExportToCSV();
}}
disabled={isExportButtonLoading}
>
{isExportButtonLoading && (
<p className="text-white">Exporting...</p>
)}
{!isExportButtonLoading && (
<>
<IoMdDownload className="h-5 w-5 text-white" />
<p className="text-white">Export to CSV</p>
</>
)}
</button>

{/* show approve/reject buttons for 'all' & 'pending' tabs */}
{(!verificationStatus || verificationStatus === "Pending") &&
!isLoadingData &&
data &&
data.items?.length > 0 && (
<>
<button
className="btn btn-sm flex-nowrap border-green bg-white text-green hover:bg-green hover:text-white"
onClick={() => onChangeBulkAction(true)}
>
<IoMdThumbsUp className="h-6 w-6" />
Approve
</button>
<button
className="btn btn-sm flex-nowrap border-red-500 bg-white text-red-500 hover:bg-red-500 hover:text-white"
onClick={() => onChangeBulkAction(false)}
>
<IoMdThumbsDown className="h-6 w-6" />
Reject
</button>
</>
)}
</div>
</div>
</div>

Expand Down

0 comments on commit 69c28b3

Please sign in to comment.