Skip to content

Commit

Permalink
Merge pull request #48 from uwblueprint/F24/Justin/Invite-User-Button…
Browse files Browse the repository at this point in the history
…-and-Modal

F24/justin/invite user button and modal
  • Loading branch information
JustinScitech authored Nov 20, 2024
2 parents dc09e33 + 74e5981 commit ad5ee70
Show file tree
Hide file tree
Showing 4 changed files with 223 additions and 5 deletions.
19 changes: 17 additions & 2 deletions frontend/src/APIClients/UserAPIClient.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { User } from "../types/UserTypes";
import { User, CreateUserDTO } from "../types/UserTypes";
import AUTHENTICATED_USER_KEY from "../constants/AuthConstants";
import baseAPIClient from "./BaseAPIClient";
import { getLocalStorageObjProperty } from "../utils/LocalStorageUtils";
Expand All @@ -18,4 +18,19 @@ const get = async (): Promise<User[]> => {
}
};

export default { get };
const create = async (formData: CreateUserDTO): Promise<CreateUserDTO> => {
const bearerToken = `Bearer ${getLocalStorageObjProperty(
AUTHENTICATED_USER_KEY,
"accessToken",
)}`;
try {
const { data } = await baseAPIClient.post("/users", formData, {
headers: { Authorization: bearerToken },
});
return data;
} catch (error) {
throw new Error(`Failed to create user: ${error}`);
}
};

export default { get, create };
102 changes: 102 additions & 0 deletions frontend/src/components/crud/AddUserFormModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import React, { useState } from "react";
import { JSONSchema7 } from "json-schema";
import { Form } from "@rjsf/bootstrap-4";
import { IChangeEvent, ISubmitEvent } from "@rjsf/core";

export interface AddUserRequest {
firstName: string;
lastName: string;
phoneNumber: string;
email: string;
role: "Administrator" | "Animal Behaviourist" | "Staff" | "Volunteer";
}

interface AddUserFormModalProps {
onSubmit: (formData: AddUserRequest) => Promise<void>;
}

const userSchema: JSONSchema7 = {
title: "Invite a user",
description: "Enter user details to send an invite",
type: "object",
required: ["firstName", "lastName", "phoneNumber", "email", "role"],
properties: {
firstName: { type: "string", title: "First Name" },
lastName: { type: "string", title: "Last Name" },
phoneNumber: { type: "string", title: "Phone Number" },
email: { type: "string", format: "email", title: "Email" },
role: {
type: "string",
title: "Role",
enum: ["Administrator", "Animal Behaviourist", "Staff", "Volunteer"],
default: "Staff",
},
},
};

