3 * Script to display results for a given procedure order.
5 * Copyright (C) 2013-2016 Rod Roark <rod@sunsetsystems.com>
7 * LICENSE: This program is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation; either version 2
10 * of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://opensource.org/licenses/gpl-license.php>.
19 * @author Rod Roark <rod@sunsetsystems.com>
22 require_once($GLOBALS["srcdir"] . "/acl.inc");
23 require_once($GLOBALS["srcdir"] . "/options.inc.php");
24 require_once($GLOBALS["srcdir"] . "/formatting.inc.php");
26 function getListItem($listid, $value) {
27 $lrow = sqlQuery("SELECT title FROM list_options " .
28 "WHERE list_id = ? AND option_id = ? AND activity = 1",
29 array($listid, $value));
30 $tmp = xl_list_label($lrow['title']);
31 if (empty($tmp)) $tmp = (($value === '') ?
'' : "($value)");
35 function myCellText($s) {
37 if ($s === '') return ' ';
41 // Check if the given string already exists in the $aNotes array.
42 // If not, stores it as a new entry.
43 // Either way, returns the corresponding key which is a small integer.
44 function storeNote($s) {
46 $key = array_search($s, $aNotes);
47 if ($key !== FALSE) return $key;
48 $key = count($aNotes);
53 // Display a single row of output including order, report and result information.
55 function generate_result_row(&$ctx, &$row, &$rrow, $priors_omitted=false) {
56 $lab_id = empty($row['lab_id' ]) ?
0 : ($row['lab_id' ] +
0);
57 $order_type_id = empty($row['order_type_id' ]) ?
0 : ($row['order_type_id' ] +
0);
58 $order_seq = empty($row['procedure_order_seq']) ?
0 : ($row['procedure_order_seq'] +
0);
59 $report_id = empty($row['procedure_report_id']) ?
0 : ($row['procedure_report_id'] +
0);
60 $procedure_code = empty($row['procedure_code' ]) ?
'' : $row['procedure_code'];
61 $procedure_name = empty($row['procedure_name' ]) ?
'' : $row['procedure_name'];
62 $date_report = empty($row['date_report' ]) ?
'' : substr($row['date_report'], 0, 16);
63 $date_report_suf = empty($row['date_report_tz' ]) ?
'' : (' ' . $row['date_report_tz' ]);
64 $date_collected = empty($row['date_collected' ]) ?
'' : substr($row['date_collected'], 0, 16);
65 $date_collected_suf = empty($row['date_collected_tz' ]) ?
'' : (' ' . $row['date_collected_tz' ]);
66 $specimen_num = empty($row['specimen_num' ]) ?
'' : $row['specimen_num'];
67 $report_status = empty($row['report_status' ]) ?
'' : $row['report_status'];
68 $review_status = empty($row['review_status' ]) ?
'received' : $row['review_status'];
71 if ($report_id && !isset($ctx['seen_report_ids'][$report_id])) {
72 $ctx['seen_report_ids'][$report_id] = true;
73 if ($review_status != 'reviewed') {
74 if ($ctx['sign_list']) $ctx['sign_list'] .= ',';
75 $ctx['sign_list'] .= $report_id;
77 // Allowing for multiple report notes separated by newlines.
78 if (!empty($row['report_notes'])) {
79 $notes = explode("\n", $row['report_notes']);
80 foreach ($notes as $note) {
81 if ($note === '') continue;
82 if ($report_noteid) $report_noteid .= ', ';
83 $report_noteid .= 1 +
storeNote($note);
87 // allow for 0 to be displayed as a result value
88 if($rrow['result'] == '' && $rrow['result'] !== 0 && $rrow['result'] !== '0') {
91 $result_result = $rrow['result'];
93 $result_code = empty($rrow['result_code' ]) ?
'' : $rrow['result_code'];
94 $result_text = empty($rrow['result_text' ]) ?
'' : $rrow['result_text'];
95 $result_abnormal = empty($rrow['abnormal' ]) ?
'' : $rrow['abnormal'];
96 $result_units = empty($rrow['units' ]) ?
'' : $rrow['units'];
97 $result_facility = empty($rrow['facility' ]) ?
'' : $rrow['facility'];
98 $result_comments = empty($rrow['comments' ]) ?
'' : $rrow['comments'];
99 $result_range = empty($rrow['range' ]) ?
'' : $rrow['range'];
100 $result_status = empty($rrow['result_status' ]) ?
'' : $rrow['result_status'];
101 $result_document_id = empty($rrow['document_id' ]) ?
'' : $rrow['document_id'];
103 // Someone changed the delimiter in result comments from \n to \r.
104 // Have to make sure results are consistent with those before that change.
105 $result_comments = str_replace("\r", "\n", $result_comments);
107 if ($i = strpos($result_comments, "\n")) { // "=" is not a mistake!
108 // If the first line of comments is not empty, then it is actually a long textual
109 // result value with lines delimited by "~" characters.
110 $result_comments = str_replace("~", "\n", substr($result_comments, 0, $i)) .
111 substr($result_comments, $i);
113 $result_comments = trim($result_comments);
116 if (!empty($result_comments)) {
117 $result_noteid = 1 +
storeNote($result_comments);
119 if ($priors_omitted) {
120 if ($result_noteid) $result_noteid .= ', ';
121 $result_noteid .= 1 +
storeNote(xl('This is the latest of multiple result values.'));
122 $ctx['priors_omitted'] = true;
125 // If a performing organization is provided, make a note for it also.
126 $result_facility = trim(str_replace("\r", "\n", $result_facility));
127 if ($result_facility) {
128 if ($result_noteid) $result_noteid .= ', ';
129 $result_noteid .= 1 +
storeNote(xl('Performing organization') . ":\n" . $result_facility);
132 if ($ctx['lastpcid'] != $order_seq) {
135 $bgcolor = "#" . (($ctx['encount'] & 1) ?
"ddddff" : "ffdddd");
137 echo " <tr class='detail' bgcolor='$bgcolor'>\n";
139 if ($ctx['lastpcid'] != $order_seq) {
140 $ctx['lastprid'] = -1; // force report fields on first line of each procedure
141 $tmp = text("$procedure_code: $procedure_name");
142 // Get the LOINC code if one exists in the compendium for this order type.
143 if (empty($GLOBALS['PATIENT_REPORT_ACTIVE'])) {
144 $trow = sqlQuery("SELECT standard_code FROM procedure_type WHERE " .
145 "lab_id = ? AND procedure_code = ? AND procedure_type = 'ord' " .
146 "ORDER BY procedure_type_id LIMIT 1",
147 array($lab_id, $procedure_code));
148 if (!empty($trow['standard_code'])) {
149 $tmp = "<a href='javascript:educlick(\"LOINC\",\"" . attr($trow['standard_code']) .
153 echo " <td>$tmp</td>\n";
156 echo " <td style='background-color:transparent'> </td>";
159 // If this starts a new report or a new order, generate the report fields.
160 if ($report_id != $ctx['lastprid']) {
162 echo myCellText(oeFormatShortDate(substr($date_report, 0, 10)) . substr($date_report, 10) . $date_report_suf);
166 echo myCellText(oeFormatShortDate(substr($date_collected, 0, 10)) . substr($date_collected, 10) . $date_collected_suf);
170 echo myCellText($specimen_num);
173 echo " <td title='" . xla('Check mark indicates reviewed') . "'>";
174 echo myCellText(getListItem('proc_rep_status', $report_status));
175 if ($row['review_status'] == 'reviewed') {
176 echo " ✓"; // unicode check mark character
180 echo " <td align='center'>";
181 echo myCellText($report_noteid);
185 echo " <td colspan='5' style='background-color:transparent'> </td>\n";
188 if ($result_code !== '' ||
$result_document_id) {
189 $tmp = myCellText($result_code);
190 if (empty($GLOBALS['PATIENT_REPORT_ACTIVE']) && !empty($result_code)) {
191 $tmp = "<a href='javascript:educlick(\"LOINC\",\"" . attr($result_code) .
194 echo " <td>$tmp</td>\n";
196 echo myCellText($result_text);
199 $tmp = myCellText(getListItem('proc_res_abnormal', $result_abnormal));
200 if ($result_abnormal && strtolower($result_abnormal) != 'no') {
201 echo "<b><font color='red'>$tmp</font></b>";
208 if ($result_document_id) {
209 $d = new Document($result_document_id);
210 echo " <td colspan='3'>";
211 if (empty($GLOBALS['PATIENT_REPORT_ACTIVE'])) {
212 echo "<a href='" . $GLOBALS['webroot'] . "/controller.php?document";
213 echo "&retrieve&patient_id=$patient_id&document_id=$result_document_id' ";
214 echo "onclick='top.restoreSession()'>";
216 echo $d->get_url_file();
217 if (empty($GLOBALS['PATIENT_REPORT_ACTIVE'])) {
221 $narrative_notes = sqlQuery("select group_concat(note SEPARATOR '\n') as notes from notes where foreign_id = ?",array($result_document_id));
222 if(!empty($narrative_notes)){
223 $nnotes = explode("\n",$narrative_notes['notes']);
224 $narrative_note_list = '';
225 foreach($nnotes as $nnote){
226 if($narrative_note_list == '') $narrative_note_list = 'Narrative Notes:';
227 $narrative_note_list .= $nnote;
230 if($narrative_note_list != ''){ if ($result_noteid) $result_noteid .= ', '; $result_noteid .= 1 +
storeNote($narrative_note_list);}
236 echo myCellText($result_result);
239 echo myCellText($result_range);
242 // Units comes from the lab so might not match anything in the proc_unit list,
243 // but in that case the call will return the same value.
244 echo myCellText(getListItemTitle('proc_unit', $result_units));
247 echo " <td align='center'>";
248 echo myCellText($result_noteid);
252 echo " <td colspan='7' style='background-color:transparent'> </td>\n";
257 $ctx['lastpcid'] = $order_seq;
258 $ctx['lastprid'] = $report_id;
262 function generate_order_report($orderid, $input_form=false, $genstyles=true, $finals_only=false) {
265 // Check authorization.
266 $thisauth = acl_check('patients', 'med');
267 if (!$thisauth) return xl('Not authorized');
269 $orow = sqlQuery("SELECT " .
270 "po.procedure_order_id, po.date_ordered, po.control_id, " .
271 "po.order_status, po.specimen_type, po.patient_id, " .
272 "pd.pubpid, pd.lname, pd.fname, pd.mname, pd.cmsportal_login, pd.language, " .
274 "pp.name AS labname, " .
275 "u.lname AS ulname, u.fname AS ufname, u.mname AS umname " .
276 "FROM procedure_order AS po " .
277 "LEFT JOIN patient_data AS pd ON pd.pid = po.patient_id " .
278 "LEFT JOIN procedure_providers AS pp ON pp.ppid = po.lab_id " .
279 "LEFT JOIN users AS u ON u.id = po.provider_id " .
280 "LEFT JOIN form_encounter AS fe ON fe.pid = po.patient_id AND fe.encounter = po.encounter_id " .
281 "WHERE po.procedure_order_id = ?",
284 $patient_id = $orow['patient_id'];
285 $language = $orow['language'];
288 <?php
if ($genstyles) { ?
>
291 <?php
if (empty($_SESSION['language_direction']) ||
$_SESSION['language_direction'] == 'ltr') { ?
>
293 .labres tr
.head
{ font
-size
:10pt
; background
-color
:#cccccc; text-align:center; }
294 .labres tr
.detail
{ font
-size
:10pt
; }
295 .labres a
, .labres a
:visited
, .labres a
:hover
{ color
:#0000cc; }
299 border
-width
: 1px
0px
0px
1px
;
302 .labres td
, .labres th
{
304 border
-width
: 0px
1px
1px
0px
;
307 /***** What is this for? Seems ugly to me. --Rod
309 background-color: #cccccc;
315 .labres tr
.head
{ font
-size
:10pt
; text
-align
:center
; }
316 .labres tr
.detail
{ font
-size
:10pt
; }
320 border
-width
: 1px
0px
0px
1px
;
323 .labres td
, .labres th
{
325 border
-width
: 0px
1px
1px
0px
;
329 .labres table td
.td
-label
{
340 <?php
if ($input_form) { ?
>
341 <script type
="text/javascript" src
="<?php echo $GLOBALS['webroot']; ?>/library/textformat.js"></script
>
342 <?php
} // end if input form ?>
344 <?php
if (empty($GLOBALS['PATIENT_REPORT_ACTIVE'])) { ?
>
346 <script type
="text/javascript" src
="<?php echo $GLOBALS['webroot']; ?>/library/dialog.js?v=<?php echo $v_js_includes; ?>"></script
>
347 <script language
="JavaScript">
349 var mypcc
= '<?php echo $GLOBALS['phone_country_code
'] ?>';
351 // Called to show patient notes related to this order in the "other" frame.
352 // This works even if we are in a separate window.
353 function showpnotes(orderid
) {
354 // Find the top or bottom frame that contains or opened this page; return if none.
355 var w
= window
.opener ? window
.opener
: window
;
356 for (; w
.name
!= 'RTop' && w
.name
!= 'RBot'; w
= w
.parent
) {
358 // This message is not translated because a developer will need to find it.
359 alert('Internal error locating target frame in ' +
(window
.opener ?
'opener' : 'window'));
363 var othername
= (w
.name
== 'RTop') ?
'RBot' : 'RTop';
364 w
.parent
.left_nav
.forceDual();
365 w
.parent
.left_nav
.loadFrame('pno1', othername
, 'patient_file/summary/pnotes_full.php?orderid=' + orderid
);
369 // Process click on LOINC code for patient education popup.
370 function educlick(codetype
, codevalue
) {
371 dlgopen('<?php echo $GLOBALS['webroot
']; ?>/interface/patient_file/education.php' +
372 '?type=' +
encodeURIComponent(codetype
) +
373 '&code=' +
encodeURIComponent(codevalue
) +
374 '&language=<?php echo urlencode($language); ?>',
375 '_blank', 1024, 750,true); // Force a new window instead of iframe to address cross site scripting potential
380 <?php
} // end if not patient report ?>
382 <?php
if ($input_form) { ?
>
383 <form method
='post' action
='single_order_results.php?orderid=<?php echo $orderid; ?>'>
384 <?php
} // end if input form ?>
388 <table width
='100%' cellpadding
='2' cellspacing
='0'>
390 <td
class="td-label" width
='5%' nowrap
><?php
echo xlt('Patient ID'); ?
></td
>
391 <td width
='45%'><?php
echo myCellText($orow['pubpid']); ?
></td
>
392 <td
class="td-label" width
='5%' nowrap
><?php
echo xlt('Order ID'); ?
></td
>
395 if (empty($GLOBALS['PATIENT_REPORT_ACTIVE'])) {
396 echo " <a href='" . $GLOBALS['webroot'];
397 echo "/interface/orders/order_manifest.php?orderid=";
398 echo attr($orow['procedure_order_id']);
399 echo "' target='_blank' onclick='top.restoreSession()'>";
401 echo myCellText($orow['procedure_order_id']);
402 if (empty($GLOBALS['PATIENT_REPORT_ACTIVE'])) {
405 if ($orow['control_id']) {
406 echo myCellText(' ' . xl('Lab') . ': ' . $orow['control_id']);
412 <td
class="td-label" nowrap
><?php
echo xlt('Patient Name'); ?
></td
>
413 <td
><?php
echo myCellText($orow['lname'] . ', ' . $orow['fname'] . ' ' . $orow['mname']); ?
></td
>
414 <td
class="td-label" nowrap
><?php
echo xlt('Ordered By'); ?
></td
>
415 <td
><?php
echo myCellText($orow['ulname'] . ', ' . $orow['ufname'] . ' ' . $orow['umname']); ?
></td
>
418 <td
class="td-label" nowrap
><?php
echo xlt('Order Date'); ?
></td
>
419 <td
><?php
echo myCellText(oeFormatShortDate($orow['date_ordered'])); ?
></td
>
420 <td
class="td-label" nowrap
><?php
echo xlt('Print Date'); ?
></td
>
421 <td
><?php
echo oeFormatShortDate(date('Y-m-d')); ?
></td
>
424 <td
class="td-label" nowrap
><?php
echo xlt('Order Status'); ?
></td
>
425 <td
><?php
echo myCellText($orow['order_status']); ?
></td
>
426 <td
class="td-label" nowrap
><?php
echo xlt('Encounter Date'); ?
></td
>
427 <td
><?php
echo myCellText(oeFormatShortDate(substr($orow['date'], 0, 10))); ?
></td
>
430 <td
class="td-label" nowrap
><?php
echo xlt('Lab'); ?
></td
>
431 <td
><?php
echo myCellText($orow['labname']); ?
></td
>
432 <td
class="td-label" nowrap
><?php
echo $orow['specimen_type'] ?
xlt('Specimen Type') : ' '; ?
></td
>
433 <td
><?php
echo myCellText($orow['specimen_type']); ?
></td
>
439 <table width
='100%' cellpadding
='2' cellspacing
='0'>
442 <td
class="td-label" rowspan
='2' valign
='middle'><?php
echo xlt('Ordered Procedure'); ?
></td
>
443 <td
class="td-label" colspan
='5'><?php
echo xlt('Report'); ?
></td
>
444 <td
class="td-label" colspan
='7'><?php
echo xlt('Results'); ?
></td
>
448 <td
><?php
echo xlt('Reported'); ?
></td
>
449 <td
><?php
echo xlt('Collected'); ?
></td
>
450 <td
><?php
echo xlt('Specimen'); ?
></td
>
451 <td
><?php
echo xlt('Status'); ?
></td
>
452 <td
><?php
echo xlt('Note'); ?
></td
>
453 <td
><?php
echo xlt('Code'); ?
></td
>
454 <td
><?php
echo xlt('Name'); ?
></td
>
455 <td
><?php
echo xlt('Abn'); ?
></td
>
456 <td
><?php
echo xlt('Value'); ?
></td
>
457 <td
><?php
echo xlt('Range'); ?
></td
>
458 <td
><?php
echo xlt('Units'); ?
></td
>
459 <td
><?php
echo xlt('Note'); ?
></td
>
464 "po.lab_id, po.date_ordered, pc.procedure_order_seq, pc.procedure_code, " .
465 "pc.procedure_name, " .
466 "pr.date_report, pr.date_report_tz, pr.date_collected, pr.date_collected_tz, " .
467 "pr.procedure_report_id, pr.specimen_num, pr.report_status, pr.review_status, pr.report_notes " .
468 "FROM procedure_order AS po " .
469 "JOIN procedure_order_code AS pc ON pc.procedure_order_id = po.procedure_order_id " .
470 "LEFT JOIN procedure_report AS pr ON pr.procedure_order_id = po.procedure_order_id AND " .
471 "pr.procedure_order_seq = pc.procedure_order_seq " .
472 "WHERE po.procedure_order_id = ? " .
473 "ORDER BY pc.procedure_order_seq, pr.date_report, pr.procedure_report_id";
475 $res = sqlStatement($query, array($orderid));
478 $empty_results = array('result_code' => '');
480 // Context for this call that may be used in other functions.
487 'seen_report_ids' => array(),
490 while ($row = sqlFetchArray($res)) {
491 $report_id = empty($row['procedure_report_id']) ?
0 : ($row['procedure_report_id'] +
0);
494 "ps.result_code, ps.result_text, ps.abnormal, ps.result, ps.range, " .
495 "ps.result_status, ps.facility, ps.units, ps.comments, ps.document_id, ps.date " .
496 "FROM procedure_result AS ps " .
497 "WHERE ps.procedure_report_id = ? " .
498 "ORDER BY ps.procedure_result_id";
500 $rres = sqlStatement($query, array($report_id));
503 // We are consolidating reports.
504 if (sqlNumRows($rres)) {
506 // First pass creates a $rrowsets[$key] for each unique result code in *this* report, with
507 // the value being an array of the corresponding result rows. This caters to multiple
508 // occurrences of the same result code in the same report.
509 while ($rrow = sqlFetchArray($rres)) {
510 $result_code = empty($rrow['result_code']) ?
'' : $rrow['result_code'];
511 $key = sprintf('%05d/', $row['procedure_order_seq']) . $result_code;
512 if (!isset($rrowsets[$key])) $rrowsets[$key] = array();
513 $rrowsets[$key][] = $rrow;
515 // Second pass builds onto the array of final results for *all* reports, where each final
516 // result for a given result code is its *array* of result rows from *one* of the reports.
517 foreach ($rrowsets as $key => $rrowset) {
518 // When two reports have the same date, use the result date to decide which is "latest".
519 if (isset($finals[$key]) &&
520 $row['date_report'] == $finals[$key][0]['date_report'] &&
521 !empty($rrow['date']) && !empty($finals[$key][1]['date']) &&
522 $rrow['date'] < $finals[$key][1]['date'])
524 $finals[$key][2] = true; // see comment below
527 // $finals[$key][2] indicates if there are multiple results for this result code.
528 $finals[$key] = array($row, $rrowset, isset($finals[$key]));
532 // We have no results for this report.
533 $key = sprintf('%05d/', $row['procedure_order_seq']);
534 $finals[$key] = array($row, array($empty_results), false);
538 // We are showing all results for all reports.
539 if (sqlNumRows($rres)) {
540 while ($rrow = sqlFetchArray($rres)) {
541 generate_result_row($ctx, $row, $rrow, false);
545 generate_result_row($ctx, $row, $empty_results, false);
551 // The sort here was removed because $finals is already ordered by procedure_result_id
552 // within procedure_order_seq which is probably desirable. Sorting by result code defeats
553 // the sequencing of results chosen by the sender.
555 foreach ($finals as $final) {
556 foreach ($final[1] as $rrow) {
557 generate_result_row($ctx, $final[0], $rrow, $final[2]);
567 <table width
='100%' style
='border-width:0px;'>
569 <td style
='border-width:0px;'>
571 if (!empty($aNotes)) {
572 echo "<table cellpadding='3' cellspacing='0'>\n";
573 echo " <tr bgcolor='#cccccc'>\n";
574 echo " <th align='center' colspan='2'>" . xlt('Notes') . "</th>\n";
576 foreach ($aNotes as $key => $value) {
578 echo " <td valign='top'>" . ($key +
1) . "</td>\n";
579 // <pre> tag because white space and a fixed font are often used to line things up.
580 echo " <td><pre style='white-space:pre-wrap;'>" . text($value) . "</pre></td>\n";
587 <td style
='border-width:0px;' align
='right' valign
='top'>
588 <?php
if ($input_form && !empty($ctx['priors_omitted']) /* empty($_POST['form_showall']) */ ) { ?
>
589 <input type
='submit' name
='form_showall' value
='<?php echo xla('Show All Results
'); ?>'
590 title
='<?php echo xla('Include all values reported
for each result code
'); ?>' />
591 <?php
} else if ($input_form && !empty($_POST['form_showall'])) { ?
>
592 <input type
='submit' name
='form_latest' value
='<?php echo xla('Latest Results Only
'); ?>'
593 title
='<?php echo xla('Show only latest values reported
for each result code
'); ?>' />
595 <?php
if (empty($GLOBALS['PATIENT_REPORT_ACTIVE'])) { ?
>
597 <input type
='button' value
='<?php echo xla('Related Patient Notes
'); ?>'
598 onclick
='showpnotes(<?php echo $orderid; ?>)' />
600 <?php
if ($input_form && $ctx['sign_list']) { ?
>
602 <input type
='hidden' name
='form_sign_list' value
='<?php echo attr($ctx['sign_list
']); ?>' />
603 <input type
='submit' name
='form_sign' value
='<?php echo xla('Sign Results
'); ?>'
604 title
='<?php echo xla('Mark these reports
as reviewed
'); ?>' />
606 // If this is a portal patient, sending them a copy is an option.
607 if ($GLOBALS['gbl_portal_cms_enable'] && $orow['cmsportal_login'] !== '') {
609 echo "<input type='checkbox' name='form_send_to_portal' value='" .
610 attr($orow['cmsportal_login']) . "' checked />\n";
611 echo xlt('Send to portal');
615 <?php
if ($input_form) { ?
>
617 <input type
='button' value
='<?php echo xla('Close
'); ?>' onclick
='window.close()' />
625 <?php
if ($input_form) { ?
>
627 <?php
} // end if input form ?>
630 } // end function generate_order_report