7 * @link http://www.open-emr.org
8 * @author Matthew Vita <matthewvita48@gmail.com>
9 * @author Jerry Padgett <sjpadgett@gmail.com>
10 * @author Brady Miller <brady.g.miller@gmail.com>
11 * @copyright Copyright (c) 2018 Matthew Vita <matthewvita48@gmail.com>
12 * @copyright Copyright (c) 2018 Jerry Padgett <sjpadgett@gmail.com>
13 * @copyright Copyright (c) 2018 Brady Miller <brady.g.miller@gmail.com>
14 * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
17 namespace OpenEMR\Services
;
19 use OpenEMR\Common\Acl\AclMain
;
20 use OpenEMR\Common\Database\QueryUtils
;
21 use OpenEMR\Common\Database\SqlQueryException
;
22 use OpenEMR\Common\Logging\SystemLogger
;
23 use OpenEMR\Common\Uuid\UuidRegistry
;
24 use OpenEMR\Services\Search\
{
26 FhirSearchWhereClauseBuilder
,
31 use OpenEMR\Validators\EncounterValidator
;
32 use OpenEMR\Validators\ProcessingResult
;
33 use Particle\Validator\Validator
;
35 require_once dirname(__FILE__
) . "/../../library/forms.inc.php";
36 require_once dirname(__FILE__
) . "/../../library/encounter.inc.php";
38 class EncounterService
extends BaseService
41 * @var EncounterValidator
43 private $encounterValidator;
45 public const ENCOUNTER_TABLE
= "form_encounter";
46 private const PATIENT_TABLE
= "patient_data";
47 private const PROVIDER_TABLE
= "users";
48 private const FACILITY_TABLE
= "facility";
51 * Default class_code from list_options. Defaults to outpatient ambulatory care.
53 const DEFAULT_CLASS_CODE
= 'AMB';
56 * Default constructor.
58 public function __construct()
60 parent
::__construct('form_encounter');
61 UuidRegistry
::createMissingUuidsForTables([self
::ENCOUNTER_TABLE
, self
::PATIENT_TABLE
, self
::PROVIDER_TABLE
,
62 self
::FACILITY_TABLE
]);
63 $this->encounterValidator
= new EncounterValidator();
67 * Returns a list of encounters matching the encounter identifier.
69 * @param $eid The encounter identifier of particular encounter
70 * @param $puuidBind - Optional variable to only allow visibility of the patient with this puuid.
71 * @return ProcessingResult which contains validation messages, internal error messages, and the data
74 public function getEncounterById($eid, $puuidBind = null)
76 $search = ['eid' => new TokenSearchField('eid', [new TokenSearchValue($eid)])];
77 return $this->search($search, true, $puuidBind);
81 * Returns a list of encounters matching the encounter identifier.
83 * @param $euuid The encounter identifier of particular encounter
84 * @param $puuidBind - Optional variable to only allow visibility of the patient with this puuid.
85 * @return ProcessingResult which contains validation messages, internal error messages, and the data
88 public function getEncounter($euuid, $puuidBind = null)
90 $search = ['euuid' => new TokenSearchField('euuid', [new TokenSearchValue($euuid, null, true)])];
91 return $this->search($search, true, $puuidBind);
95 * Returns an encounter matching the patient and encounter identifier.
97 * @param $pid The legacy identifier of particular patient
98 * @param $encounter_id The identifier of a particular encounter
99 * @return array first row of encounter data
101 public function getOneByPidEid($pid, $encounter_id)
103 $encounterResult = $this->search(['pid' => $pid, 'eid' => $encounter_id], $options = ['limit' => '1']);
104 if ($encounterResult->hasData()) {
105 return $encounterResult->getData()[0];
110 public function getUuidFields(): array
112 return ['provider_uuid', 'facility_uuid', 'euuid', 'puuid', 'billing_facility_uuid'
113 , 'facility_location_uuid', 'billing_location_uuid', 'referrer_uuid'];
117 * Given a patient pid return the most recent patient encounter for that patient
118 * @param $pid The unique public id (pid) for the patient.
119 * @return array|null Returns the encounter if found, null otherwise
121 public function getMostRecentEncounterForPatient($pid): ?
array
123 $pid = new TokenSearchField('pid', [new TokenSearchValue($pid, null)]);
124 // we discovered that most queries were ordering by encounter id which may NOT be the most recent encounter as
125 // an older historical encounter may be entered after a more recent encounter, so ordering be encounter id screws
127 $result = $this->search(['pid' => $pid], true, '', ['limit' => 1, 'order' => '`date` DESC']);
128 if ($result->hasData()) {
129 return array_pop($result->getData());
135 * Returns a list of encounters matching optional search criteria.
136 * Search criteria is conveyed by array where key = field/column name, value = field value.
137 * If no search criteria is provided, all records are returned.
139 * @param array $search search array parameters
140 * @param bool $isAndCondition specifies if AND condition is used for multiple criteria. Defaults to true.
141 * @param string $puuidBindValue - Optional puuid to only allow visibility of the patient with this puuid.
142 * @param array $options - Optional array of sql clauses like LIMIT, ORDER, etc
143 * @return bool|ProcessingResult|true|null ProcessingResult which contains validation messages, internal error messages, and the data
146 public function search($search = array(), $isAndCondition = true, $puuidBindValue = '', $options = array())
148 $limit = $options['limit'] ??
null;
149 $sqlBindArray = array();
150 $processingResult = new ProcessingResult();
152 // Validating and Converting _id to UUID byte
153 if (isset($search['uuid'])) {
154 $isValidEncounter = $this->encounterValidator
->validateId(
156 self
::ENCOUNTER_TABLE
,
160 if ($isValidEncounter !== true) {
161 return $isValidEncounter;
163 $search['uuid'] = UuidRegistry
::uuidToBytes($search['uuid']);
165 // passed in uuid string to bind patient via their uuid.
167 if (!empty($puuidBindValue)) {
168 // code to support patient binding
169 $isValidPatient = $this->encounterValidator
->validateId('uuid', self
::PATIENT_TABLE
, $puuidBindValue, true);
170 if ($isValidPatient !== true) {
171 return $isValidPatient;
173 $pid = $this->getIdByUuid(UuidRegistry
::uuidToBytes($puuidBindValue), self
::PATIENT_TABLE
, "pid");
175 $processingResult->setValidationMessages("Invalid pid");
176 return $processingResult;
178 $search['puuid'] = new TokenSearchField('puuid', [new TokenSearchValue($puuidBindValue, null, true)]);
181 $sql = "SELECT fe.eid,
189 fe.last_level_billed,
190 fe.last_level_closed,
200 class.notes as class_title,
205 facilities.facility_id,
206 facilities.facility_uuid,
207 facilities.facility_name,
208 facilities.facility_location_uuid,
210 fa.billing_facility_id,
211 fa.billing_facility_uuid,
212 fa.billing_facility_name,
213 fa.billing_location_uuid,
216 fe.referring_provider_id,
217 providers.provider_uuid,
218 providers.provider_username,
219 referrers.referrer_uuid,
220 referrers.referrer_username,
221 fe.discharge_disposition,
222 discharge_list.discharge_disposition_text
248 discharge_disposition,
249 pid as encounter_pid,
250 referring_provider_id
253 LEFT JOIN openemr_postcalendar_categories as opc
254 ON opc.pc_catid = fe.pc_catid
255 LEFT JOIN list_options as class ON class.option_id = fe.class_code
258 facility.id AS billing_facility_id
259 ,facility.uuid AS billing_facility_uuid
260 ,facility.`name` AS billing_facility_name
261 ,locations.uuid AS billing_location_uuid
263 LEFT JOIN uuid_mapping AS locations
264 ON locations.target_uuid = facility.uuid AND locations.resource='Location'
265 ) fa ON fa.billing_facility_id = fe.billing_facility
271 ) patient ON fe.encounter_pid = patient.pid
274 id AS provider_provider_id
275 ,uuid AS provider_uuid
276 ,`username` AS provider_username
279 npi IS NOT NULL and npi != ''
280 ) providers ON fe.provider_id = providers.provider_provider_id
283 id AS referring_provider_id
284 ,uuid AS referrer_uuid
285 ,`username` AS referrer_username
288 npi IS NOT NULL and npi != ''
289 ) referrers ON fe.referring_provider_id = referrers.referring_provider_id
292 facility.id AS facility_id
293 ,facility.uuid AS facility_uuid
294 ,facility.`name` AS facility_name
295 ,`locations`.`uuid` AS facility_location_uuid
297 LEFT JOIN uuid_mapping AS locations
298 ON locations.target_uuid = facility.uuid AND locations.resource='Location'
299 ) facilities ON facilities.facility_id = fe.facility_id
301 select option_id AS discharge_option_id
302 ,title AS discharge_disposition_text
304 WHERE list_id = 'discharge-disposition'
305 ) discharge_list ON fe.discharge_disposition = discharge_list.discharge_option_id";
308 $whereFragment = FhirSearchWhereClauseBuilder
::build($search, $isAndCondition);
309 $sql .= $whereFragment->getFragment();
311 if (empty($options['order'])) {
312 $sql .= " ORDER BY fe.eid DESC";
314 $sql .= " ORDER BY " . $options['order'];
318 if (is_int($limit) && $limit > 0) {
319 $sql .= " LIMIT " . $limit;
322 $records = QueryUtils
::fetchRecords($sql, $whereFragment->getBoundValues());
324 if (!empty($records)) {
325 foreach ($records as $row) {
326 $resultRecord = $this->createResultRecordFromDatabaseResult($row);
327 $processingResult->addData($resultRecord);
330 } catch (SqlQueryException
$exception) {
331 // we shouldn't hit a query exception
332 (new SystemLogger())->error($exception->getMessage(), ['trace' => $exception->getTraceAsString()]);
333 $processingResult->addInternalError("Error selecting data from database");
334 } catch (SearchFieldException
$exception) {
335 (new SystemLogger())->error($exception->getMessage(), ['trace' => $exception->getTraceAsString(), 'field' => $exception->getField()]);
336 $processingResult->setValidationMessages([$exception->getField() => $exception->getMessage()]);
339 return $processingResult;
343 * Inserts a new Encounter record.
345 * @param $puuid The patient identifier of particular encounter
346 * @param $data The encounter fields (array) to insert.
347 * @return ProcessingResult which contains validation messages, internal error messages, and the data
350 public function insertEncounter($puuid, $data)
352 $processingResult = new ProcessingResult();
353 $processingResult = $this->encounterValidator
->validate(
354 array_merge($data, ["puuid" => $puuid]),
355 EncounterValidator
::DATABASE_INSERT_CONTEXT
358 if (!$processingResult->isValid()) {
359 return $processingResult;
362 $encounter = generate_id();
363 $data['encounter'] = $encounter;
364 $data['uuid'] = UuidRegistry
::getRegistryForTable(self
::ENCOUNTER_TABLE
)->createUuid();
365 if (empty($data['date'])) {
366 $data['date'] = date("Y-m-d");
368 $puuidBytes = UuidRegistry
::uuidToBytes($puuid);
369 $data['pid'] = $this->getIdByUuid($puuidBytes, self
::PATIENT_TABLE
, "pid");
370 $query = $this->buildInsertColumns($data);
371 $sql = " INSERT INTO form_encounter SET ";
372 $sql .= $query['set'];
374 $results = sqlInsert(
381 "New Patient Encounter",
385 $data["provider_id"],
389 $data['referring_provider_id']
393 $processingResult->addData(array(
394 'encounter' => $encounter,
395 'uuid' => UuidRegistry
::uuidToString($data['uuid']),
398 $processingResult->addProcessingError("error processing SQL Insert");
401 return $processingResult;
405 * Updates an existing Encounter record.
407 * @param $puuid The patient identifier of particular encounter.
408 * @param $euuid - The Encounter identifier used for update.
409 * @param $data - The updated Encounter data fields
410 * @return ProcessingResult which contains validation messages, internal error messages, and the data
413 public function updateEncounter($puuid, $euuid, $data)
415 $processingResult = new ProcessingResult();
416 $processingResult = $this->encounterValidator
->validate(
417 array_merge($data, ["puuid" => $puuid, "euuid" => $euuid]),
418 EncounterValidator
::DATABASE_UPDATE_CONTEXT
421 if (!$processingResult->isValid()) {
422 return $processingResult;
425 $puuidBytes = UuidRegistry
::uuidToBytes($puuid);
426 $euuidBytes = UuidRegistry
::uuidToBytes($euuid);
427 $pid = $this->getIdByUuid($puuidBytes, self
::PATIENT_TABLE
, "pid");
428 $encounter = $this->getIdByUuid($euuidBytes, self
::ENCOUNTER_TABLE
, "encounter");
430 $facilityService = new FacilityService();
431 $facilityresult = $facilityService->getById($data["facility_id"]);
432 $facility = $facilityresult['name'];
433 $result = sqlQuery("SELECT sensitivity FROM form_encounter WHERE encounter = ?", array($encounter));
434 if ($result['sensitivity'] && !AclMain
::aclCheckCore('sensitivities', $result['sensitivity'])) {
435 return "You are not authorized to see this encounter.";
438 // See view.php to allow or disallow updates of the encounter date.
439 if (!AclMain
::aclCheckCore('encounters', 'date_a')) {
440 unset($data["date"]);
443 $data['facility'] = $facility;
445 $query = $this->buildUpdateColumns($data);
446 $sql = " UPDATE form_encounter SET ";
447 $sql .= $query['set'];
448 $sql .= " WHERE encounter = ?";
449 $sql .= " AND pid = ?";
451 array_push($query['bind'], $encounter);
452 array_push($query['bind'], $pid);
453 $results = sqlStatement(
459 $processingResult = $this->getEncounter($euuid, $puuid);
461 $processingResult->addProcessingError("error processing SQL Update");
464 return $processingResult;
467 public function insertSoapNote($pid, $eid, $data)
469 $soapSql = " INSERT INTO form_soap SET";
470 $soapSql .= " date=NOW(),";
471 $soapSql .= " activity=1,";
472 $soapSql .= " pid=?,";
473 $soapSql .= " subjective=?,";
474 $soapSql .= " objective=?,";
475 $soapSql .= " assessment=?,";
476 $soapSql .= " plan=?";
478 $soapResults = sqlInsert(
493 $formSql = "INSERT INTO forms SET";
494 $formSql .= " date=NOW(),";
495 $formSql .= " encounter=?,";
496 $formSql .= " form_name='SOAP',";
497 $formSql .= " authorized='1',";
498 $formSql .= " form_id=?,";
499 $formSql .= " pid=?,";
500 $formSql .= " formdir='soap'";
502 $formResults = sqlInsert(
511 return array($soapResults, $formResults);
514 public function updateSoapNote($pid, $eid, $sid, $data)
516 $sql = " UPDATE form_soap SET";
517 $sql .= " date=NOW(),";
518 $sql .= " activity=1,";
520 $sql .= " subjective=?,";
521 $sql .= " objective=?,";
522 $sql .= " assessment=?,";
524 $sql .= " where id=?";
539 public function updateVital($pid, $eid, $vid, $data)
541 $data['date'] = date("Y-m-d H:i:s");
542 $data['activity'] = 1;
547 $vitalsService = new VitalsService();
548 $updatedRecords = $vitalsService->save($data);
549 return $updatedRecords;
552 public function insertVital($pid, $eid, $data)
555 $data['authorized'] = '1';
557 $vitalsService = new VitalsService();
558 $savedVitals = $vitalsService->save($data);
560 // need to grab the form record here, not sure why people need this but sure, why not, since the old method returned
561 // it we will keep the functionality.
562 $vitalsFormId = $savedVitals['id'];
563 $formId = intval(QueryUtils
::fetchSingleValue('select id FROM forms WHERE form_id = ? ', 'id', [$vitalsFormId]));
564 return [$vitalsFormId, $formId];
567 public function getVitals($pid, $eid)
569 $vitalsService = new VitalsService();
570 $vitals = $vitalsService->getVitalsForPatientEncounter($pid, $eid) ??
[];
574 public function getVital($pid, $eid, $vid)
576 $vitalsService = new VitalsService();
577 $vitals = $vitalsService->getVitalsForForm($vid);
578 if (!empty($vitals) && $vitals['eid'] == $eid && $vitals['pid'] == $pid) {
584 public function getSoapNotes($pid, $eid)
586 $sql = " SELECT fs.*";
587 $sql .= " FROM forms fo";
588 $sql .= " JOIN form_soap fs on fs.id = fo.form_id";
589 $sql .= " WHERE fo.encounter = ?";
590 $sql .= " AND fs.pid = ?";
592 $statementResults = sqlStatement($sql, array($eid, $pid));
595 while ($row = sqlFetchArray($statementResults)) {
596 array_push($results, $row);
602 public function getSoapNote($pid, $eid, $sid)
604 $sql = " SELECT fs.*";
605 $sql .= " FROM forms fo";
606 $sql .= " JOIN form_soap fs on fs.id = fo.form_id";
607 $sql .= " WHERE fo.encounter = ?";
608 $sql .= " AND fs.id = ?";
609 $sql .= " AND fs.pid = ?";
611 return sqlQuery($sql, array($eid, $sid, $pid));
614 public function validateSoapNote($soapNote)
616 $validator = new Validator();
618 $validator->optional('subjective')->lengthBetween(2, 65535);
619 $validator->optional('objective')->lengthBetween(2, 65535);
620 $validator->optional('assessment')->lengthBetween(2, 65535);
621 $validator->optional('plan')->lengthBetween(2, 65535);
623 return $validator->validate($soapNote);
626 public function validateVital($vital)
628 $validator = new Validator();
630 $validator->optional('temp_method')->lengthBetween(1, 255);
631 $validator->optional('note')->lengthBetween(1, 255);
632 $validator->optional('BMI_status')->lengthBetween(1, 255);
633 $validator->optional('bps')->numeric();
634 $validator->optional('bpd')->numeric();
635 $validator->optional('weight')->numeric();
636 $validator->optional('height')->numeric();
637 $validator->optional('temperature')->numeric();
638 $validator->optional('pulse')->numeric();
639 $validator->optional('respiration')->numeric();
640 $validator->optional('BMI')->numeric();
641 $validator->optional('waist_circ')->numeric();
642 $validator->optional('head_circ')->numeric();
643 $validator->optional('oxygen_saturation')->numeric();
645 return $validator->validate($vital);
648 public function getEncountersForPatientByPid($pid)
650 $encounterResult = $this->search(['pid' => $pid]);
651 if ($encounterResult->hasData()) {
652 return $encounterResult->getData();
658 * The result of this function returns the format needed by the frontend with the window.left_nav.setPatientEncounter function
662 public function getPatientEncounterListWithCategories($pid)
664 $encounters = $this->getEncountersForPatientByPid($pid);
671 foreach ($encounters as $index => $encounter) {
672 $encounterList['ids'][$index] = $encounter['eid'];
673 $encounterList['dates'][$index] = date("Y-m-d", strtotime($encounter['date']));
674 $encounterList['categories'][$index] = $encounter['pc_catname'];
676 return $encounterList;
680 * Returns the sensitivity level for the encounter matching the patient and encounter identifier.
682 * @param $pid The legacy identifier of particular patient
683 * @param $encounter_id The identifier of a particular encounter
684 * @return string sensitivity_level of first row of encounter data
686 public function getSensitivity($pid, $encounter_id)
688 $encounterResult = $this->search(['pid' => $pid, 'eid' => $encounter_id], $options = ['limit' => '1']);
689 if ($encounterResult->hasData()) {
690 return $encounterResult->getData()[0]['sensitivity'];
696 * Returns the referring provider for the encounter matching the patient and encounter identifier.
698 * @param $pid The legacy identifier of particular patient
699 * @param $encounter_id The identifier of a particular encounter
700 * @return string referring provider of first row of encounter data (it's an id from the users table)
702 public function getReferringProviderID($pid, $encounter_id)
704 $encounterResult = $this->search(['pid' => $pid, 'eid' => $encounter_id], $options = ['limit' => '1']);
705 if ($encounterResult->hasData()) {
706 return $encounterResult->getData()[0]['referring_provider_id'] ??
'';
712 * Return an array of encounters within a date range
714 * @param $start_date Any encounter starting on this date
715 * @param $end_date Any encounter ending on this date
716 * @return Array Encounter data payload.
718 public function getEncountersByDateRange($startDate, $endDate)
720 $dateField = new DateSearchField('date', ['ge' . $startDate, 'le' . $endDate], DateSearchField
::DATE_TYPE_DATE
, $isAnd = true);
721 $encounterResult = $this->search(['date' => $dateField]);
722 if ($encounterResult->hasData()) {
723 $result = $encounterResult->getData();
730 * Returns the default POS code that is set in the facility table.
732 * @param $facility_id
735 public function getPosCode($facility_id)
737 $sql = "SELECT pos_code FROM facility WHERE id = ?";
738 $result = sqlQuery($sql, [$facility_id]);
739 return $result['pos_code'];