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\Events\Services\ServiceDeleteEvent
;
21 use OpenEMR\Services\Search\DateSearchField
;
22 use OpenEMR\Services\Search\FhirSearchWhereClauseBuilder
;
23 use OpenEMR\Services\Search\TokenSearchField
;
24 use OpenEMR\Services\Search\TokenSearchValue
;
25 use OpenEMR\Validators\ProcessingResult
;
26 use Particle\Validator\Exception\InvalidValueException
;
27 use Particle\Validator\Validator
;
29 class AppointmentService
extends BaseService
31 const TABLE_NAME
= "openemr_postcalendar_events";
32 const PATIENT_TABLE
= "patient_data";
33 const PRACTITIONER_TABLE
= "users";
34 const FACILITY_TABLE
= "facility";
36 const CATEGORY_CONSTANT_NO_SHOW
= 'no_show';
39 * @var EncounterService
41 private $encounterService;
46 private $patientService;
49 * Default constructor.
51 public function __construct()
53 parent
::__construct(self
::TABLE_NAME
);
54 UuidRegistry
::createMissingUuidsForTables([self
::TABLE_NAME
, self
::PATIENT_TABLE
, self
::PRACTITIONER_TABLE
,
55 self
::FACILITY_TABLE
]);
58 public function setEncounterService(EncounterService
$service)
60 $this->encounterService
= $service;
63 public function getEncounterService()
65 if (empty($this->encounterService
)) {
66 $this->encounterService
= new EncounterService();
68 return $this->encounterService
;
71 public function setPatientService(PatientService
$patientService)
73 $this->patientService
= $patientService;
76 public function getPatientService()
78 if (empty($this->patientService
)) {
79 $this->patientService
= new PatientService();
81 return $this->patientService
;
84 public function getUuidFields(): array
86 return ['puuid', 'pce_aid_uuid', 'pc_uuid', 'facility_uuid', 'billing_location_uuid' ];
89 public function validate($appointment)
91 $validator = new Validator();
93 $validator->required('pc_catid')->numeric();
94 $validator->required('pc_title')->lengthBetween(2, 150);
95 $validator->required('pc_duration')->numeric();
96 $validator->required('pc_hometext')->string();
97 $validator->required('pc_apptstatus')->string();
98 $validator->required('pc_eventDate')->datetime('Y-m-d');
99 $validator->required('pc_startTime')->length(5); // HH:MM is 5 chars
100 $validator->required('pc_facility')->numeric();
101 $validator->required('pc_billing_location')->numeric();
102 $validator->optional('pc_aid')->numeric()
103 ->callback(function ($value, $data) {
104 $id = QueryUtils
::fetchSingleValue('Select id FROM users WHERE id = ? ', 'id', [$value]);
106 throw new InvalidValueException('pc_aid must be for a valid user', 'pc_aid');
110 $validator->optional('pid')->callback(function ($value, $data) {
111 $id = QueryUtils
::fetchSingleValue('Select id FROM patient_data WHERE pid = ? ', 'id', [$value]);
113 throw new InvalidValueException('pid must be for a valid patient', 'pid');
118 return $validator->validate($appointment);
121 public function search($search, $isAndCondition = true)
123 $sql = "SELECT pce.pc_eid,
130 providers.uuid AS pce_aid_uuid,
131 providers.npi AS pce_aid_npi,
139 pce.pc_billing_location,
144 f1.name as facility_name,
145 f1_map.uuid as facility_uuid,
146 f2.name as billing_location_name,
147 f2_map.uuid as billing_location_uuid
151 uuid AS pc_uuid, -- we do this because our uuid registry requires the field to be named this way
165 openemr_postcalendar_events
167 LEFT JOIN facility as f1 ON pce.pc_facility = f1.id
168 LEFT JOIN uuid_mapping as f1_map ON f1_map.target_uuid=f1.uuid AND f1_map.resource='Location'
169 LEFT JOIN facility as f2 ON pce.pc_billing_location = f2.id
170 LEFT JOIN uuid_mapping as f2_map ON f2_map.target_uuid=f2.uuid AND f2_map.resource='Location'
179 ) pd ON pd.pid = pce.pc_pid
180 LEFT JOIN users as providers ON pce.pc_aid = providers.id";
182 $whereClause = FhirSearchWhereClauseBuilder
::build($search, $isAndCondition);
184 $sql .= $whereClause->getFragment();
185 $sqlBindArray = $whereClause->getBoundValues();
186 $statementResults = QueryUtils
::sqlStatementThrowException($sql, $sqlBindArray);
188 $processingResult = new ProcessingResult();
189 while ($row = sqlFetchArray($statementResults)) {
190 $processingResult->addData($this->createResultRecordFromDatabaseResult($row));
193 return $processingResult;
196 public function getAppointmentsForPatient($pid)
198 $sqlBindArray = array();
200 $sql = "SELECT pce.pc_eid,
207 providers.uuid AS pce_aid_uuid,
208 providers.npi AS pce_aid_npi,
216 pce.pc_billing_location,
220 f1.name as facility_name,
221 f1_map.uuid as facility_uuid,
222 f2.name as billing_location_name,
223 f2_map.uuid as billing_location_uuid
224 FROM openemr_postcalendar_events as pce
225 LEFT JOIN facility as f1 ON pce.pc_facility = f1.id
226 LEFT JOIN uuid_mapping as f1_map ON f1_map.target_uuid=f1.uuid AND f1_map.resource='Location'
227 LEFT JOIN facility as f2 ON pce.pc_billing_location = f2.id
228 LEFT JOIN uuid_mapping as f2_map ON f2_map.target_uuid=f2.uuid AND f2_map.resource='Location'
229 LEFT JOIN patient_data as pd ON pd.pid = pce.pc_pid
230 LEFT JOIN users as providers ON pce.pc_aid = providers.id";
233 $sql .= " WHERE pd.pid = ?";
234 array_push($sqlBindArray, $pid);
237 $records = QueryUtils
::fetchRecords($sql, $sqlBindArray);
239 if (!empty($records)) {
240 foreach ($records as $record) {
241 $finalRecords[] = $this->createResultRecordFromDatabaseResult($record);
244 return $finalRecords;
247 public function getAppointment($eid)
249 $sql = "SELECT pce.pc_eid,
256 providers.uuid AS pce_aid_uuid,
257 providers.npi AS pce_aid_npi,
266 pce.pc_billing_location,
272 f1.name as facility_name,
273 f1_map.uuid as facility_uuid,
274 f2.name as billing_location_name,
275 f2_map.uuid as billing_location_uuid
276 FROM openemr_postcalendar_events as pce
277 LEFT JOIN facility as f1 ON pce.pc_facility = f1.id
278 LEFT JOIN uuid_mapping as f1_map ON f1_map.target_uuid=f1.uuid AND f1_map.resource='Location'
279 LEFT JOIN facility as f2 ON pce.pc_billing_location = f2.id
280 LEFT JOIN uuid_mapping as f2_map ON f2_map.target_uuid=f2.uuid AND f2_map.resource='Location'
281 LEFT JOIN patient_data as pd ON pd.pid = pce.pc_pid
282 LEFT JOIN users as providers ON pce.pc_aid = providers.id
283 WHERE pce.pc_eid = ?";
285 $records = QueryUtils
::fetchRecords($sql, [$eid]);
287 if (!empty($records)) {
288 foreach ($records as $record) {
289 $finalRecords[] = $this->createResultRecordFromDatabaseResult($record);
292 return $finalRecords;
295 public function insert($pid, $data)
297 $startUnixTime = strtotime($data['pc_startTime']);
298 $startTime = date('H:i:s', $startUnixTime);
300 // DateInterval _needs_ a valid constructor, so set it to 0s then update.
301 $endTimeInterval = new \
DateInterval('PT0S');
302 $endTimeInterval->s
= $data['pc_duration'];
304 $endTime = (new \
DateTime())->setTimestamp($startUnixTime)->add($endTimeInterval);
305 $uuid = (new UuidRegistry())->createUuid();
307 $sql = " INSERT INTO openemr_postcalendar_events SET";
309 $sql .= " pc_pid=?,";
310 $sql .= " pc_catid=?,";
311 $sql .= " pc_title=?,";
312 $sql .= " pc_duration=?,";
313 $sql .= " pc_hometext=?,";
314 $sql .= " pc_eventDate=?,";
315 $sql .= " pc_apptstatus=?,";
316 $sql .= " pc_startTime=?,";
317 $sql .= " pc_endTime=?,";
318 $sql .= " pc_facility=?,";
319 $sql .= " pc_billing_location=?,";
320 $sql .= " pc_informant=1,";
321 $sql .= " pc_eventstatus=1,";
322 $sql .= " pc_sharing=1,";
325 $results = sqlInsert(
332 $data["pc_duration"],
333 $data["pc_hometext"],
334 $data["pc_eventDate"],
335 $data['pc_apptstatus'],
337 $endTime->format('H:i:s'),
338 $data["pc_facility"],
339 $data["pc_billing_location"],
340 $data["pc_aid"] ??
null
349 * @param $recurr_affect
350 * @param $event_selected_date
353 public function deleteAppointment($eid, $recurr_affect, $event_selected_date)
355 // =======================================
356 // multi providers event
357 // =======================================
358 if ($GLOBALS['select_multi_providers']) {
359 // what is multiple key around this $eid?
360 $row = sqlQuery("SELECT pc_multiple FROM openemr_postcalendar_events WHERE pc_eid = ?", array($eid));
362 // obtain current list of providers regarding the multiple key
363 $providers_current = array();
364 $up = sqlStatement("SELECT pc_aid FROM openemr_postcalendar_events WHERE pc_multiple=?", array($row['pc_multiple']));
365 while ($current = sqlFetchArray($up)) {
366 $providers_current[] = $current['pc_aid'];
369 // establish a WHERE clause
370 if ($row['pc_multiple']) {
371 $whereClause = "pc_multiple = ?";
372 $whereBind = $row['pc_multiple'];
374 $whereClause = "pc_eid = ?";
378 if ($recurr_affect == 'current') {
379 // update all existing event records to exclude the current date
380 foreach ($providers_current as $provider) {
381 // update the provider's original event
382 // get the original event's repeat specs
383 $origEvent = sqlQuery("SELECT pc_recurrspec FROM openemr_postcalendar_events " .
384 " WHERE pc_aid <=> ? AND pc_multiple=?", array($provider,$row['pc_multiple']));
385 $oldRecurrspec = unserialize($origEvent['pc_recurrspec'], ['allowed_classes' => false]);
386 $selected_date = date("Y-m-d", strtotime($event_selected_date));
387 if ($oldRecurrspec['exdate'] != "") {
388 $oldRecurrspec['exdate'] .= "," . $selected_date;
390 $oldRecurrspec['exdate'] .= $selected_date;
393 // mod original event recur specs to exclude this date
394 sqlStatement("UPDATE openemr_postcalendar_events SET " .
395 " pc_recurrspec = ? " .
396 " WHERE " . $whereClause, array(serialize($oldRecurrspec), $whereBind));
398 } elseif ($recurr_affect == 'future') {
399 // update all existing event records to stop recurring on this date-1
400 $selected_date = date("Y-m-d", (strtotime($event_selected_date) - 24 * 60 * 60));
401 foreach ($providers_current as $provider) {
402 // In case of a change in the middle of the event
403 if (strcmp($_POST['event_start_date'], $event_selected_date) != 0) {
404 // update the provider's original event
405 sqlStatement("UPDATE openemr_postcalendar_events SET " .
407 " WHERE " . $whereClause, array($selected_date), $whereBind);
408 } else { // In case of a change in the event head
409 // as we need to notify events that we are deleting this record we need to grab all of the pc_eid
410 // so we can process the events
411 $pc_eids = QueryUtils
::fetchTableColumn(
412 "SELECT pc_eid FROM openemr_postcalendar_events WHERE " . $whereClause,
416 foreach ($pc_eids as $pc_eid) {
417 $this->deleteAppointmentRecord($pc_eid);
422 // really delete the event from the database
423 // as we need to notify events that we are deleting this record we need to grab all of the pc_eid
424 // so we can process the events
425 $pc_eids = QueryUtils
::fetchTableColumn(
426 "SELECT pc_eid FROM openemr_postcalendar_events WHERE " . $whereClause,
430 foreach ($pc_eids as $pc_eid) {
431 $this->deleteAppointmentRecord($pc_eid);
434 } else { // single provider event
435 if ($recurr_affect == 'current') {
436 // mod original event recur specs to exclude this date
437 // get the original event's repeat specs
438 $origEvent = sqlQuery("SELECT pc_recurrspec FROM openemr_postcalendar_events WHERE pc_eid = ?", array($eid));
439 $oldRecurrspec = unserialize($origEvent['pc_recurrspec'], ['allowed_classes' => false]);
440 $selected_date = date("Ymd", strtotime($_POST['selected_date']));
441 if ($oldRecurrspec['exdate'] != "") {
442 $oldRecurrspec['exdate'] .= "," . $selected_date;
444 $oldRecurrspec['exdate'] .= $selected_date;
447 sqlStatement("UPDATE openemr_postcalendar_events SET " .
448 " pc_recurrspec = ? " .
449 " WHERE pc_eid = ?", array(serialize($oldRecurrspec),$eid));
450 } elseif ($recurr_affect == 'future') {
451 // mod original event to stop recurring on this date-1
452 $selected_date = date("Ymd", (strtotime($_POST['selected_date']) - 24 * 60 * 60));
453 sqlStatement("UPDATE openemr_postcalendar_events SET " .
455 " WHERE pc_eid = ?", array($selected_date,$eid));
457 // fully delete the event from the database
458 $this->deleteAppointmentRecord($eid);
463 public function deleteAppointmentRecord($eid)
465 $servicePreDeleteEvent = new ServiceDeleteEvent($this, $eid);
466 $this->getEventDispatcher()->dispatch($servicePreDeleteEvent, ServiceDeleteEvent
::EVENT_PRE_DELETE
);
467 QueryUtils
::sqlStatementThrowException("DELETE FROM openemr_postcalendar_events WHERE pc_eid = ?", $eid);
468 $servicePostDeleteEvent = new ServiceDeleteEvent($this, $eid);
469 $this->getEventDispatcher()->dispatch($servicePostDeleteEvent, ServiceDeleteEvent
::EVENT_POST_DELETE
);
473 * Returns a list of categories
476 public function getCalendarCategories()
478 $sql = "SELECT pc_catid, pc_constant_id, pc_catname, pc_cattype,aco_spec, pc_last_updated FROM openemr_postcalendar_categories "
479 . " WHERE pc_active = 1 ORDER BY pc_seq";
480 return QueryUtils
::fetchRecords($sql);
484 * check to see if a status code exist as a check in
488 public static function isCheckInStatus($option)
490 $row = sqlQuery("SELECT toggle_setting_1 FROM list_options WHERE " .
491 "list_id = 'apptstat' AND option_id = ? AND activity = 1", array($option));
492 if (empty($row['toggle_setting_1'])) {
500 * check to see if a status code exist as a check out
504 public static function isCheckOutStatus($option)
506 $row = sqlQuery("SELECT toggle_setting_2 FROM list_options WHERE " .
507 "list_id = 'apptstat' AND option_id = ? AND activity = 1", array($option));
508 if (empty($row['toggle_setting_2'])) {
515 public function isPendingStatus($option)
517 // TODO: @adunsulag is there ANY way to track this in the database of what statii are pending?
518 if ($option == '^') {
525 * Returns a list of appointment statuses (also used with encounters).
528 public function getAppointmentStatuses()
530 $listService = new ListService();
531 $options = $listService->getOptionsByListName('apptstat', ['activity' => 1]);
536 * Checks to see if the passed in status is a valid appointment status for calendar appointments.
537 * @param $status_option_id The status to check if its a valid appointment status
538 * @return bool True if its valid, false otherwise
540 public function isValidAppointmentStatus($status_option_id)
542 $listService = new ListService();
543 $option = $listService->getListOption('apptstat', $status_option_id);
544 if (!empty($option)) {
551 * Updates the status for an appointment. TODO: should be refactored at some point to update the entire record
552 * @param $eid number The id of the appointment event
553 * @param $status string The status the appointment event should be set to.
554 * @param $user number The user performing the update
555 * @param $encounter number The encounter of the appointment
557 public function updateAppointmentStatus($eid, $status, $user, $encounter = '')
559 $appt = $this->getAppointment($eid);
561 throw new \
InvalidArgumentException("Appointment does not exist for eid " . $eid);
563 // TODO: Not sure why getAppointment returns an array of records instead of a single record
567 $sql = "UPDATE " . self
::TABLE_NAME
. " SET pc_apptstatus = ? WHERE pc_eid = ? ";
568 $binds = [$status, $eid];
570 if (!empty($appt['pid'])) {
571 $trackerService = new PatientTrackerService();
572 $trackerService->manage_tracker_status($appt['pc_eventDate'], $appt['pc_startTime'], $eid, $appt['pid'], $user, $status, $appt['pc_room'], $encounter);
574 $this->getLogger()->error("AppointmentService->updateAppointmentStatus() failed to update manage_tracker_status"
575 . " as patient pid was empty", ['pc_eid' => $eid, 'status' => $status, 'user' => $user, 'encounter' => $encounter]);
577 return QueryUtils
::sqlStatementThrowException($sql, $binds);
583 * @return array The most recent encounter for a given appointment
585 public function getEncounterForAppointment($pc_eid, $pid)
587 $appointment = $this->getAppointment($pc_eid)[0];
588 $date = $appointment['pc_eventDate'];
589 // we grab the most recent encounter for today's date for the given patient
590 $encounterService = $this->getEncounterService();
591 $dateField = new DateSearchField('date', ['eq' . $date], DateSearchField
::DATE_TYPE_DATE
);
592 $pidField = new TokenSearchField('pid', [new TokenSearchValue($pid)]);
593 // returns the most recent encounter for the given appointment..
594 // TODO: @adunsulag we should look at in the future of making an actual join table between encounters and appointments...
595 // this fuzzy match by date seems like it will have major problems for both inpatient settings as well as any kind
596 // of emergency care (patient sees doctor, patient does telehealth visit during the night due to crisis situation).
597 $encounterResult = $encounterService->search(['date' => $dateField, 'pid' => $pidField], true, null, ['limit' => 1]);
598 if ($encounterResult->hasData()) {
599 $result = $encounterResult->getData();
600 return array_pop($result);
605 public function createEncounterForAppointment($eid)
607 $appointment = $this->getAppointment($eid)[0];
608 $patientService = $this->getPatientService();
609 $patientUuid = UuidRegistry
::uuidToString($patientService->getUuid($appointment['pid']));
611 $userService = new UserService();
612 $user = $userService->getUser($appointment['pc_aid']);
613 $authGroup = UserService
::getAuthGroupForUser($user['username']);
615 $pos_code = QueryUtils
::fetchSingleValue(
616 "SELECT pos_code FROM facility WHERE id = ?",
618 [$appointment['pc_facility']]
621 $visit_reason = $appointment['pc_hometext'] ??
xl('Please indicate visit reason');
622 if (!empty($GLOBALS['auto_create_prevent_reason'] ??
0)) {
623 $visit_reason = 'Please indicate visit reason';
626 'pc_catid' => $appointment['pc_catid']
627 // TODO: where would we get this information if it wasn't defaulted to ambulatory? Should this be a globals setting?
628 // this is imitating the work from encounter_events.inc.php::todaysEncounterCheck
629 ,'class_code' => EncounterService
::DEFAULT_CLASS_CODE
630 ,'puuid' => $patientUuid
631 ,'pid' => $appointment['pid']
632 ,'provider_id' => $user['id']
633 ,'reason' => $visit_reason
634 ,'facility_id' => $appointment['pc_facility']
635 ,'billing_facility' => $appointment['pc_billing_location']
636 ,'pos_code' => $pos_code
637 ,'user' => $user['username']
638 ,'group' => $authGroup
641 $encounterService = $this->getEncounterService();
642 $result = $encounterService->insertEncounter($patientUuid, $data);
643 if ($result->hasData()) {
644 $result = $result->getData();
645 return $result[0]['encounter'];
651 * Returns the calendar category record from a supplied category id
654 public function getOneCalendarCategory($cat_id)
656 $sql = "SELECT * FROM openemr_postcalendar_categories WHERE pc_catid = ?";
657 return QueryUtils
::fetchRecords($sql, [$cat_id]);
660 public function searchCalendarCategories(array $oeSearchParameters)
662 $sql = "SELECT * FROM openemr_postcalendar_categories ";
663 $whereClause = FhirSearchWhereClauseBuilder
::build($oeSearchParameters, true);
664 $sql .= $whereClause->getFragment();
665 $sqlBindArray = $whereClause->getBoundValues();
666 $records = QueryUtils
::fetchRecords($sql, $sqlBindArray);
667 $processingResult = new ProcessingResult();
668 if (!empty($records)) {
669 $processingResult->setData($records);
671 // TODO: look at handling offset and limit here
672 return $processingResult;