Skip to content

Commit ad5ee70

Browse files
Merge pull request #48 from uwblueprint/F24/Justin/Invite-User-Button-and-Modal
F24/justin/invite user button and modal
2 parents dc09e33 + 74e5981 commit ad5ee70

File tree

4 files changed

+223
-5
lines changed

4 files changed

+223
-5
lines changed

frontend/src/APIClients/UserAPIClient.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { User } from "../types/UserTypes";
1+
import { User, CreateUserDTO } from "../types/UserTypes";
22
import AUTHENTICATED_USER_KEY from "../constants/AuthConstants";
33
import baseAPIClient from "./BaseAPIClient";
44
import { getLocalStorageObjProperty } from "../utils/LocalStorageUtils";
@@ -18,4 +18,19 @@ const get = async (): Promise<User[]> => {
1818
}
1919
};
2020

21-
export default { get };
21+
const create = async (formData: CreateUserDTO): Promise<CreateUserDTO> => {
22+
const bearerToken = `Bearer ${getLocalStorageObjProperty(
23+
AUTHENTICATED_USER_KEY,
24+
"accessToken",
25+
)}`;
26+
try {
27+
const { data } = await baseAPIClient.post("/users", formData, {
28+
headers: { Authorization: bearerToken },
29+
});
30+
return data;
31+
} catch (error) {
32+
throw new Error(`Failed to create user: ${error}`);
33+
}
34+
};
35+
36+
export default { get, create };
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import React, { useState } from "react";
2+
import { JSONSchema7 } from "json-schema";
3+
import { Form } from "@rjsf/bootstrap-4";
4+
import { IChangeEvent, ISubmitEvent } from "@rjsf/core";
5+
6+
export interface AddUserRequest {
7+
firstName: string;
8+
lastName: string;
9+
phoneNumber: string;
10+
email: string;
11+
role: "Administrator" | "Animal Behaviourist" | "Staff" | "Volunteer";
12+
}
13+
14+
interface AddUserFormModalProps {
15+
onSubmit: (formData: AddUserRequest) => Promise<void>;
16+
}
17+
18+
const userSchema: JSONSchema7 = {
19+
title: "Invite a user",
20+
description: "Enter user details to send an invite",
21+
type: "object",
22+
required: ["firstName", "lastName", "phoneNumber", "email", "role"],
23+
properties: {
24+
firstName: { type: "string", title: "First Name" },
25+
lastName: { type: "string", title: "Last Name" },
26+
phoneNumber: { type: "string", title: "Phone Number" },
27+
email: { type: "string", format: "email", title: "Email" },
28+
role: {
29+
type: "string",
30+
title: "Role",
31+
enum: ["Administrator", "Animal Behaviourist", "Staff", "Volunteer"],
32+
default: "Staff",
33+
},
34+
},
35+
};
36+
37+
const uiSchema = {
38+
role: {
39+
"ui:widget": "select",
40+
},
41+
};
42+
43+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
44+
const validate = (formData: AddUserRequest, errors: any) => {
45+
const phoneRegex = /^\d{3}-\d{3}-\d{4}$/;
46+
const phoneRegex2 = /^\d{10}$/;
47+
if (
48+
!phoneRegex.test(formData.phoneNumber) &&
49+
!phoneRegex2.test(formData.phoneNumber)
50+
) {
51+
errors.phoneNumber.addError("Phone number must be in xxx-xxx-xxxx format.");
52+
}
53+
if (!formData.email.includes("@")) {
54+
errors.email.addError("Email must be in address@domain format.");
55+
}
56+
return errors;
57+
};
58+
59+
const AddUserFormModal = ({
60+
onSubmit,
61+
}: AddUserFormModalProps): React.ReactElement => {
62+
const [formFields, setFormFields] = useState<AddUserRequest | null>(null);
63+
const [loading, setLoading] = useState(false);
64+
const [error, setError] = useState<string | null>(null);
65+
66+
const handleSubmit = async ({ formData }: ISubmitEvent<AddUserRequest>) => {
67+
setLoading(true);
68+
setError(null);
69+
try {
70+
await onSubmit(formData);
71+
setFormFields(null);
72+
} catch (err) {
73+
setError("An error occurred while sending the invite.");
74+
} finally {
75+
setLoading(false);
76+
}
77+
};
78+
79+
return (
80+
<div>
81+
<Form
82+
formData={formFields}
83+
schema={userSchema}
84+
uiSchema={uiSchema}
85+
validate={validate}
86+
onChange={({ formData }: IChangeEvent<AddUserRequest>) =>
87+
setFormFields(formData)
88+
}
89+
onSubmit={handleSubmit}
90+
>
91+
<div style={{ textAlign: "center" }}>
92+
<button type="submit" className="btn btn-primary" disabled={loading}>
93+
{loading ? "Sending..." : "Send Invite"}
94+
</button>
95+
</div>
96+
</Form>
97+
{error && <p style={{ color: "red" }}>{error}</p>}
98+
</div>
99+
);
100+
};
101+
102+
export default AddUserFormModal;

