Skip to content

Commit d1ad5e7

Browse files
javtranfrancisli
andauthored
[Closes #218] Allergy records in admin view (#254)
* Initial push for /allergies and /allergies/:id - Created a new route to view all allergies - Created a new route to view an allergy with id To-do: - Add test for new get API for single allergy - Create edit allergy route * Modal to create new Allergy * added delete and update route for allergies * Can delete an allergy * can edit allergy details * lint fix * changed allergy sidebar icon * Add permissions check to new sidebar items, format * Allow all characters in Allergy code, only require code if System is set --------- Co-authored-by: Francis Li <[email protected]>
1 parent 912ee6d commit d1ad5e7

File tree

17 files changed

+960
-9
lines changed

17 files changed

+960
-9
lines changed

client/src/LifelineAPI.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,59 @@ export default class LifelineAPI {
216216
return response;
217217
}
218218

219+
// --- ALLERGIES ---
220+
221+
static async getAllergies (query, page) {
222+
const response = await fetch(
223+
`${SERVER_BASE_URL}/allergies?allergy=${query}&page=${page}`
224+
);
225+
return response;
226+
}
227+
228+
static async getAllergy (id) {
229+
const response = await fetch(
230+
`${SERVER_BASE_URL}/allergies/${id}`
231+
);
232+
return response;
233+
}
234+
235+
static async createAllergy (data) {
236+
const response = await fetch(`${SERVER_BASE_URL}/allergies/register`, {
237+
method: 'POST',
238+
headers: {
239+
'Content-Type': 'application/json',
240+
},
241+
body: JSON.stringify(data),
242+
});
243+
return response;
244+
}
245+
246+
static async updateAllergy (id, data) {
247+
const response = await fetch(
248+
`${SERVER_BASE_URL}/allergies/${id}`,
249+
{
250+
method: 'PATCH',
251+
headers: {
252+
'Content-Type': 'application/json',
253+
},
254+
body: JSON.stringify(data),
255+
}
256+
);
257+
return response;
258+
}
259+
260+
static async deleteAllergy (allergyId) {
261+
const response = await fetch(
262+
`${SERVER_BASE_URL}/allergies/${allergyId}`,
263+
{
264+
method: 'DELETE',
265+
}
266+
);
267+
return response;
268+
}
269+
219270
// --- MISCELLANEOUS ---
271+
220272
static async getHealthcareChoices (route, query) {
221273
if (route === 'hospital') {
222274
return this.getHospitals(query);

client/src/components/Sidebar/Sidebar.jsx

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
// TbSettings,
99
TbBuildingHospital,
1010
TbStethoscope,
11+
TbMoodSick,
1112
} from 'react-icons/tb';
1213
import { FiLogOut, FiUsers } from 'react-icons/fi';
1314
import { LuLayoutDashboard } from 'react-icons/lu';
@@ -61,6 +62,13 @@ const allNavigationItems = {
6162
label: 'Hospitals',
6263
href: '/hospitals',
6364
icon: <TbBuildingHospital className={classes.navbar__icon} />,
65+
minRole: 'STAFF',
66+
},
67+
{
68+
label: 'Allergies',
69+
href: '/allergies',
70+
icon: <TbMoodSick className={classes.navbar__icon} />,
71+
minRole: 'STAFF',
6472
},
6573
],
6674
},
@@ -110,17 +118,17 @@ export function Sidebar ({ toggleSidebar }) {
110118
const filteredSections = [
111119
{
112120
...allNavigationItems.adminPanel,
113-
links: allNavigationItems.adminPanel.links.filter(link =>
121+
links: allNavigationItems.adminPanel.links.filter((link) =>
114122
hasPermission(link.minRole)
115123
),
116124
},
117125
{
118126
...allNavigationItems.management,
119-
links: allNavigationItems.management.links.filter(link =>
127+
links: allNavigationItems.management.links.filter((link) =>
120128
hasPermission(link.minRole)
121129
),
122130
},
123-
].filter(section => section.links.length > 0);
131+
].filter((section) => section.links.length > 0);
124132

125133
return (
126134
<Stack
@@ -155,7 +163,7 @@ export function Sidebar ({ toggleSidebar }) {
155163
<Text fz='md' fw='600'>
156164
{link.label}
157165
</Text>
158-
}
166+
}
159167
leftSection={link.icon}
160168
target={link.target}
161169
onClick={toggleSidebar}
@@ -178,11 +186,7 @@ export function Sidebar ({ toggleSidebar }) {
178186
</>
179187
)}
180188
</Box>
181-
<a
182-
className={classes.footer__logout}
183-
href='/logout'
184-
onClick={onLogout}
185-
>
189+
<a className={classes.footer__logout} href='/logout' onClick={onLogout}>
186190
<FiLogOut />
187191
</a>
188192
</Group>
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import {
2+
Container,
3+
Group,
4+
TextInput,
5+
Button,
6+
Divider,
7+
Pagination,
8+
LoadingOverlay,
9+
Title,
10+
} from '@mantine/core';
11+
12+
import { TbSearch as IconSearch } from 'react-icons/tb';
13+
14+
import { useDebouncedCallback, useDisclosure } from '@mantine/hooks';
15+
import { useState } from 'react';
16+
17+
import AllergiesTable from './AllergiesTable';
18+
import { useAllergies } from './useAllergies';
19+
import AllergyModal from './AllergyModal';
20+
21+
export default function Allergies () {
22+
const [inputValue, setInputValue] = useState('');
23+
const [opened, { open, close }] = useDisclosure(false);
24+
const { allergies, headers, isFetching, page, pages, setPage, setSearch } = useAllergies();
25+
const handleSearch = useDebouncedCallback((query) => {
26+
setSearch(query);
27+
}, 500);
28+
return (
29+
<Container>
30+
<AllergyModal opened={opened} close={close} />
31+
<Group justify='space-between' wrap='nowrap' my='sm'>
32+
<Title order={3} mr='md'>
33+
Allergies
34+
</Title>
35+
<Group>
36+
<TextInput
37+
leftSectionPointerEvents='none'
38+
leftSection={<IconSearch />}
39+
placeholder='Search'
40+
onChange={(event) => {
41+
setInputValue(event.currentTarget.value);
42+
handleSearch(event.currentTarget.value);
43+
}}
44+
value={inputValue}
45+
/>
46+
<Button variant='filled' onClick={open}>
47+
Create Allergy
48+
</Button>
49+
</Group>
50+
</Group>
51+
<Divider mb='xl' />
52+
<LoadingOverlay
53+
visible={isFetching}
54+
zIndex={1000}
55+
overlayProps={{ radius: 'sm', blur: 2 }}
56+
/>
57+
<AllergiesTable headers={headers} data={allergies} />
58+
<Pagination total={pages} value={page} onChange={setPage} />
59+
</Container>
60+
);
61+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import PropTypes from 'prop-types';
2+
3+
import { useState, useCallback } from 'react';
4+
import { Modal, Button, Text } from '@mantine/core';
5+
import { useDisclosure } from '@mantine/hooks';
6+
import { notifications } from '@mantine/notifications';
7+
8+
import { useAppContext } from '#app/AppContext';
9+
import AllergiesTableRow from './AllergiesTableRow';
10+
import DataTable from '#components/DataTable/DataTable';
11+
import { useDeleteAllergy } from './useDeleteAllergy';
12+
13+
const allergiesTableProps = {
14+
headers: PropTypes.arrayOf(
15+
PropTypes.shape({
16+
key: PropTypes.string.isRequired,
17+
text: PropTypes.node,
18+
})
19+
),
20+
data: PropTypes.arrayOf(
21+
PropTypes.shape({
22+
id: PropTypes.string.isRequired,
23+
name: PropTypes.string.isRequired,
24+
type: PropTypes.string.isRequired,
25+
})
26+
),
27+
};
28+
29+
/**
30+
* Allergies table component
31+
* @param {PropTypes.InferProps<typeof allergiesTableProps>} props
32+
*/
33+
export default function AllergiesTable ({ headers, data }) {
34+
const [opened, { open, close }] = useDisclosure(false);
35+
const [allergy, setSelectedAllergy] = useState(null);
36+
const { mutateAsync: deleteAllergy, isPending } = useDeleteAllergy();
37+
const { user } = useAppContext();
38+
39+
const showDeleteConfirmation = useCallback(
40+
(allergies) => {
41+
setSelectedAllergy(allergies);
42+
open();
43+
},
44+
[open]
45+
);
46+
47+
const confirmAllergyDeletion = async () => {
48+
try {
49+
await deleteAllergy(allergy.id);
50+
notifications.show({
51+
title: 'Success',
52+
message: 'Allergy deleted successfully.',
53+
color: 'green',
54+
});
55+
} catch (error) {
56+
console.error('Failed to delete Allergy:', error);
57+
notifications.show({
58+
title: 'Error',
59+
message: 'Failed to delete Allergy.',
60+
color: 'red',
61+
});
62+
}
63+
if (!isPending) {
64+
setSelectedAllergy(null);
65+
close();
66+
}
67+
};
68+
69+
const cancelAllergyDeletion = () => {
70+
setSelectedAllergy(null);
71+
close();
72+
};
73+
74+
const renderRow = useCallback(
75+
(allergy) => (
76+
<AllergiesTableRow
77+
key={allergy.id}
78+
allergy={allergy}
79+
headers={headers}
80+
onDelete={showDeleteConfirmation}
81+
showDeleteMenu={user?.role === 'ADMIN'}
82+
/>
83+
),
84+
[headers, user?.role, showDeleteConfirmation]
85+
);
86+
87+
return (
88+
<>
89+
<DataTable
90+
headers={headers}
91+
data={data}
92+
renderRow={renderRow}
93+
emptyStateMessage='No allergies found.'
94+
/>
95+
<Modal
96+
opened={opened}
97+
onClose={close}
98+
title='Delete Allergy'
99+
>
100+
<Text fw={600}>
101+
Are you sure you want to delete this Allergy {allergy?.name}?
102+
</Text>
103+
<Button
104+
color='red'
105+
fullWidth
106+
onClick={confirmAllergyDeletion}
107+
loading={isPending}
108+
>
109+
Yes
110+
</Button>
111+
<Button
112+
color='blue'
113+
fullWidth
114+
onClick={cancelAllergyDeletion}
115+
disabled={isPending}
116+
>
117+
No
118+
</Button>
119+
</Modal>
120+
</>
121+
);
122+
}
123+
124+
AllergiesTable.propTypes = allergiesTableProps;
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import PropTypes from 'prop-types';
2+
import { useNavigate, Link } from 'react-router';
3+
import { Table } from '@mantine/core';
4+
import {
5+
TbUser as IconUser,
6+
TbTrash as IconTrash,
7+
} from 'react-icons/tb';
8+
import TableMenu from '#components/DataTable/TableMenu';
9+
10+
const allergiesTableRowProps = {
11+
headers: PropTypes.arrayOf(
12+
PropTypes.shape({
13+
key: PropTypes.string.isRequired,
14+
text: PropTypes.node,
15+
})
16+
),
17+
allergy: PropTypes.shape({
18+
id: PropTypes.string.isRequired,
19+
name: PropTypes.string.isRequired,
20+
type: PropTypes.string.isRequired,
21+
}),
22+
23+
onDelete: PropTypes.func.isRequired,
24+
showDeleteMenu: PropTypes.bool.isRequired,
25+
};
26+
27+
/**
28+
* Allergies table row component
29+
* @param {PropTypes.InferProps<typeof allergiesTableRowProps>} props
30+
*/
31+
export default function AllergiesTableRow ({
32+
headers,
33+
allergy,
34+
onDelete,
35+
showDeleteMenu,
36+
}) {
37+
const navigate = useNavigate();
38+
39+
const menuItems = [
40+
{
41+
icon: <IconUser size={18} />,
42+
label: 'View/Edit',
43+
to: `/allergies/${allergy.id}`,
44+
component: Link,
45+
}
46+
];
47+
48+
if (showDeleteMenu) {
49+
menuItems.push({
50+
icon: <IconTrash size={18} />,
51+
label: 'Delete',
52+
color: 'red',
53+
onClick: () => onDelete({ id: allergy.id, name: allergy.name }),
54+
});
55+
}
56+
57+
return (
58+
<Table.Tr className='clickable' key={allergy.id}>
59+
{headers.map((header) => (
60+
<Table.Td onClick={() => navigate(`/allergies/${allergy.id}`)} key={allergy[header.key] + header.key}>
61+
{allergy[header.key]}
62+
</Table.Td>
63+
))}
64+
<Table.Td style={{ textAlign: 'right' }}>
65+
<TableMenu menuItems={menuItems} />
66+
</Table.Td>
67+
</Table.Tr>
68+
);
69+
}
70+
71+
AllergiesTableRow.propTypes = allergiesTableRowProps;

0 commit comments

Comments
 (0)