Skip to content

Commit

Permalink
Merge pull request #239 from masslight/develop
Browse files Browse the repository at this point in the history
develop to main for v0.2 ballston
  • Loading branch information
saewitz authored Jun 28, 2024
2 parents fd325be + 26f033c commit 9e25370
Show file tree
Hide file tree
Showing 14 changed files with 200 additions and 20 deletions.
2 changes: 1 addition & 1 deletion packages/ehr-utils/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "ehr-utils",
"private": true,
"version": "0.1",
"version": "0.2",
"main": "lib/main.ts",
"types": "lib/main.ts",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion packages/ottehr-components/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "ottehr-components",
"private": true,
"version": "0.1",
"version": "0.2",
"main": "lib/main.ts",
"types": "lib/main.ts",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion packages/telemed-ehr/app/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "telemed-ehr-app",
"version": "0.1",
"version": "0.2",
"private": true,
"browserslist": {
"production": [
Expand Down
2 changes: 2 additions & 0 deletions packages/telemed-ehr/app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import EditStatePage from './telemed/features/telemed-admin/EditState';
import { isLocalOrDevOrTestingOrTrainingEnv } from './telemed/utils/env.helper';
import { RoleType } from './types/types';
import { AppointmentPage } from './pages/AppointmentPage';
import AddSchedulePage from './pages/AddSchedulePage';

const enablePhoton = false && isLocalOrDevOrTestingOrTrainingEnv;

Expand Down Expand Up @@ -102,6 +103,7 @@ function App(): ReactElement {
<Route path="/visits/add" element={<AddPatient />} />
<Route path="/visit/:id" element={<AppointmentPage />} />
<Route path="/schedules" element={<SchedulesPage />} />
<Route path="/schedule/:schedule-type/add" element={<AddSchedulePage />} />
<Route path="/schedule/:schedule-type/:id" element={<SchedulePage />} />
<Route path="/employees" element={<EmployeesPage />} />
<Route path="/employee/:id" element={<EditEmployeePage />} />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ReactElement, useEffect, useMemo, useState } from 'react';
import {
Box,
Button,
Paper,
Table,
TableBody,
Expand All @@ -24,6 +25,7 @@ import { DateTime } from 'luxon';
import { OVERRIDE_DATE_FORMAT } from '../helpers/formatDateTime';
import { Closure, ClosureType, ScheduleExtension } from '../types/types';
import { useApiClients } from '../hooks/useAppClients';
import { Add } from '@mui/icons-material';

export type ScheduleType = 'office' | 'provider' | 'group';

Expand Down Expand Up @@ -221,6 +223,11 @@ export const ScheduleInformation = ({ scheduleType }: ScheduleInformationProps):

return (
<Paper sx={{ padding: 2 }}>
<Link to={`/schedule/${scheduleType}/add`}>
<Button variant="contained" startIcon={<Add />}>
Add {scheduleType}
</Button>
</Link>
<TableContainer>
{/* Items Search Box */}
<Box sx={{ display: 'flex' }}>
Expand Down
86 changes: 86 additions & 0 deletions packages/telemed-ehr/app/src/pages/AddSchedulePage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { LoadingButton } from '@mui/lab';
import { Box, Paper, TextField, Typography } from '@mui/material';
import { ReactElement, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import CustomBreadcrumbs from '../components/CustomBreadcrumbs';
import { useApiClients } from '../hooks/useAppClients';
import PageContainer from '../layout/PageContainer';
import { getResource } from './Schedule';
import { Resource } from 'fhir/r4';

export default function AddSchedulePage(): ReactElement {
// Define variables to interact w database and navigate to other pages
const { fhirClient } = useApiClients();
const navigate = useNavigate();
const scheduleType = useParams()['schedule-type'] as 'office' | 'provider' | 'group';

if (!scheduleType) {
throw new Error('scheduleType is not defined');
}

// state variables
const [name, setName] = useState<string | undefined>(undefined);
const [firstName, setFirstName] = useState<string | undefined>(undefined);
const [lastName, setLastName] = useState<string | undefined>(undefined);
const [loading, setLoading] = useState<boolean>(false);

async function createSchedule(event: any): Promise<void> {
event.preventDefault();
if (!fhirClient) {
return;
}
setLoading(true);
const resource: Resource = await fhirClient.createResource({
resourceType: getResource(scheduleType),
name: scheduleType === 'provider' ? [{ given: [firstName], family: lastName }] : name,
});
navigate(`/schedule/${scheduleType}/${resource.id}`);
setLoading(false);
}

return (
<PageContainer>
<>
<Box marginX={12}>
{/* Breadcrumbs */}
<CustomBreadcrumbs
chain={[
{ link: '/schedules', children: 'Schedules' },
{ link: '#', children: `Add ${scheduleType}` },
]}
/>
<Paper sx={{ padding: 2 }}>
{/* Page title */}
<Typography variant="h3" color="primary.dark" marginTop={1}>
Add {scheduleType}
</Typography>
<form onSubmit={createSchedule}>
{scheduleType === 'provider' ? (
<>
<TextField
label="First name"
required
value={firstName}
onChange={(event) => setFirstName(event.target.value)}
/>
<TextField
label="Last name"
required
value={lastName}
onChange={(event) => setLastName(event.target.value)}
/>
</>
) : (
<TextField label="Name" required value={name} onChange={(event) => setName(event.target.value)} />
)}
<br />
<LoadingButton type="submit" loading={loading} variant="contained" sx={{ marginTop: 2 }}>
Save
</LoadingButton>
</form>
</Paper>
</Box>
</>
</PageContainer>
);
}
103 changes: 91 additions & 12 deletions packages/telemed-ehr/app/src/pages/Schedule.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { TabContext, TabList, TabPanel } from '@mui/lab';
import { Box, Button, Grid, Paper, Skeleton, Tab, Typography } from '@mui/material';
import { Address, Extension, HealthcareService, Location, Practitioner } from 'fhir/r4';
import { LoadingButton, TabContext, TabList, TabPanel } from '@mui/lab';
import { Box, Button, Grid, Paper, Skeleton, Tab, TextField, Typography } from '@mui/material';
import { Address, Extension, HealthcareService, Identifier, Location, Practitioner } from 'fhir/r4';
import { ReactElement, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { otherColors } from '../CustomThemeProvider';
Expand All @@ -11,14 +11,31 @@ import Schedule from '../components/schedule/Schedule';
import { getName } from '../components/ScheduleInformation';
import Loading from '../components/Loading';
import GroupSchedule from '../components/schedule/GroupSchedule';
import { Operation } from 'fast-json-patch';

const START_SCHEDULE =
'{"schedule":{"monday":{"open":8,"close":15,"openingBuffer":0,"closingBuffer":0,"workingDay":true,"hours":[{"hour":8,"capacity":2},{"hour":9,"capacity":2},{"hour":10,"capacity":2},{"hour":11,"capacity":2},{"hour":12,"capacity":2},{"hour":13,"capacity":2},{"hour":14,"capacity":2},{"hour":15,"capacity":2},{"hour":16,"capacity":2},{"hour":17,"capacity":3},{"hour":18,"capacity":3},{"hour":19,"capacity":3},{"hour":20,"capacity":1}]},"tuesday":{"open":8,"close":15,"openingBuffer":0,"closingBuffer":0,"workingDay":true,"hours":[{"hour":8,"capacity":2},{"hour":9,"capacity":2},{"hour":10,"capacity":2},{"hour":11,"capacity":2},{"hour":12,"capacity":2},{"hour":13,"capacity":2},{"hour":14,"capacity":2},{"hour":15,"capacity":2},{"hour":16,"capacity":2},{"hour":17,"capacity":3},{"hour":18,"capacity":3},{"hour":19,"capacity":3},{"hour":20,"capacity":1}]},"wednesday":{"open":8,"close":15,"openingBuffer":0,"closingBuffer":0,"workingDay":true,"hours":[{"hour":8,"capacity":2},{"hour":9,"capacity":2},{"hour":10,"capacity":2},{"hour":11,"capacity":2},{"hour":12,"capacity":2},{"hour":13,"capacity":2},{"hour":14,"capacity":2},{"hour":15,"capacity":2},{"hour":16,"capacity":2},{"hour":17,"capacity":3},{"hour":18,"capacity":3},{"hour":19,"capacity":3},{"hour":20,"capacity":1}]},"thursday":{"open":8,"close":15,"openingBuffer":0,"closingBuffer":0,"workingDay":true,"hours":[{"hour":8,"capacity":2},{"hour":9,"capacity":2},{"hour":10,"capacity":2},{"hour":11,"capacity":2},{"hour":12,"capacity":2},{"hour":13,"capacity":2},{"hour":14,"capacity":2},{"hour":15,"capacity":2},{"hour":16,"capacity":2},{"hour":17,"capacity":3},{"hour":18,"capacity":3},{"hour":19,"capacity":3},{"hour":20,"capacity":1}]},"friday":{"open":8,"close":15,"openingBuffer":0,"closingBuffer":0,"workingDay":true,"hours":[{"hour":8,"capacity":2},{"hour":9,"capacity":2},{"hour":10,"capacity":2},{"hour":11,"capacity":2},{"hour":12,"capacity":2},{"hour":13,"capacity":2},{"hour":14,"capacity":2},{"hour":15,"capacity":2},{"hour":16,"capacity":2},{"hour":17,"capacity":3},{"hour":18,"capacity":3},{"hour":19,"capacity":3},{"hour":20,"capacity":1}]},"saturday":{"open":8,"close":15,"openingBuffer":0,"closingBuffer":0,"workingDay":true,"hours":[{"hour":8,"capacity":2},{"hour":9,"capacity":2},{"hour":10,"capacity":2},{"hour":11,"capacity":2},{"hour":12,"capacity":2},{"hour":13,"capacity":2},{"hour":14,"capacity":2},{"hour":15,"capacity":2},{"hour":16,"capacity":2},{"hour":17,"capacity":3},{"hour":18,"capacity":3},{"hour":19,"capacity":3},{"hour":20,"capacity":1}]},"sunday":{"open":8,"close":15,"openingBuffer":0,"closingBuffer":0,"workingDay":true,"hours":[{"hour":8,"capacity":2},{"hour":9,"capacity":2},{"hour":10,"capacity":2},{"hour":11,"capacity":2},{"hour":12,"capacity":2},{"hour":13,"capacity":2},{"hour":14,"capacity":2},{"hour":15,"capacity":2},{"hour":16,"capacity":2},{"hour":17,"capacity":3},{"hour":18,"capacity":3},{"hour":19,"capacity":3},{"hour":20,"capacity":1}]}},"scheduleOverrides":{}}';
const IDENTIFIER_SLUG = 'https://fhir.ottehr.com/r4/slug';

export function getResource(
scheduleType: 'office' | 'provider' | 'group',
): 'Location' | 'Practitioner' | 'HealthcareService' {
if (scheduleType === 'office') {
return 'Location';
} else if (scheduleType === 'provider') {
return 'Practitioner';
} else if (scheduleType === 'group') {
return 'HealthcareService';
}

console.log(`'scheduleType unknown ${scheduleType}`);
throw new Error('scheduleType unknown');
}

export default function SchedulePage(): ReactElement {
// Define variables to interact w database and navigate to other pages
const { fhirClient } = useApiClients();
const scheduleType = useParams()['schedule-type'];
const scheduleType = useParams()['schedule-type'] as 'office' | 'provider' | 'group';
const id = useParams().id as string;

if (!scheduleType) {
Expand All @@ -28,26 +45,24 @@ export default function SchedulePage(): ReactElement {
// state variables
const [tabName, setTabName] = useState('schedule');
const [item, setItem] = useState<Location | Practitioner | HealthcareService | undefined>(undefined);
const [slug, setSlug] = useState<string | undefined>(undefined);
const [slugLoading, setSlugLoading] = useState<boolean>(false);

// get the location from the database
useEffect(() => {
async function getItem(schedule: 'Location' | 'Practitioner' | 'HealthcareService'): Promise<void> {
if (!fhirClient) {
return;
}
const itemTemp = (await fhirClient.readResource({
const itemTemp: Location | Practitioner | HealthcareService = (await fhirClient.readResource({
resourceType: schedule,
resourceId: id,
})) as any;
setItem(itemTemp);
const slugTemp = itemTemp?.identifier?.find((identifierTemp) => identifierTemp.system === IDENTIFIER_SLUG);
setSlug(slugTemp?.value);
}
if (scheduleType === 'office') {
void getItem('Location');
} else if (scheduleType === 'provider') {
void getItem('Practitioner');
} else if (scheduleType === 'group') {
void getItem('HealthcareService');
}
void getItem(getResource(scheduleType));
}, [fhirClient, id, scheduleType]);

// utility functions
Expand Down Expand Up @@ -112,6 +127,61 @@ export default function SchedulePage(): ReactElement {
setItem(patchedResource);
}

async function updateSlug(event: any): Promise<void> {
event.preventDefault();
setSlugLoading(true);
const identifiers = item?.identifier || [];
// make a copy of identifier
let identifiersTemp: Identifier[] | undefined = [...identifiers];
const hasIdentifiers = identifiersTemp.length > 0;
const hasSlug = item?.identifier?.find((identifierTemp) => identifierTemp.system === IDENTIFIER_SLUG);
const removingSlug = !slug || slug === '';
const updatedSlugIdentifier: Identifier = {
system: IDENTIFIER_SLUG,
value: slug,
};

if (removingSlug && !hasSlug) {
console.log('Removing slug but none set');
setSlugLoading(false);
return;
} else if (removingSlug && hasSlug) {
console.log('Removing slug from identifiers');
const identifiersUpdated = item?.identifier?.filter(
(identifierTemp) => identifierTemp.system !== IDENTIFIER_SLUG,
);
identifiersTemp = identifiersUpdated;
} else if (!hasIdentifiers) {
console.log('No identifiers, adding one');
identifiersTemp = [updatedSlugIdentifier];
} else if (hasIdentifiers && !hasSlug) {
console.log('Has identifiers without a slug, adding one');
identifiersTemp.push(updatedSlugIdentifier);
} else if (hasIdentifiers && hasSlug) {
console.log('Has identifiers with a slug, replacing one');
const identifierIndex = item?.identifier?.findIndex(
(identifierTemp) => identifierTemp.system === IDENTIFIER_SLUG,
);

if (identifierIndex !== undefined && identifiers) {
identifiersTemp[identifierIndex] = updatedSlugIdentifier;
}
}
const operation: Operation = {
op: !hasIdentifiers ? 'add' : 'replace',
path: '/identifier',
value: identifiersTemp,
};

const itemTemp = await fhirClient?.patchResource({
resourceType: getResource(scheduleType),
resourceId: id,
operations: [operation],
});
setItem(itemTemp as Location | Practitioner | HealthcareService);
setSlugLoading(false);
}

return (
<PageContainer>
<>
Expand Down Expand Up @@ -174,6 +244,15 @@ export default function SchedulePage(): ReactElement {
</TabPanel>
{/* General tab */}
<TabPanel value="general">
<Paper sx={{ marginBottom: 2, padding: 3 }}>
<form onSubmit={(event) => updateSlug(event)}>
<TextField label="Slug" value={slug} onChange={(event) => setSlug(event.target.value)} />
<br />
<LoadingButton type="submit" loading={slugLoading} variant="contained" sx={{ marginTop: 2 }}>
Save
</LoadingButton>
</form>
</Paper>
<Paper sx={{ padding: 3 }}>
<Grid container direction="row" justifyContent="flex-start" alignItems="flex-start">
<Grid item xs={6}>
Expand Down
2 changes: 1 addition & 1 deletion packages/telemed-ehr/zambdas/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "telemed-ehrzambdas",
"version": "0.1",
"version": "0.2",
"private": true,
"scripts": {
"start": "npm run start:local",
Expand Down
5 changes: 5 additions & 0 deletions packages/telemed-ehr/zambdas/src/shared/accessPolicies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ export const ADMINISTRATOR_RULES = [
effect: 'Allow',
resource: ['FHIR:Communication', 'FHIR:PractitionerRole'],
},
{
action: ['FHIR:Create'],
effect: 'Allow',
resource: ['FHIR:Location', 'FHIR:Practitioner', 'FHIR:HealthcareService'],
},
{
action: ['Messaging:SendTransactionalSMS'],
effect: 'Allow',
Expand Down
2 changes: 1 addition & 1 deletion packages/telemed-intake/app/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "telemed-intake-app",
"private": true,
"version": "0.1",
"version": "0.2",
"type": "module",
"scripts": {
"start:local": "vite",
Expand Down
2 changes: 1 addition & 1 deletion packages/telemed-intake/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "ottehr-telemed",
"private": true,
"version": "0.1",
"version": "0.2",
"type": "module",
"scripts": {
"start": "vite",
Expand Down
2 changes: 1 addition & 1 deletion packages/telemed-intake/zambdas/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "telemed-intake-zambdas",
"version": "0.1",
"version": "0.2",
"private": true,
"scripts": {
"start": "npm run start:local",
Expand Down
1 change: 1 addition & 0 deletions packages/telemed-intake/zambdas/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export enum PersonSex {
male = 'male',
female = 'female',
other = 'other',
unknown = 'unknown',
}

export enum PatientEthnicity {
Expand Down
2 changes: 1 addition & 1 deletion packages/utils/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "ottehr-utils",
"private": true,
"version": "0.1",
"version": "0.2",
"main": "lib/main.ts",
"types": "lib/main.ts",
"scripts": {
Expand Down

0 comments on commit 9e25370

Please sign in to comment.