frontend/src/components/pages/UserManagementPage.tsx

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,39 @@ import {
99
TableContainer,
1010
VStack,
1111
Button,
12+
Alert,
13+
AlertIcon,
14+
CloseButton,
1215
} from "@chakra-ui/react";
1316
import UserAPIClient from "../../APIClients/UserAPIClient";
1417
import { User } from "../../types/UserTypes";
1518
import MainPageButton from "../common/MainPageButton";
19+
import AddUserFormModal, { AddUserRequest } from "../crud/AddUserFormModal";
20+
21+
const handleUserSubmit = async (formData: AddUserRequest) => {
22+
// eslint-disable-next-line no-useless-catch
23+
try {
24+
await UserAPIClient.create({
25+
firstName: formData.firstName,
26+
lastName: formData.lastName,
27+
phoneNumber: formData.phoneNumber,
28+
email: formData.email,
29+
role: formData.role as
30+
| "Administrator"
31+
| "Animal Behaviourist"
32+
| "Staff"
33+
| "Volunteer",
34+
});
35+
} catch (error) {
36+
throw error;
37+
}
38+
};
1639

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

2046
const getUsers = async () => {
2147
try {
@@ -24,10 +50,22 @@ const UserManagementPage = (): React.ReactElement => {
2450
setUsers(fetchedUsers);
2551
}
2652
} catch (error) {
27-
/* TODO: error handling */
53+
setErrorMessage(`Failed to get users: ${error}`);
2854
}
2955
};
3056

57+
const addUser = () => {
58+
setIsModalOpen(true);
59+
};
60+
61+
const closeModal = () => {
62+
setIsModalOpen(false);
63+
};
64+
65+
const refreshUserManagementTable = async () => {
66+
await getUsers();
67+
};
68+
3169
useEffect(() => {
3270
getUsers();
3371
}, []);
@@ -36,27 +74,84 @@ const UserManagementPage = (): React.ReactElement => {
3674
<div style={{ textAlign: "center", width: "75%", margin: "0px auto" }}>
3775
<h1>User Management</h1>
3876
<VStack spacing="24px" style={{ margin: "24px auto" }}>
77+
{successMessage && (
78+
<Alert status="success" mb={4}>
79+
<AlertIcon />
80+
{successMessage}
81+
<CloseButton
82+
position="absolute"
83+
right="8px"
84+
top="8px"
85+
onClick={() => setSuccessMessage(null)}
86+
/>
87+
</Alert>
88+
)}
89+
{errorMessage && (
90+
<Alert status="error" mb={4}>
91+
<AlertIcon />
92+
{errorMessage}
93+
<CloseButton
94+
position="absolute"
95+
right="8px"
96+
top="8px"
97+
onClick={() => setErrorMessage(null)}
98+
/>
99+
</Alert>
100+
)}
39101
<TableContainer>
40102
<Table variant="simple">
41103
<Thead>
42104
<Tr>
43105
<Th>First Name</Th>
44106
<Th>Last Name</Th>
107+
<Th>Email</Th>
45108
<Th>Role</Th>
109+
<Th>Status</Th>
46110
</Tr>
47111
</Thead>
48112
<Tbody>
49113
{users.map((user) => (
50114
<Tr key={user.id}>
51115
<Td>{user.firstName}</Td>
52116
<Td>{user.lastName}</Td>
117+
<Td>{user.email}</Td>
53118
<Td>{user.role}</Td>
119+
<Td>{user.status}</Td>
54120
</Tr>
55121
))}
56122
</Tbody>
57123
</Table>
58124
</TableContainer>
59-
<Button onClick={getUsers}>Refresh</Button>
125+
<Button onClick={addUser}>+ Add a User</Button>
126+
{isModalOpen && (
127+
<AddUserFormModal
128+
onSubmit={async (formData) => {
129+
// Clear previous messages
130+
setSuccessMessage(null);
131+
setErrorMessage(null);
132+
try {
133+
await handleUserSubmit(formData);
134+
setSuccessMessage("Invite Sent! ✔️");
135+
closeModal();
136+
refreshUserManagementTable();
137+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
138+
} catch (error: any) {
139+
// Customize error message based on backend response
140+
if (
141+
error.response &&
142+
error.response.data &&
143+
error.response.data.message
144+
) {
145+
setErrorMessage(error.response.data.message);
146+
} else {
147+
setErrorMessage(
148+
"An error occurred while sending the invite.",
149+
);
150+
}
151+
}
152+
}}
153+
/>
154+
)}
60155
<MainPageButton />
61156
</VStack>
62157
</div>

frontend/src/types/UserTypes.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ export type User = {
33
firstName: string;
44
lastName: string;
55
email: string;
6-
role: string;
6+
role: "Administrator" | "Animal Behaviourist" | "Staff" | "Volunteer";
77
status: string;
8+
skillLevel?: number | null;
9+
canSeeAllLogs?: boolean | null;
10+
canAssignUsersToTasks?: boolean | null;
11+
phoneNumber?: string | null;
812
};
13+
14+
export type CreateUserDTO = Omit<User, "id" | "status">;

0 commit comments

Comments
 (0)