diff --git a/_rest_config.php b/_rest_config.php index c1f652a315d..d931bb969e9 100644 --- a/_rest_config.php +++ b/_rest_config.php @@ -202,19 +202,30 @@ public static function verifyAccessToken() $logger = new SystemLogger(); $response = self::createServerResponse(); $request = self::createServerRequest(); - $server = new ResourceServer( - new AccessTokenRepository(), - self::$publicKey - ); try { + // if we there's a key problem need to catch the exception + $server = new ResourceServer( + new AccessTokenRepository(), + self::$publicKey + ); $raw = $server->validateAuthenticatedRequest($request); } catch (OAuthServerException $exception) { $logger->error("RestConfig->verifyAccessToken() OAuthServerException", ["message" => $exception->getMessage()]); return $exception->generateHttpResponse($response); } catch (\Exception $exception) { - $logger->error("RestConfig->verifyAccessToken() Exception", ["message" => $exception->getMessage()]); - return (new OAuthServerException($exception->getMessage(), 0, 'unknown_error', 500)) - ->generateHttpResponse($response); + if ($exception instanceof LogicException) { + $logger->error( + "RestConfig->verifyAccessToken() LogicException, likely oauth2 public key is missing, corrupted, or misconfigured", + ["message" => $exception->getMessage()] + ); + return (new OAuthServerException("Invalid access token", 0, 'invalid_token', 401)) + ->generateHttpResponse($response); + } else { + $logger->error("RestConfig->verifyAccessToken() Exception", ["message" => $exception->getMessage()]); + // do NOT reveal what happened at the server level if we have a server exception + return (new OAuthServerException("Server Error", 0, 'unknown_error', 500)) + ->generateHttpResponse($response); + } } return $raw; diff --git a/_rest_routes.inc.php b/_rest_routes.inc.php index b2792744a90..d3fa1ef324e 100644 --- a/_rest_routes.inc.php +++ b/_rest_routes.inc.php @@ -7622,6 +7622,15 @@ * ) * ), * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( * name="patient", * in="query", * description="The uuid for the patient.", @@ -7814,6 +7823,15 @@ * ) * ), * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( * name="patient", * in="query", * description="The uuid for the patient.", @@ -7945,6 +7963,15 @@ * ) * ), * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( * name="patient", * in="query", * description="The uuid for the patient.", @@ -8113,6 +8140,15 @@ * ) * ), * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( * name="patient", * in="query", * description="The uuid for the patient.", @@ -8196,6 +8232,15 @@ * type="string" * ) * ), + * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), * @OA\Response( * response="200", * description="Standard Response", @@ -8305,6 +8350,15 @@ * ) * ), * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( * name="patient", * in="query", * description="The uuid for the patient.", @@ -8484,6 +8538,15 @@ * ) * ), * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( * name="patient", * in="query", * description="The uuid for the patient.", @@ -8642,6 +8705,15 @@ * ) * ), * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( * name="patient", * in="query", * description="The uuid for the patient.", @@ -8802,6 +8874,15 @@ * ) * ), * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( * name="patient", * in="query", * description="The uuid for the patient.", @@ -9011,6 +9092,15 @@ * ) * ), * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( * name="patient", * in="query", * description="The uuid for the patient.", @@ -9338,6 +9428,15 @@ * ) * ), * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( * name="patient", * in="query", * description="The uuid for the patient.", @@ -9537,6 +9636,15 @@ * ) * ), * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( * name="patient", * in="query", * description="The uuid for the patient.", @@ -9697,6 +9805,15 @@ * ) * ), * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( * name="patient", * in="query", * description="The uuid for the patient.", @@ -9861,6 +9978,24 @@ * ) * ), * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( * name="patient", * in="query", * description="The uuid for the patient.", @@ -10014,6 +10149,15 @@ * type="string" * ) * ), + * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), * @OA\Response( * response="200", * description="Standard Response", @@ -10133,6 +10277,24 @@ * path="/fhir/Medication", * description="Returns a list of Medication resources.", * tags={"fhir"}, + * @OA\Parameter( + * name="_id", + * in="query", + * description="The uuid for the Medication resource.", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), * @OA\Response( * response="200", * description="Standard Response", @@ -10270,6 +10432,15 @@ * ) * ), * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( * name="patient", * in="query", * description="The uuid for the patient.", @@ -10458,6 +10629,15 @@ * ) * ), * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( * name="patient", * in="query", * description="The uuid for the patient.", @@ -10684,6 +10864,15 @@ * ) * ), * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( * name="name", * in="query", * description="The name of the Organization resource.", @@ -11491,6 +11680,15 @@ * ) * ), * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( * name="identifier", * in="query", * description="The identifier of the Patient resource.", @@ -11860,6 +12058,24 @@ * description="Returns a list of Person resources.", * tags={"fhir"}, * @OA\Parameter( + * name="_id", + * in="query", + * description="The uuid for the Person resource.", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( * name="name", * in="query", * description="The name of the Person resource.", @@ -12138,6 +12354,15 @@ * ) * ), * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( * name="name", * in="query", * description="The name of the Practitioner resource.", @@ -12569,6 +12794,24 @@ * description="Returns a list of PractitionerRole resources.", * tags={"fhir"}, * @OA\Parameter( + * name="_id", + * in="query", + * description="The uuid for the PractitionerRole resource.", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( * name="specialty", * in="query", * description="The specialty of the PractitionerRole resource.", @@ -12727,6 +12970,15 @@ * ) * ), * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Parameter( * name="patient", * in="query", * description="The uuid for the patient.", @@ -13051,6 +13303,15 @@ * type="string" * ) * ), + * @OA\Parameter( + * name="_lastUpdated", + * in="query", + * description="Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)", + * required=false, + * @OA\Schema( + * type="string" + * ) + * ), * @OA\Response( * response="200", * description="Standard Response", diff --git a/interface/super/load_codes.php b/interface/super/load_codes.php index 9bea7da6e07..89d122b13d1 100644 --- a/interface/super/load_codes.php +++ b/interface/super/load_codes.php @@ -226,7 +226,10 @@ that (zipped or not). You may do the same with the weekly updates, but for those uncheck the "" checkbox above.

- +

+ +

+
diff --git a/library/classes/Installer.class.php b/library/classes/Installer.class.php index 319d14dad11..3bb2eb0ac7b 100644 --- a/library/classes/Installer.class.php +++ b/library/classes/Installer.class.php @@ -1346,14 +1346,23 @@ private function execute_sql($sql, $showError = true) $this->user_database_connection(); } - $results = mysqli_query($this->dbh, $sql); - if ($results) { - return $results; - } else { + try { + $results = mysqli_query($this->dbh, $sql); + if ($results) { + return $results; + } else { + if ($showError) { + $error_mes = mysqli_error($this->dbh); + $this->error_message = "unable to execute SQL: '$sql' due to: " . $error_mes; + error_log("ERROR IN OPENEMR INSTALL: Unable to execute SQL: " . htmlspecialchars($sql, ENT_QUOTES) . " due to: " . htmlspecialchars($error_mes, ENT_QUOTES)); + } + return false; + } + // this exception only occurs if MYSQLI_REPORT_STRICT is enabled (see https://www.php.net/manual/en/mysqli.query.php) + } catch (\mysqli_sql_exception $exception) { if ($showError) { - $error_mes = mysqli_error($this->dbh); - $this->error_message = "unable to execute SQL: '$sql' due to: " . $error_mes; - error_log("ERROR IN OPENEMR INSTALL: Unable to execute SQL: " . htmlspecialchars($sql, ENT_QUOTES) . " due to: " . htmlspecialchars($error_mes, ENT_QUOTES)); + $this->error_message = "unable to execute SQL: '$sql' due to: " . $exception->getMessage(); + error_log("ERROR IN OPENEMR INSTALL: Unable to execute SQL: " . htmlspecialchars($sql, ENT_QUOTES) . " due to: " . htmlspecialchars($exception->getMessage(), ENT_QUOTES)); } return false; } diff --git a/sql/7_0_2-to-7_0_3_upgrade.sql b/sql/7_0_2-to-7_0_3_upgrade.sql index 40f7ffb72cb..3cf5447325d 100644 --- a/sql/7_0_2-to-7_0_3_upgrade.sql +++ b/sql/7_0_2-to-7_0_3_upgrade.sql @@ -134,4 +134,72 @@ INSERT INTO `supported_external_dataloads` (`load_type`, `load_source`, `load_re #IfNotRow4D supported_external_dataloads load_type ICD10 load_source CMS load_release_date 2024-10-01 load_filename Zip File 3 2025 ICD-10-PCS Codes File.zip INSERT INTO `supported_external_dataloads` (`load_type`, `load_source`, `load_release_date`, `load_filename`, `load_checksum`) VALUES ('ICD10', 'CMS', '2024-10-01', 'Zip File 3 2025 ICD-10-PCS Codes File.zip', 'a47ceb9a09fcc475fec19cee6526a335'); -#EndIf \ No newline at end of file +#EndIf + +#IfMissingColumn users date_created +ALTER TABLE `users` ADD `date_created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP; +#EndIf + +#IfMissingColumn users last_updated +ALTER TABLE `users` ADD `last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP; +#EndIf + +#IfMissingColumn facility date_created +ALTER TABLE `facility` ADD `date_created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP; +#EndIf + +#IfMissingColumn facility last_updated +ALTER TABLE `facility` ADD `last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP; +#EndIf + +#IfMissingColumn insurance_companies date_created +ALTER TABLE `insurance_companies` ADD `date_created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP; +#EndIf + +#IfMissingColumn insurance_companies last_updated +ALTER TABLE `insurance_companies` ADD `last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP; +#EndIf + +#IfMissingColumn facility_user_ids date_created +ALTER TABLE `facility_user_ids` ADD `date_created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP; +#EndIf + +#IfMissingColumn facility_user_ids last_updated +ALTER TABLE `facility_user_ids` ADD `last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP; +#EndIf + +#IfMissingColumn openemr_postcalendar_categories pc_last_updated +ALTER TABLE `openemr_postcalendar_categories` ADD `pc_last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP; +#EndIf + +#IfMissingColumn list_options last_updated +ALTER TABLE `list_options` ADD `last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP; +#EndIf + +#IfMissingColumn form_clinical_notes last_updated +ALTER TABLE `form_clinical_notes` ADD `last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP; +#EndIf + +#IfMissingColumn form_vitals last_updated +ALTER TABLE `form_vitals` ADD `last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP; +#EndIf + +#IfMissingColumn procedure_providers date_created +ALTER TABLE `procedure_providers` ADD `date_created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP; +#EndIf + +#IfMissingColumn procedure_providers last_updated +ALTER TABLE `procedure_providers` ADD `last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP; +#EndIf + +#IfMissingColumn drugs date_created +ALTER TABLE `drugs` ADD `date_created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP; +#EndIf + +#IfMissingColumn drugs last_updated +ALTER TABLE `drugs` ADD `last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP; +#EndIf + +#IfMissingColumn patient_data last_updated +ALTER TABLE `patient_data` ADD `last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP; +#EndIf diff --git a/sql/database.sql b/sql/database.sql index 1fd6da2f49f..6954d015db1 100644 --- a/sql/database.sql +++ b/sql/database.sql @@ -1485,6 +1485,8 @@ CREATE TABLE `drugs` ( `drug_code` varchar(25) NULL, `consumable` tinyint(1) NOT NULL DEFAULT 0 COMMENT '1 = will not show on the fee sheet', `dispensable` tinyint(1) NOT NULL DEFAULT 1 COMMENT '0 = pharmacy elsewhere, 1 = dispensed here', + `date_created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`drug_id`), UNIQUE KEY `uuid` (`uuid`) ) ENGINE=InnoDB AUTO_INCREMENT=1; @@ -1737,6 +1739,8 @@ CREATE TABLE `facility` ( `info` TEXT, `weno_id` VARCHAR(10) DEFAULT NULL, `inactive` tinyint(1) NOT NULL DEFAULT '0', + `date_created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY `uuid` (`uuid`), PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4; @@ -1745,7 +1749,7 @@ CREATE TABLE `facility` ( -- Inserting data for table `facility` -- -INSERT INTO `facility` VALUES (3, NULL, 'Your Clinic Name Here', '000-000-0000', '000-000-0000', '', '', '', '', '', '', NULL, NULL, 1, 1, 1, NULL, '', '', '', '', '', '','#99FFFF','0', '', '1', '', '', '', '', '', '', '', '', NULL, 0); +INSERT INTO `facility` VALUES (3, NULL, 'Your Clinic Name Here', '000-000-0000', '000-000-0000', '', '', '', '', '', '', NULL, NULL, 1, 1, 1, NULL, '', '', '', '', '', '','#99FFFF','0', '', '1', '', '', '', '', '', '', '', '', NULL, 0, NOW(), NOW()); -- -------------------------------------------------------- @@ -1761,6 +1765,8 @@ CREATE TABLE `facility_user_ids` ( `uuid` binary(16) DEFAULT NULL, `field_id` varchar(31) NOT NULL COMMENT 'references layout_options.field_id', `field_value` TEXT, + `date_created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `uid` (`uid`,`facility_id`,`field_id`), KEY `uuid` (`uuid`) @@ -1839,6 +1845,7 @@ CREATE TABLE `form_clinical_notes` ( `clinical_notes_type` varchar(100) DEFAULT NULL, `clinical_notes_category` varchar(100) DEFAULT NULL, `note_related_to` text, + `last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uuid` (`uuid`) ) ENGINE=InnoDB; @@ -2293,6 +2300,7 @@ CREATE TABLE `form_vitals` ( `ped_bmi` DECIMAL(6,2) default '0.00', `ped_head_circ` DECIMAL(6,2) default '0.00', `inhaled_oxygen_concentration` DECIMAL(6,2) DEFAULT '0.00', + `last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `pid` (`pid`), UNIQUE KEY `uuid` (`uuid`) @@ -3137,6 +3145,8 @@ CREATE TABLE `insurance_companies` ( `eligibility_id` VARCHAR(32) default NULL, `x12_default_eligibility_id` INT(11) default NULL, `cqm_sop` int DEFAULT NULL COMMENT 'HL7 Source of Payment for eCQMs', + `date_created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uuid` (`uuid`) ) ENGINE=InnoDB; @@ -3715,6 +3725,7 @@ CREATE TABLE `list_options` ( `subtype` varchar(31) NOT NULL DEFAULT '', `edit_options` tinyint(1) NOT NULL DEFAULT '1', `timestamp` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`list_id`,`option_id`) ) ENGINE=InnoDB; @@ -7289,6 +7300,7 @@ CREATE TABLE `openemr_postcalendar_categories` ( `pc_active` tinyint(1) NOT NULL default 1, `pc_seq` int(11) NOT NULL default '0', `aco_spec` VARCHAR(63) NOT NULL default 'encounters|notes', + `pc_last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`pc_catid`), UNIQUE KEY (`pc_constant_id`), KEY `basic_cat` (`pc_catname`,`pc_catcolor`) @@ -7298,21 +7310,37 @@ CREATE TABLE `openemr_postcalendar_categories` ( -- Inserting data for table `openemr_postcalendar_categories` -- -INSERT INTO `openemr_postcalendar_categories` VALUES (1,'no_show', 'No Show', '#dee2e6', 'Reserved to define when an event did not occur as specified.', 0, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"0";s:22:"event_repeat_freq_type";s:1:"0";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 0, 0, 0, 0, 0, 0, 0,1,1,'encounters|notes'); -INSERT INTO `openemr_postcalendar_categories` VALUES (2,'in_office', 'In Office', '#cce5ff', 'Reserved todefine when a provider may haveavailable appointments after.', 1, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"1";s:22:"event_repeat_freq_type";s:1:"4";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 0, 1, 3, 2, 0, 0, 1,1,2,'encounters|notes'); -INSERT INTO `openemr_postcalendar_categories` VALUES (3,'out_of_office', 'Out Of Office', '#fdb172', 'Reserved to define when a provider may not have available appointments after.', 1, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"1";s:22:"event_repeat_freq_type";s:1:"4";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 0, 1, 3, 2, 0, 0, 1,1,3,'encounters|notes'); -INSERT INTO `openemr_postcalendar_categories` VALUES (4,'vacation', 'Vacation', '#e9ecef', 'Reserved for use to define Scheduled Vacation Time', 0, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"0";s:22:"event_repeat_freq_type";s:1:"0";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 0, 0, 0, 0, 1, 0, 1,1,4,'encounters|notes'); -INSERT INTO `openemr_postcalendar_categories` VALUES (5,'office_visit', 'Office Visit', '#ffecb4', 'Normal Office Visit', 0, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"0";s:22:"event_repeat_freq_type";s:1:"0";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 900, 0, 0, 0, 0, 0,0,1,5,'encounters|notes'); -INSERT INTO `openemr_postcalendar_categories` VALUES (6,'holidays','Holidays','#8663ba','Clinic holiday',0,NULL,'a:5:{s:17:"event_repeat_freq";s:1:"1";s:22:"event_repeat_freq_type";s:1:"4";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}',0,86400,1,3,2,0,0,2,1,6,'encounters|notes'); -INSERT INTO `openemr_postcalendar_categories` VALUES (7,'closed','Closed','#2374ab','Clinic closed',0,NULL,'a:5:{s:17:"event_repeat_freq";s:1:"1";s:22:"event_repeat_freq_type";s:1:"4";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}',0,86400,1,3,2,0,0,2,1,7,'encounters|notes'); -INSERT INTO `openemr_postcalendar_categories` VALUES (8,'lunch', 'Lunch', '#ffd351', 'Lunch', 1, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"1";s:22:"event_repeat_freq_type";s:1:"4";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 3600, 0, 3, 2, 0, 0, 1,1,8,'encounters|notes'); -INSERT INTO `openemr_postcalendar_categories` VALUES (9,'established_patient', 'Established Patient', '#93d3a2', '', 0, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"0";s:22:"event_repeat_freq_type";s:1:"0";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 900, 0, 0, 0, 0, 0, 0,1,9,'encounters|notes'); -INSERT INTO `openemr_postcalendar_categories` VALUES (10,'new_patient','New Patient', '#a2d9e2', '', 0, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"0";s:22:"event_repeat_freq_type";s:1:"0";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 1800, 0, 0, 0, 0, 0, 0,1,10,'encounters|notes'); -INSERT INTO `openemr_postcalendar_categories` VALUES (11,'reserved','Reserved','#b02a37','Reserved',1,NULL,'a:5:{s:17:\"event_repeat_freq\";s:1:\"1\";s:22:\"event_repeat_freq_type\";s:1:\"4\";s:19:\"event_repeat_on_num\";s:1:\"1\";s:19:\"event_repeat_on_day\";s:1:\"0\";s:20:\"event_repeat_on_freq\";s:1:\"0\";}',0,900,0,3,2,0,0, 1,1,11,'encounters|notes'); -INSERT INTO `openemr_postcalendar_categories` VALUES (12,'health_and_behavioral_assessment', 'Health and Behavioral Assessment', '#ced4da', 'Health and Behavioral Assessment', 0, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"0";s:22:"event_repeat_freq_type";s:1:"0";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 900, 0, 0, 0, 0, 0,0,1,12,'encounters|notes'); -INSERT INTO `openemr_postcalendar_categories` VALUES (13,'preventive_care_services', 'Preventive Care Services', '#d3c6ec', 'Preventive Care Services', 0, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"0";s:22:"event_repeat_freq_type";s:1:"0";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 900, 0, 0, 0, 0, 0,0,1,13,'encounters|notes'); -INSERT INTO `openemr_postcalendar_categories` VALUES (14,'ophthalmological_services', 'Ophthalmological Services', '#febe89', 'Ophthalmological Services', 0, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"0";s:22:"event_repeat_freq_type";s:1:"0";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 900, 0, 0, 0, 0, 0,0,1,14,'encounters|notes'); -INSERT INTO `openemr_postcalendar_categories` VALUES (15,'group_therapy', 'Group Therapy' , '#adb5bd' , 'Group Therapy', 0, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"0";s:22:"event_repeat_freq_type";s:1:"0";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 3600, 0, 0, 0, 0, 0, 3, 1, 15,'encounters|notes'); + +INSERT INTO `openemr_postcalendar_categories`(`pc_catid`, `pc_constant_id`, `pc_catname`, `pc_catcolor`, `pc_catdesc`, `pc_recurrtype`, `pc_enddate`, `pc_recurrspec`, `pc_recurrfreq`, `pc_duration`, `pc_end_date_flag`, `pc_end_date_type`, `pc_end_date_freq`, `pc_end_all_day`, `pc_dailylimit`, `pc_cattype`, `pc_active`, `pc_seq`, `aco_spec`) + VALUES (1,'no_show', 'No Show', '#dee2e6', 'Reserved to define when an event did not occur as specified.', 0, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"0";s:22:"event_repeat_freq_type";s:1:"0";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 0, 0, 0, 0, 0, 0, 0,1,1,'encounters|notes'); +INSERT INTO `openemr_postcalendar_categories`(`pc_catid`, `pc_constant_id`, `pc_catname`, `pc_catcolor`, `pc_catdesc`, `pc_recurrtype`, `pc_enddate`, `pc_recurrspec`, `pc_recurrfreq`, `pc_duration`, `pc_end_date_flag`, `pc_end_date_type`, `pc_end_date_freq`, `pc_end_all_day`, `pc_dailylimit`, `pc_cattype`, `pc_active`, `pc_seq`, `aco_spec`) + VALUES (2,'in_office', 'In Office', '#cce5ff', 'Reserved todefine when a provider may haveavailable appointments after.', 1, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"1";s:22:"event_repeat_freq_type";s:1:"4";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 0, 1, 3, 2, 0, 0, 1,1,2,'encounters|notes'); +INSERT INTO `openemr_postcalendar_categories`(`pc_catid`, `pc_constant_id`, `pc_catname`, `pc_catcolor`, `pc_catdesc`, `pc_recurrtype`, `pc_enddate`, `pc_recurrspec`, `pc_recurrfreq`, `pc_duration`, `pc_end_date_flag`, `pc_end_date_type`, `pc_end_date_freq`, `pc_end_all_day`, `pc_dailylimit`, `pc_cattype`, `pc_active`, `pc_seq`, `aco_spec`) + VALUES (3,'out_of_office', 'Out Of Office', '#fdb172', 'Reserved to define when a provider may not have available appointments after.', 1, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"1";s:22:"event_repeat_freq_type";s:1:"4";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 0, 1, 3, 2, 0, 0, 1,1,3,'encounters|notes'); +INSERT INTO `openemr_postcalendar_categories`(`pc_catid`, `pc_constant_id`, `pc_catname`, `pc_catcolor`, `pc_catdesc`, `pc_recurrtype`, `pc_enddate`, `pc_recurrspec`, `pc_recurrfreq`, `pc_duration`, `pc_end_date_flag`, `pc_end_date_type`, `pc_end_date_freq`, `pc_end_all_day`, `pc_dailylimit`, `pc_cattype`, `pc_active`, `pc_seq`, `aco_spec`) + VALUES (4,'vacation', 'Vacation', '#e9ecef', 'Reserved for use to define Scheduled Vacation Time', 0, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"0";s:22:"event_repeat_freq_type";s:1:"0";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 0, 0, 0, 0, 1, 0, 1,1,4,'encounters|notes'); +INSERT INTO `openemr_postcalendar_categories`(`pc_catid`, `pc_constant_id`, `pc_catname`, `pc_catcolor`, `pc_catdesc`, `pc_recurrtype`, `pc_enddate`, `pc_recurrspec`, `pc_recurrfreq`, `pc_duration`, `pc_end_date_flag`, `pc_end_date_type`, `pc_end_date_freq`, `pc_end_all_day`, `pc_dailylimit`, `pc_cattype`, `pc_active`, `pc_seq`, `aco_spec`) + VALUES (5,'office_visit', 'Office Visit', '#ffecb4', 'Normal Office Visit', 0, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"0";s:22:"event_repeat_freq_type";s:1:"0";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 900, 0, 0, 0, 0, 0,0,1,5,'encounters|notes'); +INSERT INTO `openemr_postcalendar_categories`(`pc_catid`, `pc_constant_id`, `pc_catname`, `pc_catcolor`, `pc_catdesc`, `pc_recurrtype`, `pc_enddate`, `pc_recurrspec`, `pc_recurrfreq`, `pc_duration`, `pc_end_date_flag`, `pc_end_date_type`, `pc_end_date_freq`, `pc_end_all_day`, `pc_dailylimit`, `pc_cattype`, `pc_active`, `pc_seq`, `aco_spec`) + VALUES (6,'holidays','Holidays','#8663ba','Clinic holiday',0,NULL,'a:5:{s:17:"event_repeat_freq";s:1:"1";s:22:"event_repeat_freq_type";s:1:"4";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}',0,86400,1,3,2,0,0,2,1,6,'encounters|notes'); +INSERT INTO `openemr_postcalendar_categories`(`pc_catid`, `pc_constant_id`, `pc_catname`, `pc_catcolor`, `pc_catdesc`, `pc_recurrtype`, `pc_enddate`, `pc_recurrspec`, `pc_recurrfreq`, `pc_duration`, `pc_end_date_flag`, `pc_end_date_type`, `pc_end_date_freq`, `pc_end_all_day`, `pc_dailylimit`, `pc_cattype`, `pc_active`, `pc_seq`, `aco_spec`) + VALUES (7,'closed','Closed','#2374ab','Clinic closed',0,NULL,'a:5:{s:17:"event_repeat_freq";s:1:"1";s:22:"event_repeat_freq_type";s:1:"4";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}',0,86400,1,3,2,0,0,2,1,7,'encounters|notes'); +INSERT INTO `openemr_postcalendar_categories`(`pc_catid`, `pc_constant_id`, `pc_catname`, `pc_catcolor`, `pc_catdesc`, `pc_recurrtype`, `pc_enddate`, `pc_recurrspec`, `pc_recurrfreq`, `pc_duration`, `pc_end_date_flag`, `pc_end_date_type`, `pc_end_date_freq`, `pc_end_all_day`, `pc_dailylimit`, `pc_cattype`, `pc_active`, `pc_seq`, `aco_spec`) + VALUES (8,'lunch', 'Lunch', '#ffd351', 'Lunch', 1, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"1";s:22:"event_repeat_freq_type";s:1:"4";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 3600, 0, 3, 2, 0, 0, 1,1,8,'encounters|notes'); +INSERT INTO `openemr_postcalendar_categories`(`pc_catid`, `pc_constant_id`, `pc_catname`, `pc_catcolor`, `pc_catdesc`, `pc_recurrtype`, `pc_enddate`, `pc_recurrspec`, `pc_recurrfreq`, `pc_duration`, `pc_end_date_flag`, `pc_end_date_type`, `pc_end_date_freq`, `pc_end_all_day`, `pc_dailylimit`, `pc_cattype`, `pc_active`, `pc_seq`, `aco_spec`) + VALUES (9,'established_patient', 'Established Patient', '#93d3a2', '', 0, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"0";s:22:"event_repeat_freq_type";s:1:"0";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 900, 0, 0, 0, 0, 0, 0,1,9,'encounters|notes'); +INSERT INTO `openemr_postcalendar_categories`(`pc_catid`, `pc_constant_id`, `pc_catname`, `pc_catcolor`, `pc_catdesc`, `pc_recurrtype`, `pc_enddate`, `pc_recurrspec`, `pc_recurrfreq`, `pc_duration`, `pc_end_date_flag`, `pc_end_date_type`, `pc_end_date_freq`, `pc_end_all_day`, `pc_dailylimit`, `pc_cattype`, `pc_active`, `pc_seq`, `aco_spec`) + VALUES (10,'new_patient','New Patient', '#a2d9e2', '', 0, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"0";s:22:"event_repeat_freq_type";s:1:"0";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 1800, 0, 0, 0, 0, 0, 0,1,10,'encounters|notes'); +INSERT INTO `openemr_postcalendar_categories`(`pc_catid`, `pc_constant_id`, `pc_catname`, `pc_catcolor`, `pc_catdesc`, `pc_recurrtype`, `pc_enddate`, `pc_recurrspec`, `pc_recurrfreq`, `pc_duration`, `pc_end_date_flag`, `pc_end_date_type`, `pc_end_date_freq`, `pc_end_all_day`, `pc_dailylimit`, `pc_cattype`, `pc_active`, `pc_seq`, `aco_spec`) + VALUES (11,'reserved','Reserved','#b02a37','Reserved',1,NULL,'a:5:{s:17:\"event_repeat_freq\";s:1:\"1\";s:22:\"event_repeat_freq_type\";s:1:\"4\";s:19:\"event_repeat_on_num\";s:1:\"1\";s:19:\"event_repeat_on_day\";s:1:\"0\";s:20:\"event_repeat_on_freq\";s:1:\"0\";}',0,900,0,3,2,0,0, 1,1,11,'encounters|notes'); +INSERT INTO `openemr_postcalendar_categories`(`pc_catid`, `pc_constant_id`, `pc_catname`, `pc_catcolor`, `pc_catdesc`, `pc_recurrtype`, `pc_enddate`, `pc_recurrspec`, `pc_recurrfreq`, `pc_duration`, `pc_end_date_flag`, `pc_end_date_type`, `pc_end_date_freq`, `pc_end_all_day`, `pc_dailylimit`, `pc_cattype`, `pc_active`, `pc_seq`, `aco_spec`) + VALUES (12,'health_and_behavioral_assessment', 'Health and Behavioral Assessment', '#ced4da', 'Health and Behavioral Assessment', 0, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"0";s:22:"event_repeat_freq_type";s:1:"0";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 900, 0, 0, 0, 0, 0,0,1,12,'encounters|notes'); +INSERT INTO `openemr_postcalendar_categories`(`pc_catid`, `pc_constant_id`, `pc_catname`, `pc_catcolor`, `pc_catdesc`, `pc_recurrtype`, `pc_enddate`, `pc_recurrspec`, `pc_recurrfreq`, `pc_duration`, `pc_end_date_flag`, `pc_end_date_type`, `pc_end_date_freq`, `pc_end_all_day`, `pc_dailylimit`, `pc_cattype`, `pc_active`, `pc_seq`, `aco_spec`) + VALUES (13,'preventive_care_services', 'Preventive Care Services', '#d3c6ec', 'Preventive Care Services', 0, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"0";s:22:"event_repeat_freq_type";s:1:"0";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 900, 0, 0, 0, 0, 0,0,1,13,'encounters|notes'); +INSERT INTO `openemr_postcalendar_categories`(`pc_catid`, `pc_constant_id`, `pc_catname`, `pc_catcolor`, `pc_catdesc`, `pc_recurrtype`, `pc_enddate`, `pc_recurrspec`, `pc_recurrfreq`, `pc_duration`, `pc_end_date_flag`, `pc_end_date_type`, `pc_end_date_freq`, `pc_end_all_day`, `pc_dailylimit`, `pc_cattype`, `pc_active`, `pc_seq`, `aco_spec`) + VALUES (14,'ophthalmological_services', 'Ophthalmological Services', '#febe89', 'Ophthalmological Services', 0, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"0";s:22:"event_repeat_freq_type";s:1:"0";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 900, 0, 0, 0, 0, 0,0,1,14,'encounters|notes'); +INSERT INTO `openemr_postcalendar_categories`(`pc_catid`, `pc_constant_id`, `pc_catname`, `pc_catcolor`, `pc_catdesc`, `pc_recurrtype`, `pc_enddate`, `pc_recurrspec`, `pc_recurrfreq`, `pc_duration`, `pc_end_date_flag`, `pc_end_date_type`, `pc_end_date_freq`, `pc_end_all_day`, `pc_dailylimit`, `pc_cattype`, `pc_active`, `pc_seq`, `aco_spec`) + VALUES (15,'group_therapy', 'Group Therapy' , '#adb5bd' , 'Group Therapy', 0, NULL, 'a:5:{s:17:"event_repeat_freq";s:1:"0";s:22:"event_repeat_freq_type";s:1:"0";s:19:"event_repeat_on_num";s:1:"1";s:19:"event_repeat_on_day";s:1:"0";s:20:"event_repeat_on_freq";s:1:"0";}', 0, 3600, 0, 0, 0, 0, 0, 3, 1, 15,'encounters|notes'); -- -------------------------------------------------------- @@ -7521,6 +7549,7 @@ CREATE TABLE `patient_data` ( `updated_by` BIGINT(20) DEFAULT NULL COMMENT 'users.id the user that last modified this record', `preferred_name` TINYTEXT, `nationality_country` TINYTEXT, + `last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY `pid` (`pid`), UNIQUE KEY `uuid` (`uuid`), KEY `id` (`id`) @@ -8895,6 +8924,8 @@ CREATE TABLE `users` ( `supervisor_id` int(11) NOT NULL DEFAULT '0', `billing_facility` TEXT, `billing_facility_id` INT(11) NOT NULL DEFAULT '0', + `date_created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uuid` (`uuid`), KEY `abook_type` (`abook_type`) @@ -9343,6 +9374,8 @@ CREATE TABLE `procedure_providers` ( `lab_director` bigint(20) NOT NULL DEFAULT '0', `active` tinyint(1) NOT NULL DEFAULT '1', `type` varchar(31) DEFAULT NULL, + `date_created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`ppid`), UNIQUE KEY `uuid` (`uuid`) ) ENGINE=InnoDB; diff --git a/src/FHIR/Export/ExportJob.php b/src/FHIR/Export/ExportJob.php index 0c771ca61e1..87eeec842f7 100644 --- a/src/FHIR/Export/ExportJob.php +++ b/src/FHIR/Export/ExportJob.php @@ -22,6 +22,8 @@ use http\Exception\InvalidArgumentException; use OpenEMR\Common\Uuid\UuidRegistry; +use OpenEMR\Services\Search\SearchComparator; +use OpenEMR\Services\Utils\DateFormatterUtils; class ExportJob { @@ -210,6 +212,16 @@ public function getResourceIncludeTime(): \DateTime return $this->resourceIncludeTime; } + public function getResourceIncludeSearchParamValue() + { + return SearchComparator::GREATER_THAN_OR_EQUAL_TO . $this->getResourceIncludeISO8601Date(); + } + + public function getResourceIncludeISO8601Date(): string + { + return DateFormatterUtils::getFormattedISO8601DateFromDateTime($this->resourceIncludeTime); + } + /** * @param \DateTime $resourceIncludeTime */ diff --git a/src/RestControllers/AuthorizationController.php b/src/RestControllers/AuthorizationController.php index ce7c95e79cc..b0c4349358d 100644 --- a/src/RestControllers/AuthorizationController.php +++ b/src/RestControllers/AuthorizationController.php @@ -90,6 +90,9 @@ class AuthorizationController public const GRANT_TYPE_CLIENT_CREDENTIALS = 'client_credentials'; public const OFFLINE_ACCESS_SCOPE = 'offline_access'; + // https://hl7.org/fhir/uv/bulkdata/authorization/index.html#issuing-access-tokens Spec states 5 min max + public const GRANT_TYPE_ACCESS_CODE_TTL = "PT300S"; // 5 minutes + public $authBaseUrl; public $authBaseFullUrl; public $siteId; @@ -642,8 +645,7 @@ public function getAuthorizationServer($includeAuthGrantRefreshToken = true): Au $client_credentials->setHttpClient(new Client()); // set our guzzle client here $authServer->enableGrantType( $client_credentials, - // https://hl7.org/fhir/uv/bulkdata/authorization/index.html#issuing-access-tokens Spec states 5 min max - new \DateInterval('PT300S') + new \DateInterval(self::GRANT_TYPE_ACCESS_CODE_TTL) ); } diff --git a/src/RestControllers/FHIR/Operations/FhirOperationExportRestController.php b/src/RestControllers/FHIR/Operations/FhirOperationExportRestController.php index f1377855296..78bfae45d41 100644 --- a/src/RestControllers/FHIR/Operations/FhirOperationExportRestController.php +++ b/src/RestControllers/FHIR/Operations/FhirOperationExportRestController.php @@ -23,6 +23,8 @@ use OpenEMR\Services\FHIR\IFhirExportableResourceService; use OpenEMR\Services\FHIR\Utils\FhirServiceLocator; use OpenEMR\Services\FHIR\UtilsService; +use OpenEMR\Services\Search\DateSearchField; +use OpenEMR\Services\Search\SearchFieldComparableValue; use Psr\Http\Message\ResponseInterface; use Psr\Log\LoggerInterface; use Ramsey\Uuid\Uuid; @@ -104,7 +106,11 @@ public function processExport($exportParams, $exportType, $acceptHeader, $prefer } $outputFormat = $exportParams['_outputFormat'] ?? ExportJob::OUTPUT_FORMAT_FHIR_NDJSON; - $since = $exportParams['_since'] ?? new \DateTime(date("Y-m-d H:i:s", 0)); // since epoch time + if (!empty($exportParams['_since'])) { + $since = $this->parseFHIRInstant($exportParams['_since']); + } else { + $since = new \DateTime(date(\DateTimeInterface::ATOM, 0)); // since epoch time + } $type = $exportParams['type'] ?? ''; $groupId = $exportParams['groupId'] ?? null; $resources = !empty($type) ? explode(",", $type) : []; @@ -624,4 +630,19 @@ private function getPatientUuidsForGroup($groupId) } return $patientUuids; } + + private function parseFHIRInstant(string $_since) + { + // concievably they could send us a date that is not an actual INSTANCE of a date, but we'll just convert it + // to a regular date anyways, if the format is invalid DateSearchField will error out. + $dateField = new DateSearchField("_since", [$_since], DateSearchField::DATE_TYPE_DATETIME); + $values = $dateField->getValues(); + $comparable = reset($values); + $value = $comparable->getValue(); + if ($value instanceof \DatePeriod) { + return $value->getStartDate(); + } else { + throw new \InvalidArgumentException("Invalid date format for _since parameter"); + } + } } diff --git a/src/Services/AppointmentService.php b/src/Services/AppointmentService.php index c6d9e62b584..1e7776877ba 100644 --- a/src/Services/AppointmentService.php +++ b/src/Services/AppointmentService.php @@ -475,7 +475,7 @@ public function deleteAppointmentRecord($eid) */ public function getCalendarCategories() { - $sql = "SELECT pc_catid, pc_constant_id, pc_catname, pc_cattype,aco_spec FROM openemr_postcalendar_categories " + $sql = "SELECT pc_catid, pc_constant_id, pc_catname, pc_cattype,aco_spec, pc_last_updated FROM openemr_postcalendar_categories " . " WHERE pc_active = 1 ORDER BY pc_seq"; return QueryUtils::fetchRecords($sql); } @@ -656,4 +656,19 @@ public function getOneCalendarCategory($cat_id) $sql = "SELECT * FROM openemr_postcalendar_categories WHERE pc_catid = ?"; return QueryUtils::fetchRecords($sql, [$cat_id]); } + + public function searchCalendarCategories(array $oeSearchParameters) + { + $sql = "SELECT * FROM openemr_postcalendar_categories "; + $whereClause = FhirSearchWhereClauseBuilder::build($oeSearchParameters, true); + $sql .= $whereClause->getFragment(); + $sqlBindArray = $whereClause->getBoundValues(); + $records = QueryUtils::fetchRecords($sql, $sqlBindArray); + $processingResult = new ProcessingResult(); + if (!empty($records)) { + $processingResult->setData($records); + } + // TODO: look at handling offset and limit here + return $processingResult; + } } diff --git a/src/Services/CareTeamService.php b/src/Services/CareTeamService.php index e3d44d0729d..4c1e9d56cd2 100644 --- a/src/Services/CareTeamService.php +++ b/src/Services/CareTeamService.php @@ -50,6 +50,7 @@ public function search($search, $isAndCondition = true) careteam_mapping.care_team_provider as providers, careteam_mapping.care_team_facility as facilities, careteam_mapping.care_team_status, + careteam_mapping.date, care_team_status_title FROM ( SELECT @@ -58,6 +59,7 @@ public function search($search, $isAndCondition = true) ,patient_data.care_team_provider ,patient_data.care_team_facility ,patient_data.care_team_status + ,patient_data.date FROM uuid_mapping -- we join on this to make sure we've got data integrity since we don't actually use foreign keys right now @@ -72,6 +74,7 @@ public function search($search, $isAndCondition = true) ,patient_history.care_team_provider ,patient_history.care_team_facility ,'inactive' AS care_team_status + ,patient_history.date FROM patient_history JOIN patient_data ON patient_history.pid = patient_data.pid diff --git a/src/Services/ClinicalNotesService.php b/src/Services/ClinicalNotesService.php index 1c6c4345a69..9c61653bafd 100644 --- a/src/Services/ClinicalNotesService.php +++ b/src/Services/ClinicalNotesService.php @@ -74,6 +74,8 @@ public function search($search, $isAndCondition = true) ,notes.clinical_notes_type ,notes.note_related_to ,notes.clinical_notes_category + ,notes.last_updated + ,forms.date_created ,lo_category.category_code ,lo_category.category_title ,patients.pid @@ -100,6 +102,7 @@ public function search($search, $isAndCondition = true) ,note_related_to ,clinical_notes_category ,form_id + ,last_updated ,user FROM form_clinical_notes @@ -109,6 +112,7 @@ public function search($search, $isAndCondition = true) id AS form_id, encounter ,pid AS form_pid + ,`date` AS date_created FROM forms ) forms ON forms.form_id = notes.form_id diff --git a/src/Services/ConditionService.php b/src/Services/ConditionService.php index a3fd1bf37f6..a1423c36823 100644 --- a/src/Services/ConditionService.php +++ b/src/Services/ConditionService.php @@ -48,6 +48,7 @@ public function search($search, $isAndCondition = true) patient.puuid, patient.patient_uuid, condition_ids.condition_uuid, + condition_ids.last_updated_time, verification.title as verification_title ,provider.provider_id ,provider.provider_npi @@ -55,7 +56,7 @@ public function search($search, $isAndCondition = true) ,provider.provider_username FROM lists INNER JOIN ( - SELECT lists.uuid AS condition_uuid FROM lists + SELECT lists.uuid AS condition_uuid, lists.modifydate as last_updated_time FROM lists ) condition_ids ON lists.uuid = condition_ids.condition_uuid LEFT JOIN list_options as verification ON verification.option_id = lists.verification and verification.list_id = 'condition-verification' RIGHT JOIN ( @@ -68,7 +69,7 @@ public function search($search, $isAndCondition = true) LEFT JOIN issue_encounter as issue ON issue.list_id =lists.id LEFT JOIN form_encounter as encounter ON encounter.encounter =issue.encounter LEFT JOIN ( - select + select id AS provider_id ,uuid AS provider_uuid ,npi AS provider_npi diff --git a/src/Services/DeviceService.php b/src/Services/DeviceService.php index b2e5540868a..e8bf46077af 100644 --- a/src/Services/DeviceService.php +++ b/src/Services/DeviceService.php @@ -38,7 +38,7 @@ public function search($search, $isAndCondition = true) ( SELECT `udi`, - `uuid`, `date`, `title`,`udi_data`, `begdate`, `diagnosis`, `user`, `pid` + `uuid`, `date`, `title`,`udi_data`, `begdate`, `diagnosis`, `user`, `pid`,modifydate FROM lists WHERE `type` = 'medical_device' ) l JOIN ( @@ -46,7 +46,7 @@ public function search($search, $isAndCondition = true) from patient_data ) patients ON l.pid = patients.pid LEFT JOIN ( - select + select id AS provider_id ,npi AS provider_npi ,uuid AS provider_uuid diff --git a/src/Services/DrugService.php b/src/Services/DrugService.php index ee72ba86132..34ff7567fda 100644 --- a/src/Services/DrugService.php +++ b/src/Services/DrugService.php @@ -94,29 +94,49 @@ public function getOne($uuid) public function search($search, $isAndCondition = true) { - $sql = "SELECT drugs.drug_id, - uuid, - name, - ndc_number, - form, - size, - unit, - route, - related_code, - active, - drug_code, + $sql = "SELECT + drug_table.drug_id, + drug_table.uuid, + drug_table.name, + drug_table.ndc_number, + drug_table.form, + drug_table.size, + drug_table.unit, + drug_table.route, + drug_table.related_code, + drug_table.active, + drug_table.drug_code, IF(drug_prescriptions.rxnorm_drugcode!='' ,drug_prescriptions.rxnorm_drugcode - ,IF(drug_code IS NULL, '', concat('RXCUI:',drug_code)) + ,IF(drug_table.drug_code IS NULL, '', drug_table.drug_code) ) AS 'rxnorm_drugcode', drug_inventory.manufacturer, drug_inventory.lot_number, - drug_inventory.expiration - FROM drugs + drug_inventory.expiration, + drug_table.drug_last_updated, + drug_table.drug_date_created + FROM ( + select + drug_id, + uuid, + name, + ndc_number, + form, + size, + unit, + route, + related_code, + active, + drug_code, + last_updated AS drug_last_updated, + date_created AS drug_date_created + FROM + drugs + ) drug_table LEFT JOIN drug_inventory - ON drugs.drug_id = drug_inventory.drug_id + ON drug_table.drug_id = drug_inventory.drug_id LEFT JOIN ( - select + select uuid AS prescription_uuid ,rxnorm_drugcode ,drug_id @@ -124,7 +144,7 @@ public function search($search, $isAndCondition = true) FROM prescriptions ) drug_prescriptions - ON drug_prescriptions.drug_id = drugs.drug_id + ON drug_prescriptions.drug_id = drug_table.drug_id LEFT JOIN ( select uuid AS puuid ,pid @@ -161,7 +181,14 @@ protected function createResultRecordFromDatabaseResult($row) $record = parent::createResultRecordFromDatabaseResult($row); if ($record['rxnorm_drugcode'] != "") { - $codes = $this->addCoding($row['rxnorm_drugcode']); + // removed the RXCUI concatenation out of the db query and into the code here + // some parts of OpenEMR adds the RXCUI designation in the drug_code such as the inventory/dispensary module + // and this causes the FHIR medication resource to not get the actual RXCUI code. + if ($row['drug_code'] == $record['rxnorm_drugcode'] && strpos($row['drug_code'], ':') === false) { + $codes = $this->addCoding("RXCUI:" . $row['drug_code']); + } else { + $codes = $this->addCoding($row['rxnorm_drugcode']); + } $updatedCodes = []; foreach ($codes as $code => $codeValues) { if (empty($codeValues['description'])) { @@ -173,6 +200,7 @@ protected function createResultRecordFromDatabaseResult($row) $record['drug_code'] = $updatedCodes; } + // TODO: @adunsulag this looks odd... why modify the original row...? look at removing this. if ($row['rxnorm_drugcode'] != "") { $row['drug_code'] = $this->addCoding($row['drug_code']); } diff --git a/src/Services/FHIR/DiagnosticReport/FhirDiagnosticReportClinicalNotesService.php b/src/Services/FHIR/DiagnosticReport/FhirDiagnosticReportClinicalNotesService.php index 2ac98de8718..55da1778b69 100644 --- a/src/Services/FHIR/DiagnosticReport/FhirDiagnosticReportClinicalNotesService.php +++ b/src/Services/FHIR/DiagnosticReport/FhirDiagnosticReportClinicalNotesService.php @@ -31,6 +31,7 @@ use OpenEMR\Services\FHIR\UtilsService; use OpenEMR\Services\ListService; use OpenEMR\Services\Search\FhirSearchParameterDefinition; +use OpenEMR\Services\Search\ISearchField; use OpenEMR\Services\Search\SearchFieldType; use OpenEMR\Services\Search\SearchModifier; use OpenEMR\Services\Search\ServiceField; @@ -65,9 +66,15 @@ protected function loadSearchParameters() 'category' => new FhirSearchParameterDefinition('category', SearchFieldType::TOKEN, ['category_code']), 'date' => new FhirSearchParameterDefinition('date', SearchFieldType::DATETIME, ['date']), '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField(), ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['last_updated']); + } + public function supportsCategory($category) { $loincCategory = "LOINC:" . $category; @@ -85,10 +92,14 @@ public function supportsCode($code) public function parseOpenEMRRecord($dataRecord = array(), $encode = false) { $report = new FHIRDiagnosticReport(); - $meta = new FHIRMeta(); - $meta->setVersionId('1'); - $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); - $report->setMeta($meta); + $fhirMeta = new FHIRMeta(); + $fhirMeta->setVersionId('1'); + if (!empty($dataRecord['last_updated'])) { + $fhirMeta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['last_updated'])); + } else { + $fhirMeta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } + $report->setMeta($fhirMeta); $id = new FHIRId(); $id->setValue($dataRecord['uuid']); diff --git a/src/Services/FHIR/DiagnosticReport/FhirDiagnosticReportLaboratoryService.php b/src/Services/FHIR/DiagnosticReport/FhirDiagnosticReportLaboratoryService.php index 393e8bc696e..ddfcb5c581c 100644 --- a/src/Services/FHIR/DiagnosticReport/FhirDiagnosticReportLaboratoryService.php +++ b/src/Services/FHIR/DiagnosticReport/FhirDiagnosticReportLaboratoryService.php @@ -29,6 +29,7 @@ use OpenEMR\Services\FHIR\UtilsService; use OpenEMR\Services\ProcedureService; use OpenEMR\Services\Search\FhirSearchParameterDefinition; +use OpenEMR\Services\Search\ISearchField; use OpenEMR\Services\Search\SearchFieldType; use OpenEMR\Services\Search\SearchModifier; use OpenEMR\Services\Search\ServiceField; @@ -71,9 +72,15 @@ protected function loadSearchParameters() 'category' => new FhirSearchParameterDefinition('category', SearchFieldType::TOKEN, ['category']), 'date' => new FhirSearchParameterDefinition('date', SearchFieldType::DATETIME, ['report_date']), '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('report_uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField(), ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['report_date']); + } + public function supportsCategory($category) { return $category === self::LAB_CATEGORY; @@ -91,14 +98,15 @@ public function supportsCode($code) public function parseOpenEMRRecord($dataRecord = array(), $encode = false) { $report = new FHIRDiagnosticReport(); - $meta = new FHIRMeta(); - $meta->setVersionId('1'); - $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); - $report->setMeta($meta); - - $dataRecordReport = array_pop($dataRecord['reports']); - + $fhirMeta = new FHIRMeta(); + $fhirMeta->setVersionId('1'); + if (!empty($dataRecordReport['date'])) { + $fhirMeta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecordReport['date'])); + } else { + $fhirMeta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } + $report->setMeta($fhirMeta); $id = new FHIRId(); $id->setValue($dataRecordReport['uuid']); diff --git a/src/Services/FHIR/DocumentReference/FhirClinicalNotesService.php b/src/Services/FHIR/DocumentReference/FhirClinicalNotesService.php index 2b71c4183dc..dcd70bfd5cb 100644 --- a/src/Services/FHIR/DocumentReference/FhirClinicalNotesService.php +++ b/src/Services/FHIR/DocumentReference/FhirClinicalNotesService.php @@ -36,6 +36,7 @@ use OpenEMR\Services\FHIR\Traits\PatientSearchTrait; use OpenEMR\Services\FHIR\UtilsService; use OpenEMR\Services\Search\FhirSearchParameterDefinition; +use OpenEMR\Services\Search\ISearchField; use OpenEMR\Services\Search\SearchFieldType; use OpenEMR\Services\Search\SearchModifier; use OpenEMR\Services\Search\ServiceField; @@ -85,16 +86,26 @@ protected function loadSearchParameters() 'category' => new FhirSearchParameterDefinition('category', SearchFieldType::TOKEN, ['category']), 'date' => new FhirSearchParameterDefinition('date', SearchFieldType::DATE, ['date']), '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField(), ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['date']); + } + public function parseOpenEMRRecord($dataRecord = array(), $encode = false) { $docReference = new FHIRDocumentReference(); - $meta = new FHIRMeta(); - $meta->setVersionId('1'); - $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); - $docReference->setMeta($meta); + $fhirMeta = new FHIRMeta(); + $fhirMeta->setVersionId('1'); + if (!empty($dataRecord['date'])) { + $fhirMeta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['date'])); + } else { + $fhirMeta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } + $docReference->setMeta($fhirMeta); $id = new FHIRId(); $id->setValue($dataRecord['uuid']); diff --git a/src/Services/FHIR/DocumentReference/FhirPatientDocumentReferenceService.php b/src/Services/FHIR/DocumentReference/FhirPatientDocumentReferenceService.php index 485b0f8b3ca..9271d883d9d 100644 --- a/src/Services/FHIR/DocumentReference/FhirPatientDocumentReferenceService.php +++ b/src/Services/FHIR/DocumentReference/FhirPatientDocumentReferenceService.php @@ -31,6 +31,7 @@ use OpenEMR\Services\FHIR\Traits\PatientSearchTrait; use OpenEMR\Services\FHIR\UtilsService; use OpenEMR\Services\Search\FhirSearchParameterDefinition; +use OpenEMR\Services\Search\ISearchField; use OpenEMR\Services\Search\SearchFieldType; use OpenEMR\Services\Search\SearchModifier; use OpenEMR\Services\Search\ServiceField; @@ -74,9 +75,15 @@ protected function loadSearchParameters() 'patient' => $this->getPatientContextSearchField(), 'date' => new FhirSearchParameterDefinition('date', SearchFieldType::DATETIME, ['date']), '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField(), ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['date']); + } + protected function searchForOpenEMRRecords($openEMRSearchParameters): ProcessingResult { if (isset($openEMRSearchParameters['category'])) { @@ -99,10 +106,14 @@ protected function searchForOpenEMRRecords($openEMRSearchParameters): Processing public function parseOpenEMRRecord($dataRecord = array(), $encode = false) { $docReference = new FHIRDocumentReference(); - $meta = new FHIRMeta(); - $meta->setVersionId('1'); - $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); - $docReference->setMeta($meta); + $fhirMeta = new FHIRMeta(); + $fhirMeta->setVersionId('1'); + if (!empty($dataRecord['date'])) { + $fhirMeta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['date'])); + } else { + $fhirMeta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } + $docReference->setMeta($fhirMeta); $id = new FHIRId(); $id->setValue($dataRecord['uuid']); diff --git a/src/Services/FHIR/FhirAllergyIntoleranceService.php b/src/Services/FHIR/FhirAllergyIntoleranceService.php index 3fdc04ad0ff..717710fc722 100644 --- a/src/Services/FHIR/FhirAllergyIntoleranceService.php +++ b/src/Services/FHIR/FhirAllergyIntoleranceService.php @@ -30,6 +30,7 @@ use OpenEMR\Services\FHIR\Traits\FhirServiceBaseEmptyTrait; use OpenEMR\Services\PractitionerService; use OpenEMR\Services\Search\FhirSearchParameterDefinition; +use OpenEMR\Services\Search\ISearchField; use OpenEMR\Services\Search\ReferenceSearchValue; use OpenEMR\Services\Search\SearchFieldType; use OpenEMR\Services\Search\ServiceField; @@ -76,9 +77,14 @@ protected function loadSearchParameters() return [ 'patient' => $this->getPatientContextSearchField(), '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('allergy_uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField(), ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['modifydate']); + } /** * Parses an OpenEMR allergyIntolerance record, returning the equivalent FHIR AllergyIntolerance Resource @@ -113,7 +119,11 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) $allergyIntoleranceResource = new FHIRAllergyIntolerance(); $fhirMeta = new FHIRMeta(); $fhirMeta->setVersionId("1"); - $fhirMeta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + if (!empty($dataRecord['date'])) { + $fhirMeta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['modifydate'])); + } else { + $fhirMeta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } $allergyIntoleranceResource->setMeta($fhirMeta); $id = new FHIRId(); diff --git a/src/Services/FHIR/FhirAppointmentService.php b/src/Services/FHIR/FhirAppointmentService.php index 658c71544b1..2c07df49680 100644 --- a/src/Services/FHIR/FhirAppointmentService.php +++ b/src/Services/FHIR/FhirAppointmentService.php @@ -65,11 +65,16 @@ protected function loadSearchParameters() return [ 'patient' => $this->getPatientContextSearchField(), '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('pc_uuid', ServiceField::TYPE_UUID)]), - '_lastUpdated' => new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['pc_time']), 'date' => new FhirSearchParameterDefinition('date', SearchFieldType::DATE, ['pc_eventDate']), + '_lastUpdated' => $this->getLastModifiedSearchField(), ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['pc_time']); + } + /** * Parses an OpenEMR data record, returning the equivalent FHIR Resource * diff --git a/src/Services/FHIR/FhirCarePlanService.php b/src/Services/FHIR/FhirCarePlanService.php index ff6411e3f6f..1d70b3f2bab 100644 --- a/src/Services/FHIR/FhirCarePlanService.php +++ b/src/Services/FHIR/FhirCarePlanService.php @@ -23,6 +23,7 @@ use OpenEMR\Services\FHIR\Traits\FhirBulkExportDomainResourceTrait; use OpenEMR\Services\FHIR\Traits\FhirServiceBaseEmptyTrait; use OpenEMR\Services\Search\FhirSearchParameterDefinition; +use OpenEMR\Services\Search\ISearchField; use OpenEMR\Services\Search\SearchFieldType; use OpenEMR\Services\Search\ServiceField; use OpenEMR\Validators\ProcessingResult; @@ -57,9 +58,16 @@ protected function loadSearchParameters() 'category' => new FhirSearchParameterDefinition('status', SearchFieldType::TOKEN, ['careplan_category']), // note even though we label this as a uuid, it is a SURROGATE UID because of the nature of CarePlan '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, ['uuid']), + '_lastUpdated' => $this->getLastModifiedSearchField(), ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + // TODO: @adunsulag introduce a last_modified date field to the care plan table as we don't track this anywhere + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['creation_date']); + } + /** * Parses an OpenEMR record, returning the equivalent FHIR Resource * @@ -73,7 +81,11 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) $fhirMeta = new FHIRMeta(); $fhirMeta->setVersionId('1'); - $fhirMeta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + if (!empty($dataRecord['creation_date'])) { + $fhirMeta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['creation_date'])); + } else { + $fhirMeta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } $carePlanResource->setMeta($fhirMeta); $fhirId = new FHIRId(); diff --git a/src/Services/FHIR/FhirCareTeamService.php b/src/Services/FHIR/FhirCareTeamService.php index 46fcd3d084f..663cae8b8e6 100644 --- a/src/Services/FHIR/FhirCareTeamService.php +++ b/src/Services/FHIR/FhirCareTeamService.php @@ -24,6 +24,7 @@ use OpenEMR\Services\FHIR\Traits\FhirBulkExportDomainResourceTrait; use OpenEMR\Services\FHIR\Traits\FhirServiceBaseEmptyTrait; use OpenEMR\Services\Search\FhirSearchParameterDefinition; +use OpenEMR\Services\Search\ISearchField; use OpenEMR\Services\Search\SearchFieldType; use OpenEMR\Services\Search\ServiceField; use OpenEMR\Validators\ProcessingResult; @@ -67,9 +68,15 @@ protected function loadSearchParameters() 'patient' => $this->getPatientContextSearchField(), 'status' => new FhirSearchParameterDefinition('status', SearchFieldType::TOKEN, ['care_team_status']), '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField(), ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['date']); + } + /** * Parses an OpenEMR careTeam record, returning the equivalent FHIR CareTeam Resource * @@ -83,7 +90,11 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) $fhirMeta = new FHIRMeta(); $fhirMeta->setVersionId('1'); - $fhirMeta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + if (!empty($dataRecord['date'])) { + $fhirMeta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['date'])); + } else { + $fhirMeta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } $careTeamResource->setMeta($fhirMeta); $id = new FHIRId(); diff --git a/src/Services/FHIR/FhirConditionService.php b/src/Services/FHIR/FhirConditionService.php index 874485b5ab9..09e902751d1 100644 --- a/src/Services/FHIR/FhirConditionService.php +++ b/src/Services/FHIR/FhirConditionService.php @@ -14,6 +14,7 @@ use OpenEMR\Services\FHIR\Traits\FhirBulkExportDomainResourceTrait; use OpenEMR\Services\FHIR\Traits\FhirServiceBaseEmptyTrait; use OpenEMR\Services\Search\FhirSearchParameterDefinition; +use OpenEMR\Services\Search\ISearchField; use OpenEMR\Services\Search\SearchFieldType; use OpenEMR\Services\Search\ServiceField; use OpenEMR\Validators\ProcessingResult; @@ -58,9 +59,15 @@ protected function loadSearchParameters() return [ 'patient' => $this->getPatientContextSearchField(), '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('condition_uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField(), ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['last_updated_time']); + } + /** * Parses an OpenEMR condition record, returning the equivalent FHIR Condition Resource * @@ -74,7 +81,11 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) $meta = new FHIRMeta(); $meta->setVersionId('1'); - $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + if (!empty($dataRecord['last_updated_time'])) { + $meta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['last_updated_time'])); + } else { + $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } $conditionResource->setMeta($meta); $id = new FHIRId(); diff --git a/src/Services/FHIR/FhirCoverageService.php b/src/Services/FHIR/FhirCoverageService.php index 2d4483f8f9c..7ecd6030e87 100644 --- a/src/Services/FHIR/FhirCoverageService.php +++ b/src/Services/FHIR/FhirCoverageService.php @@ -6,6 +6,7 @@ use OpenEMR\FHIR\R4\FHIRElement\FHIRCoding; use OpenEMR\FHIR\R4\FHIRElement\FHIRCode; use OpenEMR\FHIR\R4\FHIRElement\FHIRId; +use OpenEMR\FHIR\R4\FHIRElement\FHIRMeta; use OpenEMR\FHIR\R4\FHIRElement\FHIRReference; use OpenEMR\FHIR\R4\FHIRDomainResource\FHIRCoverage; use OpenEMR\Services\FHIR\FhirServiceBase; @@ -13,6 +14,7 @@ use OpenEMR\Services\FHIR\Traits\FhirServiceBaseEmptyTrait; use OpenEMR\Services\InsuranceService; use OpenEMR\Services\Search\FhirSearchParameterDefinition; +use OpenEMR\Services\Search\ISearchField; use OpenEMR\Services\Search\SearchFieldType; use OpenEMR\Services\Search\ServiceField; use OpenEMR\Validators\ProcessingResult; @@ -53,10 +55,16 @@ protected function loadSearchParameters() return [ 'patient' => $this->getPatientContextSearchField(), 'payor' => new FhirSearchParameterDefinition('payor', SearchFieldType::TOKEN, ['provider']), - '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]) + '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField(), ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['date']); + } + /** * Parses an OpenEMR Insurance record, returning the equivalent FHIR Coverage Resource * @@ -67,7 +75,13 @@ protected function loadSearchParameters() public function parseOpenEMRRecord($dataRecord = array(), $encode = false) { $coverageResource = new FHIRCoverage(); - $meta = array('versionId' => '1', 'lastUpdated' => UtilsService::getDateFormattedAsUTC()); + $meta = new FHIRMeta(); + $meta->setVersionId('1'); + if (!empty($dataRecord['date'])) { + $meta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['date'])); + } else { + $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } $coverageResource->setMeta($meta); $id = new FHIRId(); diff --git a/src/Services/FHIR/FhirDeviceService.php b/src/Services/FHIR/FhirDeviceService.php index b9eb9560830..d65b79fa2a7 100644 --- a/src/Services/FHIR/FhirDeviceService.php +++ b/src/Services/FHIR/FhirDeviceService.php @@ -14,12 +14,14 @@ use OpenEMR\FHIR\R4\FHIRDomainResource\FHIRDevice; use OpenEMR\FHIR\R4\FHIRElement\FHIRDateTime; use OpenEMR\FHIR\R4\FHIRElement\FHIRId; +use OpenEMR\FHIR\R4\FHIRElement\FHIRMeta; use OpenEMR\FHIR\R4\FHIRResource\FHIRDevice\FHIRDeviceUdiCarrier; use OpenEMR\Services\DeviceService; use OpenEMR\Services\FHIR\Traits\BulkExportSupportAllOperationsTrait; use OpenEMR\Services\FHIR\Traits\FhirBulkExportDomainResourceTrait; use OpenEMR\Services\FHIR\Traits\FhirServiceBaseEmptyTrait; use OpenEMR\Services\Search\FhirSearchParameterDefinition; +use OpenEMR\Services\Search\ISearchField; use OpenEMR\Services\Search\SearchFieldType; use OpenEMR\Services\Search\ServiceField; use OpenEMR\Validators\ProcessingResult; @@ -52,9 +54,15 @@ protected function loadSearchParameters() return [ 'patient' => $this->getPatientContextSearchField(), '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField(), ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['modifydate']); + } + /** * Parses an OpenEMR data record, returning the equivalent FHIR Resource * @@ -66,7 +74,14 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) { $device = new FHIRDevice(); - $device->setMeta(UtilsService::createFhirMeta('1', UtilsService::getDateFormattedAsUTC())); + $fhirMeta = new FHIRMeta(); + $fhirMeta->setVersionId('1'); + if (!empty($dataRecord['modifydate'])) { + $fhirMeta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['modifydate'])); + } else { + $fhirMeta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } + $device->setMeta($fhirMeta); $id = new FHIRId(); $id->setValue($dataRecord['uuid']); diff --git a/src/Services/FHIR/FhirDiagnosticReportService.php b/src/Services/FHIR/FhirDiagnosticReportService.php index 8aacf715a01..38097e641b8 100644 --- a/src/Services/FHIR/FhirDiagnosticReportService.php +++ b/src/Services/FHIR/FhirDiagnosticReportService.php @@ -20,6 +20,7 @@ use OpenEMR\Services\FHIR\Traits\MappedServiceCodeTrait; use OpenEMR\Services\FHIR\Traits\PatientSearchTrait; use OpenEMR\Services\Search\FhirSearchParameterDefinition; +use OpenEMR\Services\Search\ISearchField; use OpenEMR\Services\Search\SearchFieldException; use OpenEMR\Services\Search\SearchFieldType; use OpenEMR\Services\Search\ServiceField; @@ -50,11 +51,19 @@ protected function loadSearchParameters() 'patient' => $this->getPatientContextSearchField(), 'code' => new FhirSearchParameterDefinition('code', SearchFieldType::TOKEN, ['code']), 'category' => new FhirSearchParameterDefinition('category', SearchFieldType::TOKEN, ['category']), + // shouldn't be a problem if date and _lastUpdated are provided as it will just be ignored with duplicate WHERE clause conditions + // TODO: @adunsulag test this assumption to make sure it is correct 'date' => new FhirSearchParameterDefinition('date', SearchFieldType::DATETIME, ['date']), '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField(), ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['date']); + } + /** * Retrieves all of the fhir observation resources mapped to the underlying openemr data elements. * @param $fhirSearchParameters The FHIR resource search parameters diff --git a/src/Services/FHIR/FhirDocumentReferenceService.php b/src/Services/FHIR/FhirDocumentReferenceService.php index 7c163097d12..78b63e84e7b 100644 --- a/src/Services/FHIR/FhirDocumentReferenceService.php +++ b/src/Services/FHIR/FhirDocumentReferenceService.php @@ -20,6 +20,7 @@ use OpenEMR\Services\FHIR\Traits\MappedServiceCodeTrait; use OpenEMR\Services\FHIR\Traits\PatientSearchTrait; use OpenEMR\Services\Search\FhirSearchParameterDefinition; +use OpenEMR\Services\Search\ISearchField; use OpenEMR\Services\Search\SearchFieldException; use OpenEMR\Services\Search\SearchFieldType; use OpenEMR\Services\Search\ServiceField; @@ -54,12 +55,20 @@ protected function loadSearchParameters() 'patient' => $this->getPatientContextSearchField(), 'type' => new FhirSearchParameterDefinition('type', SearchFieldType::TOKEN, ['type']), 'category' => new FhirSearchParameterDefinition('category', SearchFieldType::TOKEN, ['category']), + // shouldn't be a problem if date and _lastUpdated are provided as it will just be ignored with duplicate WHERE clause conditions + // TODO: @adunsulag test this assumption to make sure it is correct 'date' => new FhirSearchParameterDefinition('date', SearchFieldType::DATETIME, ['date']), // it will search all the services, but since we are only grabbing a single id this should be relatively fast '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField(), ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['date']); + } + /** * Retrieves all of the fhir observation resources mapped to the underlying openemr data elements. * @param $fhirSearchParameters The FHIR resource search parameters diff --git a/src/Services/FHIR/FhirEncounterService.php b/src/Services/FHIR/FhirEncounterService.php index 0bfada182ee..5cccdb6ca4a 100644 --- a/src/Services/FHIR/FhirEncounterService.php +++ b/src/Services/FHIR/FhirEncounterService.php @@ -39,6 +39,7 @@ use OpenEMR\Services\FHIR\Traits\FhirServiceBaseEmptyTrait; use OpenEMR\Services\FHIR\Traits\PatientSearchTrait; use OpenEMR\Services\Search\FhirSearchParameterDefinition; +use OpenEMR\Services\Search\ISearchField; use OpenEMR\Services\Search\SearchFieldType; use OpenEMR\Services\Search\ServiceField; use OpenEMR\Validators\ProcessingResult; @@ -95,10 +96,15 @@ protected function loadSearchParameters() ), 'patient' => $this->getPatientContextSearchField(), 'date' => new FhirSearchParameterDefinition('date', SearchFieldType::DATETIME, ['date']), - '_lastUpdated' => new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['last_update']) + '_lastUpdated' => $this->getLastModifiedSearchField(), ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['last_update']); + } + /** * Parses an OpenEMR patient record, returning the equivalent FHIR Patient Resource * https://build.fhir.org/ig/HL7/US-Core-R4/StructureDefinition-us-core-encounter-definitions.html diff --git a/src/Services/FHIR/FhirGoalService.php b/src/Services/FHIR/FhirGoalService.php index fc523b76a3d..2ee3c321b89 100644 --- a/src/Services/FHIR/FhirGoalService.php +++ b/src/Services/FHIR/FhirGoalService.php @@ -25,6 +25,7 @@ use OpenEMR\Services\FHIR\Traits\FhirBulkExportDomainResourceTrait; use OpenEMR\Services\FHIR\Traits\FhirServiceBaseEmptyTrait; use OpenEMR\Services\Search\FhirSearchParameterDefinition; +use OpenEMR\Services\Search\ISearchField; use OpenEMR\Services\Search\SearchFieldType; use OpenEMR\Services\Search\ServiceField; use OpenEMR\Validators\ProcessingResult; @@ -59,15 +60,22 @@ protected function loadSearchParameters() 'patient' => $this->getPatientContextSearchField(), // note even though we label this as a uuid, it is a SURROGATE UID because of the nature of how goals are stored '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, ['uuid']), + '_lastUpdated' => $this->getLastModifiedSearchField(), ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + // TODO: @adunsulag introduce a last_modified date field to the care plan table as we don't track this anywhere + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['creation_date']); + } + /** * Parses an OpenEMR careTeam record, returning the equivalent FHIR CareTeam Resource * * @param array $dataRecord The source OpenEMR data record * @param boolean $encode Indicates if the returned resource is encoded into a string. Defaults to false. - * @return FHIRCareTeam + * @return FHIRGoal */ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) { @@ -75,7 +83,11 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) $fhirMeta = new FHIRMeta(); $fhirMeta->setVersionId('1'); - $fhirMeta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + if (!empty($dataRecord['creation_date'])) { + $fhirMeta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['creation_date'])); + } else { + $fhirMeta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } $goal->setMeta($fhirMeta); $fhirId = new FHIRId(); diff --git a/src/Services/FHIR/FhirGroupService.php b/src/Services/FHIR/FhirGroupService.php index ab1b3c7d854..5f258fafb7f 100644 --- a/src/Services/FHIR/FhirGroupService.php +++ b/src/Services/FHIR/FhirGroupService.php @@ -19,6 +19,7 @@ use OpenEMR\Services\FHIR\Traits\MappedServiceTrait; use OpenEMR\Services\FHIR\Traits\PatientSearchTrait; use OpenEMR\Services\Search\FhirSearchParameterDefinition; +use OpenEMR\Services\Search\ISearchField; use OpenEMR\Services\Search\SearchFieldException; use OpenEMR\Services\Search\SearchFieldType; use OpenEMR\Services\Search\ServiceField; @@ -48,9 +49,15 @@ protected function loadSearchParameters() return [ 'patient' => $this->getPatientContextSearchField(), '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField(), ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['date']); + } + /** * Retrieves all of the fhir observation resources mapped to the underlying openemr data elements. * @param $fhirSearchParameters The FHIR resource search parameters diff --git a/src/Services/FHIR/FhirImmunizationService.php b/src/Services/FHIR/FhirImmunizationService.php index b00d0a859e8..9f4db5a09ab 100644 --- a/src/Services/FHIR/FhirImmunizationService.php +++ b/src/Services/FHIR/FhirImmunizationService.php @@ -62,9 +62,15 @@ protected function loadSearchParameters() return [ 'patient' => $this->getPatientContextSearchField(), '_id' => new FhirSearchParameterDefinition('uuid', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField(), ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['update_date']); + } + /** * Parses an OpenEMR immunization record, returning the equivalent FHIR Immunization Resource * @@ -78,7 +84,11 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) $meta = new FHIRMeta(); $meta->setVersionId('1'); - $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + if (!empty($dataRecord['update_date'])) { + $meta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['update_date'])); + } else { + $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } $immunizationResource->setMeta($meta); $id = new FHIRId(); diff --git a/src/Services/FHIR/FhirLocationService.php b/src/Services/FHIR/FhirLocationService.php index f3c23e97931..9778bc06c29 100644 --- a/src/Services/FHIR/FhirLocationService.php +++ b/src/Services/FHIR/FhirLocationService.php @@ -59,10 +59,16 @@ public function __construct() protected function loadSearchParameters() { return [ - '_id' => new FhirSearchParameterDefinition('uuid', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]) + '_id' => new FhirSearchParameterDefinition('uuid', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField() ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['last_updated']); + } + /** * Parses an OpenEMR location record, returning the equivalent FHIR Location Resource * @@ -76,7 +82,11 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) $meta = new FHIRMeta(); $meta->setVersionId('1'); - $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + if (!empty($dataRecord['last_updated'])) { + $meta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['last_updated'])); + } else { + $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } $locationResource->setMeta($meta); $id = new FHIRId(); diff --git a/src/Services/FHIR/FhirMedicationRequestService.php b/src/Services/FHIR/FhirMedicationRequestService.php index 0c52b04e44f..674804743af 100644 --- a/src/Services/FHIR/FhirMedicationRequestService.php +++ b/src/Services/FHIR/FhirMedicationRequestService.php @@ -109,9 +109,15 @@ protected function loadSearchParameters() 'intent' => new FhirSearchParameterDefinition('intent', SearchFieldType::TOKEN, ['intent']), 'status' => new FhirSearchParameterDefinition('status', SearchFieldType::TOKEN, ['status']), '_id' => new FhirSearchParameterDefinition('uuid', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField() ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['date_modified']); + } + /** * Parses an OpenEMR prescription record, returning the equivalent FHIR Patient Resource * @@ -125,7 +131,11 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) $meta = new FHIRMeta(); $meta->setVersionId('1'); - $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + if (!empty($dataRecord['date_modified'])) { + $meta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['date_modified'])); + } else { + $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } $medRequestResource->setMeta($meta); $id = new FHIRId(); diff --git a/src/Services/FHIR/FhirMedicationService.php b/src/Services/FHIR/FhirMedicationService.php index 864c1ba6a98..9adbf598125 100644 --- a/src/Services/FHIR/FhirMedicationService.php +++ b/src/Services/FHIR/FhirMedicationService.php @@ -48,9 +48,15 @@ protected function loadSearchParameters() { return [ '_id' => new FhirSearchParameterDefinition('uuid', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField() ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['drug_last_updated']); + } + /** * Parses an OpenEMR medication record, returning the equivalent FHIR Medication Resource * @@ -64,7 +70,11 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) $meta = new FHIRMeta(); $meta->setVersionId('1'); - $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + if (!empty($dataRecord['drug_last_updated'])) { + $meta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['drug_last_updated'])); + } else { + $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } $medicationResource->setMeta($meta); $id = new FHIRId(); diff --git a/src/Services/FHIR/FhirObservationService.php b/src/Services/FHIR/FhirObservationService.php index f3af4cef617..493d390cf7e 100644 --- a/src/Services/FHIR/FhirObservationService.php +++ b/src/Services/FHIR/FhirObservationService.php @@ -76,9 +76,15 @@ protected function loadSearchParameters(): array 'category' => new FhirSearchParameterDefinition('category', SearchFieldType::TOKEN, ['category']), 'date' => new FhirSearchParameterDefinition('date', SearchFieldType::DATETIME, ['date']), '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, ['uuid']), + '_lastUpdated' => $this->getLastModifiedSearchField() ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['date_modified']); + } + /** * Retrieves all of the fhir observation resources mapped to the underlying openemr data elements. * @param $fhirSearchParameters The FHIR resource search parameters diff --git a/src/Services/FHIR/FhirOrganizationService.php b/src/Services/FHIR/FhirOrganizationService.php index cba23a2ef86..e600a56d5a3 100644 --- a/src/Services/FHIR/FhirOrganizationService.php +++ b/src/Services/FHIR/FhirOrganizationService.php @@ -81,10 +81,16 @@ public function getSearchParams() 'address-city' => new FhirSearchParameterDefinition('address-city', SearchFieldType::STRING, ['city']), 'address-postalcode' => new FhirSearchParameterDefinition('address-postalcode', SearchFieldType::STRING, ['postal_code', "zip"]), 'address-state' => new FhirSearchParameterDefinition('address-state', SearchFieldType::STRING, ['state']), - 'name' => new FhirSearchParameterDefinition('name', SearchFieldType::STRING, ['name']) + 'name' => new FhirSearchParameterDefinition('name', SearchFieldType::STRING, ['name']), + '_lastUpdated' => $this->getLastModifiedSearchField() ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['date']); + } + public function getOne($fhirResourceId, $puuidBind = null): ProcessingResult { return $this->getAll(['_id' => $fhirResourceId], $puuidBind); diff --git a/src/Services/FHIR/FhirPatientService.php b/src/Services/FHIR/FhirPatientService.php index 573fad5869d..cdde9982cdd 100644 --- a/src/Services/FHIR/FhirPatientService.php +++ b/src/Services/FHIR/FhirPatientService.php @@ -90,6 +90,8 @@ class FhirPatientService extends FhirServiceBase implements IFhirExportableResou const FIELD_NAME_GENDER = 'sex'; + private ?array $searchParameters = null; + public function __construct() { parent::__construct(); @@ -143,11 +145,16 @@ protected function loadSearchParameters() 'given' => new FhirSearchParameterDefinition('given', SearchFieldType::STRING, ['fname', 'mname']), 'phone' => new FhirSearchParameterDefinition('phone', SearchFieldType::TOKEN, ['phone_home', 'phone_biz', 'phone_cell']), 'telecom' => new FhirSearchParameterDefinition('telecom', SearchFieldType::TOKEN, ['email','email_direct', 'phone_home', 'phone_biz', 'phone_cell']), - '_lastUpdated' => new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['date']), + '_lastUpdated' => $this->getLastModifiedSearchField(), 'generalPractitioner' => new FhirSearchParameterDefinition('generalPractitioner', SearchFieldType::REFERENCE, ['provider_uuid']) ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['last_updated']); + } + /** * Parses an OpenEMR patient record, returning the equivalent FHIR Patient Resource * @@ -161,8 +168,8 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) $meta = new FHIRMeta(); $meta->setVersionId('1'); - if (!empty($dataRecord['date'])) { - $meta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['date'])); + if (!empty($dataRecord['last_updated'])) { + $meta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['last_updated'])); } else { $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); } @@ -172,7 +179,7 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) $id = new FHIRId(); $id->setValue($dataRecord['uuid']); $patientResource->setId($id); - $patientResource->setDeceasedBoolean($dataRecord[ 'deceasedDate' ] != null); + $patientResource->setDeceasedBoolean($dataRecord[ 'deceased_date' ] != null); $this->parseOpenEMRPatientSummaryText($patientResource, $dataRecord); $this->parseOpenEMRPatientName($patientResource, $dataRecord); diff --git a/src/Services/FHIR/FhirPersonService.php b/src/Services/FHIR/FhirPersonService.php index b5dafac4a4b..025a6fb6d86 100644 --- a/src/Services/FHIR/FhirPersonService.php +++ b/src/Services/FHIR/FhirPersonService.php @@ -67,10 +67,16 @@ protected function loadSearchParameters() 'given' => new FhirSearchParameterDefinition('given', SearchFieldType::STRING, ["fname", "mname"]), 'name' => new FhirSearchParameterDefinition('name', SearchFieldType::STRING, ["users.title", "fname", "mname", "lname"]), - '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]) + '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField() ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['last_updated']); + } + /** * Parses an OpenEMR user record, returning the equivalent FHIR Person Resource @@ -85,7 +91,11 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) $meta = new FHIRMeta(); $meta->setVersionId('1'); - $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + if (!empty($dataRecord['last_updated'])) { + $meta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['last_updated'])); + } else { + $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } $person->setMeta($meta); $person->setActive($dataRecord['active'] == "1" ? true : false); diff --git a/src/Services/FHIR/FhirPractitionerRoleService.php b/src/Services/FHIR/FhirPractitionerRoleService.php index 4e75a93db6c..23b72168bc3 100644 --- a/src/Services/FHIR/FhirPractitionerRoleService.php +++ b/src/Services/FHIR/FhirPractitionerRoleService.php @@ -46,10 +46,21 @@ protected function loadSearchParameters() return [ 'specialty' => new FhirSearchParameterDefinition('specialty', SearchFieldType::TOKEN, ['specialty_code']), 'practitioner' => new FhirSearchParameterDefinition('practitioner', SearchFieldType::STRING, ['user_name']), - '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('providers.facility_role_uuid', ServiceField::TYPE_UUID)]) + '_id' => new FhirSearchParameterDefinition( + '_id', + SearchFieldType::TOKEN, + [new ServiceField('providers.facility_role_uuid', ServiceField::TYPE_UUID)] + ), + '_lastUpdated' => $this->getLastModifiedSearchField() ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + // we just go off of role as specialty gets updated at the same time + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['role_last_updated']); + } + /** * Parses an OpenEMR practitionerRole record, returning the equivalent FHIR PractitionerRole Resource * @@ -63,7 +74,11 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) $meta = new FHIRMeta(); $meta->setVersionId('1'); - $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + if (!empty($dataRecord['role_last_updated'])) { + $meta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['role_last_updated'])); + } else { + $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } $practitionerRoleResource->setMeta($meta); $id = new FHIRId(); diff --git a/src/Services/FHIR/FhirPractitionerService.php b/src/Services/FHIR/FhirPractitionerService.php index 41fc9539cbb..7652ff43e6c 100644 --- a/src/Services/FHIR/FhirPractitionerService.php +++ b/src/Services/FHIR/FhirPractitionerService.php @@ -64,10 +64,18 @@ protected function loadSearchParameters() 'address-state' => new FhirSearchParameterDefinition('address-state', SearchFieldType::STRING, ['state']), 'family' => new FhirSearchParameterDefinition('family', SearchFieldType::STRING, ["lname"]), 'given' => new FhirSearchParameterDefinition('given', SearchFieldType::STRING, ["fname", "mname"]), - 'name' => new FhirSearchParameterDefinition('name', SearchFieldType::STRING, ["title", "fname", "mname", "lname"]) + 'name' => new FhirSearchParameterDefinition('name', SearchFieldType::STRING, ["title", "fname", "mname", "lname"]), + '_lastUpdated' => $this->getLastModifiedSearchField() ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + // TODO: @adunsulag I don't like specifying full table name here in the search field, but I don't see a way around it + // right now... if we ever need to implement better escaping this is an issue. + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['users.last_updated']); + } + /** * Parses an OpenEMR practitioner record, returning the equivalent FHIR Practitioner Resource @@ -82,7 +90,11 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) $meta = new FHIRMeta(); $meta->setVersionId('1'); - $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + if (!empty($dataRecord['last_updated'])) { + $meta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['last_updated'])); + } else { + $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } $practitionerResource->setMeta($meta); $practitionerResource->setActive($dataRecord['active'] == "1" ? true : false); diff --git a/src/Services/FHIR/FhirProcedureService.php b/src/Services/FHIR/FhirProcedureService.php index 0ed8ce946fa..700d061f34a 100644 --- a/src/Services/FHIR/FhirProcedureService.php +++ b/src/Services/FHIR/FhirProcedureService.php @@ -70,9 +70,15 @@ protected function loadSearchParameters() 'patient' => $this->getPatientContextSearchField(), 'date' => new FhirSearchParameterDefinition('date', SearchFieldType::DATETIME, ['report_date']), '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField(), ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['last_updated']); + } + /** * Retrieves all of the fhir observation resources mapped to the underlying openemr data elements. diff --git a/src/Services/FHIR/FhirProvenanceService.php b/src/Services/FHIR/FhirProvenanceService.php index d007cf4fdaa..317eab65212 100644 --- a/src/Services/FHIR/FhirProvenanceService.php +++ b/src/Services/FHIR/FhirProvenanceService.php @@ -230,7 +230,7 @@ public function getAll($fhirSearchParameters, $puuidBind = null): ProcessingResu if (!empty($fhirSearchParameters['_id'])) { $fhirSearchResult = $this->getProvenanceRecordsForId($fhirSearchParameters['_id'], $puuidBind); } else { - $fhirSearchResult = $this->getAllProvenanceRecordsFromServices($puuidBind); + $fhirSearchResult = $this->getAllProvenanceRecordsFromServices($fhirSearchParameters, $puuidBind); } } catch (SearchFieldException $exception) { $systemLogger = new SystemLogger(); @@ -242,13 +242,15 @@ public function getAll($fhirSearchParameters, $puuidBind = null): ProcessingResu return $fhirSearchResult; } - private function getAllProvenanceRecordsFromServices($puuidBind = null) + private function getAllProvenanceRecordsFromServices(array $fhirSearchParameters, $puuidBind = null) { $processingResult = new ProcessingResult(); if (empty($this->serviceLocator)) { (new SystemLogger())->errorLogCaller("class was not properly configured with the service locator"); } + $searchParams = $this->filterSupportedSearchParams($fhirSearchParameters); + // we only return provenances for $servicesByResource = $this->serviceLocator->findServices(IResourceUSCIGProfileService::class); @@ -258,13 +260,13 @@ private function getAllProvenanceRecordsFromServices($puuidBind = null) continue; } try { - $this->addAllProvenanceRecordsForService($processingResult, $service, [], $puuidBind); + $this->addAllProvenanceRecordsForService($processingResult, $service, $searchParams, $puuidBind); } catch (SearchFieldException $ex) { $systemLogger = new SystemLogger(); - $systemLogger->error(get_class($this) . "->getAll() exception thrown", ['message' => $exception->getMessage(), - 'field' => $exception->getField(), 'trace' => $exception->getTraceAsString()]); + $systemLogger->error(get_class($this) . "->getAll() exception thrown", ['message' => $ex->getMessage(), + 'field' => $ex->getField(), 'trace' => $ex->getTraceAsString()]); // put our exception information here - $processingResult->setValidationMessages([$exception->getField() => $exception->getMessage()]); + $processingResult->setValidationMessages([$ex->getField() => $ex->getMessage()]); return $processingResult; } catch (Exception $ex) { $systemLogger = new SystemLogger(); @@ -333,57 +335,6 @@ private function getProvenanceRecordsForId($id, $puuidBind) return $processingResult; } - /** - * Searches for OpenEMR records using OpenEMR search parameters - * @param openEMRSearchParameters OpenEMR search fields - * @param $puuidBind - Optional variable to only allow visibility of the patient with this puuid. - * @return OpenEMR records - */ - protected function searchForOpenEMRRecords($openEMRSearchParameters): ProcessingResult - { - $patientToken = $openEMRSearchParameters['patient'] ?? new TokenSearchField('patient', []); - $patientBinding = !empty($patientToken->getValues()) ? $patientToken->getValues()[0]->getCode() : null; - /** - * @var TokenSearchField - */ - $id = $openEMRSearchParameters['_id'] ?? new TokenSearchField('_id', []); - $processingResult = new ProcessingResult(); - foreach ($id->getValues() as $value) { - // should be in format of ResourceType/uuid - $code = $value->getCode() ?? ""; - try { - $idParts = explode(":", $code); - $resourceName = array_shift($idParts); - - $innerId = implode(":", $idParts); - $className = RestControllerHelper::FHIR_SERVICES_NAMESPACE . $resourceName . "Service"; - if (class_exists($className)) { - $newServiceClass = new $className(); - if ($newServiceClass instanceof IResourceReadableService) { - $searchParams = [ - '_id' => $innerId - ,'_revinclude' => 'Provenance:target' - ]; - $results = $newServiceClass->getAll($searchParams, $patientBinding); - if ($results->hasData()) { - foreach ($results->getData() as $datum) { - if ($datum instanceof FHIRProvenance) { - $processingResult->addData($datum); - } - } - } else { - $processingResult->addProcessingResult($results); - } - } - } - } catch (\Exception $exception) { - // TODO: @adunsulag log the exception - $processingResult->addInternalError("Server error occurred in returning provenance for _id " . $code); - } - } - return $processingResult; - } - /** * Returns the Canonical URIs for the FHIR resource for each of the US Core Implementation Guide Profiles that the * resource implements. Most resources have only one profile, but several like DiagnosticReport and Observation @@ -416,7 +367,9 @@ public function getSurrogateKeyForResource(FHIRDomainResource $resource) "Resource missing required Meta->lastUpdated field", ['resource' => $resource->getId(), 'type' => $resource->get_fhirElementName()] ); - } else { + // patients were the only ones who actually were tracking a valid last updated date instead of the most + // current timestamp for V1 so we need to check for that, everything else is V2 as last updated wasn't really tracked. + } else if ($resource->get_fhirElementName() === 'Patient') { // we use DATE_ATOM to get an ISO8601 compatible date as DATE_ISO8601 does not actually conform to an ISO8601 date for php legacy purposes $lastUpdated = \DateTime::createFromFormat(DATE_ATOM, $resource->getMeta()->getLastUpdated()); @@ -490,23 +443,43 @@ public function export(ExportStreamWriter $writer, ExportJob $job, $lastResource $searchParams[$searchField->getName()] = implode(",", $patientUuids); } } - - $serviceResult = $service->getAll($searchParams); - // now loop through and grab all of our provenance resources - if ($serviceResult->hasData()) { - foreach ($serviceResult->getData() as $record) { - if (!($record instanceof FHIRDomainResource)) { - throw new ExportException(self::class . " returned records that are not a valid fhir resource type for this class", 0, $lastResourceIdExported); - } - // we only want to write out provenance records - if (!($record instanceof FHIRProvenance)) { - continue; + $searchParams['_lastUpdated'] = $job->getResourceIncludeSearchParamValue(); + try { + $serviceResult = $service->getAll($searchParams); + // now loop through and grab all of our provenance resources + if ($serviceResult->hasData()) { + foreach ($serviceResult->getData() as $record) { + if (!($record instanceof FHIRDomainResource)) { + throw new ExportException(self::class . " returned records that are not a valid fhir resource type for this class", 0, $lastResourceIdExported); + } + // we only want to write out provenance records + if (!($record instanceof FHIRProvenance)) { + continue; + } + $writer->append($record); + $lastResourceIdExported = $record->getId(); } - $writer->append($record); - $lastResourceIdExported = $record->getId(); } + } catch (SearchFieldException $exception) { + $message = $exception->getMessage() . " Search Field " . $exception->getField(); + throw new ExportException($message, 0, $lastResourceIdExported); } } } } + + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + // nothing to really do here as we handle it internally in the export operation + return null; + } + + private function filterSupportedSearchParams(array $fhirSearchParameters) + { + $supportedParams = []; + if (isset($fhirSearchParameters['_lastUpdated'])) { + $supportedParams['_lastUpdated'] = $fhirSearchParameters['_lastUpdated']; + } + return $supportedParams; + } } diff --git a/src/Services/FHIR/FhirValueSetService.php b/src/Services/FHIR/FhirValueSetService.php index 31f26b4df7f..d9659511b73 100644 --- a/src/Services/FHIR/FhirValueSetService.php +++ b/src/Services/FHIR/FhirValueSetService.php @@ -85,13 +85,13 @@ class FhirValueSetService extends FhirServiceBase implements IResourceUSCIGProfi */ - const USCGI_PROFILE_URI = 'http://hl7.org/fhir/StructureDefinition/shareablevalueset'; const APPOINTMENT_TYPE = 'appointment-type'; public function __construct() { parent::__construct(); + // TODO: @adunsulag we need to look at adding a mapping service here in order to get our value sets out. $this->appointmentService = new AppointmentService(); $this->listOptionService = new ListService(); } @@ -103,9 +103,20 @@ protected function loadSearchParameters() { return [ '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('id', ServiceField::TYPE_STRING)]), + '_lastUpdated' => $this->getLastModifiedSearchField(), ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['sublist_updated_date', 'last_updated']); + } + + private function getLastModifiedSearchFieldForAppointmentCategories() + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['pc_last_updated']); + } + /** * Retrieves all of the fhir observation resources mapped to the underlying openemr data elements. * @param $fhirSearchParameters The FHIR resource search parameters @@ -115,74 +126,14 @@ public function getAll($fhirSearchParameters, $puuidBind = null): ProcessingResu { $fhirSearchResult = new ProcessingResult(); try { - if ( - !isset($fhirSearchParameters[ '_id' ]) - // could be array (AND) or comma-delimited string value (OR) - // check array first but should only be len 1 ("AND", becuase cannot be 2 simultaneous) - || ( is_array($fhirSearchParameters[ '_id' ]) - && count($fhirSearchParameters[ '_id' ]) == 1 - && $fhirSearchParameters[ '_id' ][ 0 ] == self::APPOINTMENT_TYPE ) - // and string which could be comma-delimiter OR of exploded values - || ( !is_array($fhirSearchParameters[ '_id' ]) - && in_array(self::APPOINTMENT_TYPE, explode(",", $fhirSearchParameters[ '_id' ])) ) - ) { - $calendarCategories = $this->appointmentService->getCalendarCategories(); - $valueSet = new FHIRValueSet(); - $valueSet->setId(self::APPOINTMENT_TYPE); - $compose = new FHIRValueSetCompose(); - $include = new FHIRValueSetInclude(); - foreach ($calendarCategories as $category) { - if ($category["pc_cattype"] != 0) { - continue; // only cat_type==0 - } - $concept = new FHIRValueSetConcept(); - $code = new FHIRCode(); - $code->setValue($category[ "pc_constant_id"]); - $concept->setCode($code); - $concept->setDisplay($category[ "pc_catname" ]); - $include->addConcept($concept); - } - $compose->addInclude($include); - $valueSet->setCompose($compose); - $fhirSearchResult->addData($valueSet); + // we don't really deal with provenance for ValueSet pieces so we will ignore this property + if (isset($fhirSearchParameters['_revinclude'])) { + unset($fhirSearchParameters['_revinclude']); } - // Now the same for list_options selected in $listNames - $list_ids = $this->listOptionService->getListIds(); - foreach ($list_ids as $listName) { - if ( - isset($fhirSearchParameters[ '_id' ]) - // could be array (AND) or comma-delimited string value (OR) - // check array first but should only be len 1 ("AND", becuase cannot be 2 simultaneous) - && ( ( is_array($fhirSearchParameters[ '_id' ]) - && count($fhirSearchParameters[ '_id' ]) == 1 - && $fhirSearchParameters[ '_id' ][ 0 ] != $listName ) - // and string which could be comma-delimiter OR of exploded values - || ( !is_array($fhirSearchParameters[ '_id' ]) - && !in_array($listName, explode(",", $fhirSearchParameters[ '_id' ])) ) ) - ) { - continue; - } - $options = $this->listOptionService->getOptionsByListName($listName); // does not return title - if (count($options) == 0) { - continue; - } - $valueSet = new FHIRValueSet(); - $valueSet->setId($listName); - $compose = new FHIRValueSetCompose(); - $include = new FHIRValueSetInclude(); - foreach ($options as $option) { - $concept = new FHIRValueSetConcept(); - $code = new FHIRCode(); - $code->setValue($option[ "option_id"]); - $concept->setCode($code); - $concept->setDisplay($option[ "title" ]); - $include->addConcept($concept); - } - $compose->addInclude($include); - $valueSet->setCompose($compose); - $fhirSearchResult->addData($valueSet); - } + $this->addAppointmentCategoriesValueSetForSearch($fhirSearchResult, $fhirSearchParameters); + + $this->addListOptionsValueSetsForSearch($fhirSearchResult, $fhirSearchParameters, $puuidBind); } catch (SearchFieldException $exception) { (new SystemLogger())->errorLogCaller("search exception thrown", ['message' => $exception->getMessage(), 'field' => $exception->getField()]); @@ -204,4 +155,82 @@ function getProfileURIs(): array { return [self::USCGI_PROFILE_URI]; } + + private function addAppointmentCategoriesValueSetForSearch(ProcessingResult $fhirSearchResult, array $fhirSearchParameters, string $puuidBind = null) + { + $this->getSearchFieldFactory()->setSearchFieldDefinition('_lastUpdated', $this->getLastModifiedSearchFieldForAppointmentCategories()); + $oeSearchParameters = $this->createOpenEMRSearchParameters($fhirSearchParameters, $puuidBind); + if ( + !isset($oeSearchParameters['_id']) + // could be array (AND) or comma-delimited string value (OR) + // check array first but should only be len 1 ("AND", becuase cannot be 2 simultaneous) + || $oeSearchParameters['_id']->hasCodeValue(self::APPOINTMENT_TYPE) + ) { + if (!isset($oeSearchParameters['_id'])) { + // if we have any match on categories we want to return everything... hate the double db call + // but rather than mess with a complex query we will just do it this way + $processingResult = $this->appointmentService->searchCalendarCategories($oeSearchParameters); + // nothing to do here as we have no categories matching so we return + if (!$processingResult->hasData()) { + return $fhirSearchResult; + } + } + $calendarCategories = $this->appointmentService->getCalendarCategories(); + $valueSet = new FHIRValueSet(); + $valueSet->setId(self::APPOINTMENT_TYPE); + $compose = new FHIRValueSetCompose(); + $include = new FHIRValueSetInclude(); + foreach ($calendarCategories as $category) { + if ($category["pc_cattype"] != 0) { + continue; // only cat_type==0 + } + $concept = new FHIRValueSetConcept(); + $code = new FHIRCode(); + $code->setValue($category["pc_constant_id"]); + $concept->setCode($code); + $concept->setDisplay($category["pc_catname"]); + $include->addConcept($concept); + } + $compose->addInclude($include); + $valueSet->setCompose($compose); + $fhirSearchResult->addData($valueSet); + } + return $fhirSearchResult; + } + + private function addListOptionsValueSetsForSearch(ProcessingResult $fhirSearchResult, array $fhirSearchParameters, ?string $puuidBind = null) + { + $this->getSearchFieldFactory()->setSearchFieldDefinition('_lastUpdated', $this->getLastModifiedSearchField()); + $oeSearchParameters = $this->createOpenEMRSearchParameters($fhirSearchParameters, $puuidBind); + + // Now the same for list_options selected in $listNames + $listsResult = $this->listOptionService->searchLists($oeSearchParameters); + if (!$listsResult->hasData()) { + $fhirSearchResult->addProcessingResult($listsResult); + return $fhirSearchResult; + } + foreach ($listsResult->getData() as $listRecord) { + $listName = $listRecord["option_id"]; + $options = $this->listOptionService->getOptionsByListName($listName); // does not return title + if (count($options) == 0) { + continue; + } + $valueSet = new FHIRValueSet(); + $valueSet->setId($listName); + $compose = new FHIRValueSetCompose(); + $include = new FHIRValueSetInclude(); + foreach ($options as $option) { + $concept = new FHIRValueSetConcept(); + $code = new FHIRCode(); + $code->setValue($option["option_id"]); + $concept->setCode($code); + $concept->setDisplay($option["title"]); + $include->addConcept($concept); + } + $compose->addInclude($include); + $valueSet->setCompose($compose); + $fhirSearchResult->addData($valueSet); + } + return $fhirSearchResult; + } } diff --git a/src/Services/FHIR/Group/FhirPatientProviderGroupService.php b/src/Services/FHIR/Group/FhirPatientProviderGroupService.php index 38d559edf5a..d66565e3b9b 100644 --- a/src/Services/FHIR/Group/FhirPatientProviderGroupService.php +++ b/src/Services/FHIR/Group/FhirPatientProviderGroupService.php @@ -20,6 +20,7 @@ use OpenEMR\Services\FHIR\UtilsService; use OpenEMR\Services\GroupService; use OpenEMR\Services\Search\FhirSearchParameterDefinition; +use OpenEMR\Services\Search\ISearchField; use OpenEMR\Services\Search\SearchFieldType; use OpenEMR\Services\Search\ServiceField; use OpenEMR\Validators\ProcessingResult; @@ -45,9 +46,15 @@ protected function loadSearchParameters() return [ 'patient' => $this->getPatientContextSearchField(), '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField(), ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['patient_last_updated']); + } + protected function searchForOpenEMRRecords($openEMRSearchParameters): ProcessingResult { return $this->service->searchPatientProviderGroups($openEMRSearchParameters); @@ -58,7 +65,11 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) $fhirGroup = new FHIRGroup(); $fhirMeta = new FHIRMeta(); $fhirMeta->setVersionId("1"); - $fhirMeta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + if (!empty($dataRecord['last_modified_date'])) { + $fhirMeta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['last_modified_date'])); + } else { + $fhirMeta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } $fhirGroup->setMeta($fhirMeta); $fhirGroup->setId($dataRecord['uuid']); diff --git a/src/Services/FHIR/IFhirExportableResourceService.php b/src/Services/FHIR/IFhirExportableResourceService.php index a008468fcfb..6c56871261c 100644 --- a/src/Services/FHIR/IFhirExportableResourceService.php +++ b/src/Services/FHIR/IFhirExportableResourceService.php @@ -18,6 +18,8 @@ use OpenEMR\FHIR\Export\ExportJob; use OpenEMR\FHIR\Export\ExportStreamWriter; use OpenEMR\FHIR\Export\ExportWillShutdownException; +use OpenEMR\Services\Search\FhirSearchParameterDefinition; +use OpenEMR\Services\Search\ISearchField; interface IFhirExportableResourceService { @@ -59,4 +61,12 @@ function supportsGroupExport(); * @return bool true if this resource service should be called for a patient export operation, false otherwise */ function supportsPatientExport(); + + /** + * Returns the search field that represents the last modified date for the resource used in the export _since + * parameter for the export operation. If the resource does not support the _since parameter then this method + * will return null and the export should return ALL the resources for the resource service. + * @return ISearchField|null The search field that represents the last modified date for the resource + */ + function getLastModifiedSearchField(): ?FhirSearchParameterDefinition; } diff --git a/src/Services/FHIR/Observation/FhirObservationLaboratoryService.php b/src/Services/FHIR/Observation/FhirObservationLaboratoryService.php index b2f6aaad2df..61aa3bddc25 100644 --- a/src/Services/FHIR/Observation/FhirObservationLaboratoryService.php +++ b/src/Services/FHIR/Observation/FhirObservationLaboratoryService.php @@ -95,9 +95,15 @@ protected function loadSearchParameters() 'category' => new FhirSearchParameterDefinition('category', SearchFieldType::TOKEN, ['category']), 'date' => new FhirSearchParameterDefinition('date', SearchFieldType::DATETIME, ['report_date']), '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('result_uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField() ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['report_date']); + } + /** * Searches for OpenEMR records using OpenEMR search parameters @@ -159,7 +165,11 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) $observation = new FHIRObservation(); $meta = new FHIRMeta(); $meta->setVersionId('1'); - $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + if (!empty($dataRecord['report_date'])) { + $meta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['report_date'])); + } else { + $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } $observation->setMeta($meta); $id = new FHIRId(); diff --git a/src/Services/FHIR/Observation/FhirObservationSocialHistoryService.php b/src/Services/FHIR/Observation/FhirObservationSocialHistoryService.php index fc64062dfc4..0ef55ca4d55 100644 --- a/src/Services/FHIR/Observation/FhirObservationSocialHistoryService.php +++ b/src/Services/FHIR/Observation/FhirObservationSocialHistoryService.php @@ -113,9 +113,15 @@ protected function loadSearchParameters() 'category' => new FhirSearchParameterDefinition('category', SearchFieldType::TOKEN, ['category']), 'date' => new FhirSearchParameterDefinition('date', SearchFieldType::DATETIME, ['date']), '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField() ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['date']); + } + /** * Inserts an OpenEMR record into the sytem. @@ -269,7 +275,11 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) $observation = new FHIRObservation(); $meta = new FHIRMeta(); $meta->setVersionId('1'); - $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + if (!empty($dataRecord['date'])) { + $meta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['date'])); + } else { + $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } $observation->setMeta($meta); $id = new FHIRId(); diff --git a/src/Services/FHIR/Observation/FhirObservationVitalsService.php b/src/Services/FHIR/Observation/FhirObservationVitalsService.php index 0c59793ac4d..6109b130676 100644 --- a/src/Services/FHIR/Observation/FhirObservationVitalsService.php +++ b/src/Services/FHIR/Observation/FhirObservationVitalsService.php @@ -241,9 +241,15 @@ protected function loadSearchParameters() 'category' => new FhirSearchParameterDefinition('category', SearchFieldType::TOKEN, ['category']), 'date' => new FhirSearchParameterDefinition('date', SearchFieldType::DATETIME, ['date']), '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField() ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['last_updated']); + } + /** * Inserts an OpenEMR record into the sytem. @@ -342,6 +348,7 @@ private function parseVitalsIntoObservationRecords(ProcessingResult $processingR , "uuid" => UuidRegistry::uuidToString($uuidMappings[self::VITALS_PANEL_LOINC_CODE]) , "user_uuid" => $record['user_uuid'] , "date" => $record['date'] + , "last_updated" => $record['last_updated'] ]; foreach ($uuidMappings as $code => $uuid) { if (!$this->isVitalSignPanelCodes($code)) { // we will skip over our vital signs code, and any pediatric stuff @@ -365,6 +372,7 @@ private function parseVitalsIntoObservationRecords(ProcessingResult $processingR , "user_uuid" => $record['user_uuid'] ,"uuid" => UuidRegistry::uuidToString($uuidMappings[$code]) ,"date" => $record['date'] + , "last_updated" => $record['last_updated'] ]; $columns = $this->getColumnsForCode($code); @@ -421,7 +429,11 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) $observation = new FHIRObservation(); $meta = new FHIRMeta(); $meta->setVersionId('1'); - $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + if (!empty($dataRecord['last_updated'])) { + $meta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['last_updated'])); + } else { + $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } $observation->setMeta($meta); $id = new FHIRId(); diff --git a/src/Services/FHIR/Organization/FhirOrganizationFacilityService.php b/src/Services/FHIR/Organization/FhirOrganizationFacilityService.php index c8424c11a94..85fd9b93ddb 100644 --- a/src/Services/FHIR/Organization/FhirOrganizationFacilityService.php +++ b/src/Services/FHIR/Organization/FhirOrganizationFacilityService.php @@ -101,10 +101,16 @@ protected function loadSearchParameters() 'address-city' => new FhirSearchParameterDefinition('address-city', SearchFieldType::STRING, ['city']), 'address-postalcode' => new FhirSearchParameterDefinition('address-postalcode', SearchFieldType::STRING, ['postal_code', "zip"]), 'address-state' => new FhirSearchParameterDefinition('address-state', SearchFieldType::STRING, ['state']), - 'name' => new FhirSearchParameterDefinition('name', SearchFieldType::STRING, ['name']) + 'name' => new FhirSearchParameterDefinition('name', SearchFieldType::STRING, ['name']), + '_lastUpdated' => $this->getLastModifiedSearchField() ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['last_updated']); + } + /** * Searches for OpenEMR records using OpenEMR search parameters * @param openEMRSearchParameters OpenEMR search fields @@ -160,10 +166,14 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) { $organizationResource = new FHIROrganization(); - $fhirMeta = new FHIRMeta(); - $fhirMeta->setVersionId('1'); - $fhirMeta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); - $organizationResource->setMeta($fhirMeta); + $meta = new FHIRMeta(); + $meta->setVersionId('1'); + if (!empty($dataRecord['last_updated'])) { + $meta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['last_updated'])); + } else { + $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } + $organizationResource->setMeta($meta); // facilities have no active / inactive state $organizationResource->setActive(true); diff --git a/src/Services/FHIR/Organization/FhirOrganizationInsuranceService.php b/src/Services/FHIR/Organization/FhirOrganizationInsuranceService.php index e5899daef0b..db6bdc6ff22 100644 --- a/src/Services/FHIR/Organization/FhirOrganizationInsuranceService.php +++ b/src/Services/FHIR/Organization/FhirOrganizationInsuranceService.php @@ -59,10 +59,16 @@ protected function loadSearchParameters() 'address-city' => new FhirSearchParameterDefinition('address-city', SearchFieldType::STRING, ['city']), 'address-postalcode' => new FhirSearchParameterDefinition('address-postalcode', SearchFieldType::STRING, ["zip"]), 'address-state' => new FhirSearchParameterDefinition('address-state', SearchFieldType::STRING, ['state']), - 'name' => new FhirSearchParameterDefinition('name', SearchFieldType::STRING, ['name']) + 'name' => new FhirSearchParameterDefinition('name', SearchFieldType::STRING, ['name']), + '_lastUpdated' => $this->getLastModifiedSearchField() ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['last_updated']); + } + protected function searchForOpenEMRRecords($openEMRSearchParameters): ProcessingResult { if (!isset($openEMRSearchParameters['name'])) { @@ -91,10 +97,14 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) { $organizationResource = new FHIROrganization(); - $fhirMeta = new FHIRMeta(); - $fhirMeta->setVersionId('1'); - $fhirMeta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); - $organizationResource->setMeta($fhirMeta); + $meta = new FHIRMeta(); + $meta->setVersionId('1'); + if (!empty($dataRecord['last_updated'])) { + $meta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['last_updated'])); + } else { + $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } + $organizationResource->setMeta($meta); $organizationResource->setActive($dataRecord['inactive'] == '0'); $narrativeText = trim($dataRecord['name'] ?? ""); diff --git a/src/Services/FHIR/Organization/FhirOrganizationProcedureProviderService.php b/src/Services/FHIR/Organization/FhirOrganizationProcedureProviderService.php index 52aced40b2f..a7caed3aefa 100644 --- a/src/Services/FHIR/Organization/FhirOrganizationProcedureProviderService.php +++ b/src/Services/FHIR/Organization/FhirOrganizationProcedureProviderService.php @@ -55,10 +55,16 @@ protected function loadSearchParameters() { return [ '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]), - 'name' => new FhirSearchParameterDefinition('name', SearchFieldType::STRING, ['name']) + 'name' => new FhirSearchParameterDefinition('name', SearchFieldType::STRING, ['name']), + '_lastUpdated' => $this->getLastModifiedSearchField() ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['last_updated']); + } + protected function searchForOpenEMRRecords($openEMRSearchParameters): ProcessingResult { if (!isset($openEMRSearchParameters['name'])) { @@ -82,10 +88,14 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) { $organizationResource = new FHIROrganization(); - $fhirMeta = new FHIRMeta(); - $fhirMeta->setVersionId('1'); - $fhirMeta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); - $organizationResource->setMeta($fhirMeta); + $meta = new FHIRMeta(); + $meta->setVersionId('1'); + if (!empty($dataRecord['last_updated'])) { + $meta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['last_updated'])); + } else { + $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } + $organizationResource->setMeta($meta); $organizationResource->setActive($dataRecord['active'] == '1'); $narrativeText = trim($dataRecord['name'] ?? ""); diff --git a/src/Services/FHIR/Procedure/FhirProcedureOEProcedureService.php b/src/Services/FHIR/Procedure/FhirProcedureOEProcedureService.php index 12507f24be4..233abb18d5e 100644 --- a/src/Services/FHIR/Procedure/FhirProcedureOEProcedureService.php +++ b/src/Services/FHIR/Procedure/FhirProcedureOEProcedureService.php @@ -72,9 +72,15 @@ protected function loadSearchParameters() 'patient' => $this->getPatientContextSearchField(), 'date' => new FhirSearchParameterDefinition('date', SearchFieldType::DATETIME, ['report_date']), '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('report_uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField(), ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['report_date']); + } + /** * Searches for OpenEMR records using OpenEMR search parameters * @param openEMRSearchParameters OpenEMR search fields @@ -110,13 +116,17 @@ protected function searchForOpenEMRRecords($openEMRSearchParameters): Processing public function parseOpenEMRRecord($dataRecord = array(), $encode = false) { $procedureResource = new FHIRProcedure(); + $report = array_pop($dataRecord['reports']); $meta = new FHIRMeta(); $meta->setVersionId('1'); - $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + if (!empty($report['date'])) { + $meta->setLastUpdated(UtilsService::getLocalDateAsUTC($report['date'])); + } else { + $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } $procedureResource->setMeta($meta); - $report = array_pop($dataRecord['reports']); $id = new FHIRId(); $id->setValue($report['uuid']); diff --git a/src/Services/FHIR/Procedure/FhirProcedureSurgeryService.php b/src/Services/FHIR/Procedure/FhirProcedureSurgeryService.php index 367e6df2e68..040e6a3f8e4 100644 --- a/src/Services/FHIR/Procedure/FhirProcedureSurgeryService.php +++ b/src/Services/FHIR/Procedure/FhirProcedureSurgeryService.php @@ -57,9 +57,15 @@ protected function loadSearchParameters() 'patient' => $this->getPatientContextSearchField(), 'date' => new FhirSearchParameterDefinition('date', SearchFieldType::DATETIME, ['begdate']), '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]), + '_lastUpdated' => $this->getLastModifiedSearchField(), ]; } + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return new FhirSearchParameterDefinition('_lastUpdated', SearchFieldType::DATETIME, ['date_modified']); + } + /** * Searches for OpenEMR records using OpenEMR search parameters * @param openEMRSearchParameters OpenEMR search fields @@ -85,7 +91,11 @@ public function parseOpenEMRRecord($dataRecord = array(), $encode = false) $meta = new FHIRMeta(); $meta->setVersionId('1'); - $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + if (!empty($dataRecord['date_modified'])) { + $meta->setLastUpdated(UtilsService::getLocalDateAsUTC($dataRecord['date_modified'])); + } else { + $meta->setLastUpdated(UtilsService::getDateFormattedAsUTC()); + } $procedureResource->setMeta($meta); $id = new FHIRId(); diff --git a/src/Services/FHIR/Traits/FhirBulkExportDomainResourceTrait.php b/src/Services/FHIR/Traits/FhirBulkExportDomainResourceTrait.php index 4629fd88247..68c408fc4b1 100644 --- a/src/Services/FHIR/Traits/FhirBulkExportDomainResourceTrait.php +++ b/src/Services/FHIR/Traits/FhirBulkExportDomainResourceTrait.php @@ -22,6 +22,10 @@ use OpenEMR\FHIR\R4\FHIRResource\FHIRDomainResource; use OpenEMR\Services\FHIR\IPatientCompartmentResourceService; use OpenEMR\Services\FHIR\IResourceReadableService; +use OpenEMR\Services\Search\DateSearchField; +use OpenEMR\Services\Search\FhirSearchParameterDefinition; +use OpenEMR\Services\Search\ISearchField; +use OpenEMR\Services\Search\SearchComparator; use OpenEMR\Services\Search\TokenSearchField; trait FhirBulkExportDomainResourceTrait @@ -68,6 +72,10 @@ public function export(ExportStreamWriter $writer, ExportJob $job, $lastResource $searchParams[$searchField->getName()] = implode(",", $patientUuids); } } + $searchField = $this->getLastModifiedSearchField(); + if ($searchField !== null) { + $searchParams[$searchField->getName()] = $job->getResourceIncludeSearchParamValue(); + } // if we can grab our list of patient ids from the export job... $processingResult = $this->getAll($searchParams); @@ -80,4 +88,9 @@ public function export(ExportStreamWriter $writer, ExportJob $job, $lastResource $lastResourceIdExported = $record->getId(); } } + + public function getLastModifiedSearchField(): ?FhirSearchParameterDefinition + { + return null; + } } diff --git a/src/Services/FHIR/UtilsService.php b/src/Services/FHIR/UtilsService.php index 8906169b2e8..2c570d0d12c 100644 --- a/src/Services/FHIR/UtilsService.php +++ b/src/Services/FHIR/UtilsService.php @@ -361,6 +361,20 @@ public static function getDateFormattedAsUTC(): string return (new \DateTime())->format(DATE_ATOM); } + public static function getLocalTimestampAsUTCDate($date) + { + // make this assumption explicit that we are using the current timezone specified in PHP + // when we use strtotime or gmdate we get bad behavior when dealing with DST + // we really should be storing dates internally as UTC instead of local time... but until that happens we have + // to do this. + // note this is what we were using before + // $date = gmdate('c', strtotime($dataRecord['date'])); + // w/ DST the date 2015-06-22 00:00:00 server time becomes 2015-06-22T04:00:00+00:00 w/o DST the server time becomes 2015-06-22T00:00:00-04:00 + $date = new \DateTime("@" . $date, new \DateTimeZone(date('P'))); + $utcDate = $date->format(DATE_ATOM); + return $utcDate; + } + public static function getLocalDateAsUTC($date) { // make this assumption explicit that we are using the current timezone specified in PHP diff --git a/src/Services/GroupService.php b/src/Services/GroupService.php index 5fc0ed3cbaa..cf6bd458816 100644 --- a/src/Services/GroupService.php +++ b/src/Services/GroupService.php @@ -46,7 +46,7 @@ public function searchPatientProviderGroups($search = array(), $isAndCondition = { // we inner join on status in case we ever decide to add a status property (and layers above this one can rely // on the property without changing code). - $sql = "SELECT + $sqlSelectFull = "SELECT patient_provider_groups.uuid ,patient_provider_groups.provider_id ,patient_provider_groups.provider_fname @@ -57,15 +57,20 @@ public function searchPatientProviderGroups($search = array(), $isAndCondition = ,patient_provider_groups.patient_fname ,patient_provider_groups.patient_mname ,patient_provider_groups.patient_lname - FROM ( + ,patient_provider_groups.creation_date + ,patient_provider_groups.patient_last_updated "; + $sqlIds = "SELECT DISTINCT patient_provider_groups.uuid "; + $sqlFrom = "FROM ( SELECT uuid_mapping.target_uuid AS pruuid ,uuid_mapping.uuid + ,uuid_mapping.created AS `creation_date` ,users.id AS provider_id ,users.fname AS provider_fname ,users.lname AS provider_lname ,users.mname AS provider_mname ,patients.uuid AS puuid + ,patients.last_updated AS patient_last_updated ,patients.title AS patient_title ,patients.fname AS patient_fname ,patients.mname AS patient_mname @@ -80,11 +85,18 @@ public function searchPatientProviderGroups($search = array(), $isAndCondition = $whereClause = FhirSearchWhereClauseBuilder::build($search, $isAndCondition); - $sql .= $whereClause->getFragment(); + $sqlIds .= $sqlFrom . $whereClause->getFragment(); $sqlBindArray = $whereClause->getBoundValues(); - $statementResults = QueryUtils::sqlStatementThrowException($sql, $sqlBindArray); - - $processingResult = $this->hydratePatientProviderSearchResultsFromQueryResource($statementResults); + $uuids = QueryUtils::fetchTableColumn($sqlIds, 'uuid', $sqlBindArray); + if (!empty($uuids)) { + // TODO: if we have a LARGE number of provider groups we will reach our max parameter count here... + // need to do optimization here for large # of providers. + $sqlSelectFull .= $sqlFrom . " WHERE patient_provider_groups.uuid IN (" . str_repeat("?, ", count($uuids) - 1) . "? )"; + $statementResults = QueryUtils::sqlStatementThrowException($sqlSelectFull, $uuids); + $processingResult = $this->hydratePatientProviderSearchResultsFromQueryResource($statementResults); + } else { + $processingResult = new ProcessingResult(); + } return $processingResult; } @@ -112,6 +124,7 @@ private function hydratePatientProviderSearchResultsFromQueryResource($queryReso $record = [ 'uuid' => $recordUuid ,'name' => $groupName + ,'last_modified_date' => $dbRecord['patient_last_updated'] ?? $dbRecord['creation_date'] ,'patients' => [] ]; $orderedList[] = $recordUuid; diff --git a/src/Services/ImmunizationService.php b/src/Services/ImmunizationService.php index fd0636d8526..3efc4c2a821 100644 --- a/src/Services/ImmunizationService.php +++ b/src/Services/ImmunizationService.php @@ -76,6 +76,7 @@ public function search($search, $isAndCondition = true) education_date, note, create_date, + update_date, amount_administered, amount_administered_unit, expiration_date, @@ -92,7 +93,7 @@ public function search($search, $isAndCondition = true) providers.provider_uuid, providers.provider_npi, providers.provider_username, - + IF( IF( information_source = 'new_immunization_record' AND @@ -133,7 +134,7 @@ public function search($search, $isAndCondition = true) notes AS refusal_reason_cdc_nip_code, codes AS refusal_reason_codes, title AS refusal_reason_description - FROM list_options + FROM list_options WHERE list_id = 'immunization_refusal_reason' ) refusal_reasons ON immunizations.refusal_reason = refusal_reasons.refusal_reason_id"; diff --git a/src/Services/InsuranceCompanyService.php b/src/Services/InsuranceCompanyService.php index 037f26aa7ca..f0d90acd33c 100644 --- a/src/Services/InsuranceCompanyService.php +++ b/src/Services/InsuranceCompanyService.php @@ -151,7 +151,9 @@ public function search($search, $isAndCondition = true) $sql .= " a.state,"; $sql .= " a.zip,"; $sql .= " a.plus_four,"; - $sql .= " a.country"; + $sql .= " a.country,"; + $sql .= " i.date_created,"; + $sql .= " i.last_updated"; $sql .= " FROM insurance_companies i "; $sql .= " LEFT JOIN (SELECT line1,line2,city,state,zip,plus_four,country,foreign_id FROM addresses) a ON i.id = a.foreign_id"; // the foreign_id here is a globally unique sequence so there is no conflict. diff --git a/src/Services/ListService.php b/src/Services/ListService.php index ab3bc55f472..2bbf7aef5d7 100644 --- a/src/Services/ListService.php +++ b/src/Services/ListService.php @@ -15,6 +15,12 @@ namespace OpenEMR\Services; use OpenEMR\Common\Database\QueryUtils; +use OpenEMR\Services\Search\FhirSearchWhereClauseBuilder; +use OpenEMR\Services\Search\SearchFieldException; +use OpenEMR\Services\Search\SearchModifier; +use OpenEMR\Services\Search\StringSearchField; +use OpenEMR\Services\Search\TokenSearchField; +use OpenEMR\Validators\ProcessingResult; use Particle\Validator\Validator; use OpenEMR\Common\Uuid\UuidRegistry; @@ -65,6 +71,50 @@ public function getListOptionsForLists($lists) return $records; } + /** + * Allows searching on the top level lists in the lists_options table. Will return the top level lists that match + * the search criteria as well as the last updated date of the sublist. + * @param $search + * @param $isAndCondition + * @return ProcessingResult + */ + public function searchLists($search, $isAndCondition = true) + { + // TODO: @adunsulag this is copy-pasta from BaseService... need to investigate if we can just have ListService extend BaseService + $processingResult = new ProcessingResult(); + try { + $sql = "SELECT + lo.*, + sub_list.sublist_updated_date + FROM + list_options lo + JOIN( + SELECT lo2.list_id AS sublist_list_id, + MAX(last_updated) AS sublist_updated_date + FROM + list_options lo2 + WHERE + lo2.list_id != 'lists' + GROUP BY + list_id + ) sub_list + ON + lo.option_id = sub_list.sublist_list_id "; + $whereFragment = FhirSearchWhereClauseBuilder::build($search, $isAndCondition); + $sql .= $whereFragment->getFragment() . " AND lo.list_id = 'lists' ORDER BY lo.seq, lo.list_id, lo.option_id "; + $records = QueryUtils::fetchRecords($sql, $whereFragment->getBoundValues()); + if (!empty($records)) { + foreach ($records as $row) { + $processingResult->addData($row); + } + } + } catch (SearchFieldException $exception) { + $processingResult->setValidationMessages([$exception->getField() => $exception->getMessage()]); + } + + return $processingResult; + } + public function getListIds() { $sql = "SELECT DISTINCT list_id FROM list_options ORDER BY list_id"; diff --git a/src/Services/LocationService.php b/src/Services/LocationService.php index a1cb52d82f5..44b8a929dbe 100644 --- a/src/Services/LocationService.php +++ b/src/Services/LocationService.php @@ -73,8 +73,9 @@ public function getAll($search = array(), $isAndCondition = true) null as fax, null as website, email, + `date` AS last_updated, "' . self::TYPE_PATIENT . '" AS `type` - from + from patient_data UNION SELECT uuid as table_uuid, @@ -88,8 +89,9 @@ public function getAll($search = array(), $isAndCondition = true) fax, website, email, + last_updated, "' . self::TYPE_FACILITY . '" AS `type` - from + from facility UNION SELECT uuid as table_uuid, @@ -103,8 +105,9 @@ public function getAll($search = array(), $isAndCondition = true) fax, url as website, email, + last_updated, "' . self::TYPE_USER . '" AS `type` - from + from users ) as location LEFT JOIN uuid_mapping ON uuid_mapping.target_uuid=location.table_uuid AND uuid_mapping.resource="Location"'; diff --git a/src/Services/PractitionerRoleService.php b/src/Services/PractitionerRoleService.php index da965b2f277..49e56954515 100644 --- a/src/Services/PractitionerRoleService.php +++ b/src/Services/PractitionerRoleService.php @@ -54,13 +54,18 @@ public function search($search, $isAndCondition = true) providers.user_name, providers.provider_id, providers.provider_uuid, + providers.provider_last_updated, facilities.facility_uuid, facilities.facility_name, role_codes.role_code, role_codes.role_title, + role_codes.role_last_updated, + specialty_codes.specialty_code, specialty_codes.specialty_title, + specialty_codes.specialty_last_updated, + physician_types.physician_type_codes, physician_types.physician_type, physician_types.physician_type_title @@ -68,13 +73,13 @@ public function search($search, $isAndCondition = true) select facility_user_ids.uuid AS facility_role_uuid, facility_user_ids.id AS facility_role_id, - -- field_value AS provider_id, facility_user_ids.facility_id, uid AS user_id, -- we are treating the user_id as the provider id -- TODO: @adunsulag figure out whether we should actually be using the user entered provider_id uid AS provider_id, users.uuid AS provider_uuid, + users.last_updated AS provider_last_updated, users.physician_type, CONCAT(COALESCE(users.fname,''), IF(users.mname IS NULL OR users.mname = '','',' '),COALESCE(users.mname,''), @@ -94,7 +99,9 @@ public function search($search, $isAndCondition = true) field_id, role.title AS role_title, facility_id, - uid AS user_id + uid AS user_id, + facility_user_ids.last_updated AS role_last_updated, + facility_user_ids.date_created AS role_date_created FROM facility_user_ids JOIN @@ -119,7 +126,9 @@ public function search($search, $isAndCondition = true) specialty.title AS specialty_title, field_id, facility_id, - uid AS user_id + uid AS user_id, + facilities_specialty.last_updated AS specialty_last_updated, + facilities_specialty.date_created AS specialty_date_created FROM facility_user_ids facilities_specialty JOIN diff --git a/src/Services/PrescriptionService.php b/src/Services/PrescriptionService.php index 6a4c013e9c6..ae87305b09f 100644 --- a/src/Services/PrescriptionService.php +++ b/src/Services/PrescriptionService.php @@ -82,7 +82,7 @@ public function getAll($search = array(), $isAndCondition = true, $puuidBind = n // order comes from our MedicationRequest intent value set, since we are only reporting on completed prescriptions // we will put the intent down as 'order' @see http://hl7.org/fhir/R4/valueset-medicationrequest-intent.html - $sql = "SELECT + $sql = "SELECT combined_prescriptions.uuid ,combined_prescriptions.source_table ,combined_prescriptions.drug @@ -100,6 +100,8 @@ public function getAll($search = array(), $isAndCondition = true, $puuidBind = n ,combined_prescriptions.note ,combined_prescriptions.status ,combined_prescriptions.drug_dosage_instructions + ,combined_prescriptions.date_added + ,combined_prescriptions.date_modified ,patient.puuid ,encounter.euuid ,practitioner.pruuid @@ -133,6 +135,7 @@ public function getAll($search = array(), $isAndCondition = true, $puuidBind = n ,IF(drugs.drug_code IS NULL, '', concat('RXCUI:',drugs.drug_code)) ) AS 'rxnorm_drugcode' ,date_added + ,date_modified ,COALESCE(prescriptions.unit,drugs.unit) AS unit ,prescriptions.`interval` ,COALESCE(prescriptions.`route`,drugs.`route`) AS 'route' @@ -142,12 +145,12 @@ public function getAll($search = array(), $isAndCondition = true, $puuidBind = n ,provider_id ,drugs.uuid AS drug_uuid ,prescriptions.drug_dosage_instructions - ,CASE + ,CASE WHEN prescriptions.end_date IS NOT NULL AND prescriptions.active = '1' THEN 'completed' WHEN prescriptions.active = '1' THEN 'active' ELSE 'stopped' END as 'status' - + FROM prescriptions LEFT JOIN @@ -166,6 +169,7 @@ public function getAll($search = array(), $isAndCondition = true, $puuidBind = n ,lists_medication.usage_category_title AS category_title ,lists.diagnosis AS rxnorm_drugcode ,`date` AS date_added + ,`modifydate` AS date_modified ,NULL as unit ,NULL as 'interval' ,NULL as `route` @@ -175,20 +179,20 @@ public function getAll($search = array(), $isAndCondition = true, $puuidBind = n ,users.id AS provider_id ,NULL as drug_uuid ,lists_medication.drug_dosage_instructions - ,CASE + ,CASE WHEN lists.enddate IS NOT NULL AND lists.activity = 1 THEN 'completed' WHEN lists.activity = 1 THEN 'active' ELSE 'stopped' END as 'status' FROM lists - LEFT JOIN + LEFT JOIN users ON users.username = lists.user LEFT JOIN lists_medication ON lists_medication.list_id = lists.id LEFT JOIN ( - select + select pid AS issues_encounter_pid , list_id AS issues_encounter_list_id -- lists have a 0..* relationship with issue_encounters which is a problem as FHIR treats medications as a 0.1 @@ -214,7 +218,7 @@ public function getAll($search = array(), $isAndCondition = true, $puuidBind = n ,title AS interval_title ,codes AS interval_codes FROM list_options - WHERE list_id='drug_route' + WHERE list_id='drug_route' ) intervals_list ON intervals_list.interval_id = combined_prescriptions.interval LEFT JOIN ( @@ -239,7 +243,7 @@ public function getAll($search = array(), $isAndCondition = true, $puuidBind = n ) encounter ON encounter.encounter = combined_prescriptions.encounter LEFT JOIN ( - SELECT + SELECT id AS practitioner_id ,uuid AS pruuid FROM users diff --git a/src/Services/ProcedureProviderService.php b/src/Services/ProcedureProviderService.php index d89f9f240f7..60f7115ebd7 100644 --- a/src/Services/ProcedureProviderService.php +++ b/src/Services/ProcedureProviderService.php @@ -58,6 +58,8 @@ public function search($search, $isAndCondition = true) ,prov.lab_director ,prov.active ,prov.type + ,prov.last_updated + ,prov.date_created FROM procedure_providers prov "; diff --git a/src/Services/Search/DateSearchField.php b/src/Services/Search/DateSearchField.php index d13dcde2731..917c78179b4 100644 --- a/src/Services/Search/DateSearchField.php +++ b/src/Services/Search/DateSearchField.php @@ -41,7 +41,7 @@ class DateSearchField extends BasicSearchField private const COMPARATOR_MATCH = "/^(\D{2})?(\d{4})(-\d{2})?(-\d{2})?(?:(T\d{2}:\d{2})(:\d{2})?)?(\.\d{1,4})?(Z|(\+|-)(\d{2}):(\d{2}))?$/"; // php's DATE_ATOM does not handle milliseconds so we have to add them in manually - private const DATE_ATOM_MILLISECONDS = 'Y-m-d\TH:i:s.uP'; + public const DATE_ATOM_MILLISECONDS = 'Y-m-d\TH:i:s.uP'; private const COMPARATOR_INDEX_FULL = 0; diff --git a/src/Services/Search/FHIRSearchFieldFactory.php b/src/Services/Search/FHIRSearchFieldFactory.php index 79c94788264..34421e0295f 100644 --- a/src/Services/Search/FHIRSearchFieldFactory.php +++ b/src/Services/Search/FHIRSearchFieldFactory.php @@ -58,6 +58,16 @@ public function getFhirUrlResolver(): FhirUrlResolver return $this->fhirUrlResolver; } + /** + * @param $fhirSearchField + * @param FhirSearchParameterDefinition $definition + * @return void + */ + public function setSearchFieldDefinition(string $fhirSearchField, FhirSearchParameterDefinition $definition) + { + $this->resourceSearchParameters[$fhirSearchField] = $definition; + } + /** * Checks whethere the factory has a search definition for the passed in search field name * @param $fhirSearchField @@ -76,8 +86,8 @@ public function getSearchFieldDefinition($fhirSearchField): FhirSearchParameterD /** * Factory method to build a search field using the factory's search field definitions. - * @param $fhirSearchField The passed in parameter name for the search field the user agent sent. Can contain search modifiers - * @param $fhirSearchValues The array of search values the user agent sent for the $fhirSearchField + * @param $fhirSearchField string The passed in parameter name for the search field the user agent sent. Can contain search modifiers + * @param $fhirSearchValues array The array of search values the user agent sent for the $fhirSearchField * @throws \InvalidArgumentException If the factory does not have a search definition for $fhirSearchField * @return CompositeSearchField|DateSearchField|StringSearchField|TokenSearchField */ diff --git a/src/Services/SurgeryService.php b/src/Services/SurgeryService.php index 17d4c040ca8..6d54be74d14 100644 --- a/src/Services/SurgeryService.php +++ b/src/Services/SurgeryService.php @@ -58,7 +58,8 @@ public function search($search, $isAndCondition = true) encounter.euuid, recorders.recorder_npi, recorders.recorder_uuid, - recorders.recorder_username + recorders.recorder_username, + surgeries.date_modified FROM ( SELECT id @@ -71,6 +72,7 @@ public function search($search, $isAndCondition = true) ,`pid` ,`comments` ,`user` as surgery_recorder + ,`modifydate` AS date_modified FROM lists WHERE `type` = 'surgery' diff --git a/src/Services/UserService.php b/src/Services/UserService.php index ca63fde1cdd..5bdd1cb5e3a 100644 --- a/src/Services/UserService.php +++ b/src/Services/UserService.php @@ -254,13 +254,17 @@ public function search($search, $isAndCondition = true) phonecell, users.notes, state_license_number, - abook.title as abook_title"; + abook.title as abook_title, + last_updated "; if ($this->_includeUsername) { $sql .= ", username"; } + // grab our address book type, make sure to use the index w/ list_id and option_id $sql .= " FROM users - LEFT JOIN list_options as abook ON abook.option_id = users.abook_type"; + LEFT JOIN ( + SELECT list_id,option_id, title FROM list_options + ) abook ON abook.list_id = 'abook_type' AND abook.option_id = users.abook_type"; $whereClause = FhirSearchWhereClauseBuilder::build($search, $isAndCondition); $sql .= $whereClause->getFragment(); diff --git a/src/Services/Utils/DateFormatterUtils.php b/src/Services/Utils/DateFormatterUtils.php index 5152e5d9b9c..baaa47d3be4 100644 --- a/src/Services/Utils/DateFormatterUtils.php +++ b/src/Services/Utils/DateFormatterUtils.php @@ -140,4 +140,13 @@ public static function getTimeFormat($seconds = false) } return $formatted; } + + public static function getFormattedISO8601DateFromDateTime(\DateTime $dateTime): string + { + // ISO8601 doesn't support fractional dates so we need to change from microseconds to milliseconds + // TODO: @adunsulag this is a hack to get around the fact that PHP does microseconds and ISO8601 uses milliseconds + // , look at refactoring all of this so we don't have to do multiple date conversions up and down the stack. + $dateStr = substr($dateTime->format('Y-m-d\TH:i:s.u'), 0, -3) . $dateTime->format('P'); + return $dateStr; + } } diff --git a/src/Services/VitalsService.php b/src/Services/VitalsService.php index 94daf4ff138..bb338117c6d 100644 --- a/src/Services/VitalsService.php +++ b/src/Services/VitalsService.php @@ -115,6 +115,8 @@ public function search($search, $isAndCondition = true) ,vitals.ped_bmi ,vitals.ped_head_circ ,vitals.inhaled_oxygen_concentration + ,vitals.last_updated + ,forms.date_created ,details.details_id ,details.interpretation_list_id ,details.interpretation_option_id @@ -132,6 +134,7 @@ public function search($search, $isAndCondition = true) ,bpd,bps,weight,height,temperature,temp_method,pulse,respiration,BMI,BMI_status,waist_circ ,head_circ,oxygen_saturation,oxygen_flow_rate,inhaled_oxygen_concentration , ped_weight_height,ped_bmi,ped_head_circ + , last_updated FROM form_vitals ) vitals @@ -143,6 +146,7 @@ public function search($search, $isAndCondition = true) ,`user` ,deleted ,formdir + ,`date` AS date_created FROM forms ) forms ON vitals.id = forms.form_id @@ -151,9 +155,11 @@ public function search($search, $isAndCondition = true) encounter AS eid ,uuid AS euuid ,`date` AS encounter_date + ,pid AS encounter_pid FROM form_encounter - ) encounters ON encounters.eid = forms.encounter + -- use both columns in order to leverage the index + ) encounters ON encounters.encounter_pid = forms.form_pid AND encounters.eid = forms.encounter LEFT JOIN ( SELECT diff --git a/swagger/openemr-api.yaml b/swagger/openemr-api.yaml index 1f83fdf6430..b9d176c4b12 100644 --- a/swagger/openemr-api.yaml +++ b/swagger/openemr-api.yaml @@ -3854,6 +3854,13 @@ paths: required: false schema: type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string - name: patient in: query @@ -3939,6 +3946,13 @@ paths: required: false schema: type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string - name: patient in: query @@ -4013,6 +4027,13 @@ paths: required: false schema: type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string - name: patient in: query @@ -4103,6 +4124,13 @@ paths: required: false schema: type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string - name: patient in: query @@ -4152,6 +4180,13 @@ paths: required: true schema: type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string responses: '200': description: 'Standard Response' @@ -4190,6 +4225,13 @@ paths: required: false schema: type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string - name: patient in: query @@ -4272,6 +4314,13 @@ paths: required: false schema: type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string - name: patient in: query @@ -4359,6 +4408,13 @@ paths: required: false schema: type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string - name: patient in: query @@ -4444,6 +4500,13 @@ paths: required: false schema: type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string - name: patient in: query @@ -4551,6 +4614,13 @@ paths: required: false schema: type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string - name: patient in: query @@ -4727,6 +4797,13 @@ paths: required: false schema: type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string - name: patient in: query @@ -4818,6 +4895,13 @@ paths: required: false schema: type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string - name: patient in: query @@ -4899,6 +4983,13 @@ paths: required: false schema: type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string - name: patient in: query @@ -4989,6 +5080,20 @@ paths: required: false schema: type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string - name: patient in: query @@ -5072,6 +5177,13 @@ paths: required: false schema: type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string responses: '200': description: 'Standard Response' @@ -5137,6 +5249,21 @@ paths: tags: - fhir description: 'Returns a list of Medication resources.' + parameters: + - + name: _id + in: query + description: 'The uuid for the Medication resource.' + required: false + schema: + type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string responses: '200': description: 'Standard Response' @@ -5210,6 +5337,13 @@ paths: required: false schema: type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string - name: patient in: query @@ -5309,6 +5443,13 @@ paths: required: false schema: type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string - name: patient in: query @@ -5413,6 +5554,13 @@ paths: required: false schema: type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string - name: name in: query @@ -5677,6 +5825,13 @@ paths: required: false schema: type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string - name: identifier in: query @@ -5997,6 +6152,20 @@ paths: - fhir description: 'Returns a list of Person resources.' parameters: + - + name: _id + in: query + description: 'The uuid for the Person resource.' + required: false + schema: + type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string - name: name in: query @@ -6149,6 +6318,13 @@ paths: required: false schema: type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string - name: name in: query @@ -6401,6 +6577,20 @@ paths: - fhir description: 'Returns a list of PractitionerRole resources.' parameters: + - + name: _id + in: query + description: 'The uuid for the PractitionerRole resource.' + required: false + schema: + type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string - name: specialty in: query @@ -6488,6 +6678,13 @@ paths: required: false schema: type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string - name: patient in: query @@ -6646,6 +6843,13 @@ paths: required: false schema: type: string + - + name: _lastUpdated + in: query + description: 'Allows filtering resources by the _lastUpdated field. A FHIR Instant value in the format YYYY-MM-DDThh:mm:ss.sss+zz:zz. See FHIR date/time modifiers for filtering options (ge,gt,le, etc)' + required: false + schema: + type: string responses: '200': description: 'Standard Response' @@ -7540,7 +7744,6 @@ components: - subscriber_postal_code - subscriber_city - subscriber_state - - subscriber_country - subscriber_sex - accept_assignment properties: