4 * ClinicalNotesService.php
6 * @link http://www.open-emr.org
7 * @author Stephen Nielson <stephen@nielson.org>
8 * @copyright Copyright (c) 2021 Stephen Nielson <stephen@nielson.org>
9 * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
12 namespace OpenEMR\Services
;
14 use OpenEMR\Common\Database\QueryUtils
;
15 use OpenEMR\Common\Uuid\UuidRegistry
;
16 use OpenEMR\Services\Search\FhirSearchWhereClauseBuilder
;
17 use OpenEMR\Services\Search\ISearchField
;
18 use OpenEMR\Services\Search\SearchFieldException
;
19 use OpenEMR\Services\Search\TokenSearchField
;
20 use OpenEMR\Services\Search\TokenSearchValue
;
21 use OpenEMR\Validators\ProcessingResult
;
23 class ClinicalNotesService
extends BaseService
25 const TABLE_NAME
= "form_clinical_notes";
27 const ACTIVITY_ACTIVE
= 1;
28 const ACTIVITY_INACTIVE
= 0;
30 public function __construct()
32 parent
::__construct(self
::TABLE_NAME
);
33 UuidRegistry
::createMissingUuidsForTables(['form_clinical_notes']);
37 * Returns a list of records matching the search criteria.
38 * Search criteria is conveyed by array where key = field/column name, value is an ISearchField
39 * If an empty array of search criteria is provided, all records are returned.
41 * The search will grab the intersection of all possible values if $isAndCondition is true, otherwise it returns
42 * the union (logical OR) of the search.
44 * More complicated searches with various sub unions / intersections can be accomplished through a CompositeSearchField
45 * that allows you to combine multiple search clauses on a single search field.
47 * @param ISearchField[] $search Hashmap of string => ISearchField where the key is the field name of the search field
48 * @param bool $isAndCondition Whether to join each search field with a logical OR or a logical AND.
49 * @return ProcessingResult The results of the search.
51 public function search($search, $isAndCondition = true)
53 // because we can have two clinical note table options (one from contrib etc), we will return an empty search
54 // result for now if the table does not conform to our CORE clinical_notes
55 $fields = $this->getFields();
56 $processingResult = new ProcessingResult();
57 if (array_search('code', $fields) === false) {
58 // there is no data right now for the other form so we leave it be.
59 return $processingResult;
62 // we leave status to be current, if we ever support entered-in-error, or superseded we can do that here.
74 ,notes.clinical_notes_type
75 ,notes.note_related_to
76 ,notes.clinical_notes_category
77 ,lo_category.category_code
78 ,lo_category.category_title
83 ,encounters.encounter_date
101 ,clinical_notes_category
114 ) forms ON forms.form_id = notes.form_id
119 ,`date` AS encounter_date
122 ) encounters ON encounters.eid = forms.encounter
129 ) patients ON forms.form_pid = patients.pid
140 ) users ON notes.`user` = users.username
144 notes AS category_code
145 ,title AS category_title
150 list_id = 'Clinical_Note_Category'
151 ) lo_category ON notes.clinical_notes_category = lo_category.option_id
153 $whereClause = FhirSearchWhereClauseBuilder
::build($search, $isAndCondition);
155 $sql .= $whereClause->getFragment();
156 $sqlBindArray = $whereClause->getBoundValues();
158 $statementResults = QueryUtils
::sqlStatementThrowException($sql, $sqlBindArray);
159 while ($row = sqlFetchArray($statementResults)) {
160 $resultRecord = $this->createResultRecordFromDatabaseResult($row);
161 $processingResult->addData($resultRecord);
163 } catch (SearchFieldException
$exception) {
164 $processingResult->setValidationMessages([$exception->getField() => $exception->getMessage()]);
166 return $processingResult;
169 protected function createResultRecordFromDatabaseResult($row)
171 // TODO: @adunsulag this is just for testing until we can figure out the right uuid schematic
172 if (!empty($row['code'])) {
173 $row['code'] = $this->addCoding($row['code']);
175 return parent
::createResultRecordFromDatabaseResult($row);
178 public function getUuidFields(): array
180 return ['uuid', 'puuid', 'euuid', 'user_uuid'];
184 * Sets the activity status flag for the given form_clinical_notes record to either be active(1) or inactive(0).
185 * @param $clinicalNoteId The unique record id for the form_clinical_notes table
186 * @param $pid The unique patient pid from the patient_data table
187 * @param $encounter The unique encounter id from the form_encounters table
188 * @param $activity The activity status: active(1) or inactive(0).
190 public function setActivityForClinicalRecord($clinicalNoteId, $pid, $encounter, $activity)
192 $sql = "UPDATE `form_clinical_notes` SET activity = ? WHERE id=? AND pid = ? AND encounter = ?";
193 $bindings = array($activity, $clinicalNoteId, $pid, $encounter);
194 QueryUtils
::sqlStatementThrowException($sql, $bindings);
198 * Given a form id remove all of the clinical note records connected to that form.
199 * @param $formId The unique id from the forms table
200 * @param $pid The unique patient pid from the patient_data table
201 * @param $encounter The unique encounter id from the form_encounters table
203 public function clearClinicalRecordsForForm($formId, $pid, $encounter)
205 QueryUtils
::sqlStatementThrowException(
206 "DELETE FROM `form_clinical_notes` WHERE form_id=? AND pid = ? AND encounter = ?",
207 array($formId, $pid, $encounter)
211 public function getClinicalRecordNoteById($id)
213 $sql = "select * from `form_clinical_notes` WHERE id = ? ";
214 $records = QueryUtils
::fetchRecords($sql, [$id]);
215 if (!empty($records)) {
221 public function createClinicalNotesParentForm($pid, $encounter, $userauthorized)
223 $largestId = QueryUtils
::fetchSingleValue("SELECT COALESCE(MAX(form_id), 0) as largestId FROM `form_clinical_notes`", 'largestId');
225 if ($largestId > 0) {
226 $form_id = $largestId +
1;
231 addForm($encounter, "Clinical Notes Form", $form_id, "clinical_notes", $pid, $userauthorized);
235 public function saveArray(array $record)
237 $form_id = $record['form_id'] ??
null;
238 $id = $record['id'] ??
null;
239 $pid = $record['pid'] ??
null;
240 $encounter = $record['encounter'] ??
null;
241 $userauthorized = $record['authorized'] ??
null;
242 $existingRecord = [];
245 if (empty($form_id) ||
empty($pid) ||
empty($encounter) ||
empty($record) ||
$userauthorized === null) {
246 throw new \
InvalidArgumentException("Record, form_id, pid, authorized and encounter must be populated");
249 unset($record['id']);
250 // we grab the existing record so we can populate the uuid if necessary
252 $existingRecord = $this->getClinicalRecordNoteById($id);
255 if (empty($form_id)) {
256 $largestId = QueryUtils
::fetchSingleValue("SELECT COALESCE(MAX(form_id), 0) as largestId FROM `form_clinical_notes`", 'largestId');
258 if ($largestId > 0) {
259 $record['form_id'] = $largestId +
1;
261 $record['form_id'] = 1;
264 addForm($encounter, "Clinical Notes Form", $record['form_id'], "clinical_notes", $pid, $userauthorized);
266 if (empty($existingRecord['uuid'])) {
267 $record['uuid'] = (new UuidRegistry(['table_name' => 'form_clinical_notes']))->createUuid();
270 $keys = array_keys($record);
271 $setValues = array_map(function ($val) {
272 return $val . " = ? ";
275 $sql = "UPDATE " . self
::TABLE_NAME
. " SET " . implode(", ", $setValues) . " WHERE id = ? ";
276 $bindValues = array_values($record);
278 QueryUtils
::sqlStatementThrowException($sql, $bindValues);
280 $sql = "INSERT INTO " . self
::TABLE_NAME
. " SET " . implode(", ", $setValues);
281 $bindValues = array_values($record);
282 $recordId = QueryUtils
::sqlInsert($sql, $bindValues);
283 $record['id'] = $recordId;
285 // if we want the id&uuid back we need to return the record here
289 public function getClinicalNoteIdsForPatientForm(int $formid, $pid, $encounter)
291 if (empty($formid) ||
empty($pid) ||
empty($encounter)) {
292 throw new \
InvalidArgumentException("formid, pid, and encounter must all be populated");
295 $sql = "SELECT id FROM `form_clinical_notes` WHERE `form_id`=? AND `pid` = ? AND `encounter` = ?";
296 return QueryUtils
::fetchTableColumn($sql, 'id', array($formid, $pid, $encounter));
300 * Retrieve all of the clinical notes for a given patient
302 * @return ProcessingResult
304 public function getClinicalNotesForPatient($pid): ProcessingResult
306 $search['pid'] = new TokenSearchField('pid', new TokenSearchValue($pid));
307 return $this->search($search);
310 public function getClinicalNotesForPatientForm(int $formid, $pid, $encounter)
312 if (empty($formid) ||
empty($pid)) {
313 throw new \
InvalidArgumentException("formid, and pid must all be populated");
316 $sql = "SELECT fcn.*, lo_category.title AS category_title, lo_category.notes AS category_code
317 FROM `form_clinical_notes` fcn
318 LEFT JOIN list_options lo_category ON lo_category.list_id = `Clinical_Notes_Category` AND lo_category.option_id = fcn.clinical_notes_category
319 LEFT JOIN list_options lo_type ON lo_type.list_id = `Clinical_Notes_Type` AND lo_type.option_id = fcn.clinical_notes_type
320 WHERE fcn.`form_id`=? AND fcn.`pid` = ? AND fcn.`encounter` = ?";
321 return QueryUtils
::fetchRecords($sql, array($formid, $pid, $encounter));
324 public function deleteClinicalNoteRecordForPatient($recordId, $pid, $encounter)
326 $sql = "DELETE FROM `form_clinical_notes` WHERE id = ? AND pid= ? AND encounter = ?";
327 QueryUtils
::sqlStatementThrowException($sql, [$recordId, $pid, $encounter]);
331 * Given a code (with or without LOINC prefix) determine if its a valid clinical note code that this service can
333 * @param $code string
334 * @return bool true if the code is valid, false otherwise
336 public function isValidClinicalNoteCode($code)
338 // make it a LOINC code
339 if (strpos($code, ":") === false) {
340 $code = "LOINC:" . $code;
342 $listService = new ListService();
343 $options = $listService->getOptionsByListName('Clinical_Note_Type', ['notes' => $code]);
344 return !empty($options);
347 public function getClinicalNoteTypes()
349 $listService = new ListService();
350 $options = $listService->getOptionsByListName('Clinical_Note_Type');
351 return $this->getListAsSelectList($options);
354 public function getClinicalNoteCategories()
356 $listService = new ListService();
357 $options = $listService->getOptionsByListName('Clinical_Note_Category');
358 return $this->getListAsSelectList($options);
361 private function getListAsSelectList($optionsList)
363 if (empty($optionsList)) {
368 foreach ($optionsList as $option) {
369 $selectList[] = ['value' => $option['option_id'], 'code' => $option['notes'], 'title' => $option['title']];