Merge dev into treatment form

This commit is contained in:
Zain Khan committed 2020-04-29 13:23:31 -05:00
commit a474bc32f4
42 files changed
+2058 -396

No files matched your search

+5 -1
View File
@@ -1,4 +1,8 @@
**/.lighthouseci/**
env/*
.vscode/*
# postgres persisted data
pgdata/
.gitsecret/keys/random_seed
!*.secret
Binary file not shown.
Binary file not shown.
Binary file not shown.
View File
Whitespace-only changes.
+1
View File
@@ -0,0 +1 @@
**/__pycache__/**
+29
View File
@@ -0,0 +1,29 @@
# Database Management
How to load a backup into your postgres docker container
First, decrypt the pg_dump files.
```
git secret reveal
```
Second, restore the pg_dump into the container.
```bash
gunzip -c ./backups/version.sql.gz | docker exec -i hdhs_database_1 psql -U postgres -d app_db
```
## Running Notes
generate backups:
```bash
docker exec hdhs_database_1 pg_dump -U postgres app_db | gzip > backups/20200408.tar.gz
```
Enter database container
```bash
docker-compose -f dev-docker-compose.yml run database bash
```
Enter DB inside container
```bash
psql --host=database --username=postgres --dbname=app_db
```
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
# Author: "abpwrs"
# Date: 20200409
# script:
isort -rc .
black .
+71
View File
@@ -0,0 +1,71 @@
from abc import ABC, abstractmethod
import numpy as np
import pandas as pd
from utils import edit_distance
"""
DESC:
Abstract Base Class (ABC) for a Database Object
Extracts shared logic from the FMP and PSQL classes
"""
class DB(ABC):
def __init__(self):
self._model = None
self.datetime_format = "%Y%m%d_%H%M"
self._table_names = []
# ABSTRACT METHDOS TO BE IMPLEMENTED BY CHILD CLASSES
# ###################################################
@abstractmethod
def read_table(self, table_name) -> pd.DataFrame:
pass
@abstractmethod
def write_table(self, data: pd.DataFrame, table_name):
pass
# ###################################################
@property
def table_names(self) -> list:
return self._table_names
def get_tablename_from_fieldname(self, field_name: str):
try:
assert field_name[:3] == "pk_" or field_name[:3] == "fk_"
except AssertionError as e:
print(
"Can only get meaningful table names from field names on primary or foreign keys"
)
raise (e)
# remove "pk_" or "fk_" --> we only enter this function if this has already matched
extracted_field_to_match = field_name[3:]
# not all fields that start with "pk_" or "fk_" end in "_id"
if field_name[-3:] == "_id":
extracted_field_to_match = extracted_field_to_match[:-3]
else:
print(f"consider fixing fieldname: {field_name}")
# function that returns the edit distance of the table to the extracted field name
matcher = lambda table_name: edit_distance(table_name, extracted_field_to_match)
try:
# map lambda over list of table `names & extract min edit distance
index_of_match = int(
np.argmin(np.array(list(map(matcher, self.table_names))), axis=0)
)
except TypeError as e:
print("FAILURE: two tables have equally close edit_distance")
# TODO: potentially add a backup lookup table here
# for now we will re-raise the error and crash the program
raise (e)
return self.table_names[index_of_match]
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env python
# Author: "hertz"
# Date: 20200310
# ####################
# DESC:
# A script to execute a SQL script inside of postgres docker container
# ####################
# args:
# 1 -- name of container running postgres to execute the script inside of
# 2 -- path to .SQL script to be execute inside of the postgres container
import argparse
import pickle
import sys
from sqlalchemy import create_engine
from psql import PSQL
engine = create_engine("postgres+psycopg2://postgres:HaitiCHI@0.0.0.0:5432/app_db")
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument("command", help="SQL command to run in container")
return parser.parse_args()
def main(argv):
# TODO: extract this from database.env (ideally but it in the PSQL class)
psql = PSQL("postgres+psycopg2://postgres:HaitiCHI@0.0.0.0:5432/app_db")
result = psql.execute_command(argv.command)
print(result)
if __name__ == "__main__":
sys.exit(main(parse_args()))
+168
View File
@@ -0,0 +1,168 @@
import json
from datetime import datetime
import numpy as np
import pandas as pd
import pyodbc
from tqdm import tqdm
from db import DB
from utils import edit_distance
"""
File Maker Pro Connection Object
Desc:
This file contains the primary logic for interacting with the file maker pro database
"""
class FMP(DB):
def __init__(self, dsn=None, username=None, password=None, connect=True):
DB.__init__(self)
if dsn is None:
dsn = input("Enter the FMP DSN: ")
if username is None:
username = input("Enter your FMP username: ")
if password is None:
password = input("Enter your FMP password: ")
self.__dsn = dsn
self.__username = username
self.__password = password
self.__connection_string = "DSN={0};UID={1};PWD={2}".format(
self.__dsn, self.__username, self.__password
)
# TODO: consider extracting this list to the DB class
self._table_names = [
"globals",
"clinics",
"patients",
"visits",
"diagnoses",
"diagnoses_made",
"meds",
"meds_dispensed",
"geonames",
"settings",
]
self._join_tables = [
"diagnoses_made",
"meds_dispensed"
]
self.__python_types_to_sql = {
"str": "VARCHAR",
"date": "TIMESTAMP",
"float64": "FLOAT",
"NoneType": "VARCHAR",
}
self.__pyodbc_connection = None
self.__cursor = None
if connect:
self.__connect()
# METHODS FROM DB SUPER CLASS
# ###########################
def read_table(self, table_name) -> pd.DataFrame:
query = "select * from {table}".format(table=table_name)
df = pd.read_sql(query, self.connection)
return df
def write_table(self, data, table_name):
# TODO: implement write_table in FMP class
print("IMPLEMENT WRITE_TABLE IN FMP")
# ###########################
def __connect(self):
if self.__pyodbc_connection is None:
self.__pyodbc_connection = pyodbc.connect(self.__connection_string)
self.__pyodbc_connection
def execute_command(self, command_string):
result = None
try:
result = pd.read_sql(command_string, self.connection)
except Exception as e:
print(f"Failure to execute: ['{command_string}']")
raise (e) # TODO: determine if crashing is appropriate
return result
@property
def python_types_to_sql(self):
return self.__python_types_to_sql
@property
def connection(self):
if self.__pyodbc_connection is None:
self.__connect()
return self.__pyodbc_connection
@property
def cursor(self):
if self.__cursor is None:
self.__cursor = self.connection.cursor()
return self.__cursor
# ABSTRACT BASE CLASS MEHTODS
def parse_database(self) -> dict():
self._model = dict()
for table_name in tqdm(self.table_names):
df = self.read_table(table_name)
self._model[table_name] = {}
col_types = [
df.iloc[0, index].__class__.__name__ for index in range(len(df.columns))
]
smallest_pk_edit_distance = float("inf")
previous_pk_field = None
for field_name, field_type in zip(df.columns, col_types):
self._model[table_name][field_name] = {}
self._model[table_name][field_name]["type"] = field_type
# extract primary keys
if field_name[:3] == "pk_":
# possible primary key
distance = edit_distance(field_name[3:-3], table_name)
if distance < smallest_pk_edit_distance:
# closest primary key so far
if previous_pk_field is not None:
# remove previously assumed pk
self._model[table_name][previous_pk_field]["pk"] = False
# set current pk
self._model[table_name][field_name]["pk"] = True
previous_pk_field = field_name
smallest_pk_edit_distance = distance
else:
self._model[table_name][field_name]["pk"] = False
else:
self._model[table_name][field_name]["pk"] = False
# extract foreign keys
if field_name[:3] == "fk_":
self._model[table_name][field_name]["fk"] = True
self._model[table_name][field_name][
"fk_table"
] = self.get_tablename_from_fieldname(field_name)
else:
self._model[table_name][field_name]["fk"] = False
return self._model
def write_model(self, model_object: dict = None):
curr_time = datetime.now().strftime(self.datetime_format)
with open("./models/{time}_model.json".format(time=curr_time), "w") as f:
if model_object is not None:
json.dump(model_object, f, indent=4)
else:
json.dump(self._model, f, indent=4)
+148
View File
@@ -0,0 +1,148 @@
import argparse
import os
import pickle
import sys
import pandas as pd
import numpy as np
from tqdm import tqdm
from fmp import FMP
from psql import PSQL
from utils import edit_distance
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument("-d", "--dsn", help="dsn name")
parser.add_argument("-u", "--username", help="username for dsn")
parser.add_argument("-p", "--password", help="password for dsn")
return parser.parse_args()
def get_pk_and_fk_from_table(fmp: FMP, df: pd.DataFrame, table_name: str):
pk = None
fks = []
smallest_pk_edit_distance = float("inf")
for field_name in df.columns:
# extract primary keys
if field_name[:3] == "pk_" and field_name[-3:] == "_id":
# possible primary key
distance = edit_distance(field_name[3:-3], table_name)
if distance < smallest_pk_edit_distance:
pk = field_name
smallest_pk_edit_distance = distance
# extract foreign keys
if field_name[:3] == "fk_" and field_name[-3:] == "_id":
fks.append((field_name, fmp.get_tablename_from_fieldname(field_name)))
return pk, fks
def save_keys(filename: str, all_data: dict):
np.save(filename, all_data)
def load_dict(filename: str):
np_load_old = np.load
np.load = lambda *a, **k,: np_load_old(*a, allow_pickle=True, **k)
np_data = np.load(filename)[()]
# Restore np.load
np.load = np_load_old
return np_data
def main(argv):
fmp = FMP(argv.dsn, argv.username, argv.password)
# TODO: extract username, password, and database name from the database.env file
# TODO: encrypt the database.env file using git-secret
psql = PSQL("postgres+psycopg2://postgres:HaitiCHI@0.0.0.0:5432/app_db")
all_data = dict()
for table_name in tqdm(fmp.table_names):
print(table_name)
df = fmp.read_table(table_name)
# drop all exact duplicates
df.drop_duplicates(inplace=True)
if table_name == "meds_dispensed":
new_df = df[df["pk_meds_dispensed"].notna()]
new_df = new_df[new_df["fk_visit_id"].notna()]
new_df = new_df[new_df["fk_med_key"].notna()]
new_df.drop_duplicates(subset=["pk_meds_dispensed","fk_visit_id", "fk_med_key"], inplace=True)
print(f"Table ({table_name}) lost ({len(df) - len(new_df)}) entries")
df = new_df
elif table_name == "diagnoses_made":
new_df = df[df["pk_diagnoses_made"].notna()]
new_df = new_df[new_df["fk_visit_id"].notna()]
new_df = new_df[new_df["fk_diagnosis_key"].notna()]
new_df.drop_duplicates(subset=["pk_diagnoses_made", "fk_visit_id", "fk_diagnosis_key"], inplace=True)
print(f"Table ({table_name}) lost ({len(df) - len(new_df)}) entries")
df = new_df
elif table_name == "settings":
df['pk_settings_id'] = df.index
elif table_name == "patients":
df.drop(["face_photo"], axis=1, inplace=True)
query = "SELECT GetAs(face_photo, 'JPEG') FROM patients"
result = fmp.cursor.execute(query)
data = result.fetchall()
data = [item[0] for item in data]
df["face_photo"] = data
psql.write_table(df, table_name, if_exists="replace")
pk_field, fk_fields = get_pk_and_fk_from_table(fmp, df, table_name)
all_data[table_name] = (pk_field, fk_fields)
filename = "table_data.npy"
save_keys(filename, all_data)
filename = "table_data.npy"
all_data = load_dict(filename)
for table_name in all_data.keys():
if table_name not in fmp._join_tables:
pk_field, _ = all_data[table_name]
if pk_field is not None:
psql.execute_command(f"ALTER TABLE {table_name} ALTER COLUMN {pk_field} TYPE numeric USING {pk_field}::numeric;")
psql.execute_command(f"ALTER TABLE {table_name} ADD PRIMARY KEY ({pk_field});")
else:
if table_name == "meds_dispensed":
for pk_field in ["pk_meds_dispensed", "fk_visit_id"]:
psql.execute_command(f"ALTER TABLE {table_name} ALTER COLUMN {pk_field} TYPE numeric USING {pk_field}::numeric;")
psql.execute_command(f"ALTER TABLE {table_name} ADD PRIMARY KEY (pk_meds_dispensed, fk_visit_id, fk_med_key);")
elif table_name == "diagnoses_made":
for pk_field in ["pk_diagnoses_made", "fk_visit_id"]:
psql.execute_command(f"ALTER TABLE {table_name} ALTER COLUMN {pk_field} TYPE numeric USING {pk_field}::numeric;")
psql.execute_command(f"ALTER TABLE {table_name} ADD PRIMARY KEY (pk_diagnoses_made, fk_visit_id, fk_diagnosis_key);")
for table_name in all_data.keys():
_, fk_fields = all_data[table_name]
for fk_field, fk_table in fk_fields:
pk_fk_table = all_data[fk_table][0]
# ASSUMES WE ARE ALWAYS LINKING TO THE PK OF THE TABLE
constraint_name = f"fk_{fk_table}_id"
psql.execute_command(f"ALTER TABLE {table_name} ALTER COLUMN {fk_field} TYPE numeric USING {fk_field}::numeric;")
'''
psql.execute_command(
f"ALTER TABLE {table_name} DROP CONSTRAINT IF EXISTS {constraint_name};"
)
'''
try:
psql.execute_command(
f"ALTER TABLE {table_name} ADD CONSTRAINT {constraint_name} FOREIGN KEY ({fk_field}) REFERENCES {fk_table}({pk_fk_table});"
)
except Exception as e:
print(f"FAILURE TO IMPLEMENT CONSTRAINT ON ({table_name}.{fk_field}) to ({fk_table}.{pk_fk_table})")
if __name__ == "__main__":
sys.exit(main(parse_args()))
+58
View File
@@ -0,0 +1,58 @@
0%| | 0/10 [00:00<?, ?it/s]
globals
10%|██████████▌ | 1/10 [00:00<00:03, 2.90it/s]
clinics
patients
30%|███████████████████████████████▏ | 3/10 [29:52<31:22, 269.00s/it]
visits
40%|█████████████████████████████████████████▌ | 4/10 [33:20<25:04, 250.80s/it]
diagnoses
50%|████████████████████████████████████████████████████ | 5/10 [33:20<14:38, 175.70s/it]
diagnoses_made
Table (diagnoses_made) lost (0) entries
60%|██████████████████████████████████████████████████████████████▍ | 6/10 [35:53<11:14, 168.71s/it]
meds
70%|████████████████████████████████████████████████████████████████████████▊ | 7/10 [35:53<05:54, 118.17s/it]
meds_dispensed
Table (meds_dispensed) lost (1) entries
80%|███████████████████████████████████████████████████████████████████████████████████▏ | 8/10 [40:33<05:33, 166.78s/it]
geonames
90%|█████████████████████████████████████████████████████████████████████████████████████████████▌ | 9/10 [41:14<02:08, 128.92s/it]
settings
100%|███████████████████████████████████████████████████████████████████████████████████████████████████████ | 10/10 [41:14<00:00, 247.44s/it]
PSQL EXECUTING: [ALTER TABLE clinics ALTER COLUMN pk_clinic_id TYPE numeric USING pk_clinic_id::numeric;]
PSQL EXECUTING: [ALTER TABLE clinics ADD PRIMARY KEY (pk_clinic_id);]
PSQL EXECUTING: [ALTER TABLE patients ALTER COLUMN pk_patient_id TYPE numeric USING pk_patient_id::numeric;]
PSQL EXECUTING: [ALTER TABLE patients ADD PRIMARY KEY (pk_patient_id);]
PSQL EXECUTING: [ALTER TABLE visits ALTER COLUMN pk_visit_id TYPE numeric USING pk_visit_id::numeric;]
PSQL EXECUTING: [ALTER TABLE visits ADD PRIMARY KEY (pk_visit_id);]
PSQL EXECUTING: [ALTER TABLE diagnoses ALTER COLUMN pk_diagnosis_id TYPE numeric USING pk_diagnosis_id::numeric;]
PSQL EXECUTING: [ALTER TABLE diagnoses ADD PRIMARY KEY (pk_diagnosis_id);]
PSQL EXECUTING: [ALTER TABLE diagnoses_made ALTER COLUMN pk_diagnoses_made TYPE numeric USING pk_diagnoses_made::numeric;]
PSQL EXECUTING: [ALTER TABLE diagnoses_made ALTER COLUMN fk_visit_id TYPE numeric USING fk_visit_id::numeric;]
PSQL EXECUTING: [ALTER TABLE diagnoses_made ADD PRIMARY KEY (pk_diagnoses_made, fk_visit_id, fk_diagnosis_key);]
PSQL EXECUTING: [ALTER TABLE meds ALTER COLUMN pk_med_id TYPE numeric USING pk_med_id::numeric;]
PSQL EXECUTING: [ALTER TABLE meds ADD PRIMARY KEY (pk_med_id);]
PSQL EXECUTING: [ALTER TABLE meds_dispensed ALTER COLUMN pk_meds_dispensed TYPE numeric USING pk_meds_dispensed::numeric;]
PSQL EXECUTING: [ALTER TABLE meds_dispensed ALTER COLUMN fk_visit_id TYPE numeric USING fk_visit_id::numeric;]
PSQL EXECUTING: [ALTER TABLE meds_dispensed ADD PRIMARY KEY (pk_meds_dispensed, fk_visit_id, fk_med_key);]
PSQL EXECUTING: [ALTER TABLE geonames ALTER COLUMN pk_geoname_id TYPE numeric USING pk_geoname_id::numeric;]
PSQL EXECUTING: [ALTER TABLE geonames ADD PRIMARY KEY (pk_geoname_id);]
PSQL EXECUTING: [ALTER TABLE settings ALTER COLUMN pk_settings_id TYPE numeric USING pk_settings_id::numeric;]
PSQL EXECUTING: [ALTER TABLE settings ADD PRIMARY KEY (pk_settings_id);]
PSQL EXECUTING: [ALTER TABLE patients ALTER COLUMN fk_geoname_id TYPE numeric USING fk_geoname_id::numeric;]
PSQL EXECUTING: [ALTER TABLE patients ADD CONSTRAINT fk_geonames_id FOREIGN KEY (fk_geoname_id) REFERENCES geonames(pk_geoname_id);]
PSQL EXECUTING: [ALTER TABLE visits ALTER COLUMN fk_patient_id TYPE numeric USING fk_patient_id::numeric;]
PSQL EXECUTING: [ALTER TABLE visits ADD CONSTRAINT fk_patients_id FOREIGN KEY (fk_patient_id) REFERENCES patients(pk_patient_id);]
Failure to execute: ['ALTER TABLE visits ADD CONSTRAINT fk_patients_id FOREIGN KEY (fk_patient_id) REFERENCES patients(pk_patient_id);']
FAILURE TO IMPLEMENT CONSTRAINT ON (visits.fk_patient_id) to (patients.pk_patient_id)
PSQL EXECUTING: [ALTER TABLE visits ALTER COLUMN fk_clinic_id TYPE numeric USING fk_clinic_id::numeric;]
PSQL EXECUTING: [ALTER TABLE visits ADD CONSTRAINT fk_clinics_id FOREIGN KEY (fk_clinic_id) REFERENCES clinics(pk_clinic_id);]
PSQL EXECUTING: [ALTER TABLE diagnoses_made ALTER COLUMN fk_visit_id TYPE numeric USING fk_visit_id::numeric;]
PSQL EXECUTING: [ALTER TABLE diagnoses_made ADD CONSTRAINT fk_visits_id FOREIGN KEY (fk_visit_id) REFERENCES visits(pk_visit_id);]
Failure to execute: ['ALTER TABLE diagnoses_made ADD CONSTRAINT fk_visits_id FOREIGN KEY (fk_visit_id) REFERENCES visits(pk_visit_id);']
FAILURE TO IMPLEMENT CONSTRAINT ON (diagnoses_made.fk_visit_id) to (visits.pk_visit_id)
PSQL EXECUTING: [ALTER TABLE meds_dispensed ALTER COLUMN fk_visit_id TYPE numeric USING fk_visit_id::numeric;]
PSQL EXECUTING: [ALTER TABLE meds_dispensed ADD CONSTRAINT fk_visits_id FOREIGN KEY (fk_visit_id) REFERENCES visits(pk_visit_id);]
Failure to execute: ['ALTER TABLE meds_dispensed ADD CONSTRAINT fk_visits_id FOREIGN KEY (fk_visit_id) REFERENCES visits(pk_visit_id);']
FAILURE TO IMPLEMENT CONSTRAINT ON (meds_dispensed.fk_visit_id) to (visits.pk_visit_id)
View File
Whitespace-only changes.
+40
View File
@@ -0,0 +1,40 @@
import pandas as pd
from sqlalchemy import create_engine
from db import DB
class PSQL(DB):
def __init__(self, db_uri):
self.__engine = create_engine(db_uri)
# METHODS FROM DB SUPER CLASS
# ###########################
def read_table(self, table_name) -> pd.DataFrame:
# TODO: implement read_table functionality in PSQL
print("NEED TO IMPLEMENT THIS")
def write_table(
self, data_frame: pd.DataFrame, table_name: str, if_exists: str = "fail"
):
data_frame.to_sql(
con=self.__engine, name=table_name, if_exists=if_exists, index=False
)
# ###########################
def open_connection(self):
return self.__engine.connect()
def execute_command(self, command_string):
print(f"PSQL EXECUTING: [{command_string}]")
result = None
try:
with self.open_connection() as connection:
result = connection.execute(command_string)
except Exception as e:
print(f"Failure to execute: ['{command_string}']")
raise (e) # TODO: determine if crashing is appropriate
return result
+58
View File
@@ -0,0 +1,58 @@
import argparse
import os
import pickle
import sys
import pyodbc
import pandas as pd
from tqdm import tqdm
from fmp import FMP
from psql import PSQL
from utils import edit_distance
from PIL import Image
import io
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument("-d", "--dsn", help="dsn name")
parser.add_argument("-u", "--username", help="username for dsn")
parser.add_argument("-p", "--password", help="password for dsn")
return parser.parse_args()
def main(argv):
fmp = FMP(argv.dsn, argv.username, argv.password)
# query = "SELECT GetAs(face_photo, 'JPEG') FROM patients WHERE pk_patient_id=31883 OR pk_patient_id=1"
# result = fmp.cursor.execute(query)
# data = result.fetchall()
# data = [item[0] for item in data]
# print(data, len(data))
# images = [Image.open(io.BytesIO(d)) for d in data]
# [image.show() for image in images]
# table_name = "diagnoses_made"
# print(table_name)
# res_1 = fmp.read_table(table_name)
# print(res_1)
table_name = "globals"
print(table_name)
res_2 = fmp.read_table(table_name)
print(res_2.columns)
print(res_2)
# print(res_1 == res_2)
# table_name = "clinics"
# print(table_name)
# res_3 = fmp.read_table(table_name)
# print(res_3)
# print(type(fmp.execute_command("SELECT face_photo FROM patients WHERE pk_patient_id=31883")["face_photo"][0]))
# print(fmp.execute_command("SELECT face_photo FROM patients WHERE pk_patient_id=31883")["face_photo"][0])
if __name__ == "__main__":
sys.exit(main(parse_args()))
+8
View File
@@ -0,0 +1,8 @@
from psql import PSQL
psql = PSQL("postgres+psycopg2://postgres:HaitiCHI@0.0.0.0:5432/app_db")
psql.execute_command("DROP SCHEMA public CASCADE")
psql.execute_command("CREATE SCHEMA public")
psql.execute_command("GRANT ALL ON SCHEMA public TO postgres")
psql.execute_command("GRANT ALL ON SCHEMA public TO public")
+22
View File
@@ -0,0 +1,22 @@
def edit_distance(str1, str2):
return edit_distance_helper(str1, str2, len(str1) - 1, len(str2) - 1)
def edit_distance_helper(str1, str2, m, n):
if m == 0:
return n
if n == 0:
return m
if str1[m] == str2[n]:
return edit_distance_helper(str1, str2, m - 1, n - 1)
return (
min(
edit_distance_helper(str1, str2, m, n - 1),
edit_distance_helper(str1, str2, m - 1, n),
edit_distance_helper(str1, str2, m - 1, n - 1),
)
+ 1
)
+1 -1
View File
@@ -24,7 +24,7 @@ services:
ports:
- "5432:5432"
expose:
- "5422"
- "5432"
environment:
POSTGRES_DB: app_db
POSTGRES_USER: postgres
+1 -1
View File
@@ -16,7 +16,7 @@ services:
database:
image: "postgres" # use latest official postgres version
env_file:
- database.env # configure postgres
- ./database.env # configure postgres
volumes:
- "database-data:/var/lib/postgresql/data/" # persist data even if container shuts down
ports:
+23 -22
View File
@@ -1,25 +1,26 @@
module.exports = {
parser: "@typescript-eslint/parser", // Specifies the ESLint parser
extends: [
"plugin:react/recommended", // Uses the recommended rules from @eslint-plugin-react
"plugin:@typescript-eslint/recommended", // Uses the recommended rules from the @typescript-eslint/eslint-plugin
"prettier/@typescript-eslint", // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier
"plugin:prettier/recommended", // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array.
],
parserOptions: {
ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features
sourceType: "module", // Allows for the use of imports
ecmaFeatures: {
jsx: true, // Allows for the parsing of JSX
parser: '@typescript-eslint/parser', // Specifies the ESLint parser
extends: [
'plugin:react/recommended', // Uses the recommended rules from @eslint-plugin-react
'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin
'prettier/@typescript-eslint', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier
'plugin:prettier/recommended', // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array.
],
parserOptions: {
ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features
sourceType: 'module', // Allows for the use of imports
ecmaFeatures: {
jsx: true, // Allows for the parsing of JSX
},
},
},
rules: {
"@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/ban-ts-ignore": "off",
},
settings: {
react: {
version: "detect", // Tells eslint-plugin-react to automatically detect the version of React to use
rules: {
'@typescript-eslint/no-empty-interface': 'off',
'@typescript-eslint/ban-ts-ignore': 'off',
'@typescript-eslint/camelcase': 'off',
},
},
};
settings: {
react: {
version: 'detect', // Tells eslint-plugin-react to automatically detect the version of React to use
},
},
}
+83 -11
View File
@@ -2198,9 +2198,9 @@
},
"dependencies": {
"acorn": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.0.tgz",
"integrity": "sha512-gac8OEcQ2Li1dxIEWGZzsp2BitJxwkwcOm0zHAJLcPJaVvm58FRnk6RkuLRpU1EujipU2ZFODv2P9DLMfnV8mw=="
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz",
"integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA=="
}
}
},
@@ -4626,6 +4626,11 @@
"integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==",
"dev": true
},
"debounce": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.0.tgz",
"integrity": "sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg=="
},
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
@@ -5005,6 +5010,37 @@
}
}
},
"easy-peasy": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/easy-peasy/-/easy-peasy-3.3.0.tgz",
"integrity": "sha512-wkTDAG+nRy+4g8Z3/k1vuTyrhxet19oY5MCmuRgOObm8dtZ2m+g5Mk/sdf3UWPuyx/sKIYH5Fy/HXab8iNKjTQ==",
"requires": {
"debounce": "^1.2.0",
"immer-peasy": "3.1.3",
"is-plain-object": "^3.0.0",
"memoizerific": "^1.11.3",
"prop-types": "^15.6.2",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"symbol-observable": "^1.2.0",
"ts-toolbelt": "^6.1.6"
},
"dependencies": {
"is-plain-object": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.0.tgz",
"integrity": "sha512-tZIpofR+P05k8Aocp7UI/2UTa9lTJSebCXpFFoR9aibpokDj/uXBsJ8luUu0tTVYKkMU6URDUuOfJZ7koewXvg==",
"requires": {
"isobject": "^4.0.0"
}
},
"isobject": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz",
"integrity": "sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA=="
}
}
},
"ecc-jsbn": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
@@ -7186,6 +7222,11 @@
"resolved": "https://registry.npmjs.org/immer/-/immer-1.10.0.tgz",
"integrity": "sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg=="
},
"immer-peasy": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/immer-peasy/-/immer-peasy-3.1.3.tgz",
"integrity": "sha512-WzoZ96A93jOmcDOLNChMWAqy+ZU8vEYQx2DcKjgo7P5SToiJs+GL+5yQbWzH8X02Lhvv6xrGgVNa1xbki66Eow=="
},
"import-cwd": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz",
@@ -7825,9 +7866,9 @@
},
"dependencies": {
"acorn": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.0.tgz",
"integrity": "sha512-gac8OEcQ2Li1dxIEWGZzsp2BitJxwkwcOm0zHAJLcPJaVvm58FRnk6RkuLRpU1EujipU2ZFODv2P9DLMfnV8mw=="
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz",
"integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA=="
},
"jsdom": {
"version": "14.1.0",
@@ -8766,9 +8807,9 @@
},
"dependencies": {
"acorn": {
"version": "5.7.3",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz",
"integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw=="
"version": "5.7.4",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz",
"integrity": "sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg=="
}
}
},
@@ -9451,6 +9492,11 @@
"resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz",
"integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0="
},
"map-or-similar": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/map-or-similar/-/map-or-similar-1.5.0.tgz",
"integrity": "sha1-beJlMXSt+12e3DPGnT6Sobdvrwg="
},
"map-visit": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz",
@@ -9489,6 +9535,14 @@
"p-is-promise": "^2.0.0"
}
},
"memoizerific": {
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/memoizerific/-/memoizerific-1.11.3.tgz",
"integrity": "sha1-fIekZGREwy11Q4VwkF8tvRsagFo=",
"requires": {
"map-or-similar": "^1.5.0"
}
},
"memory-fs": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
@@ -12929,6 +12983,20 @@
"strip-indent": "^3.0.0"
}
},
"redux": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz",
"integrity": "sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==",
"requires": {
"loose-envify": "^1.4.0",
"symbol-observable": "^1.2.0"
}
},
"redux-thunk": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz",
"integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw=="
},
"regenerate": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz",
@@ -14861,8 +14929,7 @@
"symbol-observable": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
"integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==",
"dev": true
"integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ=="
},
"symbol-tree": {
"version": "3.2.4",
@@ -15282,6 +15349,11 @@
"resolved": "https://registry.npmjs.org/ts-pnp/-/ts-pnp-1.1.5.tgz",
"integrity": "sha512-ti7OGMOUOzo66wLF3liskw6YQIaSsBgc4GOAlWRnIEj8htCxJUxskanMUoJOD6MDCRAXo36goXJZch+nOS0VMA=="
},
"ts-toolbelt": {
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-6.5.1.tgz",
"integrity": "sha512-zjnZ/vy1eUA0li3H0JXecl0R3jiP42snpLimsrppt9V3LLbM4NM4jMgjXQ4S67hvehq+r9CxpX4Wj6RnFRzReA=="
},
"tslib": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz",
+1
View File
@@ -13,6 +13,7 @@
"@types/react-dom": "^16.9.5",
"@types/react-router-dom": "^5.1.4",
"antd-mobile": "^2.3.1",
"easy-peasy": "^3.3.0",
"node-sass": "^4.13.1",
"react": "^16.12.0",
"react-dom": "^16.12.0",
+1 -1
View File
@@ -2,7 +2,7 @@ import React from 'react'
import { render } from '@testing-library/react'
import App from './App'
test('renders learn react link', () => {
test('renders login element', () => {
const { getByText } = render(<App />)
const linkElement = getByText(/Login/i)
expect(linkElement).toBeInTheDocument()
+53 -9
View File
@@ -1,13 +1,19 @@
import React from 'react'
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'
import { StoreProvider } from 'easy-peasy'
import store from './state/store'
import Login from './components/Login'
import LandingPage from './components/LandingPage'
import Wrapper from './Wrapper'
import PatientInfoForm from './components/PatientInfoForm'
import PatientSearch from './components/PatientSearch'
import DiagnosisForm from './components/DiagnosisForm'
import './scss/App.scss'
import './antd-mobile.css'
<<<<<<< HEAD
import TreatmentForm from './components/TreatmentForm'
const App: React.FunctionComponent = () => {
@@ -21,19 +27,57 @@ const App: React.FunctionComponent = () => {
<TreatmentForm />
</Route>
<Route path="/patientinfo">
=======
import DebugShowEasyPeasy from './components/DebugShowEasyPeasy'
const App: React.FunctionComponent = () => {
return (
<StoreProvider store={store}>
<Router>
<Switch>
<Route path="/login">
<Login />
</Route>
<Route path="/landing">
<LandingPage />
</Route>
<Route path="/search">
<Wrapper
navTitle="Patient Search"
leftArrowText="Home"
leftArrowRoute="/landing"
>
<PatientSearch />
</Wrapper>
</Route>
<Route path="/patientinfo">
<Wrapper
navTitle="Patient Information"
leftArrowText="Home"
leftArrowRoute="/"
>
<PatientInfoForm />
</Wrapper>
</Route>
<Route path="/diagnosis">
>>>>>>> origin/dev
<Wrapper
navTitle="Patient Information"
leftArrowText="Home"
leftArrowRoute="/"
navTitle="Diagnosis"
leftArrowText="Patient Info"
leftArrowRoute="/patientinfo"
>
<PatientInfoForm />
<DiagnosisForm />
</Wrapper>
</Route>
<Route path="/">
<Login />
</Route>
</Switch>
</Router>
<Route path="/easy-peasy">
<DebugShowEasyPeasy />
</Route>
<Route path="/">
<Login />
</Route>
</Switch>
</Router>
</StoreProvider>
)
}
+16
View File
@@ -0,0 +1,16 @@
/* istanbul ignore file */
// This component is a debug utility to show
// all contents of easy-peasy when rendered
import React, { ReactElement } from 'react'
import { useStoreState } from '../state/storeIndex'
export default function DebugShowEasyPeasy(): ReactElement {
const patientInfoFormData = useStoreState(
(state) => state.patientInfoFormModel.fields,
)
console.log(patientInfoFormData)
return <div>patientInfoFormData: {JSON.stringify(patientInfoFormData)}</div>
}
+189
View File
@@ -0,0 +1,189 @@
{
"diagnoses": [
{
"name": "Asthma",
"id": "asthma"
},
{
"name": "Allergies",
"id": "allergies"
},
{
"name": "Anemia",
"id": "anemia"
},
{
"name": "Malnutrition",
"id": "malnutrition"
},
{
"name": "Depression",
"id": "depression"
},
{
"name": "Diabetes",
"id": "diabetes"
},
{
"id": "eye",
"group-title": "Eye",
"group": [
{
"name": "Ptergium",
"id": "ptergium"
},
{
"name": "Cataract",
"id": "cataract"
},
{
"name": "Dry Eye",
"id": "dry-eye"
},
{
"name": "Conjuntivitis",
"id": "conjuntivitis"
}
]
},
{
"name": "Fever",
"id": "fever"
},
{
"name": "Possible Malaria",
"id": "malaria"
},
{
"name": "Filariasis",
"id": "filariasis"
},
{
"id": "intestinal",
"group-title": "Intestinal",
"group": [
{ "name": "Gastritis", "id": "gastritis" },
{ "name": "GERD", "id": "gerd" },
{ "name": "Gastroenteritis", "id": "gastroenteritis" },
{ "name": "Dysentery", "id": "dysentery" },
{ "name": "Possible Cholera", "id": "cholera" }
]
},
{
"name": "Headache",
"id": "headache"
},
{
"group-title": "Hernia",
"id": "hernia",
"group": [
{ "name": "Inguinal", "id": "inguinal" },
{ "name": "Umbilical", "id": "umbilical" }
]
},
{
"id": "cardiovascular",
"group-title": "Cardiovascular",
"group": [
{ "name": "Hypertension", "id": "hypertension" },
{ "name": "Stroke", "id": "stroke" },
{ "name": "Angina", "id": "angina" },
{ "name": "CHF", "id": "chf" }
]
},
{
"name": "Healthy",
"id": "healthy"
},
{
"name": "Musculoskeletal Pain",
"id": "musculoskeletal"
},
{
"group-title": "Otitis",
"id": "otitis",
"group": [
{ "name": "Right", "id": "otitis-right" },
{ "name": "Left", "id": "otitis-left" }
]
},
{
"name": "Pneumonia",
"id": "pneumonia"
},
{
"id": "sinusitis",
"name": "Sinusitis"
},
{
"name": "URI",
"id": "uri"
},
{
"name": "Pregnancy",
"id": "pregnancy",
"text-input-title": "Weeks",
"text-input-type": "number",
"max-length": 2,
"text-input-id": "pregnancy-weeks"
},
{
"group-title": "Dermatological",
"id": "dermatological",
"group": [
{ "name": "Atopic Derm", "id": "atopic-derm" },
{ "name": "Fungal", "id": "fungal" },
{ "name": "Scabies", "id": "scabies" },
{ "name": "Impetigo", "id": "Impetigo" },
{ "name": "Tinea Capitus (kerion)", "id": "tinea-capitus" }
]
},
{
"name": "Possible STD",
"id": "std"
},
{
"name": "Possible TB",
"id": "tb"
},
{
"name": "Possible Parasites",
"id": "parasites"
},
{
"name": "UTI",
"id": "uti"
},
{
"group-title": "Vaginal",
"id": "vaginal",
"group": [
{ "name": "Vaginitis", "id": "vaginitis" },
{ "name": "Candidal", "id": "candidal" },
{ "name": "Trich", "id": "trich" },
{ "name": "BV", "id": "bv" }
]
},
{
"name": "Needs to see Dentist",
"id": "dentist"
},
{
"name": "Needs to see Eye Doctor",
"id": "eye-doctor"
},
{
"name": "Surgical candidate",
"id": "surgical-candidate"
},
{
"text-input-title": "Other Diagnosis",
"text-input-id": "other-diagnosis"
},
{
"text-input-title": "Diagnoses Comments",
"text-input-id": "comments"
}
]
}
+135
View File
@@ -0,0 +1,135 @@
import React from 'react'
import { render, fireEvent } from '@testing-library/react'
import DiagnosisForm from './DiagnosisForm'
import * as data from './DiagnosisFields.json'
/*
Diagnosis Fields are stored in DiagnosisFields.json in the format:
{
"diagnoses" : [
{
"name": "Name",
"id": "id",
"text-input-title": "Title",
"text-input-type": "type" (i.e. number)
"group-title": "Group Title",
"group": [
{ "name": "Name1", "id": "id1" },
{ "name": "Name2", "id": "id2" }
]
},
{...}
]
}
*/
const diagnosisFields = data.diagnoses
test('renders the form', async () => {
const dom = render(<DiagnosisForm />)
expect(dom.getByTestId('diagnosis-form-wrapper')).toBeTruthy()
})
test('renders each diagnoses.name data field', () => {
const { getByText } = render(<DiagnosisForm />)
diagnosisFields.forEach((diagnosis) => {
if (diagnosis.name != null) {
const re = new RegExp(diagnosis.name, 'gi')
const field = getByText(re)
expect(field).toBeInTheDocument()
}
})
})
test(' each diagnoses.name has a corresponding checkbox and is able to be checked/unchecked', () => {
const { getByTestId } = render(<DiagnosisForm />)
diagnosisFields.forEach((diagnosis) => {
if (diagnosis.name != null && diagnosis.id != null) {
const re = new RegExp(diagnosis.id, 'gi')
const checkboxItem = getByTestId(re)
expect(checkboxItem).toHaveClass('am-checkbox-item')
const checkBox = checkboxItem.getElementsByTagName('span')[0]
expect(checkBox).not.toHaveClass('am-checkbox-checked')
const checkBoxInput = checkboxItem.getElementsByClassName(
'am-checkbox-input',
)[0]
fireEvent.click(checkBoxInput)
expect(checkBox).toHaveClass('am-checkbox-checked')
}
})
})
test('renders each diagnoses.text-input-title data field as a text entry field', () => {
const { getByTestId } = render(<DiagnosisForm />)
diagnosisFields.forEach((diagnosis) => {
if (
diagnosis['text-input-title'] != null &&
diagnosis['text-input-title'] != null
) {
const re = new RegExp(diagnosis['text-input-id'], 'gi')
const field = getByTestId(re)
expect(field).toBeInTheDocument()
expect(field).toHaveAttribute('type', 'text')
}
})
})
test('renders each diagnoses.group-title data field as a header', () => {
const { getByText } = render(<DiagnosisForm />)
diagnosisFields.forEach((diagnosis) => {
if (diagnosis['group-title'] != null && diagnosis.group != null) {
const re = new RegExp(`\^${diagnosis['group-title']}\$`, 'gi')
const field = getByText(re)
expect(field).toBeInTheDocument()
expect(field).toHaveClass('am-list-header')
}
})
})
test('renders each diagnoses.group entry in the same list element', () => {
const { getByTestId } = render(<DiagnosisForm />)
diagnosisFields.forEach((diagnosis) => {
if (diagnosis.group != null && diagnosis.id != null) {
// get the list by diagnosis.id
const re = new RegExp(`\^${diagnosis.id}\$`, 'gi')
const field = getByTestId(re)
expect(field).toBeInTheDocument()
expect(field).toHaveClass('am-list')
// For each element in the group, ensure it is contained in the list
diagnosis.group.forEach((groupElement) => {
const el = getByTestId(groupElement.id)
expect(field).toContainElement(el)
})
}
})
})
test('renders each diagnoses.[el] as a checkbox that can be checked/unchecked', () => {
const { getByTestId } = render(<DiagnosisForm />)
diagnosisFields.forEach((diagnosis) => {
if (diagnosis.group != null) {
diagnosis.group.forEach((groupElement) => {
const re = new RegExp(groupElement.id, 'gi')
const groupCheckboxItem = getByTestId(re)
expect(groupCheckboxItem).toBeInTheDocument()
expect(groupCheckboxItem).toHaveClass('am-checkbox-item')
const checkBox = groupCheckboxItem.getElementsByTagName(
'span',
)[0]
expect(checkBox).not.toHaveClass('am-checkbox-checked')
const checkBoxInput = groupCheckboxItem.getElementsByClassName(
'am-checkbox-input',
)[0]
fireEvent.click(checkBoxInput)
expect(checkBox).toHaveClass('am-checkbox-checked')
})
}
})
})
+117
View File
@@ -0,0 +1,117 @@
import React, { ReactElement } from 'react'
import { List, Checkbox, InputItem, WhiteSpace } from 'antd-mobile'
import '../scss/DiagnosisForm.scss'
import * as data from './DiagnosisFields.json'
/*
Diagnosis Fields are stored in DiagnosisFields.json in the format:
{
"diagnoses" : [
{
"name": "Name",
"id": "id",
"text-input-title": "Title",
"text-input-id": "id" for `id` and `data-testid` attributes
"text-input-type": "type" (i.e. number)
"max-length": 3
"group-title": "Group Title",
"group": [
{ "name": "Name1", "id": "id1" },
{ "name": "Name2", "id": "id2" }
]
},
{...}
]
}
*/
const CheckboxItem = Checkbox.CheckboxItem
const diagnosisFields = data.diagnoses
export default function DiagnosisForm(): ReactElement {
return (
<div id="diagnosis-form-wrapper" data-testid="diagnosis-form-wrapper">
{diagnosisFields.map((field) => {
const formElements: ReactElement[] = []
if (field.name != null) {
formElements.push(
<List>
<CheckboxItem
id={field.id}
data-testid={field.id}
key={field.id}
>
{field.name}
</CheckboxItem>
</List>,
)
}
if (
field['text-input-title'] != null &&
field['text-input-id'] != null
) {
formElements.push(
<List renderHeader={() => field['text-input-title']}>
<InputItem
id={field['text-input-id']}
data-testid={field['text-input-id']}
key={field['text-input-id']}
type={
field['text-input-type'] === 'number'
? 'number'
: 'text'
}
maxLength={
field['max-length']
? field['max-length']
: null
}
/>
</List>,
)
}
if (field.group != null) {
const groupElements: ReactElement[] = []
// add each group element to the internalElement array
field.group.forEach((ele: any) => {
if (ele.id != null && ele.name != null) {
groupElements.push(
<CheckboxItem
id={ele.id}
data-testid={ele.id}
key={ele.id}
>
{ele.name}
</CheckboxItem>,
)
}
})
// Render the entire list with the internal elements
if (field['group-title'] != null && field.id !== null) {
formElements.push(
<List
renderHeader={() => field['group-title']}
data-testid={field.id}
>
{groupElements}
<WhiteSpace
className="list-whitespace"
size="lg"
/>
</List>,
)
}
}
return <> {formElements} </>
})}
</div>
)
}
+10 -4
View File
@@ -1,11 +1,17 @@
import React from 'react'
import { render } from '@testing-library/react'
import { BrowserRouter as Router } from 'react-router-dom'
import LandingPage from './LandingPage'
test('renders learn react link', () => {
const { getByText } = render(<LandingPage />)
const lookupButtonElement = getByText(/Patient Lookup/i)
const newPatientButtonElement = getByText(/New Patient/i)
test('renders landing buttons', () => {
const landingComponent = render(
<Router>
<LandingPage />
</Router>,
)
const lookupButtonElement = landingComponent.getByText(/Patient Lookup/i)
const newPatientButtonElement = landingComponent.getByText(/New Patient/i)
expect(lookupButtonElement).toBeInTheDocument()
expect(newPatientButtonElement).toBeInTheDocument()
})
+4 -6
View File
@@ -1,16 +1,14 @@
import React, { ReactElement } from 'react'
import { WhiteSpace, Button } from 'antd-mobile'
import { UserAddOutlined } from '@ant-design/icons'
import { useHistory } from 'react-router-dom'
import '../scss/LandingPage.scss'
export default function LandingPage(): ReactElement {
/* istanbul ignore next */
// TODO: these onClick functions could be mocked in a unit test
const patientLookup = () => {
console.log('Clicked Patient Lookup')
}
let history = useHistory()
// TODO: these onClick functions could be mocked in a unit test
/* istanbul ignore next */
const newPatient = () => {
console.log('Clicked New Patient')
@@ -18,7 +16,7 @@ export default function LandingPage(): ReactElement {
return (
<div id="landing-wrapper">
<div className="button-flex">
<Button onClick={patientLookup} icon="search">
<Button onClick={() => history.push('/search')} icon="search">
Patient Lookup
</Button>
<WhiteSpace />
+128 -86
View File
@@ -1,105 +1,147 @@
import React from "react";
import { render, fireEvent, prettyDOM } from "@testing-library/react";
import PatientInfoForm from "./PatientInfoForm";
import React from 'react'
import { render, fireEvent, prettyDOM } from '@testing-library/react'
import PatientInfoForm from './PatientInfoForm'
import { StoreProvider } from 'easy-peasy'
import store from '../state/store'
test("female-only div is only displayed if the sex is selected as female", async () => {
const femaleOnlyFormFields = ["Pregnant?", "LMP", "Wants Planning?"];
test('female-only div is only displayed if the sex is selected as female', async () => {
const femaleOnlyFormFields = ['Pregnant?', 'LMP', 'Wants Planning?']
const dom = render(<PatientInfoForm />);
const dom = render(
<StoreProvider store={store}>
<PatientInfoForm />
</StoreProvider>,
)
// ensure the female-only div is not displayed
femaleOnlyFormFields.forEach((fieldText) => {
expect(dom.queryByText(fieldText)).toBeNull();
});
// ensure the female-only div is not displayed
femaleOnlyFormFields.forEach((fieldText) => {
expect(dom.queryByText(fieldText)).toBeNull()
})
// click the radio button for 'female'
const femaleButton = dom.getByTestId("female-button");
fireEvent.click(femaleButton);
// click the radio button for 'female'
const femaleButton = dom.getByTestId('female-button')
fireEvent.click(femaleButton)
femaleOnlyFormFields.forEach((fieldText) => {
expect(dom.queryByText(fieldText)).toBeTruthy();
});
});
femaleOnlyFormFields.forEach((fieldText) => {
expect(dom.queryByText(fieldText)).toBeTruthy()
})
})
test("sets visit input", () => {
const dom = render(<PatientInfoForm />);
const firstVisitYesInput = dom.queryByTestId("first-visit-Yes");
const checkBox = firstVisitYesInput.getElementsByTagName("span")[0];
test('sets visit input', () => {
const dom = render(
<StoreProvider store={store}>
<PatientInfoForm />
</StoreProvider>,
)
const firstVisitYesInput = dom.queryByTestId('first-visit-Yes')
const checkBox = firstVisitYesInput.getElementsByTagName('span')[0]
expect(checkBox).not.toHaveClass("am-radio-checked");
fireEvent.click(firstVisitYesInput);
expect(checkBox).toHaveClass("am-radio-checked");
});
expect(checkBox).not.toHaveClass('am-radio-checked')
fireEvent.click(firstVisitYesInput)
expect(checkBox).toHaveClass('am-radio-checked')
})
test("sets pregnant input", () => {
const dom = render(<PatientInfoForm />);
const femaleButton = dom.getByTestId("female-button");
fireEvent.click(femaleButton);
test('sets pregnant input', () => {
const dom = render(
<StoreProvider store={store}>
<PatientInfoForm />
</StoreProvider>,
)
const femaleButton = dom.getByTestId('female-button')
fireEvent.click(femaleButton)
const pregnantYesInput = dom.queryByTestId("pregnant-Yes");
const checkBox = pregnantYesInput.getElementsByTagName("span")[0];
const pregnantYesInput = dom.queryByTestId('pregnant-Yes')
const checkBox = pregnantYesInput.getElementsByTagName('span')[0]
expect(checkBox).not.toHaveClass("am-radio-checked");
fireEvent.click(pregnantYesInput);
expect(checkBox).toHaveClass("am-radio-checked");
});
expect(checkBox).not.toHaveClass('am-radio-checked')
fireEvent.click(pregnantYesInput)
expect(checkBox).toHaveClass('am-radio-checked')
})
test("sets planning input", () => {
const dom = render(<PatientInfoForm />);
const femaleButton = dom.getByTestId("female-button");
fireEvent.click(femaleButton);
test('sets planning input', () => {
const dom = render(
<StoreProvider store={store}>
<PatientInfoForm />
</StoreProvider>,
)
const femaleButton = dom.getByTestId('female-button')
fireEvent.click(femaleButton)
const planningYesInput = dom.queryByTestId("planning-Yes");
const checkBox = planningYesInput.getElementsByTagName("span")[0];
const planningYesInput = dom.queryByTestId('planning-Yes')
const checkBox = planningYesInput.getElementsByTagName('span')[0]
expect(checkBox).not.toHaveClass("am-radio-checked");
fireEvent.click(planningYesInput);
expect(checkBox).toHaveClass("am-radio-checked");
});
expect(checkBox).not.toHaveClass('am-radio-checked')
fireEvent.click(planningYesInput)
expect(checkBox).toHaveClass('am-radio-checked')
})
test("sets albendazole input", () => {
const dom = render(<PatientInfoForm />);
const femaleButton = dom.getByTestId("female-button");
test('validates blood pressure input', () => {
const dom = render(
<StoreProvider store={store}>
<PatientInfoForm />
</StoreProvider>,
)
const albendazoleYesInput = dom.queryByTestId("albendazole-Yes");
const checkBox = albendazoleYesInput.getElementsByTagName("span")[0];
const bpInput = dom.getByPlaceholderText(
'Blood Pressure',
) as HTMLInputElement
fireEvent.change(bpInput, { target: { value: '130/85' } })
expect(bpInput.value).toBe('130/85')
fireEvent.change(bpInput, { target: { value: '' } })
fireEvent.change(bpInput, { target: { value: 'abcd' } })
expect(bpInput.value).toBe('') // Empty String
})
expect(checkBox).not.toHaveClass("am-radio-checked");
fireEvent.click(albendazoleYesInput);
expect(checkBox).toHaveClass("am-radio-checked");
});
test('sets albendazole input', () => {
const dom = render(
<StoreProvider store={store}>
<PatientInfoForm />
</StoreProvider>,
)
test("renders the Patient Info Form fields", () => {
const formFields = [
"First Name",
"First Name",
"Last Name",
"Last Name",
"Sex",
"Birth",
"Age",
"Phone",
"Town",
"Visit",
"Nickname",
"BP",
"Temp",
"Pulse",
"Weight",
"Height",
"Z-Score",
"Albendazole",
"Allergies",
"Medicines",
"Chief Complaint",
"History",
"Exam",
];
const albendazoleYesInput = dom.queryByTestId('albendazole-Yes')
const checkBox = albendazoleYesInput.getElementsByTagName('span')[0]
const { getByText } = render(<PatientInfoForm />);
expect(checkBox).not.toHaveClass('am-radio-checked')
fireEvent.click(albendazoleYesInput)
expect(checkBox).toHaveClass('am-radio-checked')
})
formFields.forEach((fieldText) => {
let re = new RegExp(fieldText, "gi");
expect(getByText(re)).toBeInTheDocument();
});
});
test('renders the Patient Info Form fields', () => {
const formFields = [
'First Name',
'First Name',
'Last Name',
'Last Name',
'Sex',
'Birth',
'Age',
'Phone',
'Town',
'Visit',
'Nickname',
'BP',
'Temp',
'Pulse',
'Weight',
'Height',
'Z-Score',
'Albendazole',
'Allergies',
'Medicines',
'Chief Complaint',
'History',
'Exam',
]
const { getByText } = render(
<StoreProvider store={store}>
<PatientInfoForm />
</StoreProvider>,
)
formFields.forEach((fieldText) => {
const re = new RegExp(fieldText, 'gi')
expect(getByText(re)).toBeInTheDocument()
})
})
+360 -254
View File
@@ -1,282 +1,388 @@
import React, { ReactElement, useState } from "react";
import React, { ReactElement, useState } from 'react'
import { useStoreActions } from '../state/storeIndex'
import { SEX } from '../state/enums'
import {
InputItem,
List,
TextareaItem,
Button,
Radio,
WhiteSpace,
} from "antd-mobile";
import "../scss/PatientInfoForm.scss";
InputItem,
List,
TextareaItem,
Button,
Radio,
WhiteSpace,
} from 'antd-mobile'
import '../scss/PatientInfoForm.scss'
const RadioItem = Radio.RadioItem;
const RadioItem = Radio.RadioItem
export default function Form(): ReactElement {
// State Hooks for Radio Buttons
// useState returns a value, and a function to set that value
const [sex, setSex] = useState(null as Number); // Default: null
const [albendazole, setAlbendazole] = useState(0); // Default: No
const [firstVisit, setFirstVisit] = useState(0); // Default: No
const [pregnant, setPregnant] = useState(0); // Default: No
const [planning, setPlanning] = useState(0); // Default: No
const [bloodPressureValue, setBloodPressureValue] = useState("");
// State Hooks for Radio Buttons
// useState returns a value, and a function to set that value
const [sex, setSex] = useState(null as number) // Default: null
const [albendazole, setAlbendazole] = useState(0) // Default: No
const [firstVisit, setFirstVisit] = useState(0) // Default: No
const [pregnant, setPregnant] = useState(0) // Default: No
const [planning, setPlanning] = useState(0) // Default: No
const [bloodPressureValue, setBloodPressureValue] = useState('')
const updateCentralState = useStoreActions(
(actions) => actions.patientInfoFormModel.updateFields,
)
// Data for Radio Buttons
const sexValues = [
{ value: 0, label: "Male" },
{ value: 1, label: "Female" },
];
// Data for Radio Buttons
const sexValues = [
{ value: 0, label: 'Male' },
{ value: 1, label: 'Female' },
]
const radioYesNo = [
{ value: 0, label: "No" },
{ value: 1, label: "Yes" },
];
const radioYesNo = [
{ value: 0, label: 'No' },
{ value: 1, label: 'Yes' },
]
/* istanbul ignore next */
const onSubmit = () => {
if (process.env.NODE_ENV === "development") {
// This shows how to access the values of the form
const firstNameField: HTMLInputElement = document.getElementById(
"firstname"
) as HTMLInputElement;
const lastNameField: HTMLInputElement = document.getElementById(
"lastname"
) as HTMLInputElement;
const nicknameField: HTMLInputElement = document.getElementById(
"nickname"
) as HTMLInputElement;
const birthdateField: HTMLInputElement = document.getElementById(
"birthdate"
) as HTMLInputElement;
const complaintField1: HTMLInputElement = document.getElementById(
"complaint1"
) as HTMLInputElement;
const complaintField2: HTMLInputElement = document.getElementById(
"complaint2"
) as HTMLInputElement;
const complaintField3: HTMLInputElement = document.getElementById(
"complaint3"
) as HTMLInputElement;
console.log("First name:", firstNameField.value);
console.log("Last name:", lastNameField.value);
console.log("Nickname:", nicknameField.value);
console.log("Birth Date:", birthdateField.value);
console.log(
"Complaints: ",
complaintField1.value +
" " +
complaintField2.value +
" " +
complaintField3.value
);
console.log("Wants Planning?: " + planning);
/* istanbul ignore next */
const onSubmit = () => {
const firstNameField: HTMLInputElement = document.getElementById(
'firstname',
) as HTMLInputElement
const lastNameField: HTMLInputElement = document.getElementById(
'lastname',
) as HTMLInputElement
const nicknameField: HTMLInputElement = document.getElementById(
'nickname',
) as HTMLInputElement
const birthdateField: HTMLInputElement = document.getElementById(
'birthdate',
) as HTMLInputElement
const age: HTMLInputElement = document.getElementById(
'age',
) as HTMLInputElement
const phone: HTMLInputElement = document.getElementById(
'phone',
) as HTMLInputElement
const town: HTMLInputElement = document.getElementById(
'town',
) as HTMLInputElement
const gravida: HTMLInputElement = document.getElementById(
'gravida',
) as HTMLInputElement
const para: HTMLInputElement = document.getElementById(
'para',
) as HTMLInputElement
const abortus: HTMLInputElement = document.getElementById(
'abortus',
) as HTMLInputElement
const lmp: HTMLInputElement = document.getElementById(
'lmp',
) as HTMLInputElement
const weight: HTMLInputElement = document.getElementById(
'weight',
) as HTMLInputElement
const height: HTMLInputElement = document.getElementById(
'height',
) as HTMLInputElement
const temp: HTMLInputElement = document.getElementById(
'temp',
) as HTMLInputElement
const pulse: HTMLInputElement = document.getElementById(
'pulse',
) as HTMLInputElement
const zScore: HTMLInputElement = document.getElementById(
'z-score',
) as HTMLInputElement
const allergies: HTMLInputElement = document.getElementById(
'allergies',
) as HTMLInputElement
const medicines: HTMLInputElement = document.getElementById(
'medicines',
) as HTMLInputElement
const complaintField1: HTMLInputElement = document.getElementById(
'complaint1',
) as HTMLInputElement
const complaintField2: HTMLInputElement = document.getElementById(
'complaint2',
) as HTMLInputElement
const complaintField3: HTMLInputElement = document.getElementById(
'complaint3',
) as HTMLInputElement
const history: HTMLInputElement = document.getElementById(
'history',
) as HTMLInputElement
const exam: HTMLInputElement = document.getElementById(
'exam',
) as HTMLInputElement
updateCentralState({
firstName: firstNameField.value,
lastName: lastNameField.value,
nickname: nicknameField.value,
sex: sex ? SEX.female : SEX.male,
birthDate: new Date(birthdateField.value),
age: parseInt(age.value),
phone: phone.value,
town: town.value,
firstVisit: firstVisit,
pregnant: pregnant,
G: sex ? parseInt(gravida.value) : null,
P: sex ? parseInt(para.value) : null,
A: sex ? parseInt(abortus.value) : null,
LMP: sex ? new Date(lmp.value) : null,
wantsPlanning: planning,
weight: parseFloat(weight.value),
height: parseFloat(height.value),
bloodPressure: bloodPressureValue,
temperature: parseFloat(temp.value),
pulse: parseFloat(pulse.value),
zScore: parseFloat(zScore.value),
albendazole: albendazole,
allergies: allergies.value,
medicines: medicines.value,
chiefComplaint: [
complaintField1.value,
complaintField2.value,
complaintField3.value,
],
history: history.value,
exam: exam.value,
})
}
};
const validateAndSetBloodPressure = (value: string) => {
const newChar = value.slice(-1);
if (newChar.match(/[0-9]|\/|^$/) && value.length < 8) {
setBloodPressureValue(value);
const validateAndSetBloodPressure = (value: string) => {
const newChar = value.slice(-1)
if (newChar.match(/[0-9]|\/|^$/) && value.length < 8) {
setBloodPressureValue(value)
}
}
};
return (
<div id="patient-info-wrapper">
<TextareaItem
title="First Name"
placeholder="First Name"
autoHeight
id="firstname"
/>
return (
<div id="patient-info-wrapper">
<TextareaItem
title="First Name"
placeholder="First Name"
autoHeight
id="firstname"
/>
<TextareaItem
title="Last Name"
placeholder="Last Name"
id="lastname"
autoHeight
/>
<TextareaItem
title="Last Name"
placeholder="Last Name"
id="lastname"
autoHeight
/>
<TextareaItem
title="Nickname"
placeholder="Nickname"
id="nickname"
autoHeight
/>
<TextareaItem
title="Nickname"
placeholder="Nickname"
id="nickname"
autoHeight
/>
<List renderHeader={() => "Sex"}>
{sexValues.map((i) => (
<RadioItem
data-testid={`${i.label.toLowerCase()}-button`}
id={`${i.label.toLowerCase()}-button`}
key={i.value}
name="sex"
checked={sex === i.value}
onClick={() => setSex(i.value)}
>
{i.label}
</RadioItem>
))}
</List>
<List renderHeader={() => 'Sex'}>
{sexValues.map((i) => (
<RadioItem
data-testid={`${i.label.toLowerCase()}-button`}
id={`${i.label.toLowerCase()}-button`}
key={i.value}
name="sex"
checked={sex === i.value}
onClick={() => setSex(i.value)}
>
{i.label}
</RadioItem>
))}
</List>
<WhiteSpace />
<WhiteSpace />
<InputItem placeholder="YYYY-MM-DD" id="birthdate" type="digit" maxLength={10}>
Birth Date
</InputItem>
<InputItem placeholder="YYYY-MM-DD" id="birthdate" maxLength={10}>
Birth Date
</InputItem>
<InputItem type="number" placeholder="Age" id="age" maxLength={3}>
Age{" "}
</InputItem>
<InputItem type="number" placeholder="Age" id="age" maxLength={3}>
Age{' '}
</InputItem>
<InputItem type="number" placeholder="Phone" id="phone">
Phone
</InputItem>
<InputItem type="number" placeholder="Phone" id="phone">
Phone
</InputItem>
<TextareaItem title="Town" placeholder="Town" id="town" autoHeight />
<TextareaItem
title="Town"
placeholder="Town"
id="town"
autoHeight
/>
<List renderHeader={() => "First Visit?"}>
{radioYesNo.map((i) => (
<RadioItem
key={i.value}
data-testid={`first-visit-${i.label}`}
name="visit"
checked={firstVisit === i.value}
onClick={() => setFirstVisit(i.value)}
>
{i.label}
</RadioItem>
))}
</List>
<List renderHeader={() => 'First Visit?'}>
{radioYesNo.map((i) => (
<RadioItem
key={i.value}
data-testid={`first-visit-${i.label}`}
name="visit"
checked={firstVisit === i.value}
onClick={() => setFirstVisit(i.value)}
>
{i.label}
</RadioItem>
))}
</List>
{/* Only display the 'female-only' div if the patient is female */}
{sex ? (
<div id="female-only">
<WhiteSpace />
<List renderHeader={() => "Pregnant?"}>
{radioYesNo.map((i) => (
<RadioItem
data-testid={`pregnant-${i.label}`}
key={i.value}
name="pregnant"
checked={pregnant === i.value}
onClick={() => setPregnant(i.value)}
>
{i.label}
</RadioItem>
))}
</List>
{/* Only display the 'female-only' div if the patient is female */}
{sex ? (
<div id="female-only">
<WhiteSpace />
<List renderHeader={() => 'Pregnant?'}>
{radioYesNo.map((i) => (
<RadioItem
data-testid={`pregnant-${i.label}`}
key={i.value}
name="pregnant"
checked={pregnant === i.value}
onClick={() => setPregnant(i.value)}
>
{i.label}
</RadioItem>
))}
</List>
<InputItem title="G" type="number" id="gravida" placeholder="0">
G
</InputItem>
<InputItem
title="G"
type="number"
id="gravida"
placeholder="0"
>
G
</InputItem>
<InputItem title="P" type="number" id="para" placeholder="0">
P
</InputItem>
<InputItem
title="P"
type="number"
id="para"
placeholder="0"
>
P
</InputItem>
<InputItem title="A" type="number" id="abortus" placeholder="0">
A
</InputItem>
<InputItem
title="A"
type="number"
id="abortus"
placeholder="0"
>
A
</InputItem>
<InputItem placeholder="YYYY-MM-DD" id="lmp" type="digit" maxLength={10}>
LMP
</InputItem>
<InputItem placeholder="YYYY-MM-DD" id="lmp" maxLength={10}>
LMP
</InputItem>
<List renderHeader={() => "Wants Planning?"}>
{radioYesNo.map((i) => (
<RadioItem
data-testid={`planning-${i.label}`}
key={i.value}
name="planning"
checked={planning === i.value}
onClick={() => setPlanning(i.value)}
>
{i.label}
</RadioItem>
))}
</List>
<List renderHeader={() => 'Wants Planning?'}>
{radioYesNo.map((i) => (
<RadioItem
data-testid={`planning-${i.label}`}
key={i.value}
name="planning"
checked={planning === i.value}
onClick={() => setPlanning(i.value)}
>
{i.label}
</RadioItem>
))}
</List>
</div>
) : null}
<WhiteSpace />
<InputItem
type="number"
placeholder="Weight"
id="weight"
maxLength={3}
>
Weight (kg)
</InputItem>
<InputItem
type="number"
placeholder="Height"
id="height"
maxLength={3}
>
Height (cm)
</InputItem>
<InputItem
placeholder="Blood Pressure"
id="bp"
value={bloodPressureValue}
onChange={validateAndSetBloodPressure}
>
BP
</InputItem>
<InputItem type="number" placeholder="Temp" id="temp" maxLength={3}>
Temp (F)
</InputItem>
<InputItem
type="number"
placeholder="Pulse"
id="pulse"
maxLength={3}
>
Pulse
</InputItem>
<InputItem type="number" placeholder="Z-Score" id="z-score">
Z-Score
</InputItem>
<List renderHeader={() => 'Albendazole?'}>
{radioYesNo.map((i) => (
<RadioItem
data-testid={`albendazole-${i.label}`}
key={i.value}
checked={albendazole === i.value}
onClick={() => setAlbendazole(i.value)}
>
{i.label}
</RadioItem>
))}
</List>
<List renderHeader={() => 'Allergies'}>
<TextareaItem autoHeight id="allergies" />
</List>
<List renderHeader={() => 'Medicines'}>
<TextareaItem autoHeight id="medicines" />
</List>
<List renderHeader={() => 'Chief Complaint'}>
<TextareaItem
placeholder="1."
// autoHeight
autoHeight
id="complaint1"
/>
<TextareaItem placeholder="2." autoHeight id="complaint2" />
<TextareaItem placeholder="3." autoHeight id="complaint3" />
</List>
<List renderHeader={() => 'History'}>
<TextareaItem autoHeight id="history" />
</List>
<List renderHeader={() => 'Exam'}>
<TextareaItem autoHeight id="exam" />
</List>
<WhiteSpace />
<div className="submit-button">
<List>
<Button data-testid="submit-button" onClick={onSubmit}>
Submit
</Button>
</List>
</div>
</div>
) : null}
<WhiteSpace />
<InputItem type="number" placeholder="Weight" id="weight" maxLength={3}>
Weight (kg)
</InputItem>
<InputItem type="number" placeholder="Height" id="height" maxLength={3}>
Height (cm)
</InputItem>
<InputItem
placeholder="Blood Pressure"
id="bp"
value={bloodPressureValue}
onChange={validateAndSetBloodPressure}
>
BP
</InputItem>
<InputItem type="number" placeholder="Temp" id="temp" maxLength={3}>
Temp (F)
</InputItem>
<InputItem type="number" placeholder="Pulse" id="pulse" maxLength={3}>
Pulse
</InputItem>
<InputItem type="number" placeholder="Z-Score" id="z-score">
Z-Score
</InputItem>
<List renderHeader={() => "Albendazole?"}>
{radioYesNo.map((i) => (
<RadioItem
data-testid={`albendazole-${i.label}`}
key={i.value}
checked={albendazole === i.value}
onClick={() => setAlbendazole(i.value)}
>
{i.label}
</RadioItem>
))}
</List>
<List renderHeader={() => "Allergies"}>
<TextareaItem autoHeight id="allergies" />
</List>
<List renderHeader={() => "Medicines"}>
<TextareaItem autoHeight id="medicines" />
</List>
<List renderHeader={() => "Chief Complaint"}>
<TextareaItem
placeholder="1."
// autoHeight
autoHeight
id="complaint1"
/>
<TextareaItem placeholder="2." autoHeight id="complaint2" />
<TextareaItem placeholder="3." autoHeight id="complaint3" />
</List>
<List renderHeader={() => "History"}>
<TextareaItem autoHeight id="history" />
</List>
<List renderHeader={() => "Exam"}>
<TextareaItem autoHeight id="exam" />
</List>
<WhiteSpace />
<div className="submit-button">
<List>
<Button onClick={onSubmit}>Submit</Button>
</List>
</div>
</div>
);
)
}
+100
View File
@@ -0,0 +1,100 @@
import React from 'react'
import { render, fireEvent } from '@testing-library/react'
import PatientSearch from './PatientSearch'
// TODO: mock out patient data
let searchComponent: any
let searchBar: HTMLInputElement
beforeEach(() => {
searchComponent = render(<PatientSearch />)
searchBar = searchComponent.getByTestId('search-bar') as HTMLInputElement
})
afterEach(() => {
searchComponent = null
searchBar = null
})
describe('search component initalization', () => {
test('it should have a search bar', () => {
expect(searchBar).toBeInTheDocument()
})
test('it should allow the search bar to be focused', () => {
expect(searchBar).not.toHaveFocus()
searchBar.focus()
expect(searchBar).toHaveFocus()
})
})
describe('search bar functionality', () => {
test('it should not display a list when no search has been performed', () => {
expect(searchComponent.queryByTestId('match-list')).toBeFalsy()
})
test('it should not display a list when search bar is empty', () => {
expect(searchComponent.queryByTestId('match-list')).toBeFalsy()
fireEvent.change(searchBar, { target: { value: 'alex' } })
expect(searchBar.value).toBe('alex')
const entryOne = searchComponent.queryByTestId('matched-patient-0')
expect(entryOne).toBeInTheDocument()
})
test('it should show one element for an exact match', () => {
expect(searchComponent.queryByTestId('match-list')).toBeFalsy()
fireEvent.change(searchBar, { target: { value: 'alex powers' } })
expect(searchBar.value).toBe('alex powers')
const entryOne = searchComponent.queryByTestId('matched-patient-0')
expect(entryOne).toBeInTheDocument()
expect(searchComponent.queryByTestId('matched-patient-1')).toBeFalsy()
fireEvent.change(searchBar, { target: { value: '' } })
expect(searchBar.value).toBe('')
expect(entryOne).not.toBeInTheDocument()
})
test('it should show add new patient for a no match', () => {
fireEvent.change(searchBar, { target: { value: 'not a real name' } })
expect(searchBar.value).toBe('not a real name')
const entryOne = searchComponent.queryByTestId('no-matched-patient')
expect(entryOne).toBeInTheDocument()
fireEvent.change(searchBar, { target: { value: '' } })
expect(searchBar.value).toBe('')
expect(entryOne).not.toBeInTheDocument()
})
test('it should have a list and defocus the search bar on enter', () => {
expect(true).toBe(true)
})
})
// TODO: make these methods more robust
// by evaluating the change of state once the navigation and new patient functions are implemented
describe('list element clicking', () => {
test('clicking an existing element should call navigateToPatientPage', () => {
// TODO: check something more robust than console log
const spy = jest.spyOn(console, 'log')
fireEvent.change(searchBar, { target: { value: 'Alex Powers' } })
expect(searchBar.value).toBe('Alex Powers')
const entryOne = searchComponent.queryByTestId('matched-patient-0')
expect(entryOne).toBeInTheDocument()
fireEvent.click(entryOne)
expect(spy).toHaveBeenCalled()
})
test('clicking an existing element should call navigateToPatientPage', () => {
// TODO: check something more robust than console log
const spy = jest.spyOn(console, 'log')
fireEvent.change(searchBar, { target: { value: 'not a real name' } })
expect(searchBar.value).toBe('not a real name')
const entryOne = searchComponent.queryByTestId('no-matched-patient')
expect(entryOne).toBeInTheDocument()
fireEvent.click(entryOne)
expect(spy).toHaveBeenCalled()
})
})
+80
View File
@@ -0,0 +1,80 @@
import React, { ReactElement, useState } from 'react'
import { SearchBar, WhiteSpace, List } from 'antd-mobile'
import '../scss/PatientSearch.scss'
import Patient from '../state/models/patient.model'
const patientsSeed: Array<Patient> = [
{ pk_patient_id: 1, first_name: 'Alex', last_name: 'Powers' },
{ pk_patient_id: 2, first_name: 'Meghan', last_name: 'McLaughlin' },
{ pk_patient_id: 3, first_name: 'Ben', last_name: 'Mitchinson' },
{ pk_patient_id: 4, first_name: 'Zain', last_name: 'Khan' },
{ pk_patient_id: 5, first_name: 'Hans', last_name: 'Johnson' },
]
export default function PatientSearch(): ReactElement {
const [matches, setMatches] = useState([])
const [patients] = useState(patientsSeed)
const [searchValue, setSearchValue] = useState('')
// TODO: replace this logic
// this could potentially use Fuse.js or fuzzy database queries
const searchForName = (value: string): void => {
setSearchValue(value)
if (value) {
const listOfMatches = patients.filter((element: Patient) => {
return `${element.first_name} ${element.last_name}`
.toLowerCase()
.includes(value.toLowerCase())
})
setMatches(listOfMatches)
} else {
setMatches([])
}
}
const addNewPatient = (): void => {
console.log('NO PATIENT FOUND, ADDING A NEW ONE')
}
const navigateToPatientPage = (patient: Patient): void => {
console.log(`Navigating to patient ${JSON.stringify(patient)}`)
}
return (
<>
<SearchBar
data-testid="search-bar"
placeholder="Patient Name"
maxLength={30}
cancelText="Cancel"
onChange={(value: string): void => searchForName(value)}
/>
<WhiteSpace />
{searchValue ? (
<>
<List data-test-id="match-list">
{matches.map((matchElement: Patient, index: number) => (
<List.Item
data-testid={`matched-patient-${index}`}
key={matchElement.pk_patient_id}
onClick={(): void =>
navigateToPatientPage(matchElement)
}
>{`${matchElement.first_name} ${matchElement.last_name}`}</List.Item>
))}
{matches.length === 0 && (
<List.Item
data-testid="no-matched-patient"
onClick={(): void => addNewPatient()}
>
Add New Patient
</List.Item>
)}
</List>
</>
) : null}
</>
)
}
+5
View File
@@ -0,0 +1,5 @@
#diagnosis-form-wrapper {
.list-whitespace {
background-color: #f5f5f9;
}
}
+3
View File
@@ -0,0 +1,3 @@
.am-search-clear-show {
display: none !important;
}
+13
View File
@@ -0,0 +1,13 @@
/* istanbul ignore file */
export enum SEX {
male = 'male',
female = 'female',
}
export enum YN {
YES = 1,
NO = 0,
}
export default {}
+6
View File
@@ -0,0 +1,6 @@
interface Patient {
pk_patient_id?: number
first_name?: string
last_name?: string
}
export default Patient
@@ -0,0 +1,48 @@
import { Action, action } from 'easy-peasy'
import { SEX, YN } from '../enums'
interface PatientInfoFormFields {
firstName?: string
lastName?: string
nickname?: string
sex?: SEX
birthDate?: Date
age?: number
phone?: string
town?: string
firstVisit?: YN
pregnant?: YN
G?: number
P?: number
A?: number
LMP?: Date
wantsPlanning?: YN
weight?: number
height?: number
bloodPressure?: string
temperature?: number
pulse?: number
zScore?: number
albendazole?: YN
allergies?: string
medicines?: string
chiefComplaint?: string[]
history?: string
exam?: string
}
export interface PatientInfoFormModel {
fields: PatientInfoFormFields
updateFields: Action<PatientInfoFormModel, PatientInfoFormFields>
}
/* istanbul ignore next */
// Form submission requests validated in integration test
const patientInfoFormModel: PatientInfoFormModel = {
fields: {},
updateFields: action((state, formObject) => {
state.fields = formObject
}),
}
export default patientInfoFormModel
+8
View File
@@ -0,0 +1,8 @@
/* istanbul ignore file */
import { createStore, persist } from 'easy-peasy'
import storeIndex from './storeIndex'
const store = createStore(persist(storeIndex))
export default store
+17
View File
@@ -0,0 +1,17 @@
/* istanbul ignore file */
import { createTypedHooks } from 'easy-peasy'
import patientInfoFormModel from './models/patientInfoForm.model'
export const storeModels = {
patientInfoFormModel,
}
export const {
useStore,
useStoreState,
useStoreActions,
useStoreDispatch,
} = createTypedHooks<typeof storeModels>()
export default storeModels