Skip to content

Commit c8072cb

Browse files
committed
Merge branch 'develop' into #1183-delete-team-endpoint
2 parents 10b2282 + 94f182f commit c8072cb

24 files changed

Lines changed: 414 additions & 137 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/prisma/seed.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
Task_Priority,
1313
Task_Status,
1414
Team,
15+
Vendor,
1516
WBS_Element_Status
1617
} from '@prisma/client';
1718
import { dbSeedAllUsers } from './seed-data/users.seed';
@@ -684,6 +685,10 @@ const performSeed: () => Promise<void> = async () => {
684685
);
685686

686687
const vendor = await ReimbursementRequestService.createVendor(thomasEmrax, 'Tesla');
688+
const vendor2 = await ReimbursementRequestService.createVendor(thomasEmrax, 'Amazon');
689+
const vendor3 = await ReimbursementRequestService.createVendor(thomasEmrax, 'Google');
690+
691+
const vendors: Vendor[] = [vendor, vendor2, vendor3];
687692

688693
const expenseType = await ReimbursementRequestService.createExpenseType(thomasEmrax, 'Equipment', 123, true);
689694

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

@@ -30,5 +30,13 @@ teamsRouter.post(
3030
);
3131
teamsRouter.post('/:teamId/set-head', intMinZero(body('userId')), validateInputs, TeamsController.setTeamHead);
3232
teamsRouter.post('/:teamId/delete', TeamsController.deleteTeam);
33+
teamsRouter.post(
34+
'/create',
35+
nonEmptyString(body('teamName')),
36+
intMinZero(body('headId')),
37+
nonEmptyString(body('slackId')),
38+
nonEmptyString(body('description')),
39+
TeamsController.createTeam
40+
);
3341

3442
export default teamsRouter;

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

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

