CCDA Testing fixes and prior commit conflicts (#5899)
[openemr.git] / interface / modules / zend_modules / module / Carecoordination / src / Carecoordination / Controller / CarecoordinationController.php
blob13cc9491b4e8541347383fbad81b888e13a9b387
1 <?php
3 /**
4 * interface/modules/zend_modules/module/Carecoordination/src/Carecoordination/Controller/CarecoordinationController.php
6 * @package OpenEMR
7 * @link https://www.open-emr.org
8 * @author Vinish K <vinish@zhservices.com>
9 * @author Chandni Babu <chandnib@zhservices.com>
10 * @author Riju KP <rijukp@zhservices.com>
11 * @copyright Copyright (c) 2014 Z&H Consultancy Services Private Limited <sam@zhservices.com>
12 * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
15 namespace Carecoordination\Controller;
17 use Application\Model\ApplicationTable;
18 use Application\Plugin\CommonPlugin;
19 use Laminas\Console\Request as ConsoleRequest;
20 use Laminas\Mvc\Controller\AbstractActionController;
21 use Laminas\View\Model\ViewModel;
22 use Laminas\View\Model\JsonModel;
23 use Application\Listener\Listener;
24 use Documents\Controller\DocumentsController;
25 use Carecoordination\Model\CarecoordinationTable;
26 use C_Document;
27 use Document;
28 use CouchDB;
29 use OpenEMR\Common\Logging\SystemLogger;
30 use OpenEMR\Services\Cda\CdaValidateDocuments;
31 use xmltoarray_parser_htmlfix;
33 class CarecoordinationController extends AbstractActionController
35 /**
36 * @var Carecoordination\Model\CarecoordinationTable
38 private $carecoordinationTable;
40 /**
41 * @var Documents\Controller\DocumentsController
43 private $documentsController;
45 public function __construct(CarecoordinationTable $table, DocumentsController $documentsController)
47 $this->carecoordinationTable = $table;
48 $this->listenerObject = new Listener();
49 $this->date_format = ApplicationTable::dateFormat($GLOBALS['date_display_format']);
50 $this->documentsController = $documentsController;
53 /**
54 * Index Page
56 * @param int $id menu id
57 * $param array $data menu details
58 * @param string $slug controller name
59 * @return \Laminas\View\Model\ViewModel
61 public function indexAction()
63 $this->redirect()->toRoute('encountermanager', array('action' => 'index'));
66 /**
67 * delete an audit record
69 * @return ViewModel
71 public function deleteAuditAction()
73 $request = $this->getRequest();
74 $amid = $request->getPost('am_id') ?? null;
75 if ($amid) {
76 $this->getCarecoordinationTable()->deleteImportAuditData(array('audit_master_id' => $amid));
78 $category_details = $this->getCarecoordinationTable()->fetch_cat_id('CCDA');
79 $records = $this->getCarecoordinationTable()->document_fetch(array('cat_title' => 'CCDA', 'type' => '12'));
80 $view = new ViewModel(array(
81 'records' => $records,
82 'category_id' => $category_details[0]['id'],
83 'file_location' => basename($_FILES['file']['name'] ?? ''),
84 'patient_id' => '00',
85 'listenerObject' => $this->listenerObject
86 ));
88 return $view;
92 * Upload CCDA file
94 public function uploadAction()
96 $request = $this->getRequest();
97 $action = $request->getPost('action');
98 $am_id = $request->getPost('am_id');
99 $document_id = $request->getPost('document_id');
101 if ($action == 'add_new_patient') {
102 $this->getCarecoordinationTable()->insert_patient($am_id, $document_id);
104 if (($request->getPost('chart_all_imports') ?? null) === 'true' && empty($action)) {
105 $records = $this->getCarecoordinationTable()->document_fetch(array('cat_title' => 'CCDA', 'type' => '12'));
106 foreach ($records as $record) {
107 if (!empty($record['matched_patient'])) {
108 // @todo figure out a way to make this auto. $data is array of doc changes.
109 // $this->getCarecoordinationTable()->insertApprovedData($data);
110 // meantime make user approve changes.
111 continue;
113 $this->getCarecoordinationTable()->insert_patient($record['amid'], $record['document_id']);
116 if (($request->getPost('delete_all_imports') ?? null) === 'true' && empty($action)) {
117 $records = $this->getCarecoordinationTable()->document_fetch(array('cat_title' => 'CCDA', 'type' => '12'));
118 foreach ($records as $record) {
119 $this->getCarecoordinationTable()->deleteImportAuditData(array('audit_master_id' => $record['amid']));
123 $upload = $request->getPost('upload');
124 $category_details = $this->getCarecoordinationTable()->fetch_cat_id('CCDA');
126 if ($upload == 1) {
127 $time_start = date('Y-m-d H:i:s');
128 $obj_doc = $this->documentsController;
129 if ($obj_doc->isZipUpload($request)) {
130 $this->importZipUpload($request);
131 } else {
132 $cdoc = $obj_doc->uploadAction($request);
133 $uploaded_documents = $this->getCarecoordinationTable()->fetch_uploaded_documents(
134 array('user' => $_SESSION['authUserID'], 'time_start' => $time_start, 'time_end' => date('Y-m-d H:i:s'))
136 if ($uploaded_documents[0]['id'] > 0) {
137 $_REQUEST["document_id"] = $uploaded_documents[0]['id'];
138 $_REQUEST["batch_import"] = 'YES';
139 $this->importAction();
142 } else {
143 $result = $this->Documents()->fetchXmlDocuments();
144 foreach ($result as $row) {
145 if ($row['doc_type'] == 'CCDA') {
146 $_REQUEST["document_id"] = $row['doc_id'];
147 $this->importAction();
148 $this->documentsController->getDocumentsTable()->updateDocumentCategoryUsingCatname($row['doc_type'], $row['doc_id']);
153 $records = $this->getCarecoordinationTable()->document_fetch(array('cat_title' => 'CCDA', 'type' => '12'));
154 foreach ($records as $key => $r) {
155 if (!empty($records[$key]['dupl_patient'] ?? null)) {
156 continue;
158 $name = $r['pat_name'];
159 foreach ($records as $k => $r1) {
160 $f = false;
161 $why = '';
162 if (!empty($r1['dupl_patient'] ?? null) || $key == $k) {
163 continue;
165 $n = $r1['pat_name'];
166 $fn = $r1['ad_fname'] == $r['ad_fname'];
167 $ln = $r1['ad_lname'] == $r['ad_lname'];
168 $dob = $r1['dob_raw'] == $r['dob_raw'];
169 if ($dob) {
170 $f = true;
171 $why = xlt('Match DOB');
173 if ($name == $n && ($f || $r1['race'] == $r['race'] || $r1['ethnicity'] == $r['ethnicity'])) {
174 if ($f) {
175 $why = xlt('Matched Demo and DOB');
176 } else {
177 $why = xlt('Matched Demo');
179 if ($r1['enc_count'] != $r['enc_count'] || $r1['cp_count'] != $r['cp_count'] || $r1['ob_count'] != $r['ob_count']) {
180 $why .= ' ' . xlt('with Mismatched Components');
182 $f = true;
184 if (
185 (($ln && !$fn || $fn && !$ln) && $dob)
186 && ($r1['race'] == $r['race'] || $r1['ethnicity'] == $r['ethnicity'])
188 $why = xlt('Name Misspelled');
189 if (
190 $r1['enc_count'] != $r['enc_count']
191 || $r1['cp_count'] != $r['cp_count']
192 || $r1['ob_count'] != $r['ob_count']
194 $why .= ' ' . xlt('with Mismatched Components');
196 $f = true;
198 if (($r1['is_qrda_document'] ?? 0) === 2) {
199 $f = false;
200 $records[$k]['dupl_patient'] = xlt('Empty Report. No QDM content.');
202 if ($f) {
203 $records[$key]['dupl_patient'] = $records[$k]['dupl_patient'] = $why;
208 $view = new ViewModel(array(
209 'records' => $records,
210 'category_id' => $category_details[0]['id'],
211 'file_location' => basename($_FILES['file']['name'] ?? ''),
212 'patient_id' => '00',
213 'listenerObject' => $this->listenerObject
215 // I haven't a clue why this delay is needed to allow batch to work from fetch.
216 if (!empty($upload)) {
217 sleep(1);
219 return $view;
222 public function newpatientImportCommandAction()
224 // get around a large ccda data array
225 ini_set("memory_limit", -1);
227 $request = $this->getRequest();
228 if (!$request instanceof ConsoleRequest) {
229 throw new RuntimeException('You can only use this action from a console!');
231 $document = $request->getParam('document');
232 $this->getCarecoordinationTable()->importNewPatient($document);
233 exit;
236 public function newpatientCommandAction()
238 $request = $this->getRequest();
239 if (!$request instanceof ConsoleRequest) {
240 throw new RuntimeException('You can only use this action from a console!');
242 $am_id = $request->getParam('am_id');
243 $document_id = $request->getParam('document_id');
244 $this->getCarecoordinationTable()->insert_patient($am_id, $document_id);
245 exit;
248 public function importCommandAction()
250 $request = $this->getRequest();
251 if (!$request instanceof ConsoleRequest) {
252 throw new RuntimeException('You can only use this action from a console!');
254 $document_id = $request->getParam('document_id');
255 $this->getCarecoordinationTable()->import($document_id);
256 exit;
260 * Function to import the data CCDA file to audit tables.
262 * @param document_id integer value
263 * @return \Laminas\View\Model\JsonModel
265 public function importAction()
267 $request = $this->getRequest();
268 if ($request->getQuery('document_id')) {
269 $_REQUEST["document_id"] = $request->getQuery('document_id');
270 $category_details = $this->getCarecoordinationTable()->fetch_cat_id('CCDA');
271 $this->documentsController->getDocumentsTable()->updateDocumentCategory($category_details[0]['id'], $_REQUEST["document_id"]);
274 $document_id = $_REQUEST["document_id"];
275 $this->getCarecoordinationTable()->import($document_id);
277 $view = new JsonModel();
278 $view->setTerminal(true);
279 return $view;
282 public function revandapproveAction()
284 $request = $this->getRequest();
285 $document_id = $request->getQuery('document_id') ? $request->getQuery('document_id') : $request->getPost('document_id', null);
286 $audit_master_id = $request->getQuery('amid') ? $request->getQuery('amid') : $request->getPost('amid', null);
287 $pid = $request->getQuery('pid') ? $request->getQuery('pid') : $request->getPost('pid', null);
289 if ($request->getPost('setval') == 'approve') {
290 $this->getCarecoordinationTable()->insertApprovedData($request->getPost());
291 return $this->redirect()->toRoute('carecoordination', array(
292 'controller' => 'Carecoordination',
293 'action' => 'upload'));
294 } elseif ($request->getPost('setval') == 'discard') {
295 $this->getCarecoordinationTable()->discardCCDAData(array('audit_master_id' => $audit_master_id));
296 return $this->redirect()->toRoute('carecoordination', array(
297 'controller' => 'Carecoordination',
298 'action' => 'upload'));
301 $documentationOf = $this->getCarecoordinationTable()->getdocumentationOf($audit_master_id);
302 $demographics = $this->getCarecoordinationTable()->getDemographics(array('audit_master_id' => $audit_master_id));
303 $demographics_old = $this->getCarecoordinationTable()->getDemographicsOld(array('pid' => $pid));
305 $problems = $this->getCarecoordinationTable()->getProblems(array('pid' => $pid));
306 $problems_audit = $this->getRevAndApproveAuditArray($audit_master_id, 'lists1');
308 $allergies = $this->getCarecoordinationTable()->getAllergies(array('pid' => $pid));
309 $allergies_audit = $this->getRevAndApproveAuditArray($audit_master_id, 'lists2');
311 $medications = $this->getCarecoordinationTable()->getMedications(array('pid' => $pid));
312 $medications_audit = $this->getRevAndApproveAuditArray($audit_master_id, 'lists3');
314 $immunizations = $this->getCarecoordinationTable()->getImmunizations(array('pid' => $pid));
315 $immunizations_audit = $this->getRevAndApproveAuditArray($audit_master_id, 'immunization');
317 $lab_results = $this->getCarecoordinationTable()->getLabResults(array('pid' => $pid));
318 $lab_results_audit = $this->getRevAndApproveAuditArray($audit_master_id, 'procedure_result');
320 $vitals = $this->getCarecoordinationTable()->getVitals(array('pid' => $pid));
321 $vitals_audit = $this->getRevAndApproveAuditArray($audit_master_id, 'vital_sign');
323 $social_history = $this->getCarecoordinationTable()->getSocialHistory(array('pid' => $pid));
324 $social_history_audit = $this->getRevAndApproveAuditArray($audit_master_id, 'social_history');
326 $encounter = $this->getCarecoordinationTable()->getEncounterData(array('pid' => $pid));
327 $encounter_audit = $this->getRevAndApproveAuditArray($audit_master_id, 'encounter');
329 $procedure = $this->getCarecoordinationTable()->getProcedure(array('pid' => $pid));
330 $procedure_audit = $this->getRevAndApproveAuditArray($audit_master_id, 'procedure');
332 $care_plan = $this->getCarecoordinationTable()->getCarePlan(array('pid' => $pid));
333 $care_plan_audit = $this->getRevAndApproveAuditArray($audit_master_id, 'care_plan');
335 $functional_cognitive_status = $this->getCarecoordinationTable()->getFunctionalCognitiveStatus(array('pid' => $pid));
336 $functional_cognitive_status_audit = $this->getRevAndApproveAuditArray($audit_master_id, 'functional_cognitive_status');
338 $referral = $this->getCarecoordinationTable()->getReferralReason(array('pid' => $pid));
339 $referral_audit = $this->getRevAndApproveAuditArray($audit_master_id, 'referral');
341 $discharge_medication_audit = $this->getRevAndApproveAuditArray($audit_master_id, 'discharge_medication');
343 $discharge_summary = array(); // TODO: stephen what happened here?? no discharge summary review?
344 $discharge_summary_audit = $this->getRevAndApproveAuditArray($audit_master_id, 'discharge_summary');
346 $gender_list = $this->getCarecoordinationTable()->getList('sex');
347 $country_list = $this->getCarecoordinationTable()->getList('country');
348 $marital_status_list = $this->getCarecoordinationTable()->getList('marital');
349 $religion_list = $this->getCarecoordinationTable()->getList('religious_affiliation');
350 $race_list = $this->getCarecoordinationTable()->getList('race');
351 $ethnicity_list = $this->getCarecoordinationTable()->getList('ethnicity');
352 $state_list = $this->getCarecoordinationTable()->getList('state');
353 $tobacco = $this->getCarecoordinationTable()->getList('smoking_status');
355 $demographics_old[0]['sex'] = $this->getCarecoordinationTable()->getListTitle($demographics_old[0]['sex'], 'sex', '');
356 $demographics_old[0]['country_code'] = $this->getCarecoordinationTable()->getListTitle($demographics_old[0]['country_code'], 'country', '');
357 $demographics_old[0]['status'] = $this->getCarecoordinationTable()->getListTitle($demographics_old[0]['status'], 'marital', '');
358 $demographics_old[0]['religion'] = $this->getCarecoordinationTable()->getListTitle($demographics_old[0]['religion'], 'religious_affiliation', '');
359 $demographics_old[0]['race'] = $this->getCarecoordinationTable()->getListTitle($demographics_old[0]['race'], 'race', '');
360 $demographics_old[0]['ethnicity'] = $this->getCarecoordinationTable()->getListTitle($demographics_old[0]['ethnicity'], 'ethnicity', '');
361 $demographics_old[0]['state'] = $this->getCarecoordinationTable()->getListTitle($demographics_old[0]['state'], 'state', '');
363 $view = new ViewModel(array(
364 'carecoordinationTable' => $this->getCarecoordinationTable(),
365 'ApplicationTable' => $this->getApplicationTable(),
366 'commonplugin' => $this->CommonPlugin(), // this comes from the Application Module
367 'demographics' => $demographics,
368 'demographics_old' => $demographics_old,
369 'problems' => $problems,
370 'problems_audit' => $problems_audit,
371 'allergies' => $allergies,
372 'allergies_audit' => $allergies_audit,
373 'medications' => $medications,
374 'medications_audit' => $medications_audit,
375 'immunizations' => $immunizations,
376 'immunizations_audit' => $immunizations_audit,
377 'lab_results' => $lab_results,
378 'lab_results_audit' => $lab_results_audit,
379 'vitals' => $vitals,
380 'vitals_audit' => $vitals_audit,
381 'social_history' => $social_history,
382 'social_history_audit' => $social_history_audit,
383 'encounter' => $encounter,
384 'encounter_audit' => $encounter_audit,
385 'procedure' => $procedure,
386 'procedure_audit' => $procedure_audit,
387 'care_plan' => $care_plan,
388 'care_plan_audit' => $care_plan_audit,
389 'functional_cognitive_status' => $functional_cognitive_status,
390 'functional_cognitive_status_audit' => $functional_cognitive_status_audit,
391 'referral' => $referral,
392 'referral_audit' => $referral_audit,
393 'discharge_medication_audit' => $discharge_medication_audit,
394 'discharge_summary' => $discharge_summary,
395 'discharge_summary_audit' => $discharge_summary_audit,
396 'amid' => $audit_master_id,
397 'pid' => $pid,
398 'document_id' => $document_id,
399 'gender_list' => $gender_list,
400 'country_list' => $country_list,
401 'marital_status_list' => $marital_status_list,
402 'religion_list' => $religion_list,
403 'race_list' => $race_list,
404 'ethnicity_list' => $ethnicity_list,
405 'tobacco' => $tobacco,
406 'state_list' => $state_list,
407 'listenerObject' => $this->listenerObject,
408 'documentationOf' => $documentationOf,
410 return $view;
413 public function getCCDAComponentsAction()
415 $request = $this->getRequest();
416 $id = $request->getQuery('id');
417 $arr = explode("-", $id);
418 $amid = $arr[0];
419 $pid = $arr[1];
420 $components = $this->getCarecoordinationTable()->getCCDAComponents(1);
421 $discharge_medication_audit = $this->getCarecoordinationTable()->createAuditArray($amid, 'discharge_medication');
422 $discharge_summary_audit = $this->getCarecoordinationTable()->createAuditArray($amid, 'discharge_summary');
423 if (count($discharge_medication_audit) > 0) {
424 $components['discharge_medication'] = 'Dishcharge Medications';
427 if (count($discharge_summary_audit) > 0) {
428 $components['discharge_summary'] = 'Dishcharge Summary';
431 $components = array_diff($components, array('instructions' => 'Instructions'));
433 $temp = '<table>';
434 foreach ($components as $key => $value) {
435 $temp .= '<tr class="se_in_9">
436 <th colspan="1" id="expandCompDetails-' . CommonPlugin::escape($key . $amid . $pid) . '" class="expandCompDetails se_in_23" component="' . CommonPlugin::escape($key) . '" amid="' . CommonPlugin::escape($amid) . '" style="padding: 0px 5px!important;"></th>
437 <th colspan="8" style="padding: 0px 0px!important;"><label>' . CommonPlugin::escape($value) . '</th>
438 </tr>
439 <tr>
440 <td colspan="9" id="hideComp-' . CommonPlugin::escape($key . $amid . $pid) . '" class="imported_ccdaComp_details" style="display: none;"></td>
441 </tr>';
444 $temp .= '</table>';
445 echo $temp;
446 exit;
449 public function getEachCCDAComponentDetailsAction()
451 $request = $this->getRequest();
452 $id = $request->getQuery('id');
453 $component = $request->getQuery('component');
454 $amid = $request->getQuery('amid');
455 $temp = '';
457 switch ($component) {
458 case 'schematron':
459 $validate = new CdaValidateDocuments();
460 $temp .= $validate->createSchematronHtml($amid);
461 break;
462 case 'allergies':
463 $allergies_audit = $this->getCarecoordinationTable()->createAuditArray($amid, 'lists2');
464 if (count($allergies_audit) > 0) {
465 $temp .= '<div><table class="narr_table" border="1" width="100%!important;">
466 <thead><tr class="narr_tr">
467 <th class="narr_th">' . Listener::z_xlt('Substance') . '</th>
468 <th class="narr_th">' . Listener::z_xlt('Reaction') . '</th>
469 <th class="narr_th">' . Listener::z_xlt('Severity') . '</th>
470 <th class="narr_th">' . Listener::z_xlt('Status') . '</th>
471 </tr></thead>
472 <tbody>';
473 foreach ($allergies_audit['lists2'] as $key => $val) {
474 $severity_option_id = $this->getCarecoordinationTable()->getOptionId('severity_ccda', '', 'SNOMED-CT:' . $val['severity_al']);
475 $severity_text = $this->getCarecoordinationTable()->getListTitle($severity_option_id, 'severity_ccda', 'SNOMED-CT:' . $val['severity_al']);
476 if ($val['enddate'] != 0 && $val['enddate'] != '') {
477 $status = 'completed';
478 } else {
479 $status = 'active';
482 $temp .= '<tr class="narr_tr">
483 <td>' . CommonPlugin::escape($val['list_code_text']) . '</td>
484 <td>' . Listener::z_xlt($val['reaction_text']) . '</td>
485 <td>' . Listener::z_xlt($severity_text) . '</td>
486 <td>' . Listener::z_xlt($status) . '</td>
487 </tr>';
490 $temp .= '</tbody></table></div>';
491 } else {
492 $temp .= Listener::z_xlt('No Known Allergies');
494 break;
495 case 'medications':
496 $medications_audit = $this->getCarecoordinationTable()->createAuditArray($amid, 'lists3');
497 if (count($medications_audit) > 0) {
498 $temp .= '<div><table class="narr_table" border="1" width="100%">
499 <thead><tr class="narr_tr">
500 <th class="narr_th">' . Listener::z_xlt('Medication') . '</th>
501 <th class="narr_th">' . Listener::z_xlt('Directions') . '</th>
502 <th class="narr_th">' . Listener::z_xlt('Start Date') . '</th>
503 <th class="narr_th">' . Listener::z_xlt('Status') . '</th>
504 <th class="narr_th">' . Listener::z_xlt('Indications') . '</th>
505 <th class="narr_th">' . Listener::z_xlt('Fill Instructions') . '</th>
506 </tr></thead>
507 <tbody>';
508 foreach ($medications_audit['lists3'] as $key => $val) {
509 if ($val['enddate'] && $val['enddate'] != 0) {
510 $active = 'completed';
511 } else {
512 $active = 'active';
515 $temp .= '<tr class="narr_tr">
516 <td>' . CommonPlugin::escape($val['drug_text']) . '</td>
517 <td>' . CommonPlugin::escape($val['rate'] . " " . $val['rate_unit'] . " " . $val['route_display'] . " " . $val['dose'] . " " . $val['dose_unit']) . '</td>
518 <td>' . ApplicationTable::fixDate(substr($val['begdate'], 0, 4) . "-" . substr($val['begdate'], 4, 2) . "-" . substr($val['begdate'], 6, 2), $this->date_format, 'yyyy-mm-dd') . '</td>
519 <td>' . Listener::z_xlt($active) . '</td>
520 <td>' . CommonPlugin::escape($val['indication']) . '</td>
521 <td>' . CommonPlugin::escape($val['note']) . '</td>
522 </tr>';
525 $temp .= '</tbody></table></div>';
526 } else {
527 $temp .= Listener::z_xlt('No Known Medications');
529 break;
530 case 'problems':
531 $problems_audit = $this->getCarecoordinationTable()->createAuditArray($amid, 'lists1');
532 if (count($problems_audit) > 0) {
533 $temp .= '<div><ul>';
534 $i = 1;
535 foreach ($problems_audit['lists1'] as $key => $val) {
536 if ($val['enddate'] != 0 && $val['enddate'] != '') {
537 $status = 'Resolved';
538 } else {
539 $status = 'Active';
542 $temp .= '<li>' . $i . '. ' . CommonPlugin::escape($val['list_code_text']) . ',' . substr($val['begdate'], 0, 4) . "-" . substr($val['begdate'], 4, 2) . "-" . substr($val['begdate'], 6, 2) . ', ' . Listener::z_xlt('Status') . ' :' . Listener::z_xlt($status) . '</li>';
543 $i++;
546 $temp .= '</ul></div>';
547 } else {
548 $temp .= Listener::z_xlt('No Known Problems');
550 break;
551 case 'immunizations':
552 $immunizations_audit = $this->getCarecoordinationTable()->createAuditArray($amid, 'immunization');
553 if (count($immunizations_audit) > 0) {
554 $temp .= '<div><table class="narr_table" border="1" width="100%">
555 <thead><tr class="narr_tr">
556 <th class="narr_th">' . Listener::z_xlt('Vaccine') . '</th>
557 <th class="narr_th">' . Listener::z_xlt('Date') . '</th>
558 <th class="narr_th">' . Listener::z_xlt('Status') . '</th>
559 </tr></thead>
560 <tbody>';
561 foreach ($immunizations_audit['immunization'] as $key => $val) {
562 $temp .= '<tr class="narr_tr">
563 <td>' . CommonPlugin::escape($val['cvx_code_text']) . '</td>
564 <td>' . $this->getCarecoordinationTable()->getMonthString(substr($val['administered_date'], 4, 2)) . ' ' . substr($val['administered_date'], 0, 4) . '</td>
565 <td>' . Listener::z_xlt('Completed') . '</td>
566 </tr>';
569 $temp .= '</tbody></table></div>';
570 } else {
571 $temp .= Listener::z_xlt('No Known Immunizations');
573 break;
574 case 'procedures':
575 $procedure_audit = $this->getCarecoordinationTable()->createAuditArray($amid, 'procedure');
576 if (count($procedure_audit) > 0) {
577 $temp .= '<div><table class="narr_table" border="1" width="100%">
578 <thead><tr class="narr_tr">
579 <th class="narr_th">' . Listener::z_xlt('Procedure') . '</th>
580 <th class="narr_th">' . Listener::z_xlt('Date') . '</th>
581 </tr></thead>
582 <tbody>';
583 foreach ($procedure_audit['procedure'] as $key => $val) {
584 $temp .= '<tr class="narr_tr">
585 <td>' . CommonPlugin::escape($val['code_text']) . '</td>
586 <td>' . ApplicationTable::fixDate(substr($val['date'], 0, 4) . "-" . substr($val['date'], 4, 2) . "-" . substr($val['date'], 6, 2), $this->date_format, 'yyyy-mm-dd') . '</td>
587 </tr>';
590 $temp .= '</tbody></table></div>';
591 } else {
592 $temp .= Listener::z_xlt('No Known Procedures');
594 break;
595 case 'results':
596 $lab_results_audit = $this->getCarecoordinationTable()->createAuditArray($amid, 'procedure_result');
597 if (count($lab_results_audit) > 0) {
598 $temp .= '<div>
599 <table class="narr_table" border="1">
600 <thead><tr class="narr_tr">
601 <th class="narr_th">' . Listener::z_xlt('Laboratory Information') . '</th>
602 <th class="narr_th">' . Listener::z_xlt('Result') . '</th>
603 <th class="narr_th">' . Listener::z_xlt('Date') . '</th>
604 </tr></thead>
605 <tbody>';
606 foreach ($lab_results_audit['procedure_result'] as $key => $val) {
607 if ($val['results_text']) {
608 $temp .= '<tr class="narr_tr">
609 <td>' . CommonPlugin::escape($val['results_text']) . ($val['results_range'] != "-" ? "(" . CommonPlugin::escape($val['results_range']) . ")" : "") . '</td>
610 <td>' . CommonPlugin::escape($val['results_value']) . " " . CommonPlugin::escape($val['results_unit']) . '</td>
611 <td>' . ApplicationTable::fixDate(substr($val['date'], 0, 4) . "-" . substr($val['date'], 4, 2) . "-" . substr($val['date'], 6, 2), $this->date_format, 'yyyy-mm-dd') . '</td>
612 </tr>';
616 $temp .= '</tbody></table></div>';
617 } else {
618 $temp .= Listener::z_xlt('No Known Lab Results');
620 break;
621 case 'plan_of_care':
622 $care_plan_audit = $this->getCarecoordinationTable()->createAuditArray($amid, 'care_plan');
623 if (count($care_plan_audit) > 0) {
624 $temp .= '<div><table class="narr_table" border="1" width="100%">
625 <head><tr class="narr_tr">
626 <th class="narr_th">' . Listener::z_xlt('Planned Activity') . '</th>
627 <th class="narr_th">' . Listener::z_xlt('Planned Date') . '</th>
628 </tr></thead>
629 <tbody>';
630 foreach ($care_plan_audit['care_plan'] as $key => $val) {
631 $temp .= '<tr class="narr_tr">
632 <td>' . CommonPlugin::escape($val['code_text']) . '</td>
633 <td>' . ApplicationTable::fixDate(substr($val['date'], 0, 4) . "-" . substr($val['date'], 4, 2) . "-" . substr($val['date'], 6, 2), $this->date_format, 'yyyy-mm-dd') . '</td>
634 </tr>';
637 $temp .= '</tbody></table></div>';
638 } else {
639 $temp .= Listener::z_xlt('No Known Plan of Care');
641 break;
642 case 'vitals':
643 $vitals_audit = $this->getCarecoordinationTable()->createAuditArray($amid, 'vital_sign');
644 if (count($vitals_audit) > 0) {
645 $temp .= '<div><table class="narr_table" border="1" width="100%">
646 <thead><tr class="narr_tr">
647 <th class="narr_th" align="right">' . Listener::z_xlt('Date / Time') . ': </th>';
648 foreach ($vitals_audit['vital_sign'] as $key => $val) {
649 $temp .= '<th class="narr_th">' . ApplicationTable::fixDate(substr($val['date'], 0, 4) . "-" . substr($val['date'], 4, 2) . "-" . substr($val['date'], 6, 2), $this->date_format, 'yyyy-mm-dd') . '</th>';
652 $temp .= '</tr></thead><tbody>
653 <tr class="narr_tr">
654 <th class="narr_th" align="left">' . Listener::z_xlt('Temperature') . '</th>';
655 foreach ($vitals_audit['vital_sign'] as $key => $val) {
656 $temp .= '<td>' . CommonPlugin::escape($val['temperature']) . '</td>';
659 $temp .= '</tr>
660 <tr class="narr_tr">
661 <th class="narr_th" align="left">' . Listener::z_xlt('Diastolic') . '</th>';
662 foreach ($vitals_audit['vital_sign'] as $key => $val) {
663 $temp .= '<td>' . CommonPlugin::escape($val['bpd']) . '</td>';
666 $temp .= '</tr>
667 <tr class="narr_tr">
668 <th class="narr_th" align="left">' . Listener::z_xlt('Systolic') . '</th>';
669 foreach ($vitals_audit['vital_sign'] as $key => $val) {
670 $temp .= '<td>' . CommonPlugin::escape($val['bps']) . '</td>';
673 $temp .= '</tr>
674 <tr class="narr_tr">
675 <th class="narr_th" align="left">' . Listener::z_xlt('Head Circumference') . '</th>';
676 foreach ($vitals_audit['vital_sign'] as $key => $val) {
677 $temp .= '<td>' . CommonPlugin::escape($val['head_circ']) . '</td>';
680 $temp .= '</tr>
681 <tr class="narr_tr">
682 <th class="narr_th" align="left">' . Listener::z_xlt('Pulse') . '</th>';
683 foreach ($vitals_audit['vital_sign'] as $key => $val) {
684 $temp .= '<td>' . CommonPlugin::escape($val['pulse']) . '</td>';
687 $temp .= '</tr>
688 <tr class="narr_tr">
689 <th class="narr_th" align="left">' . Listener::z_xlt('Height') . '</th>';
690 foreach ($vitals_audit['vital_sign'] as $key => $val) {
691 $temp .= '<td>' . CommonPlugin::escape($val['height']) . '</td>';
694 $temp .= '</tr>
695 <tr class="narr_tr">
696 <th class="narr_th" align="left">' . Listener::z_xlt('Oxygen Saturation') . '</th>';
697 foreach ($vitals_audit['vital_sign'] as $key => $val) {
698 $temp .= '<td>' . CommonPlugin::escape($val['oxygen_saturation']) . '</td>';
701 $temp .= '</tr>
702 <tr class="narr_tr">
703 <th class="narr_th" align="left">' . Listener::z_xlt('Breath') . '</th>';
704 foreach ($vitals_audit['vital_sign'] as $key => $val) {
705 $temp .= '<td>' . CommonPlugin::escape($val['breath']) . '</td>';
708 $temp .= '</tr>
709 <tr class="narr_tr">
710 <th class="narr_th" align="left">' . Listener::z_xlt('Weight') . '</th>';
711 foreach ($vitals_audit['vital_sign'] as $key => $val) {
712 $temp .= '<td>' . CommonPlugin::escape($val['weight']) . '</td>';
715 $temp .= '</tr>
716 <tr class="narr_tr">
717 <th class="narr_th" align="left">' . Listener::z_xlt('BMI') . '</th>';
718 foreach ($vitals_audit['vital_sign'] as $key => $val) {
719 $temp .= '<td>' . CommonPlugin::escape($val['BMI']) . '</td>';
722 $temp .= '</tr></tbody></table></div>';
723 } else {
724 $temp .= Listener::z_xlt('No Known Vitals');
726 break;
727 case 'social_history':
728 $social_history_audit = $this->getCarecoordinationTable()->createAuditArray($amid, 'social_history');
729 if (count($social_history_audit) > 0) {
730 $temp .= '<div><table class="narr_table" border="1" width="100%">
731 <thead><tr class="narr_tr">
732 <th class="narr_th">' . Listener::z_xlt('Social History Element') . '</th>
733 <th class="narr_th">' . Listener::z_xlt('Description') . '</th>
734 <th class="narr_th">' . Listener::z_xlt('Effective Dates') . '</th>
735 </tr></thead>
736 <tbody>';
737 foreach ($social_history_audit['social_history'] as $key => $val) {
738 $array_his_tobacco = explode("|", $val['smoking']);
739 if ($array_his_tobacco[2] != 0 && $array_his_tobacco[2] != '') {
740 $his_tob_date = substr($array_his_tobacco[2], 0, 4) . "-" . substr($array_his_tobacco[2], 4, 2) . "-" . substr($array_his_tobacco[2], 6, 2);
743 $temp .= '<tr class="narr_tr">
744 <td>' . Listener::z_xlt('Smoking') . '</td>
745 <td>' . CommonPlugin::escape($array_his_tobacco[0]) . '</td>
746 <td>' . ApplicationTable::fixDate($his_tob_date, $this->date_format, 'yyyy-mm-dd') . '</td>
747 </tr>';
750 $temp .= '</tbody></table></div>';
751 } else {
752 $temp .= Listener::z_xlt('No Known Social History');
754 break;
755 case 'encounters':
756 $encounter_audit = $this->getCarecoordinationTable()->createAuditArray($amid, 'encounter');
757 if (count($encounter_audit) > 0) {
758 $temp .= '<div><table class="narr_table" border="1" width="100%">
759 <thead><tr class="narr_tr">
760 <th class="narr_th">' . Listener::z_xlt('Encounter') . '</th>
761 <th class="narr_th">' . Listener::z_xlt('Performer') . '</th>
762 <th class="narr_th">' . Listener::z_xlt('Location') . '</th>
763 <th class="narr_th">' . Listener::z_xlt('Date') . '</th>
764 <th class="narr_th">' . Listener::z_xlt('Encounter Diagnosis') . '</th>
765 <th class="narr_th">' . Listener::z_xlt('Status') . '</th>
766 <th class="narr_th">' . Listener::z_xlt('Reason for Visit') . '</th>
767 </tr></thead>
768 <tbody>';
769 foreach ($encounter_audit['encounter'] as $key => $val) {
770 if (!empty($val['code_text'])) {
771 $encounter_activity = 'Active';
772 } else {
773 $encounter_activity = '';
776 $enc_date = substr($val['date'], 0, 4) . "-" . substr($val['date'], 4, 2) . "-" . substr($val['date'], 6, 2);
777 $temp .= '<tr class="narr_tr">
778 <td>' . CommonPlugin::escape($val['pc_catname']) . '</td>
779 <td>' . CommonPlugin::escape($val['provider_name']) . '</td>
780 <td>' . CommonPlugin::escape($val['represented_organization_name']) . '</td>
781 <td>' . ApplicationTable::fixDate($enc_date, $this->date_format, 'yyyy-mm-dd') . '</td>
782 <td>' . (!empty($val['code_text']) ? CommonPlugin::escape($val['encounter_diagnosis_issue']) : '') . '</td>
783 <td>' . Listener::z_xlt($encounter_activity) . '</td>
784 <td>' . (!empty($val['code_text']) ? CommonPlugin::escape($val['code_text']) : '') . '</td>
785 <td></td>
786 </tr>';
789 $temp .= '</tbody></table></div>';
790 } else {
791 $temp .= Listener::z_xlt('No Known Encounters');
793 break;
794 case 'functional_status':
795 $functional_cognitive_status_audit = $this->getCarecoordinationTable()->createAuditArray($amid, 'functional_cognitive_status');
796 if (count($functional_cognitive_status_audit) > 0) {
797 $temp .= '<div><table class="narr_table" border="1" width="100%">
798 <thead><tr class="narr_tr">
799 <th class="narr_th">' . Listener::z_xlt('Functional Condition') . '</th>
800 <th class="narr_th">' . Listener::z_xlt('Effective Dates') . '</th>
801 <th class="narr_th">' . Listener::z_xlt('Condition Status') . '</th>
802 </tr></thead>
803 <tbody>';
804 foreach ($functional_cognitive_status_audit['functional_cognitive_status'] as $key => $val) {
805 $temp .= '<tr class="narr_tr">
806 <td>' . CommonPlugin::escape($val['description']) . '</td>
807 <td>' . ApplicationTable::fixDate(substr($val['date'], 0, 4) . "-" . substr($val['date'], 4, 2) . "-" . substr($val['date'], 6, 2), $this->date_format, 'yyyy-mm-dd') . '</td>
808 <td>' . Listener::z_xlt('Active') . '</td>
809 </tr>';
812 $temp .= '</tbody></table></div>';
813 } else {
814 $temp .= Listener::z_xlt('No Known Social Functional Status');
816 break;
817 case 'referral':
818 $referral_audit = $this->getCarecoordinationTable()->createAuditArray($amid, 'referral');
819 if (count($referral_audit) > 0) {
820 $temp .= '<div>';
821 foreach ($referral_audit['referral'] as $key => $val) {
822 $referal_data = explode("#$%^&*", $val['body']);
823 foreach ($referal_data as $k => $v) {
824 $temp .= '<p>' . CommonPlugin::escape($v) . '</p>';
828 $temp .= '</div>';
829 } else {
830 $temp .= Listener::z_xlt('No Known Referrals');
832 break;
833 case 'instructions':
834 $temp .= Listener::z_xlt('No Known Clinical Instructions');
835 break;
836 case 'discharge_medication':
837 $discharge_medication_audit = $this->getCarecoordinationTable()->createAuditArray($amid, 'discharge_medication');
838 $temp .= '<div><table class="narr_table" border="1" width="100%">
839 <thead><tr class="narr_tr">
840 <th class="narr_th">' . Listener::z_xlt('Medication') . '</th>
841 <th class="narr_th">' . Listener::z_xlt('Directions') . '</th>
842 <th class="narr_th">' . Listener::z_xlt('Start Date') . '</th>
843 <th class="narr_th">' . Listener::z_xlt('Status') . '</th>
844 <th class="narr_th">' . Listener::z_xlt('Indications') . '</th>
845 <th class="narr_th">' . Listener::z_xlt('Fill Instructions') . '</th>
846 </tr></thead>
847 <tbody>';
848 foreach ($discharge_medication_audit['discharge_medication'] as $key => $val) {
849 if ($val['enddate'] && $val['enddate'] != 0) {
850 $active = 'completed';
851 } else {
852 $active = 'active';
855 $temp .= '<tr class="narr_tr">
856 <td>' . CommonPlugin::escape($val['drug_text']) . '</td>
857 <td>' . CommonPlugin::escape($val['rate'] . " " . $val['rate_unit'] . " " . $val['route_display'] . " " . $val['dose'] . " " . $val['dose_unit']) . '</td>
858 <td>' . ApplicationTable::fixDate(substr($val['begdate'], 0, 4) . "-" . substr($val['begdate'], 4, 2) . "-" . substr($val['begdate'], 6, 2), $this->date_format, 'yyyy-mm-dd') . '</td>
859 <td>' . Listener::z_xlt($active) . '</td>
860 <td>' . CommonPlugin::escape($val['indication']) . '</td>
861 <td>' . CommonPlugin::escape($val['note']) . '</td>
862 </tr>';
865 $temp .= '</tbody></table></div>';
866 break;
867 case 'discharge_summary':
868 $discharge_summary_audit = $this->getCarecoordinationTable()->createAuditArray($amid, 'discharge_summary');
869 $temp .= '<div>';
870 foreach ($discharge_summary_audit['discharge_summary'] as $key => $val) {
871 $text = str_replace("#$%", "<br />", CommonPlugin::escape($val['text']));
872 $temp .= $text;
875 $temp .= '</div>';
876 break;
879 echo $temp;
880 exit;
884 * Table gateway
886 * @return \Carecoordination\Model\CarecoordinationTable
888 public function getCarecoordinationTable()
890 return $this->carecoordinationTable;
894 * Returns the application table.
896 public function getApplicationTable()
898 return $this->applicationTable;
902 * PHP 7 requires foreach iterables to not be null / undefined. There's a ton of code in the revandapprove.phtml file that assumes
903 * the arrays are not empty... so to skip over the foreach's we are giving them empty values.
905 * @param $audit_master_id
906 * @param $table_name
907 * @return array
909 private function getRevAndApproveAuditArray($audit_master_id, $table_name)
911 $audit = $this->getCarecoordinationTable()->createAuditArray($audit_master_id, $table_name);
912 if (empty($audit[$table_name])) {
913 $audit[$table_name] = []; // leave it empty so we don't fail in the template
915 return $audit;
919 private function sanitizeZip($zipLocation)
921 // TODO: @adunsulag NOTE that zip files can be in any order... so we can't assume that this is alphabetical
922 // to fix this may involve extracting the zip and re-ordering all of the entries...
923 // another one would be to just create hash map indexes with patient names mapped to nested documents...
924 // this will only be an issue if we are migrating documents as the CCDA files themselves are self-contained
925 // TODO: fire off an event about sanitizing the zip file
926 // should have sanitization settings and let someone filter them...
927 // event response should have a boolean for skipSanitization in case a module has already done the sanitization
929 $z = new \ZipArchive();
930 // if a zip file exist we want to overwrite it when we save
931 $z->open($zipLocation);
932 $z->setArchiveComment(""); // remove any comments so we don't deal with buffer overflows on the zip extraction
933 $patientCountHash = [];
934 $patientCount = 0;
935 $patientNameIndex = 1;
936 $patientDocumentsIndex = 2;
937 $maxPatients = 500;
938 $maxDocuments = 500;
939 $maxFileComponents = 5;
941 for ($i = 0; $i < $z->numFiles; $i++) {
942 $stat = $z->statIndex($i);
943 // explode and make sure we have our three parts
944 // our max directory structure is 4... anything more than that and we will bail
945 $fileComponents = explode("/", str_replace('\\', '/', $stat['name']), 5);
946 $componentCount = count($fileComponents);
947 $shouldDeleteIndex = false;
949 // now we need to do our document import for our ccda for this patient
950 if ($componentCount <= $patientNameIndex) {
951 $shouldDeleteIndex = false; // we don't want to delete if we are in folders before our patient name index
952 } else if ($componentCount == ($patientNameIndex + 1)) {
953 // if they have more than maxDocuments in ccd files we need to break out of someone trying to directory
954 // bomb the file system
955 $patientCountHash[$patientNameIndex] = $patientCountHash[$patientNameIndex] ?? 0;
957 // let's check for ccda
958 if ($patientCount > $maxPatients) {
959 $shouldDeleteIndex = true; // no more processing of patient ccdas as we've reached our max import size
960 } else if ($patientCountHash[$patientNameIndex] > $maxDocuments) {
961 $shouldDeleteIndex = true;
962 // can fire off events for modifying what files we keep / process...
963 // we don't process anything but xml ccds
964 } else if (strrpos($fileComponents[$patientNameIndex], '.xml') === false) {
965 $shouldDeleteIndex = true;
966 // if we have a ccd we need to set our document count and increment our patient count
967 // note this logic allows multiple patient ccds to be here as long as they are in the same folder
968 } else if (!isset($patientCountHash[$fileComponents[$patientNameIndex]])) {
969 $patientCountHash[$patientNameIndex] = 0;
970 $patientCount++;
971 } else {
972 $patientCountHash[$patientNameIndex];
974 } else if ($componentCount == ($patientDocumentsIndex + 1)) {
975 if ($patientCountHash[$patientNameIndex] > $maxDocuments) {
976 $shouldDeleteIndex = true;
977 } else {
978 $patientCountHash[$patientNameIndex] += 1;
980 } else {
981 $shouldDeleteIndex = true;
984 // TODO: fire off an event with the patient name, current index, file name, and zip archive object
985 // we can filter on whether we should keep this and let module writers do their own thing if they want to
986 // retain any of the documents or not for their own custom processing.
987 if ($shouldDeleteIndex) {
988 $z->deleteIndex($i);
991 $z->close();
994 private function printZipContents($zipLocation)
996 $z = new \ZipArchive();
997 $z->open($zipLocation);
998 for ($i = 0; $i < $z->numFiles; $i++) {
999 $stat = $z->statIndex($i);
1000 (new SystemLogger())->error("File in zip is " . $stat['name']);
1002 $z->close();
1005 private function importZipUpload($request)
1007 // our file structure is
1008 // import_name / patient_name / ccda.xml
1009 // import_name / patient_name / ccda.html
1010 // import_name / patient_name / ccda.xsl
1012 // we will need to have these limit options configurable but we have to be careful to
1013 // we will limit our docsToImport to 500
1014 // we will limit our patientsToImport to 500
1016 $z = new \ZipArchive();
1017 $tmpFile = reset($_FILES);
1018 $tmpFileName = $tmpFile['tmp_name'];
1019 $this->printZipContents($tmpFileName);
1021 // make sure we only have our documents folder and our ccda file
1022 $this->sanitizeZip($tmpFileName);
1023 $z->open($tmpFileName);
1024 $category_details = $this->getCarecoordinationTable()->fetch_cat_id('CCDA');
1025 $catId = $category_details[0]['id'] ?? null;
1026 if (empty($catId)) {
1027 throw new \RuntimeException("Could not find document category id for category of CCDA");
1029 $auditMasterRecordByPatients = [];
1030 for ($i = 0; $i < $z->numFiles; $i++) {
1031 $stat = $z->statIndex($i);
1032 // explode and make sure we have our three parts
1033 // our max directory structure is 4... anything more than that and we will bail
1034 $fileComponents = explode("/", str_replace('\\', '/', $stat['name']), 5);
1035 $componentCount = count($fileComponents);
1037 // now we need to do our document import for our ccda for this patient
1038 if ($componentCount == 2) {
1039 // let's process the ccda
1040 $file_name = basename($stat['name']);
1042 $pid = '00';
1043 $ob = new Document();
1044 $contents = $z->getFromIndex($i);
1045 if (stripos($file_name, '.xml') !== false) {
1046 $ret = $ob->createDocument($pid, $catId, $file_name, 'text/xml', $contents);
1047 if (!empty($ret)) {
1048 throw new \RuntimeException("Failed to create document from zip file " . $file_name . " error returned was " . $ret);
1051 $auditMasterRecordId = $this->getCarecoordinationTable()->import($ob->get_id());
1052 // we can use this to do any other processing as the files should be in order
1053 $auditMasterRecordByPatients[$fileComponents[2]] = $auditMasterRecordId;
1057 $z->close();