Skip to content

Commit

Permalink
Openemr fhir medication request (openemr#4558)
Browse files Browse the repository at this point in the history
* Implementing MedicationRequest with prescriptions.

We store medications in two different spots, medication list and
prescriptions table.  WENO stores them in prescriptions table and
NewCROP stores them in medication list.  For now we are just working
with the prescription table and will add the medication list in a bit.

* Add debugs for scope repo

* Moved where scopes are loaded into auth controller

* FHIR MedicationRequest resource

Fixed embedded medication.  Moved the status from the FHIR resource down
into the prescription level.

Got the drug codes to work correctly. Removed the unit, interval, and
route stuff which wasn't working and didn't have the right codes.

Implemented the intent and status ordering.  Moved the route stuff into
a separate function so we could re-use it if we do indeed need it later.

Added initial provenance resource implementation.

Added a narrative entity helper function to Utility.

* MedicationRequest FHIR implementation

This was a much bigger endeavor than expected due to the way OpenEMR handles medications and the way FHIR handles medications.
US Core combines the listing of a patient's medications and the patient's perscriptions into a single MedicationRequest resource.  FHIR R4 has a breakdown of various resources but US Core brings them into a single resource.  Many of the US core must support fields are not supported in the NewCrop and WENO e-prescribe and so we add those fields to the Medications issues screen.

In this commit I add to the Medication Issues screen the ability to specify a medication order's intent, the usage category of a medication, and an open text field for dosage instructions for a patient taking the medication.

The usage category specifies where the medication is intended to be used (at home, in an inpatient, outpatient, or at the time of discharge).  Eventually its been proposed to move this into the procedure order screen which will have to be a project by someone else.

I've gone and created a lists_medication table to hold all of the medication specific fields for an issue there.  I ended up moving the patient issue saving to its own service so we could centralize this logic and make sure we are saving data consistently.  I had to modify the deleter.php file to make sure we delete the data properly.  All the extra medication data is optional in the database so we don't break any existing logic elsewhere where we utilize issues.  I noticed the add_edit_issue.php was using our old system of escaping columns and I moved the logic to use parameterized sql binding.

I had to fix some bugs in the BaseService.php classes dealing with NULL values.

* Notes on future work that should be done.

* Fix style issues

* Had minor sequence order issue.

* Fix style issues.

* Fix sql problems from code review.
  • Loading branch information
adunsulag committed Aug 9, 2021
1 parent 24c7a86 commit b00a1e7
Show file tree
Hide file tree
Showing 19 changed files with 926 additions and 251 deletions.
1 change: 1 addition & 0 deletions interface/forms/eye_mag/save.php
Original file line number Diff line number Diff line change
Expand Up @@ -702,6 +702,7 @@
}

if ($issue != '0') { //if this issue already exists we are updating it...
// TODO: @adunsulag do we need to have the eye-form use the PatientIssuesService?
$query = "UPDATE lists SET " .
"type = '" . add_escape_custom($form_type) . "', " .
"title = '" . add_escape_custom($_REQUEST['form_title']) . "', " .
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1987,6 +1987,7 @@ public function insertApprovedData($data)
$data['lists3-enddate-con'][$i] = (null);
}

// TODO: Note this is the only way right now to create / update prescriptions is via CCDA...
$q_upd_pres = "UPDATE prescriptions
SET date_added=?,
drug=?,
Expand Down
1 change: 1 addition & 0 deletions interface/patient_file/deleter.php
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ function popup_close() {
$ids = explode(",", $issue);
foreach ($ids as $id) {
row_delete("issue_encounter", "list_id = '" . add_escape_custom($id) . "'");
row_delete("lists_medication", "list_id = '" . add_escape_custom($id) . "'");
row_delete("lists", "id = '" . add_escape_custom($id) . "'");
}
} elseif ($document) {
Expand Down
121 changes: 46 additions & 75 deletions interface/patient_file/summary/add_edit_issue.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
use OpenEMR\Common\Csrf\CsrfUtils;
use OpenEMR\Core\Header;
use OpenEMR\MedicalDevice\MedicalDevice;
use OpenEMR\Services\PatientIssuesService;

// TBD - Resolve functional issues if opener is included in Header
?>
Expand Down Expand Up @@ -230,86 +231,49 @@ function ActiveIssueCodeRecycleFn($thispid2, $ISSUE_TYPES2)
}
}

