Skip to content

Commit

Permalink
BG-1037: admin members (#2640)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
ap-justin committed Jan 11, 2024
1 parent 7bff953 commit 21eb6c2
Show file tree
Hide file tree
Showing 11 changed files with 300 additions and 2 deletions.
2 changes: 2 additions & 0 deletions src/components/Icon/icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
IoClose,
IoCloseCircle,
IoCrop,
IoPeople,
IoWalletSharp,
IoWarning,
} from "react-icons/io5";
Expand Down Expand Up @@ -138,6 +139,7 @@ export const icons = {
Up: VscTriangleUp,
Upload: AiOutlineUpload,
User: FaUserCircle,
Users: IoPeople,
Wallet: IoWalletSharp,
Warning: IoWarning,
Widget: MdWidgets,
Expand Down
1 change: 1 addition & 0 deletions src/constants/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const adminRoutes = {
banking: "banking",
widget_config: "widget-config",
donations: "donations",
members: "members",
} as const;

export enum regRoutes {
Expand Down
94 changes: 94 additions & 0 deletions src/pages/Admin/Charity/Members/AddForm.tsx
Original file line number Diff line number Diff line change
@@ -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<infer U> ? U : never;
const {
handleSubmit,
formState: { isSubmitting },
} = methods;

const submit: SubmitHandler<FV> = 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: (
<p className="py-6">
User succesfully added!{" "}
<span className="font-semibold">{fv.email}</span> should signin to
apply new credentials.
</p>
),
});
} catch (err) {
showModal(Prompt, {
headline: "Error.",
children: (
<p className="py-6 text-red">Failed to add {fv.email} to members</p>
),
});
}
};

return (
<Modal
onSubmit={handleSubmit(submit)}
as="form"
className="p-6 fixed-center z-10 grid gap-4 text-gray-d2 dark:text-white bg-white dark:bg-blue-d4 sm:w-full w-[90vw] sm:max-w-lg rounded overflow-hidden"
>
<FormProvider {...methods}>
<Field<FV> name="email" label="Email" required />
<Field<FV> name="firstName" label="First name" required />
<Field<FV> name="lastName" label="Last name" required />
</FormProvider>
<button disabled={isSubmitting} type="submit" className="btn-orange mt-6">
Add member
</button>
</Modal>
);
}
112 changes: 112 additions & 0 deletions src/pages/Admin/Charity/Members/List.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div>
<button
type="button"
disabled={queryState.isLoading}
className="justify-self-end btn-orange px-4 py-1.5 text-sm gap-2 mb-2"
onClick={() =>
showModal(AddForm, { added: queryState.data || [], endowID: id })
}
>
<Icon type="Plus" />
<span>Invite user</span>
</button>
<QueryLoader
queryState={queryState}
messages={{
loading: <Skeleton />,
error: "Failed to get members",
empty: "No members found.",
}}
>
{(members) => <Loaded members={members} />}
</QueryLoader>
</div>
);
}

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 (
<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"
>
<td className="w-8" />
<>Email</>
</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"
>
{members.map((member) => (
<Cells
key={member}
type="td"
cellClass="p-3 border-t border-prim max-w-[256px] truncate first:rounded-bl last:rounded-br"
>
<button
disabled={isLoading}
onClick={() => handleRemove(member)}
type="button"
className="text-red disabled:text-gray"
>
<Icon type="Dash" />
</button>
<>{member}</>
</Cells>
))}
</TableSection>
</table>
);
}

function Skeleton() {
return (
<div className="grid gap-y-1 mt-2">
<ContentLoader className="h-12 w-full rounded" />
<ContentLoader className="h-12 w-full rounded" />
<ContentLoader className="h-12 w-full rounded" />
<ContentLoader className="h-12 w-full rounded" />
</div>
);
}
10 changes: 10 additions & 0 deletions src/pages/Admin/Charity/Members/Members.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import List from "./List";

export default function Members() {
return (
<div className="grid content-start gap-y-6 @lg:gap-y-8 @container">
<h3 className="text-[2rem]">Manage Members</h3>
<List />
</div>
);
}
1 change: 1 addition & 0 deletions src/pages/Admin/Charity/Members/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./Members";
11 changes: 9 additions & 2 deletions src/pages/Admin/Charity/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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],
},
]}
/>
}
>
<Route path={adminRoutes.donations} element={<Donations />} />

<Route path={adminRoutes.edit_profile} element={<EditProfile />} />
<Route path={adminRoutes.programs} element={<Programs />} />
<Route
path={adminRoutes.program_editor + "/:id"}
element={<ProgramEditor />}
/>
<Route path={adminRoutes.widget_config} element={<Widget />} />

<Route path={adminRoutes.members} element={<Members />} />
<Route path={adminRoutes.banking} element={<Banking />} />
<Route
path={adminRoutes.banking + "/new"}
Expand All @@ -41,6 +47,7 @@ export default function Charity() {
path={adminRoutes.banking + "/:id"}
element={<PayoutMethodDetails />}
/>
<Route path={adminRoutes.widget_config} element={<Widget />} />
<Route index element={<Dashboard />} />
<Route
path="*"
Expand Down
9 changes: 9 additions & 0 deletions src/pages/Admin/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,15 @@ export const LINKS: {
size: 24,
},
},
members: {
title: "Members",
to: "members",
icon: {
type: "Users",
size: 24,
},
},

banking: {
title: "Banking",
to: sidebarRoutes.banking,
Expand Down
1 change: 1 addition & 0 deletions src/services/aws/aws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export const aws = createApi({
"application",
"banking-applications",
"banking-application",
"users",
],
reducerPath: "aws",
baseQuery: awsBaseQuery,
Expand Down
49 changes: 49 additions & 0 deletions src/services/aws/users.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import {
DeleteEndowAdminPayload,
NewEndowAdminPayload,
} from "types/aws/ap/users";
import { TEMP_JWT } from "constants/auth";
import { version as v } from "../helpers";
import { aws } from "./aws";

const users = aws.injectEndpoints({
endpoints: (builder) => ({
users: builder.query<string[], number>({
providesTags: ["users"],
query: (endowID) => {
return {
url: `/${v(1)}/users/${endowID}`,
headers: { Authorization: TEMP_JWT },
};
},
}),
newEndowAdmin: builder.mutation<unknown, NewEndowAdminPayload>({
invalidatesTags: (_, error) => (error ? [] : ["users"]),
query: ({ endowID, ...payload }) => {
return {
method: "POST",
url: `/${v(1)}/users/${endowID}`,
body: payload,
headers: { Authorization: TEMP_JWT },
};
},
}),
deleteEndowAdmin: builder.mutation<unknown, DeleteEndowAdminPayload>({
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;
12 changes: 12 additions & 0 deletions src/types/aws/ap/users.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export type NewEndowAdminPayload = {
endowID: number;
firstName: string;
lastName: string;
email: string;
endowName: string;
};

export type DeleteEndowAdminPayload = {
endowID: number;
email: string;
};

0 comments on commit 21eb6c2

Please sign in to comment.