Skip to content

Commit

Permalink
Feat openemr b11 7742 decision support interventions (openemr#7743)
Browse files Browse the repository at this point in the history
* CDR rules refactor, PSR-4, testability

Refactored the CDR rules editing engine to be in the source tree so we
can have PSR-4 autoloading of the files.

Refactored the CDR controllers to be able to use Request/Response
objects for PSR-7 compatability and to make it possible to unit test
without having to do guzzle or curl requests.

This is prep work to be able to edit some of the interface to support
our onc b11 requirements.

* Moved template files fixed template bugs

Moved the template files to be in the templates directory to better
align with OpenEMR coding standards.  Eventually it would be nice to
move these to all be twig files.  Leaving them alone for now.

Removed the undecorated template file as its not used anywhere.

Removed the edit/helper/common.php file and moved its elements into the
RuleTemplateExtension where the hope is to eventually have these be twig
functions.

* Refactored AJAX code for rules to centralize code

Made the AJAX code for the CDR rules to be centralized using the same
controller logic that the rest of the rule engine uses for consistency
and access control.

* Cleaned up Frontend Controller includes

Removed the common, header, and ui files that were being included in the
index.php and moved all of the logic into the src folder tree.

* DSI additional source attributes

Added the additional ONC required dsi source attributes for our existing
CDR engine.  This includes the display and editing for those attributes.

Following attributes are added
- patient race usage
- patient ethnicity usage
- patient language usage
- patient sexual orientation usage
- patient sex usage
- patient dob usage
- patient health status assessments usage
- patient social determinants of health usage

* Initial prototype of b11 View rules

Put in the foundation for the b11 viewing of predictive dsi attributes.

Right now it shows as a questionnaire, but may switch this to a regular
form.  I'll have to play around and see what I like better here.

* WIP b11 Source Attributes Predictive DSI

Added predictive source attributes into the list options to give admin's
the ability to add custom attributes.

Added new table dsi_source_attributes for tracking the source attribute
information on an oauth_clients entity.

Added a field to oauth_clients table to track if the client is a
predictive or evidence based api client.  At some point we want a many
to one relationship here, but for now we treat api clients as a 1:1
relationship for evidence based / predictive ai clients.

Refactored a great deal of the api client admin interface to use twig to
modernize the codebase and add twig files for the external-cdr pieces.

This is a foundational work to support in the future the CDS Hooks
standard.

Also did code style fixes on the class

* Style fixes for clinical decision rules refactor

* Fix db/dsi type mismatch, helper links on client

Fixed a mismatch between the database comments for the dsi type and what
the code constants in client entity were.

Fixed the helper links on the client.

* Implement source attribute dialog display

Implemented the display of the source attributes from the smart launch
screen for displaying Decession Support Interventions Source
Information.

* Add CSRF token to attribute saves.

* b11 DSI api client registration

Added the registration pieces to the smart app client registration page.

Added the dsi_type and dsi_source_attributes metadata attributes to the
registration endpoint.  Only existing attributes configured in the
system can be configured and supported.

* Remove hyphens from dsi attribute definitions

* Add dsi information to introspection endpoint

Added the dsi information to the api endpoint that is retrieved from the
introspection.  Note there is another outstanding PR that this work
depends on in order for the endpoint to execute due to some pathing
issues.  So this commit will not be useuable without bringing in PR
 openemr#7741.

* Implement evidence based rules for external cdr.

* Fix no-dsi api clients not loading

* Display source attribute info on cdr rule info

Added a dialog info box that shows the cdr rule information when you
click on the help icon for a cdr evidence based rule.

Added a new review controller to handle this.

Moved ACL checks into the router controller.  Opened up the review
controller to regular permissions.  I don't see any security check that
needs to happen on read only basis here.

* Cleaned up cdr demographics tool tip

Now that source attributes are showing up in a dialog, we don't need the
tooltip to show a bunch of stuff on the rule tooltip.

Fixed width constraints on the source attribute editing as 50 characters
was not correct when it should be 255 characters.

Fixed the web reference not showing up in the edit/details view of the
CDR rules.

* Inserting feedback on evidence based CDR

Made it so that feedback gets inserted into the alert logs.  Users can
provide feedback on the action taken in the CDR for user development.

* Implement feedback in cdr_log

Made it so the cdr log will show the feedback and allow you to export
the CDR evidence based feedback in a CSV download.

Refactored the CDR_Log process to leverage the same controller
infrastructure as the CDR admin controllers and dialog viewer.  Trying
to centralize the code more in order to improve cohesion and
maintenance.

I kept the existing view infrastructure as it'd be too major of a
refactor right now to switch to twig.

Fixed some style issues as well.

* Added facility for location tracking

* Change column size, sync database.sql

Made the database.sql have the changes that were put in the sql patch.

Changed the column size to be text instead of varchar

* Fix escaping issue.

* Change sequence on list_options for more flexibility

* Fix escaping

* Fix style changes

* Fix dsi_type if its not provided

Needed to fix the dsi type if the client does not provide one in the
registration.  Fixed some other errors showing up on the logs if data
was not provided.

* Fix registration with predictive dsi

Prior commit was breaking the registration ouch.

* Fix broken unit tests

Routing controllers needed more DI service injectors to handle the unit
testing changes.  Removed the list test and added a save test on the
external cdr.

Fixed some other broken unit tests.

* Fix thrown warnings with vital forms test

* Hide cdr link if no services enabled.

* Default unknown text if source attribute missing

Made it so an unknown text is showing on details view of CDR rules and
DSI smart app source attributes per ONC requirements.

* Fix style issues.

* See if we can fix the styles.

Not sure why the auto-corrector isn't catching this, but trying this
again.

* Fix sql text datatype, improve source descriptions.

Made the source descriptions clearer.

Changed the SQL text datatype to remove not null attribute, fixed php
code to support nullable attributes.

* Direct edit links to simplify UX/testing

Added a direct edit button if you are an admin to the CDR rules and to
the smart 3rd party DSI attributes.  This simplifies the UX testing and
allows users to go right from the dialog popups to editing the
attributes.  I don't have deep back links supported which would be a
bigger change but this is good enough to get from viewing to editing.

I'm using a one time message passing via postMessage to communicate
between the dialogs and the pages.  At some point it'd be nice to
generalize this, but that's an exercise for another day.
  • Loading branch information
adunsulag authored Oct 27, 2024
1 parent 9733cdd commit cd290ff
Show file tree
Hide file tree
Showing 153 changed files with 6,808 additions and 3,036 deletions.
46 changes: 46 additions & 0 deletions interface/patient_file/summary/demographics.php
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,52 @@ function load_location(location) {
url: $(this).attr('href')
});
});
$(".cdr-rule-btn-info-launch").on("click", function (e) {
let pid = <?php echo js_escape($pid); ?>;
let csrfToken = <?php echo js_escape(CsrfUtils::collectCsrfToken()); ?>;
let ruleId = $(this).data("ruleId");
let launchUrl = "<?php echo $GLOBALS['webroot']; ?>/interface/super/rules/index.php?action=review!view&pid="
+ encodeURIComponent(pid) + "&rule_id=" + encodeURIComponent(ruleId) + "&csrf_token_form=" + encodeURIComponent(csrfToken);
e.preventDefault();
e.stopPropagation();
// as we're loading another iframe, make sure to sync session
window.top.restoreSession();

let windowMessageHandler = function () {
console.log("received message ", event);
if (event.origin !== window.location.origin) {
return;
}
let data = event.data;
if (data && data.type === 'cdr-edit-source') {
dlgclose();
window.top.removeEventListener('message', windowMessageHandler);
// loadFrame already handles webroot and /interface/ prefix.
let editUrl = '/super/rules/index.php?action=edit!summary&id=' +encodeURIComponent(data.ruleId)
+ "&csrf_token=" + encodeURIComponent(csrfToken);
window.parent.left_nav.loadFrame('adm', 'adm0', editUrl);
}
};
window.top.addEventListener('message', windowMessageHandler);

dlgopen('', '', 800, 200, '', '', {
buttons: [{
text: <?php echo xlj('Close'); ?>,
close: true,
style: 'secondary btn-sm'
}],
// don't think we need to refresh
// onClosed: 'refreshme',
allowResize: true,
allowDrag: true,
dialogId: 'rulereview',
type: 'iframe',
url: launchUrl,
onClose: function() {
window.top.removeEventListener('message', windowMessageHandler);
}
});
})
});
<?php } // end crw
?>
Expand Down
2 changes: 1 addition & 1 deletion interface/reports/amc_full_report.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
require_once("../../library/classes/rulesets/library/RsReportFactoryAbstract.php");
require_once("../../library/classes/rulesets/Amc/AmcReportFactory.php");

