Skip to content

Commit

Permalink
Fixes openemr#6720 appointment dialog hooks (openemr#6721)
Browse files Browse the repository at this point in the history
* Fixes openemr#6720 appointment dialog hooks

Made it so we can launch a smart app with a specific appointment context
as well as hook into the appointment close dialog.

Also added in an ehr-launch-client.php file so we can quickly do an ehr
initiated app launch.

* Add copyright headers

* Fix styles
  • Loading branch information
adunsulag authored Aug 11, 2023
1 parent 0520046 commit 1982551
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 4 deletions.
16 changes: 16 additions & 0 deletions interface/main/calendar/add_edit_event.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@
use OpenEMR\Core\Header;
use OpenEMR\Events\Appointments\AppointmentSetEvent;
use OpenEMR\Events\Appointments\AppointmentRenderEvent;
use OpenEMR\Events\Appointments\AppointmentDialogCloseEvent;
use OpenEMR\Common\Logging\SystemLogger;

//Check access control
if (!AclMain::aclCheckCore('patients', 'appt', '', array('write','wsome'))) {
Expand Down Expand Up @@ -764,6 +766,20 @@ function setEventDate($start_date, $recurrence)
}

if (!empty($_POST['form_action'])) {
$closeEvent = new AppointmentDialogCloseEvent();
$closeEvent->setDialogAction($_POST['form_action']);
$closeEventData = ['form_action' => $_POST['form_action']];
if (isset($eid)) {
$closeEvent->setAppointmentId($eid);
}
$event = $GLOBALS['kernel']->getEventDispatcher()->dispatch($closeEvent, AppointmentDialogCloseEvent::EVENT_NAME);
// listeners can stop the window from closing if they want to add any additional workflow steps
// to the calendar appointment flow for their own workflow dialogs here they will need
// to implement the dialog closing and duplicate the logic of what happens here in this closing event.
if ($event->isPropagationStopped()) {
(new SystemLogger())->debug("add_edit_event.php: event propagation stopped before closing dialog, exiting");
exit();
}
// Close this window and refresh the calendar (or the patient_tracker) display.
echo "<html>\n<body>\n<script>\n";
if ($info_msg) {
Expand Down
34 changes: 34 additions & 0 deletions interface/smart/ehr-launch-client.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

/**
* ehr-launch-client.php Main entry point for the OpenEMR OAUTH2 / SMART client in ehr launch
* Allows a smart app to launch into the OpenEMR EHR in a seamless interaction
* @package openemr
* @link http://www.open-emr.org
* @author Stephen Nielson <[email protected]>
* @copyright Copyright (c) 2023 Discover and Change, Inc. <[email protected]>
* @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
*/

require_once("../globals.php");

$controller = new \OpenEMR\FHIR\SMART\SmartLaunchController();

$intentData = [];
try {
$intentData['appointment_id'] = $_REQUEST['appointment_id'] ?? null;
$controller->redirectAndLaunchSmartApp(
$_REQUEST['intent'] ?? null,
$_REQUEST['client_id'] ?? null,
$_REQUEST['csrf_token'] ?? null,
$intentData
);
} catch (CsrfInvalidException $exception) {
CsrfUtils::csrfNotVerified();
} catch (AccessDeniedException $exception) {
(new SystemLogger())->critical($exception->getMessage(), ["trace" => $exception->getTraceAsString()]);
die();
} catch (Exception $exception) {
(new SystemLogger())->error($exception->getMessage(), ["trace" => $exception->getTraceAsString()]);
die("Unknown system error occurred");
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use OpenEMR\Common\Logging\SystemLogger;
use OpenEMR\FHIR\SMART\SmartLaunchController;
use OpenEMR\FHIR\SMART\SMARTLaunchToken;
use OpenEMR\Services\FHIR\UtilsService;

class SMARTSessionTokenContextBuilder
{
Expand Down Expand Up @@ -58,6 +59,9 @@ public function getEHRLaunchContext()
if (!empty($launchToken->getIntent())) {
$context['intent'] = $launchToken->getIntent();
}
if (!empty($launchToken->getAppointmentUuid())) {
$context['fhirContext'] = [UtilsService::createRelativeReference('Appointment', $launchToken->getAppointmentUuid())];
}
$context['smart_style_url'] = $this->getSmartStyleURL();
} catch (\Exception $ex) {
$this->logger->error("SMARTSessionTokenContextBuilder->getAccessTokenContextParameters() Failed to decode launch context parameter", ['error' => $ex->getMessage()]);
Expand Down
48 changes: 48 additions & 0 deletions src/Events/Appointments/AppointmentDialogCloseEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

/**
* AppointmentDialogCloseEvent fires when the appointment dialog screen (add_edit_event.php) is triggered to be closed
* This event is fired before the server sends the instructions to the client to close the dialog, and allows a plugin
* to perform any actions before the dialog is closed (such as preventing the closure, or by performing some action)
*
* @package openemr
* @link http://www.open-emr.org
* @author Stephen Nielson <[email protected]>
* @copyright Copyright (c) 2023 Discover and Change, Inc. <[email protected]>
* @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
*/

namespace OpenEMR\Events\Appointments;

use Symfony\Contracts\EventDispatcher\Event;

class AppointmentDialogCloseEvent extends Event
{
const EVENT_NAME = 'openemr.appointment.add_edit_event.close.before';

private $pc_eid;
private $dialog_action;

public function __construct()
{
}

public function setAppointmentId($pc_eid)
{
$this->pc_eid = $pc_eid;
}
public function getAppointmentId()
{
return $this->pc_eid;
}

public function getDialogAction()
{
return $this->dialog_action;
}

public function setDialogAction($dialog_action)
{
$this->dialog_action = $dialog_action;
}
}
30 changes: 29 additions & 1 deletion src/FHIR/SMART/SMARTLaunchToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,23 @@
class SMARTLaunchToken
{
public const INTENT_PATIENT_DEMOGRAPHICS_DIALOG = 'patient.demographics.dialog';
public const VALID_INTENTS = [self::INTENT_PATIENT_DEMOGRAPHICS_DIALOG];
public const VALID_INTENTS = [self::INTENT_PATIENT_DEMOGRAPHICS_DIALOG, self::INTENT_APPOINTMENT_DIALOG];

// used on the appointment add/edit dialog, context will include the selected appointment
// for now this intent is used by custom apps that consume the openemr.appointment.add_edit_event.close.before event
// to present a SMART app as a 2nd step to the add/edit appointment workflow
public const INTENT_APPOINTMENT_DIALOG = 'appointment.edit.dialog';

/**
* @var string|null The patient UUID If
*/
private $patient;
private $intent;
private $encounter;
/**
* @var string The uuid of the appointment
*/
private ?string $appointmentUuid;

public function __construct($patientUUID = null, $encounterUUID = null)
{
Expand All @@ -35,6 +47,7 @@ public function __construct($patientUUID = null, $encounterUUID = null)
}
$this->patient = $patientUUID;
$this->encounter = $encounterUUID;
$this->appointmentUuid = null;
}

/**
Expand Down Expand Up @@ -100,6 +113,9 @@ public function serialize()
if (!empty($intent)) {
$context['i'] = $intent;
}
if (!empty($this->getAppointmentUuid())) {
$context['apt'] = $this->getAppointmentUuid();
}

// no security is really needed here... just need to be able to wrap
// the current context into some kind of opaque id that the app will pass to the server and we can then
Expand Down Expand Up @@ -138,10 +154,22 @@ public function deserialize($serialized)
if (!empty($context['i']) && $this->isValidIntent($context['i'])) {
$this->setIntent($context['i']);
}
if (!empty($context['apt'])) {
$this->setAppointmentUuid($context['apt']);
}
}

public function isValidIntent($intent)
{
return array_search($intent, self::VALID_INTENTS) !== false;
}
public function setAppointmentUuid(string $appointmentUuid)
{
$this->appointmentUuid = $appointmentUuid;
}

public function getAppointmentUuid(): ?string
{
return $this->appointmentUuid;
}
}
72 changes: 69 additions & 3 deletions src/FHIR/SMART/SmartLaunchController.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,15 @@

namespace OpenEMR\FHIR\SMART;

use OpenEMR\Common\Acl\AccessDeniedException;
use OpenEMR\Common\Acl\AclMain;
use OpenEMR\Common\Auth\OpenIDConnect\Entities\ClientEntity;
use OpenEMR\Common\Auth\OpenIDConnect\Repositories\ClientRepository;
use OpenEMR\Common\Csrf\CsrfUtils;
use OpenEMR\Common\Uuid\UuidRegistry;
use OpenEMR\Events\PatientDemographics\RenderEvent;
use OpenEMR\Services\AppointmentService;
use OpenEMR\Services\EncounterService;
use OpenEMR\Services\PatientService;
use Symfony\Component\EventDispatcher\EventDispatcher;
use OpenEMR\FHIR\Config\ServerConfig;
Expand Down Expand Up @@ -121,6 +127,62 @@ public function renderPatientSmartLaunchSection(RenderEvent $event)
<?php
// it's too bad we don't have a centralized page renderer we could tie this into and render javascript at the
// end of our footer pages on everything...
}

public function redirectAndLaunchSmartApp($intent, $client_id, $csrf_token, array $intentData)
{
$clientRepository = new ClientRepository();
$client = $clientRepository->getClientEntity($client_id);
if (empty($client)) {
throw new \Exception("Invalid client id");
}
CsrfUtils::verifyCsrfToken($csrf_token);
$puuid = null;
$euuid = null;
if (isset($_SESSION['pid'])) {
// grab the patient puuid
$patientService = new PatientService();
$puuid = UuidRegistry::uuidToString($patientService->getUuid($_SESSION['pid']));
}
if (!empty($_SESSION['encounter'])) {
// grab the encounter euuid
$euuid = UuidRegistry::uuidToString(EncounterService::getUuidById($_SESSION['encounter'], 'form_encounter', 'encounter'));
}
$appointmentUuid = null;
if (!empty($intentData)) {
// let's grab specific data
if (!empty($intentData['appointment_id'])) {
if (!AclMain::aclCheckCore('patients', 'appt')) {
throw new AccessDeniedException("patients", "appt", "You do not have permission to access appointments");
}
$appointmentService = new AppointmentService();
$appointment = $appointmentService->getAppointment($intentData['appointment_id']);
if (!empty($appointment)) {
$patientService = new PatientService();
$appointmentUuid = $appointment[0]['pc_uuid'];
$pid = $appointment[0]['pid'];
$puuid = UuidRegistry::uuidToString($patientService->getUuid($pid));
// at some point if the appointment has a link to encounters we could grab that here.
// $euuid = UuidRegistry::uuidToString(EncounterService::getUuidById($appointment['encounter'], 'form_encounter', 'encounter'));
}
}
}

$issuer = (new ServerConfig())->getFhirUrl();
$launchCode = $this->getLaunchCodeContext($puuid, $euuid, $intent);

if (!empty($appointmentUuid)) {
$launchCode->setAppointmentUuid($appointmentUuid);
}
$serializedCode = $launchCode->serialize();
$launchParams = "?launch=" . urlencode($serializedCode) . "&iss=" . urlencode($issuer) . "&aud=" . urlencode($issuer);
$redirectUrl = $client->getLaunchUri($launchParams);
header("Location: " . $redirectUrl);
exit;
}

public function renderLaunchScript()
{
?>
<script>
(function(window) {
Expand Down Expand Up @@ -164,10 +226,14 @@ private function getSMARTClients()
return $smartList;
}

private function getLaunchCodeContext($patientUUID, $encounterId = null)
private function getLaunchCodeContext($patientUUID, $encounterId = null, $intent = null)
{
$token = new SMARTLaunchToken($patientUUID, $encounterId);
$token->setIntent(SMARTLaunchToken::INTENT_PATIENT_DEMOGRAPHICS_DIALOG);
return $token->serialize();
$token->setIntent($intent);
if (empty($intent)) {
$intent = SMARTLaunchToken::INTENT_PATIENT_DEMOGRAPHICS_DIALOG;
}
$token->setIntent($intent);
return $token;
}
}

0 comments on commit 1982551

Please sign in to comment.