Add title to all appointment sql queries (#7311)
[openemr.git] / src / Services / AppointmentService.php
blobc6d9e62b584e3da65b70f6b291454eb406eea1f8
1 <?php
3 /**
4 * AppointmentService
6 * @package OpenEMR
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';
38 /**
39 * @var EncounterService
41 private $encounterService;
43 /**
44 * @var PatientService
46 private $patientService;
48 /**
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]);
105 if (empty($id)) {
106 throw new InvalidValueException('pc_aid must be for a valid user', 'pc_aid');
108 return true;
110 $validator->optional('pid')->callback(function ($value, $data) {
111 $id = QueryUtils::fetchSingleValue('Select id FROM patient_data WHERE pid = ? ', 'id', [$value]);
112 if (empty($id)) {
113 throw new InvalidValueException('pid must be for a valid patient', 'pid');
115 return true;
118 return $validator->validate($appointment);
121 public function search($search, $isAndCondition = true)
123 $sql = "SELECT pce.pc_eid,
124 pce.pc_uuid,
125 pd.puuid,
126 pd.fname,
127 pd.lname,
128 pd.DOB,
129 pd.pid,
130 providers.uuid AS pce_aid_uuid,
131 providers.npi AS pce_aid_npi,
132 pce.pc_aid,
133 pce.pc_apptstatus,
134 pce.pc_eventDate,
135 pce.pc_startTime,
136 pce.pc_endTime,
137 pce.pc_time,
138 pce.pc_facility,
139 pce.pc_billing_location,
140 pce.pc_catid,
141 pce.pc_pid,
142 pce.pc_duration,
143 pce.pc_title,
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
148 FROM (
149 SELECT
150 pc_eid,
151 uuid AS pc_uuid, -- we do this because our uuid registry requires the field to be named this way
152 pc_aid,
153 pc_apptstatus,
154 pc_eventDate,
155 pc_startTime,
156 pc_duration,
157 pc_endTime,
158 pc_time,
159 pc_facility,
160 pc_billing_location,
161 pc_catid,
162 pc_pid,
163 pc_title
164 FROM
165 openemr_postcalendar_events
166 ) pce
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'
171 LEFT JOIN (
172 select uuid AS puuid
173 ,fname
174 ,lname
175 ,DOB
176 ,pid
177 FROM
178 patient_data
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,
201 pce.uuid AS pc_uuid,
202 pd.fname,
203 pd.lname,
204 pd.DOB,
205 pd.pid,
206 pd.uuid AS puuid,
207 providers.uuid AS pce_aid_uuid,
208 providers.npi AS pce_aid_npi,
209 pce.pc_aid,
210 pce.pc_apptstatus,
211 pce.pc_eventDate,
212 pce.pc_startTime,
213 pce.pc_endTime,
214 pce.pc_time,
215 pce.pc_facility,
216 pce.pc_billing_location,
217 pce.pc_catid,
218 pce.pc_pid,
219 pce.pc_title,
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";
232 if ($pid) {
233 $sql .= " WHERE pd.pid = ?";
234 array_push($sqlBindArray, $pid);
237 $records = QueryUtils::fetchRecords($sql, $sqlBindArray);
238 $finalRecords = [];
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,
250 pce.uuid AS pc_uuid,
251 pd.fname,
252 pd.lname,
253 pd.DOB,
254 pd.pid,
255 pd.uuid AS puuid,
256 providers.uuid AS pce_aid_uuid,
257 providers.npi AS pce_aid_npi,
258 pce.pc_aid,
259 pce.pc_apptstatus,
260 pce.pc_eventDate,
261 pce.pc_startTime,
262 pce.pc_endTime,
263 pce.pc_time,
264 pce.pc_duration,
265 pce.pc_facility,
266 pce.pc_billing_location,
267 pce.pc_catid,
268 pce.pc_room,
269 pce.pc_pid,
270 pce.pc_hometext,
271 pce.pc_title,
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]);
286 $finalRecords = [];
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";
308 $sql .= " uuid=?,";
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,";
323 $sql .= " pc_aid=?";
325 $results = sqlInsert(
326 $sql,
327 array(
328 $uuid,
329 $pid,
330 $data["pc_catid"],
331 $data["pc_title"],
332 $data["pc_duration"],
333 $data["pc_hometext"],
334 $data["pc_eventDate"],
335 $data['pc_apptstatus'],
336 $startTime,
337 $endTime->format('H:i:s'),
338 $data["pc_facility"],
339 $data["pc_billing_location"],
340 $data["pc_aid"] ?? null
344 return $results;
348 * @param $eid
349 * @param $recurr_affect
350 * @param $event_selected_date
351 * @return void
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'];
373 } else {
374 $whereClause = "pc_eid = ?";
375 $whereBind = $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;
389 } else {
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 " .
406 " pc_enddate = ? " .
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,
413 'pc_eid',
414 [$whereBind]
416 foreach ($pc_eids as $pc_eid) {
417 $this->deleteAppointmentRecord($pc_eid);
421 } else {
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,
427 'pc_eid',
428 [$whereBind]
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;
443 } else {
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 " .
454 " pc_enddate = ? " .
455 " WHERE pc_eid = ?", array($selected_date,$eid));
456 } else {
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
474 * @return array
476 public function getCalendarCategories()
478 $sql = "SELECT pc_catid, pc_constant_id, pc_catname, pc_cattype,aco_spec 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
485 * @param $option
486 * @return bool
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'])) {
493 return(false);
496 return(true);
500 * check to see if a status code exist as a check out
501 * @param $option
502 * @return bool
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'])) {
509 return(false);
512 return(true);
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 == '^') {
519 return true;
521 return false;
525 * Returns a list of appointment statuses (also used with encounters).
526 * @return array
528 public function getAppointmentStatuses()
530 $listService = new ListService();
531 $options = $listService->getOptionsByListName('apptstat', ['activity' => 1]);
532 return $options;
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)) {
545 return true;
547 return false;
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);
560 if (empty($appt)) {
561 throw new \InvalidArgumentException("Appointment does not exist for eid " . $eid);
562 } else {
563 // TODO: Not sure why getAppointment returns an array of records instead of a single record
564 $appt = $appt[0];
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);
573 } else {
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);
581 * @param $eid
582 * @param $pid
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);
602 return null;
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 = ?",
617 'pos_code',
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';
625 $data = [
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'];
647 return null;
651 * Returns the calendar category record from a supplied category id
652 * @return array
654 public function getOneCalendarCategory($cat_id)
656 $sql = "SELECT * FROM openemr_postcalendar_categories WHERE pc_catid = ?";
657 return QueryUtils::fetchRecords($sql, [$cat_id]);