7 * @link http://www.open-emr.org
8 * @author Matthew Vita <matthewvita48@gmail.com>
9 * @author Brady Miller <brady.g.miller@gmail.com>
10 * @copyright Copyright (c) 2018 Matthew Vita <matthewvita48@gmail.com>
11 * @copyright Copyright (c) 2018 Brady Miller <brady.g.miller@gmail.com>
12 * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
15 namespace OpenEMR\Services
;
17 use MongoDB\Driver\Query
;
18 use OpenEMR\Common\Database\QueryUtils
;
19 use OpenEMR\Common\Uuid\UuidRegistry
;
20 use OpenEMR\Services\Search\DateSearchField
;
21 use OpenEMR\Services\Search\FhirSearchWhereClauseBuilder
;
22 use OpenEMR\Services\Search\TokenSearchField
;
23 use OpenEMR\Services\Search\TokenSearchValue
;
24 use OpenEMR\Validators\ProcessingResult
;
25 use Particle\Validator\Exception\InvalidValueException
;
26 use Particle\Validator\Validator
;
28 class AppointmentService
extends BaseService
30 const TABLE_NAME
= "openemr_postcalendar_events";
31 const PATIENT_TABLE
= "patient_data";
32 const PRACTITIONER_TABLE
= "users";
33 const FACILITY_TABLE
= "facility";
36 * @var EncounterService
38 private $encounterService;
43 private $patientService;
46 * Default constructor.
48 public function __construct()
50 parent
::__construct(self
::TABLE_NAME
);
51 UuidRegistry
::createMissingUuidsForTables([self
::TABLE_NAME
, self
::PATIENT_TABLE
, self
::PRACTITIONER_TABLE
,
52 self
::FACILITY_TABLE
]);
55 public function setEncounterService(EncounterService
$service)
57 $this->encounterService
= $service;
60 public function getEncounterService()
62 if (empty($this->encounterService
)) {
63 $this->encounterService
= new EncounterService();
65 return $this->encounterService
;
68 public function setPatientService(PatientService
$patientService)
70 $this->patientService
= $patientService;
73 public function getPatientService()
75 if (empty($this->patientService
)) {
76 $this->patientService
= new PatientService();
78 return $this->patientService
;
81 public function getUuidFields(): array
83 return ['puuid', 'pce_aid_uuid', 'pc_uuid'];
86 public function validate($appointment)
88 $validator = new Validator();
90 $validator->required('pc_catid')->numeric();
91 $validator->required('pc_title')->lengthBetween(2, 150);
92 $validator->required('pc_duration')->numeric();
93 $validator->required('pc_hometext')->string();
94 $validator->required('pc_apptstatus')->string();
95 $validator->required('pc_eventDate')->datetime('Y-m-d');
96 $validator->required('pc_startTime')->length(5); // HH:MM is 5 chars
97 $validator->required('pc_facility')->numeric();
98 $validator->required('pc_billing_location')->numeric();
99 $validator->optional('pc_aid')->numeric()
100 ->callback(function ($value, $data) {
101 $id = QueryUtils
::fetchSingleValue('Select id FROM users WHERE id = ? ', 'id', [$value]);
103 throw new InvalidValueException('pc_aid must be for a valid user', 'pc_aid');
107 $validator->optional('pid')->callback(function ($value, $data) {
108 $id = QueryUtils
::fetchSingleValue('Select id FROM patient_data WHERE pid = ? ', 'id', [$value]);
110 throw new InvalidValueException('pid must be for a valid patient', 'pid');
115 return $validator->validate($appointment);
118 public function search($search, $isAndCondition = true)
120 $sql = "SELECT pce.pc_eid,
127 providers.uuid AS pce_aid_uuid,
128 providers.npi AS pce_aid_npi,
135 pce.pc_billing_location,
139 f1.name as facility_name,
140 f2.name as billing_location_name
144 uuid AS pc_uuid, -- we do this because our uuid registry requires the field to be named this way
156 openemr_postcalendar_events
158 LEFT JOIN facility as f1 ON pce.pc_facility = f1.id
159 LEFT JOIN facility as f2 ON pce.pc_billing_location = f2.id
168 ) pd ON pd.pid = pce.pc_pid
169 LEFT JOIN users as providers ON pce.pc_aid = providers.id";
171 $whereClause = FhirSearchWhereClauseBuilder
::build($search, $isAndCondition);
173 $sql .= $whereClause->getFragment();
174 $sqlBindArray = $whereClause->getBoundValues();
175 $statementResults = QueryUtils
::sqlStatementThrowException($sql, $sqlBindArray);
177 $processingResult = new ProcessingResult();
178 while ($row = sqlFetchArray($statementResults)) {
179 $processingResult->addData($this->createResultRecordFromDatabaseResult($row));
182 return $processingResult;
185 public function getAppointmentsForPatient($pid)
187 $sqlBindArray = array();
189 $sql = "SELECT pce.pc_eid,
196 providers.uuid AS pce_aid_uuid,
197 providers.npi AS pce_aid_npi,
204 pce.pc_billing_location,
207 f1.name as facility_name,
208 f2.name as billing_location_name
209 FROM openemr_postcalendar_events as pce
210 LEFT JOIN facility as f1 ON pce.pc_facility = f1.id
211 LEFT JOIN facility as f2 ON pce.pc_billing_location = f2.id
212 LEFT JOIN patient_data as pd ON pd.pid = pce.pc_pid
213 LEFT JOIN users as providers ON pce.pc_aid = providers.id";
216 $sql .= " WHERE pd.pid = ?";
217 array_push($sqlBindArray, $pid);
220 $records = QueryUtils
::fetchRecords($sql, $sqlBindArray);
222 if (!empty($records)) {
223 foreach ($records as $record) {
224 $finalRecords[] = $this->createResultRecordFromDatabaseResult($record);
227 return $finalRecords;
230 public function getAppointment($eid)
232 $sql = "SELECT pce.pc_eid,
239 providers.uuid AS pce_aid_uuid,
240 providers.npi AS pce_aid_npi,
248 pce.pc_billing_location,
253 f1.name as facility_name,
254 f2.name as billing_location_name
255 FROM openemr_postcalendar_events as pce
256 LEFT JOIN facility as f1 ON pce.pc_facility = f1.id
257 LEFT JOIN facility as f2 ON pce.pc_billing_location = f2.id
258 LEFT JOIN patient_data as pd ON pd.pid = pce.pc_pid
259 LEFT JOIN users as providers ON pce.pc_aid = providers.id
260 WHERE pce.pc_eid = ?";
262 $records = QueryUtils
::fetchRecords($sql, [$eid]);
264 if (!empty($records)) {
265 foreach ($records as $record) {
266 $finalRecords[] = $this->createResultRecordFromDatabaseResult($record);
269 return $finalRecords;
272 public function insert($pid, $data)
274 $startTime = date("H:i:s", strtotime($data['pc_startTime']));
275 // TODO: Why are we adding strings with numbers? How is this even working
276 $endTime = $startTime . $data['pc_duration'];
277 $uuid = (new UuidRegistry())->createUuid();
279 $sql = " INSERT INTO openemr_postcalendar_events SET";
281 $sql .= " pc_pid=?,";
282 $sql .= " pc_catid=?,";
283 $sql .= " pc_title=?,";
284 $sql .= " pc_duration=?,";
285 $sql .= " pc_hometext=?,";
286 $sql .= " pc_eventDate=?,";
287 $sql .= " pc_apptstatus=?,";
288 $sql .= " pc_startTime=?,";
289 $sql .= " pc_endTime=?,";
290 $sql .= " pc_facility=?,";
291 $sql .= " pc_billing_location=?,";
292 $sql .= " pc_informant=1,";
293 $sql .= " pc_eventstatus=1,";
294 $sql .= " pc_sharing=1,";
297 $results = sqlInsert(
304 $data["pc_duration"],
305 $data["pc_hometext"],
306 $data["pc_eventDate"],
307 $data['pc_apptstatus'],
310 $data["pc_facility"],
311 $data["pc_billing_location"],
312 $data["pc_aid"] ??
null
319 public function delete($eid)
321 QueryUtils
::sqlStatementThrowException("DELETE FROM openemr_postcalendar_events WHERE pc_eid = ?", $eid);
322 return ['message' => 'record deleted'];
326 * Returns a list of categories
329 public function getCalendarCategories()
331 $sql = "SELECT pc_catid, pc_constant_id, pc_catname, pc_cattype,aco_spec FROM openemr_postcalendar_categories "
332 . " WHERE pc_active = 1 ORDER BY pc_seq";
333 return QueryUtils
::fetchRecords($sql);
337 * check to see if a status code exist as a check in
341 public function isCheckInStatus($option)
343 $row = sqlQuery("SELECT toggle_setting_1 FROM list_options WHERE " .
344 "list_id = 'apptstat' AND option_id = ? AND activity = 1", array($option));
345 if (empty($row['toggle_setting_1'])) {
353 * check to see if a status code exist as a check out
357 public function isCheckOutStatus($option)
359 $row = sqlQuery("SELECT toggle_setting_2 FROM list_options WHERE " .
360 "list_id = 'apptstat' AND option_id = ? AND activity = 1", array($option));
361 if (empty($row['toggle_setting_2'])) {
368 public function isPendingStatus($option)
370 // TODO: @adunsulag is there ANY way to track this in the database of what statii are pending?
371 if ($option == '^') {
378 * Returns a list of appointment statuses (also used with encounters).
381 public function getAppointmentStatuses()
383 $listService = new ListService();
384 $options = $listService->getOptionsByListName('apptstat', ['activity' => 1]);
389 * Checks to see if the passed in status is a valid appointment status for calendar appointments.
390 * @param $status_option_id The status to check if its a valid appointment status
391 * @return bool True if its valid, false otherwise
393 public function isValidAppointmentStatus($status_option_id)
395 $listService = new ListService();
396 $option = $listService->getListOption('apptstat', $status_option_id);
397 if (!empty($option)) {
404 * Updates the status for an appointment. TODO: should be refactored at some point to update the entire record
405 * @param $eid number The id of the appointment event
406 * @param $status string The status the appointment event should be set to.
407 * @param $user number The user performing the update
408 * @param $encounter number The encounter of the appointment
410 public function updateAppointmentStatus($eid, $status, $user, $encounter = '')
412 $appt = $this->getAppointment($eid);
414 throw new \
InvalidArgumentException("Appointment does not exist for eid " . $eid);
416 // TODO: Not sure why getAppointment returns an array of records instead of a single record
420 $sql = "UPDATE " . self
::TABLE_NAME
. " SET pc_apptstatus = ? WHERE pc_eid = ? ";
421 $binds = [$status, $eid];
423 if (!empty($appt['pid'])) {
424 $trackerService = new PatientTrackerService();
425 $trackerService->manage_tracker_status($appt['pc_eventDate'], $appt['pc_startTime'], $eid, $appt['pid'], $user, $status, $appt['pc_room'], $encounter);
427 $this->getLogger()->error("AppointmentService->updateAppointmentStatus() failed to update manage_tracker_status"
428 . " as patient pid was empty", ['pc_eid' => $eid, 'status' => $status, 'user' => $user, 'encounter' => $encounter]);
430 return QueryUtils
::sqlStatementThrowException($sql, $binds);
436 * @return array The most recent encounter for a given appointment
438 public function getEncounterForAppointment($pc_eid, $pid)
440 $appointment = $this->getAppointment($pc_eid)[0];
441 $date = $appointment['pc_eventDate'];
442 // we grab the most recent encounter for today's date for the given patient
443 $encounterService = $this->getEncounterService();
444 $dateField = new DateSearchField('date', ['eq' . $date], DateSearchField
::DATE_TYPE_DATE
);
445 $pidField = new TokenSearchField('pid', [new TokenSearchValue($pid)]);
446 // returns the most recent encounter for the given appointment..
447 // TODO: @adunsulag we should look at in the future of making an actual join table between encounters and appointments...
448 // this fuzzy match by date seems like it will have major problems for both inpatient settings as well as any kind
449 // of emergency care (patient sees doctor, patient does telehealth visit during the night due to crisis situation).
450 $encounterResult = $encounterService->search(['date' => $dateField, 'pid' => $pidField], true, null, ['limit' => 1]);
451 if ($encounterResult->hasData()) {
452 $result = $encounterResult->getData();
453 return array_pop($result);
458 public function createEncounterForAppointment($eid)
460 $appointment = $this->getAppointment($eid)[0];
461 $patientService = $this->getPatientService();
462 $patientUuid = UuidRegistry
::uuidToString($patientService->getUuid($appointment['pid']));
464 $userService = new UserService();
465 $user = $userService->getUser($appointment['pc_aid']);
466 $authGroup = UserService
::getAuthGroupForUser($user['username']);
468 $pos_code = QueryUtils
::fetchSingleValue(
469 "SELECT pos_code FROM facility WHERE id = ?",
471 [$appointment['pc_facility']]
475 'pc_catid' => $appointment['pc_catid']
476 // TODO: where would we get this information if it wasn't defaulted to ambulatory? Should this be a globals setting?
477 // this is imitating the work from encounter_events.inc.php::todaysEncounterCheck
478 ,'class_code' => EncounterService
::DEFAULT_CLASS_CODE
479 ,'puuid' => $patientUuid
480 ,'pid' => $appointment['pid']
481 ,'provider_id' => $user['id']
482 ,'reason' => $appointment['pc_hometext'] ??
xl('Please indicate visit reason')
483 ,'facility_id' => $appointment['pc_facility']
484 ,'billing_facility' => $appointment['pc_billing_location']
485 ,'pos_code' => $pos_code
486 ,'user' => $user['username']
487 ,'group' => $authGroup
490 $encounterService = $this->getEncounterService();
491 $result = $encounterService->insertEncounter($patientUuid, $data);
492 if ($result->hasData()) {
493 $result = $result->getData();
494 return $result[0]['encounter'];
500 * Returns the calendar category record from a supplied category id
503 public function getOneCalendarCategory($cat_id)
505 $sql = "SELECT * FROM openemr_postcalendar_categories WHERE pc_catid = ?";
506 return QueryUtils
::fetchRecords($sql, [$cat_id]);