$form_begin = ($_POST['form_begin']) ? DateToYYYYMMDD($_POST['form_begin']) : '';
$form_end = ($_POST['form_end']) ? DateToYYYYMMDD($_POST['form_end']) : '';
$form_begin = !empty($_POST['form_begin']) ? DateToYYYYMMDD($_POST['form_begin']) : null;
$form_end = !empty($_POST['form_end']) ? DateToYYYYMMDD($_POST['form_end']) : null;
$form_return = !empty($_POST['form_return']) ? DateToYYYYMMDD($_POST['form_return']) : null;

$form_injury_part = $_POST['form_medical_system'] ?? '';
$form_injury_type = $_POST['form_medical_type'] ?? '';

if ($issue) {
$query = "UPDATE lists SET " .
"type = '" . add_escape_custom($text_type) . "', " .
"title = '" . add_escape_custom($_POST['form_title']) . "', " .
"udi = '" . add_escape_custom($_POST['form_udi']) . "', " .
"udi_data = '" . add_escape_custom($_POST['udi_data']) . "', " .
"comments = '" . add_escape_custom($_POST['form_comments']) . "', " .
"begdate = " . QuotedOrNull($form_begin) . ", " .
"enddate = " . QuotedOrNull($form_end) . ", " .
"returndate = " . QuotedOrNull($form_return ?? null) . ", " .
"diagnosis = '" . add_escape_custom($_POST['form_diagnosis']) . "', " .
"occurrence = '" . add_escape_custom($_POST['form_occur']) . "', " .
"classification = '" . add_escape_custom($_POST['form_classification']) . "', " .
"reinjury_id = '" . add_escape_custom($_POST['form_reinjury_id'] ?? '') . "', " .
"referredby = '" . add_escape_custom($_POST['form_referredby']) . "', " .
"injury_grade = '" . add_escape_custom($_POST['form_injury_grade'] ?? '') . "', " .
"injury_part = '" . add_escape_custom($form_injury_part) . "', " .
"injury_type = '" . add_escape_custom($form_injury_type) . "', " .
"outcome = '" . add_escape_custom($_POST['form_outcome']) . "', " .
"destination = '" . add_escape_custom($_POST['form_destination']) . "', " .
"reaction ='" . add_escape_custom($_POST['form_reaction']) . "', " .
"verification ='" . add_escape_custom($_POST['form_verification']) . "', " .
"severity_al ='" . add_escape_custom($_POST['form_severity_id']) . "', " .
"list_option_id ='" . add_escape_custom($_POST['form_title_id']) . "', " .
"erx_uploaded = '0', " .
"modifydate = NOW() " .
"WHERE id = '" . add_escape_custom($issue) . "'";
sqlStatement($query);
if ($text_type == "medication" && $form_end != '') {
sqlStatement(
'UPDATE prescriptions SET '
. 'medication = 0 where patient_id = ? '
. " and upper(trim(drug)) = ? "
. ' and medication = 1',
array($thispid, strtoupper($_POST['form_title']))
);
$issueRecord = [
'type' => $text_type
// I don't like how these date columns use this quasi 'NULL' value but its how the underlying service works...
,'begdate' => $form_begin ?? "NULL"
,'enddate' => $form_end ?? "NULL"
,'returndate' => $form_return ?? "NULL"
,'erx_uploaded' => 0
,'id' => $issue ?? null
,'pid' => $thispid
];
// TODO: we could simplify this array by just adding 'form_' onto everything but not all of the fields precisely match so that would need to be fixed up
$issue_form_fields = ['title' => 'form_title', 'udi' => 'form_udi', 'udi_data' => 'udi_data', 'form_comments' => 'comments'
, 'diagnosis' => 'form_diagnosis', 'occurrence' => 'form_occur', 'classification' => 'form_classification'
, 'reinjury_id' => 'form_reinjury_id', 'referredby' => 'form_referredby', 'injury_grade' => 'form_injury_grade'
, 'outcome' => 'form_outcome', 'destination' => 'form_destination', 'reaction' => 'form_reaction'
, 'verification' => 'form_verification', 'severity_al' => 'form_severity_id', 'list_option_id' => 'form_title_id'];
foreach ($issue_form_fields as $field => $form_field) {
if (isset($_POST[$form_field])) {
$issueRecord[$field] = $_POST[$form_field];
}
}

// now populate medication
if (isset($_POST['form_medication'])) {
$issueRecord['medication'] = $_POST['form_medication'];
}

$patientIssuesService = new PatientIssuesService();
if ($issue) {
$patientIssuesService->updateIssue($issueRecord);
} else {
$issue = sqlInsert(
"INSERT INTO lists ( " .
"date, pid, type, title, udi, udi_data, activity, comments, begdate, enddate, returndate, " .
"diagnosis, occurrence, classification, referredby, user, groupname, " .
"outcome, destination, reinjury_id, injury_grade, injury_part, injury_type, " .
"reaction, verification, severity_al, list_option_id " .
") VALUES ( " .
"NOW(), " .
"'" . add_escape_custom($thispid) . "', " .
"'" . add_escape_custom($text_type) . "', " .
"'" . add_escape_custom($_POST['form_title']) . "', " .
"'" . add_escape_custom($_POST['form_udi']) . "', " .
"'" . add_escape_custom($_POST['udi_data']) . "', " .
"1, " .
"'" . add_escape_custom($_POST['form_comments']) . "', " .
QuotedOrNull($form_begin) . ", " .
QuotedOrNull($form_end) . ", " .
QuotedOrNull($form_return ?? null) . ", " .
"'" . add_escape_custom($_POST['form_diagnosis']) . "', " .
"'" . add_escape_custom($_POST['form_occur']) . "', " .
"'" . add_escape_custom($_POST['form_classification']) . "', " .
"'" . add_escape_custom($_POST['form_referredby']) . "', " .
"'" . add_escape_custom($_SESSION['authUser']) . "', " .
"'" . add_escape_custom($_SESSION['authProvider']) . "', " .
"'" . add_escape_custom($_POST['form_outcome']) . "', " .
"'" . add_escape_custom($_POST['form_destination']) . "', " .
"'" . add_escape_custom($_POST['form_reinjury_id'] ?? '') . "', " .
"'" . add_escape_custom($_POST['form_injury_grade'] ?? '') . "', " .
"'" . add_escape_custom($form_injury_part) . "', " .
"'" . add_escape_custom($form_injury_type) . "', " .
"'" . add_escape_custom($_POST['form_reaction']) . "', " .
"'" . add_escape_custom($_POST['form_verification']) . "', " .
"'" . add_escape_custom($_POST['form_severity_id']) . "', " .
"'" . add_escape_custom($_POST['form_title_id']) . "' " .
")"
);
$issueRecord["date"] = date("Y-m-d H:m:s");
$issueRecord['activity'] = 1;
$issueRecord['user'] = $_SESSION['authUser'];
$issueRecord['groupname'] = $_SESSION['authProvider'];
$patientIssuesService->createIssue($issueRecord);
}

// For record/reporting purposes, place entry in lists_touch table.
Expand Down Expand Up @@ -353,7 +317,8 @@ function ActiveIssueCodeRecycleFn($thispid2, $ISSUE_TYPES2)

$irow = array();
if ($issue) {
$irow = sqlQuery("SELECT * FROM lists WHERE id = ?", array($issue));
$patientIssuesService = new PatientIssuesService();
$irow = $patientIssuesService->getOneById($issue);
if (!AclMain::aclCheckIssue($irow['type'], '', 'write')) {
die(xlt("Edit is not authorized!"));
}
Expand Down Expand Up @@ -965,6 +930,12 @@ function divclick(cb, divid) {
<?php echo rbinput('form_destination', '4', 'GP via podiatry', 'destination') ?>
<?php } ?>
</div>

<?php if ($irow['type'] == 'medication') : ?>
<!-- any medication specific issue information goes here -->
<?php include "add_edit_issue_medication_fragment.php"; ?>
<?php endif; ?>

<br />
<?php //can change position of buttons by creating a class 'position-override' and adding rule text-alig:center or right as the case may be in individual stylesheets
?>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

/**
* add_edit_issue_medication_fragment.php Represents the medication fields used for the medication type issue list
* @package openemr
* @link http:https://www.open-emr.org
* @author Stephen Nielson <[email protected]>
* @copyright Copyright (c) 2021 Stephen Nielson <[email protected]>
* @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
*/

/**
* @global $irow The issues list record row we are working with
*/
if (empty($irow)) {
return;
}
$medication = $irow['medication'] ?? [];
$usage_category = $medication['usage_category'] ?? null;
$request_intent = $medication['request_intent'] ?? null;
?>
<div class="form-group col-12">
<label class="col-form-label" for="medication[usage_category]"><?php echo xlt('Medication Usage'); ?>:</label>
<?php
generate_form_field(array('data_type' => 1, 'field_id' => 'medication[usage_category]', 'list_id' => 'medication-usage-category', 'empty_title' => 'SKIP'), $usage_category);
?>
</div>
<div class="form-group col-12">
<label class="col-form-label" for="medication[request_intent]"><?php echo xlt('Medication Request Intent'); ?>:</label>
<?php
generate_form_field(array('data_type' => 1, 'field_id' => 'medication[request_intent]', 'list_id' => 'medication-request-intent'), $request_intent);
?>
</div>
<div class="form-group col-12">
<label class="col-form-label" for="form_medication[drug_dosage_instructions]"><?php echo xlt('Medication Dosage Instructions'); ?>:</label>
<textarea class="form-control" name='form_medication[drug_dosage_instructions]' id='form_medication[drug_dosage_instructions]'
rows="4"><?php echo text($medication['drug_dosage_instructions'] ?? '') ?></textarea>
</div>
5 changes: 0 additions & 5 deletions oauth2/provider/scope-authorize.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,6 @@
use OpenEMR\Common\Csrf\CsrfUtils;
use OpenEMR\Core\Header;

$scopeString = $_SESSION['scopes'] ?? '';
$scopes = explode(' ', $scopeString);

$claims = $_SESSION['claims'] ?? [];

?>
<html>
<head>
Expand Down
47 changes: 47 additions & 0 deletions sql/6_0_0-to-6_1_0_upgrade.sql
Original file line number Diff line number Diff line change
Expand Up @@ -885,3 +885,50 @@ UPDATE `list_options` SET `seq` = 30, `notes` = 'The care team is temporarily on
UPDATE `list_options` SET `seq` = 40, `notes` = 'The care team was, but is no longer, participating in the coordination and delivery of care.' WHERE `list_id` = 'Care_Team_Status' AND `option_id` = 'inactive';
UPDATE `list_options` SET `seq` = 50, `notes` = 'The care team should have never existed.' WHERE `list_id` = 'Care_Team_Status' AND `option_id` = 'entered-in-error';
#EndIf

#IfMissingColumn prescriptions drug_dosage_instructions
ALTER TABLE `prescriptions` ADD COLUMN drug_dosage_instructions longtext COMMENT 'Medication dosage instructions';
#EndIf

#IfMissingColumn prescriptions usage_category
ALTER TABLE `prescriptions` ADD COLUMN `usage_category` VARCHAR(100) NULL COMMENT 'option_id in list_options.list_id=medication-usage-category';
ALTER TABLE `prescriptions` ADD COLUMN `usage_category_title` VARCHAR(255) NOT NULL COMMENT 'title in list_options.list_id=medication-usage-category';
ALTER TABLE `prescriptions` ADD COLUMN `request_intent` VARCHAR(100) NULL COMMENT 'option_id in list_options.list_id=medication-request-intent';
ALTER TABLE `prescriptions` ADD COLUMN `request_intent_title` VARCHAR(255) NOT NULL COMMENT 'title in list_options.list_id=medication-request-intent';
#EndIf

#IfNotTable lists_medication
CREATE TABLE `lists_medication` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT
, `list_id` BIGINT(20) NULL COMMENT 'FK Reference to lists.id'
, `drug_dosage_instructions` LONGTEXT NULL COMMENT 'Free text dosage instructions for taking the drug'
, `usage_category` VARCHAR(100) NULL COMMENT 'option_id in list_options.list_id=medication-usage-category'
, `usage_category_title` VARCHAR(255) NOT NULL COMMENT 'title in list_options.list_id=medication-usage-category'
, `request_intent` VARCHAR(100) NULL COMMENT 'option_id in list_options.list_id=medication-request-intent'
, `request_intent_title` VARCHAR(255) NOT NULL COMMENT 'title in list_options.list_id=medication-request-intent'
, PRIMARY KEY (`id`)
, INDEX `lists_med_usage_category_idx`(`usage_category`)
, INDEX `lists_med_request_intent_idx`(`request_intent`)
, INDEX `lists_medication_list_idx` (`list_id`)
) ENGINE = InnoDB COMMENT = 'Holds additional data about patient medications.';
#EndIf

#IfNotRow2D list_options list_id lists option_id medication-usage-category
INSERT INTO list_options (list_id,option_id,title, seq, is_default, option_value, notes) VALUES ('lists','medication-usage-category','Medication Usage Category',0, 1, 0, 'Values taken from http:https://hl7.org/fhir/R4/valueset-medicationrequest-category.html');
INSERT INTO list_options (list_id,option_id,title,seq,is_default,activity, notes) VALUES ('medication-usage-category','inpatient','Inpatient',10,0,1, 'Includes requests for medications to be administered or consumed in an inpatient or acute care setting');
INSERT INTO list_options (list_id,option_id,title,seq,is_default,activity, notes) VALUES ('medication-usage-category','outpatient','Outpatient',20,0,1, 'Includes requests for medications to be administered or consumed in an outpatient setting (for example, Emergency Department, Outpatient Clinic, Outpatient Surgery, Doctor''s office)');
INSERT INTO list_options (list_id,option_id,title,seq,is_default,activity, notes) VALUES ('medication-usage-category','community','Home/Community',30,1,1, 'Includes requests for medications to be administered or consumed by the patient in their home (this would include long term care or nursing homes, hospices, etc.)');
INSERT INTO list_options (list_id,option_id,title,seq,is_default,activity, notes) VALUES ('medication-usage-category','discharge','Discharge',40,0,1, 'Includes requests for medications created when the patient is being released from a facility');
#EndIf

#IfNotRow2D list_options list_id lists option_id medication-request-intent
INSERT INTO list_options (list_id,option_id,title, seq, is_default, option_value, notes) VALUES ('lists','medication-request-intent','Medication Request Intent',0, 1, 0, 'Values taken from http:https://hl7.org/fhir/R4/valueset-medicationrequest-intent.html');
INSERT INTO list_options (list_id,option_id,title,seq,is_default,activity, notes) VALUES ('medication-request-intent','proposal','Proposal',10,0,1, 'The request is a suggestion made by someone/something that doesn''t have an intention to ensure it occurs and without providing an authorization to act.');
INSERT INTO list_options (list_id,option_id,title,seq,is_default,activity, notes) VALUES ('medication-request-intent','plan','Plan',20,0,1, 'The request represents an intention to ensure something occurs without providing an authorization for others to act.');
INSERT INTO list_options (list_id,option_id,title,seq,is_default,activity, notes) VALUES ('medication-request-intent','order','Order',30,1,1, 'The request represents a request/demand and authorization for action');
INSERT INTO list_options (list_id,option_id,title,seq,is_default,activity, notes) VALUES ('medication-request-intent','original-order','Original Order',40,0,1, 'The request represents the original authorization for the medication request.');
INSERT INTO list_options (list_id,option_id,title,seq,is_default,activity, notes) VALUES ('medication-request-intent','reflex-order','Reflex Order',50,0,1, 'The request represents an automatically generated supplemental authorization for action based on a parent authorization together with initial results of the action taken against that parent authorization.');
INSERT INTO list_options (list_id,option_id,title,seq,is_default,activity, notes) VALUES ('medication-request-intent','filler-order','Filler Order',60,0,1, 'The request represents the view of an authorization instantiated by a fulfilling system representing the details of the fulfiller''s intention to act upon a submitted order.');
INSERT INTO list_options (list_id,option_id,title,seq,is_default,activity, notes) VALUES ('medication-request-intent','instance-order','Instance Order',70,0,1, 'The request represents an instance for the particular order, for example a medication administration record.');
INSERT INTO list_options (list_id,option_id,title,seq,is_default,activity, notes) VALUES ('medication-request-intent','option','Option',80,0,1, 'The request represents a component or option for a RequestGroup that establishes timing, conditionality and/or other constraints among a set of requests.');
#EndIf
Loading

0 comments on commit b00a1e7

Please sign in to comment.