From 12a86cfda92a340c2ee41898996d59d972878477 Mon Sep 17 00:00:00 2001 From: John Dunning Date: Thu, 14 Jul 2022 19:45:26 -0700 Subject: [PATCH 01/33] Create Metadata class to store field metadata Move patient field setup into metadata/patient.js. --- metadata/Metadata.js | 54 ++++++++++++++ metadata/patient.js | 128 +++++++++++++++++++++++++++++++++ models/patient.js | 165 ++----------------------------------------- 3 files changed, 186 insertions(+), 161 deletions(-) create mode 100644 metadata/Metadata.js create mode 100644 metadata/patient.js diff --git a/metadata/Metadata.js b/metadata/Metadata.js new file mode 100644 index 00000000..e28a0f3e --- /dev/null +++ b/metadata/Metadata.js @@ -0,0 +1,54 @@ +const { DataTypes } = require('sequelize'); + +const Suffixes = { + [DataTypes.UUID]: '_uuid', + [DataTypes.ENUM]: 'enum', +}; + +function createColName({ name, colName, type }){ + if (colName) { + return colName; + } + + return name.toLowerCase() + (Suffixes[type] || ''); +} + +function createParams(fields) { + const params = []; + + fields.forEach(({ name, type }) => { + if (type !== DataTypes.UUID && type !== DataTypes.DATE) { + params.push(name); + } + }); + + return Object.freeze(params); +} + +class Metadata { + constructor(fields) + { + this.fields = Object.freeze(fields.map((field) => { + field.colName = createColName(field); + + return field; + })); + this.params = createParams(this.fields); + } + + getFieldHash() { + const hash = {}; + + this.fields.forEach(({ name, colName: field, ...rest }) => { + hash[name] = { field, ... rest }; + }); + + return hash; + } + + getParams() { + return this.params; + } +} + +module.exports = Metadata; diff --git a/metadata/patient.js b/metadata/patient.js new file mode 100644 index 00000000..5d3c126e --- /dev/null +++ b/metadata/patient.js @@ -0,0 +1,128 @@ +const { DataTypes } = require('sequelize'); +const Metadata = require('./Metadata'); + +const fields = [ + { + name: 'id', + colName: 'patient_uuid', + type: DataTypes.UUID, + primaryKey: true, + autoIncrement: true, + }, + { + name: 'EmergencyMedicalServiceCallId', + colName: 'emergencymedicalservicecall_uuid', + type: DataTypes.UUID, + allowNull: false, + unique: true, + }, + { + name: 'age', + type: DataTypes.INTEGER, + }, + { + name: 'sex', + type: DataTypes.ENUM('MALE', 'FEMALE', 'NON-BINARY'), + }, + { + name: 'emergencyServiceResponseType', + type: DataTypes.ENUM('CODE 2', 'CODE 3'), + }, + { + name: 'chiefComplaintDescription', + type: DataTypes.TEXT, + }, + { + name: 'stableIndicator', + type: DataTypes.BOOLEAN, + }, + { + name: 'systolicBloodPressure', + type: DataTypes.INTEGER, + }, + { + name: 'diastolicBloodPressure', + type: DataTypes.INTEGER, + }, + { + name: 'heartRateBpm', + type: DataTypes.INTEGER, + }, + { + name: 'respiratoryRate', + type: DataTypes.INTEGER, + }, + { + name: 'oxygenSaturation', + type: DataTypes.INTEGER, + }, + { + name: 'lowOxygenResponseType', + type: DataTypes.ENUM('ROOM AIR', 'SUPPLEMENTAL OXYGEN'), + }, + { + name: 'supplementalOxygenAmount', + type: DataTypes.INTEGER, + }, + { + name: 'temperature', + type: DataTypes.DECIMAL, + }, + { + name: 'etohSuspectedIndicator', + type: DataTypes.BOOLEAN, + }, + { + name: 'drugsSuspectedIndicator', + type: DataTypes.BOOLEAN, + }, + { + name: 'psychIndicator', + type: DataTypes.BOOLEAN, + }, + { + name: 'combativeBehaviorIndicator', + type: DataTypes.BOOLEAN, + }, + { + name: 'restraintIndicator', + type: DataTypes.BOOLEAN, + }, + { + name: 'covid19SuspectedIndicator', + colName: 'covid-19suspectedindicator', + type: DataTypes.BOOLEAN, + }, + { + name: 'ivIndicator', + colName: 'ivindicator', + type: DataTypes.BOOLEAN, + }, + { + name: 'otherObservationNotes', + colName: 'otherobservationnotes', + type: DataTypes.TEXT, + }, + { + name: 'createdAt', + colName: 'recordcreatetimestamp', + type: DataTypes.DATE, + }, + { + name: 'CreatedById', + colName: 'recordcreateuser_uuid', + type: DataTypes.UUID, + }, + { + name: 'updatedAt', + colName: 'recordupdatetimestamp', + type: DataTypes.DATE, + }, + { + name: 'UpdatedById', + colName: 'recordupdateuser_uuid', + type: DataTypes.UUID, + }, +]; + +module.exports = new Metadata(fields); diff --git a/models/patient.js b/models/patient.js index 274a2ff0..434dc758 100644 --- a/models/patient.js +++ b/models/patient.js @@ -1,34 +1,10 @@ const { Model } = require('sequelize'); +const patient = require('../metadata/patient'); -const PatientParams = [ - 'age', - 'sex', - 'emergencyServiceResponseType', - 'chiefComplaintDescription', - 'stableIndicator', - 'systolicBloodPressure', - 'diastolicBloodPressure', - 'heartRateBpm', - 'respiratoryRate', - 'oxygenSaturation', - 'lowOxygenResponseType', - 'supplementalOxygenAmount', - 'temperature', - 'etohSuspectedIndicator', - 'drugsSuspectedIndicator', - 'psychIndicator', - 'combativeBehaviorIndicator', - 'restraintIndicator', - 'covid19SuspectedIndicator', - 'ivIndicator', - 'otherObservationNotes', -]; -Object.freeze(PatientParams); - -module.exports = (sequelize, DataTypes) => { +module.exports = (sequelize) => { class Patient extends Model { static get Params() { - return PatientParams; + return patient.getParams(); } static associate(models) { @@ -40,140 +16,7 @@ module.exports = (sequelize, DataTypes) => { } } Patient.init( - { - id: { - field: 'patient_uuid', - type: DataTypes.UUID, - primaryKey: true, - autoIncrement: true, - }, - EmergencyMedicalServiceCallId: { - field: 'emergencymedicalservicecall_uuid', - type: DataTypes.UUID, - unique: true, - allowNull: false, - }, - age: { - field: 'age', - type: DataTypes.INTEGER, - allowNull: true, - }, - sex: { - field: 'sexenum', - type: DataTypes.ENUM('MALE', 'FEMALE', 'NON-BINARY'), - allowNull: true, - }, - emergencyServiceResponseType: { - field: 'emergencyserviceresponsetypeenum', - type: DataTypes.ENUM('CODE 2', 'CODE 3'), - }, - chiefComplaintDescription: { - field: 'chiefcomplaintdescription', - type: DataTypes.TEXT, - allowNull: true, - }, - stableIndicator: { - field: 'stableindicator', - type: DataTypes.BOOLEAN, - allowNull: true, - }, - systolicBloodPressure: { - field: 'systolicbloodpressure', - type: DataTypes.INTEGER, - allowNull: true, - }, - diastolicBloodPressure: { - field: 'diastolicbloodpressure', - type: DataTypes.INTEGER, - allowNull: true, - }, - heartRateBpm: { - field: 'heartratebpm', - type: DataTypes.INTEGER, - allowNull: true, - }, - respiratoryRate: { - field: 'respiratoryrate', - type: DataTypes.INTEGER, - allowNull: true, - }, - oxygenSaturation: { - field: 'oxygensaturation', - type: DataTypes.INTEGER, - allowNull: true, - }, - lowOxygenResponseType: { - field: 'lowoxygenresponsetypeenum', - type: DataTypes.ENUM('ROOM AIR', 'SUPPLEMENTAL OXYGEN'), - allowNull: true, - }, - supplementalOxygenAmount: { - field: 'supplementaloxygenamount', - type: DataTypes.INTEGER, - allowNull: true, - }, - temperature: { - field: 'temperature', - type: DataTypes.DECIMAL, - allowNull: true, - }, - etohSuspectedIndicator: { - field: 'etohsuspectedindicator', - type: DataTypes.BOOLEAN, - allowNull: true, - }, - drugsSuspectedIndicator: { - field: 'drugssuspectedindicator', - type: DataTypes.BOOLEAN, - allowNull: true, - }, - psychIndicator: { - field: 'psychindicator', - type: DataTypes.BOOLEAN, - allowNull: true, - }, - combativeBehaviorIndicator: { - field: 'combativebehaviorindicator', - type: DataTypes.BOOLEAN, - allowNull: true, - }, - restraintIndicator: { - field: 'restraintindicator', - type: DataTypes.BOOLEAN, - allowNull: true, - }, - covid19SuspectedIndicator: { - field: 'covid-19suspectedindicator', - type: DataTypes.BOOLEAN, - allowNull: true, - }, - ivIndicator: { - field: 'ivindicator', - type: DataTypes.BOOLEAN, - allowNull: true, - }, - otherObservationNotes: { - field: 'otherobservationnotes', - type: DataTypes.TEXT, - allowNull: true, - }, - createdAt: { - field: 'recordcreatetimestamp', - type: DataTypes.DATE, - }, - CreatedById: { - field: 'recordcreateuser_uuid', - type: DataTypes.UUID, - }, - updatedAt: { - field: 'recordupdatetimestamp', - type: DataTypes.DATE, - }, - UpdatedById: { - field: 'recordupdateuser_uuid', - type: DataTypes.UUID, - }, - }, + patient.getFieldHash(), { sequelize, timestamps: true, From 638c97fe95dbce773f44ab63bb8374c9f88c2827 Mon Sep 17 00:00:00 2001 From: John Dunning Date: Fri, 15 Jul 2022 16:04:20 -0700 Subject: [PATCH 02/33] Move metadata into src/metadata CRA annoyingly refuses to import anything from outside that folder. --- models/patient.js | 2 +- {metadata => src/metadata}/Metadata.js | 108 ++++++++++++------------- {metadata => src/metadata}/patient.js | 0 3 files changed, 55 insertions(+), 55 deletions(-) rename {metadata => src/metadata}/Metadata.js (94%) rename {metadata => src/metadata}/patient.js (100%) diff --git a/models/patient.js b/models/patient.js index 434dc758..c8e648f0 100644 --- a/models/patient.js +++ b/models/patient.js @@ -1,5 +1,5 @@ const { Model } = require('sequelize'); -const patient = require('../metadata/patient'); +const patient = require('../src/metadata/patient'); module.exports = (sequelize) => { class Patient extends Model { diff --git a/metadata/Metadata.js b/src/metadata/Metadata.js similarity index 94% rename from metadata/Metadata.js rename to src/metadata/Metadata.js index e28a0f3e..1d9bf07b 100644 --- a/metadata/Metadata.js +++ b/src/metadata/Metadata.js @@ -1,54 +1,54 @@ -const { DataTypes } = require('sequelize'); - -const Suffixes = { - [DataTypes.UUID]: '_uuid', - [DataTypes.ENUM]: 'enum', -}; - -function createColName({ name, colName, type }){ - if (colName) { - return colName; - } - - return name.toLowerCase() + (Suffixes[type] || ''); -} - -function createParams(fields) { - const params = []; - - fields.forEach(({ name, type }) => { - if (type !== DataTypes.UUID && type !== DataTypes.DATE) { - params.push(name); - } - }); - - return Object.freeze(params); -} - -class Metadata { - constructor(fields) - { - this.fields = Object.freeze(fields.map((field) => { - field.colName = createColName(field); - - return field; - })); - this.params = createParams(this.fields); - } - - getFieldHash() { - const hash = {}; - - this.fields.forEach(({ name, colName: field, ...rest }) => { - hash[name] = { field, ... rest }; - }); - - return hash; - } - - getParams() { - return this.params; - } -} - -module.exports = Metadata; +const { DataTypes } = require('sequelize'); + +const Suffixes = { + [DataTypes.UUID]: '_uuid', + [DataTypes.ENUM]: 'enum', +}; + +function createColName({ name, colName, type }){ + if (colName) { + return colName; + } + + return name.toLowerCase() + (Suffixes[type] || ''); +} + +function createParams(fields) { + const params = []; + + fields.forEach(({ name, type }) => { + if (type !== DataTypes.UUID && type !== DataTypes.DATE) { + params.push(name); + } + }); + + return Object.freeze(params); +} + +class Metadata { + constructor(fields) + { + this.fields = Object.freeze(fields.map((field) => { + field.colName = createColName(field); + + return field; + })); + this.params = createParams(this.fields); + } + + getFieldHash() { + const hash = {}; + + this.fields.forEach(({ name, colName: field, ...rest }) => { + hash[name] = { field, ... rest }; + }); + + return hash; + } + + getParams() { + return this.params; + } +} + +module.exports = Metadata; diff --git a/metadata/patient.js b/src/metadata/patient.js similarity index 100% rename from metadata/patient.js rename to src/metadata/patient.js From 03106508270c3152dedf8d9cc63f2340c8286661 Mon Sep 17 00:00:00 2001 From: John Dunning Date: Tue, 19 Jul 2022 00:05:08 -0700 Subject: [PATCH 03/33] Add getters/setters to Ringdown based on field metadata from patient Also generate PropTypes from field metadata. Use a shared DeliveryStatus in Ringdown.js instead of defining a copy. Move deliveryStatus.js to src/constants/DeliveryStatus.js. Rename Metadata.js to ModelMetadata.js. Add FieldMetadata.js and convertToSequelizeField.js. --- constants/deliveryStatus.js | 31 --- constants/index.js | 1 - models/hospital.js | 2 +- models/hospitalStatusUpdate.js | 2 +- models/patient.js | 16 +- models/patientDelivery.js | 2 +- models/patientDeliveryUpdate.js | 2 +- routes/api/ringdowns.js | 2 +- seeders/20210204001430-ringdowns-seed.js | 2 +- src/Models/Ringdown.js | 260 ++++------------------- src/constants/DeliveryStatus.js | 17 ++ src/constants/index.js | 1 + src/metadata/FieldMetadata.js | 31 +++ src/metadata/Metadata.js | 54 ----- src/metadata/ModelMetadata.js | 65 ++++++ src/metadata/convertToSequelizeField.js | 32 +++ src/metadata/patient.js | 69 +++--- test/integration/api/ringdowns.js | 2 +- test/unit/models/patientDelivery.js | 2 +- wss.js | 2 +- 20 files changed, 246 insertions(+), 349 deletions(-) delete mode 100644 constants/deliveryStatus.js delete mode 100644 constants/index.js create mode 100644 src/constants/DeliveryStatus.js create mode 100644 src/constants/index.js create mode 100644 src/metadata/FieldMetadata.js delete mode 100644 src/metadata/Metadata.js create mode 100644 src/metadata/ModelMetadata.js create mode 100644 src/metadata/convertToSequelizeField.js diff --git a/constants/deliveryStatus.js b/constants/deliveryStatus.js deleted file mode 100644 index 9d6bed6d..00000000 --- a/constants/deliveryStatus.js +++ /dev/null @@ -1,31 +0,0 @@ -const DeliveryStatus = { - RINGDOWN_SENT: 'RINGDOWN SENT', - RINGDOWN_RECEIVED: 'RINGDOWN RECEIVED', - RINGDOWN_CONFIRMED: 'RINGDOWN CONFIRMED', - ARRIVED: 'ARRIVED', - OFFLOADED: 'OFFLOADED', - OFFLOADED_ACKNOWLEDGED: 'OFFLOADED ACKNOWLEDGED', - RETURNED_TO_SERVICE: 'RETURNED TO SERVICE', - CANCELLED: 'CANCELLED', - CANCEL_ACKNOWLEDGED: 'CANCEL ACKNOWLEDGED', - REDIRECTED: 'REDIRECTED', - REDIRECT_ACKNOWLEDGED: 'REDIRECT ACKNOWLEDGED', -}; - -DeliveryStatus.ALL_STATUSES = [ - DeliveryStatus.RINGDOWN_SENT, - DeliveryStatus.RINGDOWN_RECEIVED, - DeliveryStatus.RINGDOWN_CONFIRMED, - DeliveryStatus.ARRIVED, - DeliveryStatus.OFFLOADED, - DeliveryStatus.OFFLOADED_ACKNOWLEDGED, - DeliveryStatus.RETURNED_TO_SERVICE, - DeliveryStatus.CANCELLED, - DeliveryStatus.CANCEL_ACKNOWLEDGED, - DeliveryStatus.REDIRECTED, - DeliveryStatus.REDIRECT_ACKNOWLEDGED, -]; - -Object.freeze(DeliveryStatus); - -module.exports = DeliveryStatus; diff --git a/constants/index.js b/constants/index.js deleted file mode 100644 index 4791c3e2..00000000 --- a/constants/index.js +++ /dev/null @@ -1 +0,0 @@ -exports.DeliveryStatus = require('./deliveryStatus'); diff --git a/models/hospital.js b/models/hospital.js index f03f97d3..34fcee0f 100644 --- a/models/hospital.js +++ b/models/hospital.js @@ -1,6 +1,6 @@ const _ = require('lodash'); const { Model } = require('sequelize'); -const { DeliveryStatus } = require('../constants'); +const { DeliveryStatus } = require('../src/constants'); module.exports = (sequelize, DataTypes) => { class Hospital extends Model { diff --git a/models/hospitalStatusUpdate.js b/models/hospitalStatusUpdate.js index 0a5685d3..12fac320 100644 --- a/models/hospitalStatusUpdate.js +++ b/models/hospitalStatusUpdate.js @@ -1,6 +1,6 @@ const _ = require('lodash'); const { Model, Op } = require('sequelize'); -const { DeliveryStatus } = require('../constants'); +const { DeliveryStatus } = require('../src/constants'); module.exports = (sequelize, DataTypes) => { class HospitalStatusUpdate extends Model { diff --git a/models/patient.js b/models/patient.js index c8e648f0..e53f743f 100644 --- a/models/patient.js +++ b/models/patient.js @@ -1,5 +1,6 @@ const { Model } = require('sequelize'); const patient = require('../src/metadata/patient'); +const convertToSequelizeField = require('../src/metadata/convertToSequelizeField'); module.exports = (sequelize) => { class Patient extends Model { @@ -15,14 +16,11 @@ module.exports = (sequelize) => { Patient.belongsTo(models.User, { as: 'UpdatedBy' }); } } - Patient.init( - patient.getFieldHash(), - { - sequelize, - timestamps: true, - tableName: 'patient', - modelName: 'Patient', - } - ); + Patient.init(patient.getFieldHash(convertToSequelizeField), { + sequelize, + timestamps: true, + tableName: 'patient', + modelName: 'Patient', + }); return Patient; }; diff --git a/models/patientDelivery.js b/models/patientDelivery.js index b4679922..9ed0e39f 100644 --- a/models/patientDelivery.js +++ b/models/patientDelivery.js @@ -1,6 +1,6 @@ const _ = require('lodash'); const { Model } = require('sequelize'); -const { DeliveryStatus } = require('../constants'); +const { DeliveryStatus } = require('../src/constants'); const PatientDeliveryParams = ['currentDeliveryStatus', 'currentDeliveryStatusDateTimeLocal', 'etaMinutes']; Object.freeze(PatientDeliveryParams); diff --git a/models/patientDeliveryUpdate.js b/models/patientDeliveryUpdate.js index 181489af..b6e2777b 100644 --- a/models/patientDeliveryUpdate.js +++ b/models/patientDeliveryUpdate.js @@ -1,5 +1,5 @@ const { Model } = require('sequelize'); -const { DeliveryStatus } = require('../constants'); +const { DeliveryStatus } = require('../src/constants'); module.exports = (sequelize, DataTypes) => { class PatientDeliveryUpdate extends Model { diff --git a/routes/api/ringdowns.js b/routes/api/ringdowns.js index 72f65222..400ae9a8 100644 --- a/routes/api/ringdowns.js +++ b/routes/api/ringdowns.js @@ -5,7 +5,7 @@ const _ = require('lodash'); const middleware = require('../../auth/middleware'); const models = require('../../models'); const { dispatchRingdownUpdate } = require('../../wss'); -const { DeliveryStatus } = require('../../constants'); +const { DeliveryStatus } = require('../../src/constants'); const { setPaginationHeaders } = require('../helpers'); diff --git a/seeders/20210204001430-ringdowns-seed.js b/seeders/20210204001430-ringdowns-seed.js index 00686c46..f005d5b3 100644 --- a/seeders/20210204001430-ringdowns-seed.js +++ b/seeders/20210204001430-ringdowns-seed.js @@ -1,6 +1,6 @@ const _ = require('lodash'); const models = require('../models'); -const { DeliveryStatus } = require('../constants'); +const { DeliveryStatus } = require('../src/constants'); async function createRingdown( email, diff --git a/src/Models/Ringdown.js b/src/Models/Ringdown.js index 05851ae7..f718efe6 100644 --- a/src/Models/Ringdown.js +++ b/src/Models/Ringdown.js @@ -1,26 +1,40 @@ import { DateTime } from 'luxon'; import PropTypes from 'prop-types'; import { PatientFieldData, ValidationState } from './PatientFieldData'; +import patient from '../metadata/patient'; +import DeliveryStatus from '../constants/DeliveryStatus'; + +function attachFields(target, fields, data) { + const props = {}; + + fields.forEach((field) => { + props[field.name] = { + get() { + return data[field.name] ?? field.defaultValue; + }, + set(newValue) { + data[field.name] = newValue; + }, + configurable: true, + enumerable: true, + }; + }); -const DeliveryStatus = { - RINGDOWN_SENT: 'RINGDOWN SENT', - RINGDOWN_RECEIVED: 'RINGDOWN RECEIVED', - RINGDOWN_CONFIRMED: 'RINGDOWN CONFIRMED', - ARRIVED: 'ARRIVED', - OFFLOADED: 'OFFLOADED', - OFFLOADED_ACKNOWLEDGED: 'OFFLOADED ACKNOWLEDGED', - RETURNED_TO_SERVICE: 'RETURNED TO SERVICE', - CANCELLED: 'CANCELLED', - CANCEL_ACKNOWLEDGED: 'CANCEL ACKNOWLEDGED', - REDIRECTED: 'REDIRECTED', - REDIRECT_ACKNOWLEDGED: 'REDIRECT ACKNOWLEDGED', -}; + Object.defineProperties(target, props); +} -DeliveryStatus.ALL_STATUSES = Object.values(DeliveryStatus); +function overrideSetter(target, key, setter) { + const descriptor = Object.getOwnPropertyDescriptor(target, key); -DeliveryStatus.is = (status, target) => DeliveryStatus.ALL_STATUSES.indexOf(status) >= DeliveryStatus.ALL_STATUSES.indexOf(target); + if (!descriptor || !descriptor.set) { + throw new Error(`setter for '${key}' does not exist on the target.`); + } -Object.freeze(DeliveryStatus); + Object.defineProperty(target, key, { + ...descriptor, + set: setter, + }); +} class Ringdown { static get Status() { @@ -34,6 +48,22 @@ class Ringdown { this.payload.hospital = this.payload.hospital || {}; this.payload.patient = this.payload.patient || {}; this.payload.patientDelivery = this.payload.patientDelivery || {}; + + // add getters/setters for patient fields + attachFields(this, patient.getObjectFields(), this.payload.patient); + + // add custom setters for these payload fields, since their settings affect other fields + overrideSetter(this, 'lowOxygenResponseType', (newValue) => { + this.payload.patient.lowOxygenResponseType = newValue; + if (newValue !== 'SUPPLEMENTAL OXYGEN') { + this.supplementalOxygenAmount = null; + } + }); + overrideSetter(this, 'combativeBehaviorIndicator', (newValue) => { + this.payload.patient.combativeBehaviorIndicator = newValue; + this.restraintIndicator = newValue && this.restraintIndicator; + }); + this.validationData = validationData || { ambulanceIdentifier: new PatientFieldData( 'ambulanceIdentifier', @@ -123,115 +153,6 @@ class Ringdown { // Patient Info - get age() { - return this.payload.patient.age ?? null; - } - - set age(newValue) { - this.payload.patient.age = newValue; - } - - get sex() { - return this.payload.patient.sex ?? null; - } - - set sex(newValue) { - this.payload.patient.sex = newValue; - } - - get emergencyServiceResponseType() { - return this.payload.patient.emergencyServiceResponseType ?? null; - } - - set emergencyServiceResponseType(newValue) { - this.payload.patient.emergencyServiceResponseType = newValue; - } - - get chiefComplaintDescription() { - return this.payload.patient.chiefComplaintDescription ?? null; - } - - set chiefComplaintDescription(newValue) { - this.payload.patient.chiefComplaintDescription = newValue; - } - - get stableIndicator() { - return this.payload.patient.stableIndicator ?? null; - } - - set stableIndicator(newValue) { - this.payload.patient.stableIndicator = newValue; - } - - // Vitals - - get systolicBloodPressure() { - return this.payload.patient.systolicBloodPressure ?? null; - } - - set systolicBloodPressure(newValue) { - this.payload.patient.systolicBloodPressure = newValue; - } - - get diastolicBloodPressure() { - return this.payload.patient.diastolicBloodPressure ?? null; - } - - set diastolicBloodPressure(newValue) { - this.payload.patient.diastolicBloodPressure = newValue; - } - - get heartRateBpm() { - return this.payload.patient.heartRateBpm ?? null; - } - - set heartRateBpm(newValue) { - this.payload.patient.heartRateBpm = newValue; - } - - get respiratoryRate() { - return this.payload.patient.respiratoryRate ?? null; - } - - set respiratoryRate(newValue) { - this.payload.patient.respiratoryRate = newValue; - } - - get oxygenSaturation() { - return this.payload.patient.oxygenSaturation ?? null; - } - - set oxygenSaturation(newValue) { - this.payload.patient.oxygenSaturation = newValue; - } - - get lowOxygenResponseType() { - return this.payload.patient.lowOxygenResponseType ?? null; - } - - set lowOxygenResponseType(newValue) { - this.payload.patient.lowOxygenResponseType = newValue; - if (newValue !== 'SUPPLEMENTAL OXYGEN') { - this.supplementalOxygenAmount = null; - } - } - - get supplementalOxygenAmount() { - return this.payload.patient.supplementalOxygenAmount ?? null; - } - - set supplementalOxygenAmount(newValue) { - this.payload.patient.supplementalOxygenAmount = newValue; - } - - get temperature() { - return this.payload.patient.temperature ?? null; - } - - set temperature(newValue) { - this.payload.patient.temperature = newValue; - } - get hasVitals() { return ( this.systolicBloodPressure || @@ -247,71 +168,6 @@ class Ringdown { // Addtl Notes - get etohSuspectedIndicator() { - return this.payload.patient.etohSuspectedIndicator ?? false; - } - - set etohSuspectedIndicator(newValue) { - this.payload.patient.etohSuspectedIndicator = newValue; - } - - get drugsSuspectedIndicator() { - return this.payload.patient.drugsSuspectedIndicator ?? false; - } - - set drugsSuspectedIndicator(newValue) { - this.payload.patient.drugsSuspectedIndicator = newValue; - } - - get psychIndicator() { - return this.payload.patient.psychIndicator ?? false; - } - - set psychIndicator(newValue) { - this.payload.patient.psychIndicator = newValue; - } - - get combativeBehaviorIndicator() { - return this.payload.patient.combativeBehaviorIndicator ?? false; - } - - set combativeBehaviorIndicator(newValue) { - this.payload.patient.combativeBehaviorIndicator = newValue; - this.restraintIndicator = newValue && this.restraintIndicator; - } - - get restraintIndicator() { - return this.payload.patient.restraintIndicator ?? false; - } - - set restraintIndicator(newValue) { - this.payload.patient.restraintIndicator = newValue; - } - - get covid19SuspectedIndicator() { - return this.payload.patient.covid19SuspectedIndicator ?? false; - } - - set covid19SuspectedIndicator(newValue) { - this.payload.patient.covid19SuspectedIndicator = newValue; - } - - get ivIndicator() { - return this.payload.patient.ivIndicator ?? false; - } - - set ivIndicator(newValue) { - this.payload.patient.ivIndicator = newValue; - } - - get otherObservationNotes() { - return this.payload.patient.otherObservationNotes ?? null; - } - - set otherObservationNotes(newValue) { - this.payload.patient.otherObservationNotes = newValue; - } - get hasAdditionalNotes() { return ( this.etohSuspectedIndicator || @@ -439,29 +295,7 @@ Ringdown.propTypes = { dispatchCallNumber: PropTypes.number.isRequired, hospitalId: PropTypes.string.isRequired, // Patient Info - age: PropTypes.number.isRequired, - sex: PropTypes.oneOf(['MALE', 'FEMALE', 'NON-BINARY']).isRequired, - emergencyServiceResponseType: PropTypes.oneOf(['CODE 2', 'CODE 3']).isRequired, - chiefComplaintDescription: PropTypes.string.isRequired, - stableIndicator: PropTypes.bool.isRequired, - // Vitals - systolicBloodPressure: PropTypes.number, - diastolicBloodPressure: PropTypes.number, - heartRateBpm: PropTypes.number, - respiratoryRate: PropTypes.number, - oxygenSaturation: PropTypes.number, - lowOxygenResponseType: PropTypes.oneOf(['ROOM AIR', 'SUPPLEMENTAL OXYGEN']), - supplementalOxygenAmount: PropTypes.number, - temperature: PropTypes.number, - // Addtl. Notes - etohSuspectedIndicator: PropTypes.bool, - drugsSuspectedIndicator: PropTypes.bool, - psychIndicator: PropTypes.bool, - combativeBehaviorIndicator: PropTypes.bool, - restraintIndicator: PropTypes.bool, - covid19SuspectedIndicator: PropTypes.bool, - ivIndicator: PropTypes.bool, - otherObservationNotes: PropTypes.string, + ...patient.getPropTypes(PropTypes), // Status etaMinutes: PropTypes.number.isRequired, currentDeliveryStatus: PropTypes.oneOf(DeliveryStatus.ALL_STATUSES), diff --git a/src/constants/DeliveryStatus.js b/src/constants/DeliveryStatus.js new file mode 100644 index 00000000..b65602ba --- /dev/null +++ b/src/constants/DeliveryStatus.js @@ -0,0 +1,17 @@ +const DeliveryStatus = { + RINGDOWN_SENT: 'RINGDOWN SENT', + RINGDOWN_RECEIVED: 'RINGDOWN RECEIVED', + RINGDOWN_CONFIRMED: 'RINGDOWN CONFIRMED', + ARRIVED: 'ARRIVED', + OFFLOADED: 'OFFLOADED', + OFFLOADED_ACKNOWLEDGED: 'OFFLOADED ACKNOWLEDGED', + RETURNED_TO_SERVICE: 'RETURNED TO SERVICE', + CANCELLED: 'CANCELLED', + CANCEL_ACKNOWLEDGED: 'CANCEL ACKNOWLEDGED', + REDIRECTED: 'REDIRECTED', + REDIRECT_ACKNOWLEDGED: 'REDIRECT ACKNOWLEDGED', +}; + +DeliveryStatus.ALL_STATUSES = Object.values(DeliveryStatus); + +module.exports = Object.freeze(DeliveryStatus); diff --git a/src/constants/index.js b/src/constants/index.js new file mode 100644 index 00000000..f22046bb --- /dev/null +++ b/src/constants/index.js @@ -0,0 +1 @@ +exports.DeliveryStatus = require('./DeliveryStatus'); diff --git a/src/metadata/FieldMetadata.js b/src/metadata/FieldMetadata.js new file mode 100644 index 00000000..4900c009 --- /dev/null +++ b/src/metadata/FieldMetadata.js @@ -0,0 +1,31 @@ +const Suffixes = { + 'uuid': '_uuid', + 'enum': 'enum', +}; +const NonParamTypes = [ + 'uuid', + 'date' +]; + +function createColName({ name, type }){ + return name.toLowerCase() + (Suffixes[type] || ''); +} + +class FieldMetadata { + constructor(field) { + Object.assign( + this, + { + colName: createColName(field), + isParam: !NonParamTypes.includes(field.type), + defaultValue: field.defaultValue ?? + field.type === 'boolean' + ? false + : null + }, + field + ); + } +} + +module.exports = FieldMetadata; diff --git a/src/metadata/Metadata.js b/src/metadata/Metadata.js deleted file mode 100644 index 1d9bf07b..00000000 --- a/src/metadata/Metadata.js +++ /dev/null @@ -1,54 +0,0 @@ -const { DataTypes } = require('sequelize'); - -const Suffixes = { - [DataTypes.UUID]: '_uuid', - [DataTypes.ENUM]: 'enum', -}; - -function createColName({ name, colName, type }){ - if (colName) { - return colName; - } - - return name.toLowerCase() + (Suffixes[type] || ''); -} - -function createParams(fields) { - const params = []; - - fields.forEach(({ name, type }) => { - if (type !== DataTypes.UUID && type !== DataTypes.DATE) { - params.push(name); - } - }); - - return Object.freeze(params); -} - -class Metadata { - constructor(fields) - { - this.fields = Object.freeze(fields.map((field) => { - field.colName = createColName(field); - - return field; - })); - this.params = createParams(this.fields); - } - - getFieldHash() { - const hash = {}; - - this.fields.forEach(({ name, colName: field, ...rest }) => { - hash[name] = { field, ... rest }; - }); - - return hash; - } - - getParams() { - return this.params; - } -} - -module.exports = Metadata; diff --git a/src/metadata/ModelMetadata.js b/src/metadata/ModelMetadata.js new file mode 100644 index 00000000..70f37fda --- /dev/null +++ b/src/metadata/ModelMetadata.js @@ -0,0 +1,65 @@ +const FieldMetadata = require('./FieldMetadata'); + +const PropTypeLookup = { + text: 'string', + integer: 'number', + decimal: 'number', + boolean: 'bool', +}; + +const identity = (field) => [field.name, field]; + +function createParamsList(fields) { + const params = fields.filter(({ isParam }) => isParam).map(({ name }) => name); + + return Object.freeze(params); +} + +class ModelMetadata { + constructor(fields) { + this.fields = Object.freeze(fields.map((field) => new FieldMetadata(field))); + this.params = createParamsList(this.fields); + } + + getFieldHash(convertField = identity) { + const hash = {}; + + this.fields.forEach((field) => { + const [name, convertedField] = convertField(field); + + hash[name] = convertedField; + }); + + return hash; + } + + getParams() { + return this.params; + } + + getObjectFields() { + return this.fields.filter(({ type, isParam }) => isParam || type === 'enum'); + } + + getPropTypes(PropTypes) { + return this.getObjectFields().reduce((result, field) => { + const { name, type, enumValues, required } = field; + const reactType = PropTypeLookup[type]; + let propType = PropTypes[reactType]; + + if (type === 'enum') { + propType = PropTypes.oneOf(enumValues); + } + + if (required) { + propType = propType.isRequired; + } + + result[name] = propType; + + return result; + }, {}); + } +} + +module.exports = ModelMetadata; diff --git a/src/metadata/convertToSequelizeField.js b/src/metadata/convertToSequelizeField.js new file mode 100644 index 00000000..dc4ab370 --- /dev/null +++ b/src/metadata/convertToSequelizeField.js @@ -0,0 +1,32 @@ +const { DataTypes } = require('sequelize'); + +const SequelizeKeys = [ + 'allowNull', + 'primaryKey', + 'autoIncrement', + 'unique', +]; + +const pick = (obj, keys) => Object.fromEntries( + keys + .filter(key => key in obj) + .map(key => [key, obj[key]]) +); + +module.exports = function convertToSequelizeField(field) { + const { name, type: typeName, enumValues } = field; + + let type = DataTypes[typeName.toUpperCase()]; + + if (typeName === 'enum') { + type = DataTypes.ENUM(enumValues); + } + + const convertedField = { + field: field.colName, + type, + ...pick(field, SequelizeKeys) + }; + + return [name, convertedField]; +} diff --git a/src/metadata/patient.js b/src/metadata/patient.js index 5d3c126e..d7bbc002 100644 --- a/src/metadata/patient.js +++ b/src/metadata/patient.js @@ -1,128 +1,133 @@ -const { DataTypes } = require('sequelize'); -const Metadata = require('./Metadata'); +const ModelMetadata = require('./ModelMetadata'); const fields = [ { name: 'id', colName: 'patient_uuid', - type: DataTypes.UUID, + type: 'uuid', primaryKey: true, autoIncrement: true, }, { name: 'EmergencyMedicalServiceCallId', colName: 'emergencymedicalservicecall_uuid', - type: DataTypes.UUID, + type: 'uuid', allowNull: false, unique: true, }, { name: 'age', - type: DataTypes.INTEGER, + type: 'integer', + required: true, }, { name: 'sex', - type: DataTypes.ENUM('MALE', 'FEMALE', 'NON-BINARY'), + type: 'enum', + enumValues: ['MALE', 'FEMALE', 'NON-BINARY'], + required: true, }, { name: 'emergencyServiceResponseType', - type: DataTypes.ENUM('CODE 2', 'CODE 3'), + type: 'enum', + enumValues: ['CODE 2', 'CODE 3'], + required: true, }, { name: 'chiefComplaintDescription', - type: DataTypes.TEXT, + type: 'text', + required: true, }, { name: 'stableIndicator', - type: DataTypes.BOOLEAN, + type: 'boolean', + required: true, }, { name: 'systolicBloodPressure', - type: DataTypes.INTEGER, + type: 'integer', }, { name: 'diastolicBloodPressure', - type: DataTypes.INTEGER, + type: 'integer', }, { name: 'heartRateBpm', - type: DataTypes.INTEGER, + type: 'integer', }, { name: 'respiratoryRate', - type: DataTypes.INTEGER, + type: 'integer', }, { name: 'oxygenSaturation', - type: DataTypes.INTEGER, + type: 'integer', }, { name: 'lowOxygenResponseType', - type: DataTypes.ENUM('ROOM AIR', 'SUPPLEMENTAL OXYGEN'), + type: 'enum', + enumValues: ['ROOM AIR', 'SUPPLEMENTAL OXYGEN'], }, { name: 'supplementalOxygenAmount', - type: DataTypes.INTEGER, + type: 'integer', }, { name: 'temperature', - type: DataTypes.DECIMAL, + type: 'decimal', }, { name: 'etohSuspectedIndicator', - type: DataTypes.BOOLEAN, + type: 'boolean', }, { name: 'drugsSuspectedIndicator', - type: DataTypes.BOOLEAN, + type: 'boolean', }, { name: 'psychIndicator', - type: DataTypes.BOOLEAN, + type: 'boolean', }, { name: 'combativeBehaviorIndicator', - type: DataTypes.BOOLEAN, + type: 'boolean', }, { name: 'restraintIndicator', - type: DataTypes.BOOLEAN, + type: 'boolean', }, { name: 'covid19SuspectedIndicator', colName: 'covid-19suspectedindicator', - type: DataTypes.BOOLEAN, + type: 'boolean', }, { name: 'ivIndicator', - colName: 'ivindicator', - type: DataTypes.BOOLEAN, + type: 'boolean', }, { name: 'otherObservationNotes', - colName: 'otherobservationnotes', - type: DataTypes.TEXT, + type: 'text', }, { name: 'createdAt', colName: 'recordcreatetimestamp', - type: DataTypes.DATE, + type: 'date', }, { name: 'CreatedById', colName: 'recordcreateuser_uuid', - type: DataTypes.UUID, + type: 'uuid', }, { name: 'updatedAt', colName: 'recordupdatetimestamp', - type: DataTypes.DATE, + type: 'date', }, { name: 'UpdatedById', colName: 'recordupdateuser_uuid', - type: DataTypes.UUID, + type: 'uuid', }, ]; -module.exports = new Metadata(fields); +module.exports = new ModelMetadata(fields); diff --git a/test/integration/api/ringdowns.js b/test/integration/api/ringdowns.js index c3c8ab3a..8b2aad0d 100644 --- a/test/integration/api/ringdowns.js +++ b/test/integration/api/ringdowns.js @@ -5,7 +5,7 @@ const session = require('supertest-session'); const helper = require('../../helper'); const app = require('../../../app'); const models = require('../../../models'); -const { DeliveryStatus } = require('../../../constants'); +const { DeliveryStatus } = require('../../../src/constants'); describe('/api/ringdowns', () => { let testSession; diff --git a/test/unit/models/patientDelivery.js b/test/unit/models/patientDelivery.js index a35620b2..3875e3f3 100644 --- a/test/unit/models/patientDelivery.js +++ b/test/unit/models/patientDelivery.js @@ -1,7 +1,7 @@ const assert = require('assert'); const helper = require('../../helper'); const models = require('../../../models'); -const { DeliveryStatus } = require('../../../constants'); +const { DeliveryStatus } = require('../../../src/constants'); describe('models.PatientDelivery', () => { beforeEach(async () => { diff --git a/wss.js b/wss.js index 67f01588..e7febfdd 100644 --- a/wss.js +++ b/wss.js @@ -2,7 +2,7 @@ const querystring = require('querystring'); const { Op } = require('sequelize'); const url = require('url'); const WebSocket = require('ws'); -const DeliveryStatus = require('./constants/deliveryStatus'); +const DeliveryStatus = require('./src/constants/DeliveryStatus'); const models = require('./models'); const userServer = new WebSocket.Server({ noServer: true }); From c021cc63361a1e8b8dab429a08afa63fa85258cf Mon Sep 17 00:00:00 2001 From: John Dunning Date: Tue, 19 Jul 2022 00:29:50 -0700 Subject: [PATCH 04/33] Prettier nonsense --- src/metadata/FieldMetadata.js | 16 +++++----------- src/metadata/convertToSequelizeField.js | 19 +++++-------------- 2 files changed, 10 insertions(+), 25 deletions(-) diff --git a/src/metadata/FieldMetadata.js b/src/metadata/FieldMetadata.js index 4900c009..7fda747a 100644 --- a/src/metadata/FieldMetadata.js +++ b/src/metadata/FieldMetadata.js @@ -1,13 +1,10 @@ const Suffixes = { - 'uuid': '_uuid', - 'enum': 'enum', + uuid: '_uuid', + enum: 'enum', }; -const NonParamTypes = [ - 'uuid', - 'date' -]; +const NonParamTypes = ['uuid', 'date']; -function createColName({ name, type }){ +function createColName({ name, type }) { return name.toLowerCase() + (Suffixes[type] || ''); } @@ -18,10 +15,7 @@ class FieldMetadata { { colName: createColName(field), isParam: !NonParamTypes.includes(field.type), - defaultValue: field.defaultValue ?? - field.type === 'boolean' - ? false - : null + defaultValue: field.defaultValue ?? field.type === 'boolean' ? false : null, }, field ); diff --git a/src/metadata/convertToSequelizeField.js b/src/metadata/convertToSequelizeField.js index dc4ab370..cadf3ba0 100644 --- a/src/metadata/convertToSequelizeField.js +++ b/src/metadata/convertToSequelizeField.js @@ -1,20 +1,11 @@ const { DataTypes } = require('sequelize'); -const SequelizeKeys = [ - 'allowNull', - 'primaryKey', - 'autoIncrement', - 'unique', -]; +const SequelizeKeys = ['allowNull', 'primaryKey', 'autoIncrement', 'unique']; -const pick = (obj, keys) => Object.fromEntries( - keys - .filter(key => key in obj) - .map(key => [key, obj[key]]) -); +const pick = (obj, keys) => Object.fromEntries(keys.filter((key) => key in obj).map((key) => [key, obj[key]])); module.exports = function convertToSequelizeField(field) { - const { name, type: typeName, enumValues } = field; + const { name, type: typeName, enumValues } = field; let type = DataTypes[typeName.toUpperCase()]; @@ -25,8 +16,8 @@ module.exports = function convertToSequelizeField(field) { const convertedField = { field: field.colName, type, - ...pick(field, SequelizeKeys) + ...pick(field, SequelizeKeys), }; return [name, convertedField]; -} +}; From 13cacd1644088da813f1ed73c6b4a00eadcb3d79 Mon Sep 17 00:00:00 2001 From: John Dunning Date: Tue, 19 Jul 2022 18:59:23 -0700 Subject: [PATCH 05/33] Add new fields to patient metadata Add PatientFields and Ringdown changes from 169-ems-update-form branch. Fix default value for status indicator. --- src/EMS/PatientFields.js | 117 ++++++++++++-------------- src/Models/Ringdown.js | 3 +- src/metadata/patient.js | 11 +++ theme/_uswds-theme-custom-styles.scss | 5 ++ 4 files changed, 72 insertions(+), 64 deletions(-) diff --git a/src/EMS/PatientFields.js b/src/EMS/PatientFields.js index ba47d086..7744f756 100644 --- a/src/EMS/PatientFields.js +++ b/src/EMS/PatientFields.js @@ -14,18 +14,38 @@ import ApiService from '../ApiService'; import Context from '../Context'; const INPUT_RANGES = { + age: { min: 0, max: 130 }, systolicBloodPressure: { min: 90, max: 180 }, diastolicBloodPressure: { min: 60, max: 120 }, heartRateBpm: { min: 40, max: 200 }, respiratoryRate: { min: 12, max: 25 }, oxygenSaturation: { min: 0, max: 100 }, temperature: { min: 80, max: 150 }, + glasgowComaScale: { min: 3, max: 15 }, }; function getRange(property, extreme) { return INPUT_RANGES[property] ? INPUT_RANGES[property][extreme] : null; } +// use prop-spreading here because all we're doing is defaulting some props and letting the rest +// pass through to FormInput +function NumericField({ property, size = 'small', min = getRange(property, 'min'), max = getRange(property, 'max'), ...props }) { + return ( + + ); +} + +NumericField.propTypes = FormInput.propTypes; + function PatientFields({ ringdown, onChange }) { const [ambulanceIds, setAmbulanceIds] = useState([]); const [dispatchCallNumbers, setDispatchCallNumbers] = useState([]); @@ -91,16 +111,14 @@ function PatientFields({ ringdown, onChange }) {
-
- +
-    - - - - + + -
@@ -242,22 +226,18 @@ function PatientFields({ ringdown, onChange }) { />
- +
- +
+ - - + +
diff --git a/src/Models/Ringdown.js b/src/Models/Ringdown.js index f718efe6..2110fc6b 100644 --- a/src/Models/Ringdown.js +++ b/src/Models/Ringdown.js @@ -46,7 +46,8 @@ class Ringdown { this.payload.ambulance = this.payload.ambulance || {}; this.payload.emsCall = this.payload.emsCall || {}; this.payload.hospital = this.payload.hospital || {}; - this.payload.patient = this.payload.patient || {}; + // default the urgency to Code 2 for a new ringdown + this.payload.patient = this.payload.patient || { emergencyServiceResponseType: 'CODE 2' }; this.payload.patientDelivery = this.payload.patientDelivery || {}; // add getters/setters for patient fields diff --git a/src/metadata/patient.js b/src/metadata/patient.js index d7bbc002..d8583dfd 100644 --- a/src/metadata/patient.js +++ b/src/metadata/patient.js @@ -40,6 +40,9 @@ const fields = [ { name: 'stableIndicator', type: 'boolean', + // though this is stored as a boolean, it's rendered in the UI as two radio buttons, not a + // checkbox, so use null as the default so neither radio is selected + defaultValue: null, required: true, }, { @@ -75,6 +78,10 @@ const fields = [ name: 'temperature', type: 'decimal', }, + { + name: 'treatmentNotes', + type: 'text', + }, { name: 'etohSuspectedIndicator', type: 'boolean', @@ -104,6 +111,10 @@ const fields = [ name: 'ivIndicator', type: 'boolean', }, + { + name: 'glasgowComaScale', + type: 'integer', + }, { name: 'otherObservationNotes', type: 'text', diff --git a/theme/_uswds-theme-custom-styles.scss b/theme/_uswds-theme-custom-styles.scss index d21a454a..9e284417 100644 --- a/theme/_uswds-theme-custom-styles.scss +++ b/theme/_uswds-theme-custom-styles.scss @@ -203,6 +203,11 @@ $spinner-border-width-sm: 0.2em; @include u-margin-top(2); } +textarea + .usa-checkbox > .usa-checkbox__label, +textarea + .usa-radio > .usa-radio__label { + @include u-margin-top(4); +} + .usa-combo-box__clear-input { display: none !important; } From b2d558d1096830560376a58830e6fc978125f629 Mon Sep 17 00:00:00 2001 From: John Dunning Date: Wed, 20 Jul 2022 23:36:31 -0700 Subject: [PATCH 06/33] Add migration for new fields in patient database Pass table and model names to ModelMetadata constructor. --- ...721055200-add-gcs-treatments-to-patient.js | 33 +++++++++++++++++++ models/patient.js | 4 +-- src/metadata/ModelMetadata.js | 4 ++- src/metadata/patient.js | 2 +- 4 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 migrations/20220721055200-add-gcs-treatments-to-patient.js diff --git a/migrations/20220721055200-add-gcs-treatments-to-patient.js b/migrations/20220721055200-add-gcs-treatments-to-patient.js new file mode 100644 index 00000000..7d1e6eaf --- /dev/null +++ b/migrations/20220721055200-add-gcs-treatments-to-patient.js @@ -0,0 +1,33 @@ +const patient = require('../src/metadata/patient'); +const convertToSequelizeField = require('../src/metadata/convertToSequelizeField'); + +const fields = patient.getFieldHash(convertToSequelizeField); +const newFields = ['treatmentNotes', 'glasgowComaScale']; +const { tableName } = patient; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.sequelize.transaction(async (transaction) => { + for (const fieldName of newFields) { + const newField = fields[fieldName]; + + await queryInterface.addColumn( + tableName, + newField.field, + newField, + { transaction } + ); + } + }); + }, + + down: async (queryInterface) => { + await queryInterface.sequelize.transaction(async (transaction) => { + for (const fieldName of newFields) { + const newField = fields[fieldName]; + + await queryInterface.removeColumn(tableName, newField.field, { transaction }); + } + }); + }, +}; diff --git a/models/patient.js b/models/patient.js index e53f743f..4ef43e4c 100644 --- a/models/patient.js +++ b/models/patient.js @@ -19,8 +19,8 @@ module.exports = (sequelize) => { Patient.init(patient.getFieldHash(convertToSequelizeField), { sequelize, timestamps: true, - tableName: 'patient', - modelName: 'Patient', + tableName: patient.tableName, + modelName: patient.modelName, }); return Patient; }; diff --git a/src/metadata/ModelMetadata.js b/src/metadata/ModelMetadata.js index 70f37fda..bdf11cf4 100644 --- a/src/metadata/ModelMetadata.js +++ b/src/metadata/ModelMetadata.js @@ -16,7 +16,9 @@ function createParamsList(fields) { } class ModelMetadata { - constructor(fields) { + constructor({ modelName, tableName = modelName.toLowerCase(), fields }) { + this.modelName = modelName; + this.tableName = tableName; this.fields = Object.freeze(fields.map((field) => new FieldMetadata(field))); this.params = createParamsList(this.fields); } diff --git a/src/metadata/patient.js b/src/metadata/patient.js index d8583dfd..bd5518ff 100644 --- a/src/metadata/patient.js +++ b/src/metadata/patient.js @@ -141,4 +141,4 @@ const fields = [ }, ]; -module.exports = new ModelMetadata(fields); +module.exports = new ModelMetadata({ modelName: 'Patient', fields }); From 185fb35243574fb380f718f05ea5514de8e8ec6a Mon Sep 17 00:00:00 2001 From: John Dunning Date: Thu, 21 Jul 2022 12:40:06 -0700 Subject: [PATCH 07/33] Tweak names in migration Pass the type instead of the whole field object. Prettier and eslint nonsense. --- ...721055200-add-gcs-treatments-to-patient.js | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/migrations/20220721055200-add-gcs-treatments-to-patient.js b/migrations/20220721055200-add-gcs-treatments-to-patient.js index 7d1e6eaf..050ae27b 100644 --- a/migrations/20220721055200-add-gcs-treatments-to-patient.js +++ b/migrations/20220721055200-add-gcs-treatments-to-patient.js @@ -1,32 +1,28 @@ +/* eslint-disable no-await-in-loop, no-restricted-syntax */ const patient = require('../src/metadata/patient'); const convertToSequelizeField = require('../src/metadata/convertToSequelizeField'); const fields = patient.getFieldHash(convertToSequelizeField); -const newFields = ['treatmentNotes', 'glasgowComaScale']; +const newFieldNames = ['treatmentNotes', 'glasgowComaScale']; const { tableName } = patient; module.exports = { - up: async (queryInterface, Sequelize) => { + async up(queryInterface, Sequelize) { await queryInterface.sequelize.transaction(async (transaction) => { - for (const fieldName of newFields) { - const newField = fields[fieldName]; + for (const name of newFieldNames) { + const { field, type } = fields[name]; - await queryInterface.addColumn( - tableName, - newField.field, - newField, - { transaction } - ); + await queryInterface.addColumn(tableName, field, type, { transaction }); } }); }, - down: async (queryInterface) => { + async down(queryInterface) { await queryInterface.sequelize.transaction(async (transaction) => { - for (const fieldName of newFields) { - const newField = fields[fieldName]; + for (const name of newFieldNames) { + const { field } = fields[name]; - await queryInterface.removeColumn(tableName, newField.field, { transaction }); + await queryInterface.removeColumn(tableName, field, { transaction }); } }); }, From b4b39ad5c27f0db31a3749a6f2234731dd9542ac Mon Sep 17 00:00:00 2001 From: John Dunning Date: Thu, 21 Jul 2022 13:26:31 -0700 Subject: [PATCH 08/33] Refactor ModelMetadata Use getFieldHash() for getting the prop types as well, passing in convertToPropType. --- src/Models/Ringdown.js | 3 +- src/metadata/ModelMetadata.js | 47 +++++-------------------------- src/metadata/convertToPropType.js | 32 +++++++++++++++++++++ 3 files changed, 41 insertions(+), 41 deletions(-) create mode 100644 src/metadata/convertToPropType.js diff --git a/src/Models/Ringdown.js b/src/Models/Ringdown.js index 2110fc6b..fecb8165 100644 --- a/src/Models/Ringdown.js +++ b/src/Models/Ringdown.js @@ -2,6 +2,7 @@ import { DateTime } from 'luxon'; import PropTypes from 'prop-types'; import { PatientFieldData, ValidationState } from './PatientFieldData'; import patient from '../metadata/patient'; +import convertToPropType from '../metadata/convertToPropType'; import DeliveryStatus from '../constants/DeliveryStatus'; function attachFields(target, fields, data) { @@ -296,7 +297,7 @@ Ringdown.propTypes = { dispatchCallNumber: PropTypes.number.isRequired, hospitalId: PropTypes.string.isRequired, // Patient Info - ...patient.getPropTypes(PropTypes), + ...patient.getFieldHash(convertToPropType), // Status etaMinutes: PropTypes.number.isRequired, currentDeliveryStatus: PropTypes.oneOf(DeliveryStatus.ALL_STATUSES), diff --git a/src/metadata/ModelMetadata.js b/src/metadata/ModelMetadata.js index bdf11cf4..d1a002da 100644 --- a/src/metadata/ModelMetadata.js +++ b/src/metadata/ModelMetadata.js @@ -1,38 +1,25 @@ const FieldMetadata = require('./FieldMetadata'); -const PropTypeLookup = { - text: 'string', - integer: 'number', - decimal: 'number', - boolean: 'bool', -}; - const identity = (field) => [field.name, field]; -function createParamsList(fields) { - const params = fields.filter(({ isParam }) => isParam).map(({ name }) => name); - - return Object.freeze(params); -} - class ModelMetadata { constructor({ modelName, tableName = modelName.toLowerCase(), fields }) { this.modelName = modelName; this.tableName = tableName; this.fields = Object.freeze(fields.map((field) => new FieldMetadata(field))); - this.params = createParamsList(this.fields); + this.params = Object.freeze(this.fields.filter(({ isParam }) => isParam).map(({ name }) => name)); } getFieldHash(convertField = identity) { - const hash = {}; - - this.fields.forEach((field) => { + return this.fields.reduce((result, field) => { const [name, convertedField] = convertField(field); - hash[name] = convertedField; - }); + if (name && convertedField) { + result[name] = convertedField; + } - return hash; + return result; + }, {}); } getParams() { @@ -42,26 +29,6 @@ class ModelMetadata { getObjectFields() { return this.fields.filter(({ type, isParam }) => isParam || type === 'enum'); } - - getPropTypes(PropTypes) { - return this.getObjectFields().reduce((result, field) => { - const { name, type, enumValues, required } = field; - const reactType = PropTypeLookup[type]; - let propType = PropTypes[reactType]; - - if (type === 'enum') { - propType = PropTypes.oneOf(enumValues); - } - - if (required) { - propType = propType.isRequired; - } - - result[name] = propType; - - return result; - }, {}); - } } module.exports = ModelMetadata; diff --git a/src/metadata/convertToPropType.js b/src/metadata/convertToPropType.js new file mode 100644 index 00000000..e7669325 --- /dev/null +++ b/src/metadata/convertToPropType.js @@ -0,0 +1,32 @@ +const PropTypes = require('prop-types'); + +const PropTypeLookup = { + text: 'string', + integer: 'number', + decimal: 'number', + boolean: 'bool', +}; + +module.exports = function convertToPropType(field) { + const { name, type, isParam, enumValues, required } = field; + // if the field isn't something we want to convert to a propType, then return an empty array so + // getFieldHash() won't include this field + let result = []; + + if (isParam || type === 'enum') { + const reactType = PropTypeLookup[type]; + let propType = PropTypes[reactType]; + + if (type === 'enum') { + propType = PropTypes.oneOf(enumValues); + } + + if (required) { + propType = propType.isRequired; + } + + result = [name, propType]; + } + + return result; +}; From ce55e042ad5498c53732df48e84550d702e201dc Mon Sep 17 00:00:00 2001 From: John Dunning Date: Thu, 21 Jul 2022 18:15:57 -0700 Subject: [PATCH 09/33] Fix server tests to handle new fields --- test/integration/api/ringdowns.js | 4 ++++ test/unit/models/patientDelivery.js | 2 ++ 2 files changed, 6 insertions(+) diff --git a/test/integration/api/ringdowns.js b/test/integration/api/ringdowns.js index 8b2aad0d..23736bf6 100644 --- a/test/integration/api/ringdowns.js +++ b/test/integration/api/ringdowns.js @@ -82,12 +82,14 @@ describe('/api/ringdowns', () => { lowOxygenResponseType: 'SUPPLEMENTAL OXYGEN', supplementalOxygenAmount: 2, temperature: 99.4, + treatmentNotes: 'Gave px lollipop', etohSuspectedIndicator: false, drugsSuspectedIndicator: true, psychIndicator: false, combativeBehaviorIndicator: false, restraintIndicator: false, covid19SuspectedIndicator: true, + glasgowComaScale: 3, ivIndicator: false, otherObservationNotes: 'Needs assistance walking', }; @@ -407,6 +409,7 @@ describe('/api/ringdowns', () => { lowOxygenResponseType: null, supplementalOxygenAmount: null, temperature: null, + treatmentNotes: null, etohSuspectedIndicator: null, drugsSuspectedIndicator: null, psychIndicator: null, @@ -414,6 +417,7 @@ describe('/api/ringdowns', () => { restraintIndicator: null, covid19SuspectedIndicator: null, ivIndicator: null, + glasgowComaScale: null, otherObservationNotes: null, }, patientDelivery: { diff --git a/test/unit/models/patientDelivery.js b/test/unit/models/patientDelivery.js index 3875e3f3..1dbde85f 100644 --- a/test/unit/models/patientDelivery.js +++ b/test/unit/models/patientDelivery.js @@ -204,6 +204,7 @@ describe('models.PatientDelivery', () => { lowOxygenResponseType: null, supplementalOxygenAmount: null, temperature: null, + treatmentNotes: null, etohSuspectedIndicator: null, drugsSuspectedIndicator: null, psychIndicator: null, @@ -211,6 +212,7 @@ describe('models.PatientDelivery', () => { restraintIndicator: null, covid19SuspectedIndicator: null, ivIndicator: null, + glasgowComaScale: null, otherObservationNotes: null, }, patientDelivery: { From f1a4eee5272cfb51a2f0e108bc19803294ccdee3 Mon Sep 17 00:00:00 2001 From: John Dunning Date: Sun, 31 Jul 2022 18:21:09 -0700 Subject: [PATCH 10/33] Add ranges to fields in metadata/patient.js --- src/EMS/PatientFields.js | 16 +++++----------- src/metadata/patient.js | 8 ++++++++ 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/EMS/PatientFields.js b/src/EMS/PatientFields.js index 7744f756..d926f3ea 100644 --- a/src/EMS/PatientFields.js +++ b/src/EMS/PatientFields.js @@ -12,20 +12,14 @@ import Heading from '../Components/Heading'; import Ringdown from '../Models/Ringdown'; import ApiService from '../ApiService'; import Context from '../Context'; +import patient from '../metadata/patient'; -const INPUT_RANGES = { - age: { min: 0, max: 130 }, - systolicBloodPressure: { min: 90, max: 180 }, - diastolicBloodPressure: { min: 60, max: 120 }, - heartRateBpm: { min: 40, max: 200 }, - respiratoryRate: { min: 12, max: 25 }, - oxygenSaturation: { min: 0, max: 100 }, - temperature: { min: 80, max: 150 }, - glasgowComaScale: { min: 3, max: 15 }, -}; +const FIELDs = patient.getFieldHash(); function getRange(property, extreme) { - return INPUT_RANGES[property] ? INPUT_RANGES[property][extreme] : null; + const { range } = FIELDs[property] || {}; + + return range ? range[extreme] : null; } // use prop-spreading here because all we're doing is defaulting some props and letting the rest diff --git a/src/metadata/patient.js b/src/metadata/patient.js index bd5518ff..28621136 100644 --- a/src/metadata/patient.js +++ b/src/metadata/patient.js @@ -19,6 +19,7 @@ const fields = [ name: 'age', type: 'integer', required: true, + range: { min: 0, max: 130 }, }, { name: 'sex', @@ -48,22 +49,27 @@ const fields = [ { name: 'systolicBloodPressure', type: 'integer', + range: { min: 90, max: 180 }, }, { name: 'diastolicBloodPressure', type: 'integer', + range: { min: 60, max: 120 }, }, { name: 'heartRateBpm', type: 'integer', + range: { min: 40, max: 200 }, }, { name: 'respiratoryRate', type: 'integer', + range: { min: 12, max: 25 }, }, { name: 'oxygenSaturation', type: 'integer', + range: { min: 0, max: 100 }, }, { name: 'lowOxygenResponseType', @@ -77,6 +83,7 @@ const fields = [ { name: 'temperature', type: 'decimal', + range: { min: 80, max: 150 }, }, { name: 'treatmentNotes', @@ -114,6 +121,7 @@ const fields = [ { name: 'glasgowComaScale', type: 'integer', + range: { min: 3, max: 15 }, }, { name: 'otherObservationNotes', From ea894db0dfe9e2bbfb9f709ebbf8adcdaaff2b29 Mon Sep 17 00:00:00 2001 From: John Dunning Date: Mon, 1 Aug 2022 13:37:48 -0700 Subject: [PATCH 11/33] Create Form component to provide context to fields Provide ringdown data and onChange handler to form fields via the Form context. Create Field component to generate checkboxes, numeric inputs, and text areas based on the metadata type. Add label and unit strings to the patient metadata. Tweak restraint label. --- src/Components/Form.js | 31 +++++ src/Components/RingdownDetails.js | 2 +- src/EMS/PatientFields.js | 209 +++++++++++++----------------- src/Models/Ringdown.js | 2 +- src/metadata/patient.js | 23 ++++ 5 files changed, 149 insertions(+), 118 deletions(-) create mode 100644 src/Components/Form.js diff --git a/src/Components/Form.js b/src/Components/Form.js new file mode 100644 index 00000000..9b0a939c --- /dev/null +++ b/src/Components/Form.js @@ -0,0 +1,31 @@ +import React, { useMemo, useContext, createContext } from 'react'; + +const FormContext = createContext(undefined); + +const Form = ({ data, onChange, children }) => { + const context = useMemo(() => ({ + data, + onChange + }), [data, onChange]); + + return ( + + {children} + + ); +}; + +const useForm = () => { + const context = useContext(FormContext); + + if (!context) { + throw new Error('useForm() must be used within a Form.'); + } + + return context; +}; + +export { + Form, + useForm, +}; diff --git a/src/Components/RingdownDetails.js b/src/Components/RingdownDetails.js index b899ae97..17526875 100644 --- a/src/Components/RingdownDetails.js +++ b/src/Components/RingdownDetails.js @@ -128,7 +128,7 @@ function RingdownDetails({ className, ringdown, isIncoming }) { Combative {ringdown.combativeBehaviorIndicator && 'Yes'} - {ringdown.restraintIndicator && <> Restrained} + {ringdown.restraintIndicator && ', Restrained'} )} diff --git a/src/EMS/PatientFields.js b/src/EMS/PatientFields.js index d926f3ea..9cb2f6bf 100644 --- a/src/EMS/PatientFields.js +++ b/src/EMS/PatientFields.js @@ -1,6 +1,7 @@ import React, { useContext, useState, useEffect } from 'react'; import PropTypes from 'prop-types'; +import { Form, useForm } from '../Components/Form'; import FormCheckbox from '../Components/FormCheckbox'; import FormComboBox from '../Components/FormComboBox'; import FormInput from '../Components/FormInput'; @@ -14,10 +15,10 @@ import ApiService from '../ApiService'; import Context from '../Context'; import patient from '../metadata/patient'; -const FIELDs = patient.getFieldHash(); +const Patient = patient.getFieldHash(); function getRange(property, extreme) { - const { range } = FIELDs[property] || {}; + const { range } = Patient[property] || {}; return range ? range[extreme] : null; } @@ -25,13 +26,18 @@ function getRange(property, extreme) { // use prop-spreading here because all we're doing is defaulting some props and letting the rest // pass through to FormInput function NumericField({ property, size = 'small', min = getRange(property, 'min'), max = getRange(property, 'max'), ...props }) { + const { data, onChange } = useForm(); + return ( @@ -40,6 +46,65 @@ function NumericField({ property, size = 'small', min = getRange(property, 'min' NumericField.propTypes = FormInput.propTypes; +/* eslint-disable react/jsx-props-no-spreading */ +function Field({ metadata, ...props }) { + const { data, onChange } = useForm(); + const { name: property, type, label, unit, required } = metadata; + const value = data[property]; + const commonProps = { + property, + label, + value, + required, + onChange, + validationState: data.getValidationState(property) + }; + + switch (type) { + case 'integer': + case 'decimal': + return ( + + ); + + case 'boolean': + return ( + + ); + + case 'text': + return ( + + ); + + default: + throw new Error(`Unknown field type: ${type}`); + } +} + +function createOptions(ids) { + return ids.map((id) => ( + + )); +} + function PatientFields({ ringdown, onChange }) { const [ambulanceIds, setAmbulanceIds] = useState([]); const [dispatchCallNumbers, setDispatchCallNumbers] = useState([]); @@ -58,24 +123,12 @@ function PatientFields({ ringdown, onChange }) { } }, [ringdown.ambulanceIdentifier, user]); - function createOptions(ids) { - const options = []; - ids.forEach((id) => - options.push( - - ) - ); - return options; - } - function handleUserInput(updatedField, inputValue) { onChange(updatedField, inputValue); } return ( - <> +
@@ -105,15 +158,7 @@ function PatientFields({ ringdown, onChange }) {
- +
- +
Exclude identifying information.
-
+
-    - - - - + + + +
O2
-
} @@ -220,74 +244,27 @@ function PatientFields({ ringdown, onChange }) { />
- +
- - - - - + + + + +
- +
- - - + + +
- + ); } diff --git a/src/Models/Ringdown.js b/src/Models/Ringdown.js index fecb8165..a4951f42 100644 --- a/src/Models/Ringdown.js +++ b/src/Models/Ringdown.js @@ -288,7 +288,7 @@ class Ringdown { } getValidationState(fieldName) { - return this.validationData[fieldName].validationState; + return this.validationData[fieldName]?.validationState; } } diff --git a/src/metadata/patient.js b/src/metadata/patient.js index 28621136..8cc38f48 100644 --- a/src/metadata/patient.js +++ b/src/metadata/patient.js @@ -18,6 +18,8 @@ const fields = [ { name: 'age', type: 'integer', + label: 'Age (estimated)', + unit: 'years', required: true, range: { min: 0, max: 130 }, }, @@ -36,6 +38,7 @@ const fields = [ { name: 'chiefComplaintDescription', type: 'text', + label: 'Chief Complaint', required: true, }, { @@ -59,16 +62,22 @@ const fields = [ { name: 'heartRateBpm', type: 'integer', + label: 'Pulse', + unit: 'beats/min', range: { min: 40, max: 200 }, }, { name: 'respiratoryRate', type: 'integer', + label: 'Respiratory Rate', + unit: 'breaths/min', range: { min: 12, max: 25 }, }, { name: 'oxygenSaturation', type: 'integer', + label: 'SpO2', + unit: '%', range: { min: 0, max: 100 }, }, { @@ -79,40 +88,51 @@ const fields = [ { name: 'supplementalOxygenAmount', type: 'integer', + unit: 'L', + range: { min: 1, max: 1000 }, }, { name: 'temperature', type: 'decimal', + label: 'Temperature', + unit: '°F', range: { min: 80, max: 150 }, }, { name: 'treatmentNotes', type: 'text', + label: 'Treatments Administered' }, { name: 'etohSuspectedIndicator', type: 'boolean', + label: 'ETOH suspected', }, { name: 'drugsSuspectedIndicator', type: 'boolean', + label: 'Drugs suspected', }, { name: 'psychIndicator', type: 'boolean', + label: 'Behavioral health needs', }, { name: 'combativeBehaviorIndicator', type: 'boolean', + label: 'Combative', }, { name: 'restraintIndicator', type: 'boolean', + label: '4-point restraint', }, { name: 'covid19SuspectedIndicator', colName: 'covid-19suspectedindicator', type: 'boolean', + label: 'COVID-19 suspected', }, { name: 'ivIndicator', @@ -121,11 +141,14 @@ const fields = [ { name: 'glasgowComaScale', type: 'integer', + label: 'GCS', + unit: '/ 15', range: { min: 3, max: 15 }, }, { name: 'otherObservationNotes', type: 'text', + label: 'Other', }, { name: 'createdAt', From ae71c0214577bfc8da033149bb5074efa8a79bae Mon Sep 17 00:00:00 2001 From: John Dunning Date: Mon, 1 Aug 2022 14:21:26 -0700 Subject: [PATCH 12/33] Move Field component into FormField.js Add form to wrap children and receive props in Form component. Remove unnecessary fragment wrapper on PatientFields. Fix degree symbol on temp. --- src/Components/Form.js | 12 +- src/Components/FormField.js | 66 +++++++++++ src/EMS/PatientFields.js | 213 ++++++++++++------------------------ src/EMS/RingdownForm.js | 9 +- src/metadata/patient.js | 2 +- 5 files changed, 150 insertions(+), 152 deletions(-) create mode 100644 src/Components/FormField.js diff --git a/src/Components/Form.js b/src/Components/Form.js index 9b0a939c..5b529e09 100644 --- a/src/Components/Form.js +++ b/src/Components/Form.js @@ -2,15 +2,20 @@ import React, { useMemo, useContext, createContext } from 'react'; const FormContext = createContext(undefined); -const Form = ({ data, onChange, children }) => { +const Form = ({ data, onChange, children, ...props }) => { const context = useMemo(() => ({ data, onChange }), [data, onChange]); + // we spread the extra props on the form so the caller can apply classes and other properties to + // the form element return ( - {children} + {/* eslint-disable-next-line react/jsx-props-no-spreading */} +
+ {children} +
); }; @@ -25,7 +30,8 @@ const useForm = () => { return context; }; +export default Form; + export { - Form, useForm, }; diff --git a/src/Components/FormField.js b/src/Components/FormField.js new file mode 100644 index 00000000..81579f0b --- /dev/null +++ b/src/Components/FormField.js @@ -0,0 +1,66 @@ +/* eslint-disable react/jsx-props-no-spreading, react/forbid-prop-types */ +import React from 'react'; +import PropTypes from 'prop-types'; +import FormCheckbox from './FormCheckbox'; +import FormInput from './FormInput'; +import FormTextArea from './FormTextArea'; +import { useForm } from './Form'; + +export default function FormField({ metadata, ...props }) { + const { data, onChange } = useForm(); + const { name: property, type, label, required } = metadata; + const value = data[property]; + const commonProps = { + property, + label, + value, + required, + onChange, + validationState: data.getValidationState(property) + }; + + switch (type) { + case 'integer': + case 'decimal': { + const { unit = metadata.unit, range = metadata.range, size = 'small' } = props; + const { min, max } = range || {}; + + return ( + + ); + } + + case 'boolean': + return ( + + ); + + case 'text': + return ( + + ); + + default: + throw new Error(`Unknown field type: ${type}`); + } +} + +FormField.propTypes = { + metadata: PropTypes.object.isRequired, +}; diff --git a/src/EMS/PatientFields.js b/src/EMS/PatientFields.js index 9cb2f6bf..400ba621 100644 --- a/src/EMS/PatientFields.js +++ b/src/EMS/PatientFields.js @@ -1,13 +1,11 @@ import React, { useContext, useState, useEffect } from 'react'; import PropTypes from 'prop-types'; -import { Form, useForm } from '../Components/Form'; -import FormCheckbox from '../Components/FormCheckbox'; +import Form from '../Components/Form'; import FormComboBox from '../Components/FormComboBox'; -import FormInput from '../Components/FormInput'; import FormRadio from '../Components/FormRadio'; import FormRadioFieldSet from '../Components/FormRadioFieldSet'; -import FormTextArea from '../Components/FormTextArea'; +import FormField from '../Components/FormField'; import Heading from '../Components/Heading'; import Ringdown from '../Models/Ringdown'; @@ -17,83 +15,6 @@ import patient from '../metadata/patient'; const Patient = patient.getFieldHash(); -function getRange(property, extreme) { - const { range } = Patient[property] || {}; - - return range ? range[extreme] : null; -} - -// use prop-spreading here because all we're doing is defaulting some props and letting the rest -// pass through to FormInput -function NumericField({ property, size = 'small', min = getRange(property, 'min'), max = getRange(property, 'max'), ...props }) { - const { data, onChange } = useForm(); - - return ( - - ); -} - -NumericField.propTypes = FormInput.propTypes; - -/* eslint-disable react/jsx-props-no-spreading */ -function Field({ metadata, ...props }) { - const { data, onChange } = useForm(); - const { name: property, type, label, unit, required } = metadata; - const value = data[property]; - const commonProps = { - property, - label, - value, - required, - onChange, - validationState: data.getValidationState(property) - }; - - switch (type) { - case 'integer': - case 'decimal': - return ( - - ); - - case 'boolean': - return ( - - ); - - case 'text': - return ( - - ); - - default: - throw new Error(`Unknown field type: ${type}`); - } -} - function createOptions(ids) { return ids.map((id) => (