Skip to content

Commit

Permalink
banking applicatiosn page
Browse files Browse the repository at this point in the history
  • Loading branch information
ap-justin committed Dec 9, 2023
1 parent 7757b6c commit 808f687
Show file tree
Hide file tree
Showing 14 changed files with 486 additions and 5 deletions.
76 changes: 76 additions & 0 deletions src/pages/BankingApplications/BankingApplications.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import withAuth from "contexts/Auth";
import Icon from "components/Icon";
import QueryLoader from "components/QueryLoader";
import Filter from "./Filter";
import Table from "./Table";
import usePagination from "./usePagination";

function BankingApplications() {
const {
data,
hasMore,
isError,
isLoading,
isLoadingNextPage,
query,
loadNextPage,
onQueryChange,
setParams,
isFetching,
} = usePagination();

const isLoadingOrError = isLoading || isLoadingNextPage || isError;

return (
<div className="grid grid-cols-[1fr_auto] content-start gap-y-4 lg:gap-y-8 lg:gap-x-3 relative padded-container pt-8 lg:pt-20 pb-8">
<h1 className="text-center text-3xl max-lg:font-work col-span-full max-lg:mb-4">
Banking Applications
</h1>
<div className="relative flex gap-x-3 items-center border border-prim w-full bg-white dark:bg-blue-d6 rounded">
<Icon
type="Search"
size={24}
className="text-gray-d2 dark:text-gray absolute top-1/2 -translate-y-1/2 left-3"
/>
<input
disabled={isError}
className="p-3 pl-10 placeholder:text-gray-d1 dark:placeholder:text-gray bg-transparent w-full outline-none disabled:bg-gray-l3 dark:disabled:bg-bluegray-d1"
type="text"
placeholder="Search applications"
value={query}
onChange={(e) => onQueryChange(e.target.value)}
/>
</div>
<Filter
isDisabled={isLoadingOrError || isFetching}
setParams={setParams}
classes="max-lg:col-span-full max-lg:w-full"
/>
<QueryLoader
queryState={{
data: data?.items,
isLoading: isLoading,
isError: isError,
}}
messages={{
loading: "Loading applications...",
error: "Failed to get applications",
empty: "No applications found.",
}}
>
{(applications) => (
<div className="grid col-span-full overflow-x-auto">
<Table
applications={applications}
hasMore={hasMore}
onLoadMore={loadNextPage}
disabled={isLoadingOrError}
isLoading={isLoadingNextPage}
/>
</div>
)}
</QueryLoader>
</div>
);
}
export default withAuth(BankingApplications, ["ap-admin"]);
49 changes: 49 additions & 0 deletions src/pages/BankingApplications/Filter/Form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Popover } from "@headlessui/react";
import { FC, FormEventHandler } from "react";
import { FormValues } from "./types";
import Icon from "components/Icon";
import { Field } from "components/form";
import StatusDropdown from "./StatusDropdown";

type Props = {
submit: FormEventHandler<HTMLFormElement>;
onReset: FormEventHandler<HTMLFormElement>;
classes?: string;
};

const Form: FC<Props> = ({ onReset, submit, classes = "" }) => {
return (
<Popover.Panel
as="form"
onSubmit={submit}
onReset={onReset}
className={`${classes} grid content-start gap-4 w-full rounded border border-prim bg-white dark:bg-blue-d5`}
>
<div className="lg:hidden relative text-[1.25rem] px-4 py-3 -mb-4 font-bold uppercase">
<span className="text-orange">Filters</span>
<Popover.Button className="absolute top-1/2 -translate-y-1/2 right-2">
<Icon type="Close" size={33} />
</Popover.Button>
</div>
<Field<FormValues> name="endowmentID" label="Endowment ID" />
<StatusDropdown classes="px-4 lg:px-6 max-lg:mb-4" />

<div className="max-lg:row-start-2 flex gap-x-4 items-center justify-between max-lg:px-4 max-lg:py-3 p-6 lg:mt-2 bg-orange-l6 dark:bg-blue-d7 border-y lg:border-t border-prim">
<h3 className="uppercase lg:hidden">Filter by</h3>
<button
type="reset"
className="text-orange underline text-sm max-lg:ml-auto"
>
Reset filters
</button>
<button
type="submit"
className="btn btn-orange px-6 py-2 rounded-sm text-xs font-work font-bold uppercase"
>
Apply filters
</button>
</div>
</Popover.Panel>
);
};
export default Form;
16 changes: 16 additions & 0 deletions src/pages/BankingApplications/Filter/StatusDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { FormValues as FV } from "./types";
import { Selector } from "components/Selector";
import { statuses } from "./constants";

