* @author Brady Miller * @author Ron Pulcer * @author Stephen Waite * @copyright Copyright (c) 2007-2016 Rod Roark * @copyright Copyright (c) 2018 Brady Miller * @copyright Copyright (c) 2019 Ron Pulcer * @copyright Copyright (c) 2019 Stephen Waite * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3 */ require_once("../globals.php"); require_once("$srcdir/appointments.inc.php"); require_once("$srcdir/patient.inc"); require_once("$srcdir/user.inc"); use OpenEMR\Core\Header; use OpenEMR\Services\FacilityService; $facilityService = new FacilityService(); function genColumn($ix) { global $html; global $SBCODES; for ($imax = count($SBCODES); $ix < $imax; ++$ix) { $a = explode('|', $SBCODES[$ix], 2); $cmd = trim($a[0]); if ($cmd == '*C') { // column break return++$ix; } if ($cmd == '*B') { // Borderless and empty $html .= "  \n"; } elseif ($cmd == '*G') { $title = text($a[1]); if (!$title) { $title = ' '; } $html .= " $title\n"; } elseif ($cmd == '*H') { $title = text($a[1]); if (!$title) { $title = ' '; } $html .= " $title\n"; } else { $title = text($a[1]); if (!$title) { $title = ' '; } $b = explode(':', $cmd); $html .= " \n"; $html .= "  \n"; if (count($b) <= 1) { $code = text($b[0]); if (!$code) { $code = ' '; } $html .= " $code\n"; $html .= " $title\n"; } else { $html .= " " . text($b[0]) . '/' . text($b[1]) . "\n"; $html .= " $title\n"; } $html .= " \n"; } } return $ix; } // MAIN Body // // Build output to handle multiple pids and and superbill for each patient. // This value is initially a maximum, and will be recomputed to // distribute lines evenly among the pages. (was 55) $lines_per_page = 55; $lines_in_stats = 8; $header_height = 44; // height of page headers in points // This tells us if patient/encounter data is to be filled in. // 1 = single PID from popup, 2=array of PIDs for session if (empty($_GET['fill'])) { $form_fill = 0; } else { $form_fill = $_GET['fill']; } // Show based on session array or single pid? $pid_list = array(); $apptdate_list = array(); if (!empty($_SESSION['pidList']) and $form_fill == 2) { $pid_list = $_SESSION['pidList']; // If PID list is in Session, then Appt. Date list is expected to be a parallel array $apptdate_list = $_SESSION['apptdateList']; } elseif ($form_fill == 1) { array_push($pid_list, $pid); //get from active PID } else { array_push($pid_list, ''); // empty element for blank form } // This file is optional. You can create it to customize how the printed // fee sheet looks, otherwise you'll get a mirror of your actual fee sheet. // if (file_exists("../../custom/fee_sheet_codes.php")) { include_once("../../custom/fee_sheet_codes.php"); } // TBD: Move these to globals.php, or make them user-specific. $fontsize = 7; $page_height = 700; $padding = 0; // The $SBCODES table is a simple indexed array whose values are // strings of the form "code|text" where code may be either a billing // code or one of the following: // // *H - A main heading, where "text" is its title (to be centered). // *G - Specifies a new category, where "text" is its name. // *B - A borderless blank row. // *C - Ends the current column and starts a new one. // If $SBCODES is not provided, then manufacture it from the Fee Sheet. // if (empty($SBCODES)) { $SBCODES = array(); $last_category = ''; // Create entries based on the fee_sheet_options table. $res = sqlStatement("SELECT * FROM fee_sheet_options " . "ORDER BY fs_category, fs_option"); while ($row = sqlFetchArray($res)) { $fs_category = $row['fs_category']; $fs_option = $row['fs_option']; $fs_codes = $row['fs_codes']; if ($fs_category !== $last_category) { $last_category = $fs_category; $SBCODES[] = '*G|' . substr($fs_category, 1); } $SBCODES[] = " |" . substr($fs_option, 1); } // Create entries based on categories defined within the codes. $pres = sqlStatement("SELECT option_id, title FROM list_options " . "WHERE list_id = 'superbill' AND activity = 1 ORDER BY seq"); while ($prow = sqlFetchArray($pres)) { $SBCODES[] = '*G|' . xl_list_label($prow['title']); $res = sqlStatement("SELECT code_type, code, code_text FROM codes " . "WHERE superbill = ? AND active = 1 " . "ORDER BY code_text", array($prow['option_id'])); while ($row = sqlFetchArray($res)) { $SBCODES[] = $row['code'] . '|' . $row['code_text']; } } // Create one more group, for Products. if ($GLOBALS['sell_non_drug_products']) { $SBCODES[] = '*G|' . xl('Products'); $tres = sqlStatement("SELECT " . "dt.drug_id, dt.selector, d.name, d.ndc_number " . "FROM drug_templates AS dt, drugs AS d WHERE " . "d.drug_id = dt.drug_id AND d.active = 1 " . "ORDER BY d.name, dt.selector, dt.drug_id"); while ($trow = sqlFetchArray($tres)) { $tmp = $trow['selector']; if ($trow['name'] !== $trow['selector']) { $tmp .= ' ' . $trow['name']; } $prodcode = empty($trow['ndc_number']) ? ('(' . $trow['drug_id'] . ')') : $trow['ndc_number']; $SBCODES[] = "$prodcode|$tmp"; } } // Extra stuff for the labs section. $SBCODES[] = '*G|' . xl('Notes'); $percol = intval((count($SBCODES) + 2) / 3); while (count($SBCODES) < $percol * 3) { $SBCODES[] = '*B|'; } // Adjust lines per page to distribute lines evenly among the pages. $pages = intval(($percol + $lines_in_stats + $lines_per_page - 1) / $lines_per_page); $lines_per_page = intval(($percol + $lines_in_stats + $pages - 1) / $pages); // Figure out page and column breaks. $pages = 1; $lines = $percol; $page_start_index = 0; while ($lines + $lines_in_stats > $lines_per_page) { ++$pages; $lines_this_page = $lines > $lines_per_page ? $lines_per_page : $lines; $lines -= $lines_this_page; array_splice($SBCODES, $lines_this_page * 3 + $page_start_index, 0, '*C|'); array_splice($SBCODES, $lines_this_page * 2 + $page_start_index, 0, '*C|'); array_splice($SBCODES, $lines_this_page * 1 + $page_start_index, 0, '*C|'); $page_start_index += $lines_this_page * 3 + 3; } array_splice($SBCODES, $lines * 2 + $page_start_index, 0, '*C|'); array_splice($SBCODES, $lines * 1 + $page_start_index, 0, '*C|'); } $lheight = sprintf('%d', ($page_height - $header_height) / $lines_per_page); // Common HTML Header information $html = " "; $html .= " "; $html .= "" . text($frow['name'] ?? '') . "" . Header::setupHeader(['opener', 'topdialog'], false) . "
"; $today = date('Y-m-d'); $alertmsg = ''; // anything here pops up in an alert box // Get details for the primary facility. $frow = $facilityService->getPrimaryBusinessEntity(); // If primary is not set try to old method of guessing...for backward compatibility if (empty($frow)) { $frow = $facilityService->getPrimaryBusinessEntity(array("useLegacyImplementation" => true)); } // Still missing... if (empty($frow)) { $alertmsg = xl("No Primary Business Entity selected in facility list"); } $logo = ''; $ma_logo_path = "sites/" . $_SESSION['site_id'] . "/images/ma_logo.png"; if (is_file("$webserver_root/$ma_logo_path")) { $logo = ""; } else { $logo = ""; } // Loop on array of PIDS $saved_pages = $pages; //Save calculated page count of a single fee sheet $loop_idx = 0; // counter for appt list foreach ($pid_list as $pid) { $apptdate = $apptdate_list[$loop_idx] ?? null; // parallel array to pid_list $appointment = fetchAppointments($apptdate, $apptdate, $pid); // Only expecting one row for pid // Set Pagebreak for multi forms if ($form_fill == 2) { $html .= "
\n"; } else { $html .= "
\n"; } if ($form_fill) { // Get the patient's name and chart number. $patdata = getPatientData($pid); // Get the referring providers info $referDoc = getUserIDInfo($patdata['ref_providerID']); } // This tracks our position in the $SBCODES array. $cindex = 0; while (--$pages >= 0) { $html .= genFacilityTitle(xl('Superbill/Fee Sheet'), -1, $logo); $html .= '' . '' . '' . '' . '' . '
' . xlt('Patient') . ': ' . text($patdata['fname'] ?? '') . ' ' . text($patdata['mname'] ?? '') . ' ' . text($patdata['lname'] ?? '') . '' . xlt('DOB') . ': ' . text(oeFormatShortDate($patdata['DOB'] ?? '')) . '' . xlt('Date of Service') . ': ' . text(oeFormatShortDate($appointment[0]['pc_eventDate'] ?? '')) . ' ' . text(oeFormatTime($appointment[0]['pc_startTime'] ?? '')) . '' . xlt('Ref Prov') . ': ' . text($referDoc['fname'] ?? '') . ' ' . text($referDoc['lname'] ?? '') . '
'; $html .= "
"; $cindex = genColumn($cindex); // Column 1 if ($pages == 0) { // if this is the last page $html .= ""; } // end if last page $html .= "
"; $html .= xlt('Patient') . ": "; if ($form_fill) { $html .= text($patdata['fname'] . ' ' . $patdata['mname'] . ' ' . $patdata['lname']) . "
\n"; $html .= text($patdata['street']) . "
\n"; $html .= text($patdata['city'] . ', ' . $patdata['state'] . ' ' . $patdata['postal_code']) . "\n"; } $html .= "
"; $html .= xlt('DOB'); $html .= ": "; if ($form_fill) { $html .= text($patdata['DOB']); $html .= "
"; } $html .= xlt('ID'); $html .= ": "; if ($form_fill) { $html .= text($patdata['pubpid']); } $html .= "
"; $html .= xlt('Provider'); $html .= ": "; $encdata = false; if ($form_fill && $encounter) { $query = "SELECT fe.reason, fe.date, u.fname, u.mname, u.lname, u.username " . "FROM forms AS f " . "JOIN form_encounter AS fe ON fe.id = f.form_id " . "LEFT JOIN users AS u ON u.username = f.user " . "WHERE f.pid = ? AND f.encounter = ? AND f.formdir = 'newpatient' AND f.deleted = 0 " . "ORDER BY f.id LIMIT 1"; $encdata = sqlQuery($query, array($pid, $encounter)); if (!empty($encdata['username'])) { $html .= $encdata['fname'] . ' ' . $encdata['mname'] . ' ' . $encdata['lname']; } } $html .= " "; $html .= xlt('Reason'); $html .= ":
"; if (!empty($encdata)) { $html .= text($encdata['reason']); } // Note: You would think that pc_comments would have the Appt. comments, // but it is actually stored in pc_hometext in DB table (openemr_postcalendar_events). $html .= $appointment['pc_hometext'] ?? ''; $html .= "
"; if (empty($GLOBALS['ippf_specific'])) { $html .= xlt('Insurance') . ":"; if ($form_fill) { foreach (array('primary', 'secondary', 'tertiary') as $instype) { $query = "SELECT * FROM insurance_data WHERE " . "pid = ? AND type = ? " . "ORDER BY date DESC LIMIT 1"; $row = sqlQuery($query, array($pid, $instype)); if (!empty($row['provider'])) { $icobj = new InsuranceCompany($row['provider']); $adobj = $icobj->get_address(); $insco_name = trim($icobj->get_name()); if ($instype != 'primary') { $html .= ","; } if ($insco_name) { $html .= " " . text($insco_name); } else { $html .= " Missing Name"; } } } } } else { // IPPF wants a visit date box with the current date in it. $html .= xlt('Visit date'); $html .= ":
\n"; if (!empty($encdata)) { $html .= text(substr($encdata['date'], 0, 10)); } else { $html .= text(oeFormatShortDate(date('Y-m-d'))) . "\n"; } } $html .= "
"; $html .= xlt('Prior Visit'); $html .= ":
"; $html .= xlt('Today\'s Charges'); $html .= ":
"; $html .= xlt('Today\'s Balance'); $html .= ":
"; $html .= xlt('Notes'); $html .= ":
"; $cindex = genColumn($cindex); // Column 2 if ($pages == 0) { // if this is the last page $html .= ""; } // end if last page $html .= "
"; $html .= xlt('Notes'); $html .= ":
"; $cindex = genColumn($cindex); // Column 3 if ($pages == 0) { // if this is the last page $html .= ""; } // end if last page $html .= "
 
"; $html .= xlt('Signature'); $html .= ":
"; $html .= "
"; // end of div.pageLetter } // end while $pages = $saved_pages; // reset $loop_idx++; // appt list counter } // end foreach // Common End Code if ($form_fill != 2) { //use native browser 'print' for multipage $html .= "