184184
/**
185+
* Creates a new team in the database
186+
* @param submitter The submitter who is trying to create a new team
187+
* @param teamName the name of the new team
188+
* @param headId the id of the user who will be the head on the new team
189+
* @param slackId the slack id for the slack channel for the team
190+
* @param description a short description of the team (must be less than 300 words)
191+
* @returns The newly created team
192+
*/
193+
static async createTeam(
194+
submitter: User,
195+
teamName: string,
196+
headId: number,
197+
slackId: string,
198+
description: string
199+
): Promise<Team> {
200+
if (!isAdmin(submitter.role)) {
201+
throw new AccessDeniedException('You must be an admin or higher to create a new team!');
202+
}
203+
204+
if (!isUnderWordCount(description, 300)) throw new HttpException(400, 'Description must be less than 300 words');
205+
206+
const newHead = await prisma.user.findUnique({
207+
where: { userId: headId }
208+
});
209+
210+
if (!newHead) throw new NotFoundException('User', headId);
211+
if (!isHead(newHead.role)) throw new HttpException(400, 'The team head must be at least a head');
212+
213+
// checking to see if any other teams have the new head as their current head
214+
const newHeadTeam = await prisma.team.findFirst({
215+
where: { headId }
216+
});
217+
218+
if (newHeadTeam) throw new HttpException(400, 'The new team head must not be a head of another team');
219+
220+
const duplicateName = await prisma.team.findFirst({
221+
where: { teamName }
222+
});
223+
224+
if (duplicateName) throw new HttpException(400, 'The new team name must not be the name of another team');
225+
226+
const createdTeam = await prisma.team.create({
227+
data: {
228+
teamName,
229+
slackId,
230+
description,
231+
head: { connect: { userId: headId } }
232+
},
233+
...teamQueryArgs
234+
});
235+
236+
return teamTransformer(createdTeam);
237+
}
238+
239+
/*
185240
* Update the given teamId's team's leads
186241
* @param submitter a user who's making this request
187242
* @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(), {
@@ -41,6 +42,10 @@ export const deleteTeam = (id: string) => {
4142
return axios.post<{ message: string }>(apiUrls.teamsDelete(id));
4243
};
4344

45+
export const createTeam = (payload: CreateTeamPayload) => {
46+
return axios.post<Team>(apiUrls.teamsCreate(), payload);
47+
};
48+
4449
export const setTeamLeads = (id: string, userIds: number[]) => {
4550
return axios.post<Team>(apiUrls.teamsSetLeads(id), {
4651
userIds

src/frontend/src/components/NERAutocomplete.tsx

Lines changed: 23 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@
66
import {
77
Autocomplete,
88
AutocompleteRenderInputParams,
9-
InputAdornment,
9+
FormHelperText,
1010
SxProps,
1111
TextField,
1212
Theme,
1313
useTheme
1414
} from '@mui/material';
15-
import SearchIcon from '@mui/icons-material/Search';
1615
import { HTMLAttributes } from 'react';
16+
import { FieldError } from 'react-hook-form';
1717

1818
interface NERAutocompleteProps {
1919
id: string;
@@ -25,6 +25,7 @@ interface NERAutocompleteProps {
2525
value?: { label: string; id: string } | null;
2626
listboxProps?: HTMLAttributes<HTMLUListElement>;
2727
filterSelectedOptions?: boolean;
28+
errorMessage?: FieldError;
2829
}
2930

3031
const NERAutocomplete: React.FC<NERAutocompleteProps> = ({
@@ -36,23 +37,17 @@ const NERAutocomplete: React.FC<NERAutocompleteProps> = ({
3637
sx,
3738
value,
3839
listboxProps,
39-
filterSelectedOptions
40+
filterSelectedOptions,
41+
errorMessage
4042
}) => {
4143
const theme = useTheme();
4244

4345
const autocompleteStyle = {
4446
height: '40px',
4547
backgroundColor: theme.palette.background.default,
4648
width: '100%',
47-
borderRadius: '25px',
4849
border: 0,
49-
'.MuiOutlinedInput-notchedOutline': {
50-
borderColor: 'black',
51-
borderRadius: '25px'
52-
},
53-
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
54-
borderColor: 'red'
55-
},
50+
borderColor: 'black',
5651
...sx
5752
};
5853

@@ -61,12 +56,7 @@ const NERAutocomplete: React.FC<NERAutocompleteProps> = ({
6156
<TextField
6257
{...params}
6358
InputProps={{
64-
...params.InputProps,
65-
startAdornment: (
66-
<InputAdornment position="start">
67-
<SearchIcon />
68-
</InputAdornment>
69-
)
59+
...params.InputProps
7060
}}
7161
placeholder={placeholder}
7262
required
@@ -75,19 +65,22 @@ const NERAutocomplete: React.FC<NERAutocompleteProps> = ({
7565
};
7666

7767
return (
78-
<Autocomplete
79-
isOptionEqualToValue={(option, value) => option.id === value.id}
80-
disablePortal
81-
id={id}
82-
onChange={onChange}
83-
options={options}
84-
sx={autocompleteStyle}
85-
size={size}
86-
renderInput={autocompleteRenderInput}
87-
value={value}
88-
filterSelectedOptions={filterSelectedOptions}
89-
ListboxProps={listboxProps}
90-
/>
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+
</>
9184
);
9285
};
9386

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: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,18 @@ import {
1111
setTeamMembers,
1212
setTeamDescription,
1313
setTeamHead,
14-
setTeamLeads,
15-
deleteTeam
14+
deleteTeam,
15+
createTeam,
16+
setTeamLeads
1617
} from '../apis/teams.api';
1718

19+
export interface CreateTeamPayload {
20+
teamName: string;
21+
headId: number;
22+
slackId: string;
23+
description: string;
24+
}
25+
1826
export const useAllTeams = () => {
1927
return useQuery<Team[], Error>(['teams'], async () => {
2028
const { data } = await getAllTeams();
@@ -92,6 +100,22 @@ export const useDeleteTeam = () => {
92100
);
93101
};
94102

103+
export const useCreateTeam = () => {
104+
const queryClient = useQueryClient();
105+
return useMutation<Team, Error, CreateTeamPayload>(
106+
['teams', 'create'],
107+
async (formData: CreateTeamPayload) => {
108+
const { data } = await createTeam(formData);
109+
return data;
110+
},
111+
{
112+
onSuccess: () => {
113+
queryClient.invalidateQueries(['teams']);
114+
}
115+
}
116+
);
117+
};
118+
95119
export const useSetTeamLeads = (teamId: string) => {
96120
const queryClient = useQueryClient();
97121
return useMutation<Team, Error, any>(

src/frontend/src/layouts/NavTopBar/NavTopBar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ const NavTopBar: React.FC<NavTopBarProps> = ({ open, handleDrawerOpen }) => {
3535
const user = useCurrentUser();
3636
return (
3737
<NERAppBar position="fixed" open={open}>
38-
<Toolbar disableGutters sx={{ height: 68, px: 1, BACKGROUND, color: TEXT_COLOR }}>
38+
<Toolbar disableGutters sx={{ height: 68, px: 1, background: BACKGROUND, color: TEXT_COLOR }}>
3939
<IconButton
4040
color="inherit"
4141
aria-label="open drawer"
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;

0 commit comments

Comments
 (0)