Skip to content

Commit

Permalink
Fixes openemr#5455 Document Type Display Order (openemr#5490)
Browse files Browse the repository at this point in the history
We decided to implement this by having multiple sort order sections for
the four document types we support in CCD-A.

I wrote a new preview option for CCD-A that examines the XML document to
figure out the correct CCDA Template document type so we can know what sort order to use to
display for the correct document.

Added a deprecation notice to the ccr/display.php in case anyone is
using it.  We no longer reference it from the documents pages so we can
probably remove it at some point.

I also added a new filter event to the patient document tree viewer so
people can modify / update the html tree rendering on the documents if
they so choose.  This made it easy to loosely couple the tree with the
Carecoordination module.
  • Loading branch information
adunsulag authored Jun 18, 2022
1 parent 495ac0e commit 28da499
Show file tree
Hide file tree
Showing 8 changed files with 430 additions and 47 deletions.
1 change: 1 addition & 0 deletions ccr/display.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* @author Ajil P.M <[email protected]>
* @author Brady Miller <[email protected]>
* @author Stephen Nielson <[email protected]>
* @deprecated 7.0.0 People should use the /interface/modules/zend_modules/public/encountermanager/previewDocument?docId=<id> REST action instead of this file
* @copyright Copyright (c) 2011 Z&H Consultancy Services Private Limited <[email protected]>
* @copyright Copyright (c) 2013 Brady Miller <[email protected]>
* @copyright Copyright (c) 2022 Discover and Change <[email protected]>
Expand Down
54 changes: 28 additions & 26 deletions controllers/C_Document.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use OpenEMR\Common\Twig\TwigContainer;
use OpenEMR\Services\FacilityService;
use OpenEMR\Services\PatientService;
use OpenEMR\Events\PatientDocuments\PatientDocumentTreeViewFilterEvent;

class C_Document extends Controller
{
Expand Down Expand Up @@ -543,7 +544,7 @@ public function view_action(string $patient_id = null, $doc_id)
$menu = new HTML_TreeMenu();

//pass an empty array because we don't want the documents for each category showing up in this list box
$rnode = $this->array_recurse($this->tree->tree, array());
$rnode = $this->array_recurse($this->tree->tree, $patient_id, array());
$menu->addItem($rnode);
$treeMenu_listbox = new HTML_TreeMenu_Listbox($menu, array("promoText" => xl('Move Document to Category:')));

Expand Down Expand Up @@ -1053,7 +1054,7 @@ public function list_action($patient_id = "")
//print_r($categories_list);

$menu = new HTML_TreeMenu();
$rnode = $this->array_recurse($this->tree->tree, $categories_list);
$rnode = $this->array_recurse($this->tree->tree, $patient_id, $categories_list);
$menu->addItem($rnode);
$treeMenu = new HTML_TreeMenu_DHTML($menu, array('images' => 'public/images', 'defaultClass' => 'treeMenuDefault'));
$treeMenu_listbox = new HTML_TreeMenu_Listbox($menu, array('linkTarget' => '_self'));
Expand Down Expand Up @@ -1084,7 +1085,7 @@ public function list_action($patient_id = "")
return $this->fetch($GLOBALS['template_dir'] . "documents/" . $this->template_mod . "_list.html");
}

