From 2ae2fe7368d38413d5bd1cf147de12d4433e6715 Mon Sep 17 00:00:00 2001 From: ap-justin <89639563+ap-justin@users.noreply.github.com> Date: Tue, 10 Sep 2024 22:09:53 +0800 Subject: [PATCH] movement data --- .../Admin/Charity/Dashboard/Dashboard.tsx | 98 ++----------- src/pages/Admin/Charity/Dashboard/Figure.tsx | 2 + src/pages/Admin/Charity/Dashboard/Loaded.tsx | 130 ++++++++++++++++++ .../Admin/Charity/Dashboard/MoveFundForm.tsx | 86 ++++++++++++ .../Admin/Charity/Dashboard/Movements.tsx | 62 +++++++++ .../Charity/Dashboard/Schedule/Schedule.tsx | 4 +- src/services/apes/apes.ts | 1 + src/services/apes/donations.ts | 16 +++ src/services/apes/tags.ts | 2 +- src/types/aws/apes/index.ts | 10 ++ 10 files changed, 320 insertions(+), 91 deletions(-) create mode 100644 src/pages/Admin/Charity/Dashboard/Loaded.tsx create mode 100644 src/pages/Admin/Charity/Dashboard/MoveFundForm.tsx create mode 100644 src/pages/Admin/Charity/Dashboard/Movements.tsx diff --git a/src/pages/Admin/Charity/Dashboard/Dashboard.tsx b/src/pages/Admin/Charity/Dashboard/Dashboard.tsx index 85b6d561ac..7509c18898 100644 --- a/src/pages/Admin/Charity/Dashboard/Dashboard.tsx +++ b/src/pages/Admin/Charity/Dashboard/Dashboard.tsx @@ -1,17 +1,11 @@ import { skipToken } from "@reduxjs/toolkit/query"; import ContentLoader from "components/ContentLoader"; -import Icon from "components/Icon"; import QueryLoader from "components/QueryLoader"; -import { Arrow, Content } from "components/Tooltip"; -import { humanize } from "helpers"; import { useEndowBalanceQuery } from "services/apes"; -import type { EndowmentBalances } from "types/aws"; import { useAdminContext } from "../../Context"; import Seo from "../Seo"; -import Figure from "./Figure"; -import { PayoutHistory } from "./PayoutHistory"; -import { Schedule } from "./Schedule"; -import { monthPeriod } from "./monthPeriod"; + +import { Loaded } from "./Loaded"; export default function Dashboard() { const { id } = useAdminContext(); @@ -34,90 +28,16 @@ export default function Dashboard() { ); } -function Loaded({ - classes = "", - ...props -}: EndowmentBalances & { classes?: string }) { - const { id } = useAdminContext(); - const period = monthPeriod(); - return ( -
-

Account Balances

-
-
- Funds held in Fidelity Government Money Market (SPAXX) consisting - of cash, US Government Securities and Repurchase Agreements - - - } - icon={} - amount={`$ ${humanize(props.donationsBal - props.payoutsMade, 2)}`} - /> -
- - Funds invested in a diversified portfolio comprising - -
-

50% - Domestic and international equities

-

30% - Fixed income

-

15% - Crypto

-

5% - Cash

-
- - - } - icon={} - amount={`$ ${humanize(props.sustainabilityFundBal, 2)}`} - /> -
} - amount={props.contributionsCount.toString()} - /> -
- -
- -

- Period - - {period.from} - {period.to} - -

- Ends in - - in {period.distance} - -

-

