Skip to content

Commit e2c3ab4

Browse files
committed
#1182 - wire up frontend
1 parent c93e603 commit e2c3ab4

6 files changed

Lines changed: 227 additions & 1 deletion

File tree

src/frontend/src/apis/teams.api.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import axios from '../utils/axios';
77
import { Team } from 'shared';
88
import { apiUrls } from '../utils/urls';
9+
import { CreateTeamPayload } from '../hooks/teams.hooks';
910

1011
export const getAllTeams = () => {
1112
return axios.get<Team[]>(apiUrls.teams(), {
@@ -36,3 +37,7 @@ export const setTeamHead = (id: string, userId: number) => {
3637
userId
3738
});
3839
};
40+
41+
export const createTeam = (payload: CreateTeamPayload) => {
42+
return axios.post<Team>(apiUrls.teamsCreate(), payload);
43+
};

src/frontend/src/hooks/teams.hooks.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,14 @@
55

66
import { useQuery, useQueryClient, useMutation } from 'react-query';
77
import { Team } from 'shared';
8-
import { getAllTeams, getSingleTeam, setTeamMembers, setTeamDescription, setTeamHead } from '../apis/teams.api';
8+
import { getAllTeams, getSingleTeam, setTeamMembers, setTeamDescription, setTeamHead, createTeam } from '../apis/teams.api';
9+
10+
export interface CreateTeamPayload {
11+
teamName: string;
12+
headId: number;
13+
slackId: string;
14+
description: string;
15+
}
916

1017
export const useAllTeams = () => {
1118
return useQuery<Team[], Error>(['teams'], async () => {
@@ -67,3 +74,19 @@ export const useEditTeamDescription = (teamId: string) => {
6774
}
6875
);
6976
};
77+
78+
export const useCreateTeam = () => {
79+
const queryClient = useQueryClient();
80+
return useMutation<Team, Error, CreateTeamPayload>(
81+
['teams', 'create'],
82+
async (formData: CreateTeamPayload) => {
83+
const { data } = await createTeam(formData);
84+
return data;
85+
},
86+
{
87+
onSuccess: () => {
88+
queryClient.invalidateQueries(['teams']);
89+
}
90+
}
91+
);
92+
};

src/frontend/src/pages/AdminToolsPage/AdminToolsPage.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { useCurrentUser } from '../../hooks/users.hooks';
99
import { isAdmin } from 'shared';
1010
import PageLayout from '../../components/PageLayout';
1111
import AdminToolsFinanceConfig from './AdminToolsFinanceConfig';
12+
import TeamsTools from './TeamsTools';
1213

1314
const AdminToolsPage: React.FC = () => {
1415
const currentUser = useCurrentUser();
@@ -18,6 +19,7 @@ const AdminToolsPage: React.FC = () => {
1819
<AdminToolsUserManagement />
1920
{isAdmin(currentUser.role) && <AdminToolsSlackUpcomingDeadlines />}
2021
{isAdmin(currentUser.role) && <AdminToolsFinanceConfig />}
22+
{isAdmin(currentUser.role) && <TeamsTools />}
2123
</PageLayout>
2224
);
2325
};
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { Controller, useForm } from 'react-hook-form';
2+
import { useToast } from '../../hooks/toasts.hooks';
3+
import * as yup from 'yup';
4+
import { yupResolver } from '@hookform/resolvers/yup';
5+
import { CreateTeamPayload, useCreateTeam } from '../../hooks/teams.hooks';
6+
import ErrorPage from '../ErrorPage';
7+
import LoadingIndicator from '../../components/LoadingIndicator';
8+
import NERFormModal from '../../components/NERFormModal';
9+
import { FormControl, FormHelperText, FormLabel } from '@mui/material';
10+
import ReactHookTextField from '../../components/ReactHookTextField';
11+
import { useAllUsers } from '../../hooks/users.hooks';
12+
import { User, isHead } from 'shared';
13+
import NERAutocomplete from '../../components/NERAutocomplete';
14+
import { fullNamePipe } from '../../utils/pipes';
15+
16+
const schema = yup.object().shape({
17+
teamName: yup.string().required('Team Name is Required'),
18+
headId: yup.string().required('You must set a Head'),
19+
slackId: yup.string().required('Team Channel SlackId is required'),
20+
description: yup.string().required('Description is Required')
21+
});
22+
23+
interface CreateTeamModalProps {
24+
showModal: boolean;
25+
handleClose: () => void;
26+
}
27+
28+
const userToAutocompleteOption = (user: User): { label: string; id: string } => {
29+
return { label: `${fullNamePipe(user)} (${user.email})`, id: `${user.userId}` };
30+
};
31+
32+
const CreateTeamModal = ({ showModal, handleClose }: CreateTeamModalProps) => {
33+
const { isLoading, mutateAsync } = useCreateTeam();
34+
const { isLoading: allUsersIsLoading, isError: allUsersIsError, error: allUsersError, data: users } = useAllUsers();
35+
const toast = useToast();
36+
37+
const defaultValues = {
38+
teamName: '',
39+
slackId: '',
40+
description: '',
41+
headId: ''
42+
};
43+
44+
const {
45+
handleSubmit,
46+
control,
47+
formState: { errors },
48+
reset
49+
} = useForm({
50+
resolver: yupResolver(schema),
51+
defaultValues
52+
});
53+
54+
if (allUsersIsError) return <ErrorPage message={allUsersError?.message} />;
55+
if (isLoading || allUsersIsLoading || !users) return <LoadingIndicator />;
56+
57+
const onFormSubmit = async (data: CreateTeamPayload) => {
58+
try {
59+
await mutateAsync({ ...data, headId: Number(data.headId) });
60+
reset(defaultValues);
61+
handleClose();
62+
} catch (error: unknown) {
63+
if (error instanceof Error) {
64+
toast.error(error.message, 5000);
65+
}
66+
}
67+
};
68+
69+
const headOptions = users
70+
.filter((user) => isHead(user.role))
71+
.sort((a, b) => (a.firstName > b.firstName ? 1 : -1))
72+
.map(userToAutocompleteOption);
73+
74+
return (
75+
<NERFormModal
76+
open={showModal}
77+
onHide={handleClose}
78+
title={'Create Team'}
79+
reset={reset}
80+
handleUseFormSubmit={handleSubmit}
81+
onFormSubmit={onFormSubmit}
82+
formId={'create-team-form'}
83+
showCloseButton
84+
>
85+
<FormControl fullWidth>
86+
<FormLabel>Team Name</FormLabel>
87+
<ReactHookTextField name="teamName" control={control} fullWidth />
88+
<FormHelperText error>{errors.teamName?.message}</FormHelperText>
89+
</FormControl>
90+
<FormControl fullWidth>
91+
<FormLabel>Head</FormLabel>
92+
<Controller
93+
name="headId"
94+
control={control}
95+
render={({ field: { onChange, value } }) => (
96+
<NERAutocomplete
97+
value={headOptions.find((option) => option.id === value) || { id: '', label: '' }}
98+
onChange={(_event, newValue) => onChange(newValue ? newValue.id : '')}
99+
options={headOptions}
100+
id="create-team-head"
101+
size="small"
102+
placeholder="Choose a user"
103+
/>
104+
)}
105+
/>
106+
<FormHelperText error>{errors.headId?.message}</FormHelperText>
107+
</FormControl>
108+
<FormControl fullWidth>
109+
<FormLabel>Slack Channel ID</FormLabel>
110+
<ReactHookTextField name="slackId" control={control} fullWidth />
111+
<FormHelperText error>{errors.slackId?.message}</FormHelperText>
112+
</FormControl>
113+
<FormControl fullWidth>
114+
<FormLabel>Description</FormLabel>
115+
<ReactHookTextField name="description" control={control} fullWidth multiline rows={5} />
116+
<FormHelperText error>{errors.description?.message}</FormHelperText>
117+
</FormControl>
118+
</NERFormModal>
119+
);
120+
};
121+
122+
export default CreateTeamModal;
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { Box, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from '@mui/material';
2+
import PageBlock from '../../layouts/PageBlock';
3+
import { NERButton } from '../../components/NERButton';
4+
import { useAllTeams } from '../../hooks/teams.hooks';
5+
import LoadingIndicator from '../../components/LoadingIndicator';
6+
import ErrorPage from '../ErrorPage';
7+
import { fullNamePipe } from '../../utils/pipes';
8+
import CreateTeamModal from './CreateTeamModal';
9+
import { useState } from 'react';
10+
11+
const TeamsTools = () => {
12+
const [showCreateModal, setShowCreateModal] = useState(false);
13+
const { data: allTeams, isLoading, isError, error } = useAllTeams();
14+
15+
if (!allTeams || isLoading) return <LoadingIndicator />;
16+
17+
if (isError) {
18+
return <ErrorPage message={error.message} />;
19+
}
20+
21+
const teamTableRows = allTeams.map((team) => (
22+
<TableRow>
23+
<TableCell sx={{ border: '2px solid black' }}>{team.teamName}</TableCell>
24+
<TableCell sx={{ border: '2px solid black' }}>{fullNamePipe(team.head)}</TableCell>
25+
<TableCell sx={{ border: '2px solid black' }}>{team.members.length}</TableCell>
26+
</TableRow>
27+
));
28+
29+
return (
30+
<PageBlock title="Team Management">
31+
<CreateTeamModal showModal={showCreateModal} handleClose={() => setShowCreateModal(false)} />
32+
<TableContainer component={Paper}>
33+
<Table>
34+
<TableHead>
35+
<TableCell
36+
align="left"
37+
sx={{ fontSize: '16px', fontWeight: 600, border: '2px solid black' }}
38+
itemType="date"
39+
width="50%"
40+
>
41+
Team Name
42+
</TableCell>
43+
<TableCell
44+
align="left"
45+
sx={{ fontSize: '16px', fontWeight: 600, border: '2px solid black' }}
46+
itemType="date"
47+
width="30%"
48+
>
49+
Head
50+
</TableCell>
51+
<TableCell
52+
align="left"
53+
sx={{ fontSize: '16px', fontWeight: 600, border: '2px solid black' }}
54+
itemType="date"
55+
width="30%"
56+
>
57+
Number of Members
58+
</TableCell>
59+
</TableHead>
60+
<TableBody>{teamTableRows}</TableBody>
61+
</Table>
62+
</TableContainer>
63+
<Box sx={{ display: 'flex', justifyContent: 'right', marginTop: '10px' }}>
64+
<NERButton variant="contained" onClick={() => setShowCreateModal(true)}>
65+
New Team
66+
</NERButton>
67+
</Box>
68+
</PageBlock>
69+
);
70+
};
71+
72+
export default TeamsTools;

src/frontend/src/utils/urls.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ const teamsById = (id: string) => `${teams()}/${id}`;
7171
const teamsSetMembers = (id: string) => `${teamsById(id)}/set-members`;
7272
const teamsSetHead = (id: string) => `${teamsById(id)}/set-head`;
7373
const teamsSetDescription = (id: string) => `${teamsById(id)}/edit-description`;
74+
const teamsCreate = () => `${teams()}/create`;
7475

7576
/**************** Description Bullet Endpoints ****************/
7677
const descriptionBullets = () => `${API_URL}/description-bullets`;
@@ -154,6 +155,7 @@ export const apiUrls = {
154155
teamsSetMembers,
155156
teamsSetHead,
156157
teamsSetDescription,
158+
teamsCreate,
157159

158160
descriptionBulletsCheck,
159161

0 commit comments

Comments
 (0)