Skip to content

Commit 94f182f

Browse files
authored
Merge pull request #1541 from Northeastern-Electric-Racing/#1182-create-team-endpoint
#1182 - Create Team Endpoint/Admin Tool
2 parents c0356df + a671e7e commit 94f182f

21 files changed

Lines changed: 385 additions & 99 deletions

File tree

src/backend/src/controllers/teams.controllers.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,17 @@ export default class TeamsController {
6363
}
6464
}
6565

66+
static async createTeam(req: Request, res: Response, next: NextFunction) {
67+
try {
68+
const { teamName, headId, slackId, description } = req.body;
69+
const submitter = await getCurrentUser(res);
70+
const team = await TeamsService.createTeam(submitter, teamName, headId, slackId, description);
71+
return res.status(200).json(team);
72+
} catch (error: unknown) {
73+
next(error);
74+
}
75+
}
76+
6677
static async setTeamLeads(req: Request, res: Response, next: NextFunction) {
6778
try {
6879
const { userIds } = req.body;

src/backend/src/routes/teams.routes.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import express from 'express';
22
import TeamsController from '../controllers/teams.controllers';
33
import { body } from 'express-validator';
44
import { validateInputs } from '../utils/utils';
5-
import { intMinZero } from '../utils/validation.utils';
5+
import { intMinZero, nonEmptyString } from '../utils/validation.utils';
66

77
const teamsRouter = express.Router();
88

@@ -29,5 +29,13 @@ teamsRouter.post(
2929
TeamsController.editDescription
3030
);
3131
teamsRouter.post('/:teamId/set-head', intMinZero(body('userId')), validateInputs, TeamsController.setTeamHead);
32+
teamsRouter.post(
33+
'/create',
34+
nonEmptyString(body('teamName')),
35+
intMinZero(body('headId')),
36+
nonEmptyString(body('slackId')),
37+
nonEmptyString(body('description')),
38+
TeamsController.createTeam
39+
);
3240

3341
export default teamsRouter;

src/backend/src/services/teams.services.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,61 @@ export default class TeamsService {
164164
}
165165

166166
/**
167+
* Creates a new team in the database
168+
* @param submitter The submitter who is trying to create a new team
169+
* @param teamName the name of the new team
170+
* @param headId the id of the user who will be the head on the new team
171+
* @param slackId the slack id for the slack channel for the team
172+
* @param description a short description of the team (must be less than 300 words)
173+
* @returns The newly created team
174+
*/
175+
static async createTeam(
176+
submitter: User,
177+
teamName: string,
178+
headId: number,
179+
slackId: string,
180+
description: string
181+
): Promise<Team> {
182+
if (!isAdmin(submitter.role)) {
183+
throw new AccessDeniedException('You must be an admin or higher to create a new team!');
184+
}
185+
186+
if (!isUnderWordCount(description, 300)) throw new HttpException(400, 'Description must be less than 300 words');
187+
188+
const newHead = await prisma.user.findUnique({
189+
where: { userId: headId }
190+
});
191+
192+
if (!newHead) throw new NotFoundException('User', headId);
193+
if (!isHead(newHead.role)) throw new HttpException(400, 'The team head must be at least a head');
194+
195+
// checking to see if any other teams have the new head as their current head
196+
const newHeadTeam = await prisma.team.findFirst({
197+
where: { headId }
198+
});
199+
200+
if (newHeadTeam) throw new HttpException(400, 'The new team head must not be a head of another team');
201+
202+
const duplicateName = await prisma.team.findFirst({
203+
where: { teamName }
204+
});
205+
206+
if (duplicateName) throw new HttpException(400, 'The new team name must not be the name of another team');
207+
208+
const createdTeam = await prisma.team.create({
209+
data: {
210+
teamName,
211+
slackId,
212+
description,
213+
head: { connect: { userId: headId } }
214+
},
215+
...teamQueryArgs
216+
});
217+
218+
return teamTransformer(createdTeam);
219+
}
220+
221+
/*
167222
* Update the given teamId's team's leads
168223
* @param submitter a user who's making this request
169224
* @param teamId a id of team to be updated

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(), {
@@ -37,6 +38,10 @@ export const setTeamHead = (id: string, userId: number) => {
3738
});
3839
};
3940

41+
export const createTeam = (payload: CreateTeamPayload) => {
42+
return axios.post<Team>(apiUrls.teamsCreate(), payload);
43+
};
44+
4045
export const setTeamLeads = (id: string, userIds: number[]) => {
4146
return axios.post<Team>(apiUrls.teamsSetLeads(id), {
4247
userIds

src/frontend/src/components/NERAutocomplete.tsx

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,17 @@
33
* See the LICENSE file in the repository root folder for details.
44
*/
55

6-
import { Autocomplete, AutocompleteRenderInputParams, SxProps, TextField, Theme, useTheme } from '@mui/material';
6+
import {
7+
Autocomplete,
8+
AutocompleteRenderInputParams,
9+
FormHelperText,
10+
SxProps,
11+
TextField,
12+
Theme,
13+
useTheme
14+
} from '@mui/material';
715
import { HTMLAttributes } from 'react';
16+
import { FieldError } from 'react-hook-form';
817

918
interface NERAutocompleteProps {
1019
id: string;
@@ -16,6 +25,7 @@ interface NERAutocompleteProps {
1625
value?: { label: string; id: string } | null;
1726
listboxProps?: HTMLAttributes<HTMLUListElement>;
1827
filterSelectedOptions?: boolean;
28+
errorMessage?: FieldError;
1929
}
2030

2131
const NERAutocomplete: React.FC<NERAutocompleteProps> = ({
@@ -27,7 +37,8 @@ const NERAutocomplete: React.FC<NERAutocompleteProps> = ({
2737
sx,
2838
value,
2939
listboxProps,
30-
filterSelectedOptions
40+
filterSelectedOptions,
41+
errorMessage
3142
}) => {
3243
const theme = useTheme();
3344

@@ -54,19 +65,22 @@ const NERAutocomplete: React.FC<NERAutocompleteProps> = ({
5465
};
5566

5667
return (
57-
<Autocomplete
58-
isOptionEqualToValue={(option, value) => option.id === value.id}
59-
disablePortal
60-
id={id}
61-
onChange={onChange}
62-
options={options}
63-
sx={autocompleteStyle}
64-
size={size}
65-
renderInput={autocompleteRenderInput}
66-
value={value}
67-
filterSelectedOptions={filterSelectedOptions}
68-
ListboxProps={listboxProps}
69-
/>
68+
<>
69+
<Autocomplete
70+
isOptionEqualToValue={(option, value) => option.id === value.id}
71+
disablePortal
72+
id={id}
73+
onChange={onChange}
74+
options={options}
75+
sx={autocompleteStyle}
76+
size={size}
77+
renderInput={autocompleteRenderInput}
78+
value={value}
79+
filterSelectedOptions={filterSelectedOptions}
80+
ListboxProps={listboxProps}
81+
/>
82+
<FormHelperText error={!!errorMessage}>{errorMessage?.message}</FormHelperText>
83+
</>
7084
);
7185
};
7286

src/frontend/src/components/ReactHookTextField.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55
import { Control, Controller, FieldError } from 'react-hook-form';
66
import { TextField, InputAdornment, SxProps, Theme, SvgIconProps } from '@mui/material';
7+
import { countWords, isUnderWordCount } from 'shared';
78

89
interface ReactHookTextFieldProps {
910
name: string;
@@ -20,6 +21,7 @@ interface ReactHookTextFieldProps {
2021
rows?: number;
2122
endAdornment?: React.ReactElement;
2223
errorMessage?: FieldError;
24+
maxLength?: number;
2325
}
2426

2527
const ReactHookTextField: React.FC<ReactHookTextFieldProps> = ({
@@ -36,7 +38,8 @@ const ReactHookTextField: React.FC<ReactHookTextFieldProps> = ({
3638
multiline,
3739
rows,
3840
endAdornment,
39-
errorMessage
41+
errorMessage,
42+
maxLength
4043
}) => {
4144
const defaultRules = { required: true };
4245

@@ -70,10 +73,11 @@ const ReactHookTextField: React.FC<ReactHookTextFieldProps> = ({
7073
sx={sx}
7174
type={type}
7275
InputProps={inputProps}
76+
inputProps={maxLength ? { ...inputProps, maxLength: isUnderWordCount(value, maxLength) ? null : 0 } : undefined}
7377
multiline={multiline}
7478
rows={rows}
7579
error={!!errorMessage}
76-
helperText={errorMessage?.message}
80+
helperText={errorMessage ? errorMessage.message : maxLength ? `${countWords(value)}/300 words` : undefined}
7781
/>
7882
)}
7983
/>

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,17 @@ import {
1111
setTeamMembers,
1212
setTeamDescription,
1313
setTeamHead,
14+
createTeam,
1415
setTeamLeads
1516
} from '../apis/teams.api';
1617

18+
export interface CreateTeamPayload {
19+
teamName: string;
20+
headId: number;
21+
slackId: string;
22+
description: string;
23+
}
24+
1725
export const useAllTeams = () => {
1826
return useQuery<Team[], Error>(['teams'], async () => {
1927
const { data } = await getAllTeams();
@@ -75,6 +83,22 @@ export const useEditTeamDescription = (teamId: string) => {
7583
);
7684
};
7785

86+
export const useCreateTeam = () => {
87+
const queryClient = useQueryClient();
88+
return useMutation<Team, Error, CreateTeamPayload>(
89+
['teams', 'create'],
90+
async (formData: CreateTeamPayload) => {
91+
const { data } = await createTeam(formData);
92+
return data;
93+
},
94+
{
95+
onSuccess: () => {
96+
queryClient.invalidateQueries(['teams']);
97+
}
98+
}
99+
);
100+
};
101+
78102
export const useSetTeamLeads = (teamId: string) => {
79103
const queryClient = useQueryClient();
80104
return useMutation<Team, Error, any>(
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Paper, Table, TableBody, TableCell, TableContainer, TableHead } from '@mui/material';
2+
3+
interface AdminToolTableProps {
4+
columns: {
5+
name: string;
6+
width?: string;
7+
}[];
8+
rows: JSX.Element[];
9+
}
10+
11+
const AdminToolTable = ({ columns, rows }: AdminToolTableProps) => {
12+
return (
13+
<TableContainer component={Paper}>
14+
<Table>
15+
<TableHead>
16+
{columns.map((column, idx) => (
17+
<TableCell
18+
key={`${column.name}-${idx}`}
19+
align="left"
20+
sx={{ fontSize: '16px', fontWeight: 600, border: '2px solid black' }}
21+
width={column.width}
22+
>
23+
{column.name}
24+
</TableCell>
25+
))}
26+
</TableHead>
27+
<TableBody>{rows}</TableBody>
28+
</Table>
29+
</TableContainer>
30+
);
31+
};
32+
33+
export default AdminToolTable;

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
};

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ const AdminToolsUserManagement: React.FC = () => {
8282
}
8383
};
8484

85-
const userToAutocompleteOption = (user: User): { label: string; id: string } => {
85+
const userToAutocompleteOptionWithRole = (user: User): { label: string; id: string } => {
8686
return { label: `${fullNamePipe(user)} (${user.email}) - ${user.role}`, id: user.userId.toString() };
8787
};
8888

@@ -93,10 +93,10 @@ const AdminToolsUserManagement: React.FC = () => {
9393
<NERAutocomplete
9494
id="users-autocomplete"
9595
onChange={usersSearchOnChange}
96-
options={users.filter((user) => rankUserRole(user.role) < currentUserRank).map(userToAutocompleteOption)}
96+
options={users.filter((user) => rankUserRole(user.role) < currentUserRank).map(userToAutocompleteOptionWithRole)}
9797
size="small"
9898
placeholder="Select a User"
99-
value={user ? userToAutocompleteOption(user) : null}
99+
value={user ? userToAutocompleteOptionWithRole(user) : null}
100100
/>
101101
</Grid>
102102
<Grid item xs={12} md={4}>

0 commit comments

Comments
 (0)