4 * Clinical Decision Rules(CDR) engine functions.
7 * @link https://www.open-emr.org
8 * @author Brady Miller <brady.g.miller@gmail.com>
9 * @author Medical Information Integration, LLC
10 * @author Ensofttek, LLC
11 * @copyright Copyright (c) 2010-2019 Brady Miller <brady.g.miller@gmail.com>
12 * @copyright Copyright (c) 2011 Medical Information Integration, LLC
13 * @copyright Copyright (c) 2011 Ensofttek, LLC
14 * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
17 require_once(dirname(__FILE__
) . "/patient.inc");
18 require_once(dirname(__FILE__
) . "/forms.inc");
19 require_once(dirname(__FILE__
) . "/options.inc.php");
20 require_once(dirname(__FILE__
) . "/report_database.inc");
22 use OpenEMR\Common\Acl\AclMain
;
23 use OpenEMR\ClinicialDecisionRules\AMC\CertificationReportTypes
;
24 use OpenEMR\Common\Logging\SystemLogger
;
25 use OpenEMR\Services\FacilityService
;
28 * Return listing of CDR reminders in log.
30 * @param string $begin_date begin date (optional)
31 * @param string $end_date end date (optional)
32 * @return sqlret sql return query
34 function listingCDRReminderLog($begin_date = '', $end_date = '')
37 if (empty($end_date)) {
38 $end_date = date('Y-m-d H:i:s');
42 $sql = "SELECT `date`, `pid`, `uid`, `category`, `value`, `new_value` FROM `clinical_rules_log` WHERE `date` <= ?";
43 $sqlArray[] = $end_date;
44 if (!empty($begin_date)) {
45 $sql .= " AND `date` >= ?";
46 $sqlArray[] = $begin_date;
49 $sql .= " ORDER BY `date` DESC";
51 return sqlStatement($sql, $sqlArray);
55 * Display the clinical summary widget.
57 * @param integer $patient_id pid of selected patient
58 * @param string $mode choose either 'reminders-all' or 'reminders-due' (required)
59 * @param string $dateTarget target date (format Y-m-d H:i:s). If blank then will test with current date as target.
60 * @param string $organize_mode Way to organize the results (default or plans)
61 * @param string $user If a user is set, then will only show rules that user has permission to see.
63 function clinical_summary_widget($patient_id, $mode, $dateTarget = '', $organize_mode = 'default', $user = '')
66 // Set date to current if not set
67 $dateTarget = ($dateTarget) ?
$dateTarget : date('Y-m-d H:i:s');
69 // Collect active actions
70 $actions = test_rules_clinic('', 'passive_alert', $dateTarget, $mode, $patient_id, '', $organize_mode, array(), 'primary', null, null, $user);
72 // Display the actions
73 $current_targets = array();
74 echo "<div class=\"list-group list-group-flush\">";
75 foreach ($actions as $action) {
76 // Deal with plan names first
77 if (isset($action['is_plan']) && $action['is_plan']) {
79 echo xlt("Plan") . ": ";
80 echo generate_display_field(array('data_type' => '1','list_id' => 'clinical_plans'), $action['id']);
85 echo "<div class=\"list-group-item p-1 d-flex w-100 justify-content-between\">";
87 // Collect the Rule Title, Bibliographical citation, Rule Developer, Rule Funding Source, and Rule Release and show it when hover over the item.
88 // Show the link for Linked referential CDS (this is set via codetype:code)
90 if (!empty($action['rule_id'])) {
91 $rule_title = getListItemTitle("clinical_rules", $action['rule_id']);
92 $ruleData = sqlQuery("SELECT `bibliographic_citation`, `developer`, `funding_source`, `release_version`, `web_reference`, `linked_referential_cds` " .
93 "FROM `clinical_rules` " .
94 "WHERE `id`=? AND `pid`=0", array($action['rule_id']));
95 $bibliographic_citation = $ruleData['bibliographic_citation'];
96 $developer = $ruleData['developer'];
97 $funding_source = $ruleData['funding_source'];
98 $release = $ruleData['release_version'];
99 $web_reference = $ruleData['web_reference'];
100 $linked_referential_cds = $ruleData['linked_referential_cds'];
101 if (!empty($rule_title)) {
102 $tooltip = xla('Rule Title') . ": " . attr($rule_title) . "
";
105 if (!empty($bibliographic_citation)) {
106 $tooltip .= xla('Rule Bibliographic Citation') . ": " . attr($bibliographic_citation) . "
";
109 if (!empty($developer)) {
110 $tooltip .= xla('Rule Developer') . ": " . attr($developer) . "
";
113 if (!empty($funding_source)) {
114 $tooltip .= xla('Rule Funding Source') . ": " . attr($funding_source) . "
";
117 if (!empty($release)) {
118 $tooltip .= xla('Rule Release') . ": " . attr($release);
121 if ((!empty($tooltip)) ||
(!empty($web_reference))) {
122 if (!empty($web_reference)) {
123 $tooltip = "<a href='" . attr($web_reference) . "' rel='noopener' target='_blank' style='white-space: pre-line;' title='" . $tooltip . "'><i class='fas fa-question-circle'></i></a>";
125 $tooltip = "<span style='white-space: pre-line;' title='" . $tooltip . "'><i class='fas fa-question-circle'></i></span>";
129 if (!empty($linked_referential_cds)) {
130 $codeParse = explode(":", $linked_referential_cds);
131 $codetype = $codeParse[0] ??
null;
132 $code = $codeParse[1] ??
null;
133 if (!empty($codetype) && !empty($code)) {
134 $tooltip .= "<a href='' title='" . xla('Link to Referential CDS') . "' onclick='referentialCdsClick(" . attr_js($codetype) . ", " . attr_js($code) . ")'><i class='fas fa-external-link-square-alt'></i></a>";
139 if ($action['custom_flag']) {
140 // Start link for reminders that use the custom rules input screen
141 $url = "../rules/patient_data.php?category=" . attr_url($action['category']);
142 $url .= "&item=" . attr_url($action['item']);
143 echo "<a href='" . $url . "' class='medium_modal' onclick='return top.restoreSession()'>";
144 } elseif ($action['clin_rem_link']) {
145 // Start link for reminders that use the custom rules input screen
146 $pieces_url = parse_url($action['clin_rem_link']);
147 $url_prefix = $pieces_url['scheme'];
148 if ($url_prefix == 'https' ||
$url_prefix == 'http') {
149 echo "<a href='" . $action['clin_rem_link'] .
150 "' class='medium_modal' onclick='return top.restoreSession()'>";
152 echo "<a href='../../../" . $action['clin_rem_link'] .
153 "' class='medium_modal' onclick='return top.restoreSession()'>";
156 // continue since no link is needed
159 // Display Reminder Details
160 echo generate_display_field(array('data_type' => '1','list_id' => 'rule_action_category'), $action['category']) .
161 ": " . generate_display_field(array('data_type' => '1','list_id' => 'rule_action'), $action['item']);
163 if ($action['custom_flag'] ||
$action['clin_rem_link']) {
164 // End link for reminders that use an html link
168 // Display due status
169 if ($action['due_status']) {
170 // Color code the status (red for past due, purple for due, green for not due and black for soon due)
171 if ($action['due_status'] == "past_due") {
172 echo "<span class='text-danger'>";
173 } elseif ($action['due_status'] == "due") {
174 echo "<span class='text-warning'>";
175 } elseif ($action['due_status'] == "not_due") {
176 echo "<span class='text-success>";
181 echo generate_display_field(array('data_type' => '1','list_id' => 'rule_reminder_due_opt'), $action['due_status']);
184 // Display the tooltip
185 if (!empty($tooltip)) {
186 echo " {$tooltip}";
191 // Add the target(and rule id and room for future elements as needed) to the $current_targets array.
192 // Only when $mode is reminders-due
193 if ($mode == "reminders-due" && $GLOBALS['enable_alert_log']) {
194 $target_temp = $action['category'] . ":" . $action['item'];
195 $current_targets[$target_temp] = array('rule_id' => $action['rule_id'],'due_status' => $action['due_status']);
201 // Compare the current with most recent action log (this function will also log the current actions)
202 // Only when $mode is reminders-due
203 if ($mode == "reminders-due" && $GLOBALS['enable_alert_log']) {
204 $new_targets = compare_log_alerts($patient_id, $current_targets, 'clinical_reminder_widget', $_SESSION['authUserID']);
205 if (!empty($new_targets) && $GLOBALS['enable_cdr_new_crp']) {
206 // If there are new action(s), then throw a popup (if the enable_cdr_new_crp global is turned on)
207 // Note I am taking advantage of a slight hack in order to run javascript within code that
208 // is being passed via an ajax call by using a dummy image.
209 echo '<img src="../../pic/empty.gif" onload="alert(\'' . xls('New Due Clinical Reminders') . '\n\n';
210 foreach ($new_targets as $key => $value) {
211 $category_item = explode(":", $key);
212 $category = $category_item[0];
213 $item = $category_item[1];
214 echo generate_display_field(array('data_type' => '1','list_id' => 'rule_action_category'), $category) .
215 ': ' . generate_display_field(array('data_type' => '1','list_id' => 'rule_action'), $item) . '\n';
218 echo '\n' . '(' . xls('See the Clinical Reminders widget for more details') . ')';
219 echo '\');this.parentNode.removeChild(this);" />';
225 * Display the active screen reminder.
227 * @param integer $patient_id pid of selected patient
228 * @param string $mode choose either 'reminders-all' or 'reminders-due' (required)
229 * @param string $dateTarget target date (format Y-m-d H:i:s). If blank then will test with current date as target.
230 * @param string $organize_mode Way to organize the results (default or plans)
231 * @param string $user If a user is set, then will only show rules that user has permission to see
232 * @param string $test Set to true when only checking if there are alerts (skips the logging then)
233 * @return string html display output.
235 function active_alert_summary($patient_id, $mode, $dateTarget = '', $organize_mode = 'default', $user = '', $test = false)
238 // Set date to current if not set
239 $dateTarget = ($dateTarget) ?
$dateTarget : date('Y-m-d H:i:s');
241 // Collect active actions
242 $actions = test_rules_clinic('', 'active_alert', $dateTarget, $mode, $patient_id, '', $organize_mode, array(), 'primary', null, null, $user);
244 if (empty($actions)) {
249 $current_targets = array();
251 // Display the actions
252 foreach ($actions as $action) {
253 // Deal with plan names first
254 if ($action['is_plan']) {
255 $returnOutput .= "<br /><b>";
256 $returnOutput .= xlt("Plan") . ": ";
257 $returnOutput .= generate_display_field(array('data_type' => '1','list_id' => 'clinical_plans'), $action['id']);
258 $returnOutput .= "</b><br />";
262 // Display Reminder Details
263 $returnOutput .= generate_display_field(array('data_type' => '1','list_id' => 'rule_action_category'), $action['category']) .
264 ": " . generate_display_field(array('data_type' => '1','list_id' => 'rule_action'), $action['item']);
266 // Display due status
267 if ($action['due_status']) {
268 // Color code the status (red for past due, purple for due, green for not due and black for soon due)
269 if ($action['due_status'] == "past_due") {
270 $returnOutput .= " (<span style='color:red'>";
271 } elseif ($action['due_status'] == "due") {
272 $returnOutput .= " (<span style='color:purple'>";
273 } elseif ($action['due_status'] == "not_due") {
274 $returnOutput .= " (<span style='color:green'>";
276 $returnOutput .= " (<span>";
279 $returnOutput .= generate_display_field(array('data_type' => '1','list_id' => 'rule_reminder_due_opt'), $action['due_status']) . "</span>)<br />";
281 $returnOutput .= "<br />";
284 // Add the target(and rule id and room for future elements as needed) to the $current_targets array.
285 // Only when $mode is reminders-due and $test is FALSE
286 if (($mode == "reminders-due") && ($test === false) && ($GLOBALS['enable_alert_log'])) {
287 $target_temp = $action['category'] . ":" . $action['item'];
288 $current_targets[$target_temp] = array('rule_id' => $action['rule_id'],'due_status' => $action['due_status']);
292 // Compare the current with most recent action log (this function will also log the current actions)
293 // Only when $mode is reminders-due and $test is FALSE
294 if (($mode == "reminders-due") && ($test === false) && ($GLOBALS['enable_alert_log'])) {
295 $new_targets = compare_log_alerts($patient_id, $current_targets, 'active_reminder_popup', $_SESSION['authUserID']);
296 if (!empty($new_targets)) {
297 $returnOutput .= "<br />" . xlt('New Items (see above for details)') . ":<br />";
298 foreach ($new_targets as $key => $value) {
299 $category_item = explode(":", $key);
300 $category = $category_item[0];
301 $item = $category_item[1];
302 $returnOutput .= generate_display_field(array('data_type' => '1','list_id' => 'rule_action_category'), $category) .
303 ': ' . generate_display_field(array('data_type' => '1','list_id' => 'rule_action'), $item) . '<br />';
308 return $returnOutput;
312 * Process and return allergy conflicts (when a active medication or presciption is on allergy list).
314 * @param integer $patient_id pid of selected patient
315 * @param string $mode either 'all' or 'new' (required)
316 * @param string $user If a user is set, then will only show rules that user has permission to see
317 * @param string $test Set to true when only checking if there are alerts (skips the logging then)
318 * @return array/boolean Array of allergy alerts or FALSE is empty.
320 function allergy_conflict($patient_id, $mode, $user, $test = false)
325 $sqlParam[] = $patient_id;
326 $res_allergies = sqlStatement("SELECT `title` FROM `lists` WHERE `type`='allergy' " .
327 "AND `activity`=1 " .
329 dateEmptySql('enddate') .
330 "OR `enddate` > NOW() ) " .
331 "AND `pid`=?", $sqlParam);
332 $allergies = array();
333 for ($iter = 0; $row = sqlFetchArray($res_allergies); $iter++
) {
334 $allergies[$iter] = $row['title'];
337 // Build sql element of IN for below queries
341 foreach ($allergies as $allergy) {
342 $sqlParam[] = $allergy;
351 // Check if allergies conflict with medications or prescriptions
352 $conflicts_unique = array();
353 if (!empty($sqlParam)) {
354 $conflicts = array();
355 $sqlParam[] = $patient_id;
356 $res_meds = sqlStatement("SELECT `title` FROM `lists` WHERE `type`='medication' " .
357 "AND `activity`=1 " .
359 dateEmptySql('enddate') .
360 "OR `enddate` > NOW() )" .
361 "AND `title` IN (" . $sqlIN . ") AND `pid`=?", $sqlParam);
362 while ($urow = sqlFetchArray($res_meds)) {
363 $conflicts[] = $urow['title'];
366 $res_rx = sqlStatement("SELECT `drug` FROM `prescriptions` WHERE `active`=1 " .
367 "AND `drug` IN (" . $sqlIN . ") AND `patient_id`=?", $sqlParam);
368 while ($urow = sqlFetchArray($res_rx)) {
369 $conflicts[] = $urow['drug'];
372 if (!empty($conflicts)) {
373 $conflicts_unique = array_unique($conflicts);
377 // If there are conflicts, $test is FALSE, and alert logging is on, then run through compare_log_alerts
378 $new_conflicts = array();
379 if ((!empty($conflicts_unique)) && $GLOBALS['enable_alert_log'] && ($test === false)) {
380 $new_conflicts = compare_log_alerts($patient_id, $conflicts_unique, 'allergy_alert', $_SESSION['authUserID'], $mode);
383 if ($mode == 'all') {
384 if (!empty($conflicts_unique)) {
385 return $conflicts_unique;
389 } else { // $mode = 'new'
390 if (!empty($new_conflicts)) {
391 return $new_conflicts;
399 * Compare current alerts with prior (in order to find new actions)
400 * Also functions to log the actions.
402 * @param integer $patient_id pid of selected patient
403 * @param array $current_targets array of targets
404 * @param string $category clinical_reminder_widget, active_reminder_popup, or allergy_alert
405 * @param integer $userid user id of user.
406 * @param string $log_trigger if 'all', then always log. If 'new', then only trigger log when a new item noted.
407 * @return array array with targets with associated rule.
409 function compare_log_alerts($patient_id, $current_targets, $category = 'clinical_reminder_widget', $userid = '', $log_trigger = 'all')
412 if (empty($userid)) {
413 $userid = $_SESSION['authUserID'];
416 if (empty($current_targets)) {
417 $current_targets = array();
420 // Collect most recent action_log
421 $prior_targets_sql = sqlQuery("SELECT `value` FROM `clinical_rules_log` " .
422 "WHERE `category` = ? AND `pid` = ? AND `uid` = ? " .
423 "ORDER BY `id` DESC LIMIT 1", array($category,$patient_id,$userid));
424 $prior_targets = array();
425 if (!empty($prior_targets_sql['value'])) {
426 $prior_targets = json_decode($prior_targets_sql['value'], true);
429 // Compare the current with most recent log
430 if (($category == 'clinical_reminder_widget') ||
($category == 'active_reminder_popup')) {
431 //using fancy structure to store multiple elements
432 $new_targets = array_diff_key($current_targets, $prior_targets);
433 } else { // $category == 'allergy_alert'
435 $new_targets = array_diff($current_targets, $prior_targets);
438 // Store current action_log and the new items
439 // If $log_trigger=='all'
440 // or If $log_trigger=='new' and there are new items
441 if (($log_trigger == 'all') ||
(($log_trigger == 'new') && (!empty($new_targets)))) {
442 $current_targets_json = json_encode($current_targets);
443 $new_targets_json = '';
444 if (!empty($new_targets)) {
445 $new_targets_json = json_encode($new_targets);
448 sqlStatement("INSERT INTO `clinical_rules_log` " .
449 "(`date`,`pid`,`uid`,`category`,`value`,`new_value`) " .
450 "VALUES (NOW(),?,?,?,?,?)", array($patient_id,$userid,$category,$current_targets_json,$new_targets_json));
453 // Return new actions (if there are any)
458 * Process clinic rules via a batching method to improve performance and decrease memory overhead.
460 * Test the clinic rules of entire clinic and create a report or patient reminders (can also test
461 * on one patient or patients of one provider). The structure of the returned results is dependent on the
462 * $organize_mode and $mode parameters.
463 * <pre>The results are dependent on the $organize_mode parameter settings
464 * 'default' organize_mode:
465 * Returns a two-dimensional array of results organized by rules (dependent on the following $mode settings):
466 * 'reminders-due' mode - returns an array of reminders (action array elements plus a 'pid' and 'due_status')
467 * 'reminders-all' mode - returns an array of reminders (action array elements plus a 'pid' and 'due_status')
468 * 'report' mode - returns an array of rows for the Clinical Quality Measures (CQM) report
469 * 'plans' organize_mode:
470 * Returns similar to default, but organizes by the active plans
473 * @param integer $provider id of a selected provider. If blank, then will test entire clinic. If 'collate_outer' or 'collate_inner', then will test each provider in entire clinic; outer will nest plans inside collated providers, while inner will nest the providers inside the plans (note inner and outer are only different if organize_mode is set to plans).
474 * @param string $type rule filter (active_alert,passive_alert,cqm,cqm_2011,cqm_2014,amc,amc_2011,amc_2014,patient_reminder). If blank then will test all rules.
475 * @param string/array $dateTarget target date (format Y-m-d H:i:s). If blank then will test with current date as target. If an array, then is holding two dates ('dateBegin' and 'dateTarget').
476 * @param string $mode choose either 'report' or 'reminders-all' or 'reminders-due' (required)
477 * @param string $plan test for specific plan only
478 * @param string $organize_mode Way to organize the results (default, plans). See above for organization structure of the results.
479 * @param array $options can hold various option (for now, used to hold the manual number of labs for the AMC report)
480 * @param string $pat_prov_rel How to choose patients that are related to a chosen provider. 'primary' selects patients that the provider is set as primary provider. 'encounter' selectes patients that the provider has seen. This parameter is only applicable if the $provider parameter is set to a provider or collation setting.
481 * @param integer $batchSize number of patients to batch (default is 100; plan to optimize this default setting in the future)
482 * @param integer $report_id id of report in database (if already bookmarked)
483 * @return array See above for organization structure of the results.
485 function test_rules_clinic_batch_method($provider = '', $type = '', $dateTarget = '', $mode = '', $plan = '', $organize_mode = 'default', $options = array(), $pat_prov_rel = 'primary', $batchSize = '', $report_id = null)
488 // Default to a batchsize, if empty
489 if (empty($batchSize)) {
493 // Collect total number of pertinent patients (to calculate batching parameters)
494 // note for group_calculation we will have some inefficiencies here
495 $totalNumPatients = (int)buildPatientArray('', $provider, $pat_prov_rel, null, null, true);
497 // Cycle through the batches and collect/combine results
498 if (($totalNumPatients %
$batchSize) > 0) {
499 // not perfectly divisible
500 $totalNumberBatches = floor($totalNumPatients / $batchSize) +
1;
502 // perfectly divisible
503 $totalNumberBatches = floor($totalNumPatients / $batchSize);
506 (new SystemLogger())->debug(
507 "test_rules_clinic_batch_method()",
508 ['totalNumPatients' => $totalNumPatients, 'totalNumberBatches' => $totalNumberBatches]
511 // Fix things in the $options array(). This now stores the number of labs to be used in the denominator in the AMC report.
512 // The problem with this variable is that is is added in every batch. So need to fix it by dividing this number by the number
513 // of planned batches(note the fixed array will go into the test_rules_clinic function, however the original will be used
514 // in the report storing/tracking engine.
515 $options_modified = $options;
516 if (!empty($options_modified['labs_manual'])) {
517 $options_modified['labs_manual'] = $options_modified['labs_manual'] / $totalNumberBatches;
520 // Prepare the database to track/store results
521 $fields = array('provider' => $provider,'mode' => $mode,'plan' => $plan,'organize_mode' => $organize_mode,'pat_prov_rel' => $pat_prov_rel);
522 if (is_array($dateTarget)) {
523 $fields = array_merge($fields, array('date_target' => $dateTarget['dateTarget']));
524 $fields = array_merge($fields, array('date_begin' => $dateTarget['dateBegin']));
526 if (empty($dateTarget)) {
527 $fields = array_merge($fields, array('date_target' => date("Y-m-d H:i:s")));
529 $fields = array_merge($fields, array('date_target' => $dateTarget));
533 if (!empty($options)) {
534 foreach ($options as $key => $value) {
535 $fields = array_merge($fields, array($key => $value));
539 $report_id = beginReportDatabase($type, $fields, $report_id);
540 setTotalItemsReportDatabase($report_id, $totalNumPatients);
542 // Set ability to itemize report if this feature is turned on
544 ( ($type == "active_alert" ||
$type == "passive_alert") && ($GLOBALS['report_itemizing_standard']) ) ||
545 ( ($type == "cqm" ||
$type == "cqm_2011" ||
$type == "cqm_2014") && ($GLOBALS['report_itemizing_cqm']) ) ||
546 ( (CertificationReportTypes
::isAMCReportType($type)) && ($GLOBALS['report_itemizing_amc']) )
548 $GLOBALS['report_itemizing_temp_flag_and_id'] = $report_id;
550 $GLOBALS['report_itemizing_temp_flag_and_id'] = 0;
553 for ($i = 0; $i < $totalNumberBatches; $i++
) {
554 // If itemization is turned on, then reset the rule id iterator
555 if ($GLOBALS['report_itemizing_temp_flag_and_id']) {
556 $GLOBALS['report_itemized_test_id_iterator'] = 1;
559 $dataSheet_batch = test_rules_clinic($provider, $type, $dateTarget, $mode, '', $plan, $organize_mode, $options_modified, $pat_prov_rel, (($batchSize * $i) +
1), $batchSize);
560 $dataSheet = array();
562 // For first cycle, simply copy it to dataSheet
563 $dataSheet = $dataSheet_batch;
566 //error_log("CDR: ".print_r($dataSheet,TRUE),0);
567 //error_log("CDR: ".($batchSize*$i)." records",0);
569 // Integrate batch results into main dataSheet
570 foreach ($dataSheet_batch as $key => $row) {
571 if (!$row['is_sub']) {
572 //skip this stuff for the sub entries (and use previous main entry in percentage calculation)
573 $total_patients = $dataSheet[$key]['total_patients'] +
$row['total_patients'];
574 $dataSheet[$key]['total_patients'] = $total_patients;
575 $excluded = $dataSheet[$key]['excluded'] +
$row['excluded'];
576 $dataSheet[$key]['excluded'] = $excluded;
577 $pass_filter = $dataSheet[$key]['pass_filter'] +
$row['pass_filter'];
578 $dataSheet[$key]['pass_filter'] = $pass_filter;
581 $pass_target = $dataSheet[$key]['pass_target'] +
$row['pass_target'];
582 $dataSheet[$key]['pass_target'] = $pass_target;
583 $dataSheet[$key]['percentage'] = calculate_percentage($pass_filter, $excluded, $pass_target);
587 //Update database to track results
588 updateReportDatabase($report_id, ($total_patients ??
null));
591 // Record results in database and send to screen, if applicable.
592 if (!empty($dataSheet)) {
593 finishReportDatabase($report_id, json_encode($dataSheet));
596 // make sure the report at least completes even if we have nothing here.
597 finishReportDatabase($report_id, json_encode([]));
602 function rules_clinic_get_providers($billing_facility, $pat_prov_rel)
605 if ($pat_prov_rel == "encounter") {
606 $rez = sqlStatementCdrEngine(
607 "SELECT id AS provider_id, lname, fname, npi, federaltaxid FROM users WHERE authorized = 1 AND users.id IN( "
608 . " SELECT DISTINCT `provider_id` FROM `form_encounter` WHERE `provider_id` IS NOT NULL and `billing_facility` = ? "
609 . " UNION SELECT DISTINCT `supervisor_id` AS `provider_id` FROM `form_encounter` WHERE `supervisor_id` "
610 . " IS NOT NULL and `billing_facility` = ? "
612 . " ORDER BY provider_id ",
613 array($billing_facility, $billing_facility)
615 } else if ($pat_prov_rel == "primary") {
616 $rez = sqlStatementCdrEngine(
617 "SELECT id AS provider_id , lname, fname, npi, federaltaxid FROM users WHERE authorized = 1 AND users.id IN ( "
618 . "SELECT DISTINCT `providerID` AS provider_id FROM `patient_data` JOIN `users` providers ON providerID=providers.id "
619 . " WHERE `providers`.billing_facility_id = ? "
621 . " ORDER BY provider_id ",
622 array($billing_facility)
627 while ($urow = sqlFetchArray($rez)) {
636 * Process clinic rules for the group_calculation provider method. This will process clinical rules for each of the
637 * billing facilities in the entire organization. Rules are applied to the entire facility where patients are connected
638 * to the billing facility either through encounters or their primary care provider. Rules are then applied to each
639 * individual provider who is connected to the billing facility. This satisifies regulatory requirements where rule
640 * calculations must be able to group results for one or more provider NPIs to a group tax id number (TIN). One example
641 * of this is in the United States where providers can reassign their medicaid/medicare reimbursements to another TIN and
642 * need to report on calculations at both the group and provider group level.
644 * @param string $type rule filter (active_alert,passive_alert,cqm,cqm_2011,cqm_2104,amc,amc_2011,amc_2014,patient_reminder). If blank then will test all rules.
645 * @param string/array $dateArray Date filter to run the calculation on. Should have two keys ('dateBegin' and 'dateTarget').
646 * @param string $mode choose either 'report' or 'reminders-all' or 'reminders-due' (required)
647 * @param integer $patient_id pid of patient. If blank then will check all patients.
648 * @param string $plan test for specific plan only
649 * @param string $organize_mode Way to organize the results (default, plans). See above for organization structure of the results.
650 * @param array $options can hold various option (for now, used to hold the manual number of labs for the AMC report)
651 * @param string $pat_prov_rel How to choose patients that are related to a chosen provider. 'primary' selects patients that the provider is set as primary provider. 'encounter' selectes patients that the provider has seen.
652 * @param integer $start applicable patient to start at (when batching process)
653 * @param integer $batchSize number of patients to batch (when batching process)
654 * @param string $user If a user is set, then will only show rules that user has permission to see(only applicable for per patient and not when do reports).
655 * @return array See above for organization structure of the results.
657 function test_rules_clinic_group_calculation($type = '', array $dateArray = array(), $mode = '', $patient_id = '', $plan = '', $organize_mode = 'default', $options = array(), $pat_prov_rel = 'primary', $start = null, $batchSize = null, $user = '')
659 (new SystemLogger())->debug(
660 "test_rules_clinic_group_calculation()",
662 ['type', 'dateArray', 'mode', 'patient_id', 'plan', 'organize_mode'
674 $facilityService = new FacilityService();
675 $billingLocations = $facilityService->getAllBillingLocations();
676 if (!empty($billingLocations)) {
677 //Collect applicable rules
678 // Note that due to a limitation in the this function, the patient_id is explicitly
679 // for grouping items when not being done in real-time or for official reporting.
680 // So for cases such as patient reminders on a clinic scale, the calling function
681 // will actually need rather than pass in a explicit patient_id for each patient in
682 // a separate call to this function.
683 $rules = resolve_rules_sql($type, $patient_id, false, $plan, $user);
684 $filteredRules = array_filter($rules, function ($rule) {
685 return $rule['amc_flag'] ||
$rule['cqm_flag'];
688 // TODO: @adunsulag I'd prefer to use a service here, but in order to be consistent with everything else in this file we will use sqlStatementCdrEngine
689 $sql = "SELECT id, name, federal_ein, facility_npi, tax_id_type FROM facility WHERE facility.billing_location = 1 "
690 . " AND id IN (SELECT DISTINCT billing_facility FROM form_encounter) "
691 . " ORDER BY facility.id ASC";
692 $frez = sqlStatementCdrEngine($sql);
693 while ($frow = sqlFetchArray($frez)) {
694 // we run with the encounter_billing_facility
696 $options['billing_facility_id'] = $frow['id'];
697 $patientData = buildPatientArray($patient_id, 'group_calculation', $pat_prov_rel, $start, $batchSize, false, $frow['id']);
699 (new SystemLogger())->debug(
700 "test_rules_clinic_group_calculation() patientIds retrieved for facility",
701 ['facilityId' => $frow['id'], 'patientData' => $patientData]
704 if (!empty($patientData)) {
706 $group_item['is_provider_group'] = true;
707 $group_item['name'] = $frow['name'];
708 $group_item['npi'] = $frow['facility_npi'];
709 $group_item['federaltaxid'] = $frow['federal_ein'];
710 $results[] = $group_item;
712 foreach ($filteredRules as $rowRule) {
713 $tempResults = test_rules_clinic_cqm_amc_rule($rowRule, $patientData, $dateArray, $dateArray, $options, null, $pat_prov_rel);
714 if (!empty($tempResults)) {
715 $results = array_merge($results, $tempResults);
717 (new SystemLogger())->debug(
718 "test_rules_clinic_group_calculation() results returned for facility",
719 ['facilityId' => $frow['id'], 'results' => $tempResults]
723 // now we are going to do our providers
724 $providers = rules_clinic_get_providers($frow['id'], $pat_prov_rel);
725 if (!empty($providers)) { // should always be populated here
726 $facility_pat_prov_rel = $pat_prov_rel . "_billing_facility";
727 foreach ($providers as $prov) {
728 $newResults = test_rules_clinic($prov['provider_id'], $type, $dateArray, $mode, $patient_id, $plan, $organize_mode, $options, $facility_pat_prov_rel, $start, $batchSize, $user);
729 if (!empty($newResults)) {
730 $provider_item['is_provider'] = true;
731 $provider_item['is_provider_in_group'] = true;
732 $provider_item['group'] = [
733 'name' => $frow['name']
734 ,'npi' => $frow['facility_npi']
735 ,'federaltaxid' => $frow['federal_ein']
737 $provider_item['prov_lname'] = $prov['lname'];
738 $provider_item['prov_fname'] = $prov['fname'];
739 $provider_item['npi'] = $prov['npi'];
740 $provider_item['federaltaxid'] = $prov['federaltaxid'];
741 $results[] = $provider_item;
742 $results = array_merge($results, $newResults);
753 * Process clinic rules for the collate outer and collate inner methods
755 * Test the clinic rules of entire clinic and create a report or patient reminders (can also test
756 * on one patient or patients of one provider). The structure of the returned results is dependent on the
757 * $organize_mode and $mode parameters.
758 * <pre>The results are dependent on the $organize_mode parameter settings
759 * 'default' organize_mode:
760 * Returns a two-dimensional array of results organized by rules (dependent on the following $mode settings):
761 * 'reminders-due' mode - returns an array of reminders (action array elements plus a 'pid' and 'due_status')
762 * 'reminders-all' mode - returns an array of reminders (action array elements plus a 'pid' and 'due_status')
763 * 'report' mode - returns an array of rows for the Clinical Quality Measures (CQM) report
764 * 'plans' organize_mode:
765 * Returns similar to default, but organizes by the active plans
768 * @param integer $provider id of a selected provider. If blank, then will test entire clinic. If 'collate_outer' or 'collate_inner', then will test each provider in entire clinic; outer will nest plans inside collated providers, while inner will nest the providers inside the plans (note inner and outer are only different if organize_mode is set to plans).
769 * @param string $type rule filter (active_alert,passive_alert,cqm,cqm_2011,cqm_2104,amc,amc_2011,amc_2014,patient_reminder). If blank then will test all rules.
770 * @param string/array $dateTarget target date (format Y-m-d H:i:s). If blank then will test with current date as target. If an array, then is holding two dates ('dateBegin' and 'dateTarget').
771 * @param string $mode choose either 'report' or 'reminders-all' or 'reminders-due' (required)
772 * @param integer $patient_id pid of patient. If blank then will check all patients.
773 * @param string $plan test for specific plan only
774 * @param string $organize_mode Way to organize the results (default, plans). See above for organization structure of the results.
775 * @param array $options can hold various option (for now, used to hold the manual number of labs for the AMC report)
776 * @param string $pat_prov_rel How to choose patients that are related to a chosen provider. 'primary' selects patients that the provider is set as primary provider. 'encounter' selectes patients that the provider has seen. This parameter is only applicable if the $provider parameter is set to a provider or collation setting.
777 * @param integer $start applicable patient to start at (when batching process)
778 * @param integer $batchSize number of patients to batch (when batching process)
779 * @param string $user If a user is set, then will only show rules that user has permission to see(only applicable for per patient and not when do reports).
780 * @return array See above for organization structure of the results.
782 function test_rules_clinic_collate($provider = '', $type = '', $dateTarget = '', $mode = '', $patient_id = '', $plan = '', $organize_mode = 'default', $options = array(), $pat_prov_rel = 'primary', $start = null, $batchSize = null, $user = '')
785 // If set the $provider to collate_outer (or collate_inner without plans organize mode),
786 // then run through this function recursively and return results.
787 if (($provider === "collate_outer") ||
($provider === "collate_inner" && $organize_mode !== 'plans')) {
788 // First, collect an array of all providers
789 $query = "SELECT id, lname, fname, npi, federaltaxid FROM users WHERE authorized = 1 ORDER BY lname, fname";
790 $ures = sqlStatementCdrEngine($query);
791 // Second, run through each provider recursively
792 while ($urow = sqlFetchArray($ures)) {
793 $newResults = test_rules_clinic($urow['id'], $type, $dateTarget, $mode, $patient_id, $plan, $organize_mode, $options, $pat_prov_rel, $start, $batchSize, $user);
794 if (!empty($newResults)) {
795 $provider_item['is_provider'] = true;
796 $provider_item['prov_lname'] = $urow['lname'];
797 $provider_item['prov_fname'] = $urow['fname'];
798 $provider_item['npi'] = $urow['npi'];
799 $provider_item['federaltaxid'] = $urow['federaltaxid'];
800 $results[] = $provider_item;
801 $results = array_merge($results, $newResults);
805 // done, so now can return results
809 // If set organize-mode to plans, then collects active plans and run through this
810 // function recursively and return results.
811 if ($organize_mode === "plans") {
812 // First, collect active plans
813 $plans_resolve = resolve_plans_sql($plan, $patient_id);
814 // Second, run through function recursively
815 foreach ($plans_resolve as $plan_item) {
816 // (if collate_inner, then nest a collation of providers within each plan)
817 if ($provider === "collate_inner") {
818 // First, collect an array of all providers
819 $query = "SELECT id, lname, fname, npi, federaltaxid FROM users WHERE authorized = 1 ORDER BY lname, fname";
820 $ures = sqlStatementCdrEngine($query);
821 // Second, run through each provider recursively
822 $provider_results = array();
823 while ($urow = sqlFetchArray($ures)) {
824 $newResults = test_rules_clinic($urow['id'], $type, $dateTarget, $mode, $patient_id, $plan_item['id'], 'default', $options, $pat_prov_rel, $start, $batchSize, $user);
825 if (!empty($newResults)) {
826 $provider_item['is_provider'] = true;
827 $provider_item['prov_lname'] = $urow['lname'];
828 $provider_item['prov_fname'] = $urow['fname'];
829 $provider_item['npi'] = $urow['npi'];
830 $provider_item['federaltaxid'] = $urow['federaltaxid'];
831 $provider_results[] = $provider_item;
832 $provider_results = array_merge($provider_results, $newResults);
836 if (!empty($provider_results)) {
837 $plan_item['is_plan'] = true;
838 $results[] = $plan_item;
839 $results = array_merge($results, $provider_results);
842 // (not collate_inner, so do not nest providers within each plan)
843 $newResults = test_rules_clinic($provider, $type, $dateTarget, $mode, $patient_id, $plan_item['id'], 'default', $options, $pat_prov_rel, $start, $batchSize, $user);
844 if (!empty($newResults)) {
845 $plan_item['is_plan'] = true;
846 $results[] = $plan_item;
847 $results = array_merge($results, $newResults);
852 // done, so now can return results
858 * Runs the AMC or CQM calculations for a given rule.
859 * @param $rowRule The rule we are going to run calculcations against
860 * @param $patientData The list of patient pids we are going to calculate our rules on
861 * @param $dateArray The start and end date of the rule for AMC calculation purposes
862 * @param $dateTarget The end date of the rule for CQM purposes
863 * @param $options Any options needed for AMC/CQM processing
864 * @return array The list of rule calculations that have been generated
865 * @throws Exception If a rule is invalid or not found
867 function test_rules_clinic_cqm_amc_rule($rowRule, $patientData, $dateArray, $dateTarget, $options, $provider, $pat_prov_rel)
869 // we need to give more context to some of our rules
870 $ruleOptions = $options;
871 $ruleOptions['pat_prov_rel'] = $pat_prov_rel;
872 if (is_numeric($provider)) {
873 $ruleOptions['provider_id'] = $provider;
875 require_once(dirname(__FILE__
) . "/classes/rulesets/ReportManager.php");
876 $manager = new ReportManager();
877 if ($rowRule['amc_flag']) {
878 // Send array of dates ('dateBegin' and 'dateTarget')
879 $tempResults = $manager->runReport($rowRule, $patientData, $dateArray, $ruleOptions);
882 $tempResults = $manager->runReport($rowRule, $patientData, $dateTarget);
888 * Process clinic rules.
890 * Test the clinic rules of entire clinic and create a report or patient reminders (can also test
891 * on one patient or patients of one provider). The structure of the returned results is dependent on the
892 * $organize_mode and $mode parameters.
893 * <pre>The results are dependent on the $organize_mode parameter settings
894 * 'default' organize_mode:
895 * Returns a two-dimensional array of results organized by rules (dependent on the following $mode settings):
896 * 'reminders-due' mode - returns an array of reminders (action array elements plus a 'pid' and 'due_status')
897 * 'reminders-all' mode - returns an array of reminders (action array elements plus a 'pid' and 'due_status')
898 * 'report' mode - returns an array of rows for the Clinical Quality Measures (CQM) report
899 * 'plans' organize_mode:
900 * Returns similar to default, but organizes by the active plans
903 * @param integer $provider id of a selected provider. If blank, then will test entire clinic. If 'collate_outer' or 'collate_inner', then will test each provider in entire clinic; outer will nest plans inside collated providers, while inner will nest the providers inside the plans (note inner and outer are only different if organize_mode is set to plans).
904 * @param string $type rule filter (active_alert,passive_alert,cqm,cqm_2011,cqm_2104,amc,amc_2011,amc_2014,patient_reminder). If blank then will test all rules.
905 * @param string/array $dateTarget target date (format Y-m-d H:i:s). If blank then will test with current date as target. If an array, then is holding two dates ('dateBegin' and 'dateTarget').
906 * @param string $mode choose either 'report' or 'reminders-all' or 'reminders-due' (required)
907 * @param integer $patient_id pid of patient. If blank then will check all patients.
908 * @param string $plan test for specific plan only
909 * @param string $organize_mode Way to organize the results (default, plans). See above for organization structure of the results.
910 * @param array $options can hold various option (for now, used to hold the manual number of labs for the AMC report)
911 * @param string $pat_prov_rel How to choose patients that are related to a chosen provider. 'primary' selects patients that the provider is set as primary provider. 'encounter' selectes patients that the provider has seen. This parameter is only applicable if the $provider parameter is set to a provider or collation setting.
912 * @param integer $start applicable patient to start at (when batching process)
913 * @param integer $batchSize number of patients to batch (when batching process)
914 * @param string $user If a user is set, then will only show rules that user has permission to see(only applicable for per patient and not when do reports).
915 * @return array See above for organization structure of the results.
917 function test_rules_clinic($provider = '', $type = '', $dateTarget = '', $mode = '', $patient_id = '', $plan = '', $organize_mode = 'default', $options = array(), $pat_prov_rel = 'primary', $start = null, $batchSize = null, $user = '')
920 // If dateTarget is an array, then organize them.
921 if (is_array($dateTarget)) {
922 $dateArray = $dateTarget;
923 $dateTarget = $dateTarget['dateTarget'];
928 // Set date to current if not set
929 $dateTarget = ($dateTarget) ?
$dateTarget : date('Y-m-d H:i:s');
931 // Prepare the results array
934 // we have a special mechanism for collation or plans organize method
935 if ($provider === "collate_outer" ||
$organize_mode === 'plans') {
936 return test_rules_clinic_collate($provider, $type, $dateTarget, $mode, $patient_id, $plan, $organize_mode, $options, $pat_prov_rel, $start, $batchSize, $user);
939 if ($provider === "group_calculation") {
940 return test_rules_clinic_group_calculation($type, $dateArray, $mode, $patient_id, $plan, $organize_mode, $options, $pat_prov_rel, $start, $batchSize, $user);
943 // Collect applicable patient pids
944 $patientData = buildPatientArray($patient_id, $provider, $pat_prov_rel, $start, $batchSize, false, $options['billing_facility_id'] ??
null);
946 // Go through each patient(s)
948 // If in report mode, then tabulate for each rule:
950 // Patients that pass the filter
951 // Patients that pass the target
952 // If in reminders mode, then create reminders for each rule:
953 // Reminder that action is due soon
954 // Reminder that action is due
955 // Reminder that action is post-due
957 //Collect applicable rules
958 // Note that due to a limitation in the this function, the patient_id is explicitly
959 // for grouping items when not being done in real-time or for official reporting.
960 // So for cases such as patient reminders on a clinic scale, the calling function
961 // will actually need rather than pass in a explicit patient_id for each patient in
962 // a separate call to this function.
963 $rules = resolve_rules_sql($type, $patient_id, false, $plan, $user);
965 foreach ($rules as $rowRule) {
966 // If using cqm or amc type, then use the hard-coded rules set.
967 // Note these rules are only used in report mode.
968 if ($rowRule['cqm_flag'] ||
$rowRule['amc_flag']) {
969 $tempResults = test_rules_clinic_cqm_amc_rule($rowRule, $patientData, $dateArray, $dateTarget, $options, $provider, $pat_prov_rel);
970 $results = array_merge($results, $tempResults);
971 // Go on to the next rule
975 // ALL OF THE BELOW RULES ARE FOR active_alert, passive_alert,patient_reminder
976 // If in reminder mode then need to collect the measurement dates
977 // from rule_reminder table
978 $target_dates = array();
979 if ($mode != "report") {
980 // Calculate the dates to check for
981 if ($type == "patient_reminder") {
982 $reminder_interval_type = "patient_reminder";
983 } else { // $type == "passive_alert" or $type == "active_alert"
984 $reminder_interval_type = "clinical_reminder";
987 $target_dates = calculate_reminder_dates($rowRule['id'], $dateTarget, $reminder_interval_type);
988 } else { // $mode == "report"
989 // Only use the target date in the report
990 $target_dates[0] = $dateTarget;
999 // Find the number of target groups
1000 $targetGroups = returnTargetGroups($rowRule['id']);
1002 if ((count($targetGroups) == 1) ||
($mode == "report")) {
1003 // If report itemization is turned on, then iterate the rule id iterator
1004 if (!empty($GLOBALS['report_itemizing_temp_flag_and_id'])) {
1005 $GLOBALS['report_itemized_test_id_iterator']++
;
1008 //skip this section if not report and more than one target group
1009 foreach ($patientData as $rowPatient) {
1010 // First, deal with deceased patients
1011 // (for now will simply skip the patient)
1012 // If want to support rules for deceased patients then will need to migrate this below
1013 // in target_dates foreach(guessing won't ever need to do this, though).
1014 // Note using the dateTarget rather than dateFocus
1015 if (is_patient_deceased($rowPatient['pid'], $dateTarget)) {
1019 // Count the total patients
1022 $dateCounter = 1; // for reminder mode to keep track of which date checking
1023 // If report itemization is turned on, reset flag.
1024 if (!empty($GLOBALS['report_itemizing_temp_flag_and_id'])) {
1025 $temp_track_pass = 1;
1028 foreach ($target_dates as $dateFocus) {
1029 //Skip if date is set to SKIP
1030 if ($dateFocus == "SKIP") {
1035 //Set date counter and reminder token (applicable for reminders only)
1036 if ($dateCounter == 1) {
1037 $reminder_due = "soon_due";
1038 } elseif ($dateCounter == 2) {
1039 $reminder_due = "due";
1040 } else { // $dateCounter == 3
1041 $reminder_due = "past_due";
1044 // Check if pass filter
1045 $passFilter = test_filter($rowPatient['pid'], $rowRule['id'], $dateFocus);
1046 if ($passFilter === "EXCLUDED") {
1047 // increment EXCLUDED and pass_filter counters
1048 // and set as FALSE for reminder functionality.
1051 $passFilter = false;
1055 // increment pass filter counter
1057 // If report itemization is turned on, trigger flag.
1058 if (!empty($GLOBALS['report_itemizing_temp_flag_and_id'])) {
1059 $temp_track_pass = 0;
1066 // Check if pass target
1067 $passTarget = test_targets($rowPatient['pid'], $rowRule['id'], '', $dateFocus);
1069 // increment pass target counter
1071 // If report itemization is turned on, then record the "passed" item and set the flag
1072 if (!empty($GLOBALS['report_itemizing_temp_flag_and_id'])) {
1073 insertItemReportTracker($GLOBALS['report_itemizing_temp_flag_and_id'], $GLOBALS['report_itemized_test_id_iterator'], 1, $rowPatient['pid']);
1074 $temp_track_pass = 1;
1077 // send to reminder results
1078 if ($mode == "reminders-all") {
1079 // place the completed actions into the reminder return array
1080 $actionArray = resolve_action_sql($rowRule['id'], '1');
1081 foreach ($actionArray as $action) {
1082 $action_plus = $action;
1083 $action_plus['due_status'] = "not_due";
1084 $action_plus['pid'] = $rowPatient['pid'];
1085 $action_plus['rule_id'] = $rowRule['id'];
1086 $results = reminder_results_integrate($results, $action_plus);
1092 // send to reminder results
1093 if ($mode != "report") {
1094 // place the uncompleted actions into the reminder return array
1095 $actionArray = resolve_action_sql($rowRule['id'], '1');
1096 foreach ($actionArray as $action) {
1097 $action_plus = $action;
1098 $action_plus['due_status'] = $reminder_due;
1099 $action_plus['pid'] = $rowPatient['pid'];
1100 $action_plus['rule_id'] = $rowRule['id'];
1101 $results = reminder_results_integrate($results, $action_plus);
1109 // If report itemization is turned on, then record the "failed" item if it did not pass
1110 if (!empty($GLOBALS['report_itemizing_temp_flag_and_id']) && !($temp_track_pass)) {
1111 insertItemReportTracker($GLOBALS['report_itemizing_temp_flag_and_id'], $GLOBALS['report_itemized_test_id_iterator'], 0, $rowPatient['pid']);
1116 // Calculate and save the data for the rule
1117 $percentage = calculate_percentage($pass_filter, $exclude_filter, $pass_target);
1118 if ($mode == "report") {
1119 $newRow = array('is_main' => true,'total_patients' => $total_patients,'excluded' => $exclude_filter,'pass_filter' => $pass_filter,'pass_target' => $pass_target,'percentage' => $percentage);
1120 $newRow = array_merge($newRow, $rowRule);
1122 // If itemization is turned on, then record the itemized_test_id
1123 if ($GLOBALS['report_itemizing_temp_flag_and_id']) {
1124 $newRow = array_merge($newRow, array('itemized_test_id' => $GLOBALS['report_itemized_test_id_iterator']));
1127 $results[] = $newRow;
1130 // Now run through the target groups if more than one
1131 if (count($targetGroups) > 1) {
1132 foreach ($targetGroups as $i) {
1133 // If report itemization is turned on, then iterate the rule id iterator
1134 if (!empty($GLOBALS['report_itemizing_temp_flag_and_id'])) {
1135 $GLOBALS['report_itemized_test_id_iterator']++
;
1138 //Reset the target counter
1141 foreach ($patientData as $rowPatient) {
1142 // First, deal with deceased patients
1143 // (for now will simply skip the patient)
1144 // If want to support rules for deceased patients then will need to migrate this below
1145 // in target_dates foreach(guessing won't ever need to do this, though).
1146 // Note using the dateTarget rather than dateFocus
1147 if (is_patient_deceased($rowPatient['pid'], $dateTarget)) {
1151 $dateCounter = 1; // for reminder mode to keep track of which date checking
1152 // If report itemization is turned on, reset flag.
1153 if (!empty($GLOBALS['report_itemizing_temp_flag_and_id'])) {
1154 $temp_track_pass = 1;
1157 foreach ($target_dates as $dateFocus) {
1158 //Skip if date is set to SKIP
1159 if ($dateFocus == "SKIP") {
1164 //Set date counter and reminder token (applicable for reminders only)
1165 if ($dateCounter == 1) {
1166 $reminder_due = "soon_due";
1167 } elseif ($dateCounter == 2) {
1168 $reminder_due = "due";
1169 } else { // $dateCounter == 3
1170 $reminder_due = "past_due";
1173 // Check if pass filter
1174 $passFilter = test_filter($rowPatient['pid'], $rowRule['id'], $dateFocus);
1175 if ($passFilter === "EXCLUDED") {
1176 $passFilter = false;
1183 // If report itemization is turned on, trigger flag.
1184 if (!empty($GLOBALS['report_itemizing_temp_flag_and_id'])) {
1185 $temp_track_pass = 0;
1189 //Check if pass target
1190 $passTarget = test_targets($rowPatient['pid'], $rowRule['id'], $i, $dateFocus);
1192 // increment pass target counter
1194 // If report itemization is turned on, then record the "passed" item and set the flag
1195 if ($GLOBALS['report_itemizing_temp_flag_and_id']) {
1196 insertItemReportTracker($GLOBALS['report_itemizing_temp_flag_and_id'], $GLOBALS['report_itemized_test_id_iterator'], 1, $rowPatient['pid']);
1197 $temp_track_pass = 1;
1200 // send to reminder results
1201 if ($mode == "reminders-all") {
1202 // place the completed actions into the reminder return array
1203 $actionArray = resolve_action_sql($rowRule['id'], $i);
1204 foreach ($actionArray as $action) {
1205 $action_plus = $action;
1206 $action_plus['due_status'] = "not_due";
1207 $action_plus['pid'] = $rowPatient['pid'];
1208 $action_plus['rule_id'] = $rowRule['id'];
1209 $results = reminder_results_integrate($results, $action_plus);
1215 // send to reminder results
1216 if ($mode != "report") {
1217 // place the actions into the reminder return array
1218 $actionArray = resolve_action_sql($rowRule['id'], $i);
1219 foreach ($actionArray as $action) {
1220 $action_plus = $action;
1221 $action_plus['due_status'] = $reminder_due;
1222 $action_plus['pid'] = $rowPatient['pid'];
1223 $action_plus['rule_id'] = $rowRule['id'];
1224 $results = reminder_results_integrate($results, $action_plus);
1232 // If report itemization is turned on, then record the "failed" item if it did not pass
1233 if (!empty($GLOBALS['report_itemizing_temp_flag_and_id']) && !($temp_track_pass)) {
1234 insertItemReportTracker($GLOBALS['report_itemizing_temp_flag_and_id'], $GLOBALS['report_itemized_test_id_iterator'], 0, $rowPatient['pid']);
1238 // Calculate and save the data for the rule
1239 $percentage = calculate_percentage($pass_filter, $exclude_filter, $pass_target);
1241 // Collect action for title (just use the first one, if more than one)
1242 $actionArray = resolve_action_sql($rowRule['id'], $i);
1243 $action = $actionArray[0];
1244 if ($mode == "report") {
1245 $newRow = array('is_sub' => true,'action_category' => $action['category'],'action_item' => $action['item'],'total_patients' => '','excluded' => '','pass_filter' => '','pass_target' => $pass_target,'percentage' => $percentage);
1247 // If itemization is turned on, then record the itemized_test_id
1248 if ($GLOBALS['report_itemizing_temp_flag_and_id']) {
1249 $newRow = array_merge($newRow, array('itemized_test_id' => $GLOBALS['report_itemized_test_id_iterator']));
1252 $results[] = $newRow;
1263 * Process patient array that is to be tested.
1265 * @param integer $provider id of a selected provider. If blank, then will test entire clinic.
1266 * @param integer $patient_id pid of patient. If blank then will check all patients.
1267 * @param string $pat_prov_rel How to choose patients that are related to a chosen provider. 'primary' selects patients that the provider is set as primary provider. 'encounter' selectes patients that the provider has seen. This parameter is only applicable if the $provider parameter is set to a provider or collation setting.
1268 * @param integer $start applicable patient to start at (when batching process)
1269 * @param integer $batchSize number of patients to batch (when batching process)
1270 * @param boolean $onlyCount If true, then will just return the total number of applicable records (ignores batching parameters)
1271 * @param integer $billing_facility id of the billing facility to constrain patient relationships to
1272 * @return array/integer Array of patient pid values or number total pertinent patients (if $onlyCount is TRUE)
1274 function buildPatientArray($patient_id = '', $provider = '', $pat_prov_rel = 'primary', $start = null, $batchSize = null, $onlyCount = false, $billing_facility = null)
1276 (new SystemLogger())->debug(
1277 "buildPatientArray()",
1278 ['patient_id' => $patient_id, 'provider' => $provider, 'pat_prov_rel' => $pat_prov_rel, 'start' => $start
1280 'batchSize' => $batchSize,
1281 'onlyCount' => $onlyCount,
1282 'billing_facility' => $billing_facility]
1286 if (!empty($patient_id)) {
1287 // only look at the selected patient
1291 $patientData[0]['pid'] = $patient_id;
1294 if (empty($provider)) {
1295 // Look at entire practice
1296 if ($start == null ||
$batchSize == null ||
$onlyCount) {
1297 $rez = sqlStatementCdrEngine("SELECT `pid` FROM `patient_data` ORDER BY `pid`");
1299 $patientNumber = sqlNumRows($rez);
1303 $rez = sqlStatementCdrEngine("SELECT `pid` FROM `patient_data` ORDER BY `pid` LIMIT ?,?", array(($start - 1),$batchSize));
1306 // Look at an individual physician
1307 if ($provider == 'group_calculation' && $pat_prov_rel == 'encounter') {
1308 return buildPatientArrayEncounterBillingFacility($start, $batchSize, $onlyCount, $billing_facility);
1309 } else if ($provider == 'group_calculation' && $pat_prov_rel == 'primary') {
1310 return buildPatientArrayPrimaryProviderBillingFacility($start, $batchSize, $onlyCount, $billing_facility);
1311 } else if ($pat_prov_rel == 'encounter_billing_facility' && is_numeric($provider)) {
1312 return buildPatientArrayEncounterBillingFacility($start, $batchSize, $onlyCount, $billing_facility, $provider);
1313 } else if ($pat_prov_rel == 'primary_billing_facility' && is_numeric($provider)) {
1314 return buildPatientArrayPrimaryProviderBillingFacility($start, $batchSize, $onlyCount, $billing_facility, $provider);
1315 } else if ($pat_prov_rel == 'encounter') {
1316 // Choose patients that are related to specific physician by an encounter (OR the provider was a referral originator)
1317 $sql = "select DISTINCT `pid` FROM `form_encounter` WHERE `provider_id` =? OR `supervisor_id` = ? "
1318 . " UNION select DISTINCT `transactions`.`pid` FROM transactions "
1319 . " INNER JOIN lbt_data ON lbt_data.form_id = transactions.id AND lbt_data.field_id = 'refer_from' AND lbt_data.field_value = ? "
1320 . " ORDER BY `pid`";
1321 if ($start == null ||
$batchSize == null ||
$onlyCount) {
1322 // we need to include referrals here as a referral can occur w/o there being an encounter
1324 $rez = sqlStatementCdrEngine($sql, array($provider, $provider, $provider));
1326 $patientNumber = sqlNumRows($rez);
1330 $sql .= " LIMIT " . intval($start) - 1 . "," . intval($batchSize);
1331 $rez = sqlStatementCdrEngine($sql, array($provider, $provider, $provider));
1333 } else { //$pat_prov_rel == 'primary'
1334 // Choose patients that are assigned to the specific physician (primary physician in patient demographics)
1335 if ($start == null ||
$batchSize == null ||
$onlyCount) {
1336 $rez = sqlStatementCdrEngine("SELECT `pid` FROM `patient_data` " .
1337 "WHERE `providerID`=? ORDER BY `pid`", array($provider));
1339 $patientNumber = sqlNumRows($rez);
1342 $rez = sqlStatementCdrEngine("SELECT `pid` FROM `patient_data` " .
1343 "WHERE `providerID`=? ORDER BY `pid` LIMIT ?,?", array($provider,($start - 1),$batchSize));
1348 // convert the sql query results into an array if returning the array
1350 for ($iter = 0; $row = sqlFetchArray($rez); $iter++
) {
1351 $patientData[$iter] = $row;
1357 // return the number of applicable patients
1358 return $patientNumber;
1360 // return array of patient pids
1361 return $patientData;
1366 * Process patient array that is to be tested. This uses the patient relationship context of encounters linked to billing facilities
1368 * @param integer $start applicable patient to start at (when batching process)
1369 * @param integer $batchSize number of patients to batch (when batching process)
1370 * @param boolean $onlyCount If true, then will just return the total number of applicable records (ignores batching parameters)
1371 * @param integer $billing_facility id of the billing facility to constrain patient relationships to, if NULL it uses ALL billing facilities
1372 * @param integer|null $provider_id The id of a provider to restrict patient data to if we have one
1373 * @return array/integer Array of patient pid values or number total pertinent patients (if $onlyCount is TRUE)
1375 function buildPatientArrayEncounterBillingFacility($start, $batchSize, $onlyCount, $billing_facility, $provider_id = null)
1377 $sql = "SELECT DISTINCT `pid` FROM `form_encounter` ";
1380 $billing_facility = intval($billing_facility); // make sure its an integer
1381 if (empty($billing_facility)) {
1382 $sql .= "WHERE `form_encounter`.`billing_facility` IS NOT NULL ";
1384 // " WHERE (`provider_id`=? OR `supervisor_id`=?) AND `billing_facility` = ? ORDER BY `pid`", array($provider,$provider, $billing_facility));
1385 $sql .= " WHERE `form_encounter`.`billing_facility` = ? ";
1386 $binds[] = $billing_facility;
1388 if (!empty($provider_id)) {
1389 $provider_id = intval($provider_id); // make sure we convert this to a pure int
1390 $sql .= " AND (`form_encounter`.`provider_id` = ? OR `form_encounter`.`supervisor_id` = ?)";
1391 $binds[] = $provider_id;
1392 $binds[] = $provider_id;
1394 $sql .= "UNION SELECT DISTINCT `transactions`.`pid` FROM `transactions` ";
1395 $sql .= "INNER JOIN `lbt_data` ON `lbt_data`.`form_id` = `transactions`.`id` AND `lbt_data`.`field_id` = 'billing_facility_id' ";
1396 $sql .= "INNER JOIN lbt_data lbt_data2 ON lbt_data.form_id = transactions.id AND lbt_data2.field_id = 'refer_from' ";
1397 if (!empty($billing_facility)) {
1398 $sql .= " WHERE `lbt_data`.`field_value` = ? ";
1399 $binds[] = $billing_facility;
1402 if (!empty($provider_id)) {
1403 $sql .= " AND `lbt_data2`.`field_value` = ? ";
1404 $binds[] = $provider_id;
1406 $sql .= "ORDER BY `pid`";
1407 if (!($start == null ||
$batchSize == null ||
$onlyCount)) {
1408 $sql .= "LIMIT " . (intval($start) - 1) . "," . intval($batchSize);
1410 $rez = sqlStatementCdrEngine($sql, $binds);
1413 $patientNumber = sqlNumRows($rez);
1414 return $patientNumber;
1418 for ($iter = 0; $row = sqlFetchArray($rez); $iter++
) {
1419 $patientData[$iter] = $row;
1421 return $patientData;
1425 * Process patient array that is to be tested. This uses the patient relationship context of the primary provider who is
1426 * linked to a billing facility
1428 * @param integer $start applicable patient to start at (when batching process)
1429 * @param integer $batchSize number of patients to batch (when batching process)
1430 * @param boolean $onlyCount If true, then will just return the total number of applicable records (ignores batching parameters)
1431 * @param integer $billing_facility id of the billing facility to constrain patient relationships to, if NULL it uses ALL billing facilities
1432 * @param integer|null $provider_id The id of a provider to restrict patient data to if we have one
1433 * @return array/integer Array of patient pid values or number total pertinent patients (if $onlyCount is TRUE)
1435 function buildPatientArrayPrimaryProviderBillingFacility($start, $batchSize, $onlyCount, $billing_facility, $provider_id = null)
1437 $sql = "SELECT DISTINCT `pid` FROM `patient_data` JOIN users providers ON `patient_data`.`providerID`=providers.id ";
1440 $billing_facility = intval($billing_facility); // make sure its an integer
1441 if (empty($billing_facility)) {
1442 $sql .= "WHERE (`providers`.`billing_facility_id` IS NOT NULL)";
1444 $sql .= " WHERE (`providers`.`billing_facility_id`=?)";
1445 $binds[] = $billing_facility;
1447 if (!empty($provider_id)) {
1448 $sql .= " AND `providers.id`=?";
1449 $binds[] = $provider_id;
1451 $sql .= "UNION SELECT DISTINCT `transactions`.`pid` FROM `transactions` ";
1452 $sql .= "INNER JOIN lbt_data ON lbt_data.form_id = transactions.id AND lbt_data.field_id = 'refer_from' ";
1453 $sql .= "INNER JOIN `users` providers ON `lbt_data`.`form_id` = `transactions`.`id` AND `lbt_data`.`form_value` = providers.id ";
1454 if (empty($billing_facility)) {
1455 $sql .= "WHERE (`providers`.`billing_facility_id` IS NOT NULL)";
1457 $sql .= " WHERE (`providers`.`billing_facility_id`=?)";
1458 $binds[] = $billing_facility;
1461 if (!empty($provider_id)) {
1462 $sql .= " AND `providers.id`=?";
1463 $binds[] = $provider_id;
1466 $sql .= "ORDER BY `pid`";
1467 if (!($start == null ||
$batchSize == null ||
$onlyCount)) {
1468 $sql .= "LIMIT " . (intval($start) - 1) . "," . intval($batchSize);
1470 $rez = sqlStatementCdrEngine($sql, $binds);
1473 $patientNumber = sqlNumRows($rez);
1474 return $patientNumber;
1478 for ($iter = 0; $row = sqlFetchArray($rez); $iter++
) {
1479 $patientData[$iter] = $row;
1481 return $patientData;
1485 * Test filter of a selected rule on a selected patient
1487 * @param integer $patient_id pid of selected patient.
1488 * @param string $rule id(string) of selected rule
1489 * @param string $dateTarget target date (format Y-m-d H:i:s). If blank then will test with current date as target.
1490 * @return boolean/string if pass filter then TRUE; if excluded then 'EXCLUDED'; if not pass filter then FALSE
1492 function test_filter($patient_id, $rule, $dateTarget)
1495 // Set date to current if not set
1496 $dateTarget = ($dateTarget) ?
$dateTarget : date('Y-m-d H:i:s');
1498 // Collect patient information
1499 $patientData = getPatientData($patient_id, "sex, DATE_FORMAT(DOB,'%Y %m %d') as DOB_TS");
1502 // ----------------- INCLUSIONS -----------------
1505 // -------- Age Filter (inclusion) ------------
1506 // Calculate patient age in years and months
1507 $patientAgeYears = convertDobtoAgeYearDecimal($patientData['DOB_TS'], $dateTarget);
1508 $patientAgeMonths = convertDobtoAgeMonthDecimal($patientData['DOB_TS'], $dateTarget);
1510 // Min age (year) Filter (assume that there in not more than one of each)
1511 $filter = resolve_filter_sql($rule, 'filt_age_min');
1512 if (!empty($filter)) {
1514 if ($row ['method_detail'] == "year") {
1515 if ($row['value'] && ($row['value'] > $patientAgeYears)) {
1520 if ($row ['method_detail'] == "month") {
1521 if ($row['value'] && ($row['value'] > $patientAgeMonths)) {
1527 // Max age (year) Filter (assume that there in not more than one of each)
1528 $filter = resolve_filter_sql($rule, 'filt_age_max');
1529 if (!empty($filter)) {
1531 if ($row ['method_detail'] == "year") {
1532 if ($row['value'] && ($row['value'] < $patientAgeYears)) {
1537 if ($row ['method_detail'] == "month") {
1538 if ($row['value'] && ($row['value'] < $patientAgeMonths)) {
1544 // -------- Gender Filter (inclusion) ---------
1545 // Gender Filter (assume that there in not more than one of each)
1546 $filter = resolve_filter_sql($rule, 'filt_sex');
1547 if (!empty($filter)) {
1549 if ($row['value'] && ($row['value'] != $patientData['sex'])) {
1554 // -------- Database Filter (inclusion) ------
1556 $filter = resolve_filter_sql($rule, 'filt_database');
1557 if ((!empty($filter)) && !database_check($patient_id, $filter, '', $dateTarget)) {
1561 // -------- Lists Filter (inclusion) ----
1562 // Set up lists filter, which is fully customizable and currently includes diagnoses, meds,
1563 // surgeries and allergies.
1564 $filter = resolve_filter_sql($rule, 'filt_lists');
1565 if ((!empty($filter)) && !lists_check($patient_id, $filter, $dateTarget)) {
1569 // -------- Procedure (labs,imaging,test,procedures,etc) Filter (inlcusion) ----
1570 // Procedure Target (includes) (may need to include an interval in the future)
1571 $filter = resolve_filter_sql($rule, 'filt_proc');
1572 if ((!empty($filter)) && !procedure_check($patient_id, $filter, '', $dateTarget)) {
1577 // ----------------- EXCLUSIONS -----------------
1580 // -------- Lists Filter (EXCLUSION) ----
1581 // Set up lists EXCLUSION filter, which is fully customizable and currently includes diagnoses, meds,
1582 // surgeries and allergies.
1583 $filter = resolve_filter_sql($rule, 'filt_lists', 0);
1584 if ((!empty($filter)) && lists_check($patient_id, $filter, $dateTarget)) {
1588 // Passed all filters, so return true.
1593 * Return an array containing existing group ids for a rule
1595 * @param string $rule id(string) of rule
1596 * @return array listing of group ids
1598 function returnTargetGroups($rule)
1601 $sql = sqlStatementCdrEngine("SELECT DISTINCT `group_id` FROM `rule_target` " .
1602 "WHERE `id`=?", array($rule));
1605 for ($iter = 0; $row = sqlFetchArray($sql); $iter++
) {
1606 $groups[] = $row['group_id'];
1613 * Test targets of a selected rule on a selected patient
1615 * @param integer $patient_id pid of selected patient.
1616 * @param string $rule id(string) of selected rule (if blank, then will ignore grouping)
1617 * @param integer $group_id group id of target group
1618 * @param string $dateTarget target date (format Y-m-d H:i:s).
1619 * @return boolean if target passes then true, otherwise false
1621 function test_targets($patient_id, $rule, string $group_id = null, $dateTarget = null)
1624 // -------- Interval Target ----
1625 $interval = resolve_target_sql($rule, $group_id, 'target_interval');
1627 // -------- Database Target ----
1628 // Database Target (includes)
1629 $target = resolve_target_sql($rule, $group_id, 'target_database');
1630 if ((!empty($target)) && !database_check($patient_id, $target, $interval, $dateTarget)) {
1634 // -------- Procedure (labs,imaging,test,procedures,etc) Target ----
1635 // Procedure Target (includes)
1636 $target = resolve_target_sql($rule, $group_id, 'target_proc');
1637 if ((!empty($target)) && !procedure_check($patient_id, $target, $interval, $dateTarget)) {
1641 // -------- Appointment Target ----
1642 // Appointment Target (includes) (Specialized functionality for appointment reminders)
1643 $target = resolve_target_sql($rule, $group_id, 'target_appt');
1645 return !((!empty($target)) && appointment_check($patient_id, $dateTarget));
1649 * Function to return active plans
1651 * @param string $type plan type filter (normal or cqm or blank)
1652 * @param integer $patient_id pid of selected patient. (if custom plan does not exist then will use the default plan)
1653 * @param boolean $configurableOnly true if only want the configurable (per patient) plans (ie. ignore cqm plans)
1654 * @return array active plans
1656 function resolve_plans_sql($type = '', $patient_id = '0', $configurableOnly = false)
1659 if ($configurableOnly) {
1660 // Collect all default, configurable (per patient) plans into an array
1661 // (ie. ignore the cqm rules)
1662 $sql = sqlStatementCdrEngine("SELECT * FROM `clinical_plans` WHERE `pid`=0 AND `cqm_flag` !=1 ORDER BY `id`");
1664 // Collect all default plans into an array
1665 $sql = sqlStatementCdrEngine("SELECT * FROM `clinical_plans` WHERE `pid`=0 ORDER BY `id`");
1668 $returnArray = array();
1669 for ($iter = 0; $row = sqlFetchArray($sql); $iter++
) {
1670 $returnArray[] = $row;
1673 // Now collect the pertinent plans
1674 $newReturnArray = array();
1676 // Need to select rules (use custom if exist)
1677 foreach ($returnArray as $plan) {
1678 $customPlan = sqlQueryCdrEngine("SELECT * FROM `clinical_plans` WHERE `id`=? AND `pid`=?", array($plan['id'],$patient_id));
1680 // Decide if use default vs custom plan (preference given to custom plan)
1681 if (!empty($customPlan)) {
1682 if ($type == "cqm") {
1683 // For CQM , do not use custom plans (these are to create standard clinic wide reports)
1686 // merge the custom plan with the default plan
1687 $mergedPlan = array();
1688 foreach ($customPlan as $key => $value) {
1689 if ($value == null && preg_match("/_flag$/", $key)) {
1690 // use default setting
1691 $mergedPlan[$key] = $plan[$key];
1693 // use custom setting
1694 $mergedPlan[$key] = $value;
1698 $goPlan = $mergedPlan;
1704 // Use the chosen plan if set
1705 if (!empty($type)) {
1706 if ($goPlan["${type}_flag"] == 1) {
1707 // active, so use the plan
1708 $newReturnArray[] = $goPlan;
1712 $goPlan['normal_flag'] == 1 ||
1713 $goPlan['cqm_flag'] == 1
1715 // active, so use the plan
1716 $newReturnArray[] = $goPlan;
1721 $returnArray = $newReturnArray;
1723 return $returnArray;
1728 * Function to return a specific plan
1730 * @param string $plan id(string) of plan
1731 * @param integer $patient_id pid of selected patient. (if set to 0, then will return the default rule).
1732 * @return array a plan
1734 function collect_plan($plan, $patient_id = '0')
1737 return sqlQueryCdrEngine("SELECT * FROM `clinical_plans` WHERE `id`=? AND `pid`=?", array($plan,$patient_id));
1741 * Function to set a specific plan activity for a specific patient
1743 * @param string $plan id(string) of plan
1744 * @param string $type plan filter (normal,cqm)
1745 * @param string $setting activity of plan (yes,no,default)
1746 * @param integer $patient_id pid of selected patient.
1748 function set_plan_activity_patient($plan, $type, $setting, $patient_id)
1751 // Don't allow messing with the default plans here
1752 if ($patient_id == "0") {
1757 if ($setting == "on") {
1759 } elseif ($setting == "off") {
1761 } else { // $setting == "default"
1765 // Collect patient specific plan, if already exists.
1766 $query = "SELECT * FROM `clinical_plans` WHERE `id` = ? AND `pid` = ?";
1767 $patient_plan = sqlQueryCdrEngine($query, array($plan,$patient_id));
1769 if (empty($patient_plan)) {
1770 // Create a new patient specific plan with flags all set to default
1771 $query = "INSERT into `clinical_plans` (`id`, `pid`) VALUES (?,?)";
1772 sqlStatementCdrEngine($query, array($plan, $patient_id));
1775 // Update patient specific row
1776 $query = "UPDATE `clinical_plans` SET `" . escape_sql_column_name($type . "_flag", array("clinical_plans")) . "`= ? WHERE id = ? AND pid = ?";
1777 sqlStatementCdrEngine($query, array($setting,$plan,$patient_id));
1781 * Function to return active rules
1783 * @param string $type rule filter (active_alert,passive_alert,cqm,cqm_2011,cqm_2014,amc_2011,amc_2014,patient_reminder)
1784 * @param integer $patient_id pid of selected patient. (if custom rule does not exist then will use the default rule)
1785 * @param boolean $configurableOnly true if only want the configurable (per patient) rules (ie. ignore cqm and amc rules)
1786 * @param string $plan collect rules for specific plan
1787 * @param string $user If a user is set, then will only show rules that user has permission to see
1788 * @return array rules
1790 function resolve_rules_sql($type = '', $patient_id = '0', $configurableOnly = false, $plan = '', $user = '')
1793 if ($configurableOnly) {
1794 // Collect all default, configurable (per patient) rules into an array
1795 // (ie. ignore the cqm and amc rules)
1796 $sql = sqlStatementCdrEngine("SELECT * FROM `clinical_rules` WHERE `pid`=0 AND `cqm_flag` !=1 AND `amc_flag` !=1 ORDER BY `id`");
1798 // Collect all default rules into an array
1799 $sql = sqlStatementCdrEngine("SELECT * FROM `clinical_rules` WHERE `pid`=0 ORDER BY `id`");
1802 $returnArray = array();
1803 for ($iter = 0; $row = sqlFetchArray($sql); $iter++
) {
1804 $returnArray[] = $row;
1807 // Now filter rules for plan (if applicable)
1808 if (!empty($plan)) {
1809 $planReturnArray = array();
1810 foreach ($returnArray as $rule) {
1811 $standardRule = sqlQueryCdrEngine("SELECT * FROM `clinical_plans_rules` " .
1812 "WHERE `plan_id`=? AND `rule_id`=?", array($plan,$rule['id']));
1813 if (!empty($standardRule)) {
1814 $planReturnArray[] = $rule;
1818 $returnArray = $planReturnArray;
1821 // Now collect the pertinent rules
1822 $newReturnArray = array();
1824 // Need to select rules (use custom if exist)
1825 foreach ($returnArray as $rule) {
1826 // If user is set, then check if user has access to the rule
1827 if (!empty($user)) {
1828 $access_control = explode(':', $rule['access_control']);
1829 if (!empty($access_control[0]) && !empty($access_control[1])) {
1830 // Section and ACO filters are not empty, so do the test for access.
1831 if (!AclMain
::aclCheckCore($access_control[0], $access_control[1], $user)) {
1832 // User does not have access to this rule, so skip the rule.
1836 // Section or ACO filters are empty, so use default patients:med aco
1837 if (!AclMain
::aclCheckCore('patients', 'med', $user)) {
1838 // User does not have access to this rule, so skip the rule.
1844 $customRule = sqlQueryCdrEngine("SELECT * FROM `clinical_rules` WHERE `id`=? AND `pid`=?", array($rule['id'],$patient_id));
1846 // Decide if use default vs custom rule (preference given to custom rule)
1847 if (!empty($customRule)) {
1848 if ($type == "cqm" || CertificationReportTypes
::isAMCReportType($type)) {
1849 // For CQM and AMC, do not use custom rules (these are to create standard clinic wide reports)
1852 // merge the custom rule with the default rule
1853 $mergedRule = array();
1854 foreach ($customRule as $key => $value) {
1855 if ($value == null && preg_match("/_flag$/", $key)) {
1856 // use default setting
1857 $mergedRule[$key] = $rule[$key];
1859 // use custom setting
1860 $mergedRule[$key] = $value;
1864 $goRule = $mergedRule;
1870 // Use the chosen rule if set
1871 if (!empty($type)) {
1872 if ($goRule["${type}_flag"] == 1) {
1873 // active, so use the rule
1874 $newReturnArray[] = $goRule;
1877 // no filter, so return the rule
1878 $newReturnArray[] = $goRule;
1882 $returnArray = $newReturnArray;
1884 return $returnArray;
1888 * Function to return a specific rule
1890 * @param string $rule id(string) of rule
1891 * @param integer $patient_id pid of selected patient. (if set to 0, then will return the default rule).
1892 * @return array rule
1894 function collect_rule($rule, $patient_id = '0')
1897 return sqlQueryCdrEngine("SELECT * FROM `clinical_rules` WHERE `id`=? AND `pid`=?", array($rule,$patient_id));
1901 * Function to set a specific rule activity for a specific patient
1903 * @param string $rule id(string) of rule
1904 * @param string $type rule filter (active_alert,passive_alert,cqm,amc,patient_reminder)
1905 * @param string $setting activity of rule (yes,no,default)
1906 * @param integer $patient_id pid of selected patient.
1908 function set_rule_activity_patient($rule, $type, $setting, $patient_id)
1911 // Don't allow messing with the default rules here
1912 if ($patient_id == "0") {
1917 if ($setting == "on") {
1919 } elseif ($setting == "off") {
1921 } else { // $setting == "default"
1925 //Collect main rule to allow setting of the access_control
1926 $original_query = "SELECT * FROM `clinical_rules` WHERE `id` = ? AND `pid` = 0";
1927 $patient_rule_original = sqlQueryCdrEngine($original_query, array($rule));
1929 // Collect patient specific rule, if already exists.
1930 $query = "SELECT * FROM `clinical_rules` WHERE `id` = ? AND `pid` = ?";
1931 $patient_rule = sqlQueryCdrEngine($query, array($rule,$patient_id));
1933 if (empty($patient_rule)) {
1934 // Create a new patient specific rule with flags all set to default
1935 $query = "INSERT into `clinical_rules` (`id`, `pid`, `access_control`) VALUES (?,?,?)";
1936 sqlStatementCdrEngine($query, array($rule, $patient_id, $patient_rule_original['access_control']));
1939 // Update patient specific row
1940 $query = "UPDATE `clinical_rules` SET `" . escape_sql_column_name($type . "_flag", ["clinical_rules"]) . "`= ?, `access_control` = ? WHERE id = ? AND pid = ?";
1941 sqlStatementCdrEngine($query, array($setting,$patient_rule_original['access_control'],$rule,$patient_id));
1945 * Function to return applicable reminder dates (relative)
1947 * @param string $rule id(string) of selected rule
1948 * @param string $reminder_method string label of filter type
1949 * @return array reminder features
1951 function resolve_reminder_sql($rule, $reminder_method)
1953 $sql = sqlStatementCdrEngine("SELECT `method_detail`, `value` FROM `rule_reminder` " .
1954 "WHERE `id`=? AND `method`=?", array($rule, $reminder_method));
1956 $returnArray = array();
1957 for ($iter = 0; $row = sqlFetchArray($sql); $iter++
) {
1958 $returnArray[] = $row;
1961 return $returnArray;
1965 * Function to return applicable filters
1967 * @param string $rule id(string) of selected rule
1968 * @param string $filter_method string label of filter type
1969 * @param string $include_flag to allow selection for included or excluded filters
1970 * @return array filters
1972 function resolve_filter_sql($rule, $filter_method, $include_flag = 1)
1974 $sql = sqlStatementCdrEngine("SELECT `method_detail`, `value`, `required_flag` FROM `rule_filter` " .
1975 "WHERE `id`=? AND `method`=? AND `include_flag`=?", array($rule, $filter_method, $include_flag));
1977 $returnArray = array();
1978 for ($iter = 0; $row = sqlFetchArray($sql); $iter++
) {
1979 $returnArray[] = $row;
1982 return $returnArray;
1986 * Function to return applicable targets
1988 * @param string $rule id(string) of selected rule
1989 * @param integer $group_id group id of target group (if blank, then will ignore grouping)
1990 * @param string $target_method string label of target type
1991 * @param string $include_flag to allow selection for included or excluded targets
1992 * @return array targets
1994 function resolve_target_sql($rule, string $group_id = null, $target_method = '', $include_flag = 1)
1998 $sql = sqlStatementCdrEngine("SELECT `value`, `required_flag`, `interval` FROM `rule_target` " .
1999 "WHERE `id`=? AND `group_id`=? AND `method`=? AND `include_flag`=?", array($rule, $group_id, $target_method, $include_flag));
2001 $sql = sqlStatementCdrEngine("SELECT `value`, `required_flag`, `interval` FROM `rule_target` " .
2002 "WHERE `id`=? AND `method`=? AND `include_flag`=?", array($rule, $target_method, $include_flag));
2005 $returnArray = array();
2006 for ($iter = 0; $row = sqlFetchArray($sql); $iter++
) {
2007 $returnArray[] = $row;
2010 return $returnArray;
2014 * Function to return applicable actions
2016 * @param string $rule id(string) of selected rule
2017 * @param integer $group_id group id of target group (if blank, then will ignore grouping)
2018 * @return array actions
2020 function resolve_action_sql($rule, $group_id = '')
2024 $sql = sqlStatementCdrEngine("SELECT b.category, b.item, b.clin_rem_link, b.reminder_message, b.custom_flag " .
2025 "FROM `rule_action` as a " .
2026 "JOIN `rule_action_item` as b " .
2027 "ON a.category = b.category AND a.item = b.item " .
2028 "WHERE a.id=? AND a.group_id=?", array($rule,$group_id));
2030 $sql = sqlStatementCdrEngine("SELECT b.category, b.item, b.value, b.custom_flag " .
2031 "FROM `rule_action` as a " .
2032 "JOIN `rule_action_item` as b " .
2033 "ON a.category = b.category AND a.item = b.item " .
2034 "WHERE a.id=?", array($rule));
2037 $returnArray = array();
2038 for ($iter = 0; $row = sqlFetchArray($sql); $iter++
) {
2039 $returnArray[] = $row;
2042 return $returnArray;
2046 * Function to check database filters and targets
2048 * @param string $patient_id pid of selected patient.
2049 * @param array $filter array containing filter/target elements
2050 * @param array $interval array containing interval elements
2051 * @param string $dateTarget target date(format Y-m-d H:i:s). blank is current date.
2052 * @return boolean true if check passed, otherwise false
2054 function database_check($patient_id, $filter, $interval = '', $dateTarget = '')
2056 $isMatch = false; //matching flag
2058 // Set date to current if not set
2059 $dateTarget = ($dateTarget) ?
$dateTarget : date('Y-m-d H:i:s');
2061 // Unpackage interval information
2062 // (Assume only one for now and only pertinent for targets)
2064 $intervalValue = '';
2065 if (!empty($interval)) {
2066 $intervalType = $interval[0]['value'];
2067 $intervalValue = $interval[0]['interval'];
2071 foreach ($filter as $row) {
2073 // [0]=>special modes
2074 $temp_df = explode("::", $row['value']);
2076 if ($temp_df[0] == "CUSTOM") {
2078 // [0]=>special modes(CUSTOM) [1]=>category [2]=>item [3]=>complete? [4]=>number of hits comparison [5]=>number of hits
2079 if (exist_custom_item($patient_id, $temp_df[1], $temp_df[2], $temp_df[3], $temp_df[4], $temp_df[5], $intervalType, $intervalValue, $dateTarget)) {
2083 // If this is a required entry then return false
2084 if ($row['required_flag']) {
2088 } elseif ($temp_df[0] == "LIFESTYLE") {
2090 // [0]=>special modes(LIFESTYLE) [1]=>column [2]=>status
2091 if (exist_lifestyle_item($patient_id, $temp_df[1], $temp_df[2], $dateTarget)) {
2095 // If this is a required entry then return false
2096 if ($row['required_flag']) {
2103 // [0]=>special modes(BLANK) [1]=>table [2]=>column [3]=>value comparison [4]=>value [5]=>number of hits comparison [6]=>number of hits
2104 if (exist_database_item($patient_id, $temp_df[1], $temp_df[2], $temp_df[3], $temp_df[4], $temp_df[5], $temp_df[6], $intervalType, $intervalValue, $dateTarget)) {
2106 if ($cond_loop > 0) { // For multiple condition check
2107 $isMatch = $isMatch && 1;
2112 // If this is a required entry then return false
2113 if ($row['required_flag']) {
2122 // return results of check
2127 * Function to check procedure filters and targets
2129 * @param string $patient_id pid of selected patient.
2130 * @param array $filter array containing filter/target elements
2131 * @param array $interval array containing interval elements
2132 * @param string $dateTarget target date(format Y-m-d H:i:s). blank is current date.
2133 * @return boolean true if check passed, otherwise false
2135 function procedure_check($patient_id, $filter, $interval = '', $dateTarget = '')
2137 $isMatch = false; //matching flag
2139 // Set date to current if not set
2140 $dateTarget = ($dateTarget) ?
$dateTarget : date('Y-m-d H:i:s');
2142 // Unpackage interval information
2143 // (Assume only one for now and only pertinent for targets)
2145 $intervalValue = '';
2146 if (!empty($interval)) {
2147 $intervalType = $interval[0]['value'];
2148 $intervalValue = $interval[0]['interval'];
2151 foreach ($filter as $row) {
2153 // [0]=>title [1]=>code [2]=>value comparison [3]=>value [4]=>number of hits comparison [5]=>number of hits
2155 // <type(ICD9,CPT4)>:<identifier>||<type(ICD9,CPT4)>:<identifier>||<identifier> etc.
2156 $temp_df = explode("::", $row['value']);
2157 if (exist_procedure_item($patient_id, $temp_df[0], $temp_df[1], $temp_df[2], $temp_df[3], $temp_df[4], $temp_df[5], $intervalType, $intervalValue, $dateTarget)) {
2161 // If this is a required entry then return false
2162 if ($row['required_flag']) {
2168 // return results of check
2173 * Function to check for appointment
2175 * @todo Complete this to allow appointment reminders.
2176 * @param string $patient_id pid of selected patient.
2177 * @param string $dateTarget target date(format Y-m-d H:i:s). blank is current date.
2178 * @return boolean true if appt exist, otherwise false
2180 function appointment_check($patient_id, $dateTarget = '')
2182 $isMatch = false; //matching flag
2184 // Set date to current if not set (although should always be set)
2185 $dateTarget = ($dateTarget) ?
$dateTarget : date('Y-m-d H:i:s');
2186 $dateTargetRound = date('Y-m-d', $dateTarget);
2189 $currentDate = date('Y-m-d H:i:s');
2191 // Basically, if the appointment is within the current date to the target date,
2192 // then return true. (will not send reminders on same day as appointment)
2193 $sql = sqlStatementCdrEngine("SELECT openemr_postcalendar_events.pc_eid, " .
2194 "openemr_postcalendar_events.pc_title, " .
2195 "openemr_postcalendar_events.pc_eventDate, " .
2196 "openemr_postcalendar_events.pc_startTime, " .
2197 "openemr_postcalendar_events.pc_endTime " .
2198 "FROM openemr_postcalendar_events " .
2199 "WHERE openemr_postcalendar_events.pc_eventDate > ? " .
2200 "AND openemr_postcalendar_events.pc_eventDate <= ? " .
2201 "AND openemr_postcalendar_events.pc_pid = ?", array($currentDate,$dateTarget,$patient_id));
2203 // return results of check
2205 // TODO: Figure out how to have multiple appointment and changing appointment reminders.
2206 // Plan to send back array of appt info (eid, time, date, etc.)
2208 if (sqlNumRows($sql) > 0) {
2216 * Function to check lists filters and targets. Customizable and currently includes diagnoses, medications, allergies and surgeries.
2218 * @param string $patient_id pid of selected patient.
2219 * @param array $filter array containing lists filter/target elements
2220 * @param string $dateTarget target date(format Y-m-d H:i:s). blank is current date.
2221 * @return boolean true if check passed, otherwise false
2223 function lists_check($patient_id, $filter, $dateTarget)
2225 $isMatch = false; //matching flag
2227 // Set date to current if not set
2228 $dateTarget = ($dateTarget) ?
$dateTarget : date('Y-m-d H:i:s');
2230 foreach ($filter as $row) {
2231 if (exist_lists_item($patient_id, $row['method_detail'], $row['value'], $dateTarget)) {
2235 // If this is a required entry then return false
2236 if ($row['required_flag']) {
2242 // return results of check
2247 * Function to check for existance of data in database for a patient
2249 * @param string $patient_id pid of selected patient.
2250 * @param string $table selected mysql table
2251 * @param string $column selected mysql column
2252 * @param string $data_comp data comparison (eq,ne,gt,ge,lt,le)
2253 * @param string $data selected data in the mysql database (1)(2)
2254 * @param string $num_items_comp number items comparison (eq,ne,gt,ge,lt,le)
2255 * @param integer $num_items_thres number of items threshold
2256 * @param string $intervalType type of interval (ie. year)
2257 * @param integer $intervalValue searched for within this many times of the interval type
2258 * @param string $dateTarget target date(format Y-m-d H:i:s).
2259 * @return boolean true if check passed, otherwise false
2261 * (1) If data ends with **, operators ne/eq are replaced by (NOT)LIKE operators
2262 * (2) If $data contains '#CURDATE#', then it will be converted to the current date.
2265 function exist_database_item($patient_id, $table, string $column = null, $data_comp = '', string $data = null, $num_items_comp = null, $num_items_thres = null, $intervalType = '', $intervalValue = '', $dateTarget = '')
2268 // Set date to current if not set
2269 $dateTarget = ($dateTarget) ?
$dateTarget : date('Y-m-d H:i:s');
2271 // Collect the correct column label for patient id in the table
2272 $patient_id_label = collect_database_label('pid', $table);
2274 // Get the interval sql query string
2275 $dateSql = sql_interval_string($table, $intervalType, $intervalValue, $dateTarget);
2277 // If just checking for existence (ie. data is empty),
2278 // then simply set the comparison operator to ne.
2283 // get the appropriate sql comparison operator
2284 $compSql = convertCompSql($data_comp);
2286 // custom issues per table can be placed here
2288 if ($table == 'immunizations') {
2289 $customSQL = " AND `added_erroneously` = '0' ";
2292 //adding table list for where condition
2294 if ($table == 'procedure_result') {
2295 $whereTables = ", procedure_order_code, " .
2296 "procedure_order, " .
2297 "procedure_report " ;
2298 $customSQL = " AND procedure_order.procedure_order_id = procedure_order_code.procedure_order_id AND " .
2299 "procedure_report.procedure_order_id = procedure_order.procedure_order_id AND " .
2300 "procedure_report.procedure_order_seq = procedure_order_code.procedure_order_seq AND " .
2301 "procedure_result.procedure_report_id = procedure_report.procedure_report_id ";
2305 if (empty($column)) {
2306 // simple search for any table entries
2307 $sql = sqlStatementCdrEngine("SELECT * " .
2308 "FROM `" . escape_table_name($table) . "` " .
2309 " " . $whereTables . " " .
2310 "WHERE " . add_escape_custom($patient_id_label) . "=? " . $customSQL, array($patient_id));
2312 // mdsupport : Allow trailing '**' in the strings to perform LIKE searches
2313 if ((substr($data, -2) == '**') && (($compSql == "=") ||
($compSql == "!="))) {
2314 $compSql = ($compSql == "!=" ?
" NOT" : "") . " LIKE CONCAT('%',?,'%') ";
2315 $data = substr_replace($data, '', -2);
2317 $compSql = $compSql . "? ";
2320 if ($whereTables == "" && strpos($table, 'form_') !== false) {
2321 //To handle standard forms starting with form_
2322 //In this case, we are assuming the date field is "date"
2323 $sql = sqlStatementCdrEngine(
2324 "SELECT b.`" . escape_sql_column_name($column, [$table]) . "` " .
2326 "LEFT JOIN `" . escape_table_name($table) . "` " . " b " .
2327 "ON (a.form_id=b.id AND a.formdir LIKE '" . add_escape_custom(substr($table, 5)) . "') " .
2328 "WHERE a.deleted != '1' " .
2329 "AND b.`" . escape_sql_column_name($column, [$table]) . "`" . $compSql .
2330 "AND b." . add_escape_custom($patient_id_label) . "=? " . $customSQL
2331 . str_replace("`date`", "b.`date`", $dateSql),
2332 array($data, $patient_id)
2335 // This allows to enter the wild card #CURDATE# in the CDR Demographics filter criteria at the value field
2336 // #CURDATE# is replace by the Current date allowing a dynamic date filtering
2337 if ($data == '#CURDATE#') {
2338 $data = date("Y-m-d");
2341 // search for number of specific items
2342 $sql = sqlStatementCdrEngine("SELECT `" . escape_sql_column_name($column, [$table]) . "` " .
2343 "FROM `" . escape_table_name($table) . "` " .
2344 " " . $whereTables . " " .
2345 "WHERE `" . escape_sql_column_name($column, [$table]) . "`" . $compSql .
2346 "AND " . add_escape_custom($patient_id_label) . "=? " . $customSQL .
2347 $dateSql, array($data, $patient_id));
2351 // See if number of returned items passes the comparison
2352 return itemsNumberCompare($num_items_comp, $num_items_thres, sqlNumRows($sql));
2356 * Function to check for existence of procedure(s) for a patient
2358 * @param string $patient_id pid of selected patient.
2359 * @param string $proc_title procedure title
2360 * @param string $proc_code procedure identifier code (array of <type(ICD9,CPT4)>:<identifier>||<type(ICD9,CPT4)>:<identifier>||<identifier> etc.)
2361 * @param string $results_comp results comparison (eq,ne,gt,ge,lt,le)
2362 * @param string $result_data results data (1)
2363 * @param string $num_items_comp number items comparison (eq,ne,gt,ge,lt,le)
2364 * @param integer $num_items_thres number of items threshold
2365 * @param string $intervalType type of interval (ie. year)
2366 * @param integer $intervalValue searched for within this many times of the interval type
2367 * @param string $dateTarget target date(format Y-m-d H:i:s).
2368 * @return boolean true if check passed, otherwise false
2370 * (1) If result_data ends with **, operators ne/eq are replaced by (NOT)LIKE operators
2373 function exist_procedure_item($patient_id, $proc_title, $proc_code, $result_comp, string $result_data = null, $num_items_comp = null, $num_items_thres = null, $intervalType = '', $intervalValue = '', $dateTarget = '')
2376 // Set date to current if not set
2377 $dateTarget = ($dateTarget) ?
$dateTarget : date('Y-m-d H:i:s');
2379 // Set the table exception (for looking up pertinent date and pid sql columns)
2380 $table = "PROCEDURE-EXCEPTION";
2382 // Collect the correct column label for patient id in the table
2383 $patient_id_label = collect_database_label('pid', $table);
2385 // Get the interval sql query string
2386 $dateSql = sql_interval_string($table, $intervalType, $intervalValue, $dateTarget);
2388 // If just checking for existence (ie result_data is empty),
2389 // then simply set the comparison operator to ne.
2390 if (empty($result_data)) {
2391 $result_comp = "ne";
2394 // get the appropriate sql comparison operator
2395 $compSql = convertCompSql($result_comp);
2397 // explode the code array
2399 if (!empty($proc_code)) {
2400 $codes = explode("||", $proc_code);
2405 // ensure proc_title is at least blank
2406 if (empty($proc_title)) {
2410 // collect specific items (use both title and/or codes) that fulfill request
2411 $sqlBindArray = array();
2412 $sql_query = "SELECT procedure_result.result FROM " .
2413 "procedure_order_code, " .
2414 "procedure_order, " .
2415 "procedure_type, " .
2416 "procedure_report, " .
2417 "procedure_result " .
2419 "procedure_order_code.procedure_code = procedure_type.procedure_code AND " .
2420 "procedure_order.procedure_order_id = procedure_order_code.procedure_order_id AND " .
2421 "procedure_order.lab_id = procedure_type.lab_id AND " .
2422 "procedure_report.procedure_order_id = procedure_order.procedure_order_id AND " .
2423 "procedure_report.procedure_order_seq = procedure_order_code.procedure_order_seq AND " .
2424 "procedure_result.procedure_report_id = procedure_report.procedure_report_id AND " .
2425 "procedure_type.procedure_type = 'ord' AND ";
2426 foreach ($codes as $tem) {
2427 $sql_query .= "( ( (procedure_type.standard_code = ? AND procedure_type.standard_code != '') " .
2428 "OR (procedure_type.procedure_code = ? AND procedure_type.procedure_code != '') ) OR ";
2429 array_push($sqlBindArray, $tem, $tem);
2432 // mdsupport : Allow trailing '**' in the strings to perform LIKE searches
2433 if ((substr($result_data, -2) == '**') && (($compSql == "=") ||
($compSql == "!="))) {
2434 $compSql = ($compSql == "!=" ?
" NOT" : "") . " LIKE CONCAT('%',?,'%') ";
2435 $result_data = substr_replace($result_data, '', -2);
2437 $compSql = $compSql . "? ";
2440 $sql_query .= "(procedure_type.name = ? AND procedure_type.name != '') ) " .
2441 "AND procedure_result.result " . $compSql .
2442 "AND " . add_escape_custom($patient_id_label) . " = ? " . $dateSql;
2443 array_push($sqlBindArray, $proc_title, $result_data, $patient_id);
2445 $sql = sqlStatementCdrEngine($sql_query, $sqlBindArray);
2447 // See if number of returned items passes the comparison
2448 return itemsNumberCompare($num_items_comp, $num_items_thres, sqlNumRows($sql));
2452 * Function to check for existance of data for a patient in the rule_patient_data table
2454 * @param string $patient_id pid of selected patient.
2455 * @param string $category label in category column
2456 * @param string $item label in item column
2457 * @param string $complete label in complete column (YES,NO, or blank)
2458 * @param string $num_items_comp number items comparison (eq,ne,gt,ge,lt,le)
2459 * @param integer $num_items_thres number of items threshold
2460 * @param string $intervalType type of interval (ie. year)
2461 * @param integer $intervalValue searched for within this many times of the interval type
2462 * @param string $dateTarget target date(format Y-m-d H:i:s).
2463 * @return boolean true if check passed, otherwise false
2465 function exist_custom_item($patient_id, $category, $item, $complete, $num_items_comp, $num_items_thres, string $intervalType = null, string $intervalValue = null, $dateTarget = null)
2469 $table = 'rule_patient_data';
2471 // Collect the correct column label for patient id in the table
2472 $patient_id_label = collect_database_label('pid', $table);
2474 // Get the interval sql query string
2475 $dateSql = sql_interval_string($table, $intervalType, $intervalValue, $dateTarget);
2477 // search for number of specific items
2478 $sql = sqlStatementCdrEngine("SELECT `result` " .
2479 "FROM `" . escape_table_name($table) . "` " .
2480 "WHERE `category`=? " .
2482 "AND `complete`=? " .
2483 "AND `" . add_escape_custom($patient_id_label) . "`=? " .
2484 $dateSql, array($category,$item,$complete,$patient_id));
2486 // See if number of returned items passes the comparison
2487 return itemsNumberCompare($num_items_comp, $num_items_thres, sqlNumRows($sql));
2491 * Function to check for existance of data for a patient in lifestyle section
2493 * @param string $patient_id pid of selected patient.
2494 * @param string $lifestyle selected label of mysql column of patient history
2495 * @param string $status specific status of selected lifestyle element
2496 * @param string $dateTarget target date(format Y-m-d H:i:s). blank is current date.
2497 * @return boolean true if check passed, otherwise false
2499 function exist_lifestyle_item($patient_id, $lifestyle, $status, $dateTarget)
2502 // Set date to current if not set
2503 $dateTarget = ($dateTarget) ?
$dateTarget : date('Y-m-d H:i:s');
2505 // Collect pertinent history data
2506 // If illegal value in $lifestyle, then will die and report error (to prevent security vulnerabilities)
2507 escape_sql_column_name($lifestyle, ['history_data']);
2508 $history = getHistoryData($patient_id, $lifestyle, '', $dateTarget);
2511 $stringFlag = strstr(($history[$lifestyle] ??
''), "|" . $status);
2512 if (empty($status)) {
2513 // Only ensuring any data has been entered into the field
2517 return !empty($history[$lifestyle]) &&
2518 $history[$lifestyle] != '|0|' &&
2523 * Function to check for lists item of a patient. Fully customizable and includes diagnoses, medications,
2524 * allergies, and surgeries.
2526 * @param string $patient_id pid of selected patient.
2527 * @param string $type type (medical_problem, allergy, medication, etc)
2528 * @param string $value value searching for (1)
2529 * @param string $dateTarget target date(format Y-m-d H:i:s).
2530 * @return boolean true if check passed, otherwise false
2532 * (1) If value ends with **, operators ne/eq are replaced by (NOT)LIKE operators
2535 function exist_lists_item($patient_id, $type, $value, $dateTarget)
2538 // Set date to current if not set
2539 $dateTarget = ($dateTarget) ?
$dateTarget : date('Y-m-d H:i:s');
2541 // Attempt to explode the value into a code type and code (if applicable)
2542 $value_array = explode("::", $value);
2543 if (count($value_array) == 2) {
2544 // Collect the code type and code
2545 $code_type = $value_array[0];
2546 $code = $value_array[1];
2548 // Modify $code for both 'CUSTOM' and diagnosis searches
2549 // Note: Diagnosis is always 'LIKE' and should not have '**'
2550 if (substr($code, -2) == '**') {
2551 $sqloper = " LIKE CONCAT('%',?,'%') ";
2552 $code = substr_replace($code, '', -2);
2557 if ($code_type == 'CUSTOM') {
2558 // Deal with custom code type first (title column in lists table)
2559 $response = sqlQueryCdrEngine("SELECT * FROM `lists` " .
2562 "AND `title` $sqloper " .
2563 "AND ( (`begdate` IS NULL AND `date`<=?) OR (`begdate` IS NOT NULL AND `begdate`<=?) ) " .
2564 "AND ( (`enddate` IS NULL) OR (`enddate` IS NOT NULL AND `enddate`>=?) )", array($type,$patient_id,$code,$dateTarget,$dateTarget,$dateTarget));
2565 if (!empty($response)) {
2569 // Deal with the set code types (diagnosis column in lists table)
2570 $response = sqlQueryCdrEngine("SELECT * FROM `lists` " .
2573 "AND `diagnosis` LIKE ? " .
2574 "AND ( (`begdate` IS NULL AND `date`<=?) OR (`begdate` IS NOT NULL AND `begdate`<=?) ) " .
2575 "AND ( (`enddate` IS NULL) OR (`enddate` IS NOT NULL AND `enddate`>=?) )", array($type,$patient_id,"%" . $code_type . ":" . $code . "%",$dateTarget,$dateTarget,$dateTarget));
2576 if (!empty($response)) {
2580 } else { // count($value_array) == 1
2581 // Search the title column in lists table
2582 // Yes, this is essentially the same as the code type listed as CUSTOM above. This provides flexibility and will ensure compatibility.
2585 if (substr($value, -2) == '**') {
2586 $sqloper = " LIKE CONCAT('%',?,'%') ";
2587 $value = substr_replace($value, '', -2);
2592 $response = sqlQueryCdrEngine("SELECT * FROM `lists` " .
2595 "AND `title` $sqloper " .
2596 "AND ( (`begdate` IS NULL AND `date`<=?) OR (`begdate` IS NOT NULL AND `begdate`<=?) ) " .
2597 "AND ( (`enddate` IS NULL) OR (`enddate` IS NOT NULL AND `enddate`>=?) )", array($type,$patient_id,$value,$dateTarget,$dateTarget,$dateTarget));
2598 if (!empty($response)) {
2602 if ($type == 'medication') { // Special case needed for medication as it need to be looked into current medications (prescriptions table) from ccda import
2603 $response = sqlQueryCdrEngine("SELECT * FROM `prescriptions` where `patient_id` = ? and `drug` $sqloper and `date_added` <= ?", array($patient_id,$value,$dateTarget));
2604 if (!empty($response)) {
2614 * Function to return part of sql query to deal with interval
2616 * @param string $table selected mysql table (or EXCEPTION(s))
2617 * @param string $intervalType type of interval (ie. year)
2618 * @param string $intervalValue searched for within this many times of the interval type
2619 * @param string $dateTarget target date(format Y-m-d H:i:s).
2620 * @return string contains pertinent date interval filter for mysql query
2622 function sql_interval_string($table, $intervalType, $intervalValue, $dateTarget)
2627 // Collect the correct column label for date in the table
2628 $date_label = collect_database_label('date', $table);
2630 // Deal with interval
2631 if (!empty($intervalType)) {
2632 switch ($intervalType) {
2634 $dateSql = "AND (" . add_escape_custom($date_label) .
2635 " BETWEEN DATE_SUB('" . add_escape_custom($dateTarget) .
2636 "', INTERVAL " . escape_limit($intervalValue) .
2637 " YEAR) AND '" . add_escape_custom($dateTarget) . "') ";
2640 $dateSql = "AND (" . add_escape_custom($date_label) .
2641 " BETWEEN DATE_SUB('" . add_escape_custom($dateTarget) .
2642 "', INTERVAL " . escape_limit($intervalValue) .
2643 " MONTH) AND '" . add_escape_custom($dateTarget) . "') ";
2646 $dateSql = "AND (" . add_escape_custom($date_label) .
2647 " BETWEEN DATE_SUB('" . add_escape_custom($dateTarget) .
2648 "', INTERVAL " . escape_limit($intervalValue) .
2649 " WEEK) AND '" . add_escape_custom($dateTarget) . "') ";
2652 $dateSql = "AND (" . add_escape_custom($date_label) .
2653 " BETWEEN DATE_SUB('" . add_escape_custom($dateTarget) .
2654 "', INTERVAL " . escape_limit($intervalValue) .
2655 " DAY) AND '" . add_escape_custom($dateTarget) . "') ";
2658 $dateSql = "AND (" . add_escape_custom($date_label) .
2659 " BETWEEN DATE_SUB('" . add_escape_custom($dateTarget) .
2660 "', INTERVAL " . escape_limit($intervalValue) .
2661 " HOUR) AND '" . add_escape_custom($dateTarget) . "') ";
2664 $dateSql = "AND (" . add_escape_custom($date_label) .
2665 " BETWEEN DATE_SUB('" . add_escape_custom($dateTarget) .
2666 "', INTERVAL " . escape_limit($intervalValue) .
2667 " MINUTE) AND '" . add_escape_custom($dateTarget) . "') ";
2670 $dateSql = "AND (" . add_escape_custom($date_label) .
2671 " BETWEEN DATE_SUB('" . add_escape_custom($dateTarget) .
2672 "', INTERVAL " . escape_limit($intervalValue) .
2673 " SECOND) AND '" . add_escape_custom($dateTarget) . "') ";
2676 // Flu season to be hard-coded as September thru February
2677 // (Should make this modifiable in the future)
2678 // ($intervalValue is not used)
2679 $dateArray = explode("-", $dateTarget);
2680 $Year = $dateArray[0];
2681 $dateThisYear = $Year . "-09-01";
2682 $dateLastYear = ($Year - 1) . "-09-01";
2685 "MONTH('" . add_escape_custom($dateTarget) . "') < 9 " .
2686 "AND " . add_escape_custom($date_label) . " >= '" . $dateLastYear . "' ) " .
2688 "MONTH('" . add_escape_custom($dateTarget) . "') >= 9 " .
2689 "AND " . add_escape_custom($date_label) . " >= '" . $dateThisYear . "' ))" .
2690 "AND " . add_escape_custom($date_label) . " <= '" . add_escape_custom($dateTarget) . "' ";
2694 $dateSql = "AND " . add_escape_custom($date_label) .
2695 " <= '" . add_escape_custom($dateTarget) . "' ";
2698 // return the sql interval string
2703 * Function to collect generic column labels from tables. It currently works for date
2704 * and pid. Will need to expand this as algorithm grows.
2706 * @param string $label element (pid or date)
2707 * @param string $table selected mysql table (or EXCEPTION(s))
2708 * @return string contains official label of selected element
2710 function collect_database_label($label, $table)
2713 if ($table == 'PROCEDURE-EXCEPTION') {
2714 // return cell to get procedure collection
2715 // special case since reuqires joing of multiple
2716 // tables to get this value
2717 if ($label == "pid") {
2718 $returnedLabel = "procedure_order.patient_id";
2719 } elseif ($label == "date") {
2720 $returnedLabel = "procedure_report.date_collected";
2722 // unknown label, so return the original label
2723 $returnedLabel = $label;
2725 } elseif ($table == 'immunizations') {
2726 // return requested label for immunization table
2727 if ($label == "pid") {
2728 $returnedLabel = "patient_id";
2729 } elseif ($label == "date") {
2730 $returnedLabel = "`administered_date`";
2732 // unknown label, so return the original label
2733 $returnedLabel = $label;
2735 } elseif ($table == 'prescriptions') {
2736 // return requested label for prescriptions table
2737 if ($label == "pid") {
2738 $returnedLabel = "patient_id";
2739 } elseif ($label == "date") {
2740 $returnedLabel = 'date_added';
2742 // unknown label, so return the original label
2743 $returnedLabel = $label;
2745 } elseif ($table == 'procedure_result') {
2746 // return requested label for prescriptions table
2747 if ($label == "pid") {
2748 $returnedLabel = "procedure_order.patient_id";
2749 } elseif ($label == "date") {
2750 $returnedLabel = "procedure_report.date_collected";
2752 // unknown label, so return the original label
2753 $returnedLabel = $label;
2755 } elseif ($table == 'openemr_postcalendar_events') {
2756 // return requested label for prescriptions table
2757 if ($label == "pid") {
2758 $returnedLabel = "pc_pid";
2759 } elseif ($label == "date") {
2760 $returnedLabel = "pc_eventdate";
2762 // unknown label, so return the original label
2763 $returnedLabel = $label;
2766 // return requested label for default tables
2767 if ($label == "pid") {
2768 $returnedLabel = "pid";
2769 } elseif ($label == "date") {
2770 $returnedLabel = "`date`";
2772 // unknown label, so return the original label
2773 $returnedLabel = $label;
2777 return $returnedLabel;
2781 * Simple function to avoid processing of duplicate actions
2783 * @param array $actions 2-dimensional array with all current active targets
2784 * @param array $action array of selected target to test for duplicate
2785 * @return boolean true if duplicate, false if not duplicate
2787 function is_duplicate_action($actions, $action)
2789 foreach ($actions as $row) {
2791 $row['category'] == $action['category'] &&
2792 $row['item'] == $action['item'] &&
2793 $row['value'] == $action['value']
2805 * Calculate the reminder dates.
2807 * This function returns an array that contains three elements (each element is a date).
2808 * <pre>The three dates are:
2809 * first date is before the target date (past_due) (default of 1 month)
2810 * second date is the target date (due)
2811 * third date is after the target date (soon_due) (default of 2 weeks)
2814 * @param string $rule id(string) of selected rule
2815 * @param string $dateTarget target date(format Y-m-d H:i:s).
2816 * @param string $type either 'patient_reminder' or 'clinical_reminder'
2817 * @return array see above for description of returned array
2819 function calculate_reminder_dates($rule, string $dateTarget = null, $type = null)
2822 // Set date to current if not set
2823 $dateTarget = ($dateTarget) ?
$dateTarget : date('Y-m-d H:i:s');
2825 // Collect the current date settings (to ensure not skip)
2826 $res = resolve_reminder_sql($rule, $type . '_current');
2829 if ($row ['method_detail'] == "SKIP") {
2830 $dateTarget = "SKIP";
2834 // Collect the past_due date
2835 $past_due_date = "";
2836 $res = resolve_reminder_sql($rule, $type . '_post');
2839 if ($row ['method_detail'] == "week") {
2840 $past_due_date = date("Y-m-d H:i:s", strtotime($dateTarget . " -" . $row ['value'] . " week"));
2843 if ($row ['method_detail'] == "month") {
2844 $past_due_date = date("Y-m-d H:i:s", strtotime($dateTarget . " -" . $row ['value'] . " month"));
2847 if ($row ['method_detail'] == "hour") {
2848 $past_due_date = date("Y-m-d H:i:s", strtotime($dateTarget . " -" . $row ['value'] . " hour"));
2851 if ($row ['method_detail'] == "SKIP") {
2852 $past_due_date = "SKIP";
2855 // empty settings, so use default of one month
2856 $past_due_date = date("Y-m-d H:i:s", strtotime($dateTarget . " -1 month"));
2859 // Collect the soon_due date
2860 $soon_due_date = "";
2861 $res = resolve_reminder_sql($rule, $type . '_pre');
2864 if ($row ['method_detail'] == "week") {
2865 $soon_due_date = date("Y-m-d H:i:s", strtotime($dateTarget . " +" . $row ['value'] . " week"));
2868 if ($row ['method_detail'] == "month") {
2869 $soon_due_date = date("Y-m-d H:i:s", strtotime($dateTarget . " +" . $row ['value'] . " month"));
2872 if ($row ['method_detail'] == "hour") {
2873 $soon_due_date = date("Y-m-d H:i:s", strtotime($dateTarget . " -" . $row ['value'] . " hour"));
2876 if ($row ['method_detail'] == "SKIP") {
2877 $soon_due_date = "SKIP";
2880 // empty settings, so use default of one month
2881 $soon_due_date = date("Y-m-d H:i:s", strtotime($dateTarget . " +2 week"));
2884 // Return the array of three dates
2885 return array($soon_due_date,$dateTarget,$past_due_date);
2889 * Adds an action into the reminder array
2891 * @param array $reminderOldArray Contains the current array of reminders
2892 * @param array $reminderNew Array of a new reminder
2893 * @return array Reminders
2895 function reminder_results_integrate($reminderOldArray, $reminderNew)
2900 // If reminderArray is empty, then insert new reminder
2901 if (empty($reminderOldArray)) {
2902 $results[] = $reminderNew;
2906 // If duplicate reminder, then replace the old one
2908 foreach ($reminderOldArray as $reminderOld) {
2910 $reminderOld['pid'] == $reminderNew['pid'] &&
2911 $reminderOld['category'] == $reminderNew['category'] &&
2912 $reminderOld['item'] == $reminderNew['item']
2914 $results[] = $reminderNew;
2917 $results[] = $reminderOld;
2921 // If a new reminder, then insert the new reminder
2923 $results[] = $reminderNew;
2930 * Compares number of items with requested comparison operator
2932 * @param string $comp Comparison operator(eq,ne,gt,ge,lt,le)
2933 * @param string $thres Threshold used in comparison
2934 * @param integer $num_items Number of items
2935 * @return boolean Comparison results
2937 function itemsNumberCompare($comp, $thres, $num_items)
2940 if (($comp == "eq") && ($num_items == $thres)) {
2942 } elseif (($comp == "ne") && ($num_items != $thres) && ($num_items > 0)) {
2944 } elseif (($comp == "gt") && ($num_items > $thres)) {
2946 } elseif (($comp == "ge") && ($num_items >= $thres)) {
2948 } elseif (($comp == "lt") && ($num_items < $thres) && ($num_items > 0)) {
2950 } elseif (($comp == "le") && ($num_items <= $thres) && ($num_items > 0)) {
2958 * Converts a text comparison operator to sql equivalent
2960 * @param string $comp Comparison operator(eq,ne,gt,ge,lt,le)
2961 * @return string contains sql compatible comparison operator
2963 function convertCompSql($comp)
2966 if ($comp == "eq") {
2968 } elseif ($comp == "ne") {
2970 } elseif ($comp == "gt") {
2972 } elseif ($comp == "ge") {
2974 } elseif ($comp == "lt") {
2976 } else { // ($comp == "le")
2983 * Function to find age in years (with decimal) on the target date
2985 * @param string $dob date of birth
2986 * @param string $target date to calculate age on
2987 * @return float years(decimal) from dob to target(date)
2989 function convertDobtoAgeYearDecimal($dob, $target)
2991 $ageInfo = parseAgeInfo($dob, $target);
2992 return $ageInfo['age'];
2996 * Function to find age in months (with decimal) on the target date
2998 * @param string $dob date of birth
2999 * @param string $target date to calculate age on
3000 * @return float months(decimal) from dob to target(date)
3002 function convertDobtoAgeMonthDecimal($dob, $target)
3004 $ageInfo = parseAgeInfo($dob, $target);
3005 return $ageInfo['age_in_months'];
3009 * Function to calculate the percentage for reports.
3011 * @param integer $pass_filter number of patients that pass filter
3012 * @param integer $exclude_filter number of patients that are excluded
3013 * @param integer $pass_target number of patients that pass target
3014 * @return string Number formatted into a percentage
3016 function calculate_percentage($pass_filt, $exclude_filt, $pass_targ)
3018 if ($pass_filt > 0) {
3019 $perc = number_format(($pass_targ / ($pass_filt - $exclude_filt)) * 100, 4) . xl('%');
3021 $perc = "0" . xl('%');