use OpenEMR\ClinicialDecisionRules\AMC\CertificationReportTypes;
use OpenEMR\ClinicalDecisionRules\AMC\CertificationReportTypes;
use OpenEMR\Common\Twig\TwigContainer;
use OpenEMR\Common\Logging\SystemLogger;

Expand Down
278 changes: 35 additions & 243 deletions interface/reports/cdr_log.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,250 +15,42 @@
require_once "$srcdir/options.inc.php";
require_once "$srcdir/clinical_rules.php";

use OpenEMR\Common\Acl\AclMain;
use OpenEMR\Common\Csrf\CsrfUtils;
use OpenEMR\ClinicalDecisionRules\Interface\ControllerRouter;
use OpenEMR\Common\Twig\TwigContainer;
use OpenEMR\Core\Header;

if (!AclMain::aclCheckCore('patients', 'med')) {
echo (new TwigContainer(null, $GLOBALS['kernel']))->getTwig()->render('core/unauthorized.html.twig', ['pageTitle' => xl("Alerts Log")]);
exit;
}

if (!empty($_POST)) {
if (!CsrfUtils::verifyCsrfToken($_POST["csrf_token_form"])) {
CsrfUtils::csrfNotVerified();
use Symfony\Component\HttpFoundation\Request;
use OpenEMR\Common\Csrf\CsrfInvalidException;
use OpenEMR\Common\Acl\AccessDeniedException;
use OpenEMR\Common\Logging\SystemLogger;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpFoundation\Response;

try {
$request = Request::createFromGlobals();
if (empty($request->get('action'))) {
$request->query->set('action', 'log!view');
}
$controllerRouter = new ControllerRouter();
$response = $controllerRouter->route($request);
} catch (AccessDeniedException | CsrfInvalidException $e) {
// Log the exception
(new SystemLogger())->errorLogCaller($e->getMessage(), ['trace' => $e->getTraceAsString()]);
$contents = (new TwigContainer(null, $GLOBALS['kernel']))->getTwig()->render('core/unauthorized.html.twig', ['pageTitle' => xl("Alerts Log")]);
// Send the error response
$response = new Response($contents, 403);
} catch (NotFoundHttpException $e) {
// Log the exception
(new SystemLogger())->errorLogCaller($e->getMessage(), ['trace' => $e->getTraceAsString()]);
$contents = (new TwigContainer(null, $GLOBALS['kernel']))->getTwig()->render('error/404.html.twig');
// Send the error response
$response = new Response($contents, 404);
} catch (Exception $e) {
// Log the exception
(new SystemLogger())->errorLogCaller($e->getMessage(), ['trace' => $e->getTraceAsString()]);
$contents = (new TwigContainer(null, $GLOBALS['kernel']))->getTwig()->render('error/general_http_error.html.twig');
// Send the error response
$response = new Response($contents, 500);
}

$form_begin_date = DateTimeToYYYYMMDDHHMMSS($_POST['form_begin_date'] ?? '');
$form_end_date = DateTimeToYYYYMMDDHHMMSS($_POST['form_end_date'] ?? '');
?>

<html>

<head>
<title><?php echo xlt('Alerts Log'); ?></title>

<?php Header::setupHeader('datetime-picker'); ?>

<script>
$(function () {
$('.datepicker').datetimepicker({
<?php $datetimepicker_timepicker = true; ?>
<?php $datetimepicker_showseconds = true; ?>
<?php $datetimepicker_formatInput = true; ?>
<?php require($GLOBALS['srcdir'] . '/js/xl/jquery-datetimepicker-2-5-4.js.php'); ?>
<?php // can add any additional javascript settings to datetimepicker here; need to prepend first setting with a comma ?>
});
});
</script>

<style>
/* specifically include & exclude from printing */
@media print {
#report_parameters {
visibility: hidden;
display: none;
}
#report_parameters_daterange {
visibility: visible;
display: inline;
}
#report_results table {
margin-top: 0px;
}
}

