Hide dashboard card 2 (#7423)
[openemr.git] / library / classes / rulesets / Amc / library / AbstractAmcReport.php
blob49dff6eba528a54cfa46b82c60adf0b4d815b04a
1 <?php
3 /**
4 * AbstractAmcReport class
6 * Copyright (C) 2011 Ken Chapple <ken@mi-squared.com>
7 * Copyright (C) 2015 Brady Miller <brady.g.miller@gmail.com>
9 * LICENSE: This program is free software; you can redistribute it and/or
10 * modify it under the terms of the GNU General Public License
11 * as published by the Free Software Foundation; either version 3
12 * of the License, or (at your option) any later version.
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License
18 * along with this program. If not, see <http://opensource.org/licenses/gpl-license.php>;.
20 * @package OpenEMR
21 * @author Ken Chapple <ken@mi-squared.com>
22 * @author Brady Miller <brady.g.miller@gmail.com>
23 * @author Discover and Change, Inc. <snielson@discoverandchange.com>
24 * @link http://www.open-emr.org
27 require_once(dirname(__FILE__) . "/../../library/RsFilterIF.php");
28 require_once('AmcFilterIF.php');
29 require_once('IAmcItemizedReport.php');
30 require_once(dirname(__FILE__) . "/../../../../clinical_rules.php");
31 require_once(dirname(__FILE__) . "/../../../../amc.php");
33 use OpenEMR\Common\Logging\SystemLogger;
34 use OpenEMR\Reports\AMC\Trackers\AMCItemTracker;
35 use OpenEMR\Reports\AMC\Trackers\AMCItemSkipTracker;
37 abstract class AbstractAmcReport implements RsReportIF
39 /**
40 * @var AmcPopulation Patient Population
42 protected $_amcPopulation;
44 protected $_resultsArray = array();
46 protected $_rowRule;
47 protected $_ruleId;
48 protected $_beginMeasurement;
49 protected $_endMeasurement;
51 protected $_manualLabNumber;
53 /**
54 * @var AMCItemTracker
56 protected $_aggregator;
58 /**
59 * @var SystemLogger
61 private $logger;
64 * @var int|null
66 protected $_billingFacilityId;
68 /**
69 * @var int|null
71 protected $_providerId;
73 public function __construct(array $rowRule, array $patientIdArray, $dateTarget, $options)
75 // require all .php files in the report's sub-folder
76 // TODO: This really needs to be moved to using our namespace autoloader... no point in doing a file stat check
77 // for every single rule we have, over and over again every time the rule is instantiated.
78 $className = get_class($this);
79 foreach (glob(dirname(__FILE__) . "/../reports/" . $className . "/*.php") as $filename) {
80 require_once($filename);
83 // require common .php files
84 foreach (glob(dirname(__FILE__) . "/../reports/common/*.php") as $filename) {
85 require_once($filename);
88 // require clinical types
89 foreach (glob(dirname(__FILE__) . "/../../../ClinicalTypes/*.php") as $filename) {
90 require_once($filename);
93 $this->_amcPopulation = new AmcPopulation($patientIdArray);
94 $this->_rowRule = $rowRule;
95 $this->_ruleId = isset($rowRule['id']) ? $rowRule['id'] : '';
96 // Parse measurement period, which is stored as array in $dateTarget ('dateBegin' and 'dateTarget').
97 $this->_beginMeasurement = $dateTarget['dateBegin'] ?? '';
98 $this->_endMeasurement = $dateTarget['dateTarget'] ?? '';
99 $this->_manualLabNumber = $options['labs_manual'] ?? 0;
101 if (isset($GLOBALS['report_itemizing_temp_flag_and_id']) && $GLOBALS['report_itemizing_temp_flag_and_id']) {
102 $this->_aggregator = $options['aggregator'] ?? new AMCItemTracker();
103 } else {
104 $this->_aggregator = new AMCItemSkipTracker();
106 $this->logger = new SystemLogger();
107 $this->logger->debug(get_class($this) . "->__construct() finished", ['patients' => $patientIdArray]);
109 $this->_billingFacilityId = $options['billing_facility_id'] ?? null;
110 $this->_providerId = $options['provider_id'] ?? null;
113 public function getPatientPopulation(): AmcPopulation
115 return $this->_amcPopulation;
118 abstract public function createNumerator();
119 abstract public function createDenominator();
120 abstract public function getObjectToCount();
122 public function getAggregator()
124 return $this->_aggregator;
127 public function getResults()
129 return $this->_resultsArray;
132 public function execute()
135 $this->logger->debug(get_class($this) . "->execute() starting function");
137 // If itemization is turned on, then iterate the rule id iterator
139 // Note that when AMC rules supports different patient populations and
140 // numerator calculation, then it will need to change placement of
141 // this and mimic the CQM rules mechanism
142 if ($GLOBALS['report_itemizing_temp_flag_and_id']) {
143 $GLOBALS['report_itemized_test_id_iterator']++;
146 $numerator = $this->createNumerator();
147 if (!$numerator instanceof AmcFilterIF) {
148 throw new Exception("Numerator must be an instance of AmcFilterIF");
151 $denominator = $this->createDenominator();
152 if (!$denominator instanceof AmcFilterIF) {
153 throw new Exception("Denominator must be an instance of AmcFilterIF");
156 $totalPatients = count($this->_amcPopulation);
158 // Figure out object to be counted
159 // (patients, labs, transitions, visits, or prescriptions)
160 $object_to_count = $this->getObjectToCount();
161 if (empty($object_to_count)) {
162 $object_to_count = "patients";
165 $this->logger->debug(get_class($this) . "->execute()", ['totalPatients' => $totalPatients, 'object_to_count' => $object_to_count]);
167 $numeratorObjects = 0;
168 $denominatorObjects = 0;
170 // Deal with the manually added labs for the electronic labs AMC measure
171 if ($object_to_count == "labs") {
172 // not sure how we account for individual reporting here.
173 $denominatorObjects = $this->_manualLabNumber;
174 $this->logger->debug(
175 get_class($this) . "->execute() manual labs processed",
176 ['numeratorObjects' => $numeratorObjects, 'denominatorObjects' => $denominatorObjects]
180 // we handle patients differently then patient referenced objects.
181 if ($object_to_count == "patients") {
182 $this->executeForPatients($numerator, $denominator, $numeratorObjects, $denominatorObjects);
183 } else {
184 $this->executeForPatientObjects($numerator, $denominator, $object_to_count, $numeratorObjects, $denominatorObjects);
188 $percentage = calculate_percentage($denominatorObjects, 0, $numeratorObjects);
190 $result = new AmcResult($this->_rowRule, $totalPatients, $denominatorObjects, 0, $numeratorObjects, $percentage);
191 $this->_resultsArray[] = &$result;
192 $this->logger->debug(get_class($this) . "->execute() leaving rule");
195 private function executeForPatients($numerator, $denominator, &$numeratorObjects, &$denominatorObjects)
197 $numeratorObjects = 0;
198 $denominatorObjects = 0;
200 foreach ($this->_amcPopulation as $patient) {
201 $pass = 0;
202 $numeratorResultItemDetails = new AmcItemizedActionData();
203 $denominatorResultItemDetails = new AmcItemizedActionData();
205 // If begin measurement is empty, then make the begin
206 // measurement the patient dob.
207 $tempBeginMeasurement = $this->getRuleBeginDateForPatient($patient);
209 // Counting patients
210 if (!$denominator->test($patient, $tempBeginMeasurement, $this->_endMeasurement)) {
211 continue;
213 if ($denominator instanceof IAmcItemizedReport) {
214 $denominatorResultItemDetails = $denominator->getItemizedDataForLastTest();
216 $denominatorObjects++;
218 if ($numerator->test($patient, $tempBeginMeasurement, $this->_endMeasurement)) {
219 $numeratorObjects++;
220 $pass = 1;
222 if ($numerator instanceof IAmcItemizedReport) {
223 $numeratorResultItemDetails = $numerator->getItemizedDataForLastTest();
225 // If itemization is turned on, then record the "passed" item
226 $this->_aggregator->addItem(
227 $GLOBALS['report_itemizing_temp_flag_and_id'],
228 $GLOBALS['report_itemized_test_id_iterator'],
229 $this->_ruleId,
230 $tempBeginMeasurement,
231 $this->_endMeasurement,
232 $pass,
233 $patient->id,
234 'patients',
235 $numeratorResultItemDetails,
236 $denominatorResultItemDetails
238 $this->logger->debug(
239 get_class($this) . "->execute() patient processed",
240 ['pid' => $patient->id, 'numeratorObjects' => $numeratorObjects, 'denominatorObjects' => $denominatorObjects]
245 private function getRuleBeginDateForPatient(AmcPatient $patient)
247 if (empty($this->_beginMeasurement)) {
248 $tempBeginMeasurement = $patient->dob;
249 } else {
250 $tempBeginMeasurement = $this->_beginMeasurement;
252 return $tempBeginMeasurement;
255 private function executeForPatientObjects($numerator, $denominator, $object_to_count, &$numeratorObjects, &$denominatorObjects)
257 foreach ($this->_amcPopulation as $patient) {
258 // If begin measurement is empty, then make the begin
259 // measurement the patient dob.
260 $tempBeginMeasurement = $this->getRuleBeginDateForPatient($patient);
262 // Counting objects other than patients
263 // First, collect the pertinent objects
264 $objects = $this->collectObjects($patient, $object_to_count, $tempBeginMeasurement, $this->_endMeasurement);
265 // Second, test each object
266 foreach ($objects as $object) {
267 $numeratorResultItemDetails = new AmcItemizedActionData();
268 $denominatorResultItemDetails = new AmcItemizedActionData();
270 $pass = 0;
271 $patient->object = $object;
272 if ($denominator->test($patient, $tempBeginMeasurement, $this->_endMeasurement)) {
273 $denominatorObjects++;
275 // now that the denominator passed, let's add in our object
276 if ($denominator instanceof IAmcItemizedReport) {
277 $denominatorResultItemDetails = $denominator->getItemizedDataForLastTest();
280 if ($numerator->test($patient, $tempBeginMeasurement, $this->_endMeasurement)) {
281 $numeratorObjects++;
282 $pass = 1;
285 if ($numerator instanceof IAmcItemizedReport) {
286 $numeratorResultItemDetails = $numerator->getItemizedDataForLastTest();
288 $this->_aggregator->addItem(
289 $GLOBALS['report_itemizing_temp_flag_and_id'],
290 $GLOBALS['report_itemized_test_id_iterator'],
291 $this->_ruleId,
292 $tempBeginMeasurement,
293 $this->_endMeasurement,
294 $pass,
295 $patient->id,
296 $object_to_count,
297 $numeratorResultItemDetails,
298 $denominatorResultItemDetails
302 $this->logger->debug(
303 get_class($this) . "->execute() patient processed",
304 ['pid' => $patient->id, 'numeratorObjects' => $numeratorObjects, 'denominatorObjects' => $denominatorObjects]
309 private function collectObjects($patient, $object_label, $begin, $end)
312 $results = array();
313 $sqlBindArray = array();
315 switch ($object_label) {
316 case "transitions-in":
317 $sql = "SELECT amc_misc_data.map_id as `encounter`, amc_misc_data.date_completed as `completed`, form_encounter.date as `date` " .
318 "FROM `amc_misc_data`, `form_encounter` " .
319 "INNER JOIN openemr_postcalendar_categories opc on opc.pc_catid = form_encounter.pc_catid " .
320 "WHERE amc_misc_data.map_id = form_encounter.encounter " .
321 "AND amc_misc_data.map_category = 'form_encounter' " .
322 "AND amc_misc_data.pid = ? AND form_encounter.pid = ? " .
323 "AND amc_misc_data.amc_id = 'med_reconc_amc' " .
324 "AND form_encounter.date >= ? AND form_encounter.date <= ? " .
325 "AND ((opc.pc_catname = 'New Patient') OR (opc.pc_catname = 'Established Patient' AND amc_misc_data.soc_provided is not null)) ";
326 array_push($sqlBindArray, $patient->id, $patient->id, $begin, $end);
327 break;
328 case "transitions-out":
329 return $this->collectTransitionOutObjects($patient, $begin, $end, $this->_billingFacilityId, $this->_providerId);
330 break;
331 case "encounters":
332 $sql = "SELECT * " .
333 "FROM `form_encounter` " .
334 "WHERE `pid` = ? " .
335 "AND `date` >= ? AND `date` <= ?";
336 array_push($sqlBindArray, $patient->id, $begin, $end);
337 break;
338 case "encounters_office_visit":
339 $sql = "SELECT * " .
340 "FROM `form_encounter` LEFT JOIN `enc_category_map` ON (form_encounter.pc_catid = enc_category_map.main_cat_id) " .
341 "WHERE enc_category_map.rule_enc_id = 'enc_off_vis' " .
342 "AND `pid` = ? " .
343 "AND `date` >= ? AND `date` <= ?";
344 array_push($sqlBindArray, $patient->id, $begin, $end);
345 break;
346 case "cpoe_medications":
347 $sql = "SELECT `drug` " .
348 "FROM `prescriptions` " .
349 "WHERE `patient_id` = ? " .
350 "AND `date_added` >= ? AND `date_added` <= ? ";
351 array_push($sqlBindArray, $patient->id, $begin, $end);
352 break;
353 case "prescriptions":
354 $sql = "SELECT * " .
355 "FROM `prescriptions` " .
356 "WHERE `patient_id` = ? " .
357 "AND `date_added` >= ? AND `date_added` <= ?";
358 array_push($sqlBindArray, $patient->id, $begin, $end);
359 break;
360 case "labs":
361 $sql = "SELECT procedure_result.result FROM " .
362 "procedure_order, " .
363 "procedure_report, " .
364 "procedure_result " .
365 "WHERE " .
366 "procedure_order.patient_id = ? AND " .
367 "procedure_order.procedure_order_id = procedure_report.procedure_order_id AND " .
368 "procedure_report.procedure_report_id = procedure_result.procedure_report_id AND " .
369 "procedure_report.date_collected >= ? AND procedure_report.date_collected <= ?";
370 array_push($sqlBindArray, $patient->id, $begin, $end);
371 break;
372 case "image_orders":
373 $sql = "SELECT pr.* FROM procedure_order pr " .
374 "INNER JOIN procedure_order_code prc ON pr.procedure_order_id = prc.procedure_order_id " .
375 "WHERE pr.patient_id = ? " .
376 "AND prc.procedure_order_title LIKE '%imaging%' " .
377 "AND (pr.date_ordered BETWEEN ? AND ?)";
378 array_push($sqlBindArray, $patient->id, $begin, $end);
379 break;
382 case "lab_radiology":
383 $sql = "SELECT pr.* FROM procedure_order pr " .
384 "INNER JOIN procedure_order_code prc ON pr.procedure_order_id = prc.procedure_order_id " .
385 "LEFT JOIN procedure_providers pp ON pr.lab_id = pp.ppid " .
386 "LEFT JOIN users u ON u.id = pp.lab_director " .
387 "WHERE pr.patient_id = ? " .
388 "AND prc.procedure_order_title LIKE '%imaging%' " .
389 "AND (pr.date_ordered BETWEEN ? AND ?)";
390 array_push($sqlBindArray, $patient->id, $begin, $end);
391 break;
393 case "cpoe_lab_orders":
394 $sql = "SELECT pr.* FROM procedure_order pr " .
395 "INNER JOIN procedure_order_code prc ON pr.procedure_order_id = prc.procedure_order_id " .
396 "LEFT JOIN procedure_providers pp ON pr.lab_id = pp.ppid " .
397 "LEFT JOIN users u ON u.id = pp.lab_director " .
398 "WHERE pr.patient_id = ? " .
399 "AND prc.procedure_order_title LIKE '%laboratory_test%' " .
400 "AND (pr.date_ordered BETWEEN ? AND ?)";
401 array_push($sqlBindArray, $patient->id, $begin, $end);
402 break;
404 case "med_orders":
405 // Still TODO
406 // AMC MU2 TODO :
407 // Note the cpoe_flag and functionality does not exist in OpenEMR official codebase.
409 $sql = "SELECT drug,erx_source as cpoe_stat " .
410 "FROM `prescriptions` " .
411 "WHERE `patient_id` = ? " .
412 "AND `date_added` BETWEEN ? AND ? ";
413 array_push($sqlBindArray, $patient->id, $begin, $end);
414 break;
416 case "lab_orders":
417 $sql = "SELECT prc.* FROM procedure_order pr " .
418 "INNER JOIN procedure_order_code prc ON pr.procedure_order_id = prc.procedure_order_id " .
419 "WHERE pr.patient_id = ? " .
420 "AND (prc.procedure_order_title LIKE '%Laboratory%' or (prc.procedure_source = 2 and prc.procedure_order_title is NULL)) " .
421 "AND (pr.date_ordered BETWEEN ? AND ?)";
422 array_push($sqlBindArray, $patient->id, $begin, $end);
423 break;
426 $rez = sqlStatement($sql, $sqlBindArray);
427 for ($iter = 0; $row = sqlFetchArray($rez); $iter++) {
428 $results[$iter] = $row;
431 return $results;
434 private function collectTransitionOutObjects($patient, $begin, $end, $billing_facility, $provider_id)
437 $results = [];
438 $sqlBindArray = [];
439 $sqlSelect = "SELECT transactions.id as id,transactions.date ";
440 $sqlFrom = "FROM transactions " .
441 "INNER JOIN lbt_data on lbt_data.form_id = transactions.id ";
442 $sqlWhere = "WHERE transactions.title = 'LBTref' " .
443 "AND transactions.pid = ? " .
444 "AND lbt_data.field_id = 'refer_date' " .
445 "AND lbt_data.field_value >= ? AND lbt_data.field_value <= ?";
447 array_push($sqlBindArray, $patient->id, $begin, $end);
449 // for group calculation methods we need to restrict the data to the billing facility and the provider who issued the referral
450 // this is because we may have patients that have been seen by the provider, but a DIFFERENT provider issued a referral for that patient
451 // without this filtering that patient's referrals would be counted
452 if (!empty($billing_facility) && intval($billing_facility) > 0) {
453 $sqlFrom .= ' INNER JOIN lbt_data lbt_billing_fac ON (lbt_billing_fac.form_id = transactions.id AND lbt_billing_fac.field_id = "billing_facility_id")';
454 $sqlWhere .= " AND lbt_billing_fac.field_value = ? ";
455 $sqlBindArray[] = intval($billing_facility);
458 if (!empty($provider_id) && intval($provider_id) > 0) {
459 $sqlFrom .= 'INNER JOIN lbt_data lbt_refer_from ON (lbt_refer_from.form_id = transactions.id AND lbt_refer_from.field_id = "refer_from") ';
460 $sqlWhere .= " AND lbt_refer_from.field_value = ? ";
461 $sqlBindArray[] = intval($provider_id);
464 $sql = $sqlSelect . $sqlFrom . $sqlWhere;
465 $rez = sqlStatement($sql, $sqlBindArray);
466 for ($iter = 0; $row = sqlFetchArray($rez); $iter++) {
467 $fres = sqlStatement(
468 "SELECT field_id, field_value FROM lbt_data WHERE form_id = ?",
469 array($row['id'])
471 while ($frow = sqlFetchArray($fres)) {
472 $row[$frow['field_id']] = $frow['field_value'];
474 $results[$iter] = $row;
476 return $results;
479 public function hydrateItemizedDataFromRecord($data): AmcItemizedActionData
483 $numerator = $this->createNumerator();
484 if ($numerator instanceof IAmcItemizedReport) {
485 $numeratorData = $numerator->hydrateItemizedDataFromRecord($data['numerator'] ?? []);
486 } else {
487 $numeratorData = new AmcItemizedActionData();
489 $denominator = $this->createDenominator();
490 if ($denominator instanceof IAmcItemizedReport) {
491 $denominatorData = $denominator->hydrateItemizedDataFromRecord($data['denominator'] ?? []);
492 } else {
493 $denominatorData = new AmcItemizedActionData();
495 $numeratorData->addActionObject($denominatorData);
496 return $numeratorData;