From bdddc55d438e26687ee0ffbb935f7715b59bb19c Mon Sep 17 00:00:00 2001 From: Rod Roark Date: Fri, 2 Aug 2013 15:43:41 -0700 Subject: [PATCH] Added support for documents embedded in HL7 OBX results, and some other e-labs improvements. Improved new createDocument function and modified C_Document class to use it. --- controllers/C_Document.class.php | 183 ++++---------------------- interface/forms/procedure_order/new.php | 4 +- interface/orders/gen_hl7_order.inc.php | 28 ++-- interface/orders/list_reports.php | 2 + interface/orders/load_compendium.php | 27 ++-- interface/orders/order_manifest.php | 34 +++-- interface/orders/qoe.inc.php | 38 ++++-- interface/orders/receive_hl7_results.inc.php | 124 +++++++++++++++-- interface/orders/single_order_results.inc.php | 63 ++++++--- library/classes/Document.class.php | 136 +++++++++++++++++-- library/globals.inc.php | 38 +++--- 11 files changed, 412 insertions(+), 265 deletions(-) diff --git a/controllers/C_Document.class.php b/controllers/C_Document.class.php index a7a682ac6..e9801b3f7 100644 --- a/controllers/C_Document.class.php +++ b/controllers/C_Document.class.php @@ -18,8 +18,6 @@ class C_Document extends Controller { var $document_categories; var $tree; var $_config; - var $file_path; - var $manual_set_owner=false; // allows manual setting of a document owner/service function C_Document($template_mod = "general") { @@ -31,31 +29,7 @@ class C_Document extends Controller { //get global config options for this namespace $this->_config = $GLOBALS['oer_config']['documents']; - if($GLOBALS['document_storage_method']==1){ - $this->file_path = $GLOBALS['OE_SITE_DIR'].'/documents/temp/'; - } - else{ - if ( (!empty($_GET['higher_level_path'])) && (is_numeric($_GET['patient_id']) && $_GET['patient_id']>0) ) { - // Allow higher level directory structure in documents directory and a patient is mapped - $this->file_path = $this->_config['repository'] . preg_replace("/[^A-Za-z0-9\/]/","_",$_GET['higher_level_path']) . "/"; - } - else if (!empty($_GET['higher_level_path'])) { - // Allow higher level directory structure in documents directory and there is no patient mapping - // (Since a patient is not mapped, will create up to 10000 random directories and increment the path_depth by 1) - $this->file_path = $this->_config['repository'] . preg_replace("/[^A-Za-z0-9\/]/","_",$_GET['higher_level_path']) . "/" . rand(1,10000) . "/"; - $_POST['path_depth'] = $_POST['path_depth'] + 1; - } - else if ( !(is_numeric($_GET['patient_id'])) || !($_GET['patient_id']>0) ) { - // This is the default action except there is no patient mapping (when patient_id is 00 or direct) - // (Since a patient is not mapped, will create up to 10000 random directories and set the path_depth to 2) - $this->file_path = $this->_config['repository'] . preg_replace("/[^A-Za-z0-9]/","_",$_GET['patient_id']) . "/" . rand(1,10000) . "/"; - $_POST['path_depth'] = 2; - } - else { - // This is the default action where the patient is is used as one level directory structure in documents directory - $this->file_path = $this->_config['repository'] . preg_replace("/[^A-Za-z0-9]/","_",$_GET['patient_id']) . "/"; - } - } + $this->_args = array("patient_id" => $_GET['patient_id']); $this->assign("STYLE", $GLOBALS['style']); @@ -108,7 +82,12 @@ class C_Document extends Controller { if (is_numeric($_POST['category_id'])) { $category_id = $_POST['category_id']; } - if (is_numeric($_POST['patient_id'])) { + + $patient_id = 0; + if (isset($_GET['patient_id']) && !$couchDB) { + $patient_id = $_GET['patient_id']; + } + else if (is_numeric($_POST['patient_id'])) { $patient_id = $_POST['patient_id']; } @@ -128,145 +107,29 @@ class C_Document extends Controller { $error .= "The system does not permit uploading files of with size 0.\n"; } }else{ - - if (!file_exists($this->file_path)) { - if (!mkdir($this->file_path,0700,true)) { - $error .= "The system was unable to create the directory for this upload, '" . $this->file_path . "'.\n"; - } + $tmpfile = fopen($_FILES['file']['tmp_name'][$key], "r"); + $filetext = fread($tmpfile, $_FILES['file']['size'][$key]); + fclose($tmpfile); + if ($doDecryption) { + $filetext = $this->decrypt($filetext, $passphrase); } - if ( $_POST['destination'] != '' ) { - $fname = $_POST['destination']; - } - $fname = preg_replace("/[^a-zA-Z0-9_.]/","_",$fname); - if (file_exists($this->file_path.$fname)) { - $error .= xl('File with same name already exists at location:','','',' ') . $this->file_path . "\n"; - $fname = basename($this->_rename_file($this->file_path.$fname)); - $_FILES['file']['name'][$key] = $fname; - $error .= xl('Current file name was changed to','','',' ') . $fname ."\n"; + $fname = $_POST['destination']; } - - if ( $doDecryption ) { - $tmpfile = fopen( $_FILES['file']['tmp_name'][$key] , "r" ); - $filetext = fread( $tmpfile, $_FILES['file']['size'][$key] ); - $plaintext = $this->decrypt( $filetext, $passphrase ); - fclose($tmpfile); - unlink( $_FILES['file']['tmp_name'][$key] ); - $tmpfile = fopen( $_FILES['file']['tmp_name'][$key], "w+" ); - fwrite( $tmpfile, $plaintext ); - fclose( $tmpfile ); - $_FILES['file']['size'][$key] = filesize( $_FILES['file']['tmp_name'][$key] ); - } - - $docid = ''; - $resp = ''; - if($couchDB == true){ - $couch = new CouchDB(); - $docname = $_SESSION['authId'].$patient_id.$encounter.$fname.date("%Y-%m-%d H:i:s"); - $docid = $couch->stringToId($docname); - $tmpfile = fopen( $_FILES['file']['tmp_name'][$key], "rb" ); - $filetext = fread( $tmpfile, $_FILES['file']['size'][$key] ); - fclose( $tmpfile ); - //--------Temporarily writing the file for calculating the hash--------// - //-----------Will be removed after calculating the hash value----------// - $temp_file = fopen($this->file_path.$fname,"w"); - fwrite($temp_file,$filetext); - fclose($temp_file); - //---------------------------------------------------------------------// - - $json = json_encode(base64_encode($filetext)); - $db = $GLOBALS['couchdb_dbase']; - $data = array($db,$docid,$patient_id,$encounter,$_FILES['file']['type'][$key],$json); - $resp = $couch->check_saveDOC($data); - if(!$resp->id || !$resp->_rev){ - $data = array($db,$docid,$patient_id,$encounter); - $resp = $couch->retrieve_doc($data); - $docid = $resp->_id; - $revid = $resp->_rev; - } - else{ - $docid = $resp->id; - $revid = $resp->rev; - } - if(!$docid && !$revid){ //if couchdb save failed - $error .= "".xl("The file could not be saved to CouchDB.") . "\n"; - if($GLOBALS['couchdb_log']==1){ - ob_start(); - var_dump($resp); - $couchError=ob_get_clean(); - $log_content = date('Y-m-d H:i:s')." ==> Uploading document: ".$fname."\r\n"; - $log_content .= date('Y-m-d H:i:s')." ==> Failed to Store document content to CouchDB.\r\n"; - $log_content .= date('Y-m-d H:i:s')." ==> Document ID: ".$docid."\r\n"; - $log_content .= date('Y-m-d H:i:s')." ==> ".print_r($data,1)."\r\n"; - $log_content .= $couchError; - $this->document_upload_download_log($patient_id,$log_content);//log error if any, for testing phase only - } - } - else - { - $this->assign("upload_success", "true"); - } - } - - if($harddisk == true){ - $uploadSuccess = false; - $move_cmd = ($non_HTTP_owner ? "rename" : "move_uploaded_file"); - if($move_cmd($_FILES['file']['tmp_name'][$key],$this->file_path.$fname)){ - $uploadSuccess = true; - $this->assign("upload_success", "true"); - } - else{ - $error .= xl("The file could not be succesfully stored, this error is usually related to permissions problems on the storage system")."\n"; - } - } - $d = new Document(); - $d->storagemethod = $GLOBALS['document_storage_method']; - if($harddisk == true) { - $d->url = "file://" .$this->file_path.$fname; - if (is_numeric($_POST['path_depth'])) { - // this is for when directory structure is more than one level - $d->path_depth = $_POST['path_depth']; - } - } - else { - $d->url = $fname; - } - if($couchDB == true){ - $d->couch_docid = $docid; - $d->couch_revid = $revid; - } - if ($_FILES['file']['type'][$key] == 'text/xml') { - $d->mimetype = 'application/xml'; + $rc = $d->createDocument($patient_id, $category_id, $fname, + $_FILES['file']['type'][$key], $filetext, + empty($_GET['higher_level_path']) ? '' : $_GET['higher_level_path'], + empty($_POST['path_depth']) ? 1 : $_POST['path_depth'], + $non_HTTP_owner); + if ($rc) { + $error .= $rc . "\n"; } else { - $d->mimetype = $_FILES['file']['type'][$key]; - } - $d->size = $_FILES['file']['size'][$key]; - $d->owner = $non_HTTP_owner ? $non_HTTP_owner : $_SESSION['authUserID']; - $sha1Hash = sha1_file( $this->file_path.$fname ); - if($couchDB == true){ - //Removing the temporary file which is used to create the hash - unlink($this->file_path.$fname); - } - $d->hash = $sha1Hash; - $d->type = $d->type_array['file_url']; - $d->set_foreign_id($patient_id); - if(( ($harddisk == true) && $uploadSuccess ) || ($couchDB == true && $docid && $revid)){ - $d->persist(); - $d->populate(); + $this->assign("upload_success", "true"); } $sentUploadStatus[] = $d; - $this->assign("file",$sentUploadStatus); - - if (is_numeric($d->get_id()) && is_numeric($category_id)){ - $sql = "REPLACE INTO categories_to_documents set category_id = '" . $category_id . "', document_id = '" . $d->get_id() . "'"; - $d->_db->Execute($sql); - } - if($GLOBALS['couchdb_log']==1 && $log_content!=''){ - $log_content .= "\r\n\r\n"; - $this->document_upload_download_log($patient_id,$log_content); - } + $this->assign("file", $sentUploadStatus); } } } @@ -668,7 +531,7 @@ class C_Document extends Controller { //see if the patient dir exists in the repository and create if not if (!file_exists($new_path)) { if (!mkdir($new_path,0700)) { - $messages .= "The system was unable to create the directory for this upload, '" . $this->file_path . "'.\n"; + $messages .= "The system was unable to create the directory for this upload, '" . $new_path . "'.\n"; continue; } } diff --git a/interface/forms/procedure_order/new.php b/interface/forms/procedure_order/new.php index 1ed1bd087..fb499a7b8 100644 --- a/interface/forms/procedure_order/new.php +++ b/interface/forms/procedure_order/new.php @@ -294,7 +294,7 @@ function addProcLine() { " title=''" + " style='width:100%;cursor:pointer;cursor:hand' readonly />" + " " + - "
: " + + "
: " + "'" + @@ -519,7 +519,7 @@ generate_form_field(array('data_type'=>1,'field_id'=>'order_status', title='' style='width:100%;cursor:pointer;cursor:hand' readonly /> -
: +
: ' onclick='sel_related(this.name)' title='' diff --git a/interface/orders/gen_hl7_order.inc.php b/interface/orders/gen_hl7_order.inc.php index 732ce7129..4625e3e88 100644 --- a/interface/orders/gen_hl7_order.inc.php +++ b/interface/orders/gen_hl7_order.inc.php @@ -209,7 +209,7 @@ function gen_hl7_order($orderid, &$out) { $d2 . $d2 . hl7Text($porow['city']) . $d2 . hl7Text($porow['state']) . - $d2 . hl7Zip($porow['zip']) . + $d2 . hl7Zip($porow['postal_code']) . $d1 . $d1 . hl7Phone($porow['phone_home']) . $d1 . hl7Phone($porow['phone_biz']) . @@ -289,7 +289,7 @@ function gen_hl7_order($orderid, &$out) { $d2 . $d2 . hl7Text($porow['city']) . $d2 . hl7Text($porow['state']) . - $d2 . hl7Zip($porow['zip']) . + $d2 . hl7Zip($porow['postal_code']) . $d1 . hl7Phone($porow['phone_home']) . $d1 . hl7Phone($porow['phone_biz']) . $d1 . hl7Date($porow['DOB']) . // DOB @@ -420,7 +420,18 @@ function send_hl7_order($ppid, $out) { $msgid = $segmsh[9]; if (empty($msgid)) return xl('Internal error: Cannot find MSH-10'); - if ($protocol == 'SFTP') { + if ($protocol == 'DL' || $pprow['orders_path'] === '') { + header("Pragma: public"); + header("Expires: 0"); + header("Cache-Control: must-revalidate, post-check=0, pre-check=0"); + header("Content-Type: application/force-download"); + header("Content-Disposition: attachment; filename=order_$msgid.hl7"); + header("Content-Description: File Transfer"); + echo $out; + exit; + } + + else if ($protocol == 'SFTP') { ini_set('include_path', ini_get('include_path') . PATH_SEPARATOR . "$srcdir/phpseclib"); require_once("$srcdir/phpseclib/Net/SFTP.php"); @@ -438,17 +449,6 @@ function send_hl7_order($ppid, $out) { } } - else if ($protocol == 'DL') { - header("Pragma: public"); - header("Expires: 0"); - header("Cache-Control: must-revalidate, post-check=0, pre-check=0"); - header("Content-Type: application/force-download"); - header("Content-Disposition: attachment; filename=order_$msgid.hl7"); - header("Content-Description: File Transfer"); - echo $out; - exit; - } - // TBD: Insert "else if ($protocol == '???') {...}" to support other protocols. else { diff --git a/interface/orders/list_reports.php b/interface/orders/list_reports.php index cc41f6627..f715c1ef0 100644 --- a/interface/orders/list_reports.php +++ b/interface/orders/list_reports.php @@ -23,10 +23,12 @@ $sanitize_all_escapes = true; $fake_register_globals = false; require_once("../globals.php"); +require_once("$srcdir/log.inc"); require_once("$srcdir/acl.inc"); require_once("$srcdir/formdata.inc.php"); require_once("$srcdir/options.inc.php"); require_once("$srcdir/formatting.inc.php"); +require_once("$srcdir/classes/Document.class.php"); require_once("./receive_hl7_results.inc.php"); require_once("./gen_hl7_order.inc.php"); diff --git a/interface/orders/load_compendium.php b/interface/orders/load_compendium.php index 59e0b68ea..ee51acfce 100644 --- a/interface/orders/load_compendium.php +++ b/interface/orders/load_compendium.php @@ -5,7 +5,7 @@ * Supports loading of lab order codes and related order entry questions from CSV * format into the procedure_order and procedure_questions tables, respectively. * -* Copyright (C) 2012 Rod Roark +* Copyright (C) 2012-2013 Rod Roark * * LICENSE: This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -32,9 +32,10 @@ require_once("$srcdir/acl.inc"); // This array is an important reference for the supported labs and their NPI // numbers as known to this program. The clinic must define at least one -// address book entry for a lab that has a supported NPI number. +// procedure provider entry for a lab that has a supported NPI number. // $lab_npi = array( + '1235138868' => 'Diagnostic Pathology Medical Group', '1235186800' => 'Pathgroup Labs LLC', '1598760985' => 'Yosemite Pathology Medical Group', ); @@ -218,8 +219,8 @@ if ($form_step == 1) { } // end load compendium else if ($form_action == 2) { // load questions - // Mark the vendor's current questions inactive. - sqlStatement("UPDATE procedure_questions SET activity = 0 WHERE lab_id = ?", + // Delete the vendor's current questions. + sqlStatement("DELETE FROM procedure_questions WHERE lab_id = ?", array($lab_id)); // What should be uploaded is the "AOE Questions" spreadsheet provided by @@ -316,9 +317,9 @@ if ($form_step == 1) { } // end load questions } // End Pathgroup - // Vendor = Yosemite Pathology Medical Group + // Vendor = YPMG or DPMG // - if ($form_vendor == '1598760985') { + if ($form_vendor == '1598760985' || $form_vendor == '1235138868') { if ($form_action == 1) { // load compendium // Mark all "ord" rows having the indicated parent as inactive. sqlStatement("UPDATE procedure_type SET activity = 0 WHERE " . @@ -360,7 +361,7 @@ if ($form_step == 1) { else if ($form_action == 2) { // load questions // Mark the vendor's current questions inactive. - sqlStatement("UPDATE procedure_questions SET activity = 0 WHERE lab_id = ?", + sqlStatement("DELETE FROM procedure_questions WHERE lab_id = ?", array($lab_id)); // What should be uploaded is the "AOE Questions" spreadsheet provided @@ -375,6 +376,8 @@ if ($form_step == 1) { // indicates that more than one choice is allowed) // 5: Response (just one; the row is duplicated for each possible value) // + $seq = 0; + $last_code = ''; while (!feof($fhcsv)) { $acsv = fgetcsv($fhcsv, 4096); if (count($acsv) < 5 || ($acsv[3] !== "false" && $acsv[3] !== "true")) continue; @@ -385,6 +388,12 @@ if ($form_step == 1) { $options = trim($acsv[5]); if (empty($pcode) || empty($qcode)) continue; + if ($pcode != $last_code) { + $seq = 0; + $last_code = $pcode; + } + ++$seq; + // Figure out field type. $fldtype = 'T'; if (strpos($acsv[4], 'Drop') !== FALSE) $fldtype = 'S'; @@ -401,8 +410,8 @@ if ($form_step == 1) { if (empty($qrow['procedure_code'])) { sqlStatement("INSERT INTO procedure_questions SET " . "lab_id = ?, procedure_code = ?, question_code = ?, question_text = ?, " . - "fldtype = ?, required = ?, options = ?, activity = 1", - array($lab_id, $pcode, $qcode, trim($acsv[2]), $fldtype, $required, $options)); + "fldtype = ?, required = ?, options = ?, seq = ?, activity = 1", + array($lab_id, $pcode, $qcode, trim($acsv[2]), $fldtype, $required, $options, $seq)); } else { if ($qrow['activity'] == '1' && $qrow['options'] !== '' && $options !== '') { diff --git a/interface/orders/order_manifest.php b/interface/orders/order_manifest.php index 2c9246df2..f0ec125f5 100644 --- a/interface/orders/order_manifest.php +++ b/interface/orders/order_manifest.php @@ -69,13 +69,17 @@ function generate_order_summary($orderid) { "po.procedure_order_id, po.patient_id, po.date_ordered, po.order_status, " . "po.date_collected, po.specimen_type, po.specimen_location, po.lab_id, po.clinical_hx, " . "pd.pubpid, pd.lname, pd.fname, pd.mname, pd.DOB, pd.sex, " . + "pd.street, pd.city, pd.state, pd.postal_code, " . "fe.date, " . "pp.name AS labname, " . - "u.lname AS ulname, u.fname AS ufname, u.mname AS umname " . + "u.lname AS ulname, u.fname AS ufname, u.mname AS umname, " . + "ru.lname AS ref_lname, ru.fname AS ref_fname, ru.mname AS ref_mname, " . + "ru.street AS ref_street, ru.city AS ref_city, ru.state AS ref_state, ru.zip AS ref_zip " . "FROM procedure_order AS po " . "LEFT JOIN patient_data AS pd ON pd.pid = po.patient_id " . "LEFT JOIN procedure_providers AS pp ON pp.ppid = po.lab_id " . "LEFT JOIN users AS u ON u.id = po.provider_id " . + "LEFT JOIN users AS ru ON ru.id = pd.ref_providerID " . "LEFT JOIN form_encounter AS fe ON fe.pid = po.patient_id AND fe.encounter = po.encounter_id " . "WHERE po.procedure_order_id = ?", array($orderid)); @@ -173,28 +177,34 @@ function generate_order_summary($orderid) { - - + + - - + + - - + + + + + + + + -   -   + + @@ -267,9 +277,9 @@ function generate_order_summary($orderid) { echo " \n"; - echo " " . text("$procedure_code") . "\n"; - echo " " . text("$procedure_name") . "\n"; - echo " " . text("$diagnoses" ) . "\n"; + echo " " . myCellText("$procedure_code") . "\n"; + echo " " . myCellText("$procedure_name") . "\n"; + echo " " . myCellText("$diagnoses") . "\n"; echo " $notes\n"; echo " \n"; } diff --git a/interface/orders/qoe.inc.php b/interface/orders/qoe.inc.php index 3a8e593cc..78fa19bfa 100644 --- a/interface/orders/qoe.inc.php +++ b/interface/orders/qoe.inc.php @@ -75,8 +75,8 @@ function generate_qoe_html($ptid=0, $orderid=0, $dbseq=0, $formseq=0) { if ($fldtype == 'T') { // Text Field. - $s .= " 5) { + $s .= ""; + } + else { + $i = 0; + foreach ($a as $aval) { + list($desc, $code) = explode(':', $aval); + if (empty($code)) $code = $desc; + if ($i) $s .= "
"; + $s .= " 8) $ret .= ' ' . substr($s, 8, 2) . ':' . substr($s, 10, 2) . ':'; @@ -86,6 +87,44 @@ function rhl7ReportStatus($s) { } /** + * Convert a lower case file extension to a MIME type. + * The extension comes from OBX[5][0] which is itself a huge assumption that + * the HL7 2.3 standard does not help with. Don't be surprised when we have to + * adapt to conventions of various other labs. + * + * @param string $fileext The lower case extension. + * @return string MIME type. + */ +function rhl7MimeType($fileext) { + if ($fileext == 'pdf') return 'application/pdf'; + if ($fileext == 'doc') return 'application/msword'; + if ($fileext == 'rtf') return 'application/rtf'; + if ($fileext == 'txt') return 'text/plain'; + if ($fileext == 'zip') return 'application/zip'; + return 'application/octet-stream'; +} + +/** + * Extract encapsulated document data according to its encoding type. + * + * @param string $enctype Encoding type from OBX[5][3]. + * @param string &$src Encoded data from OBX[5][4]. + * @return string Decoded data, or FALSE if error. + */ +function rhl7DecodeData($enctype, &$src) { + if ($enctype == 'Base64') return base64_decode($src); + if ($enctype == 'A' ) return rhl7Text($src); + if ($enctype == 'Hex') { + $data = ''; + for ($i = 0; $i < strlen($src) - 1; $i += 2) { + $data .= chr(hexdec($src[$i] . $src[$i+1])); + } + return $data; + } + return FALSE; +} + +/** * Parse and save. * * @param string &$pprow A row from the procedure_providers table. @@ -117,6 +156,7 @@ function receive_hl7_results(&$hl7) { $procedure_report_id = 0; $arep = array(); // holding area for OBR and its NTE data $ares = array(); // holding area for OBX and its NTE data + $code_seq_array = array(); // tracks sequence numbers of order codes // This is so we know where we are if a segment like NTE that can appear in // different places is encountered. @@ -128,6 +168,15 @@ function receive_hl7_results(&$hl7) { $d2 = substr($hl7, 4, 1); // typically ^ $d3 = substr($hl7, 5, 1); // typically ~ + // We'll need the document category ID for any embedded documents. + $catrow = sqlQuery("SELECT id FROM categories WHERE name = ?", + array($GLOBALS['lab_results_category_name'])); + if (empty($catrow['id'])) { + return xl('Document category for lab results does not exist') . + ': ' . $GLOBALS['lab_results_category_name']; + } + $results_category_id = $catrow['id']; + $segs = explode($d0, $hl7); foreach ($segs as $seg) { @@ -210,12 +259,21 @@ function receive_hl7_results(&$hl7) { // They did not return an encounter number to verify, so more checking // might be done here to make sure the patient seems to match. } + $code_seq_array = array(); } // Find the order line item (procedure code) that matches this result. + // If there is more than one, then we select the one whose sequence number + // is next after the last sequence number encountered for this procedure + // code; this assumes that result OBRs are returned in the same sequence + // as the corresponding OBRs in the order. + if (!isset($code_seq_array[$in_procedure_code])) { + $code_seq_array[$in_procedure_code] = 0; + } $pcquery = "SELECT pc.* FROM procedure_order_code AS pc " . "WHERE pc.procedure_order_id = ? AND pc.procedure_code = ? " . - "ORDER BY procedure_order_seq LIMIT 1"; - $pcrow = sqlQuery($pcquery, array($in_orderid, $in_procedure_code)); + "ORDER BY (procedure_order_seq <= ?), procedure_order_seq LIMIT 1"; + $pcrow = sqlQuery($pcquery, array($in_orderid, $in_procedure_code, + $code_seq_array['$in_procedure_code'])); if (empty($pcrow)) { // There is no matching procedure in the order, so it must have been // added after the original order was sent, either as a manual request @@ -229,6 +287,7 @@ function receive_hl7_results(&$hl7) { array($in_orderid, $in_procedure_code, $in_procedure_name)); $pcrow = sqlQuery($pcquery, array($in_orderid, $in_procedure_code)); } + $code_seq_array[$in_procedure_code] = 0 + $pcrow['procedure_order_seq']; $arep = array(); $arep['procedure_order_id'] = $in_orderid; $arep['procedure_order_seq'] = $pcrow['procedure_order_seq']; @@ -250,17 +309,31 @@ function receive_hl7_results(&$hl7) { } $ares = array(); $ares['procedure_report_id'] = $procedure_report_id; - // OBX-5 can be a very long string of text with "~" as line separators. - // The first line of comments is reserved for such things. - if (strlen($a[5]) > 200) { + $ares['result_data_type'] = substr($a[2], 0, 1); // N, S, F or E + $ares['comments'] = $commentdelim; + if ($a[2] == 'ED') { + // This is the case of results as an embedded document. We will create + // a normal patient document in the assigned category for lab results. + $tmp = explode($d2, $a[5]); + $fileext = strtolower($tmp[0]); + $filename = date("Ymd_His") . '.' . $fileext; + $data = rhl7DecodeData($tmp[3], &$tmp[4]); + if ($data === FALSE) return xl('Invalid encapsulated data encoding type') . ': ' . $tmp[3]; + $d = new Document(); + $rc = $d->createDocument($porow['patient_id'], $results_category_id, + $filename, rhl7MimeType($fileext), $data); + if ($rc) return $rc; // This would be error message text. + $ares['document_id'] = $d->get_id(); + } + else if (strlen($a[5]) > 200) { + // OBX-5 can be a very long string of text with "~" as line separators. + // The first line of comments is reserved for such things. $ares['result_data_type'] = 'L'; $ares['result'] = ''; $ares['comments'] = rhl7Text($a[5]) . $commentdelim; } else { - $ares['result_data_type'] = substr($a[2], 0, 1); // N, S or F $ares['result'] = rhl7Text($a[5]); - $ares['comments'] = $commentdelim; } $tmp = explode($d2, $a[3]); $ares['result_code'] = rhl7Text($tmp[0]); @@ -273,6 +346,31 @@ function receive_hl7_results(&$hl7) { $ares['result_status'] = rhl7ReportStatus($a[11]); } + else if ($a[0] == 'ZEF') { + // ZEF segment is treated like an OBX with an embedded Base64-encoded PDF. + $context = 'OBX'; + rhl7FlushResult($ares); + if (!$procedure_report_id) { + $procedure_report_id = rhl7FlushReport($arep); + } + $ares = array(); + $ares['procedure_report_id'] = $procedure_report_id; + $ares['result_data_type'] = 'E'; + $ares['comments'] = $commentdelim; + // + $fileext = 'pdf'; + $filename = date("Ymd_His") . '.' . $fileext; + $data = rhl7DecodeData('Base64', $a[2]); + if ($data === FALSE) return xl('ZEF segment internal error'); + $d = new Document(); + $rc = $d->createDocument($porow['patient_id'], $results_category_id, + $filename, rhl7MimeType($fileext), $data); + if ($rc) return $rc; // This would be error message text. + $ares['document_id'] = $d->get_id(); + // + $ares['date'] = $arep['date_report']; + } + else if ($a[0] == 'NTE' && $context == 'OBX') { $ares['comments'] .= rhl7Text($a[3]) . $commentdelim; } @@ -311,13 +409,19 @@ function poll_hl7_results(&$messages) { $hl7 = ''; if ($protocol == 'SFTP') { + $remote_port = 22; + // Hostname may have ":port" appended to specify a nonstandard port number. + if ($i = strrpos($remote_host, ':')) { + $remote_port = 0 + substr($remote_host, $i + 1); + $remote_host = substr($remote_host, 0, $i); + } ini_set('include_path', ini_get('include_path') . PATH_SEPARATOR . "$srcdir/phpseclib"); require_once("$srcdir/phpseclib/Net/SFTP.php"); // Compute the target path name. $pathname = '.'; if ($pprow['results_path']) $pathname = $pprow['results_path'] . '/' . $pathname; // Connect to the server and enumerate files to process. - $sftp = new Net_SFTP($remote_host); + $sftp = new Net_SFTP($remote_host, $remote_port); if (!$sftp->login($pprow['login'], $pprow['password'])) { return xl('Login to remote host') . " '$remote_host' " . xl('failed'); } @@ -350,7 +454,9 @@ function poll_hl7_results(&$messages) { // Parse and process its contents. $msg = receive_hl7_results($hl7); if ($msg) { - $messages[] = xl('Error processing file') . " '$file': " . $msg; + $tmp = xl('Error processing file') . " '$file': " . $msg; + $messages[] = $tmp; + newEvent("lab-results-error", $_SESSION['authUser'], $_SESSION['authProvider'], 0, $tmp); ++$badcount; continue; } diff --git a/interface/orders/single_order_results.inc.php b/interface/orders/single_order_results.inc.php index a22884947..0a9e0da61 100644 --- a/interface/orders/single_order_results.inc.php +++ b/interface/orders/single_order_results.inc.php @@ -23,6 +23,7 @@ require_once("$srcdir/acl.inc"); require_once("$srcdir/formdata.inc.php"); require_once("$srcdir/options.inc.php"); require_once("$srcdir/formatting.inc.php"); +require_once("$srcdir/classes/Document.class.php"); function getListItem($listid, $value) { $lrow = sqlQuery("SELECT title FROM list_options " . @@ -34,6 +35,7 @@ function getListItem($listid, $value) { } function myCellText($s) { + $s = trim($s); if ($s === '') return ' '; return text($s); } @@ -59,7 +61,7 @@ function generate_order_report($orderid, $input_form=false) { $orow = sqlQuery("SELECT " . "po.procedure_order_id, po.date_ordered, " . - "po.order_status, po.specimen_type, " . + "po.order_status, po.specimen_type, po.patient_id, " . "pd.pubpid, pd.lname, pd.fname, pd.mname, " . "fe.date, " . "pp.name AS labname, " . @@ -71,6 +73,8 @@ function generate_order_report($orderid, $input_form=false) { "LEFT JOIN form_encounter AS fe ON fe.pid = po.patient_id AND fe.encounter = po.encounter_id " . "WHERE po.procedure_order_id = ?", array($orderid)); + + $patient_id = $orow['patient_id']; ?>