/* specifically exclude some from the screen */
@media screen {
#report_parameters_daterange {
visibility: hidden;
display: none;
}
}
</style>
</head>

<body class="body_top">

<!-- Required for the popup date selectors -->
<div id="overDiv" style="position:absolute; visibility:hidden; z-index:1000;"></div>

<span class='title'><?php echo xlt('Alerts Log'); ?></span>

<form method='post' name='theform' id='theform' action='cdr_log.php' onsubmit='return top.restoreSession()'>
<input type="hidden" name="csrf_token_form" value="<?php echo attr(CsrfUtils::collectCsrfToken()); ?>" />
<input type="hidden" name="search" value="1" />

<div id="report_parameters">

<table>
<tr>
<td width='470px'>
<div style='float: left'>

<table class='text'>

<tr>
<td class='col-form-label'>
<?php echo xlt('Begin Date'); ?>:
</td>
<td>
<input type='text' name='form_begin_date' id='form_begin_date' size='20' value='<?php echo attr(oeFormatDateTime($form_begin_date, "global", true)); ?>'
class='datepicker form-control'>
</td>
</tr>

<tr>
<td class='col-form-label'>
<?php echo xlt('End Date'); ?>:
</td>
<td>
<input type='text' name='form_end_date' id='form_end_date' size='20' value='<?php echo attr(oeFormatDateTime($form_end_date, "global", true)); ?>'
class='datepicker form-control'>
</td>
</tr>
</table>
</div>

