Skip to content

Commit 87b6865

Browse files
committed
index code admin tools editting
1 parent 6a120ed commit 87b6865

11 files changed

Lines changed: 294 additions & 8 deletions

File tree

src/backend/src/controllers/reimbursement-requests.controllers.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,23 @@ export default class ReimbursementRequestsController {
525525
}
526526
}
527527

528+
static async editIndexCode(req: Request, res: Response, next: NextFunction) {
529+
try {
530+
const { name, code } = req.body;
531+
const { indexCodeId } = req.params;
532+
const updatedIndexCode = await ReimbursementRequestService.editIndexCode(
533+
req.currentUser,
534+
req.organization,
535+
indexCodeId,
536+
name,
537+
code
538+
);
539+
res.status(200).json(updatedIndexCode);
540+
} catch (error: unknown) {
541+
next(error);
542+
}
543+
}
544+
528545
static async deleteIndexCode(req: Request, res: Response, next: NextFunction) {
529546
try {
530547
const { indexCodeId } = req.params;

src/backend/src/routes/reimbursement-requests.routes.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ reimbursementRequestsRouter.post(
3232
ReimbursementRequestController.createIndexCode
3333
);
3434

35+
reimbursementRequestsRouter.post(
36+
'/index-codes/:indexCodeId/edit',
37+
nonEmptyString(body('name')),
38+
nonEmptyString(body('code')),
39+
validateInputs,
40+
ReimbursementRequestController.editIndexCode
41+
);
3542
reimbursementRequestsRouter.get('/index-codes/:indexCodeId', ReimbursementRequestController.getSingleIndexCode);
3643

3744
reimbursementRequestsRouter.get('/index-codes', ReimbursementRequestController.getAllIndexCodes);

src/backend/src/services/reimbursement-requests.services.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1566,6 +1566,50 @@ export default class ReimbursementRequestService {
15661566
return indexCodes.map(indexCodeTransformer);
15671567
}
15681568

1569+
/**
1570+
* Edit an index code
1571+
* @param user the user editing the index code
1572+
* @param organization the organization the user is in
1573+
* @param indexCodeId the id of the index code to edit
1574+
* @param name the new name of the index code
1575+
* @param code the new code of the index code
1576+
* @returns the updated index code
1577+
*/
1578+
static async editIndexCode(user: User, organization: Organization, indexCodeId: string, name: string, code: string) {
1579+
const indexCode = await prisma.index_Code.findUnique({
1580+
where: {
1581+
indexCodeId
1582+
}
1583+
});
1584+
1585+
if (!userHasPermission(user.userId, organization.organizationId, isAdmin)) {
1586+
throw new AccessDeniedAdminOnlyException('Only admins can update index codes');
1587+
}
1588+
1589+
if (!indexCode) {
1590+
throw new NotFoundException('Index Code', indexCodeId);
1591+
}
1592+
1593+
if (indexCode.dateDeleted) {
1594+
throw new DeletedException('Index Code', indexCodeId);
1595+
}
1596+
1597+
if (indexCode.organizationId !== organization.organizationId) {
1598+
throw new InvalidOrganizationException('Index Code');
1599+
}
1600+
1601+
const updatedCode = await prisma.index_Code.update({
1602+
where: { indexCodeId },
1603+
data: {
1604+
name,
1605+
code
1606+
},
1607+
...getIndexCodeQueryArgs(organization.organizationId)
1608+
});
1609+
1610+
return indexCodeTransformer(updatedCode);
1611+
}
1612+
15691613
/**
15701614
* Deletes the index code
15711615
*

src/frontend/src/apis/finance.api.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ import {
2121
ReimbursementRequestDataPayload,
2222
SpendingBarDataPayload,
2323
ReimbursementRequestCategoryDataPayload,
24-
OtherProductReasonPayload
24+
OtherProductReasonPayload,
25+
IndexCodePayload
2526
} from '../hooks/finance.hooks';
2627
import axios from '../utils/axios';
2728
import { apiUrls } from '../utils/urls';
@@ -493,6 +494,19 @@ export const getAllIndexCodes = () => {
493494
});
494495
};
495496

497+
/**
498+
* Edits an index code
499+
*
500+
* @param indexCodeId the id of the index code to edit
501+
* @param indexCodePayload the new data for the index code
502+
* @returns the updated index code
503+
*/
504+
export const editIndexCode = (indexCodeId: string, indexCodePayload: IndexCodePayload) => {
505+
return axios.post<IndexCode>(apiUrls.editIndexCode(indexCodeId), indexCodePayload, {
506+
transformResponse: (data) => JSON.parse(data)
507+
});
508+
};
509+
496510
/**
497511
* API call to get the list of all Other Product Reason
498512
*

src/frontend/src/hooks/finance.hooks.ts

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ import {
6262
deleteAccountCode,
6363
deleteOtherProductReason,
6464
deleteSponsorTier,
65-
editSponsorTier
65+
editSponsorTier,
66+
editIndexCode
6667
} from '../apis/finance.api';
6768
import {
6869
IndexCode,
@@ -176,6 +177,11 @@ export interface OtherProductReasonPayload {
176177
budget: number;
177178
}
178179

180+
export interface IndexCodePayload {
181+
name: string;
182+
code: string;
183+
}
184+
179185
/**
180186
* Custom React hook to create a sponsor
181187
*
@@ -266,10 +272,6 @@ export const useCreateReimbursementRequestComment = (reimbursementRequestId: str
266272
);
267273
};
268274

269-
export interface IndexCodePayload {
270-
name: string;
271-
}
272-
273275
export interface ReimbursementRequestProjectDataPayload {
274276
projectId: string;
275277
startDate?: Date;
@@ -892,6 +894,26 @@ export const useGetAllIndexCodes = () => {
892894
});
893895
};
894896

897+
/** Custom React Hook to edit an IndexCode
898+
*
899+
* @returns the updated IndexCode
900+
*/
901+
export const useEditIndexCode = (indexCodeId: string) => {
902+
const queryClient = useQueryClient();
903+
return useMutation<IndexCode, Error, IndexCodePayload>(
904+
['index-codes', 'edit'],
905+
async (indexCodePayload: IndexCodePayload) => {
906+
const { data } = await editIndexCode(indexCodeId, indexCodePayload);
907+
return data;
908+
},
909+
{
910+
onSuccess: () => {
911+
queryClient.invalidateQueries(['index-codes']);
912+
}
913+
}
914+
);
915+
};
916+
895917
/**
896918
* Custom React Hook to get all Other Product Reasons
897919
*

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Box, Grid, Typography } from '@mui/material';
22
import AccountManagerTable from './FinanceConfig/AccountManagerTable';
33
import CategoriesTable from './FinanceConfig/CategoriesTable';
44
import SponsorTierTable from './FinanceConfig/SponsorTierTable';
5+
import IndexCodesTable from './FinanceConfig/IndexCodesTable';
56

67
const AdminToolsFinanceConfig: React.FC = () => {
78
return (
@@ -19,6 +20,9 @@ const AdminToolsFinanceConfig: React.FC = () => {
1920
<Grid item direction="column" xs={12} md={6}>
2021
<SponsorTierTable />
2122
</Grid>
23+
<Grid item direction="column" xs={12} md={6}>
24+
<IndexCodesTable />
25+
</Grid>
2226
</Grid>
2327
</Box>
2428
);
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import IndexCodeFormModal from './IndexCodeFormModal';
2+
import { useEditIndexCode } from '../../../hooks/finance.hooks';
3+
import { IndexCode } from 'shared';
4+
import LoadingIndicator from '../../../components/LoadingIndicator';
5+
import ErrorPage from '../../ErrorPage';
6+
7+
interface EditIndexCodeModalProps {
8+
showModal: boolean;
9+
handleClose: () => void;
10+
indexCode: IndexCode;
11+
}
12+
13+
const EditIndexCodeModal: React.FC<EditIndexCodeModalProps> = ({ showModal, handleClose, indexCode }) => {
14+
const { isLoading, isError, error, mutateAsync } = useEditIndexCode(indexCode.indexCodeId);
15+
16+
if (isError) return <ErrorPage message={error?.message} />;
17+
if (isLoading) return <LoadingIndicator />;
18+
19+
return (
20+
<IndexCodeFormModal
21+
defaultValues={indexCode}
22+
showModal={showModal}
23+
handleClose={handleClose}
24+
mutateAsync={mutateAsync}
25+
/>
26+
);
27+
};
28+
29+
export default EditIndexCodeModal;
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { FormControl, FormHelperText, FormLabel } from '@mui/material';
2+
import ReactHookTextField from '../../../components/ReactHookTextField';
3+
import { useForm } from 'react-hook-form';
4+
import { useToast } from '../../../hooks/toasts.hooks';
5+
import * as yup from 'yup';
6+
import { yupResolver } from '@hookform/resolvers/yup';
7+
import { IndexCode } from 'shared';
8+
import NERFormModal from '../../../components/NERFormModal';
9+
import { IndexCodePayload } from '../../../hooks/finance.hooks';
10+
11+
const schema = yup.object().shape({
12+
name: yup.string().required('Name is required'),
13+
code: yup.string().required('Code is required')
14+
});
15+
16+
interface IndexCodeFormModalProps {
17+
showModal: boolean;
18+
handleClose: () => void;
19+
defaultValues?: IndexCode;
20+
mutateAsync: (data: IndexCodePayload) => Promise<IndexCode>;
21+
}
22+
23+
const IndexCodeFormModal: React.FC<IndexCodeFormModalProps> = ({ showModal, handleClose, defaultValues, mutateAsync }) => {
24+
const toast = useToast();
25+
26+
const onSubmit = async (data: IndexCodePayload) => {
27+
try {
28+
await mutateAsync(data);
29+
toast.success(`Index Code: ${data.name} ${defaultValues ? 'edited' : 'created'} successfully!`);
30+
} catch (error: unknown) {
31+
if (error instanceof Error) {
32+
toast.error(error.message);
33+
}
34+
}
35+
handleClose();
36+
};
37+
38+
const {
39+
handleSubmit,
40+
control,
41+
reset,
42+
formState: { errors }
43+
} = useForm({
44+
resolver: yupResolver(schema),
45+
defaultValues: {
46+
name: defaultValues?.name ?? '',
47+
code: defaultValues?.code ?? ''
48+
}
49+
});
50+
51+
return (
52+
<NERFormModal
53+
open={showModal}
54+
onHide={handleClose}
55+
title={`${defaultValues ? 'Edit' : 'Create'} Index Code`}
56+
reset={() => reset({ name: '', code: '' })}
57+
handleUseFormSubmit={handleSubmit}
58+
onFormSubmit={onSubmit}
59+
formId="edit-index-code-form"
60+
showCloseButton
61+
>
62+
<FormControl>
63+
<FormLabel>Name</FormLabel>
64+
<ReactHookTextField name="name" control={control} sx={{ width: 1 }} />
65+
<FormHelperText error>{errors.name?.message}</FormHelperText>
66+
<FormLabel>Code</FormLabel>
67+
<ReactHookTextField name="code" control={control} sx={{ width: 1 }} />
68+
<FormHelperText error>{errors.code?.message}</FormHelperText>
69+
</FormControl>
70+
</NERFormModal>
71+
);
72+
};
73+
74+
export default IndexCodeFormModal;
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { Box, TableCell, TableRow, Typography } from '@mui/material';
2+
import AdminToolTable from '../AdminToolTable';
3+
import { useState } from 'react';
4+
import LoadingIndicator from '../../../components/LoadingIndicator';
5+
import ErrorPage from '../../ErrorPage';
6+
import { useGetAllIndexCodes } from '../../../hooks/finance.hooks';
7+
import { IndexCode } from 'shared';
8+
import EditIndexCodeModal from './EditIndexCodeModal';
9+
10+
const IndexCodesTable: React.FC = () => {
11+
const {
12+
data: indexCodes,
13+
isLoading: indexCodesIsLoading,
14+
isError: indexCodesIsError,
15+
error: indexCodesError
16+
} = useGetAllIndexCodes();
17+
18+
const [indexCodeToEdit, setIndexCodeToEdit] = useState<IndexCode | null>(null);
19+
20+
if (!indexCodes || indexCodesIsLoading) {
21+
return <LoadingIndicator />;
22+
}
23+
if (indexCodesIsError) {
24+
return <ErrorPage message={indexCodesError?.message} />;
25+
}
26+
27+
const indexCodeTableRows = indexCodes.map((indexCode, index) => (
28+
<TableRow key={indexCode.indexCodeId} hover sx={{ cursor: 'pointer' }}>
29+
<TableCell
30+
sx={{ borderBottom: index === indexCodes.length - 1 ? 'none' : 'default' }}
31+
onClick={() => {
32+
setIndexCodeToEdit(indexCode);
33+
}}
34+
>
35+
<Typography>{indexCode.name}</Typography>
36+
</TableCell>
37+
<TableCell
38+
sx={{ borderBottom: index === indexCodes.length - 1 ? 'none' : 'default' }}
39+
onClick={() => {
40+
setIndexCodeToEdit(indexCode);
41+
}}
42+
>
43+
<Typography>{indexCode.code}</Typography>
44+
</TableCell>
45+
</TableRow>
46+
));
47+
48+
return (
49+
<Box>
50+
<Box sx={{ display: 'flex', justifyContent: 'left', marginTop: '20px', paddingBottom: '20px' }}>
51+
<Typography variant="h5" gutterBottom color="white">
52+
Index Codes
53+
</Typography>
54+
</Box>
55+
{indexCodeToEdit && (
56+
<EditIndexCodeModal
57+
showModal={!!indexCodeToEdit}
58+
handleClose={() => setIndexCodeToEdit(null)}
59+
indexCode={indexCodeToEdit}
60+
/>
61+
)}
62+
<AdminToolTable
63+
columns={[
64+
{ name: 'Name', width: '50%' },
65+
{ name: 'Code', width: '50%' }
66+
]}
67+
rows={indexCodeTableRows}
68+
/>
69+
</Box>
70+
);
71+
};
72+
73+
export default IndexCodesTable;

src/frontend/src/pages/FinancePage/ReimbursementRequestForm/ReimbursementProductTable.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,8 @@ const ReimbursementProductTable: React.FC<ReimbursementProductTableProps> = ({
160160

161161
if (refundSources.length > 1) {
162162
reimbursementProducts.forEach((product, index) => {
163-
setValue(`reimbursementProducts.${index}.refundSources.${0}.amount`, product.refundSources[0].amount / 100);
164-
setValue(`reimbursementProducts.${index}.refundSources.${1}.amount`, product.refundSources[1].amount / 100);
163+
setValue(`reimbursementProducts.${index}.refundSources.${0}.amount`, product.refundSources[0]?.amount ?? 0 / 100);
164+
setValue(`reimbursementProducts.${index}.refundSources.${1}.amount`, product.refundSources[1]?.amount ?? 0 / 100);
165165
});
166166
}
167167

0 commit comments

Comments
 (0)