public function &array_recurse($array, $categories = array())
public function &array_recurse($array, $patient_id, $categories = array())
{
if (!is_array($array)) {
$array = array();
Expand All @@ -1107,7 +1108,7 @@ public function &array_recurse($array, $categories = array())
$current_node = &$this->_last_node;
}

$this->array_recurse($ar, $categories);
$this->array_recurse($ar, $patient_id, $categories);
} else {
if ($id === 0 && !empty($ar)) {
$info = $this->tree->get_node_info($id);
Expand All @@ -1133,29 +1134,30 @@ public function &array_recurse($array, $categories = array())
if (!AclMain::aclCheckAcoSpec($doc['aco_spec'])) {
$link = '';
}
if ($this->tree->get_node_name($id) == "CCR") {
$current_node->addItem(new HTML_TreeNode(array(
'text' => oeFormatShortDate($doc['docdate']) . ' ' . $doc['document_name'] . '-' . $doc['document_id'],
'link' => $link,
'icon' => $icon,
'expandedIcon' => $expandedIcon,
'events' => array('Onclick' => "javascript:newwindow=window.open('ccr/display.php?type=CCR&doc_id=" . attr_url($doc['document_id']) . "','_blank');")
)));
} elseif ($this->tree->get_node_name($id) == "CCD") {
$current_node->addItem(new HTML_TreeNode(array(
'text' => oeFormatShortDate($doc['docdate']) . ' ' . $doc['document_name'] . '-' . $doc['document_id'],
'link' => $link,
'icon' => $icon,
'expandedIcon' => $expandedIcon,
'events' => array('Onclick' => "javascript:newwindow=window.open('ccr/display.php?type=CCD&doc_id=" . attr_url($doc['document_id']) . "','_blank');")
)));
// CCD view
$nodeInfo = $this->tree->get_node_info($id);
$treeViewFilterEvent = new PatientDocumentTreeViewFilterEvent();
$treeViewFilterEvent->setCategoryTreeNode($this->tree);
$treeViewFilterEvent->setDocumentId($doc['document_id']);
$treeViewFilterEvent->setDocumentName($doc['document_name']);
$treeViewFilterEvent->setCategoryId($id);
$treeViewFilterEvent->setCategoryInfo($nodeInfo);
$treeViewFilterEvent->setPid($patient_id);

$htmlNode = new HTML_TreeNode(array(
'text' => oeFormatShortDate($doc['docdate']) . ' ' . $doc['document_name'] . '-' . $doc['document_id'],
'link' => $link,
'icon' => $icon,
'expandedIcon' => $expandedIcon
));

$treeViewFilterEvent->setHtmlTreeNode($htmlNode);
$filteredEvent = $GLOBALS['kernel']->getEventDispatcher()->dispatch($treeViewFilterEvent, PatientDocumentTreeViewFilterEvent::EVENT_NAME);
if ($filteredEvent->getHtmlTreeNode() != null) {
$current_node->addItem($filteredEvent->getHtmlTreeNode());
} else {
$current_node->addItem(new HTML_TreeNode(array(
'text' => oeFormatShortDate($doc['docdate']) . ' ' . $doc['document_name'] . '-' . $doc['document_id'],
'link' => $link,
'icon' => $icon,
'expandedIcon' => $expandedIcon
)));
// add the original node if we got back nothing from the server
$current_node->addItem($htmlNode);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,18 @@
namespace Carecoordination\Controller;

use Application\Listener\Listener;
use Carecoordination\Model\CcdaDocumentTemplateOids;
use Carecoordination\Model\CcdaGlobalsConfiguration;
use Carecoordination\Model\CcdaUserPreferencesTransformer;
use Carecoordination\Model\EncountermanagerTable;
use DOMDocument;
use Laminas\Filter\Compress\Zip;
use Laminas\Hydrator\Exception\RuntimeException;
use Laminas\Mvc\Controller\AbstractActionController;
use Laminas\View\Model\JsonModel;
use Laminas\View\Model\ViewModel;
use OpenEMR\Common\Logging\SystemLogger;
use OpenEMR\Common\Twig\TwigContainer;
use OpenEMR\Cqm\QrdaControllers\QrdaReportController;
use OpenEMR\Services\FacilityService;
use OpenEMR\Services\PractitionerService;
Expand Down Expand Up @@ -203,6 +208,69 @@ public function indexAction()
return $index;
}

/**
* Action handle for previewing a ccda document. Given the id of a document in
* @return ViewModel
*/
public function previewDocumentAction()
{

$request = $this->getRequest();
$docId = $request->getQuery("docId");

$document = new \Document($docId);
try {
$twig = new TwigContainer(null, $GLOBALS['kernel']);
// can_access will check session if no params are passed.
if (!$document->can_access()) {
echo $twig->getTwig()->render("templates/error/400.html.twig", ['statusCode' => 401, 'errorMessage' => 'Access Denied']);
exit;
} else if ($document->is_deleted()) {
echo $twig->getTwig()->render("templates/error/404.html.twig");
exit;
}

$content = $document->get_data();
if (empty($content)) {
echo $twig->getTwig()->render("templates/error/404.html.twig");
exit;
}
$content = $document->get_data();

$ccdaGlobalsConfiguration = new CcdaGlobalsConfiguration();
$ccdaUserPreferencesTransformer = new CcdaUserPreferencesTransformer(
$ccdaGlobalsConfiguration->getMaxSections(),
$ccdaGlobalsConfiguration->getSectionDisplayOrder()
);
$updatedContent = $ccdaUserPreferencesTransformer->transform($content);

// time to use our stylesheets
// TODO: @adunsulag we need to put this transformation process into its own class that we can reuse
$stylesheet = dirname(__FILE__) . "/../../../../../public/xsl/cda.xsl";

if (!file_exists($stylesheet)) {
throw new \RuntimeException("Could not find stylesheet file at location: " . $stylesheet);
}
$xmlDom = new DOMDocument();
$xmlDom->loadXML($updatedContent);
$ss = new DOMDocument();
$ss->load($stylesheet);
$proc = new XSLTProcessor();
$proc->importStylesheet($ss);
$updatedContent = $proc->transformToXml($xmlDom);
echo $updatedContent;
} catch (\Exception $exception) {
echo "Failed to generate preview for docId " . text($docId);
(new SystemLogger())->errorLogCaller(
"Failed to generate preview for ccda document",
['docId' => $docId, 'message' => $exception, 'trace' => $exception->getTraceAsString()]
);
}
$view = new ViewModel();
$view->setTerminal(true);
return $view;
}

public function buildCCDAHtml($content)
{
return $this->getEncountermanagerTable()->getCcdaAsHTML($content);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@
use Carecoordination\Model\CcdaGlobalsConfiguration;
use Carecoordination\Model\CcdaUserPreferencesTransformer;
use DOMDocument;
use HTML_TreeNode;
use OpenEMR\Common\Logging\SystemLogger;
use OpenEMR\Events\Globals\GlobalsInitializedEvent;
use OpenEMR\Events\PatientDocuments\PatientDocumentCreateCCDAEvent;
use OpenEMR\Events\PatientDocuments\PatientDocumentTreeViewFilterEvent;
use OpenEMR\Services\CDADocumentService;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use OpenEMR\Events\PatientDocuments\PatientDocumentViewCCDAEvent;
Expand All @@ -31,17 +33,24 @@ class CCDAEventsSubscriber implements EventSubscriberInterface
*/
private $generator;

/**
* @var string The url that users will be sent to inside OpenEMR to view a CCDA
*/
private $viewCcdaUrl;

public function __construct(CcdaGenerator $generator)
{
$this->generator = $generator;
$this->viewCcdaUrl = $GLOBALS['webroot'] . "/interface/modules/zend_modules/public/encountermanager/previewDocument";
}

public static function getSubscribedEvents()
{
return [
PatientDocumentCreateCCDAEvent::EVENT_NAME_CCDA_CREATE => 'onCCDACreateEvent',
PatientDocumentViewCCDAEvent::EVENT_NAME => 'onCCDAViewEvent',
GlobalsInitializedEvent::EVENT_HANDLE => 'setupUserGlobalSettings'
GlobalsInitializedEvent::EVENT_HANDLE => 'setupUserGlobalSettings',
PatientDocumentTreeViewFilterEvent::EVENT_NAME => 'onPatientDocumentTreeViewFilter'
];
}

Expand Down Expand Up @@ -151,4 +160,21 @@ public function setupUserGlobalSettings(GlobalsInitializedEvent $event)
$ccdaGlobalsConfiguration = new CcdaGlobalsConfiguration();
$ccdaGlobalsConfiguration->setupGlobalSections($service);
}

public function onPatientDocumentTreeViewFilter(PatientDocumentTreeViewFilterEvent $event)
{
if ($event->getHtmlTreeNode() != null) {
$categoryInfo = $event->getCategoryInfo();
// we are going to setup our onclick event to launch our
// TODO: do we want to look at our LOINC codes here as that seems to be more accurate than if we went with just names...
if (in_array(strtoupper(trim($categoryInfo['name'] ?? "")), ["CCR","CCDA","CCD"])) {
$htmlNode = $event->getHtmlTreeNode();
$url = $this->viewCcdaUrl . "?docId=" . attr_url($event->getDocumentId());
$htmlNode->events = [
'onClick' => "javascript:newwindow=window.open('" . $url . "','_blank');"
];
}
}
return $event;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

/**
* CcdaDocumentTemplateOids contains all of the CCD-A oids for the document template types we support in OpenEMR.
*
* @package openemr
* @link http://www.open-emr.org
* @author Stephen Nielson <[email protected]>
* @copyright Copyright (c) 2022 Discover and Change <[email protected]>
* @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
*/

namespace Carecoordination\Model;

// @see http://www.hl7.org/ccdasearch/ for a listing of this oids (Last accessed on June 14th 2022)
class CcdaDocumentTemplateOids
{
const CCD = "2.16.840.1.113883.10.20.22.1.2";
const REFERRAL = "2.16.840.1.113883.10.20.22.1.14";
const TRANSFER_SUMMARY = "2.16.840.1.113883.10.20.22.1.13";
const CAREPLAN = "2.16.840.1.113883.10.20.22.1.15";
const CCDA_DOCUMENT_TEMPLATE_OIDS = [self::CCD, self::REFERRAL, self::TRANSFER_SUMMARY, self::CAREPLAN];

public static function isValidDocumentTemplateOid($oid)
{
return in_array($oid, self::CCDA_DOCUMENT_TEMPLATE_OIDS);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,17 @@ class CcdaGlobalsConfiguration
const GLOBAL_SECTION_NAME = 'Carecoordination';

const GLOBAL_KEY_CCDA_MAX_SECTIONS = 'ccda_view_max_sections';
const GLOBAL_KEY_CCDA_SORT_ORDER = 'ccda_section_sort_order';

// these are based on the four document types we allow
const GLOBAL_KEY_CCDA_CCD_SORT_ORDER = 'ccda_ccd_section_sort_order';
const GLOBAL_KEY_CCDA_REFERRAL_SORT_ORDER = 'ccda_referral_section_sort_order';
const GLOBAL_KEY_CCDA_TOC_SORT_ORDER = 'ccda_toc_section_sort_order';
const GLOBAL_KEY_CCDA_CAREPLAN_SORT_ORDER = 'ccda_careplan_section_sort_order';

/**
* @var array in memory cache of the ccda list options in the database
*/
private $ccdaSections;

public function setupGlobalSections(GlobalsService $service)
{
Expand All @@ -38,37 +48,66 @@ public function setupGlobalSections(GlobalsService $service)
$service->appendToSection(self::GLOBAL_SECTION_NAME, self::GLOBAL_KEY_CCDA_MAX_SECTIONS, $setting);


$setting = new GlobalSetting(
xl('Section Display Order'),
GlobalSetting::DATA_TYPE_MULTI_SORTED_LIST_SELECTOR,
'',
xl('The order of clinical information sections to display when viewing a CCD-A document'),
true
);
$setting->addFieldOption(GlobalSetting::DATA_TYPE_OPTION_LIST_ID, 'ccda-sections');
$service->appendToSection(self::GLOBAL_SECTION_NAME, self::GLOBAL_KEY_CCDA_SORT_ORDER, $setting);
$docTypeSortOrderSections = [
xl("CCD Section Display Order") => self::GLOBAL_KEY_CCDA_CCD_SORT_ORDER
,xl("Referral Section Display Order") => self::GLOBAL_KEY_CCDA_REFERRAL_SORT_ORDER
,xl("Transition of Care Section Display Order") => self::GLOBAL_KEY_CCDA_TOC_SORT_ORDER
,xl("Careplan Section Display Order") => self::GLOBAL_KEY_CCDA_CAREPLAN_SORT_ORDER
,xl("Referral Section Display Order") => self::GLOBAL_KEY_CCDA_REFERRAL_SORT_ORDER
];
foreach ($docTypeSortOrderSections as $name => $globalKey) {
$setting = new GlobalSetting(
$name,
GlobalSetting::DATA_TYPE_MULTI_SORTED_LIST_SELECTOR,
'',
xl('The order of clinical information sections to display when viewing a CCD-A document'),
true
);
$setting->addFieldOption(GlobalSetting::DATA_TYPE_OPTION_LIST_ID, 'ccda-sections');
$service->appendToSection(self::GLOBAL_SECTION_NAME, $globalKey, $setting);
}
}

public function getMaxSections(): int
{
return intval($GLOBALS[self::GLOBAL_KEY_CCDA_MAX_SECTIONS] ?? 0);
}

/**
* Retrieves an mapped array of sorted section oids where each key in the map is the oid of a document template in CCD-A
* that we support inside of OpenEMR. This will retrieve the global settings that users have configured for their
* carecoordination document types.
* @return array
*/
public function getSectionDisplayOrder(): array
{
return [
CcdaDocumentTemplateOids::CCD => $this->getSectionDisplayOrderForType(self::GLOBAL_KEY_CCDA_CCD_SORT_ORDER)
,CcdaDocumentTemplateOids::CAREPLAN => $this->getSectionDisplayOrderForType(self::GLOBAL_KEY_CCDA_CAREPLAN_SORT_ORDER)
,CcdaDocumentTemplateOids::TRANSFER_SUMMARY => $this->getSectionDisplayOrderForType(self::GLOBAL_KEY_CCDA_TOC_SORT_ORDER)
,CcdaDocumentTemplateOids::REFERRAL => $this->getSectionDisplayOrderForType(self::GLOBAL_KEY_CCDA_REFERRAL_SORT_ORDER)
];
}

/**
* Retrieves an array of sorted section oids for the given global key we want to retrieve.
* @param string $key
* @return array
*/
private function getSectionDisplayOrderForType($key = self::GLOBAL_KEY_CCDA_CCD_SORT_ORDER)
{
$codeService = new CodeTypesService();
$sortOrder = array();
$sortOrderIndexesByKeys = [];
if (!empty($GLOBALS[self::GLOBAL_KEY_CCDA_SORT_ORDER])) {
$sortString = $GLOBALS[self::GLOBAL_KEY_CCDA_SORT_ORDER] ?? "";
if (!empty($GLOBALS[$key])) {
$sortString = $GLOBALS[$key] ?? "";
$sortOrder = explode(";", $sortString);
$sortOrderIndexesByKeys = array_combine($sortOrder, array_keys($sortOrder));
}
if (!empty($sortOrder)) {
// now we are going to grab our keys from the list service
$listService = new ListService();
// should be less than 50 items, better to just use memory than try to hit the db off a search
$sections = $listService->getOptionsByListName('ccda-sections');
$sections = $this->getCcdaSections();
foreach ($sections as $section) {
$option_id = $section['option_id'] ?? 'undefined';
if (isset($sortOrderIndexesByKeys[$option_id])) {
Expand All @@ -81,4 +120,13 @@ public function getSectionDisplayOrder(): array
}
return $sortOrder;
}

private function getCcdaSections()
{
if (empty($this->ccdaSections)) {
$listService = new ListService();
$this->ccdaSections = $listService->getOptionsByListName('ccda-sections');
}
return $this->ccdaSections;
}
}
Loading

0 comments on commit 28da499

Please sign in to comment.