export default function StatusDropdown({ classes = "" }) {
return (
<div className={classes + " grid gap-2"}>
<label className="text-sm">Status</label>
<Selector<FV, "status", string>
name="status"
classes={{ button: "dark:bg-blue-d6" }}
options={statuses}
/>
</div>
);
}
8 changes: 8 additions & 0 deletions src/pages/BankingApplications/Filter/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { BankingApplicationStatus } from "types/aws";
import { OptionType } from "types/components";

export const statuses: OptionType<BankingApplicationStatus>[] = [
{ label: "Rejected", value: "rejected" },
{ label: "Under Review", value: "under-review" },
{ label: "Approved", value: "approved" },
];
78 changes: 78 additions & 0 deletions src/pages/BankingApplications/Filter/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { Popover } from "@headlessui/react";
import { yupResolver } from "@hookform/resolvers/yup";
import { FormEventHandler, useRef } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { FormValues as FV } from "./types";
import { BankingApplicationsQueryParams } from "types/aws";
import Icon, { DrawerIcon } from "components/Icon";
import { cleanObject } from "helpers/cleanObject";
import Form from "./Form";
import { schema } from "./schema";

type Props = {
classes?: string;
setParams: React.Dispatch<
React.SetStateAction<BankingApplicationsQueryParams>
>;
isDisabled: boolean;
};

export default function Filter({ setParams, classes = "", isDisabled }: Props) {
const buttonRef = useRef<HTMLButtonElement | null>(null);

const methods = useForm<FV>({
mode: "onChange",
reValidateMode: "onChange",
resolver: yupResolver(schema),
defaultValues: {
endowmentID: "",
status: { label: "Under Review", value: "under-review" },
},
});

const { handleSubmit, reset } = methods;

async function submit(data: FV) {
setParams(
cleanObject({
status: data.status.value,
endowmentID: +data.endowmentID,
requestor: "bg-admin",
})
);
buttonRef.current?.click();
}

const onReset: FormEventHandler<HTMLFormElement> = () => {
reset();
setParams({ requestor: "bg-admin" });
buttonRef.current?.click();
};
return (
<Popover className={`${classes} flex relative items-center`}>
<Popover.Button
ref={buttonRef}
disabled={isDisabled}
className="w-full lg:w-[22.3rem] flex justify-center items-center p-3 rounded bg-orange text-white lg:dark:text-gray lg:text-gray-d1 lg:bg-white lg:dark:bg-blue-d6 lg:justify-between disabled:bg-gray lg:disabled:bg-gray-l3 lg:dark:disabled:bg-bluegray-d1 lg:border lg:border-prim"
>
{({ open }) => (
<>
<Icon className="lg:hidden" type="Filter" size={20} />
<div className="uppercase font-semibold text-[0.9375rem] ">
Filter
</div>
<DrawerIcon isOpen={open} className="hidden lg:inline" size={21} />
</>
)}
</Popover.Button>

<FormProvider {...methods}>
<Form
submit={handleSubmit(submit)}
onReset={onReset}
classes="max-lg:fixed max-lg:inset-x-0 max-lg:top-0 lg:mt-1 absolute top-full z-20"
/>
</FormProvider>
</Popover>
);
}
12 changes: 12 additions & 0 deletions src/pages/BankingApplications/Filter/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ObjectSchema, number, object } from "yup";
import { FormValues } from "./types";
import { SchemaShape } from "schemas/types";
import { optionType } from "schemas/shape";

export const schema = object<any, SchemaShape<FormValues>>({
endowmentID: number()
.typeError("must be a number")
.integer("must be integer")
.min(1),
status: optionType(),
}) as ObjectSchema<FormValues>;
7 changes: 7 additions & 0 deletions src/pages/BankingApplications/Filter/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { BankingApplicationStatus } from "types/aws";
import { OptionType } from "types/components";