</td>
<td align='left' valign='middle' height="100%">
<table style='border-left: 1px solid; width:100%; height:100%' >
<tr>
<td>
<div class="text-center">
<div class="btn-group" role="group">
<a id='search_button' href='#' class='btn btn-secondary btn-search' onclick='top.restoreSession(); $("#theform").submit()'>
<?php echo xlt('Search'); ?>
</a>
</div>
</div>
</td>
</tr>
</table>
</td>
</tr>
</table>

</div> <!-- end of search parameters -->

<br />

<?php if (!empty($_POST['search']) && ($_POST['search'] == 1)) { ?>
<div id="report_results">
<table class="table">

<thead>
<th align='center'>
<?php echo xlt('Date'); ?>
</th>

<th align='center'>
<?php echo xlt('Patient ID'); ?>
</th>

<th align='center'>
<?php echo xlt('User ID'); ?>
</th>

<th align='center'>
<?php echo xlt('Category'); ?>
</th>

<th align='center'>
<?php echo xlt('All Alerts'); ?>
</th>

<th align='center'>
<?php echo xlt('New Alerts'); ?>
</th>

</thead>
<tbody> <!-- added for better print-ability -->
<?php
$res = listingCDRReminderLog($form_begin_date, $form_end_date);

while ($row = sqlFetchArray($res)) {
//Create category title
if ($row['category'] == 'clinical_reminder_widget') {
$category_title = xl("Passive Alert");
} elseif ($row['category'] == 'active_reminder_popup') {
$category_title = xl("Active Alert");
} elseif ($row['category'] == 'allergy_alert') {
$category_title = xl("Allergy Warning");
} else {
$category_title = $row['category'];
}

//Prepare the targets
$all_alerts = json_decode($row['value'], true);
if (!empty($row['new_value'])) {
$new_alerts = json_decode($row['new_value'], true);
}
?>
<tr>
<td><?php echo text(oeFormatDateTime($row['date'], "global", true)); ?></td>
<td><?php echo text($row['pid']); ?></td>
<td><?php echo text($row['uid']); ?></td>
<td><?php echo text($category_title); ?></td>
<td>
<?php
//list off all targets with rule information shown when hover
foreach ($all_alerts as $targetInfo => $alert) {
if (($row['category'] == 'clinical_reminder_widget') || ($row['category'] == 'active_reminder_popup')) {
$rule_title = getListItemTitle("clinical_rules", $alert['rule_id']);
$catAndTarget = explode(':', $targetInfo);
$category = $catAndTarget[0];
$target = $catAndTarget[1];
echo "<span title='" . attr($rule_title) . "'>" .
generate_display_field(array('data_type' => '1','list_id' => 'rule_action_category'), $category) .
": " . generate_display_field(array('data_type' => '1','list_id' => 'rule_action'), $target) .
" (" . generate_display_field(array('data_type' => '1','list_id' => 'rule_reminder_due_opt'), $alert['due_status']) . ")" .
"<span><br />";
} else { // $row['category'] == 'allergy_alert'
echo $alert . "<br />";
}
}
?>
</td>
<td>
<?php
if (!empty($row['new_value'])) {
//list new targets with rule information shown when hover
foreach ($new_alerts as $targetInfo => $alert) {
if (($row['category'] == 'clinical_reminder_widget') || ($row['category'] == 'active_reminder_popup')) {
$rule_title = getListItemTitle("clinical_rules", $alert['rule_id']);
$catAndTarget = explode(':', $targetInfo);
$category = $catAndTarget[0];
$target = $catAndTarget[1];
echo "<span title='" . attr($rule_title) . "'>" .
generate_display_field(array('data_type' => '1','list_id' => 'rule_action_category'), $category) .
": " . generate_display_field(array('data_type' => '1','list_id' => 'rule_action'), $target) .
" (" . generate_display_field(array('data_type' => '1','list_id' => 'rule_reminder_due_opt'), $alert['due_status']) . ")" .
"<span><br />";
} else { // $row['category'] == 'allergy_alert'
echo $alert . "<br />";
}
}
} else {
echo "&nbsp;";
}
?>
</td>
</tr>

<?php
} // $row = sqlFetchArray($res) while
?>
</tbody>
</table>
</div> <!-- end of search results -->

<?php } // end of if search button clicked ?>

