mirror of
https://github.com/Haiti-Distributed-Healthcare-System/hdhs.git
synced 2026-11-12 01:06:21 +00:00
Merge dev into treatment form
This commit is contained in:
commit
a474bc32f4
42 files changed
+2058
-396
No files matched your search
+5
-1
@@ -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.
Whitespace-only changes.
@@ -0,0 +1 @@
|
||||
**/__pycache__/**
|
||||
@@ -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
|
||||
```
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
# Author: "abpwrs"
|
||||
# Date: 20200409
|
||||
|
||||
# script:
|
||||
isort -rc .
|
||||
black .
|
||||
|
||||
@@ -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]
|
||||
Executable
+40
@@ -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()))
|
||||
@@ -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)
|
||||
@@ -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()))
|
||||
@@ -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)
|
||||
Whitespace-only changes.
+40
@@ -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
|
||||
@@ -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()))
|
||||
@@ -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
@@ -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
|
||||
)
|
||||
@@ -24,7 +24,7 @@ services:
|
||||
ports:
|
||||
- "5432:5432"
|
||||
expose:
|
||||
- "5422"
|
||||
- "5432"
|
||||
environment:
|
||||
POSTGRES_DB: app_db
|
||||
POSTGRES_USER: postgres
|
||||
|
||||
@@ -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
@@ -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
|
||||
},
|
||||
},
|
||||
}
|
||||
Generated
+83
-11
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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')
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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()
|
||||
})
|
||||
@@ -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 />
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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>
|
||||
);
|
||||
)
|
||||
}
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#diagnosis-form-wrapper {
|
||||
.list-whitespace {
|
||||
background-color: #f5f5f9;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.am-search-clear-show {
|
||||
display: none !important;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/* istanbul ignore file */
|
||||
|
||||
export enum SEX {
|
||||
male = 'male',
|
||||
female = 'female',
|
||||
}
|
||||
|
||||
export enum YN {
|
||||
YES = 1,
|
||||
NO = 0,
|
||||
}
|
||||
|
||||
export default {}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in new issue
Block a user