You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
225 lines
10 KiB
225 lines
10 KiB
<?php |
|
|
|
require_once("../globals.php"); |
|
require_once "$srcdir/options.inc.php"; |
|
require_once "$srcdir/report_database.inc"; |
|
// TODO: @adunsulag we need to move ALL this AMC stuff into a namespace. Any AMC classes should use autoloader not requires. |
|
require_once("../../library/classes/rulesets/library/RsReportIF.php"); |
|
require_once("../../library/classes/rulesets/library/RsUnimplementedIF.php"); |
|
require_once("../../library/classes/rulesets/library/RsPatient.php"); |
|
require_once("../../library/classes/rulesets/library/RsPopulation.php"); |
|
require_once("../../library/classes/rulesets/library/RsResultIF.php"); |
|
require_once("../../library/classes/rulesets/ReportTypes.php"); |
|
require_once("../../library/classes/rulesets/library/RsReportFactoryAbstract.php"); |
|
require_once("../../library/classes/rulesets/Amc/AmcReportFactory.php"); |
|
|
|
use OpenEMR\ClinicialDecisionRules\AMC\CertificationReportTypes; |
|
use OpenEMR\Common\Twig\TwigContainer; |
|
use OpenEMR\Common\Logging\SystemLogger; |
|
|
|
function formatPatientReportData($report_id, &$data, $type_report, $amc_report_types = array()) |
|
{ |
|
$dataSheet = json_decode($data, true) ?? []; |
|
$formatted = []; |
|
$main_pass_filter = 0; |
|
foreach ($dataSheet as $row) { |
|
$row['type'] = $type_report; |
|
$row['total_patients'] = $row['total_patients'] ?? 0; |
|
$failed_items = null; |
|
$displayFieldSubHeader = ""; |
|
|
|
$row['type'] = 'amc'; |
|
if (!empty($amc_report_types[$type_report]['code_col'])) { |
|
$code_col = $amc_report_types[$type_report]['code_col']; |
|
$displayFieldSubHeader .= " " . text($amc_report_types[$type_report]['abbr']) . ":" |
|
. text($row[$code_col]) . " "; |
|
} |
|
|
|
if (isset($row['is_main'])) { |
|
// note that the is_main record must always come before is_sub in the report or the data will not work. |
|
$main_pass_filter = $row['pass_filter'] ?? 0; |
|
$row['display_field'] = generate_display_field(array('data_type' => '1', 'list_id' => 'clinical_rules'), $row['id']); |
|
if ($type_report == "standard") { |
|
// Excluded is not part of denominator in standard rules so do not use in calculation |
|
$failed_items = $row['pass_filter'] - $row['pass_target']; |
|
} else { |
|
$failed_items = $row['pass_filter'] - $row['pass_target'] - $row['excluded']; |
|
} |
|
$row['display_field_sub'] = ($displayFieldSubHeader != "") ? "($displayFieldSubHeader)" : null; |
|
} else if (isset($row['is_sub'])) { |
|
$row['display_field'] = generate_display_field(array('data_type' => '1', 'list_id' => 'rule_action_category'), $row['action_category']) |
|
. ': ' . generate_display_field(array('data_type' => '1', 'list_id' => 'rule_action'), $row['action_item']); |
|
// Excluded is not part of denominator in standard rules so do not use in calculation |
|
$failed_items = $main_pass_filter - $row['pass_target']; |
|
} else if (isset($row['is_plan'])) { |
|
$row['display_field'] = generate_display_field(array('data_type' => '1', 'list_id' => 'clinical_plans'), $row['id']); |
|
} |
|
|
|
// this is very inefficient, but we are doing it just to get the report written for now. |
|
if (isset($row['itemized_test_id'])) { |
|
$row['itemized_patients'] = collectItemizedPatientData($report_id, $row['itemized_test_id']); |
|
} |
|
|
|
$row['failed_items'] = $failed_items; |
|
$formatted[] = $row; |
|
} |
|
|
|
|
|
return $formatted; |
|
} |
|
|
|
function collectItemizedPatientData($report_id, $itemized_test_id) |
|
{ |
|
$has_results = true; |
|
$batchSize = 100; // we will do 100 results at a time so we don't overload the MySQL server |
|
$reportDataByPid = []; |
|
$numHeaders = []; |
|
$denomHeaders = []; |
|
$index = 0; |
|
$infiniteLoopMax = 100000; // 100*10^5 = 10 million records is the max we'll support without failing |
|
while ($has_results && $index < $infiniteLoopMax) { |
|
$sql = "SELECT * FROM `report_itemized` WHERE report_id = ? AND itemized_test_id = ? LIMIT " . $index . "," . $batchSize; |
|
$sqlParameters = array($report_id, $itemized_test_id); |
|
$rez = sqlStatementCdrEngine($sql, $sqlParameters); |
|
if ($rez == false || $rez->EOF) { |
|
$has_results = false; |
|
continue; |
|
} |
|
|
|
for ($iter = $index; $row = sqlFetchArray($rez); $iter++) { |
|
$pid = $row['pid']; |
|
if (!isset($reportDataByPid[$pid])) { |
|
$reportDataByPid[$pid] = []; |
|
} |
|
$reportDataByPid[$pid][] = $iter;// need to keep the indexes so we can populate patients later |
|
$hydratedRecord = ['patient' => $pid, 'numerator' => [], 'denominator' => []]; |
|
$ruleId = $row['rule_id'] ?? null; |
|
if (!empty($ruleId) && !empty($row['item_details'])) { |
|
if (empty($ruleObjectHash[$ruleId])) { |
|
$ruleObjectHash[$ruleId] = getRuleObjectForId($ruleId) ?? new AMC_Unimplemented(); |
|
} |
|
$data = json_decode($row['item_details'], true) ?? null; |
|
if (!empty($data)) { |
|
$data = $ruleObjectHash[$ruleId]->hydrateItemizedDataFromRecord($data)->getActionData(); |
|
|
|
$hydratedRecord['numerator'] = $data['numerator'] ?? []; |
|
$hydratedRecord['denominator'] = $data['denominator'] ?? []; |
|
|
|
// grab our headers |
|
foreach ($hydratedRecord['numerator'] as $key => $item) { |
|
if (empty($numHeaders[$key])) { |
|
$numHeaders[$key] = $item['label']; |
|
} |
|
// save memory usage by clearing out the label, we match with the header key |
|
unset($hydratedRecord['numerator'][$key]['label']); |
|
} |
|
foreach ($hydratedRecord['denominator'] as $key => $item) { |
|
if (empty($numHeaders[$key])) { |
|
$denomHeaders[$key] = $item['label']; |
|
} |
|
// save memory usage by clearing out the label, we match with the header key |
|
unset($hydratedRecord['denominator'][$key]['label']); |
|
} |
|
} |
|
} |
|
$reportData[$iter] = $hydratedRecord; |
|
} |
|
$index += $batchSize; |
|
} |
|
|
|
// now grab all the patients and let's populate here |
|
$sanitizedPids = array_map('intval', array_keys($reportDataByPid)); |
|
$totalPids = count($sanitizedPids); |
|
$aggregatedPatientRecords = []; |
|
if (!empty($sanitizedPids)) { |
|
for ($i = 0; $i < $totalPids; $i += $batchSize) { |
|
$slicedPids = array_slice($sanitizedPids, $i, $batchSize, true); |
|
|
|
$sql = "SELECT `patient_data`.*, DATE_FORMAT(`patient_data`.`DOB`,'%m/%d/%Y') as DOB_TS FROM patient_data WHERE pid " |
|
. " IN (" . implode(",", $slicedPids) . ")"; |
|
|
|
$rez = sqlStatementCdrEngine($sql, []); |
|
for ($iter = 0; $row = sqlFetchArray($rez); $iter++) { |
|
$record = ['patient' => $row, 'records' => []]; |
|
$pid = $row['pid']; |
|
// if its empty it means we processed it on a previous batch and everything's already been set so move on |
|
// we shouldn't ever have duplicate pids... since we've handled it at the application layer, but we will |
|
// be safe here. |
|
if (empty($reportDataByPid[$pid])) { |
|
continue; |
|
} |
|
// do we want to make this an object to preserve memory? Lot of duplicate counting |
|
foreach ($reportDataByPid[$pid] as $index) { |
|
unset($reportData[$index]['patient']); |
|
$record['records'][] = $reportData[$index]; |
|
// now remove the record so we conserve memory |
|
$reportData[$index] = null; // we set it to empty to clear it out and not reorder any internal array |
|
} |
|
|
|
unset($reportDataByPid[$pid]); |
|
$aggregatedPatientRecords[] = $record; |
|
} |
|
} |
|
} |
|
|
|
return ['patients' => $aggregatedPatientRecords, 'numeratorHeaders' => $numHeaders, 'denominatorHeaders' => $denomHeaders]; |
|
} |
|
|
|
function getRuleObjectForId($ruleId) |
|
{ |
|
|
|
try { |
|
$reportManager = new AmcReportFactory(); |
|
$rule = ReportTypes::getClassName($ruleId); |
|
$report = $reportManager->createReport($rule, ['id' => $ruleId], [], [], []); |
|
return $report; |
|
} catch (\Exception $error) { |
|
(new SystemLogger())->errorLogCaller("Failed to instantiate rule class for rule", ['rule_id' => $ruleId |
|
, 'message' => $error->getTraceAsString()]); |
|
} |
|
return null; |
|
} |
|
|
|
$report_id = (isset($_GET['report_id'])) ? trim($_GET['report_id']) : ""; |
|
|
|
// Collect the back variable, if pertinent |
|
$back_link = (isset($_GET['back'])) ? trim($_GET['back']) : ""; |
|
$twigContainer = new TwigContainer(null, $GLOBALS['kernel']); |
|
$twig = $twigContainer->getTwig(); |
|
$report_view = collectReportDatabase($report_id); |
|
if (!empty($report_view)) { |
|
$amc_report_types = CertificationReportTypes::getReportTypeRecords(); |
|
$type_report = $report_view['type']; |
|
$amc_report_data = $amc_report_types[$type_report] ?? array(); |
|
|
|
|
|
// See if showing an old report or creating a new report |
|
// now we are going to create our report |
|
$dataSheet = formatPatientReportData($report_id, $report_view['data'], true, false, $type_report, $amc_report_data); |
|
$form_provider = $_POST['form_provider'] ?? ''; |
|
|
|
$subTitle = ''; |
|
if ($report_view['provider'] == "group_calculation") { |
|
$subTitle = xl("Group Calculation Method"); |
|
} else if (is_numeric($report_view['provider'])) { |
|
// grab the provider |
|
$userService = new \OpenEMR\Services\UserService(); |
|
$provider = $userService->getUser($report_view['provider']); |
|
$providerTitle = ($provider['lname'] ?? '') . ',' . ($provider['fname'] ?? '') |
|
. ' (' . xl('NPI') . ':' . ($provider['npi'] ?? '') . ')'; |
|
$subTitle = xl("Individual Calculation Method") . ' - ' . trim($providerTitle); |
|
} |
|
$title = $amc_report_types[$type_report]['title']; |
|
$data = [ |
|
'report_id' => $report_id |
|
, 'collate_outer' => $form_provider == 'collate_outer' |
|
, 'datasheet' => $dataSheet |
|
, 'title' => $title |
|
, 'subTitle' => $subTitle |
|
, 'reportDate' => $report_view['date_report'] ?? '' |
|
]; |
|
|
|
echo $twig->render('reports/cqm/amc-full-report.html.twig', $data); |
|
} else { |
|
echo $twig->render('error/404.html.twig', ['statusCode' => 404]); |
|
}
|
|
|