Skip to content

Commit

Permalink
[Closes #376] Site Admin can set Hospital order (#377)
Browse files Browse the repository at this point in the history
* Add Hospitals tab to Site Admin and show list

* Drag-and-drop sort

* Formatting
  • Loading branch information
francisli authored Aug 7, 2024
1 parent f8daff4 commit 772655c
Show file tree
Hide file tree
Showing 11 changed files with 156 additions and 2 deletions.
2 changes: 2 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@
"react-router": "^6.14.2",
"react-router-dom": "^6.14.2",
"react-scripts": "5.0.1",
"react-sortablejs": "^6.1.4",
"react-use-websocket": "^2.9.1",
"sass": "^1.57.1",
"sass-loader": "^13.2.0",
"shared": "file:../shared",
"sortablejs": "^1.15.2",
"use-sound": "^4.0.1",
"uswds": "^2.13.3"
},
Expand Down
6 changes: 6 additions & 0 deletions client/src/Admin/AdminNavigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,12 @@ function AdminNavigation() {
>
<HospitalIcon variation="outlined" className="admin-navigation__link-icon" /> <span>Organizations</span>
</NavLink>
<NavLink
to={`${url}/site/hospitals`}
className={({ isActive }) => `admin-navigation__link ${isActive ? 'admin-navigation__link--active' : ''}`}
>
<HospitalIcon variation="outlined" className="admin-navigation__link-icon" /> <span>Hospitals</span>
</NavLink>
</>
)}
{(!user?.isSuperUser || !isSiteAdmin) && (
Expand Down
55 changes: 55 additions & 0 deletions client/src/Admin/Site/Hospitals/HospitalsList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React, { useEffect, useState } from 'react';
import { ReactSortable } from 'react-sortablejs';

import ApiService from '../../../ApiService';

function HospitalsList() {
const [records, setRecords] = useState([]);

useEffect(() => {
ApiService.hospitals.index().then((response) => setRecords(response.data));
}, []);

async function onDrop(records) {
let i = 1;
for (const record of records) {
record.sortSequenceNumber = i++;
}
setRecords(records);
try {
await ApiService.hospitals.sort(records.map((r) => ({ id: r.id, sortSequenceNumber: r.sortSequenceNumber })));
} catch (err) {
console.log(err);
}
}

return (
<main>
<div className="display-flex flex-align-center flex-justify">
<h1>Hospitals</h1>
</div>
<table className="usa-table usa-table--striped usa-table--borderless width-full">
<thead>
<tr>
<th></th>
<th className="w-35">Name</th>
<th className="w-35">Organization</th>
<th>State Facility Code</th>
</tr>
</thead>
<ReactSortable tag="tbody" list={records} setList={(list) => onDrop(list)}>
{records.map((r) => (
<tr key={r.id}>
<td className="cursor--grab">&equiv;</td>
<td>{r.name}</td>
<td>{r.organization?.name}</td>
<td>{r.stateFacilityCode}</td>
</tr>
))}
</ReactSortable>
</table>
</main>
);
}

export default HospitalsList;
13 changes: 13 additions & 0 deletions client/src/Admin/Site/Hospitals/HospitalsRoutes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Routes, Route } from 'react-router-dom';

import HospitalsList from './HospitalsList';

function HospitalsRoutes() {
return (
<Routes>
<Route path="" element={<HospitalsList />} />
</Routes>
);
}

export default HospitalsRoutes;
2 changes: 2 additions & 0 deletions client/src/Admin/Site/SiteRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import { Navigate, Route, Routes } from 'react-router-dom';
import SiteDashboard from './SiteDashboard';
import OrganizationsRoutes from './Organizations/OrganizationsRoutes';
import ClientsRoutes from './Clients/ClientsRoutes';
import HospitalsRoutes from './Hospitals/HospitalsRoutes';

function SiteRoutes() {
return (
<Routes>
<Route path="dashboard" element={<SiteDashboard />} />
<Route path="clients/*" element={<ClientsRoutes />} />
<Route path="organizations/*" element={<OrganizationsRoutes />} />
<Route path="hospitals/*" element={<HospitalsRoutes />} />
<Route path="" element={<Navigate to="dashboard" />} />
</Routes>
);
Expand Down
3 changes: 3 additions & 0 deletions client/src/ApiService.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ export default {
update(id, data) {
return instance.patch(`/api/hospitals/${id}`, data);
},
sort(data) {
return instance.patch(`/api/hospitals/sort`, data);
},
},
hospitalStatuses: {
get() {
Expand Down
4 changes: 4 additions & 0 deletions client/src/theme/_uswds-theme-custom-styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,10 @@ textarea + .usa-radio > .usa-radio__label {
}
}

.cursor--grab {
cursor: grab;
}

// add some width helpers for table column sizing

.w-5 {
Expand Down
7 changes: 5 additions & 2 deletions server/models/hospital.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,11 @@ module.exports = (sequelize) => {
}

toJSON() {
const attributes = { ...this.get() };
return _.pick(attributes, ['id', 'name', 'state', 'stateFacilityCode', 'sortSequenceNumber', 'isActive']);
const json = _.pick(this.get(), ['id', 'name', 'state', 'stateFacilityCode', 'sortSequenceNumber', 'isActive']);
if (this.Organization) {
json.organization = this.Organization.toJSON();
}
return json;
}
}

Expand Down
18 changes: 18 additions & 0 deletions server/routes/api/hospitals.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ router.get('/', middleware.isAdminUser, async (req, res) => {
options.where = {
OrganizationId,
};
} else {
options.include = ['Organization'];
}
const records = await models.Hospital.findAll(options);
res.json(records.map((record) => record.toJSON()));
Expand Down Expand Up @@ -79,6 +81,22 @@ router.get('/:id', middleware.isAdminUser, async (req, res) => {
}
});

router.patch(
'/sort',
middleware.isAdminUser,
wrapper(async (req, res) => {
await models.sequelize.transaction(async (transaction) => {
await Promise.all(
req.body.map((record) => {
const { id, sortSequenceNumber } = record;
return models.Hospital.update({ sortSequenceNumber }, { where: { id }, transaction });
})
);
});
res.status(HttpStatus.OK).end();
})
);

router.patch(
'/:id',
middleware.isAdminUser,
Expand Down
25 changes: 25 additions & 0 deletions server/test/integration/api/hospitals.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,31 @@ describe('/api/hospitals', () => {
});
});

describe('PATCH /sort', () => {
it('bulk updates the sort sequence of the specified hospital records', async () => {
await testSession
.patch('/api/hospitals/sort')
.send([
{
id: '7f666fe4-dbdd-4c7f-ab44-d9157379a680',
sortSequenceNumber: 1,
},
{
id: '00752f60-068f-11eb-adc1-0242ac120002',
sortSequenceNumber: 2,
},
])
.set('Accept', 'application/json')
.expect(HttpStatus.OK);

let record = await models.Hospital.findByPk('7f666fe4-dbdd-4c7f-ab44-d9157379a680');
assert.deepStrictEqual(record.sortSequenceNumber, 1);

record = await models.Hospital.findByPk('00752f60-068f-11eb-adc1-0242ac120002');
assert.deepStrictEqual(record.sortSequenceNumber, 2);
});
});

describe('PATCH /:id', () => {
it('updates an existing Hospital record', async () => {
await testSession
Expand Down
23 changes: 23 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3641,6 +3641,11 @@ classlist-polyfill@1.2.0:
resolved "https://registry.npmjs.org/classlist-polyfill/-/classlist-polyfill-1.2.0.tgz"
integrity sha512-GzIjNdcEtH4ieA2S8NmrSxv7DfEV5fmixQeyTmqmRmRJPGpRBaSnA2a0VrCjyT8iW8JjEdMbKzDotAJf+ajgaQ==

classnames@2.3.1:
version "2.3.1"
resolved "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz"
integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==

classnames@^2.3.1:
version "2.3.2"
resolved "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz"
Expand Down Expand Up @@ -9883,6 +9888,14 @@ react-scripts@5.0.1:
optionalDependencies:
fsevents "^2.3.2"

react-sortablejs@^6.1.4:
version "6.1.4"
resolved "https://registry.npmjs.org/react-sortablejs/-/react-sortablejs-6.1.4.tgz"
integrity sha512-fc7cBosfhnbh53Mbm6a45W+F735jwZ1UFIYSrIqcO/gRIFoDyZeMtgKlpV4DdyQfbCzdh5LoALLTDRxhMpTyXQ==
dependencies:
classnames "2.3.1"
tiny-invariant "1.2.0"

react-use-websocket@^2.9.1:
version "2.9.1"
resolved "https://registry.npmjs.org/react-use-websocket/-/react-use-websocket-2.9.1.tgz"
Expand Down Expand Up @@ -10594,6 +10607,11 @@ sockjs@^0.3.24:
uuid "^8.3.2"
websocket-driver "^0.7.4"

sortablejs@^1.15.2:
version "1.15.2"
resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.15.2.tgz#4e9f7bda4718bd1838add9f1866ec77169149809"
integrity sha512-FJF5jgdfvoKn1MAKSdGs33bIqLi3LmsgVTliuX6iITj834F+JRQZN90Z93yql8h0K2t0RwDPBmxwlbZfDcxNZA==

source-list-map@^2.0.0, source-list-map@^2.0.1:
version "2.0.1"
resolved "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz"
Expand Down Expand Up @@ -11161,6 +11179,11 @@ timers-ext@^0.1.7:
es5-ext "~0.10.46"
next-tick "1"

tiny-invariant@1.2.0:
version "1.2.0"
resolved "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.2.0.tgz"
integrity sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg==

titleize@2:
version "2.1.0"
resolved "https://registry.npmjs.org/titleize/-/titleize-2.1.0.tgz"
Expand Down

0 comments on commit 772655c

Please sign in to comment.