From 2d9fba1b92f323ecb6c4470fa61a39c3b8255bee Mon Sep 17 00:00:00 2001
From: bradymiller
Date: Fri, 26 Nov 2010 01:08:22 -0800
Subject: [PATCH] Interim development of a clinical decision making engine.
Engine depends on rules, filters, targets, and actions. Basically,
if a patient fits in the filter, and the target(s) is false, then the
action(s) happens. The current scheme allows one filter
and multiple targets and actions per rule, is very flexible, and supports
internationalization. Engine is used for clinical decision support,
clinical quality measures (CQM) and patient reminders.
FUNCTIONALITY:
1) Clinical Decision Support widget can be found in the top right
of the patient summary screen.
2) Clinical Reminder widget can be found in bottom left (above vitals
widget) of the patient summary screen.
3) Clinical Reminder page can be found at Administration->Patient
Reminders. The batch to send the messages can be run from this script
by clicking the Send REminder Batch button.
4) CQM report can be found at Reports->Clinic->Quality Measures.
TODO:
1. Finish rules for the 10 CQM rules (will require algorithm adjustments
to deal with the encounter types and frequencies that are used in the
CQM rules).
2. Ensure NIST is accomplished for CQM and clinical decision support.
3. Get the email and voice patient reminder mechanisms to work (may ask
Visolve to help with testing the modified mavix//voice email scripts).
4. Ensure NIST is accomplished for the patient reminders.
5. Add a admin gui to allow per patient customization (note the
mechanism to do this already exist, so just need a simple gui for it)
6. Add mechanism to allow multiple filters per rule.
7. Add a admin gui to allow rule modification and creation of new rules.
8. Add a mechanism for plans.
---
.../batchcom/batch_phone_notification.php | 250 ++++
interface/batchcom/batch_reminders.php | 73 +
interface/main/left_nav.php | 6 +
.../reminder/patient_reminders.php | 242 ++++
interface/patient_file/rules/patient_data.php | 228 ++++
.../summary/clinical_reminders_fragment.php | 27 +
.../patient_file/summary/demographics.php | 52 +-
.../summary/patient_reminders_fragment.php | 27 +
interface/reports/cqm.php | 301 +++++
library/clinical_rules.php | 1172 +++++++++++++++++
library/forms.inc | 27 +-
library/globals.inc.php | 28 +
library/maviq_phone_api.php | 129 ++
library/patient.inc | 19 +-
library/reminders.php | 302 +++++
.../batch_phone_notification.php | 250 ++++
.../sms_email_reminder/batch_reminders.php | 74 ++
sql/database.sql | 725 ++++++++++
18 files changed, 3919 insertions(+), 13 deletions(-)
create mode 100644 interface/batchcom/batch_phone_notification.php
create mode 100644 interface/batchcom/batch_reminders.php
create mode 100644 interface/patient_file/reminder/patient_reminders.php
create mode 100644 interface/patient_file/rules/patient_data.php
create mode 100644 interface/patient_file/summary/clinical_reminders_fragment.php
create mode 100644 interface/patient_file/summary/patient_reminders_fragment.php
create mode 100644 interface/reports/cqm.php
create mode 100644 library/clinical_rules.php
create mode 100644 library/maviq_phone_api.php
create mode 100644 library/reminders.php
create mode 100644 modules/sms_email_reminder/batch_phone_notification.php
create mode 100644 modules/sms_email_reminder/batch_reminders.php
diff --git a/interface/batchcom/batch_phone_notification.php b/interface/batchcom/batch_phone_notification.php
new file mode 100644
index 00000000000..e6dcaadaa8a
--- /dev/null
+++ b/interface/batchcom/batch_phone_notification.php
@@ -0,0 +1,250 @@
+
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+////////////////////////////////////////////////////////////////////
+// Package: cron_phone_notification
+// Purpose: to be run by cron every hour, look for appointments
+// in the pre-notification period and send an phone reminder
+// Based on cron_email_notification by Larry Lart
+// Created by:
+// Updated by: Maviq on 01/12/2010
+////////////////////////////////////////////////////////////////////
+
+$backpic = "";
+//phone notification
+$ignoreAuth=1;
+
+//Set the working directory to the path of the file
+$current_dir = dirname($_SERVER['SCRIPT_FILENAME']);
+chdir($current_dir);
+
+//SANITIZE ALL ESCAPES
+$sanitize_all_escapes=true;
+
+//STOP FAKE REGISTER GLOBALS
+$fake_register_globals=false;
+
+require_once("../../interface/globals.php");
+require_once("$srcdir/maviq_phone_api.php");
+require_once("$srcdir/formdata.inc.php");
+
+$type = "Phone";
+$before_trigger_hours = 72; // 3 days is default
+//Get the values from Global
+$before_trigger_hours = $GLOBALS['phone_notification_hour'];
+//set up the phone notification settings for external phone service
+$phone_url = $GLOBALS['phone_gateway_url'] ;
+$phone_id = $GLOBALS['phone_gateway_username'];
+$phone_token = $GLOBALS['phone_gateway_password'];
+$phone_time_range = $GLOBALS['phone_time_range'];
+
+//get the facility_id-message map
+$facilities = cron_getFacilitiesMap();
+//print_r($facilities);
+$fac_phone_map = $facilities['phone_map'];
+$fac_msg_map = $facilities['msg_map'];
+
+// get patient data for send alert
+$db_patient = cron_getPhoneAlertpatientData($type, $before_trigger_hours);
+echo " " . htmlspecialchars( xl("Total Records Found") . ": " . count($db_patient), ENT_QUOTES);
+
+//Create a new instance of the phone service client
+$client = new MaviqClient($phone_id, $phone_token, $phone_url);
+
+for($p=0;$p $prow['fname'],
+ "lastName" => $prow['lname'],
+ "phone" => $prow['phone_home'],
+ "apptDate" => $appt_date,
+ "apptTime" => $appt_time,
+ "doctor" => $prow['pc_aid'],
+ "greeting" => $greeting,
+ "timeRange" => $phone_time_range,
+ "type" => "appointment",
+ "timeZone" => date('P'),
+ "callerId" => $fac_phone_map[$prow['pc_facility']]
+ );
+
+ //Make the call
+ $response = $client->sendRequest("appointment", "POST", $data);
+
+ // check response for success or error
+ if($response->IsError) {
+ $strMsg = "Error starting phone call for {$prow['fname']} | {$prow['lname']} | {$prow['phone_home']} | {$appt_date} | {$appt_time} | {$response->ErrorMessage}\n";
+ }
+ else {
+ $strMsg = "\n========================".$type." || ".date("Y-m-d H:i:s")."=========================";
+ $strMsg .= "\nPhone reminder sent successfully: {$prow['fname']} | {$prow['lname']} | | {$prow['phone_home']} | {$appt_date} | {$appt_time} ";
+ // insert entry in notification_log table
+ cron_InsertNotificationLogEntry($prow,$greeting,$phone_url);
+
+ //update entry >> pc_sendalertsms='Yes'
+ cron_updateentry($type,$prow['pid'],$prow['pc_eid']);
+
+ }
+
+ //echo $strMsg;
+ WriteLog( $strMsg );
+
+}
+
+sqlClose();
+
+////////////////////////////////////////////////////////////////////
+// Function: cron_updateentry
+// Purpose: update status yes if alert send to patient
+////////////////////////////////////////////////////////////////////
+function cron_updateentry($type,$pid,$pc_eid)
+{
+
+ $query = "update openemr_postcalendar_events set ";
+
+ // larry :: and here again same story - this time for sms pc_sendalertsms - no such field in the table
+ if($type=='SMS')
+ $query.=" pc_sendalertsms='YES' ";
+ elseif ($type=='Email')
+ $query.=" pc_sendalertemail='YES' ";
+ //Added by Yijin for phone reminder.. Uses the same field as SMS.
+ elseif($type=='Phone')
+ $query.=" pc_sendalertsms='YES' ";
+
+ $query .=" where pc_pid=? and pc_eid=? ";
+ //echo " ".$query;
+ $db_sql = (sqlStatement($query, array($pid, $pc_eid)));
+}
+
+////////////////////////////////////////////////////////////////////
+// Function: cron_getPhoneAlertpatientData
+// Purpose: get patient data for send to alert
+////////////////////////////////////////////////////////////////////
+function cron_getPhoneAlertpatientData( $type, $trigger_hours )
+{
+
+ //Added by Yijin 1/12/10 to handle phone reminders. Patient needs to have hipaa Voice flag set to yes and a home phone
+ if($type=='Phone'){
+ $ssql = " and pd.hipaa_voice='YES' and pd.phone_home<>'' and ope.pc_sendalertsms='NO' and ope.pc_apptstatus != '*' ";
+
+ $check_date = date("Y-m-d", mktime(date("H")+$trigger_hours, 0, 0, date("m"), date("d"), date("Y")));
+
+ }
+
+ $patient_field = "pd.pid,pd.title,pd.fname,pd.lname,pd.mname,pd.phone_cell,pd.email,pd.hipaa_allowsms,pd.hipaa_allowemail,pd.phone_home,pd.hipaa_voice,";
+ $ssql .= " and (ope.pc_eventDate=?)";
+
+ $query = "select $patient_field pd.pid,ope.pc_eid,ope.pc_pid,ope.pc_title,
+ ope.pc_hometext,ope.pc_eventDate,ope.pc_endDate,
+ ope.pc_duration,ope.pc_alldayevent,ope.pc_startTime,ope.pc_endTime,ope.pc_facility
+ from
+ openemr_postcalendar_events as ope ,patient_data as pd
+ where
+ ope.pc_pid=pd.pid $ssql
+ order by
+ ope.pc_eventDate,ope.pc_endDate,pd.pid";
+
+ $db_patient = (sqlStatement($query) , array($check_date) );
+ $patient_array = array();
+ $cnt=0;
+ while ($prow = sqlFetchArray($db_patient))
+ {
+ $patient_array[$cnt] = $prow;
+ $cnt++;
+ }
+ return $patient_array;
+}
+
+////////////////////////////////////////////////////////////////////
+// Function: cron_InsertNotificationLogEntry
+// Purpose: insert log entry in table
+////////////////////////////////////////////////////////////////////
+function cron_InsertNotificationLogEntry($prow,$phone_msg,$phone_gateway)
+{
+ $patient_info = $prow['title']." ".$prow['fname']." ".$prow['mname']." ".$prow['lname']."|||".$prow['phone_home'];
+
+ $message = $phone_msg;
+
+ $sql_loginsert = "INSERT INTO `notification_log` ( `iLogId` , `pid` , `pc_eid` , `message`, `type` , `patient_info` , `smsgateway_info` , `pc_eventDate` , `pc_endDate` , `pc_startTime` , `pc_endTime` , `dSentDateTime` ) VALUES ";
+ $sql_loginsert .= "(NULL , ?, ?, ?, 'Phone', ?, ?, ?, ?, ?, ?, ?)";
+ $db_loginsert = ( sqlStatement( $sql_loginsert ), array($prow[pid], $prow[pc_eid], $message, $patient_info, $phone_gateway, $prow[pc_eventDate], $prow[pc_endDate], $prow[pc_startTime], $prow[pc_endTime], date("Y-m-d H:i:s")) );
+}
+
+////////////////////////////////////////////////////////////////////
+// Function: WriteLog
+// Purpose: written log into file
+////////////////////////////////////////////////////////////////////
+function WriteLog( $data )
+{
+ $log_file = $GLOBALS['phone_reminder_log_dir'];
+
+ if ($log_file != null) {
+
+ $filename = $log_file . "/"."phone_reminder_cronlog_".date("Ymd").".html";
+
+ if (!$fp = fopen($filename, 'a'))
+ {
+ print "Cannot open file ($filename)";
+
+ }else {
+
+ $sdata = "\n====================================================================\n";
+
+ if (!fwrite($fp, $data.$sdata))
+ {
+ print "Cannot write to file ($filename)";
+ }
+
+ fclose($fp);
+ }
+ }
+}
+////////////////////////////////////////////////////////////////////
+// Function: cron_getFacilities
+// Purpose: get facilities data once and store in map
+////////////////////////////////////////////////////////////////////
+function cron_getFacilitiesMap()
+{
+ //get the facility_name-message map from Globals
+ $message_map = $GLOBALS['phone_appt_message'];
+ //create a new array to store facility_id to message map
+ $facility_msg_map = array();
+ $facility_phone_map = array();
+ //get facilities from the database
+ $query = "select fac.id, fac.name, fac.phone from facility as fac";
+ $db_res = (sqlStatement($query));
+ while ($prow = sqlFetchArray($db_res))
+ {
+ $facility_msg_map[$prow['id']] = $message_map[$prow['name']];
+ $facility_phone_map[$prow['id']] = $prow['phone'];
+ }
+
+ $facility_map = array(
+ 'msg_map' => $facility_msg_map,
+ 'phone_map' => $facility_phone_map
+ );
+
+ return $facility_map;
+
+}
+?>
+
diff --git a/interface/batchcom/batch_reminders.php b/interface/batchcom/batch_reminders.php
new file mode 100644
index 00000000000..3ef9ac62dfb
--- /dev/null
+++ b/interface/batchcom/batch_reminders.php
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/interface/main/left_nav.php b/interface/main/left_nav.php
index e1694269140..5e553854022 100644
--- a/interface/main/left_nav.php
+++ b/interface/main/left_nav.php
@@ -990,6 +990,7 @@ function selpopup(selobj) {
+
@@ -1015,6 +1016,11 @@ function selpopup(selobj) {
+
+
+
diff --git a/interface/patient_file/reminder/patient_reminders.php b/interface/patient_file/reminder/patient_reminders.php
new file mode 100644
index 00000000000..8128778fa2a
--- /dev/null
+++ b/interface/patient_file/reminder/patient_reminders.php
@@ -0,0 +1,242 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+" .
+ " ";
+}
+for($i = 0; $i < count($sort); $i++) {
+ if($sortby == $sort[$i]) {
+ switch($sortorder) {
+ case "asc" : $sortlink[$i] = "" .
+ " ";
+ break;
+ case "desc" : $sortlink[$i] = "" .
+ " ";
+ break;
+ } break;
+ }
+}
+// This is for managing page numbering and display beneath the Patient Reminders table.
+$listnumber = 25;
+$sqlBindArray = array();
+if (!empty($patient_id)) {
+ $add_sql = "AND a.pid=? ";
+ array_push($sqlBindArray,$patient_id);
+}
+$sql = "SELECT a.id, a.due_status, a.category, a.item, a.date_created, a.date_sent, b.fname, b.lname " .
+ "FROM `patient_reminders` as a, `patient_data` as b " .
+ "WHERE a.active='1' AND a.pid=b.pid ".$add_sql;
+$result = sqlStatement($sql, $sqlBindArray);
+if(sqlNumRows($result) != 0) {
+ $total = sqlNumRows($result);
+}
+else {
+ $total = 0;
+}
+if($begin == "" or $begin == 0) {
+ $begin = 0;
+}
+$prev = $begin - $listnumber;
+$next = $begin + $listnumber;
+$start = $begin + 1;
+$end = $listnumber + $start - 1;
+if($end >= $total) {
+ $end = $total;
+}
+if($end < $start) {
+ $start = 0;
+}
+if($prev >= 0) {
+ $prevlink = "<< ";
+}
+else {
+ $prevlink = "<<";
+}
+
+if($next < $total) {
+ $nextlink = ">> ";
+}
+else {
+ $nextlink = ">>";
+}
+?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ '1','list_id'=>'rule_action_category'),$myrow['category']) . " : " .
+ generate_display_field(array('data_type'=>'1','list_id'=>'rule_action'),$myrow['item']); ?>
+
+ '1','list_id'=>'rule_reminder_due_opt'),$myrow['due_status']); ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/interface/patient_file/rules/patient_data.php b/interface/patient_file/rules/patient_data.php
new file mode 100644
index 00000000000..72ceff014a7
--- /dev/null
+++ b/interface/patient_file/rules/patient_data.php
@@ -0,0 +1,228 @@
+
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+
+//SANITIZE ALL ESCAPES
+$sanitize_all_escapes=true;
+//
+
+//STOP FAKE REGISTER GLOBALS
+$fake_register_globals=false;
+//
+
+require_once("../../globals.php");
+require_once("$srcdir/acl.inc");
+require_once("$srcdir/options.inc.php");
+
+?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+(" . htmlspecialchars( xl('Not authorized'), ENT_NOQUOTES) . ")
\n";
+ echo "\n\n";
+ exit();
+}
+
+if ($_POST['form_complete']) {
+ // Save that form as a row in rule_patient_data table
+ // and then close the window/modul.
+
+ // Collect and trim variables
+ if (isset($_POST['form_entryID'])) $form_entryID = trim($_POST['form_entryID']);
+ $form_date = trim($_POST['form_date']);
+ $form_category = trim($_POST['form_category']);
+ $form_item = trim($_POST['form_item']);
+ $form_complete = trim($_POST['form_complete']);
+ $form_result = trim($_POST['form_result']);
+
+ if (!isset($form_entryID)) {
+ // Insert new row of data into rule_patient_data table
+ sqlInsert("INSERT INTO `rule_patient_data` (`date`, `pid`, `category`, `item`, `complete`, `result`) " .
+ "VALUES (?,?,?,?,?,?)", array($form_date, $pid, $form_category, $form_item, $form_complete, $form_result) );
+ }
+ else { // $form_mode == "edit"
+ // Modify selected row in rule_patient_data table
+ sqlStatement("UPDATE `rule_patient_data` " .
+ "SET `date`=?, `complete`=?, `result`=? " .
+ "WHERE `id`=?", array($form_date,$form_complete,$form_result,$form_entryID) );
+ }
+
+ // Close this window and refresh the patient summary display.
+ echo "\n\n\n\n\n";
+ exit();
+}
+
+// Display the form
+// Collect and trim variables
+$category = trim($_GET['category']);
+$item = trim($_GET['item']);
+if (isset($_GET['entryID'])) $entryID = trim($_GET['entryID']);
+
+// Collect data if a specific entry is selected
+if (isset($entryID)) {
+ $selectedEntry = sqlQuery("SELECT `date`, `complete`, `result` " .
+ "FROM `rule_patient_data` " .
+ "WHERE `id`=?", array($entryID) );
+ $form_date = $selectedEntry['date'];
+ $form_complete = $selectedEntry['complete'];
+ $form_result = $selectedEntry['result'];
+}
+
+?>
+
+
+'1','list_id'=>'rule_action_category'),$category) .
+" - " . generate_display_field(array('data_type'=>'1','list_id'=>'rule_action'),$item); ?>
+
+
+
+
+
+
+
+
+
+
+
+
+= 1) { //display table ?>
+
+
+
+
+
+
+
+ ";
+ }
+ else {
+ echo "";
+ }
+ if (isset($entryID) && ($entryID == $row['id'])) {
+ // hide the edit button
+ echo " ";
+ }
+ else { // show the edit button
+ echo "" .
+ "" . htmlspecialchars( xl('Edit'), ENT_NOQUOTES) . " " .
+ " ";
+ }
+ echo "" . htmlspecialchars( $row['date'], ENT_NOQUOTES) . " ";
+ echo "" . htmlspecialchars( $row['complete'], ENT_NOQUOTES) . " ";
+ echo "" . nl2br( htmlspecialchars( $row['result'], ENT_NOQUOTES) ) . " ";
+ echo " ";
+ } ?>
+
+" . htmlspecialchars( xl('No previous entries.'), ENT_NOQUOTES) . "";
+} ?>
+
+
+