- - -
- -
- ); -} - function LoaderSkeleton() { return (
-
- - - -
- - + + + + + + +
); } diff --git a/src/pages/Admin/Charity/Dashboard/Figure.tsx b/src/pages/Admin/Charity/Dashboard/Figure.tsx index a414c79f99..26dca5bfbe 100644 --- a/src/pages/Admin/Charity/Dashboard/Figure.tsx +++ b/src/pages/Admin/Charity/Dashboard/Figure.tsx @@ -9,6 +9,7 @@ type Props = { amount: string; /** must be wrapped by tooltip content */ tooltip?: ReactNode; + actions?: ReactNode; }; export default function Figure(props: Props) { @@ -30,6 +31,7 @@ export default function Figure(props: Props) { {props.icon}

{props.amount}

+ {props.actions}
); } diff --git a/src/pages/Admin/Charity/Dashboard/Loaded.tsx b/src/pages/Admin/Charity/Dashboard/Loaded.tsx new file mode 100644 index 0000000000..2174107518 --- /dev/null +++ b/src/pages/Admin/Charity/Dashboard/Loaded.tsx @@ -0,0 +1,130 @@ +import Icon from "components/Icon"; +import { Arrow, Content } from "components/Tooltip"; +import { useModalContext } from "contexts/ModalContext"; +import { humanize } from "helpers"; +import { useAdminContext } from "pages/Admin/Context"; +import type { EndowmentBalances } from "types/aws"; +import Figure from "./Figure"; +import { MoveFundForm } from "./MoveFundForm"; +import { Movements } from "./Movements"; +import { PayoutHistory } from "./PayoutHistory"; +import { Schedule } from "./Schedule"; +import { monthPeriod } from "./monthPeriod"; + +export function Loaded({ + classes = "", + ...props +}: EndowmentBalances & { classes?: string }) { + const { id } = useAdminContext(); + const period = monthPeriod(); + const { showModal } = useModalContext(); + + const mov = props.movementDetails ?? { + "liq-cash": 0, + "liq-lock": 0, + "lock-cash": 0, + }; + + const liqDeductions = Object.entries(mov).reduce( + (sum, [k, v]) => (k.startsWith("liq-") ? sum + v : sum), + 0 + ); + const lockDeductions = Object.entries(mov).reduce( + (sum, [k, v]) => (k.startsWith("liq-") ? sum + v : sum), + 0 + ); + + const grantFromBal = Object.entries(mov).reduce( + (sum, [k, v]) => (k.endsWith("-cash") ? sum + v : sum), + 0 + ); + + return ( +
+

Account Balances

+
+
+ Funds held in Fidelity Government Money Market (SPAXX) consisting + of cash, US Government Securities and Repurchase Agreements + + + } + icon={} + amount={`$ ${humanize(props.donationsBal - props.payoutsMade, 2)}`} + actions={ +
+ +
+ } + /> +
+ + Funds invested in a diversified portfolio comprising + +
+

50% - Domestic and international equities

+

30% - Fixed income

+

15% - Crypto

+

5% - Cash

+
+ + + } + icon={} + amount={`$ ${humanize(props.sustainabilityFundBal, 2)}`} + /> +
} + amount={props.contributionsCount.toString()} + /> +
+ +
+ +

+ Period + + {period.from} - {period.to} + +

+ Ends in + + in {period.distance} + +

+

+ + + + +
+ +
+ ); +} diff --git a/src/pages/Admin/Charity/Dashboard/MoveFundForm.tsx b/src/pages/Admin/Charity/Dashboard/MoveFundForm.tsx new file mode 100644 index 0000000000..f1f66ab0c4 --- /dev/null +++ b/src/pages/Admin/Charity/Dashboard/MoveFundForm.tsx @@ -0,0 +1,86 @@ +import { Field, Input, Label } from "@headlessui/react"; +import { yupResolver } from "@hookform/resolvers/yup"; +import Modal from "components/Modal"; +import { useErrorContext } from "contexts/ErrorContext"; +import { useModalContext } from "contexts/ModalContext"; +import { humanize } from "helpers"; +import { useForm } from "react-hook-form"; +import { schema, stringNumber } from "schemas/shape"; +import { useMoveFundsMutation } from "services/apes"; +import type { BalanceMovement } from "types/aws"; + +interface IMoveFundForm { + effect: "append" | "override"; + type: keyof BalanceMovement; + endowId: number; + balance: number; + mov: BalanceMovement; +} + +export function MoveFundForm(props: IMoveFundForm) { + const [moveFund, { isLoading }] = useMoveFundsMutation(); + const { closeModal } = useModalContext(); + const { handleError } = useErrorContext(); + type FV = { amount: string }; + + const { + handleSubmit, + register, + formState: { errors, isSubmitting }, + } = useForm({ + defaultValues: { amount: "" }, + resolver: yupResolver( + schema({ + amount: stringNumber( + (s) => s.required("required"), + (n) => n.positive().max(props.balance, "can't be more than balance") + ), + }) + ), + }); + + return ( + { + try { + await moveFund({ + endowId: props.endowId, + ...props.mov, + [props.type]: props.mov[props.type] + +fv.amount, + }).unwrap(); + closeModal(); + } catch (err) { + handleError(err, { context: "moving funds" }); + } + })} + as="form" + className="fixed-center z-10 grid gap-y-4 text-navy-d4 bg-white sm:w-full w-[90vw] sm:max-w-lg rounded-lg p-6" + > +

+ Available balance + + $ {humanize(props.balance)} + +