</form>

</body>

</html>
// Send the normal response
$response->send();
exit;
2 changes: 1 addition & 1 deletion interface/reports/cqm.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
require_once "$srcdir/report_database.inc.php";

use OpenEMR\Common\Acl\AclMain;
use OpenEMR\ClinicialDecisionRules\AMC\CertificationReportTypes;
use OpenEMR\ClinicalDecisionRules\AMC\CertificationReportTypes;
use OpenEMR\Common\Csrf\CsrfUtils;
use OpenEMR\Common\Twig\TwigContainer;
use OpenEMR\Services\PractitionerService;
Expand Down
2 changes: 1 addition & 1 deletion interface/reports/report_results.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
require_once "$srcdir/clinical_rules.php";
require_once "$srcdir/report_database.inc.php";

use OpenEMR\ClinicialDecisionRules\AMC\CertificationReportTypes;
use OpenEMR\ClinicalDecisionRules\AMC\CertificationReportTypes;
use OpenEMR\Common\Acl\AclMain;
use OpenEMR\Common\Csrf\CsrfUtils;
use OpenEMR\Common\Twig\TwigContainer;
Expand Down
12 changes: 10 additions & 2 deletions interface/smart/admin-client.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,18 @@
use OpenEMR\Common\Csrf\CsrfUtils;
use OpenEMR\FHIR\SMART\ClientAdminController;
use OpenEMR\Common\Logging\SystemLogger;
use OpenEMR\Common\Twig\TwigContainer;
use Symfony\Component\HttpFoundation\Request;

$router = new ClientAdminController(new ClientRepository(), new SystemLogger(), 'admin-client.php');
$twig = (new TwigContainer(null, $GLOBALS['kernel']))->getTwig();

$router = new ClientAdminController(new ClientRepository(), new SystemLogger(), $twig, 'admin-client.php');
try {
$router->dispatch(($_REQUEST['action'] ?? null), $_REQUEST);
$request = Request::createFromGlobals();
$response = $router->dispatch($request);
if (isset($response) && $response instanceof \Symfony\Component\HttpFoundation\Response) {
$response->send();
}
} catch (CsrfInvalidException $exception) {
CsrfUtils::csrfNotVerified();
} catch (AccessDeniedException $exception) {
Expand Down
Loading

0 comments on commit cd290ff

Please sign in to comment.