export type FormValues = {
endowmentID: string;
status: OptionType<BankingApplicationStatus>;
};
28 changes: 28 additions & 0 deletions src/pages/BankingApplications/LoadMoreBtn.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import LoaderRing from "components/LoaderRing";

export default function LoadMoreBtn({
onLoadMore,
disabled,
isLoading,
}: {
onLoadMore(): void;
disabled: boolean;
isLoading: boolean;
}) {
return (
<button
type="button"
onClick={onLoadMore}
disabled={disabled}
className="flex items-center justify-center gap-3 uppercase text-sm font-bold rounded-b w-full h-12 hover:bg-orange-l5 dark:hover:bg-blue-d3 active:bg-orange-l4 dark:active:bg-blue-d2 disabled:bg-gray-l3 disabled:text-gray aria-disabled:bg-gray-l3 aria-disabled:dark:bg-bluegray disabled:dark:bg-bluegray"
>
{isLoading ? (
<>
<LoaderRing thickness={10} classes="w-6" /> Loading...
</>
) : (
"Load More"
)}
</button>
);
}
116 changes: 116 additions & 0 deletions src/pages/BankingApplications/Table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { Link } from "react-router-dom";
import { TableProps } from "./types";
import { BankingApplicationStatus } from "types/aws";
import Icon from "components/Icon";
import TableSection, { Cells } from "components/TableSection";
import { appRoutes } from "constants/routes";
import LoadMoreBtn from "./LoadMoreBtn";

export default function Table({
applications,
classes = "",
disabled,
isLoading,
hasMore,
onLoadMore,
}: TableProps) {
return (
<table
className={`${classes} w-full text-sm rounded border border-separate border-spacing-0 border-prim`}
>
<TableSection
type="thead"
rowClass="bg-orange-l6 dark:bg-blue-d7 divide-x divide-prim"
>
<Cells
type="th"
cellClass="px-3 py-4 text-xs uppercase font-semibold text-left first:rounded-tl last:rounded-tr"
>
<>ID</>
<>Endowment</>
<>Bank name</>
<>Account number</>
<>Currency</>
<>Bank Statement</>
<>Date created</>
<>Status</>
<span className="flex justify-center">Details</span>
</Cells>
</TableSection>
<TableSection
type="tbody"
rowClass="even:bg-orange-l6 dark:odd:bg-blue-d6 dark:even:bg-blue-d7 divide-x divide-prim"
selectedClass="bg-orange-l5 dark:bg-blue-d4"
>
{applications
.map((row) => (
<Cells
key={row.wiseRecipientID}
type="td"
cellClass={`p-3 border-t border-prim max-w-[256px] truncate ${
hasMore ? "" : "first:rounded-bl last:rounded-br"
}`}
>
<>{row.wiseRecipientID}</>
<>{row.endowmentId}</>
<>{new Date(row.dateCreated).toLocaleDateString()}</>
<>{row.status}</>
<td className="text-center">
<Status status={row.status} />
</td>
<Link
to={appRoutes.applications + `/${row.wiseRecipientID}`}
className="text-center w-full inline-block hover:text-orange active:text-orange-d1"
>
<Icon
size={24}
type="Folder"
title="application details"
className="inline-block"
/>
</Link>
</Cells>
))
.concat(
hasMore ? (
<td
colSpan={9}
key="load-more-btn"
className="border-t border-prim rounded-b"
>
<LoadMoreBtn
onLoadMore={onLoadMore}
disabled={disabled}
isLoading={isLoading}
/>
</td>
) : (
[]
)
)}
</TableSection>
</table>
);
}

const bg: { [key in BankingApplicationStatus]: string } = {
approved: "bg-green",
"under-review": "bg-gray-d1",
rejected: "bg-red",
};

const text: { [key in BankingApplicationStatus]: string } = {
"under-review": "Under review",
rejected: "Rejected",
approved: "Approved",
};

function Status({ status }: { status: BankingApplicationStatus }) {
return (
<p
className={`${bg[status]} rounded px-3 py-1 inline-block uppercase text-xs text-white`}
>
{text[status]}
</p>
);
}
Loading

0 comments on commit 808f687

Please sign in to comment.