From 21eb6c2e77fe4443d90dd814a42a54ce8a5d814f Mon Sep 17 00:00:00 2001 From: ap-justin <89639563+ap-justin@users.noreply.github.com> Date: Thu, 11 Jan 2024 21:52:44 +0800 Subject: [PATCH] BG-1037: admin members (#2640) * members link * members page * title * users query * members table * add user btn * new endow admin endpoint * add wiring * new endow admin * delete endpoint * add delete loading * update message * include endow name * use is submitting instead --- src/components/Icon/icons.ts | 2 + src/constants/routes.ts | 1 + src/pages/Admin/Charity/Members/AddForm.tsx | 94 ++++++++++++++++ src/pages/Admin/Charity/Members/List.tsx | 112 ++++++++++++++++++++ src/pages/Admin/Charity/Members/Members.tsx | 10 ++ src/pages/Admin/Charity/Members/index.ts | 1 + src/pages/Admin/Charity/index.tsx | 11 +- src/pages/Admin/constants.ts | 9 ++ src/services/aws/aws.ts | 1 + src/services/aws/users.ts | 49 +++++++++ src/types/aws/ap/users.ts | 12 +++ 11 files changed, 300 insertions(+), 2 deletions(-) create mode 100644 src/pages/Admin/Charity/Members/AddForm.tsx create mode 100644 src/pages/Admin/Charity/Members/List.tsx create mode 100644 src/pages/Admin/Charity/Members/Members.tsx create mode 100644 src/pages/Admin/Charity/Members/index.ts create mode 100644 src/services/aws/users.ts create mode 100644 src/types/aws/ap/users.ts diff --git a/src/components/Icon/icons.ts b/src/components/Icon/icons.ts index 6f08103fe4..718f400e0d 100644 --- a/src/components/Icon/icons.ts +++ b/src/components/Icon/icons.ts @@ -52,6 +52,7 @@ import { IoClose, IoCloseCircle, IoCrop, + IoPeople, IoWalletSharp, IoWarning, } from "react-icons/io5"; @@ -138,6 +139,7 @@ export const icons = { Up: VscTriangleUp, Upload: AiOutlineUpload, User: FaUserCircle, + Users: IoPeople, Wallet: IoWalletSharp, Warning: IoWarning, Widget: MdWidgets, diff --git a/src/constants/routes.ts b/src/constants/routes.ts index 88abd94e9c..1627a7ddd7 100644 --- a/src/constants/routes.ts +++ b/src/constants/routes.ts @@ -23,6 +23,7 @@ export const adminRoutes = { banking: "banking", widget_config: "widget-config", donations: "donations", + members: "members", } as const; export enum regRoutes { diff --git a/src/pages/Admin/Charity/Members/AddForm.tsx b/src/pages/Admin/Charity/Members/AddForm.tsx new file mode 100644 index 0000000000..68c5c3bc49 --- /dev/null +++ b/src/pages/Admin/Charity/Members/AddForm.tsx @@ -0,0 +1,94 @@ +import { yupResolver } from "@hookform/resolvers/yup"; +import { + FormProvider, + SubmitHandler, + UseFormReturn, + useForm, +} from "react-hook-form"; +import { object } from "yup"; +import { useLazyProfileQuery } from "services/aws/aws"; +import { useNewEndowAdminMutation } from "services/aws/users"; +import { useModalContext } from "contexts/ModalContext"; +import Modal from "components/Modal"; +import Prompt from "components/Prompt"; +import { Field } from "components/form"; +import { requiredString } from "schemas/string"; + +export type Props = { + endowID: number; + added: string[]; +}; + +export default function AddForm({ added, endowID }: Props) { + const [addAdmin] = useNewEndowAdminMutation(); + const [profile] = useLazyProfileQuery(); + const { setModalOption, showModal } = useModalContext(); + const methods = useForm({ + resolver: yupResolver( + object({ + firstName: requiredString, + lastName: requiredString, + email: requiredString + .email("invalid email") + .notOneOf(added, "already a member"), + }) + ), + }); + + type FV = typeof methods extends UseFormReturn ? U : never; + const { + handleSubmit, + formState: { isSubmitting }, + } = methods; + + const submit: SubmitHandler = async (fv) => { + try { + setModalOption("isDismissible", false); + //get endowname + const { data } = await profile({ id: endowID, fields: ["name"] }); + + await addAdmin({ + firstName: fv.firstName, + lastName: fv.lastName, + email: fv.email, + endowID, + endowName: data?.name || `Endowment:${endowID}`, + }).unwrap(); + + showModal(Prompt, { + headline: "Success!", + children: ( +

+ User succesfully added!{" "} + {fv.email} should signin to + apply new credentials. +

+ ), + }); + } catch (err) { + showModal(Prompt, { + headline: "Error.", + children: ( +

Failed to add {fv.email} to members

+ ), + }); + } + }; + + return ( + + + name="email" label="Email" required /> + name="firstName" label="First name" required /> + name="lastName" label="Last name" required /> + + + + ); +} diff --git a/src/pages/Admin/Charity/Members/List.tsx b/src/pages/Admin/Charity/Members/List.tsx new file mode 100644 index 0000000000..47f840d83e --- /dev/null +++ b/src/pages/Admin/Charity/Members/List.tsx @@ -0,0 +1,112 @@ +import { useAdminContext } from "pages/Admin/Context"; +import { useDeleteEndowAdminMutation, useUsersQuery } from "services/aws/users"; +import { useAuthenticatedUser } from "contexts/Auth"; +import { useModalContext } from "contexts/ModalContext"; +import ContentLoader from "components/ContentLoader"; +import Icon from "components/Icon"; +import QueryLoader from "components/QueryLoader"; +import TableSection, { Cells } from "components/TableSection"; +import AddForm from "./AddForm"; + +export default function List() { + const { showModal } = useModalContext(); + const { id } = useAdminContext(); + + const queryState = useUsersQuery(id); + return ( +
+ + , + error: "Failed to get members", + empty: "No members found.", + }} + > + {(members) => } + +
+ ); +} + +type LoadedProps = { + classes?: string; + members: string[]; +}; +function Loaded({ members, classes = "" }: LoadedProps) { + const { email: user } = useAuthenticatedUser(); + const { id } = useAdminContext(); + const [removeUser, { isLoading }] = useDeleteEndowAdminMutation(); + + async function handleRemove(toRemove: string) { + if (toRemove === user) return window.alert("Can't delete self"); + if (!window.confirm(`Are you sure you want to remove ${toRemove}?`)) return; + + const result = await removeUser({ email: toRemove, endowID: id }); + if ("error" in result) return window.alert("Failed to remove user"); + } + + return ( + + + +
+ <>Email + + + + {members.map((member) => ( + + + <>{member} + + ))} + +
+ ); +} + +function Skeleton() { + return ( +
+ + + + +
+ ); +} diff --git a/src/pages/Admin/Charity/Members/Members.tsx b/src/pages/Admin/Charity/Members/Members.tsx new file mode 100644 index 0000000000..f9b7885f27 --- /dev/null +++ b/src/pages/Admin/Charity/Members/Members.tsx @@ -0,0 +1,10 @@ +import List from "./List"; + +export default function Members() { + return ( +
+

Manage Members

+ +
+ ); +} diff --git a/src/pages/Admin/Charity/Members/index.ts b/src/pages/Admin/Charity/Members/index.ts new file mode 100644 index 0000000000..6ebc690a88 --- /dev/null +++ b/src/pages/Admin/Charity/Members/index.ts @@ -0,0 +1 @@ +export { default } from "./Members"; diff --git a/src/pages/Admin/Charity/index.tsx b/src/pages/Admin/Charity/index.tsx index 62b711b7ff..e3a051b233 100644 --- a/src/pages/Admin/Charity/index.tsx +++ b/src/pages/Admin/Charity/index.tsx @@ -6,6 +6,7 @@ import Banking, { NewPayoutMethod, PayoutMethodDetails } from "./Banking"; import Dashboard from "./Dashboard"; import Donations from "./Donations"; import EditProfile from "./EditProfile"; +import Members from "./Members/Members"; import ProgramEditor from "./ProgramEditor"; import Programs from "./Programs"; import Widget from "./Widget"; @@ -19,19 +20,24 @@ export default function Charity() { linkGroups={[ { links: [LINKS.dashboard, LINKS.donations] }, { title: "Profile", links: [LINKS.edit_profile, LINKS.programs] }, - { title: "Manage", links: [LINKS.banking, LINKS.widget_config] }, + { + title: "Manage", + links: [LINKS.members, LINKS.banking, LINKS.widget_config], + }, ]} /> } > } /> + } /> } /> } /> - } /> + + } /> } /> } /> + } /> } /> ({ + users: builder.query({ + providesTags: ["users"], + query: (endowID) => { + return { + url: `/${v(1)}/users/${endowID}`, + headers: { Authorization: TEMP_JWT }, + }; + }, + }), + newEndowAdmin: builder.mutation({ + invalidatesTags: (_, error) => (error ? [] : ["users"]), + query: ({ endowID, ...payload }) => { + return { + method: "POST", + url: `/${v(1)}/users/${endowID}`, + body: payload, + headers: { Authorization: TEMP_JWT }, + }; + }, + }), + deleteEndowAdmin: builder.mutation({ + invalidatesTags: (_, error) => (error ? [] : ["users"]), + query: ({ endowID, ...payload }) => { + return { + method: "DELETE", + url: `/${v(1)}/users/${endowID}`, + body: payload, + headers: { Authorization: TEMP_JWT }, + }; + }, + }), + }), +}); + +export const { + useUsersQuery, + useNewEndowAdminMutation, + useDeleteEndowAdminMutation, +} = users; diff --git a/src/types/aws/ap/users.ts b/src/types/aws/ap/users.ts new file mode 100644 index 0000000000..ea27d34f22 --- /dev/null +++ b/src/types/aws/ap/users.ts @@ -0,0 +1,12 @@ +export type NewEndowAdminPayload = { + endowID: number; + firstName: string; + lastName: string; + email: string; + endowName: string; +}; + +export type DeleteEndowAdminPayload = { + endowID: number; + email: string; +};