+ + + + + {errors.amount?.message} + + + +
+ ); +} diff --git a/src/pages/Admin/Charity/Dashboard/Movements.tsx b/src/pages/Admin/Charity/Dashboard/Movements.tsx new file mode 100644 index 0000000000..64b0f97f6b --- /dev/null +++ b/src/pages/Admin/Charity/Dashboard/Movements.tsx @@ -0,0 +1,62 @@ +import Icon from "components/Icon"; +import { humanize } from "helpers"; +import type { ReactNode } from "react"; +import type { BalanceMovement } from "types/aws"; + +type Flow = keyof BalanceMovement; +const asset: { + [K in Flow]: { + icon: ReactNode; + source: string; + title: string; + }; +} = { + "liq-cash": { + title: "Grant", + icon: , + source: "Savings", + }, + "lock-cash": { + title: "Grant", + icon: , + source: "Investment", + }, + "liq-lock": { + title: "Invest", + icon: , + source: "Savings", + }, +}; + +export function Movements({ + classes = "", + ...props +}: BalanceMovement & { classes?: string }) { + const movs = Object.entries(props).filter(([_, v]) => v > 0); + + if (movs.length === 0) return null; + + return ( +
+

+ Pending transactions +

+
+ {Object.entries(props) + .filter(([_, v]) => v > 0) + .map(([k, v]) => ( +
+ {asset[k as Flow].icon} + {asset[k as Flow].title} + $ {humanize(v)} + edit + cancel +
+ ))} +
+
+ ); +} diff --git a/src/pages/Admin/Charity/Dashboard/Schedule/Schedule.tsx b/src/pages/Admin/Charity/Dashboard/Schedule/Schedule.tsx index 3bff5f538b..2dc58441ba 100644 --- a/src/pages/Admin/Charity/Dashboard/Schedule/Schedule.tsx +++ b/src/pages/Admin/Charity/Dashboard/Schedule/Schedule.tsx @@ -20,6 +20,7 @@ interface Props { classes?: string; periodNext: string; periodRemaining: string; + grantFromBal: number; } export function Schedule(props: Props) { const { id } = useAdminContext(); @@ -94,7 +95,8 @@ export function Schedule(props: Props) { amount={props.amount} tooltip={(val) => val !== 0 && - val < MIN_PROCESSING_AMOUNT && ( + /** include additional grant from bal */ + val + props.grantFromBal < MIN_PROCESSING_AMOUNT && ( res.grantId, }), endowBalance: builder.query({ + providesTags: ["balance"], query: (endowId) => `${v(1)}/balances/${endowId}`, }), stripePaymentStatus: builder.query< diff --git a/src/services/apes/donations.ts b/src/services/apes/donations.ts index a3ed17bb2f..fba52ffed1 100644 --- a/src/services/apes/donations.ts +++ b/src/services/apes/donations.ts @@ -1,5 +1,7 @@ import { IS_TEST } from "constants/env"; import type { + BalanceMovement, + EndowmentBalances, PayoutsPage, PayoutsQueryParams, ReceiptPayload, @@ -9,6 +11,7 @@ import { apes } from "../apes"; export const { useRequestReceiptMutation, + useMoveFundsMutation, useCurrenciesQuery, usePayoutsQuery, useLazyPayoutsQuery, @@ -32,5 +35,18 @@ export const { payouts: builder.query({ query: ({ endowId }) => `endowments/${endowId}/payouts`, }), + moveFunds: builder.mutation< + EndowmentBalances, + BalanceMovement & { endowId: number } + >({ + invalidatesTags: (_, error) => (error ? [] : ["balance"]), + query: ({ endowId, ...payload }) => { + return { + url: `endowments/${endowId}/move-balance`, + method: "PUT", + body: payload, + }; + }, + }), }), }); diff --git a/src/services/apes/tags.ts b/src/services/apes/tags.ts index f953bf3d48..bc9a1e2e10 100644 --- a/src/services/apes/tags.ts +++ b/src/services/apes/tags.ts @@ -1 +1 @@ -export const tags = ["chain", "tokens"] as const; +export const tags = ["chain", "tokens", "balance"] as const; diff --git a/src/types/aws/apes/index.ts b/src/types/aws/apes/index.ts index 80681e29f7..2acae8aacb 100644 --- a/src/types/aws/apes/index.ts +++ b/src/types/aws/apes/index.ts @@ -46,6 +46,15 @@ export interface Chain { >; } +export interface BalanceMovement { + /** investment */ + "liq-lock": number; + /** withdrawal from savings */ + "liq-cash": number; + /** withdrawal from sustainability fund */ + "lock-cash": number; +} + export type EndowmentBalances = { contributionsCount: number; donationsBal: number; @@ -54,6 +63,7 @@ export type EndowmentBalances = { sustainabilityFundBal: number; totalContributions: number; totalEarnings: number; + movementDetails?: BalanceMovement; }; export * from "./paypal";