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/migrations/20220721055200-add-gcs-treatments-to-patient.js b/migrations/20220721055200-add-gcs-treatments-to-patient.js new file mode 100644 index 00000000..6b80705b --- /dev/null +++ b/migrations/20220721055200-add-gcs-treatments-to-patient.js @@ -0,0 +1,15 @@ +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.sequelize.transaction(async (transaction) => { + await queryInterface.addColumn('patient', 'treatmentnotes', Sequelize.TEXT, { transaction }); + await queryInterface.addColumn('patient', 'glasgowcomascale', Sequelize.INTEGER, { transaction }); + }); + }, + + async down(queryInterface) { + await queryInterface.sequelize.transaction(async (transaction) => { + await queryInterface.removeColumn('patient', 'glasgowcomascale', { transaction }); + await queryInterface.removeColumn('patient', 'treatmentnotes', { transaction }); + }); + }, +}; diff --git a/models/ambulance.js b/models/ambulance.js index 49e1db79..329d98a1 100644 --- a/models/ambulance.js +++ b/models/ambulance.js @@ -1,6 +1,8 @@ const { Model } = require('sequelize'); +const metadata = require('../src/shared/metadata/ambulance'); +const convertToSequelizeField = require('../src/shared/convertToSequelizeField'); -module.exports = (sequelize, DataTypes) => { +module.exports = (sequelize) => { class Ambulance extends Model { static associate(models) { Ambulance.belongsTo(models.Organization); @@ -14,53 +16,11 @@ module.exports = (sequelize, DataTypes) => { } } - Ambulance.init( - { - id: { - field: 'ambulance_uuid', - type: DataTypes.UUID, - primaryKey: true, - autoIncrement: true, - }, - OrganizationId: { - field: 'emsorganization_uuid', - type: DataTypes.UUID, - }, - ambulanceIdentifier: { - field: 'ambulanceidentifier', - type: DataTypes.STRING, - unique: true, - allowNull: false, - }, - isActive: { - field: 'activeindicator', - type: DataTypes.BOOLEAN, - allowNull: false, - defaultValue: 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, - }, - }, - { - sequelize, - timestamps: true, - tableName: 'ambulance', - modelName: 'Ambulance', - } - ); + Ambulance.init(metadata.getFieldHash(convertToSequelizeField), { + sequelize, + timestamps: true, + tableName: metadata.tableName, + modelName: metadata.modelName, + }); return Ambulance; }; diff --git a/models/emergencyMedicalServiceCall.js b/models/emergencyMedicalServiceCall.js index c30e9f59..5c3c0ecc 100644 --- a/models/emergencyMedicalServiceCall.js +++ b/models/emergencyMedicalServiceCall.js @@ -1,6 +1,8 @@ const { Model } = require('sequelize'); +const metadata = require('../src/shared/metadata/emergencyMedicalServiceCall'); +const convertToSequelizeField = require('../src/shared/convertToSequelizeField'); -module.exports = (sequelize, DataTypes) => { +module.exports = (sequelize) => { class EmergencyMedicalServiceCall extends Model { /** * Helper method for defining associations. @@ -17,47 +19,11 @@ module.exports = (sequelize, DataTypes) => { EmergencyMedicalServiceCall.belongsTo(models.User, { as: 'UpdatedBy' }); } } - EmergencyMedicalServiceCall.init( - { - id: { - field: 'emergencymedicalservicecall_uuid', - type: DataTypes.UUID, - primaryKey: true, - autoIncrement: true, - }, - dispatchCallNumber: { - field: 'dispatchcallnumber', - type: DataTypes.INTEGER, - allowNull: false, - }, - startDateTimeLocal: { - field: 'startdatetimelocal', - type: DataTypes.DATE, - allowNull: false, - }, - 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, - }, - }, - { - sequelize, - timestamps: true, - tableName: 'emergencymedicalservicecall', - modelName: 'EmergencyMedicalServiceCall', - } - ); + EmergencyMedicalServiceCall.init(metadata.getFieldHash(convertToSequelizeField), { + sequelize, + timestamps: true, + tableName: metadata.tableName, + modelName: metadata.modelName, + }); return EmergencyMedicalServiceCall; }; diff --git a/models/emergencyMedicalServiceCallAmbulance.js b/models/emergencyMedicalServiceCallAmbulance.js index e07021ef..c778df3e 100644 --- a/models/emergencyMedicalServiceCallAmbulance.js +++ b/models/emergencyMedicalServiceCallAmbulance.js @@ -1,6 +1,8 @@ const { Model } = require('sequelize'); +const metadata = require('../src/shared/metadata/emergencyMedicalServiceCallAmbulance'); +const convertToSequelizeField = require('../src/shared/convertToSequelizeField'); -module.exports = (sequelize, DataTypes) => { +module.exports = (sequelize) => { class EmergencyMedicalServiceCallAmbulance extends Model { /** * Helper method for defining associations. @@ -15,50 +17,11 @@ module.exports = (sequelize, DataTypes) => { EmergencyMedicalServiceCallAmbulance.belongsTo(models.User, { as: 'UpdatedBy' }); } } - EmergencyMedicalServiceCallAmbulance.init( - { - id: { - field: 'emergencymedicalservicecallambulance_uuid', - type: DataTypes.UUID, - primaryKey: true, - autoIncrement: true, - }, - EmergencyMedicalServiceCallId: { - field: 'emergencymedicalservicecall_uuid', - type: DataTypes.UUID, - }, - AmbulanceId: { - field: 'ambulance_uuid', - type: DataTypes.UUID, - }, - startDateTimeLocal: { - field: 'startdatetimelocal', - type: DataTypes.DATE, - allowNull: false, - }, - 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, - }, - }, - { - sequelize, - timestamps: true, - tableName: 'emergencymedicalservicecallambulance', - modelName: 'EmergencyMedicalServiceCallAmbulance', - } - ); + EmergencyMedicalServiceCallAmbulance.init(metadata.getFieldHash(convertToSequelizeField), { + sequelize, + timestamps: true, + tableName: metadata.tableName, + modelName: metadata.modelName, + }); return EmergencyMedicalServiceCallAmbulance; }; diff --git a/models/hospital.js b/models/hospital.js index f03f97d3..52ad9597 100644 --- a/models/hospital.js +++ b/models/hospital.js @@ -1,8 +1,10 @@ const _ = require('lodash'); const { Model } = require('sequelize'); -const { DeliveryStatus } = require('../constants'); +const { DeliveryStatus } = require('../src/shared/constants'); +const metadata = require('../src/shared/metadata/hospital'); +const convertToSequelizeField = require('../src/shared/convertToSequelizeField'); -module.exports = (sequelize, DataTypes) => { +module.exports = (sequelize) => { class Hospital extends Model { static associate(models) { Hospital.belongsTo(models.Organization); @@ -48,66 +50,11 @@ module.exports = (sequelize, DataTypes) => { } } - Hospital.init( - { - // this is the value used to determine who each nurse belongs to - id: { - field: 'hospital_uuid', - type: DataTypes.UUID, - primaryKey: true, - autoIncrement: true, - }, - OrganizationId: { - field: 'healthcareorganization_uuid', - type: DataTypes.UUID, - }, - name: { - field: 'hospitalname', - type: DataTypes.STRING, - unique: true, - allowNull: false, - }, - state: { - field: 'hospitalstate', - type: DataTypes.STRING, - }, - stateFacilityCode: { - field: 'hospitalstatefacilitycode', - type: DataTypes.STRING, - }, - sortSequenceNumber: { - field: 'sortsequencenumber', - type: DataTypes.INTEGER, - }, - isActive: { - field: 'activeindicator', - type: DataTypes.BOOLEAN, - allowNull: false, - defaultValue: 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, - }, - }, - { - sequelize, - timestamps: true, - tableName: 'hospital', - modelName: 'Hospital', - } - ); + Hospital.init(metadata.getFieldHash(convertToSequelizeField), { + sequelize, + timestamps: true, + tableName: metadata.tableName, + modelName: metadata.modelName, + }); return Hospital; }; diff --git a/models/hospitalStatusUpdate.js b/models/hospitalStatusUpdate.js index 0a5685d3..c8ffaae2 100644 --- a/models/hospitalStatusUpdate.js +++ b/models/hospitalStatusUpdate.js @@ -1,8 +1,10 @@ const _ = require('lodash'); const { Model, Op } = require('sequelize'); -const { DeliveryStatus } = require('../constants'); +const { DeliveryStatus } = require('../src/shared/constants'); +const metadata = require('../src/shared/metadata/hospitalStatusUpdate'); +const convertToSequelizeField = require('../src/shared/convertToSequelizeField'); -module.exports = (sequelize, DataTypes) => { +module.exports = (sequelize) => { class HospitalStatusUpdate extends Model { static associate(models) { HospitalStatusUpdate.belongsTo(models.Hospital); @@ -78,88 +80,12 @@ module.exports = (sequelize, DataTypes) => { return json; } } - HospitalStatusUpdate.init( - { - id: { - field: 'hospitalstatusupdate_uuid', - type: DataTypes.UUID, - primaryKey: true, - autoIncrement: true, - }, - HospitalId: { - field: 'hospital_uuid', - type: DataTypes.UUID, - unique: true, - allowNull: false, - }, - EdAdminUserId: { - field: 'edadminuser_uuid', - type: DataTypes.UUID, - unique: true, - allowNull: false, - }, - updateDateTimeLocal: { - field: 'updatedatetimelocal', - type: DataTypes.DATE, - unique: true, - allowNull: false, - }, - openEdBedCount: { - field: 'openedbedcount', - type: DataTypes.INTEGER, - allowNull: false, - }, - openPsychBedCount: { - field: 'openpsychbedcount', - type: DataTypes.INTEGER, - allowNull: false, - }, - bedCountUpdateDateTimeLocal: { - field: 'bedcountupdatedatetimelocal', - type: DataTypes.DATE, - }, - divertStatusIndicator: { - field: 'divertstatusindicator', - type: DataTypes.BOOLEAN, - allowNull: false, - }, - divertStatusUpdateDateTimeLocal: { - field: 'divertstatusupdatedatetimelocal', - type: DataTypes.DATE, - }, - additionalServiceAvailabilityNotes: { - field: 'additionalserviceavailabilitynotes', - type: DataTypes.TEXT, - allowNull: true, - }, - notesUpdateDateTimeLocal: { - field: 'notesupdatedatetimelocal', - type: DataTypes.DATE, - }, - 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, - }, - }, - { - sequelize, - timestamps: true, - tableName: 'hospitalstatusupdate', - modelName: 'HospitalStatusUpdate', - } - ); + HospitalStatusUpdate.init(metadata.getFieldHash(convertToSequelizeField), { + sequelize, + timestamps: true, + tableName: metadata.tableName, + modelName: metadata.modelName, + }); HospitalStatusUpdate.addScope('latest', () => ({ attributes: [sequelize.literal('DISTINCT ON("HospitalStatusUpdate".hospital_uuid) 1')].concat( Object.keys(HospitalStatusUpdate.rawAttributes) diff --git a/models/hospitalUser.js b/models/hospitalUser.js index 9f8f9c01..519714ce 100644 --- a/models/hospitalUser.js +++ b/models/hospitalUser.js @@ -1,7 +1,9 @@ const _ = require('lodash'); const { Model } = require('sequelize'); +const metadata = require('../src/shared/metadata/hospitalUser'); +const convertToSequelizeField = require('../src/shared/convertToSequelizeField'); -module.exports = (sequelize, DataTypes) => { +module.exports = (sequelize) => { class HospitalUser extends Model { static associate(models) { HospitalUser.belongsTo(models.Hospital); @@ -18,68 +20,12 @@ module.exports = (sequelize, DataTypes) => { } } - HospitalUser.init( - { - id: { - field: 'hospitaluser_uuid', - type: DataTypes.UUID, - primaryKey: true, - autoIncrement: true, - }, - HospitalId: { - field: 'hospital_uuid', - type: DataTypes.UUID, - unique: true, - allowNull: false, - }, - EdAdminUserId: { - field: 'edadminuser_uuid', - type: DataTypes.STRING, - unique: true, - allowNull: false, - }, - isActive: { - field: 'activeindicator', - type: DataTypes.BOOLEAN, - allowNull: false, - defaultValue: true, - }, - isInfoUser: { - field: 'infouserindicator', - type: DataTypes.BOOLEAN, - defaultValue: true, - allowNull: false, - }, - isRingdownUser: { - field: 'ringdownuserindicator', - type: DataTypes.BOOLEAN, - defaultValue: true, - allowNull: false, - }, - 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, - }, - }, - { - sequelize, - timestamps: true, - tableName: 'hospitaluser', - modelName: 'HospitalUser', - } - ); + HospitalUser.init(metadata.getFieldHash(convertToSequelizeField), { + sequelize, + timestamps: true, + tableName: metadata.tableName, + modelName: metadata.modelName, + }); HospitalUser.addScope('active', { where: { isActive: true }, diff --git a/models/organization.js b/models/organization.js index 40c05152..baa459f2 100644 --- a/models/organization.js +++ b/models/organization.js @@ -1,7 +1,9 @@ const _ = require('lodash'); const { Model } = require('sequelize'); +const metadata = require('../src/shared/metadata/organization'); +const convertToSequelizeField = require('../src/shared/convertToSequelizeField'); -module.exports = (sequelize, DataTypes) => { +module.exports = (sequelize) => { class Organization extends Model { static associate(models) { Organization.hasMany(models.Ambulance); @@ -18,65 +20,11 @@ module.exports = (sequelize, DataTypes) => { return _.pick(attributes, ['id', 'name', 'type', 'timeZoneIsoCode', 'isActive', 'hospitals']); } } - Organization.init( - { - id: { - field: 'organization_uuid', - type: DataTypes.UUID, - primaryKey: true, - autoIncrement: true, - }, - name: { - field: 'organizationname', - type: DataTypes.STRING, - allowNull: false, - }, - type: { - field: 'organizationtypeenum', - type: DataTypes.ENUM('EMS', 'HEALTHCARE', 'C4SF'), - allowNull: false, - }, - state: { - field: 'organizationstate', - type: DataTypes.STRING, - }, - stateUniqueId: { - field: 'organizationstateuniqueid', - type: DataTypes.STRING, - }, - timeZoneIsoCode: { - field: 'timezoneisocode', - type: DataTypes.STRING, - }, - isActive: { - field: 'activeindicator', - type: DataTypes.BOOLEAN, - allowNull: false, - defaultValue: 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, - }, - }, - { - sequelize, - timestamps: true, - tableName: 'organization', - modelName: 'Organization', - } - ); + Organization.init(metadata.getFieldHash(convertToSequelizeField), { + sequelize, + timestamps: true, + tableName: metadata.tableName, + modelName: metadata.modelName, + }); return Organization; }; diff --git a/models/patient.js b/models/patient.js index 274a2ff0..156f86ba 100644 --- a/models/patient.js +++ b/models/patient.js @@ -1,34 +1,11 @@ const { Model } = require('sequelize'); +const metadata = require('../src/shared/metadata/patient'); +const convertToSequelizeField = require('../src/shared/convertToSequelizeField'); -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 metadata.getParams(); } static associate(models) { @@ -39,147 +16,11 @@ module.exports = (sequelize, DataTypes) => { Patient.belongsTo(models.User, { as: 'UpdatedBy' }); } } - 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, - }, - }, - { - sequelize, - timestamps: true, - tableName: 'patient', - modelName: 'Patient', - } - ); + Patient.init(metadata.getFieldHash(convertToSequelizeField), { + sequelize, + timestamps: true, + tableName: metadata.tableName, + modelName: metadata.modelName, + }); return Patient; }; diff --git a/models/patientDelivery.js b/models/patientDelivery.js index b4679922..62e10b44 100644 --- a/models/patientDelivery.js +++ b/models/patientDelivery.js @@ -1,11 +1,10 @@ const _ = require('lodash'); const { Model } = require('sequelize'); -const { DeliveryStatus } = require('../constants'); +const { DeliveryStatus } = require('../src/shared/constants'); +const metadata = require('../src/shared/metadata/patientDelivery'); +const convertToSequelizeField = require('../src/shared/convertToSequelizeField'); -const PatientDeliveryParams = ['currentDeliveryStatus', 'currentDeliveryStatusDateTimeLocal', 'etaMinutes']; -Object.freeze(PatientDeliveryParams); - -module.exports = (sequelize, DataTypes) => { +module.exports = (sequelize) => { class PatientDelivery extends Model { static get Status() { return DeliveryStatus; @@ -160,7 +159,7 @@ module.exports = (sequelize, DataTypes) => { }, hospital: _.pick(hospital, ['id', 'name']), patient: _.pick(patient, sequelize.models.Patient.Params), - patientDelivery: _.pick(this, PatientDeliveryParams), + patientDelivery: _.pick(this, metadata.getParams()), }; json.patientDelivery.timestamps = {}; const patientDeliveryUpdates = this.PatientDeliveryUpdates || (await this.getPatientDeliveryUpdates(options)); @@ -170,70 +169,11 @@ module.exports = (sequelize, DataTypes) => { return json; } } - PatientDelivery.init( - { - id: { - field: 'patientdelivery_uuid', - type: DataTypes.UUID, - primaryKey: true, - autoIncrement: true, - }, - AmbulanceId: { - field: 'ambulance_uuid', - type: DataTypes.UUID, - allowNull: false, - }, - PatientId: { - field: 'patient_uuid', - type: DataTypes.UUID, - allowNull: false, - }, - HospitalId: { - field: 'hospital_uuid', - type: DataTypes.UUID, - allowNull: false, - }, - ParamedicUserId: { - field: 'paramedicuser_uuid', - type: DataTypes.UUID, - allowNull: false, - }, - currentDeliveryStatus: { - field: 'currentdeliverystatusenum', - type: DataTypes.ENUM(DeliveryStatus.ALL_STATUSES), - allowNull: false, - }, - currentDeliveryStatusDateTimeLocal: { - field: 'currentdeliverystatusdatetimelocal', - type: DataTypes.DATE, - }, - etaMinutes: { - field: 'etaminutes', - type: DataTypes.INTEGER, - }, - 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, - }, - }, - { - sequelize, - timestamps: true, - tableName: 'patientdelivery', - modelName: 'PatientDelivery', - } - ); + PatientDelivery.init(metadata.getFieldHash(convertToSequelizeField), { + sequelize, + timestamps: true, + tableName: metadata.tableName, + modelName: metadata.modelName, + }); return PatientDelivery; }; diff --git a/models/patientDeliveryUpdate.js b/models/patientDeliveryUpdate.js index 181489af..117d205d 100644 --- a/models/patientDeliveryUpdate.js +++ b/models/patientDeliveryUpdate.js @@ -1,7 +1,8 @@ const { Model } = require('sequelize'); -const { DeliveryStatus } = require('../constants'); +const metadata = require('../src/shared/metadata/patientDeliveryUpdate'); +const convertToSequelizeField = require('../src/shared/convertToSequelizeField'); -module.exports = (sequelize, DataTypes) => { +module.exports = (sequelize) => { class PatientDeliveryUpdate extends Model { static associate(models) { PatientDeliveryUpdate.belongsTo(models.PatientDelivery); @@ -9,51 +10,11 @@ module.exports = (sequelize, DataTypes) => { PatientDeliveryUpdate.belongsTo(models.User, { as: 'UpdatedBy' }); } } - PatientDeliveryUpdate.init( - { - id: { - field: 'patientdeliveryupdate_uuid', - type: DataTypes.UUID, - primaryKey: true, - autoIncrement: true, - }, - PatientDeliveryId: { - field: 'patientdelivery_uuid', - type: DataTypes.UUID, - allowNull: false, - }, - deliveryStatus: { - field: 'deliverystatusenum', - type: DataTypes.ENUM(DeliveryStatus.ALL_STATUSES), - allowNull: false, - }, - deliveryStatusDateTimeLocal: { - field: 'deliverystatusdatetimelocal', - type: DataTypes.DATE, - }, - 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, - }, - }, - { - sequelize, - timestamps: true, - tableName: 'patientdeliveryupdate', - modelName: 'PatientDeliveryUpdate', - } - ); + PatientDeliveryUpdate.init(metadata.getFieldHash(convertToSequelizeField), { + sequelize, + timestamps: true, + tableName: metadata.tableName, + modelName: metadata.modelName, + }); return PatientDeliveryUpdate; }; diff --git a/models/user.js b/models/user.js index 2423c77d..971c5c26 100644 --- a/models/user.js +++ b/models/user.js @@ -1,10 +1,12 @@ const bcrypt = require('bcrypt'); const _ = require('lodash'); const { Model } = require('sequelize'); +const metadata = require('../src/shared/metadata/user'); +const convertToSequelizeField = require('../src/shared/convertToSequelizeField'); const SALT_ROUNDS = 10; -module.exports = (sequelize, DataTypes) => { +module.exports = (sequelize) => { class User extends Model { static associate(models) { User.belongsTo(models.Organization); @@ -38,115 +40,12 @@ module.exports = (sequelize, DataTypes) => { } } - User.init( - { - id: { - field: 'user_uuid', - type: DataTypes.UUID, - primaryKey: true, - autoIncrement: true, - }, - OrganizationId: { - field: 'organization_uuid', - type: DataTypes.UUID, - }, - firstName: { - field: 'firstname', - type: DataTypes.STRING, - allowNull: false, - }, - lastName: { - field: 'lastname', - type: DataTypes.STRING, - allowNull: false, - }, - name: { - type: DataTypes.VIRTUAL(DataTypes.STRING, ['firstName', 'lastName']), - get() { - return `${this.firstName} ${this.lastName}`.trim(); - }, - }, - email: { - field: 'email', - type: DataTypes.CITEXT, - unique: { - msg: 'This email address has already been used.', - }, - validate: { - isEmail: { - msg: 'This is not a valid email address.', - }, - }, - }, - subjectId: { - field: 'subjectid', - type: DataTypes.STRING, - }, - password: { - type: new DataTypes.VIRTUAL(DataTypes.STRING), - validate: { - is: { - args: [/^(?=.*[A-Z])(?=.*[!@#$&*])(?=.*[0-9])(?=.*[a-z]).{8,}$/], - msg: 'At least 8 characters, including upper and lowercase letters, a number, and a symbol.', - }, - }, - }, - hashedPassword: { - field: 'hashedpassword', - type: DataTypes.STRING, - }, - ssoData: { - field: 'ssodata', - type: DataTypes.JSONB, - }, - isOperationalUser: { - field: 'operationaluserindicator', - type: DataTypes.BOOLEAN, - allowNull: false, - defaultValue: false, - }, - isAdminUser: { - field: 'administrativeuserindicator', - type: DataTypes.BOOLEAN, - allowNull: false, - defaultValue: false, - }, - isSuperUser: { - field: 'superuserindicator', - type: DataTypes.BOOLEAN, - allowNull: false, - defaultValue: false, - }, - isActive: { - field: 'activeindicator', - type: DataTypes.BOOLEAN, - allowNull: false, - defaultValue: 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, - }, - }, - { - sequelize, - timestamps: true, - tableName: 'batsuser', - modelName: 'User', - } - ); + User.init(metadata.getFieldHash(convertToSequelizeField), { + sequelize, + timestamps: true, + tableName: metadata.tableName, + modelName: metadata.modelName, + }); User.beforeSave(async (user) => { /// if a new password has been set, hash for storage diff --git a/package.json b/package.json index 6fdf9fd5..3ef2177d 100644 --- a/package.json +++ b/package.json @@ -157,6 +157,6 @@ "eslint-plugin-react-hooks": "^4.1.0", "postcss": "^8.4.14", "postcss-cli": "^10.0.0", - "prettier": "2.1.1" + "prettier": "2.7.1" } } diff --git a/routes/api/ringdowns.js b/routes/api/ringdowns.js index 72f65222..9da2038e 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/shared/constants'); const { setPaginationHeaders } = require('../helpers'); diff --git a/seeders/20210204001430-ringdowns-seed.js b/seeders/20210204001430-ringdowns-seed.js index 00686c46..4105b1a1 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/shared/constants'); async function createRingdown( email, diff --git a/src/Components/Form.js b/src/Components/Form.js new file mode 100644 index 00000000..9d5e4e27 --- /dev/null +++ b/src/Components/Form.js @@ -0,0 +1,49 @@ +import React, { useMemo, useContext, createContext } from 'react'; +import PropTypes from 'prop-types'; + +const FormContext = createContext(undefined); + +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 ( + + {/* eslint-disable-next-line react/jsx-props-no-spreading */} +
{children}
+
+ ); +}; + +Form.propTypes = { + // eslint-disable-next-line react/forbid-prop-types + data: PropTypes.object.isRequired, + onChange: PropTypes.func, + children: PropTypes.node, +}; + +Form.defaultProps = { + onChange: null, + children: null, +}; + +const useForm = () => { + const context = useContext(FormContext); + + if (!context) { + throw new Error('useForm() must be used within a Form.'); + } + + return context; +}; + +export default Form; + +export { useForm }; diff --git a/src/Components/FormField.js b/src/Components/FormField.js new file mode 100644 index 00000000..1d7f7b05 --- /dev/null +++ b/src/Components/FormField.js @@ -0,0 +1,68 @@ +/* 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'; + +// prettier-ignore +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 ( + =true" in a form submission when it's checked + value={true} + {...props} + /> + ); + + case 'text': + return ( + + ); + + default: + throw new Error(`Unknown field type: ${type}`); + } +} + +FormField.propTypes = { + metadata: PropTypes.object.isRequired, +}; diff --git a/src/Components/Heading.js b/src/Components/Heading.js index d05549bc..7da768e3 100644 --- a/src/Components/Heading.js +++ b/src/Components/Heading.js @@ -6,10 +6,8 @@ import './Heading.scss'; function Heading({ title, subtitle, badge, children }) { return (

-
- {title} - {subtitle &&
{subtitle}
} -
+ {title} + {subtitle &&
{subtitle}
} {badge &&
{badge}
} {children}

diff --git a/src/Components/RingdownDetails.js b/src/Components/RingdownDetails.js index b899ae97..878fdb62 100644 --- a/src/Components/RingdownDetails.js +++ b/src/Components/RingdownDetails.js @@ -105,6 +105,12 @@ function RingdownDetails({ className, ringdown, isIncoming }) { Additional notes + {ringdown.treatmentNotes && ( + + Treatments Administered + {ringdown.treatmentNotes} + + )} {ringdown.etohSuspectedIndicator && ( ETOH @@ -128,7 +134,7 @@ function RingdownDetails({ className, ringdown, isIncoming }) { Combative {ringdown.combativeBehaviorIndicator && 'Yes'} - {ringdown.restraintIndicator && <> Restrained} + {ringdown.restraintIndicator && ', Restrained'} )} @@ -144,6 +150,12 @@ function RingdownDetails({ className, ringdown, isIncoming }) { {ringdown.ivIndicator && 'Started'} )} + {ringdown.glasgowComaScale && ( + + GCS + {ringdown.glasgowComaScale} + + )} {ringdown.otherObservationNotes && ( Other diff --git a/src/EMS/PatientFields.js b/src/EMS/PatientFields.js index ba47d086..d86196d8 100644 --- a/src/EMS/PatientFields.js +++ b/src/EMS/PatientFields.js @@ -1,31 +1,28 @@ import React, { useContext, useState, useEffect } from 'react'; import PropTypes from 'prop-types'; -import FormCheckbox from '../Components/FormCheckbox'; 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'; import ApiService from '../ApiService'; import Context from '../Context'; +import patient from '../shared/metadata/patient'; -const INPUT_RANGES = { - 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 }, -}; +const Patient = patient.getFieldHash(); -function getRange(property, extreme) { - return INPUT_RANGES[property] ? INPUT_RANGES[property][extreme] : null; +function createOptions(ids) { + return ids.map((id) => ( + + )); } +// prettier-ignore function PatientFields({ ringdown, onChange }) { const [ambulanceIds, setAmbulanceIds] = useState([]); const [dispatchCallNumbers, setDispatchCallNumbers] = useState([]); @@ -44,265 +41,149 @@ 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 ( - <> -
- -
-
- -
-
- -
-
- -
-
- -
- + +
+
+ - - - - - - - - -
- -
- Exclude identifying information. -
-
-
- +
+
+ +
+ + + + +
+ +
+
+ +
+ + + + + +
+ +
+ Exclude identifying information. +
+
+
+ + + + +
+ +
+
+ - - - -
- -
-
- -    - - - -    + - -
- - - -
- O2 -
- + + + + +
+ + + +
+ O2
- } - value="SUPPLEMENTAL OXYGEN" - disabled={ringdown.oxygenSaturation === null || ringdown.oxygenSaturation === ''} - /> -
-
- -
-
- -
-
- - - - -
- +
+ } + value="SUPPLEMENTAL OXYGEN" + disabled={ringdown.oxygenSaturation === null || ringdown.oxygenSaturation === ''} /> -
- - - - -
+ +
+ + + + +
+
+ + + + + +
+ +
+ + + +
- + ); } diff --git a/src/EMS/RingdownForm.js b/src/EMS/RingdownForm.js index 6f4f7ba0..ebcf6aab 100644 --- a/src/EMS/RingdownForm.js +++ b/src/EMS/RingdownForm.js @@ -10,6 +10,7 @@ import Spinner from '../Components/Spinner'; import RingdownCard from '../Components/RingdownCard'; import Heading from '../Components/Heading'; import Alert from '../Components/Alert'; +import Form from '../Components/Form'; import HospitalSelection from './HospitalSelection'; import PatientFields from './PatientFields'; import RingdownStatus from './RingdownStatus'; @@ -94,10 +95,15 @@ function RingdownForm({ className }) { setRingdowns([...ringdowns]); } + // prettier-ignore return ( <> {ringdowns && ringdowns.length === 0 && ( -
+

@@ -151,7 +157,7 @@ function RingdownForm({ className }) { onPrimary={handleConfirmCancel} /> )} -

+ )} {ringdowns && ringdowns.length > 0 && ( diff --git a/src/Models/Ringdown.js b/src/Models/Ringdown.js index 05851ae7..ca6020f5 100644 --- a/src/Models/Ringdown.js +++ b/src/Models/Ringdown.js @@ -1,69 +1,73 @@ +// eslint-disable func-names import { DateTime } from 'luxon'; import PropTypes from 'prop-types'; import { PatientFieldData, ValidationState } from './PatientFieldData'; - -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); - -DeliveryStatus.is = (status, target) => DeliveryStatus.ALL_STATUSES.indexOf(status) >= DeliveryStatus.ALL_STATUSES.indexOf(target); - -Object.freeze(DeliveryStatus); +import * as metadata from '../shared/metadata'; +import convertToPropType from '../shared/convertToPropType'; +import DeliveryStatus from '../shared/constants/DeliveryStatus'; + +// define the fields that must all have valid input to make the ringdown valid. the second array item is an optional function to determine +// whether the field's current value is valid as input. by default, the field is counted as having input if its value is truthy. the +// array order should be the same as the field order in PatientFields. +const validatedFields = [ + ['ambulanceIdentifier'], + ['dispatchCallNumber'], + ['emergencyServiceResponseType'], + ['age'], + ['sex'], + ['chiefComplaintDescription'], + ['stableIndicator', (value) => typeof value === 'boolean'], + ['all', () => ValidationState.NO_INPUT], +]; + +// define the names of the objects that will be added to the Ringdown payload property, and the list of its fields for which getters/setters +// will be added. if the object name is an array, the second string is used as the object name. +const payloadModels = [ + ['ambulance', ['ambulanceIdentifier']], + [['emergencyMedicalServiceCall', 'emsCall'], ['dispatchCallNumber']], + // we want to expose the hospital id field under a different name, so we'll define it in the class below instead of here + ['hospital', []], + ['patient', metadata.patient.getObjectFields()], + ['patientDelivery', ['etaMinutes', 'currentDeliveryStatus']], +]; + +// build a hash with an empty default object for each sub-object in the payload +function createDefaultPayload() { + const emptyPayload = payloadModels.reduce((result, [name]) => { + const objectName = name instanceof Array ? name[1] : name; + result[objectName] = {}; + return result; + }, {}); + + return { + ...emptyPayload, + // default the urgency to Code 2 for a fresh ringdown if the Code 3 option has been disabled + patient: (window.env.REACT_APP_DISABLE_CODE_3 && { emergencyServiceResponseType: 'CODE 2' }) || {}, + }; +} class Ringdown { static get Status() { return DeliveryStatus; } + static ascendingByOrder(a, b) { + if (a.order < b.order) { + return -1; + } + if (a.order === b.order) { + return 0; + } + return 1; + } + constructor(payload, validationData) { - this.payload = payload || {}; - this.payload.ambulance = this.payload.ambulance || {}; - this.payload.emsCall = this.payload.emsCall || {}; - this.payload.hospital = this.payload.hospital || {}; - this.payload.patient = this.payload.patient || {}; - this.payload.patientDelivery = this.payload.patientDelivery || {}; - this.validationData = validationData || { - ambulanceIdentifier: new PatientFieldData( - 'ambulanceIdentifier', - 0, - this.ambulanceIdentifier ? ValidationState.INPUT : ValidationState.NO_INPUT - ), - dispatchCallNumber: new PatientFieldData( - 'dispatchCallNumber', - 1, - this.dispatchCallNumber ? ValidationState.INPUT : ValidationState.NO_INPUT - ), - age: new PatientFieldData('age', 2, this.age ? ValidationState.INPUT : ValidationState.NO_INPUT), - sex: new PatientFieldData('sex', 3, this.sex ? ValidationState.INPUT : ValidationState.NO_INPUT), - emergencyServiceResponseType: new PatientFieldData( - 'emergencyServiceResponseType', - 4, - this.emergencyServiceResponseType ? ValidationState.INPUT : ValidationState.NO_INPUT - ), - chiefComplaintDescription: new PatientFieldData( - 'chiefComplaintDescription', - 5, - this.chiefComplaintDescription ? ValidationState.INPUT : ValidationState.NO_INPUT - ), - stableIndicator: new PatientFieldData( - 'stableIndicator', - 6, - typeof this.stableIndicator === 'boolean' ? ValidationState.INPUT : ValidationState.NO_INPUT - ), - all: new PatientFieldData('all', 7, ValidationState.NO_INPUT), + this.payload = { + ...createDefaultPayload(), + ...payload, }; + + this.setValidationData(validationData); } clone() { @@ -79,40 +83,9 @@ class Ringdown { return this.payload.id; } - // Ambulance - - get ambulance() { - return this.payload.ambulance ?? {}; - } - - get ambulanceIdentifier() { - return this.payload.ambulance.ambulanceIdentifier ?? ''; - } - - set ambulanceIdentifier(newValue) { - this.payload.ambulance.ambulanceIdentifier = newValue; - } - - // EMS Call - - get emsCall() { - return this.payload.emsCall ?? {}; - } - - get dispatchCallNumber() { - return this.payload.emsCall.dispatchCallNumber ?? ''; - } - - set dispatchCallNumber(newValue) { - this.payload.emsCall.dispatchCallNumber = newValue; - } - // Hospital - get hospital() { - return this.payload.hospital ?? {}; - } - + // special-case this field, since the field name (id) is getting mapped to a different name on the Ringdown (hospitalId) get hospitalId() { return this.payload.hospital.id ?? null; } @@ -123,115 +96,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,73 +111,9 @@ 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.treatmentNotes || this.etohSuspectedIndicator || this.drugsSuspectedIndicator || this.psychIndicator || @@ -321,6 +121,7 @@ class Ringdown { this.restraintIndicator || this.covid19SuspectedIndicator || this.ivIndicator || + this.glasgowComaScale || this.otherObservationNotes ); } @@ -335,22 +136,6 @@ class Ringdown { : null; } - get etaMinutes() { - return this.payload.patientDelivery.etaMinutes ?? null; - } - - set etaMinutes(newValue) { - this.payload.patientDelivery.etaMinutes = newValue; - } - - get currentDeliveryStatus() { - return this.payload.patientDelivery.currentDeliveryStatus ?? null; - } - - set currentDeliveryStatus(newValue) { - this.payload.patientDelivery.currentDeliveryStatus = newValue; - } - get timestamps() { return this.payload.patientDelivery.timestamps ?? {}; } @@ -385,14 +170,21 @@ class Ringdown { // Form validation - static ascendingByOrder(a, b) { - if (a.order < b.order) { - return -1; - } - if (a.order === b.order) { - return 0; + setValidationData(validationData) { + if (validationData) { + this.validationData = validationData; + } else { + this.validationData = validatedFields.reduce((result, field, i) => { + const [name, validator] = field; + const fieldValue = this[name]; + const value = typeof validator === 'function' ? validator(fieldValue) : fieldValue; + const state = value ? ValidationState.INPUT : ValidationState.NO_INPUT; + + result[name] = new PatientFieldData(name, i, state); + + return result; + }, {}); } - return 1; } validatePatientFields(updatedField, inputValue) { @@ -430,38 +222,89 @@ class Ringdown { } getValidationState(fieldName) { - return this.validationData[fieldName].validationState; + return this.validationData[fieldName]?.validationState; + } +} + +function attachFields(target, objectName, fields) { + const props = {}; + + fields.forEach((field) => { + props[field.name] = { + get() { + return this.payload[objectName][field.name] ?? field.defaultValue; + }, + set(newValue) { + this.payload[objectName][field.name] = newValue; + }, + configurable: true, + enumerable: true, + }; + }); + + Object.defineProperties(target, props); +} + +function overrideSetter(target, key, setter) { + const descriptor = Object.getOwnPropertyDescriptor(target, key); + + if (!descriptor || !descriptor.set) { + throw new Error(`setter for '${key}' does not exist on the target.`); } + + Object.defineProperty(target, key, { + ...descriptor, + set: setter, + }); } +// add the getters and setters to the Ringdown prototype for each field of each object in the payload +payloadModels.forEach(([modelInfo, fieldNames]) => { + let metadataName = modelInfo; + let objectName = modelInfo; + + if (modelInfo instanceof Array) { + // this model is being aliased under a different name on the Ringdown + [metadataName, objectName] = modelInfo; + } + + // get the ModelMetadata fields, either filtering by the list of strings in payloadModels or the prefetched array + const fields = + typeof fieldNames[0] === 'string' ? metadata[metadataName].getFields(undefined, ({ name }) => fieldNames.includes(name)) : fieldNames; + + // add a getter/setter for each field + attachFields(Ringdown.prototype, objectName, fields); + + // add a getter to return this payload sub-object + Object.defineProperties(Ringdown.prototype, { + [objectName]: { + get() { + return this.payload[objectName]; + }, + configurable: true, + enumerable: true, + }, + }); +}); + +// add custom setters for these payload fields, since their values affect other fields +overrideSetter(Ringdown.prototype, 'lowOxygenResponseType', function (newValue) { + this.payload.patient.lowOxygenResponseType = newValue; + if (newValue !== 'SUPPLEMENTAL OXYGEN') { + this.supplementalOxygenAmount = null; + } +}); +overrideSetter(Ringdown.prototype, 'combativeBehaviorIndicator', function (newValue) { + this.payload.patient.combativeBehaviorIndicator = newValue; + this.restraintIndicator = newValue && this.restraintIndicator; +}); + Ringdown.propTypes = { ambulanceIdentifier: PropTypes.string.isRequired, 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, + ...metadata.patient.getFieldHash(convertToPropType), // Status etaMinutes: PropTypes.number.isRequired, currentDeliveryStatus: PropTypes.oneOf(DeliveryStatus.ALL_STATUSES), diff --git a/src/shared/FieldMetadata.js b/src/shared/FieldMetadata.js new file mode 100644 index 00000000..7dff4f25 --- /dev/null +++ b/src/shared/FieldMetadata.js @@ -0,0 +1,34 @@ +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, + // assign these default values first so the field can override them below + { + colName: createColName(field), + isParam: !NonParamTypes.includes(field.type), + defaultValue: field.defaultValue ?? field.type === 'boolean' ? false : null, + }, + field + ); + } + + toString() { + return `FieldMetadata: ${this.name}`; + } + + get [Symbol.toStringTag]() { + return this.toString(); + } +} + +module.exports = FieldMetadata; diff --git a/src/shared/ModelMetadata.js b/src/shared/ModelMetadata.js new file mode 100644 index 00000000..2144fdd2 --- /dev/null +++ b/src/shared/ModelMetadata.js @@ -0,0 +1,61 @@ +const FieldMetadata = require('./FieldMetadata'); + +const identity = (field) => field; +const keyValue = (field) => [field.name, field]; +const all = () => true; + +const createdUpdatedFields = [ + { + name: 'createdAt', + colName: 'recordcreatetimestamp', + type: 'date', + }, + { + name: 'CreatedById', + colName: 'recordcreateuser_uuid', + type: 'uuid', + }, + { + name: 'updatedAt', + colName: 'recordupdatetimestamp', + type: 'date', + }, + { + name: 'UpdatedById', + colName: 'recordupdateuser_uuid', + type: 'uuid', + }, +]; + +class ModelMetadata { + constructor({ modelName, tableName = modelName.toLowerCase(), fields }) { + this.modelName = modelName; + this.tableName = tableName; + // append the standard created and updated fields to all models + this.fields = Object.freeze([...fields, ...createdUpdatedFields].map((field) => new FieldMetadata(field))); + this.params = Object.freeze( + this.getFields( + ({ name }) => name, + ({ isParam }) => isParam + ) + ); + } + + getFields(convertField = identity, filter = all) { + return this.fields.filter(filter).map(convertField); + } + + getFieldHash(convertField = keyValue, filter = all) { + return Object.fromEntries(this.getFields(convertField, filter)); + } + + getParams() { + return this.params; + } + + getObjectFields() { + return this.getFields(undefined, ({ type, isParam }) => isParam || type === 'enum'); + } +} + +module.exports = ModelMetadata; diff --git a/src/shared/constants/DeliveryStatus.js b/src/shared/constants/DeliveryStatus.js new file mode 100644 index 00000000..520cac2a --- /dev/null +++ b/src/shared/constants/DeliveryStatus.js @@ -0,0 +1,19 @@ +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); + +DeliveryStatus.is = (status, target) => DeliveryStatus.ALL_STATUSES.indexOf(status) >= DeliveryStatus.ALL_STATUSES.indexOf(target); + +module.exports = Object.freeze(DeliveryStatus); diff --git a/src/shared/constants/index.js b/src/shared/constants/index.js new file mode 100644 index 00000000..f22046bb --- /dev/null +++ b/src/shared/constants/index.js @@ -0,0 +1 @@ +exports.DeliveryStatus = require('./DeliveryStatus'); diff --git a/src/shared/convertToPropType.js b/src/shared/convertToPropType.js new file mode 100644 index 00000000..7783c048 --- /dev/null +++ b/src/shared/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, typeArgs, 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(typeArgs); + } + + if (required) { + propType = propType.isRequired; + } + + result = [name, propType]; + } + + return result; +}; diff --git a/src/shared/convertToSequelizeField.js b/src/shared/convertToSequelizeField.js new file mode 100644 index 00000000..8027a71a --- /dev/null +++ b/src/shared/convertToSequelizeField.js @@ -0,0 +1,33 @@ +const { DataTypes } = require('sequelize'); + +const SequelizeKeys = ['allowNull', 'autoIncrement', 'defaultValue', 'get', 'primaryKey', 'set', 'unique', 'validate']; + +const pick = (obj, keys) => Object.fromEntries(keys.filter((key) => key in obj).map((key) => [key, obj[key]])); + +const getDataType = (typeName) => DataTypes[typeName.toUpperCase()]; + +module.exports = function convertToSequelizeField(field) { + const { name, colName, typeArgs, type: typeName } = field; + const sqlAttributes = pick(field, SequelizeKeys); + let type = getDataType(typeName); + + if (typeName === 'enum') { + type = new DataTypes.ENUM(typeArgs); + } else if (typeName === 'virtual' && typeArgs) { + const [returnType, fields] = typeArgs; + + type = new DataTypes.VIRTUAL(returnType && getDataType(returnType), fields); + } + + // virtual fields don't have a column name in the db + if (typeName !== 'virtual') { + sqlAttributes.field = colName; + } + + const convertedField = { + type, + ...sqlAttributes, + }; + + return [name, convertedField]; +}; diff --git a/src/shared/metadata/ambulance.js b/src/shared/metadata/ambulance.js new file mode 100644 index 00000000..16809615 --- /dev/null +++ b/src/shared/metadata/ambulance.js @@ -0,0 +1,32 @@ +const ModelMetadata = require('../ModelMetadata'); + +const fields = [ + { + name: 'id', + colName: 'ambulance_uuid', + type: 'uuid', + primaryKey: true, + autoIncrement: true, + }, + { + name: 'OrganizationId', + colName: 'emsorganization_uuid', + type: 'uuid', + }, + { + name: 'ambulanceIdentifier', + type: 'string', + unique: true, + allowNull: false, + defaultValue: '', + }, + { + name: 'isActive', + colName: 'activeindicator', + type: 'boolean', + allowNull: false, + defaultValue: true, + }, +]; + +module.exports = new ModelMetadata({ modelName: 'Ambulance', fields }); diff --git a/src/shared/metadata/emergencyMedicalServiceCall.js b/src/shared/metadata/emergencyMedicalServiceCall.js new file mode 100644 index 00000000..9e6bc366 --- /dev/null +++ b/src/shared/metadata/emergencyMedicalServiceCall.js @@ -0,0 +1,24 @@ +const ModelMetadata = require('../ModelMetadata'); + +const fields = [ + { + name: 'id', + colName: 'emergencymedicalservicecall_uuid', + type: 'uuid', + primaryKey: true, + autoIncrement: true, + }, + { + name: 'dispatchCallNumber', + type: 'integer', + allowNull: false, + defaultValue: '', + }, + { + name: 'startDateTimeLocal', + type: 'date', + allowNull: false, + }, +]; + +module.exports = new ModelMetadata({ modelName: 'EmergencyMedicalServiceCall', fields }); diff --git a/src/shared/metadata/emergencyMedicalServiceCallAmbulance.js b/src/shared/metadata/emergencyMedicalServiceCallAmbulance.js new file mode 100644 index 00000000..935eab68 --- /dev/null +++ b/src/shared/metadata/emergencyMedicalServiceCallAmbulance.js @@ -0,0 +1,28 @@ +const ModelMetadata = require('../ModelMetadata'); + +const fields = [ + { + name: 'id', + colName: 'emergencymedicalservicecallambulance_uuid', + type: 'uuid', + primaryKey: true, + autoIncrement: true, + }, + { + name: 'EmergencyMedicalServiceCallId', + colName: 'emergencymedicalservicecall_uuid', + type: 'uuid', + }, + { + name: 'AmbulanceId', + colName: 'ambulance_uuid', + type: 'uuid', + }, + { + name: 'startDateTimeLocal', + type: 'date', + allowNull: false, + }, +]; + +module.exports = new ModelMetadata({ modelName: 'EmergencyMedicalServiceCallAmbulance', fields }); diff --git a/src/shared/metadata/hospital.js b/src/shared/metadata/hospital.js new file mode 100644 index 00000000..4e455ccd --- /dev/null +++ b/src/shared/metadata/hospital.js @@ -0,0 +1,47 @@ +const ModelMetadata = require('../ModelMetadata'); + +const fields = [ + { + // this is the value used to determine who each nurse belongs to + name: 'id', + colName: 'hospital_uuid', + type: 'uuid', + primaryKey: true, + autoIncrement: true, + }, + { + name: 'OrganizationId', + colName: 'healthcareorganization_uuid', + type: 'uuid', + }, + { + name: 'name', + colName: 'hospitalname', + type: 'string', + unique: true, + allowNull: false, + }, + { + name: 'state', + colName: 'hospitalstate', + type: 'string', + }, + { + name: 'stateFacilityCode', + colName: 'hospitalstatefacilitycode', + type: 'string', + }, + { + name: 'sortSequenceNumber', + type: 'integer', + }, + { + name: 'isActive', + colName: 'activeindicator', + type: 'boolean', + allowNull: false, + defaultValue: true, + }, +]; + +module.exports = new ModelMetadata({ modelName: 'Hospital', fields }); diff --git a/src/shared/metadata/hospitalStatusUpdate.js b/src/shared/metadata/hospitalStatusUpdate.js new file mode 100644 index 00000000..54983616 --- /dev/null +++ b/src/shared/metadata/hospitalStatusUpdate.js @@ -0,0 +1,65 @@ +const ModelMetadata = require('../ModelMetadata'); + +const fields = [ + { + name: 'id', + colName: 'hospitalstatusupdate_uuid', + type: 'uuid', + primaryKey: true, + autoIncrement: true, + }, + { + name: 'HospitalId', + colName: 'hospital_uuid', + type: 'uuid', + unique: true, + allowNull: false, + }, + { + name: 'EdAdminUserId', + colName: 'edadminuser_uuid', + type: 'uuid', + unique: true, + allowNull: false, + }, + { + name: 'updateDateTimeLocal', + type: 'date', + unique: true, + allowNull: false, + }, + { + name: 'openEdBedCount', + type: 'integer', + allowNull: false, + }, + { + name: 'openPsychBedCount', + type: 'integer', + allowNull: false, + }, + { + name: 'bedCountUpdateDateTimeLocal', + type: 'date', + }, + { + name: 'divertStatusIndicator', + type: 'boolean', + allowNull: false, + }, + { + name: 'divertStatusUpdateDateTimeLocal', + type: 'date', + }, + { + name: 'additionalServiceAvailabilityNotes', + type: 'text', + allowNull: true, + }, + { + name: 'notesUpdateDateTimeLocal', + type: 'date', + }, +]; + +module.exports = new ModelMetadata({ modelName: 'HospitalStatusUpdate', fields }); diff --git a/src/shared/metadata/hospitalUser.js b/src/shared/metadata/hospitalUser.js new file mode 100644 index 00000000..92560c19 --- /dev/null +++ b/src/shared/metadata/hospitalUser.js @@ -0,0 +1,48 @@ +const ModelMetadata = require('../ModelMetadata'); + +const fields = [ + { + name: 'id', + colName: 'hospitaluser_uuid', + type: 'uuid', + primaryKey: true, + autoIncrement: true, + }, + { + name: 'HospitalId', + colName: 'hospital_uuid', + type: 'uuid', + unique: true, + allowNull: false, + }, + { + name: 'EdAdminUserId', + colName: 'edadminuser_uuid', + type: 'uuid', + unique: true, + allowNull: false, + }, + { + name: 'isActive', + colName: 'activeindicator', + type: 'boolean', + allowNull: false, + defaultValue: true, + }, + { + name: 'isInfoUser', + colName: 'infouserindicator', + type: 'boolean', + allowNull: false, + defaultValue: true, + }, + { + name: 'isRingdownUser', + colName: 'ringdownuserindicator', + type: 'boolean', + allowNull: false, + defaultValue: true, + }, +]; + +module.exports = new ModelMetadata({ modelName: 'HospitalUser', fields }); diff --git a/src/shared/metadata/index.js b/src/shared/metadata/index.js new file mode 100644 index 00000000..2863b5c5 --- /dev/null +++ b/src/shared/metadata/index.js @@ -0,0 +1,25 @@ +const ambulance = require('./ambulance'); +const emergencyMedicalServiceCall = require('./emergencyMedicalServiceCall'); +const emergencyMedicalServiceCallAmbulance = require('./emergencyMedicalServiceCallAmbulance'); +const hospital = require('./hospital'); +const hospitalStatusUpdate = require('./hospitalStatusUpdate'); +const hospitalUser = require('./hospitalUser'); +const organization = require('./organization'); +const patient = require('./patient'); +const patientDeliveryUpdate = require('./patientDeliveryUpdate'); +const patientDelivery = require('./patientDelivery'); +const user = require('./user'); + +module.exports = { + ambulance, + emergencyMedicalServiceCall, + emergencyMedicalServiceCallAmbulance, + hospital, + hospitalStatusUpdate, + hospitalUser, + organization, + patient, + patientDelivery, + patientDeliveryUpdate, + user, +}; diff --git a/src/shared/metadata/organization.js b/src/shared/metadata/organization.js new file mode 100644 index 00000000..0b3db79a --- /dev/null +++ b/src/shared/metadata/organization.js @@ -0,0 +1,48 @@ +const ModelMetadata = require('../ModelMetadata'); + +const fields = [ + { + name: 'id', + colName: 'organization_uuid', + type: 'uuid', + primaryKey: true, + autoIncrement: true, + }, + { + name: 'name', + colName: 'organizationname', + type: 'string', + allowNull: false, + }, + { + name: 'type', + colName: 'organizationtypeenum', + type: 'enum', + typeArgs: ['EMS', 'HEALTHCARE', 'C4SF'], + allowNull: false, + }, + { + name: 'state', + colName: 'organizationstate', + type: 'string', + }, + { + name: 'stateUniqueId', + colName: 'organizationstateuniqueid', + type: 'string', + }, + { + name: 'timeZoneIsoCode', + type: 'string', + defaultValue: 'PST', + }, + { + name: 'isActive', + colName: 'activeindicator', + type: 'boolean', + allowNull: false, + defaultValue: true, + }, +]; + +module.exports = new ModelMetadata({ modelName: 'Organization', fields }); diff --git a/src/shared/metadata/patient.js b/src/shared/metadata/patient.js new file mode 100644 index 00000000..f6809227 --- /dev/null +++ b/src/shared/metadata/patient.js @@ -0,0 +1,155 @@ +const ModelMetadata = require('../ModelMetadata'); + +const fields = [ + { + name: 'id', + colName: 'patient_uuid', + type: 'uuid', + primaryKey: true, + autoIncrement: true, + }, + { + name: 'EmergencyMedicalServiceCallId', + colName: 'emergencymedicalservicecall_uuid', + type: 'uuid', + allowNull: false, + unique: true, + }, + { + name: 'emergencyServiceResponseType', + type: 'enum', + typeArgs: ['CODE 2', 'CODE 3'], + required: true, + }, + { + name: 'age', + type: 'integer', + label: 'Age (estimated)', + unit: 'years', + required: true, + range: { min: 0, max: 130 }, + }, + { + name: 'sex', + type: 'enum', + typeArgs: ['MALE', 'FEMALE', 'NON-BINARY'], + required: true, + }, + { + name: 'chiefComplaintDescription', + type: 'text', + label: 'Chief Complaint', + required: true, + }, + { + 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, + }, + { + name: 'systolicBloodPressure', + type: 'integer', + range: { min: 90, max: 180 }, + }, + { + name: 'diastolicBloodPressure', + type: 'integer', + range: { min: 60, max: 120 }, + }, + { + 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 }, + }, + { + name: 'lowOxygenResponseType', + type: 'enum', + typeArgs: ['ROOM AIR', 'SUPPLEMENTAL OXYGEN'], + }, + { + 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', + type: 'boolean', + }, + { + name: 'glasgowComaScale', + type: 'integer', + label: 'GCS', + unit: '/ 15', + range: { min: 3, max: 15 }, + }, + { + name: 'otherObservationNotes', + type: 'text', + label: 'Other', + }, +]; + +module.exports = new ModelMetadata({ modelName: 'Patient', fields }); diff --git a/src/shared/metadata/patientDelivery.js b/src/shared/metadata/patientDelivery.js new file mode 100644 index 00000000..06ca52ab --- /dev/null +++ b/src/shared/metadata/patientDelivery.js @@ -0,0 +1,56 @@ +const ModelMetadata = require('../ModelMetadata'); +const { DeliveryStatus } = require('../constants'); + +const fields = [ + { + name: 'id', + colName: 'patientdelivery_uuid', + type: 'uuid', + primaryKey: true, + autoIncrement: true, + }, + { + name: 'AmbulanceId', + colName: 'ambulance_uuid', + type: 'uuid', + allowNull: false, + }, + { + name: 'PatientId', + colName: 'patient_uuid', + type: 'uuid', + allowNull: false, + }, + { + name: 'HospitalId', + colName: 'hospital_uuid', + type: 'uuid', + allowNull: false, + }, + { + name: 'ParamedicUserId', + colName: 'paramedicuser_uuid', + type: 'uuid', + allowNull: false, + }, + { + name: 'currentDeliveryStatus', + type: 'enum', + typeArgs: DeliveryStatus.ALL_STATUSES, + allowNull: false, + isParam: true, + }, + { + name: 'currentDeliveryStatusDateTimeLocal', + type: 'date', + isParam: true, + }, + { + name: 'etaMinutes', + type: 'integer', + range: { min: 0 }, + isParam: true, + }, +]; + +module.exports = new ModelMetadata({ modelName: 'PatientDelivery', fields }); diff --git a/src/shared/metadata/patientDeliveryUpdate.js b/src/shared/metadata/patientDeliveryUpdate.js new file mode 100644 index 00000000..eb2ea3cc --- /dev/null +++ b/src/shared/metadata/patientDeliveryUpdate.js @@ -0,0 +1,30 @@ +const ModelMetadata = require('../ModelMetadata'); +const { DeliveryStatus } = require('../constants'); + +const fields = [ + { + name: 'id', + colName: 'patientdeliveryupdate_uuid', + type: 'uuid', + primaryKey: true, + autoIncrement: true, + }, + { + name: 'PatientDeliveryId', + colName: 'patientdelivery_uuid', + type: 'uuid', + allowNull: false, + }, + { + name: 'deliveryStatus', + type: 'enum', + typeArgs: DeliveryStatus.ALL_STATUSES, + allowNull: false, + }, + { + name: 'deliveryStatusDateTimeLocal', + type: 'date', + }, +]; + +module.exports = new ModelMetadata({ modelName: 'PatientDeliveryUpdate', fields }); diff --git a/src/shared/metadata/user.js b/src/shared/metadata/user.js new file mode 100644 index 00000000..ae13beac --- /dev/null +++ b/src/shared/metadata/user.js @@ -0,0 +1,99 @@ +const ModelMetadata = require('../ModelMetadata'); + +const fields = [ + { + name: 'id', + colName: 'user_uuid', + type: 'uuid', + primaryKey: true, + autoIncrement: true, + }, + { + name: 'OrganizationId', + colName: 'organization_uuid', + type: 'uuid', + }, + { + name: 'firstName', + type: 'string', + allowNull: false, + }, + { + name: 'lastName', + type: 'string', + allowNull: false, + }, + { + name: 'name', + type: 'virtual', + typeArgs: ['string', ['firstName', 'lastName']], + get() { + return `${this.firstName} ${this.lastName}`.trim(); + }, + }, + { + name: 'email', + type: 'citext', + unique: { + msg: 'This email address has already been used.', + }, + validate: { + isEmail: { + msg: 'This is not a valid email address.', + }, + }, + }, + { + name: 'subjectId', + type: 'string', + }, + { + name: 'password', + type: 'virtual', + typeArgs: ['string'], + validate: { + is: { + args: [/^(?=.*[A-Z])(?=.*[!@#$&*])(?=.*[0-9])(?=.*[a-z]).{8,}$/], + msg: 'At least 8 characters, including upper and lowercase letters, a number, and a symbol.', + }, + }, + }, + { + name: 'hashedPassword', + type: 'string', + }, + { + name: 'ssoData', + type: 'jsonb', + }, + { + name: 'isOperationalUser', + colName: 'operationaluserindicator', + type: 'boolean', + allowNull: false, + defaultValue: false, + }, + { + name: 'isAdminUser', + colName: 'administrativeuserindicator', + type: 'boolean', + allowNull: false, + defaultValue: false, + }, + { + name: 'isSuperUser', + colName: 'superuserindicator', + type: 'boolean', + allowNull: false, + defaultValue: false, + }, + { + name: 'isActive', + colName: 'activeindicator', + type: 'boolean', + allowNull: false, + defaultValue: true, + }, +]; + +module.exports = new ModelMetadata({ modelName: 'User', tableName: 'batsuser', fields }); diff --git a/test/integration/api/ringdowns.js b/test/integration/api/ringdowns.js index c3c8ab3a..d476943a 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/shared/constants'); describe('/api/ringdowns', () => { let testSession; @@ -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,13 +409,15 @@ describe('/api/ringdowns', () => { lowOxygenResponseType: null, supplementalOxygenAmount: null, temperature: null, - etohSuspectedIndicator: null, - drugsSuspectedIndicator: null, - psychIndicator: null, - combativeBehaviorIndicator: null, - restraintIndicator: null, - covid19SuspectedIndicator: null, - ivIndicator: null, + treatmentNotes: null, + etohSuspectedIndicator: false, + drugsSuspectedIndicator: false, + psychIndicator: false, + combativeBehaviorIndicator: false, + restraintIndicator: false, + covid19SuspectedIndicator: false, + ivIndicator: false, + glasgowComaScale: null, otherObservationNotes: null, }, patientDelivery: { diff --git a/test/unit/models/patientDelivery.js b/test/unit/models/patientDelivery.js index a35620b2..932f5366 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/shared/constants'); describe('models.PatientDelivery', () => { beforeEach(async () => { @@ -204,13 +204,15 @@ describe('models.PatientDelivery', () => { lowOxygenResponseType: null, supplementalOxygenAmount: null, temperature: null, - etohSuspectedIndicator: null, - drugsSuspectedIndicator: null, - psychIndicator: null, - combativeBehaviorIndicator: null, - restraintIndicator: null, - covid19SuspectedIndicator: null, - ivIndicator: null, + treatmentNotes: null, + etohSuspectedIndicator: false, + drugsSuspectedIndicator: false, + psychIndicator: false, + combativeBehaviorIndicator: false, + restraintIndicator: false, + covid19SuspectedIndicator: false, + ivIndicator: false, + glasgowComaScale: null, otherObservationNotes: null, }, patientDelivery: { diff --git a/theme/_uswds-theme-custom-styles.scss b/theme/_uswds-theme-custom-styles.scss index 776f9b58..0af64020 100644 --- a/theme/_uswds-theme-custom-styles.scss +++ b/theme/_uswds-theme-custom-styles.scss @@ -211,6 +211,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; } diff --git a/wss.js b/wss.js index 67f01588..497e1759 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/shared/constants/DeliveryStatus'); const models = require('./models'); const userServer = new WebSocket.Server({ noServer: true }); diff --git a/yarn.lock b/yarn.lock index c3770c73..8c3cb748 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10363,10 +10363,10 @@ prettier-linter-helpers@^1.0.0: dependencies: fast-diff "^1.1.2" -prettier@2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.1.1.tgz#d9485dd5e499daa6cb547023b87a6cf51bee37d6" - integrity sha512-9bY+5ZWCfqj3ghYBLxApy2zf6m+NJo5GzmLTpr9FsApsfjriNnS2dahWReHMi7qNPhhHl9SYHJs2cHZLgexNIw== +prettier@2.7.1: + version "2.7.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.7.1.tgz#e235806850d057f97bb08368a4f7d899f7760c64" + integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g== pretty-bytes@^5.1.0: version "5.3.0"