Skip to content

Commit ebf1e9f

Browse files
committed
add data save and patient autocomplete
1 parent df7ba8c commit ebf1e9f

13 files changed

+328
-56
lines changed

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,12 @@
3131
"test": "react-scripts test --env=jsdom"
3232
},
3333
"dependencies": {
34+
"camelcase": "^5.0.0",
3435
"electron-devtools-installer": "^2.2.4",
3536
"electron-is-dev": "^1.0.1",
37+
"electron-store": "^2.0.0",
3638
"formik": "^1.3.2",
39+
"nedb": "^1.8.0",
3740
"react": "^16.6.3",
3841
"react-dom": "^16.6.3",
3942
"react-scripts": "2.1.1",

public/manifest.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
2-
"short_name": "React App",
3-
"name": "Create React App Sample",
2+
"short_name": "Tracking Tool",
3+
"name": "Tracking Tool",
44
"icons": [
55
{
66
"src": "favicon.ico",

src/App.js

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,53 @@ import 'semantic-ui-css/semantic.min.css';
44

55
import React from 'react';
66
import { Button, Container, Divider, Header, Icon } from 'semantic-ui-react';
7-
import { PatientEncounterForm } from './PatientEncounterForm';
7+
import { ensureUserDirectoryExists, rootPathExists } from './store';
8+
import { Error } from './Error';
89
import { FirstTimeSetup } from './FirstTimeSetup';
10+
import { PatientEncounterForm } from './PatientEncounterForm';
911

1012
function isFirstTime() {
11-
return true;
13+
return !rootPathExists();
1214
}
1315

1416
type AppState = {
15-
encounter: ?string
17+
encounter: ?string,
18+
error: ?string,
19+
firstTimeSetup: boolean
1620
};
1721

1822
class App extends React.Component<{}, AppState> {
1923
state = {
20-
encounter: null
24+
encounter: null,
25+
error: null,
26+
firstTimeSetup: isFirstTime()
2127
};
2228

2329
render() {
24-
const { encounter } = this.state;
30+
const { encounter, error, firstTimeSetup } = this.state;
31+
32+
if (error) {
33+
return <Error error={error} />;
34+
}
35+
36+
if (firstTimeSetup) {
37+
return <FirstTimeSetup onComplete={() => this.setState({ firstTimeSetup: false })} />;
38+
}
2539

26-
if (isFirstTime()) {
27-
return <FirstTimeSetup />;
40+
try {
41+
ensureUserDirectoryExists();
42+
} catch (err) {
43+
this.setState({ error: err });
2844
}
2945

3046
if (encounter === 'patient') {
31-
return <PatientEncounterForm onCancel={() => this.setState({ encounter: null })} />;
47+
return (
48+
<PatientEncounterForm
49+
onCancel={() => this.setState({ encounter: null })}
50+
onComplete={() => this.setState({ encounter: null })}
51+
onError={err => this.setState({ error: err.message })}
52+
/>
53+
);
3254
}
3355

3456
return (

src/Error.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// @flow
2+
3+
import React from 'react';
4+
5+
export class Error extends React.Component<*> {
6+
render() {
7+
const { error } = this.props;
8+
9+
return (
10+
<div>
11+
Tracking Tool encountered an error:
12+
<p>{error}</p>
13+
</div>
14+
);
15+
}
16+
}

src/FirstTimeSetup.js

Lines changed: 45 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,58 @@
1-
import React from 'react';
1+
// @flow
22

3-
import { Button, Header } from 'semantic-ui-react';
3+
import './first-time-setup.css';
4+
import React from 'react';
5+
import { Button, Container, Header, Icon } from 'semantic-ui-react';
6+
import { isEmpty } from 'lodash';
7+
import { setRootPath } from './store';
48

59
const electron = window.require('electron');
6-
// const fs = electron.remote.require('fs');
10+
const fs = electron.remote.require('fs');
11+
const path = electron.remote.require('path');
712

8-
function selectDirectory() {
9-
electron.remote.dialog.showOpenDialog({
10-
buttonLabel: 'Choose Directory',
11-
properties: ['openDirectory']
12-
});
13-
}
13+
const ROOT_DIRECTORY_FILE = 'tracking-tool-root.txt';
14+
15+
type FirstTimeSetupProps = {
16+
onComplete: () => void
17+
};
18+
19+
export class FirstTimeSetup extends React.Component<FirstTimeSetupProps> {
20+
handleOnClick = () => {
21+
const selectedPaths = electron.remote.dialog.showOpenDialog({
22+
buttonLabel: 'Choose Directory',
23+
properties: ['openDirectory']
24+
});
25+
26+
if (isEmpty(selectedPaths)) {
27+
return;
28+
}
29+
30+
const [selectedPath] = selectedPaths;
31+
32+
const rootFilePath = path.join(selectedPath, ROOT_DIRECTORY_FILE);
33+
34+
if (fs.existsSync(rootFilePath)) {
35+
setRootPath(selectedPath);
1436

15-
// fs.readdir('.', (err, files) => {
16-
// console.log({ err, files });
17-
// });
37+
this.props.onComplete();
38+
}
39+
};
1840

19-
export class FirstTimeSetup extends React.Component<*> {
2041
render() {
2142
return (
22-
<React.Fragment>
23-
<Header as="h1" content="First Time Setup" />
43+
<Container text>
44+
<Header as="h1" content="First Time Setup" id="first-time-setup-header" />
2445

25-
<p>
26-
This is the first time you&#39;ve run Tracking Tool on this computer. Please choose your
27-
team&#39;s data directory:
28-
</p>
46+
<Header
47+
as="h2"
48+
content="This is the first time you've run Tracking Tool on this computer. Please choose your team's data directory."
49+
id="first-time-setup-description"
50+
/>
2951

30-
<Button content="Choose data directory" onClick={selectDirectory} size="big" />
31-
</React.Fragment>
52+
<Button icon onClick={this.handleOnClick} primary size="huge">
53+
Choose Team Directory <Icon name="open folder" />
54+
</Button>
55+
</Container>
3256
);
3357
}
3458
}

src/InfoButton.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// @flow
2+
13
import React from 'react';
24
import { Icon, Popup } from 'semantic-ui-react';
35

src/PatientEncounterForm.js

Lines changed: 90 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@ import './App.css';
55
import React from 'react';
66
import { Checkbox, Divider, Dropdown, Grid, Header, Input, Form, Popup } from 'semantic-ui-react';
77
import { DOCTORS } from './doctors';
8+
import { openEncounters } from './data';
89
import { Formik } from 'formik';
910
import { InfoButton } from './InfoButton';
1011
import {
1112
initialInterventionValues,
1213
interventionGroups,
1314
interventionOptions
1415
} from './patient-interventions';
15-
import { isEmpty } from 'lodash';
16+
import { isEmpty, sortBy } from 'lodash';
1617

1718
function makeOptions(options) {
1819
return options.map(option => ({ value: option, text: option }));
@@ -81,30 +82,54 @@ const REQUIRED_FIELDS = [
8182
'encounterDate',
8283
'location',
8384
'md',
84-
'mrn',
85-
'patientName',
86-
'timeSpent'
85+
'patientName'
8786
];
8887

88+
const NUMERIC_FIELDS = ['mrn', 'numberOfTasks', 'timeSpent'];
89+
8990
type PatientEncounterFormProps = {
90-
onCancel: () => void
91+
onCancel: () => void,
92+
onComplete: () => void,
93+
onError: Error => void
9194
};
9295

9396
type PatientEncounterFormState = {
97+
patientOptions: { key: string, text: string, value: string, encounter: ?{} }[],
9498
show: ?string
9599
};
96100

97101
export class PatientEncounterForm extends React.Component<
98102
PatientEncounterFormProps,
99103
PatientEncounterFormState
100104
> {
105+
encounters: *;
106+
101107
state = {
108+
patientOptions: [],
102109
show: null
103110
};
104111

105-
handleCancel = () => this.props.onCancel();
112+
componentDidMount() {
113+
this.encounters = openEncounters();
114+
}
115+
116+
handlePatientSearchChange = (e: *, value: string) => {
117+
this.encounters.find({ patientName: new RegExp(value, 'i') }, (err, docs) => {
118+
// TODO add context like last visit data
119+
const patientOptions = sortBy(docs, ['patientName']).map(doc => ({
120+
key: doc.patientName,
121+
value: doc.patientName,
122+
text: doc.patientName,
123+
encounter: doc
124+
}));
125+
126+
this.setState({ patientOptions });
127+
});
128+
};
106129

107130
render() {
131+
const { patientOptions, show } = this.state;
132+
108133
return (
109134
<Formik
110135
initialValues={INITIAL_VALUES}
@@ -121,13 +146,11 @@ export class PatientEncounterForm extends React.Component<
121146
}
122147
}
123148

124-
if (!/^\d+$/.test(values.numberOfTasks)) {
125-
errors.numberOfTasks = true;
126-
}
127-
128-
if (!/^\d+$/.test(values.timeSpent)) {
129-
errors.timeSpent = true;
130-
}
149+
NUMERIC_FIELDS.forEach(field => {
150+
if (!/^\d+$/.test(values[field])) {
151+
errors[field] = true;
152+
}
153+
});
131154

132155
REQUIRED_FIELDS.forEach(field => {
133156
if (isEmpty(values[field])) {
@@ -138,10 +161,15 @@ export class PatientEncounterForm extends React.Component<
138161
return errors;
139162
}}
140163
onSubmit={(values, { setSubmitting }) => {
141-
setTimeout(() => {
142-
alert(JSON.stringify(values, null, 2));
164+
this.encounters.insert(values, err => {
143165
setSubmitting(false);
144-
}, 400);
166+
167+
if (err) {
168+
this.props.onError(err);
169+
} else {
170+
this.props.onComplete();
171+
}
172+
});
145173
}}
146174
>
147175
{({
@@ -159,6 +187,42 @@ export class PatientEncounterForm extends React.Component<
159187
const handleChange = (e, { name, value, checked }) =>
160188
setFieldValue(name, value !== undefined ? value : checked);
161189

190+
const handlePatientAddition = (e, { value }) => {
191+
this.setState(state => ({
192+
patientOptions: [
193+
{
194+
key: value,
195+
text: value,
196+
value,
197+
encounter: null
198+
},
199+
...state.patientOptions
200+
]
201+
}));
202+
};
203+
204+
const handlePatientChange = (e, { name, value, options }) => {
205+
setFieldValue(name, value);
206+
207+
const selectedOption = options.find(option => option.value === value);
208+
209+
let encounter = selectedOption && selectedOption.encounter;
210+
211+
if (!encounter) {
212+
encounter = INITIAL_VALUES;
213+
}
214+
215+
setFieldValue('mrn', encounter.mrn);
216+
setFieldValue('dateOfBirth', encounter.dateOfBirth);
217+
setFieldValue('clinic', encounter.clinic);
218+
setFieldValue('location', encounter.location);
219+
setFieldValue('md', encounter.md);
220+
setFieldValue('diagnosisType', encounter.diagnosisType);
221+
setFieldValue('diagnosisFreeText', encounter.diagnosisFreeText);
222+
setFieldValue('diagnosisStage', encounter.diagnosisStage);
223+
setFieldValue('research', encounter.research);
224+
};
225+
162226
const renderField = intervention => (
163227
<Form.Field
164228
checked={values[intervention.fieldName]}
@@ -167,7 +231,7 @@ export class PatientEncounterForm extends React.Component<
167231
label={
168232
<label>
169233
{intervention.name}{' '}
170-
{this.state.show === intervention.fieldName && (
234+
{show === intervention.fieldName && (
171235
<InfoButton content={intervention.description} on="hover" />
172236
)}
173237
</label>
@@ -250,14 +314,20 @@ export class PatientEncounterForm extends React.Component<
250314

251315
<Form.Group widths="equal">
252316
<Form.Field
253-
control={Input}
317+
allowAdditions
318+
control={Dropdown}
254319
error={touched.patientName && errors.patientName}
255320
id="input-patient-name"
256321
label="Patient Name"
257322
name="patientName"
323+
onAddItem={handlePatientAddition}
258324
onBlur={handleBlur}
259-
onChange={handleChange}
325+
onChange={handlePatientChange}
326+
options={patientOptions}
327+
onSearchChange={this.handlePatientSearchChange}
260328
placeholder="Last, First Middle"
329+
search
330+
selection
261331
value={values.patientName}
262332
/>
263333

@@ -469,7 +539,7 @@ export class PatientEncounterForm extends React.Component<
469539
trigger={
470540
<Form.Button content="Cancel" disabled={isSubmitting} negative size="big" />
471541
}
472-
content={<Form.Button content="Confirm?" onClick={this.handleCancel} />}
542+
content={<Form.Button content="Confirm?" onClick={this.props.onCancel} />}
473543
on="click"
474544
/>
475545
</Form.Group>

src/data.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// @flow
2+
3+
import { userFilePath } from './store';
4+
5+
const DataStore = window.require('nedb');
6+
7+
export const openEncounters = () =>
8+
new DataStore({
9+
autoload: true,
10+
filename: userFilePath('encounters.json')
11+
});

0 commit comments

Comments
 (0)