bug: fix typo in 837I script (#7469)
[openemr.git] / interface / modules / zend_modules / module / Carecoordination / src / Carecoordination / Controller / CarecoordinationController.php
blob833854465d8f6bc0801f7b2afc197a4e4a6f84da
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\Mvc\Controller\AbstractActionController;
20 use Laminas\View\Model\ViewModel;
21 use Laminas\View\Model\JsonModel;
22 use Application\Listener\Listener;
23 use Documents\Controller\DocumentsController;
24 use Carecoordination\Model\CarecoordinationTable;
25 use C_Document;
26 use Document;
27 use CouchDB;
28 use OpenEMR\Common\Logging\SystemLogger;
29 use OpenEMR\Services\Cda\CdaValidateDocuments;
30 use xmltoarray_parser_htmlfix;
32 class CarecoordinationController extends AbstractActionController
34 /**
35 * @var Carecoordination\Model\CarecoordinationTable
37 private $carecoordinationTable;
39 /**
40 * @var Documents\Controller\DocumentsController
42 private $documentsController;
44 /**
45 * @var Application\Listener\Listener
47 private $listenerObject;
49 /**
50 * @var string
52 private $date_format;
54 public function __construct(CarecoordinationTable $table, DocumentsController $documentsController)
56 $this->carecoordinationTable = $table;
57 $this->listenerObject = new Listener();
58 $this->date_format = ApplicationTable::dateFormat($GLOBALS['date_display_format']);
59 $this->documentsController = $documentsController;
62 /**
63 * Index Page
65 * @param int $id menu id
66 * $param array $data menu details
67 * @param string $slug controller name
68 * @return \Laminas\View\Model\ViewModel
70 public function indexAction()
72 $this->redirect()->toRoute('encountermanager', array('action' => 'index'));
75 /**
76 * delete an audit record
78 * @return ViewModel
80 public function deleteAuditAction()
82 $request = $this->getRequest();
83 $amid = $request->getPost('am_id') ?? null;
84 if ($amid) {
85 $this->getCarecoordinationTable()->deleteImportAuditData(array('audit_master_id' => $amid));
87 $category_details = $this->getCarecoordinationTable()->fetch_cat_id('CCDA');
88 $records = $this->getCarecoordinationTable()->document_fetch(array('cat_title' => 'CCDA', 'type' => '12'));
89 $view = new ViewModel(array(
90 'records' => $records,
91 'category_id' => $category_details[0]['id'],
92 'file_location' => basename($_FILES['file']['name'] ?? ''),
93 'patient_id' => '00',
94 'listenerObject' => $this->listenerObject
95 ));
97 return $view;
101 * Upload CCDA file
103 public function uploadAction()
105 $request = $this->getRequest();
106 $action = $request->getPost('action');
107 $am_id = $request->getPost('am_id');
108 $document_id = $request->getPost('document_id');
110 if ($action == 'add_new_patient') {
111 $this->getCarecoordinationTable()->insert_patient($am_id, $document_id);
113 if (($request->getPost('chart_all_imports') ?? null) === 'true' && empty($action)) {
114 $records = $this->getCarecoordinationTable()->document_fetch(array('cat_title' => 'CCDA', 'type' => '12'));
115 foreach ($records as $record) {
116 if (!empty($record['matched_patient']) && empty($record['is_unstructured_document'])) {
117 // @todo figure out a way to make this auto. $data is array of doc changes.
118 // $this->getCarecoordinationTable()->insertApprovedData($data);
119 // meantime make user approve changes.
120 continue;
122 $this->getCarecoordinationTable()->insert_patient($record['amid'], $record['document_id']);
125 if (($request->getPost('delete_all_imports') ?? null) === 'true' && empty($action)) {
126 $records = $this->getCarecoordinationTable()->document_fetch(array('cat_title' => 'CCDA', 'type' => '12'));
127 foreach ($records as $record) {
128 $this->getCarecoordinationTable()->deleteImportAuditData(array('audit_master_id' => $record['amid']));
132 $upload = $request->getPost('upload');
133 $category_details = $this->getCarecoordinationTable()->fetch_cat_id('CCDA');
135 if ($upload == 1) {
136 $time_start = date('Y-m-d H:i:s');
137 $obj_doc = $this->documentsController;
138 if ($obj_doc->isZipUpload($request)) {
139 $this->importZipUpload($request);
140 } else {
141 $cdoc = $obj_doc->uploadAction($request);
142 $uploaded_documents = $this->getCarecoordinationTable()->fetch_uploaded_documents(
143 array('user' => $_SESSION['authUserID'], 'time_start' => $time_start, 'time_end' => date('Y-m-d H:i:s'))
145 if ($uploaded_documents[0]['id'] > 0) {
146 $_REQUEST["document_id"] = $uploaded_documents[0]['id'];
147 $_REQUEST["batch_import"] = 'YES';
148 $this->importAction();
151 } else {
152 $result = $this->Documents()->fetchXmlDocuments();
153 foreach ($result as $row) {
154 if ($row['doc_type'] == 'CCDA') {
155 $_REQUEST["document_id"] = $row['doc_id'];
156 $this->importAction();
157 $this->documentsController->getDocumentsTable()->updateDocumentCategoryUsingCatname($row['doc_type'], $row['doc_id']);
162 $records = $this->getCarecoordinationTable()->document_fetch(array('cat_title' => 'CCDA', 'type' => '12'));
163 foreach ($records as $key => $r) {
164 if (!empty($records[$key]['dupl_patient'] ?? null)) {
165 continue;
167 $name = $r['pat_name'];
168 // compare to the other imported items for duplicates being imported
169 foreach ($records as $k => $r1) {
170 $f = false;
171 $why = '';
172 if (!empty($r1['dupl_patient'] ?? null) || $key == $k) {
173 if (!empty($records[$key]['matched_patient'] ?? null)) {
174 $why = xlt('Duplicate demographics and components for MRN') . ' ' . text($records[$key]['pid'] ?? '');
175 $records[$k]['dupl_patient'] = $why;
177 continue;
179 $n = $r1['pat_name'];
180 $fn = $r1['ad_fname'] == $r['ad_fname'];
181 $ln = $r1['ad_lname'] == $r['ad_lname'];
182 $dob = $r1['dob_raw'] == $r['dob_raw'];
183 if ($dob) {
184 $f = true;
185 $why = xlt('Match DOB');
187 if ($name == $n && ($f || $r1['race'] == $r['race'] || $r1['ethnicity'] == $r['ethnicity'])) {
188 if ($f) {
189 $why = xlt('Matched Demographic and DOB');
190 } else {
191 $why = xlt('Matched Demographic');
193 if ($r1['enc_count'] != $r['enc_count'] || $r1['cp_count'] != $r['cp_count'] || $r1['ob_count'] != $r['ob_count']) {
194 $why .= ' ' . xlt('with Mismatched Components');
196 $f = true;
198 if (
199 (($ln && !$fn || $fn && !$ln) && $dob)
200 && ($r1['race'] == $r['race'] || $r1['ethnicity'] == $r['ethnicity'])
202 $why = xlt('Name Misspelled');
203 if (
204 $r1['enc_count'] != $r['enc_count']
205 || $r1['cp_count'] != $r['cp_count']
206 || $r1['ob_count'] != $r['ob_count']
208 $why .= ' ' . xlt('with Mismatched Components');
210 $f = true;
212 if (($r1['is_qrda_document'] ?? 0) === 2) {
213 $f = false;
214 $records[$k]['dupl_patient'] = xlt('Empty Report. No QDM content.');
216 if ($f) {
217 if (empty($records[$k]['matched_patient']) && empty($records[$key]['matched_patient'])) {
218 $why = xlt('Another imported document duplicates') . ' ' . $why;
220 $records[$key]['dupl_patient'] = $records[$k]['dupl_patient'] = $why;
225 $view = new ViewModel(array(
226 'records' => $records,
227 'category_id' => $category_details[0]['id'],
228 'file_location' => basename($_FILES['file']['name'] ?? ''),
229 'patient_id' => '00',
230 'listenerObject' => $this->listenerObject
232 // I haven't a clue why this delay is needed to allow batch to work from fetch.
233 if (!empty($upload)) {
234 sleep(1);
236 return $view;
240 * Function to import the data CCDA file to audit tables.
242 * @param document_id integer value
243 * @return \Laminas\View\Model\JsonModel
245 public function importAction()
247 $request = $this->getRequest();
248 if ($request->getQuery('document_id')) {
249 $_REQUEST["document_id"] = $request->getQuery('document_id');
250 $category_details = $this->getCarecoordinationTable()->fetch_cat_id('CCDA');
251 $this->documentsController->getDocumentsTable()->updateDocumentCategory($category_details[0]['id'], $_REQUEST["document_id"]);
254 $document_id = $_REQUEST["document_id"];
255 $this->getCarecoordinationTable()->import($document_id);
257 $view = new JsonModel();
258 $view->setTerminal(true);
259 return $view;
262 public function revandapproveAction()
264 $request = $this->getRequest();
265 $document_id = $request->getQuery('document_id') ? $request->getQuery('document_id') : $request->getPost('document_id', null);
266 $audit_master_id = $request->getQuery('amid') ? $request->getQuery('amid') : $request->getPost('amid', null);
267 $pid = $request->getQuery('pid') ? $request->getQuery('pid') : $request->getPost('pid', null);
269 if ($request->getPost('setval') == 'approve') {
270 $this->getCarecoordinationTable()->insertApprovedData($request->getPost());
271 return $this->redirect()->toRoute('carecoordination', array(
272 'controller' => 'Carecoordination',
273 'action' => 'upload'));
274 } elseif ($request->getPost('setval') == 'discard') {
275 $this->getCarecoordinationTable()->discardCCDAData(array('audit_master_id' => $audit_master_id));
276 return $this->redirect()->toRoute('carecoordination', array(
277 'controller' => 'Carecoordination',
278 'action' => 'upload'));
281 $documentationOf = $this->getCarecoordinationTable()->getdocumentationOf($audit_master_id);
282 $demographics = $this->getCarecoordinationTable()->getDemographics(array('audit_master_id' => $audit_master_id));
283 $demographics_old = $this->getCarecoordinationTable()->getDemographicsOld(array('pid' => $pid));
285 $problems = $this->getCarecoordinationTable()->getProblems(array('pid' => $pid));
286 $problems_audit = $this->getRevAndApproveAuditArray($audit_master_id, 'lists1');
288 $allergies = $this->getCarecoordinationTable()->getAllergies(array('pid' => $pid));
289 $allergies_audit = $this->getRevAndApproveAuditArray($audit_master_id, 'lists2');
291 $medications = $this->getCarecoordinationTable()->getMedications(array('pid' => $pid));
292 $medications_audit = $this->getRevAndApproveAuditArray($audit_master_id, 'lists3');
294 $immunizations = $this->getCarecoordinationTable()->getImmunizations(array('pid' => $pid));
295 $immunizations_audit = $this->getRevAndApproveAuditArray($audit_master_id, 'immunization');
297 $lab_results = $this->getCarecoordinationTable()->getLabResults(array('pid' => $pid));
298 $lab_results_audit = $this->getRevAndApproveAuditArray($audit_master_id, 'procedure_result');
300 $vitals = $this->getCarecoordinationTable()->getVitals(array('pid' => $pid));
301 $vitals_audit = $this->getRevAndApproveAuditArray($audit_master_id, 'vital_sign');
303 $social_history = $this->getCarecoordinationTable()->getSocialHistory(array('pid' => $pid));
304 $social_history_audit = $this->getRevAndApproveAuditArray($audit_master_id, 'social_history');
306 $encounter = $this->getCarecoordinationTable()->getEncounterData(array('pid' => $pid));
307 $encounter_audit = $this->getRevAndApproveAuditArray($audit_master_id, 'encounter');
309 $procedure = $this->getCarecoordinationTable()->getProcedure(array('pid' => $pid));
310 $procedure_audit = $this->getRevAndApproveAuditArray($audit_master_id, 'procedure');
312 $care_plan = $this->getCarecoordinationTable()->getCarePlan(array('pid' => $pid));
313 $care_plan_audit = $this->getRevAndApproveAuditArray($audit_master_id, 'care_plan');
315 $functional_cognitive_status = $this->getCarecoordinationTable()->getFunctionalCognitiveStatus(array('pid' => $pid));
316 $functional_cognitive_status_audit = $this->getRevAndApproveAuditArray($audit_master_id, 'functional_cognitive_status');
318 $referral = $this->getCarecoordinationTable()->getReferralReason(array('pid' => $pid));
319 $referral_audit = $this->getRevAndApproveAuditArray($audit_master_id, 'referral');
321 $discharge_medication_audit = $this->getRevAndApproveAuditArray($audit_master_id, 'discharge_medication');
323 $discharge_summary = array(); // TODO: stephen what happened here?? no discharge summary review?
324 $discharge_summary_audit = $this->getRevAndApproveAuditArray($audit_master_id, 'discharge_summary');
326 $gender_list = $this->getCarecoordinationTable()->getList('sex');
327 $country_list = $this->getCarecoordinationTable()->getList('country');
328 $marital_status_list = $this->getCarecoordinationTable()->getList('marital');
329 $religion_list = $this->getCarecoordinationTable()->getList('religious_affiliation');
330 $race_list = $this->getCarecoordinationTable()->getList('race');
331 $ethnicity_list = $this->getCarecoordinationTable()->getList('ethnicity');
332 $state_list = $this->getCarecoordinationTable()->getList('state');
333 $tobacco = $this->getCarecoordinationTable()->getList('smoking_status');
335 $demographics_old[0]['sex'] = $this->getCarecoordinationTable()->getListTitle($demographics_old[0]['sex'], 'sex', '');
336 $demographics_old[0]['country_code'] = $this->getCarecoordinationTable()->getListTitle($demographics_old[0]['country_code'], 'country', '');
337 $demographics_old[0]['status'] = $this->getCarecoordinationTable()->getListTitle($demographics_old[0]['status'], 'marital', '');
338 $demographics_old[0]['religion'] = $this->getCarecoordinationTable()->getListTitle($demographics_old[0]['religion'], 'religious_affiliation', '');
339 $demographics_old[0]['race'] = $this->getCarecoordinationTable()->getListTitle($demographics_old[0]['race'], 'race', '');
340 $demographics_old[0]['ethnicity'] = $this->getCarecoordinationTable()->getListTitle($demographics_old[0]['ethnicity'], 'ethnicity', '');
341 $demographics_old[0]['state'] = $this->getCarecoordinationTable()->getListTitle($demographics_old[0]['state'], 'state', '');
343 $view = new ViewModel(array(
344 'carecoordinationTable' => $this->getCarecoordinationTable(),
345 'ApplicationTable' => $this->getApplicationTable(),
346 'commonplugin' => $this->CommonPlugin(), // this comes from the Application Module
347 'demographics' => $demographics,
348 'demographics_old' => $demographics_old,
349 'problems' => $problems,
350 'problems_audit' => $problems_audit,
351 'allergies' => $allergies,
352 'allergies_audit' => $allergies_audit,
353 'medications' => $medications,
354 'medications_audit' => $medications_audit,
355 'immunizations' => $immunizations,
356 'immunizations_audit' => $immunizations_audit,
357 'lab_results' => $lab_results,
358 'lab_results_audit' => $lab_results_audit,
359 'vitals' => $vitals,
360 'vitals_audit' => $vitals_audit,
361 'social_history' => $social_history,
362 'social_history_audit' => $social_history_audit,
363 'encounter' => $encounter,
364 'encounter_audit' => $encounter_audit,
365 'procedure' => $procedure,
366 'procedure_audit' => $procedure_audit,
367 'care_plan' => $care_plan,
368 'care_plan_audit' => $care_plan_audit,
369 'functional_cognitive_status' => $functional_cognitive_status,
370 'functional_cognitive_status_audit' => $functional_cognitive_status_audit,
371 'referral' => $referral,
372 'referral_audit' => $referral_audit,
373 'discharge_medication_audit' => $discharge_medication_audit,
374 'discharge_summary' => $discharge_summary,
375 'discharge_summary_audit' => $discharge_summary_audit,
376 'amid' => $audit_master_id,
377 'pid' => $pid,
378 'document_id' => $document_id,
379 'gender_list' => $gender_list,
380 'country_list' => $country_list,
381 'marital_status_list' => $marital_status_list,
382 'religion_list' => $religion_list,
383 'race_list' => $race_list,
384 'ethnicity_list' => $ethnicity_list,
385 'tobacco' => $tobacco,
386 'state_list' => $state_list,
387 'listenerObject' => $this->listenerObject,
388 'documentationOf' => $documentationOf,
390 return $view;
393 public function getCCDAComponentsAction()
395 $request = $this->getRequest();
396 $id = $request->getQuery('id');
397 $arr = explode("-", $id);
398 $amid = $arr[0];
399 $pid = $arr[1];
400 $components = $this->getCarecoordinationTable()->getCCDAComponents(1);
401 $discharge_medication_audit = $this->getCarecoordinationTable()->createAuditArray($amid, 'discharge_medication');
402 $discharge_summary_audit = $this->getCarecoordinationTable()->createAuditArray($amid, 'discharge_summary');
403 if (count($discharge_medication_audit) > 0) {
404 $components['discharge_medication'] = 'Dishcharge Medications';
407 if (count($discharge_summary_audit) > 0) {
408 $components['discharge_summary'] = 'Dishcharge Summary';
411 $components = array_diff($components, array('instructions' => 'Instructions'));
413 $temp = '<table>';
414 foreach ($components as $key => $value) {
415 $temp .= '<tr class="se_in_9">
416 <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>
417 <th colspan="8" style="padding: 0px 0px!important;"><label>' . CommonPlugin::escape($value) . '</th>
418 </tr>
419 <tr>
420 <td colspan="9" id="hideComp-' . CommonPlugin::escape($key . $amid . $pid) . '" class="imported_ccdaComp_details" style="display: none;"></td>
421 </tr>';
424 $temp .= '</table>';
425 echo $temp;
426 exit;
429 public function getEachCCDAComponentDetailsAction()
431 $request = $this->getRequest();
432 $id = $request->getQuery('id');
433 $component = $request->getQuery('component');
434 $amid = $request->getQuery('amid');
435 $temp = '';
437 switch ($component) {
438 case 'schematron':
439 $validate = new CdaValidateDocuments();
440 $temp .= $validate->createSchematronHtml($amid);
441 break;
442 case 'allergies':
443 $allergies_audit = $this->getCarecoordinationTable()->createAuditArray($amid, 'lists2');
444 if (count($allergies_audit) > 0) {
445 $temp .= '<div><table class="narr_table" border="1" width="100%!important;">
446 <thead><tr class="narr_tr">
447 <th class="narr_th">' . Listener::z_xlt('Substance') . '</th>
448 <th class="narr_th">' . Listener::z_xlt('Reaction') . '</th>
449 <th class="narr_th">' . Listener::z_xlt('Severity') . '</th>
450 <th class="narr_th">' . Listener::z_xlt('Status') . '</th>
451 </tr></thead>
452 <tbody>';
453 foreach ($allergies_audit['lists2'] as $key => $val) {
454 $severity_option_id = $this->getCarecoordinationTable()->getOptionId('severity_ccda', '', 'SNOMED-CT:' . $val['severity_al']);
455 $severity_text = $this->getCarecoordinationTable()->getListTitle($severity_option_id, 'severity_ccda', 'SNOMED-CT:' . $val['severity_al']);
456 if ($val['enddate'] != 0 && $val['enddate'] != '') {
457 $status = 'completed';
458 } else {
459 $status = 'active';
462 $temp .= '<tr class="narr_tr">
463 <td>' . CommonPlugin::escape($val['list_code_text']) . '</td>
464 <td>' . Listener::z_xlt($val['reaction_text']) . '</td>
465 <td>' . Listener::z_xlt($severity_text) . '</td>
466 <td>' . Listener::z_xlt($status) . '</td>
467 </tr>';
470 $temp .= '</tbody></table></div>';
471 } else {
472 $temp .= Listener::z_xlt('No Known Allergies');
474 break;
475 case 'medications':
476 $medications_audit = $this->getCarecoordinationTable()->createAuditArray($amid, 'lists3');
477 if (count($medications_audit) > 0) {
478 $temp .= '<div><table class="narr_table" border="1" width="100%">
479 <thead><tr class="narr_tr">
480 <th class="narr_th">' . Listener::z_xlt('Medication') . '</th>
481 <th class="narr_th">' . Listener::z_xlt('Directions') . '</th>
482 <th class="narr_th">' . Listener::z_xlt('Start Date') . '</th>
483 <th class="narr_th">' . Listener::z_xlt('Status') . '</th>
484 <th class="narr_th">' . Listener::z_xlt('Indications') . '</th>
485 <th class="narr_th">' . Listener::z_xlt('Fill Instructions') . '</th>
486 </tr></thead>
487 <tbody>';
488 foreach ($medications_audit['lists3'] as $key => $val) {
489 if ($val['enddate'] && $val['enddate'] != 0) {
490 $active = 'completed';
491 } else {
492 $active = 'active';
495 $temp .= '<tr class="narr_tr">
496 <td>' . CommonPlugin::escape($val['drug_text']) . '</td>
497 <td>' . CommonPlugin::escape($val['rate'] . " " . $val['rate_unit'] . " " . $val['route_display'] . " " . $val['dose'] . " " . $val['dose_unit']) . '</td>
498 <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>
499 <td>' . Listener::z_xlt($active) . '</td>
500 <td>' . CommonPlugin::escape($val['indication']) . '</td>
501 <td>' . CommonPlugin::escape($val['note']) . '</td>
502 </tr>';
505 $temp .= '</tbody></table></div>';
506 } else {
507 $temp .= Listener::z_xlt('No Known Medications');
509 break;
510 case 'problems':
511 $problems_audit = $this->getCarecoordinationTable()->createAuditArray($amid, 'lists1');
512 if (count($problems_audit) > 0) {
513 $temp .= '<div><ul>';
514 $i = 1;
515 foreach ($problems_audit['lists1'] as $key => $val) {
516 if ($val['enddate'] != 0 && $val['enddate'] != '') {
517 $status = 'Resolved';
518 } else {
519 $status = 'Active';
522 $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>';
523 $i++;
526 $temp .= '</ul></div>';
527 } else {
528 $temp .= Listener::z_xlt('No Known Problems');
530 break;
531 case 'immunizations':
532 $immunizations_audit = $this->getCarecoordinationTable()->createAuditArray($amid, 'immunization');
533 if (count($immunizations_audit) > 0) {
534 $temp .= '<div><table class="narr_table" border="1" width="100%">
535 <thead><tr class="narr_tr">
536 <th class="narr_th">' . Listener::z_xlt('Vaccine') . '</th>
537 <th class="narr_th">' . Listener::z_xlt('Date') . '</th>
538 <th class="narr_th">' . Listener::z_xlt('Status') . '</th>
539 </tr></thead>
540 <tbody>';
541 foreach ($immunizations_audit['immunization'] as $key => $val) {
542 $temp .= '<tr class="narr_tr">
543 <td>' . CommonPlugin::escape($val['cvx_code_text']) . '</td>
544 <td>' . $this->getCarecoordinationTable()->getMonthString(substr($val['administered_date'], 4, 2)) . ' ' . substr($val['administered_date'], 0, 4) . '</td>
545 <td>' . Listener::z_xlt('Completed') . '</td>
546 </tr>';
549 $temp .= '</tbody></table></div>';
550 } else {
551 $temp .= Listener::z_xlt('No Known Immunizations');
553 break;
554 case 'procedures':
555 $procedure_audit = $this->getCarecoordinationTable()->createAuditArray($amid, 'procedure');
556 if (count($procedure_audit) > 0) {
557 $temp .= '<div><table class="narr_table" border="1" width="100%">
558 <thead><tr class="narr_tr">
559 <th class="narr_th">' . Listener::z_xlt('Procedure') . '</th>
560 <th class="narr_th">' . Listener::z_xlt('Date') . '</th>
561 </tr></thead>
562 <tbody>';
563 foreach ($procedure_audit['procedure'] as $key => $val) {
564 $temp .= '<tr class="narr_tr">
565 <td>' . CommonPlugin::escape($val['code_text']) . '</td>
566 <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>
567 </tr>';
570 $temp .= '</tbody></table></div>';
571 } else {
572 $temp .= Listener::z_xlt('No Known Procedures');
574 break;
575 case 'results':
576 $lab_results_audit = $this->getCarecoordinationTable()->createAuditArray($amid, 'procedure_result');
577 if (count($lab_results_audit) > 0) {
578 $temp .= '<div>
579 <table class="narr_table" border="1">
580 <thead><tr class="narr_tr">
581 <th class="narr_th">' . Listener::z_xlt('Laboratory Information') . '</th>
582 <th class="narr_th">' . Listener::z_xlt('Result') . '</th>
583 <th class="narr_th">' . Listener::z_xlt('Date') . '</th>
584 </tr></thead>
585 <tbody>';
586 foreach ($lab_results_audit['procedure_result'] as $key => $val) {
587 if ($val['results_text']) {
588 $temp .= '<tr class="narr_tr">
589 <td>' . CommonPlugin::escape($val['results_text']) . ($val['results_range'] != "-" ? "(" . CommonPlugin::escape($val['results_range']) . ")" : "") . '</td>
590 <td>' . CommonPlugin::escape($val['results_value']) . " " . CommonPlugin::escape($val['results_unit']) . '</td>
591 <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>
592 </tr>';
596 $temp .= '</tbody></table></div>';
597 } else {
598 $temp .= Listener::z_xlt('No Known Lab Results');
600 break;
601 case 'plan_of_care':
602 $care_plan_audit = $this->getCarecoordinationTable()->createAuditArray($amid, 'care_plan');
603 if (count($care_plan_audit) > 0) {
604 $temp .= '<div><table class="narr_table" border="1" width="100%">
605 <head><tr class="narr_tr">
606 <th class="narr_th">' . Listener::z_xlt('Planned Activity') . '</th>
607 <th class="narr_th">' . Listener::z_xlt('Planned Date') . '</th>
608 </tr></thead>
609 <tbody>';
610 foreach ($care_plan_audit['care_plan'] as $key => $val) {
611 $temp .= '<tr class="narr_tr">
612 <td>' . CommonPlugin::escape($val['code_text']) . '</td>
613 <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>
614 </tr>';
617 $temp .= '</tbody></table></div>';
618 } else {
619 $temp .= Listener::z_xlt('No Known Plan of Care');
621 break;
622 case 'vitals':
623 $vitals_audit = $this->getCarecoordinationTable()->createAuditArray($amid, 'vital_sign');
624 if (count($vitals_audit) > 0) {
625 $temp .= '<div><table class="narr_table" border="1" width="100%">
626 <thead><tr class="narr_tr">
627 <th class="narr_th" align="right">' . Listener::z_xlt('Date / Time') . ': </th>';
628 foreach ($vitals_audit['vital_sign'] as $key => $val) {
629 $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>';
632 $temp .= '</tr></thead><tbody>
633 <tr class="narr_tr">
634 <th class="narr_th" align="left">' . Listener::z_xlt('Temperature') . '</th>';
635 foreach ($vitals_audit['vital_sign'] as $key => $val) {
636 $temp .= '<td>' . CommonPlugin::escape($val['temperature']) . '</td>';
639 $temp .= '</tr>
640 <tr class="narr_tr">
641 <th class="narr_th" align="left">' . Listener::z_xlt('Diastolic') . '</th>';
642 foreach ($vitals_audit['vital_sign'] as $key => $val) {
643 $temp .= '<td>' . CommonPlugin::escape($val['bpd']) . '</td>';
646 $temp .= '</tr>
647 <tr class="narr_tr">
648 <th class="narr_th" align="left">' . Listener::z_xlt('Systolic') . '</th>';
649 foreach ($vitals_audit['vital_sign'] as $key => $val) {
650 $temp .= '<td>' . CommonPlugin::escape($val['bps']) . '</td>';
653 $temp .= '</tr>
654 <tr class="narr_tr">
655 <th class="narr_th" align="left">' . Listener::z_xlt('Head Circumference') . '</th>';
656 foreach ($vitals_audit['vital_sign'] as $key => $val) {
657 $temp .= '<td>' . CommonPlugin::escape($val['head_circ']) . '</td>';
660 $temp .= '</tr>
661 <tr class="narr_tr">
662 <th class="narr_th" align="left">' . Listener::z_xlt('Pulse') . '</th>';
663 foreach ($vitals_audit['vital_sign'] as $key => $val) {
664 $temp .= '<td>' . CommonPlugin::escape($val['pulse']) . '</td>';
667 $temp .= '</tr>
668 <tr class="narr_tr">
669 <th class="narr_th" align="left">' . Listener::z_xlt('Height') . '</th>';
670 foreach ($vitals_audit['vital_sign'] as $key => $val) {
671 $temp .= '<td>' . CommonPlugin::escape($val['height']) . '</td>';
674 $temp .= '</tr>
675 <tr class="narr_tr">
676 <th class="narr_th" align="left">' . Listener::z_xlt('Oxygen Saturation') . '</th>';
677 foreach ($vitals_audit['vital_sign'] as $key => $val) {
678 $temp .= '<td>' . CommonPlugin::escape($val['oxygen_saturation']) . '</td>';
681 $temp .= '</tr>
682 <tr class="narr_tr">
683 <th class="narr_th" align="left">' . Listener::z_xlt('Breath') . '</th>';
684 foreach ($vitals_audit['vital_sign'] as $key => $val) {
685 $temp .= '<td>' . CommonPlugin::escape($val['breath']) . '</td>';
688 $temp .= '</tr>
689 <tr class="narr_tr">
690 <th class="narr_th" align="left">' . Listener::z_xlt('Weight') . '</th>';
691 foreach ($vitals_audit['vital_sign'] as $key => $val) {
692 $temp .= '<td>' . CommonPlugin::escape($val['weight']) . '</td>';
695 $temp .= '</tr>
696 <tr class="narr_tr">
697 <th class="narr_th" align="left">' . Listener::z_xlt('BMI') . '</th>';
698 foreach ($vitals_audit['vital_sign'] as $key => $val) {
699 $temp .= '<td>' . CommonPlugin::escape($val['BMI']) . '</td>';
702 $temp .= '</tr></tbody></table></div>';
703 } else {
704 $temp .= Listener::z_xlt('No Known Vitals');
706 break;
707 case 'social_history':
708 $social_history_audit = $this->getCarecoordinationTable()->createAuditArray($amid, 'social_history');
709 if (count($social_history_audit) > 0) {
710 $temp .= '<div><table class="narr_table" border="1" width="100%">
711 <thead><tr class="narr_tr">
712 <th class="narr_th">' . Listener::z_xlt('Social History Element') . '</th>
713 <th class="narr_th">' . Listener::z_xlt('Description') . '</th>
714 <th class="narr_th">' . Listener::z_xlt('Effective Dates') . '</th>
715 </tr></thead>
716 <tbody>';
717 foreach ($social_history_audit['social_history'] as $key => $val) {
718 $array_his_tobacco = explode("|", $val['smoking']);
719 if ($array_his_tobacco[2] != 0 && $array_his_tobacco[2] != '') {
720 $his_tob_date = substr($array_his_tobacco[2], 0, 4) . "-" . substr($array_his_tobacco[2], 4, 2) . "-" . substr($array_his_tobacco[2], 6, 2);
723 $temp .= '<tr class="narr_tr">
724 <td>' . Listener::z_xlt('Smoking') . '</td>
725 <td>' . CommonPlugin::escape($array_his_tobacco[0]) . '</td>
726 <td>' . ApplicationTable::fixDate($his_tob_date, $this->date_format, 'yyyy-mm-dd') . '</td>
727 </tr>';
730 $temp .= '</tbody></table></div>';
731 } else {
732 $temp .= Listener::z_xlt('No Known Social History');
734 break;
735 case 'encounters':
736 $encounter_audit = $this->getCarecoordinationTable()->createAuditArray($amid, 'encounter');
737 if (count($encounter_audit) > 0) {
738 $temp .= '<div><table class="narr_table" border="1" width="100%">
739 <thead><tr class="narr_tr">
740 <th class="narr_th">' . Listener::z_xlt('Encounter') . '</th>
741 <th class="narr_th">' . Listener::z_xlt('Performer') . '</th>
742 <th class="narr_th">' . Listener::z_xlt('Location') . '</th>
743 <th class="narr_th">' . Listener::z_xlt('Date') . '</th>
744 <th class="narr_th">' . Listener::z_xlt('Encounter Diagnosis') . '</th>
745 <th class="narr_th">' . Listener::z_xlt('Status') . '</th>
746 <th class="narr_th">' . Listener::z_xlt('Reason for Visit') . '</th>
747 </tr></thead>
748 <tbody>';
749 foreach ($encounter_audit['encounter'] as $key => $val) {
750 if (!empty($val['code_text'])) {
751 $encounter_activity = 'Active';
752 } else {
753 $encounter_activity = '';
756 $enc_date = substr($val['date'], 0, 4) . "-" . substr($val['date'], 4, 2) . "-" . substr($val['date'], 6, 2);
757 $temp .= '<tr class="narr_tr">
758 <td>' . CommonPlugin::escape($val['pc_catname']) . '</td>
759 <td>' . CommonPlugin::escape($val['provider_name']) . '</td>
760 <td>' . CommonPlugin::escape($val['represented_organization_name']) . '</td>
761 <td>' . ApplicationTable::fixDate($enc_date, $this->date_format, 'yyyy-mm-dd') . '</td>
762 <td>' . (!empty($val['code_text']) ? CommonPlugin::escape($val['encounter_diagnosis_issue']) : '') . '</td>
763 <td>' . Listener::z_xlt($encounter_activity) . '</td>
764 <td>' . (!empty($val['code_text']) ? CommonPlugin::escape($val['code_text']) : '') . '</td>
765 <td></td>
766 </tr>';
769 $temp .= '</tbody></table></div>';
770 } else {
771 $temp .= Listener::z_xlt('No Known Encounters');
773 break;
774 case 'functional_status':
775 $functional_cognitive_status_audit = $this->getCarecoordinationTable()->createAuditArray($amid, 'functional_cognitive_status');
776 if (count($functional_cognitive_status_audit) > 0) {
777 $temp .= '<div><table class="narr_table" border="1" width="100%">
778 <thead><tr class="narr_tr">
779 <th class="narr_th">' . Listener::z_xlt('Functional Condition') . '</th>
780 <th class="narr_th">' . Listener::z_xlt('Effective Dates') . '</th>
781 <th class="narr_th">' . Listener::z_xlt('Condition Status') . '</th>
782 </tr></thead>
783 <tbody>';
784 foreach ($functional_cognitive_status_audit['functional_cognitive_status'] as $key => $val) {
785 $temp .= '<tr class="narr_tr">
786 <td>' . CommonPlugin::escape($val['description']) . '</td>
787 <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>
788 <td>' . Listener::z_xlt('Active') . '</td>
789 </tr>';
792 $temp .= '</tbody></table></div>';
793 } else {
794 $temp .= Listener::z_xlt('No Known Social Functional Status');
796 break;
797 case 'referral':
798 $referral_audit = $this->getCarecoordinationTable()->createAuditArray($amid, 'referral');
799 if (count($referral_audit) > 0) {
800 $temp .= '<div>';
801 foreach ($referral_audit['referral'] as $key => $val) {
802 $referal_data = explode("#$%^&*", $val['body']);
803 foreach ($referal_data as $k => $v) {
804 $temp .= '<p>' . CommonPlugin::escape($v) . '</p>';
808 $temp .= '</div>';
809 } else {
810 $temp .= Listener::z_xlt('No Known Referrals');
812 break;
813 case 'instructions':
814 $temp .= Listener::z_xlt('No Known Clinical Instructions');
815 break;
816 case 'discharge_medication':
817 $discharge_medication_audit = $this->getCarecoordinationTable()->createAuditArray($amid, 'discharge_medication');
818 $temp .= '<div><table class="narr_table" border="1" width="100%">
819 <thead><tr class="narr_tr">
820 <th class="narr_th">' . Listener::z_xlt('Medication') . '</th>
821 <th class="narr_th">' . Listener::z_xlt('Directions') . '</th>
822 <th class="narr_th">' . Listener::z_xlt('Start Date') . '</th>
823 <th class="narr_th">' . Listener::z_xlt('Status') . '</th>
824 <th class="narr_th">' . Listener::z_xlt('Indications') . '</th>
825 <th class="narr_th">' . Listener::z_xlt('Fill Instructions') . '</th>
826 </tr></thead>
827 <tbody>';
828 foreach ($discharge_medication_audit['discharge_medication'] as $key => $val) {
829 if ($val['enddate'] && $val['enddate'] != 0) {
830 $active = 'completed';
831 } else {
832 $active = 'active';
835 $temp .= '<tr class="narr_tr">
836 <td>' . CommonPlugin::escape($val['drug_text']) . '</td>
837 <td>' . CommonPlugin::escape($val['rate'] . " " . $val['rate_unit'] . " " . $val['route_display'] . " " . $val['dose'] . " " . $val['dose_unit']) . '</td>
838 <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>
839 <td>' . Listener::z_xlt($active) . '</td>
840 <td>' . CommonPlugin::escape($val['indication']) . '</td>
841 <td>' . CommonPlugin::escape($val['note']) . '</td>
842 </tr>';
845 $temp .= '</tbody></table></div>';
846 break;
847 case 'discharge_summary':
848 $discharge_summary_audit = $this->getCarecoordinationTable()->createAuditArray($amid, 'discharge_summary');
849 $temp .= '<div>';
850 foreach ($discharge_summary_audit['discharge_summary'] as $key => $val) {
851 $text = str_replace("#$%", "<br />", CommonPlugin::escape($val['text']));
852 $temp .= $text;
855 $temp .= '</div>';
856 break;
859 echo $temp;
860 exit;
864 * Table gateway
866 * @return \Carecoordination\Model\CarecoordinationTable
868 public function getCarecoordinationTable()
870 return $this->carecoordinationTable;
874 * Returns the application table.
876 public function getApplicationTable()
878 return $this->applicationTable;
882 * PHP 7 requires foreach iterables to not be null / undefined. There's a ton of code in the revandapprove.phtml file that assumes
883 * the arrays are not empty... so to skip over the foreach's we are giving them empty values.
885 * @param $audit_master_id
886 * @param $table_name
887 * @return array
889 private function getRevAndApproveAuditArray($audit_master_id, $table_name)
891 $audit = $this->getCarecoordinationTable()->createAuditArray($audit_master_id, $table_name);
892 if (empty($audit[$table_name])) {
893 $audit[$table_name] = []; // leave it empty so we don't fail in the template
895 return $audit;
899 private function sanitizeZip($zipLocation)
901 // TODO: @adunsulag NOTE that zip files can be in any order... so we can't assume that this is alphabetical
902 // to fix this may involve extracting the zip and re-ordering all of the entries...
903 // another one would be to just create hash map indexes with patient names mapped to nested documents...
904 // this will only be an issue if we are migrating documents as the CCDA files themselves are self-contained
905 // TODO: fire off an event about sanitizing the zip file
906 // should have sanitization settings and let someone filter them...
907 // event response should have a boolean for skipSanitization in case a module has already done the sanitization
909 $z = new \ZipArchive();
910 // if a zip file exist we want to overwrite it when we save
911 $z->open($zipLocation);
912 $z->setArchiveComment(""); // remove any comments so we don't deal with buffer overflows on the zip extraction
913 $patientCountHash = [];
914 $patientCount = 0;
915 $patientNameIndex = 1;
916 $patientDocumentsIndex = 2;
917 $maxPatients = 500;
918 $maxDocuments = 500;
919 $maxFileComponents = 5;
921 for ($i = 0; $i < $z->numFiles; $i++) {
922 $stat = $z->statIndex($i);
923 // explode and make sure we have our three parts
924 // our max directory structure is 4... anything more than that and we will bail
925 $fileComponents = explode("/", str_replace('\\', '/', $stat['name']), 5);
926 $componentCount = count($fileComponents);
927 $shouldDeleteIndex = false;
929 // now we need to do our document import for our ccda for this patient
930 if ($componentCount <= $patientNameIndex) {
931 $shouldDeleteIndex = false; // we don't want to delete if we are in folders before our patient name index
932 } elseif ($componentCount == ($patientNameIndex + 1)) {
933 // if they have more than maxDocuments in ccd files we need to break out of someone trying to directory
934 // bomb the file system
935 $patientCountHash[$patientNameIndex] = $patientCountHash[$patientNameIndex] ?? 0;
937 // let's check for ccda
938 if ($patientCount > $maxPatients) {
939 $shouldDeleteIndex = true; // no more processing of patient ccdas as we've reached our max import size
940 } elseif ($patientCountHash[$patientNameIndex] > $maxDocuments) {
941 $shouldDeleteIndex = true;
942 // can fire off events for modifying what files we keep / process...
943 // we don't process anything but xml ccds
944 } elseif (strrpos($fileComponents[$patientNameIndex], '.xml') === false) {
945 $shouldDeleteIndex = true;
946 // if we have a ccd we need to set our document count and increment our patient count
947 // note this logic allows multiple patient ccds to be here as long as they are in the same folder
948 } elseif (!isset($patientCountHash[$fileComponents[$patientNameIndex]])) {
949 $patientCountHash[$patientNameIndex] = 0;
950 $patientCount++;
951 } else {
952 $patientCountHash[$patientNameIndex];
954 } elseif ($componentCount == ($patientDocumentsIndex + 1)) {
955 if ($patientCountHash[$patientNameIndex] ?? '' > $maxDocuments) {
956 $shouldDeleteIndex = true;
957 } else {
958 if (!empty($patientCountHash[$patientNameIndex])) {
959 $patientCountHash[$patientNameIndex] += 1;
960 } else {
961 $patientCountHash[$patientNameIndex] = 0;
964 } else {
965 $shouldDeleteIndex = true;
968 // TODO: fire off an event with the patient name, current index, file name, and zip archive object
969 // we can filter on whether we should keep this and let module writers do their own thing if they want to
970 // retain any of the documents or not for their own custom processing.
971 if ($shouldDeleteIndex) {
972 $z->deleteIndex($i);
975 $z->close();
978 private function printZipContents($zipLocation)
980 $z = new \ZipArchive();
981 $z->open($zipLocation);
982 for ($i = 0; $i < $z->numFiles; $i++) {
983 $stat = $z->statIndex($i);
984 (new SystemLogger())->error("File in zip is " . $stat['name']);
986 $z->close();
989 private function importZipUpload($request)
991 // our file structure is
992 // import_name / patient_name / ccda.xml
993 // import_name / patient_name / ccda.html
994 // import_name / patient_name / ccda.xsl
996 // we will need to have these limit options configurable but we have to be careful to
997 // we will limit our docsToImport to 500
998 // we will limit our patientsToImport to 500
1000 $z = new \ZipArchive();
1001 $tmpFile = reset($_FILES);
1002 $tmpFileName = $tmpFile['tmp_name'];
1003 $this->printZipContents($tmpFileName);
1005 // make sure we only have our documents folder and our ccda file
1006 $this->sanitizeZip($tmpFileName);
1007 $z->open($tmpFileName);
1008 $category_details = $this->getCarecoordinationTable()->fetch_cat_id('CCDA');
1009 $catId = $category_details[0]['id'] ?? null;
1010 if (empty($catId)) {
1011 throw new \RuntimeException("Could not find document category id for category of CCDA");
1013 $auditMasterRecordByPatients = [];
1014 for ($i = 0; $i < $z->numFiles; $i++) {
1015 $stat = $z->statIndex($i);
1016 // explode and make sure we have our three parts
1017 // our max directory structure is 4... anything more than that and we will bail
1018 $fileComponents = explode("/", str_replace('\\', '/', $stat['name']), 5);
1019 $componentCount = count($fileComponents);
1021 // now we need to do our document import for our ccda for this patient
1022 if ($componentCount > 0) {
1023 // let's process the ccda
1024 $file_name = basename($stat['name']);
1026 $pid = '00';
1027 $ob = new Document();
1028 $contents = $z->getFromIndex($i);
1029 if (stripos($file_name, '.xml') !== false) {
1030 $ret = $ob->createDocument($pid, $catId, $file_name, 'text/xml', $contents);
1031 if (!empty($ret)) {
1032 throw new \RuntimeException("Failed to create document from zip file " . $file_name . " error returned was " . $ret);
1035 $auditMasterRecordId = $this->getCarecoordinationTable()->import($ob->get_id());
1036 // we can use this to do any other processing as the files should be in order
1037 $auditMasterRecordByPatients[$fileComponents[2] ?? ''] = $auditMasterRecordId;
1041 $z->close();