const uiSchema = {
role: {
"ui:widget": "select",
},
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const validate = (formData: AddUserRequest, errors: any) => {
const phoneRegex = /^\d{3}-\d{3}-\d{4}$/;
const phoneRegex2 = /^\d{10}$/;
if (
!phoneRegex.test(formData.phoneNumber) &&
!phoneRegex2.test(formData.phoneNumber)
) {
errors.phoneNumber.addError("Phone number must be in xxx-xxx-xxxx format.");
}
if (!formData.email.includes("@")) {
errors.email.addError("Email must be in address@domain format.");
}
return errors;
};

const AddUserFormModal = ({
onSubmit,
}: AddUserFormModalProps): React.ReactElement => {
const [formFields, setFormFields] = useState<AddUserRequest | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

const handleSubmit = async ({ formData }: ISubmitEvent<AddUserRequest>) => {
setLoading(true);
setError(null);
try {
await onSubmit(formData);
setFormFields(null);
} catch (err) {
setError("An error occurred while sending the invite.");
} finally {
setLoading(false);
}
};

return (
<div>
<Form
formData={formFields}
schema={userSchema}
uiSchema={uiSchema}
validate={validate}
onChange={({ formData }: IChangeEvent<AddUserRequest>) =>
setFormFields(formData)
}
onSubmit={handleSubmit}
>
<div style={{ textAlign: "center" }}>
<button type="submit" className="btn btn-primary" disabled={loading}>
{loading ? "Sending..." : "Send Invite"}
</button>
</div>
</Form>
{error && <p style={{ color: "red" }}>{error}</p>}
</div>
);
};

export default AddUserFormModal;
99 changes: 97 additions & 2 deletions frontend/src/components/pages/UserManagementPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,39 @@ import {
TableContainer,
VStack,
Button,
Alert,
AlertIcon,
CloseButton,
} from "@chakra-ui/react";
import UserAPIClient from "../../APIClients/UserAPIClient";
import { User } from "../../types/UserTypes";
import MainPageButton from "../common/MainPageButton";
import AddUserFormModal, { AddUserRequest } from "../crud/AddUserFormModal";

const handleUserSubmit = async (formData: AddUserRequest) => {
// eslint-disable-next-line no-useless-catch
try {
await UserAPIClient.create({
firstName: formData.firstName,
lastName: formData.lastName,
phoneNumber: formData.phoneNumber,
email: formData.email,
role: formData.role as
| "Administrator"
| "Animal Behaviourist"
| "Staff"
| "Volunteer",
});
} catch (error) {
throw error;
}
};

const UserManagementPage = (): React.ReactElement => {
const [users, setUsers] = useState<User[]>([]);
const [isModalOpen, setIsModalOpen] = useState(false);
const [successMessage, setSuccessMessage] = useState<string | null>(null);
const [errorMessage, setErrorMessage] = useState<string | null>(null);

const getUsers = async () => {
try {
Expand All @@ -24,10 +50,22 @@ const UserManagementPage = (): React.ReactElement => {
setUsers(fetchedUsers);
}
} catch (error) {
/* TODO: error handling */
setErrorMessage(`Failed to get users: ${error}`);
}
};

const addUser = () => {
setIsModalOpen(true);
};

const closeModal = () => {
setIsModalOpen(false);
};

const refreshUserManagementTable = async () => {
await getUsers();
};

useEffect(() => {
getUsers();
}, []);
Expand All @@ -36,27 +74,84 @@ const UserManagementPage = (): React.ReactElement => {
<div style={{ textAlign: "center", width: "75%", margin: "0px auto" }}>
<h1>User Management</h1>
<VStack spacing="24px" style={{ margin: "24px auto" }}>
{successMessage && (
<Alert status="success" mb={4}>
<AlertIcon />
{successMessage}
<CloseButton
position="absolute"
right="8px"
top="8px"
onClick={() => setSuccessMessage(null)}
/>
</Alert>
)}
{errorMessage && (
<Alert status="error" mb={4}>
<AlertIcon />
{errorMessage}
<CloseButton
position="absolute"
right="8px"
top="8px"
onClick={() => setErrorMessage(null)}
/>
</Alert>
)}
<TableContainer>
<Table variant="simple">
<Thead>
<Tr>
<Th>First Name</Th>
<Th>Last Name</Th>
<Th>Email</Th>
<Th>Role</Th>
<Th>Status</Th>
</Tr>
</Thead>
<Tbody>
{users.map((user) => (
<Tr key={user.id}>
<Td>{user.firstName}</Td>
<Td>{user.lastName}</Td>
<Td>{user.email}</Td>
<Td>{user.role}</Td>
<Td>{user.status}</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
<Button onClick={getUsers}>Refresh</Button>
<Button onClick={addUser}>+ Add a User</Button>
{isModalOpen && (
<AddUserFormModal
onSubmit={async (formData) => {
// Clear previous messages
setSuccessMessage(null);
setErrorMessage(null);
try {
await handleUserSubmit(formData);
setSuccessMessage("Invite Sent! ✔️");
closeModal();
refreshUserManagementTable();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
// Customize error message based on backend response
if (
error.response &&
error.response.data &&
error.response.data.message
) {
setErrorMessage(error.response.data.message);
} else {
setErrorMessage(
"An error occurred while sending the invite.",
);
}
}
}}
/>
)}
<MainPageButton />
</VStack>
</div>
Expand Down
8 changes: 7 additions & 1 deletion frontend/src/types/UserTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ export type User = {
firstName: string;
lastName: string;
email: string;
role: string;
role: "Administrator" | "Animal Behaviourist" | "Staff" | "Volunteer";
status: string;
skillLevel?: number | null;
canSeeAllLogs?: boolean | null;
canAssignUsersToTasks?: boolean | null;
phoneNumber?: string | null;
};

export type CreateUserDTO = Omit<User, "id" | "status">;

0 comments on commit ad5ee70

Please sign in to comment.