Appointments Table
You should now have a working CCL script that will collect your appointment data for your patient. To quickly test that our CCL script is working as expected, we can use the activity log to view not only the execution of our script, but also view the output as well.
- Return to your browser window and refresh the page with the browser refresh button. Your MPage should display the default "Create something stunning here!" message.
- Open the activity log by pressing the CTRL + / keys. You should see output similar to the screen below showing your script call as well as the time the script took to complete the operation.
-
On the left hand side of the screen you will see CustomService(1) listed under the Data Services menu. The (1) in the CustomService menu indicates that 1 custom service data set has been loaded.
Click on the CustomService(1) label to expand the view. You should now see your appointments label. Click the appointments label to view the content returned from CCL in the right window as shown below.
Appointment Summary Component
Although it is possible to write your code in the app component it is not recommended as it reduces the scalability of your Angular application. Instead, you should perform any initialization you need in your app component files and put your business logic in custom components and services. This will allow for easier maintenance and expandability in the future.
-
Create a new component called appointment-summary in the Visual Studio Code command
line.
ng generate component appointment-summary
-
Modify app.component.html so it references your new component and the
mpage-log-component as shown below.
<app-appointment-summary></app-appointment-summary> <mpage-log-component></mpage-log-component>
-
Open appointment-summary.component.ts and import AppointmentDataService and add it to
the constructor as appointmentDS. We will also set the change detection strategy to onPush to help reduce the
number of calls to our methods.
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { AppointmentDataService } from '../appointment-data.service'; @Component({ selector: 'app-appointment-summary', templateUrl: './appointment-summary.component.html', styleUrls: ['./appointment-summary.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) export class AppointmentSummaryComponent implements OnInit { constructor(public appointmentDS: AppointmentDataService) { } ngOnInit(): void { } }
-
Open appointment-data.service.ts add add the following tow public get methods.
// Returns the appointments data public get appointments(): any[] { return this.customService.get('appointments').appointments; } // Determine if appointments have been loaded public get appointmentsLoaded(): boolean { return this.customService.isLoaded('appointments'); }
The data collected for our appointments is currently stored as a Clinical Office Custom Service object. We are using the get method from the CustomService object to return the data we have stored under the appointments label.
Our appointments object contains an array called appointments which we are accessing directly. This means that when we make a call to the appointments get method it will return the data inside the appointments array rather than the appointments object itself. This just means we can refer to our data inside our HTML as appointmentDS.appointments instead of appointmentDS.appointments.appointments.
-
The majority of errors encountered in Angular (and JavaScript) is trying to access undefined or invalid data. Our appointment summary component is no exception. We will be relying on the existence of our appointments object being loaded.
Modify app.component.html so the app-appointment-summary component only loads once your appointmentsLoaded method qualifies as true. Your HTML code should be as follows:
<app-appointment-summary *ngIf="appointmentDS.appointmentsLoaded"></app-appointment-summary> <mpage-log-component></mpage-log-component>
-
Open appointment-summary.component.html and replace the content with the following line of code to generate
a table containing our appointment information.
<mpage-table [tableData]="appointmentDS.appointments"></mpage-table>
If you view your MPage in the browser you should see your appointment table appear with data provided your chosen patient has appointments qualified. We will discuss the mpage-table component in detail momentarily, but first I invite you to explore your MPage output by entering values in the filters and clicking on the titles to change the sorting of your data.
Intelligent Tables with mpage-table
The mpage-table component was designed with the purpose of allowing the development of rich Cerner aware tables with minimal effort by the developer. This new table control was introduced in the 3.5 version of Clinical Office as a solution to handle the common repetitive task of adding tables to MPages.
Creating your own tables using the Angular Material table control is a simple task but often time-consuming once you start adding features such as filtering, sorting and user defined column settings. It is still sometimes necessary to create your own custom tables, however over the years of developing MPages for clients we have found that the functionality the mpage-table control offers covers 90% of the table use cases.
The screenshot below shows our table along with the activity log open and displaying the appointments custom service entry. Please refer to this image as we describe some functionality provided to you with our simple implementation of the mpage-table component.
- Field titles are automatically derived from the field names in our JSON. Standard camelCase field names have spacing and capitalization applied to the field names converting values such as "apptType" to "Appt Type". Fields ending with "DtTm" have the value "DtTm" replaced with "Date/Time".
- Date and Date/Time fields are formatted automatically. The default is the Cerner "dd-MMM-yyyy HH:mm" format but can easily be changed with the optional dateFormat/dateTimeFormat properties available on the mpage-table component. The type of format applied is based entirely on the JSON field name. Fields ending with the word "Date" (e.g. "regDate") will use the dateFormat setting where fields ending with "DtTm" (e.g. "dischDtTm") will be formatted with the dateTimeFormat.
- Filters of loaded data are made available in the table. If the number of unique entries is low, a drop-down list of filterable options is made available. For columns with more than 15 unique items the drop-down list will be replaced by a searchable text field. The searchable text field allows wildcard values on both the left and right side of the text. Date fields are unique in that the filters expose a date range for filtering.
- JSON fields ending with Id or Cd are ignored and not displayed in your table. This data is typically not shown to end users but still exists in your data for additional logic you may need to perform.
- Column titles can be clicked to change the sort of the table data.
The mpage-table component offers many more features and as we progress through this lesson we will explore some of them, starting with adding the toolbar to allow user customization.
-
Add [toolbar]="true" to your mpage-table component in
appointment-summary.component.html as shown below.
<mpage-table [tableData]="appointmentDS.appointments" [toolbar]="true"></mpage-table>
- Refresh your page in the browser. You should now see a toolbar on the left side of the table. This toolbar provides buttons to clear all filters, change sorting and setup multiple-column sorting, re-configure the column layout of the table and finally clear any customizations.
Saving User Column Configurations
The toolbar offers a great deal of flexibility to our users however all settings are lost as soon as the page is refreshed. With a few steps and the help of the DMInfo read/write features included in the CustomService data service we can save user preferences.
-
For this next step, we will be using the IColumnConfig interface. Please add it to your import statement in
appointment-data.service.ts as follows:
import { Injectable } from '@angular/core'; V4+ import { CustomService, IColumnConfig } from '@clinicaloffice/clinical-office-mpage-core'; < V3 import { CustomService, IColumnConfig } from '@clinicaloffice/clinical-office-mpage';
-
Add [(columnConfig)]="appointmentDS.columnConfig" to your mpage-table component in
appointment-summary.component.html as shown below.
<mpage-table [tableData]="appointmentDS.appointments" [toolbar]="true" [(columnConfig)]="appointmentDS.columnConfig"></mpage-table>
You will notice an error appear as we have not created the columnConfig object in our data service. -
Open appointment-data.service.ts and add the columnConfig object to your public
variables. This new object should be added after the export class statement and before the constructor as
shown below.
export class AppointmentDataService { public loading = false; public columnConfig: IColumnConfig = {columns: [], columnSort: [], freezeLeft: 0}; constructor(public customService: CustomService) { }
The columnConfig object is an IColumnConfig type which requires a columns array, columnSort array and freezeLeft numeric variable. The two changes above assign the internal column configuration of our table as a new object variable called columnConfig. Any changes to columnConfig either externally or through the table control will update this new object. -
We have any number of ways that we can trigger saving our column preferences but for the purpose of this project we are going to add a toolbar to the top of the screen with a simple save button.
From the console in Visual Studio Code, create a new component called toolbar with the statement:
ng generate component toolbar
-
Add the app-toolbar component to the top of
appointment-summary.component.html. Your HTML file should appear as follows:
<app-toolbar></app-toolbar> <mpage-table [tableData]="appointmentDS.appointments" [toolbar]="true" [(columnConfig)]="appointmentDS.columnConfig"></mpage-table>
-
Open toolbar.component.ts and inject the AppointmentDataService as a new variable
called appointmentDS. Don't forget to also set the change detection strategy to onPush. Your file should
appear as follows:
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; import { AppointmentDataService } from '../appointment-data.service'; @Component({ selector: 'app-toolbar', templateUrl: './toolbar.component.html', styleUrls: ['./toolbar.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) export class ToolbarComponent implements OnInit { constructor(public appointmentDS: AppointmentDataService) { } ngOnInit(): void { } }
-
Our toolbar component is eventually going to contain user submission options, but for now we are only concerned with the preference save. The Angular Material library offers components including toolbars, buttons and icon controls that make putting together an attractive toolbar trivial.
Replace the content of toolbar-component.html with the following code.
<mat-toolbar> <button mat-icon-button title="Save user preferences" (click)="appointmentDS.savePreferences()"> <mat-icon>save</mat-icon> </button> </mat-toolbar>
The code above renders a material toolbar that spans the entire width of the browser which contains a single button with a disk icon. Hovering over the icon displays the text "Save user preferences" and clicking the button will trigger our soon-to-be written appointmentDS.savePreferences() method.
-
We will want to notify our end users that their preferences have saved and to do so we will make use of the
SnackBar component in Angular Material. Open appointment-data.service.ts and add an
import for the MatSnackBar component in your import section.
import { Injectable } from '@angular/core'; V4+ import { CustomService, IColumnConfig } from '@clinicaloffice/clinical-office-mpage-core'; < V3 import { CustomService, IColumnConfig } from '@clinicaloffice/clinical-office-mpage'; import { MatSnackBar } from '@angular/material/snack-bar';
-
Assign the MatSnackBar component to a private variable called snackBar in your constructor.
constructor(public customService: CustomService, private snackBar: MatSnackBar) { }
-
Add a new method called savePreferences() near the bottom of the data service class and type in the following
code. We will explain it step-by-step after you type it in.
// Save user preferences public savePreferences(): void { this.customService.executeDmInfoAction('saveUserPrefs', 'w', [ { infoDomain: '1trn_appt_mp', infoName: 'column_prefs', infoDate: new Date(), infoChar: '', infoNumber: 0, infoLongText: JSON.stringify({ columnConfig: this.columnConfig }), infoDomainId: this.customService.mpage.prsnlId } ], () => { this.snackBar.open('Saved Preferences.', 'Ok', {duration: 1000}); }); }
- We call the executeDMInfoAction method found in the custom service with a id name of "saveUserPrefs" and have indicated that we are going to write a record with the "w" value in the second parameter. Our this parameter is an array that represents a single entry in the Cerner DM_INFO table. We are required to complete all the fields however the ones that concern us the most are the infoDomain field which identifies our application, the infoName field represents the type of data we are saving, infoLongText represents our preference data and infoDomainId represents the id of the user we are saving the data for.
- In our infoLongText entry, we are using the JSON.stringify method to convert our configuration data into text that can be stored in the database. We are also wrapping our columnConfig object into a new variable called columnConfig. This is in preparation for later in this class where we will also use the same code to store the user entry prompts we will be adding.
- The payload for the executeDmInfoAction also contains a callback method that opens the Angular Material Snackbar component with a "Saved Preferences." message that appears on screen for one second once the DM info action has completed.
- Test your code by changing the sort order of your table and re-arrange the columns. Click the save button and take a look at your debugger. You should see a new entry called "saveUserPrefs" which contains the payload you sent to CCL.
-
The next step is to load your preferences and apply them to your table. Start by adding a new method to
appointment-data.service.ts called loadPreferences as shown below.
// Load user preferences public loadPreferences(): void { this.loading = true; const prefMessage = this.customService.emptyDmInfo; prefMessage.infoDomain = '1trn_appt_mp'; prefMessage.infoName = 'column_prefs'; prefMessage.infoDomainId = this.customService.mpage.prsnlId this.customService.executeDmInfoAction('userPrefs', 'r', [ prefMessage ], () => { // Check for user preferences and assign them if (this.customService.isLoaded('userPrefs')) { const config = JSON.parse(this.customService.get('userPrefs').dmInfo[0].longText); this.columnConfig = config.columnConfig; } this.loadAppointments() }); }
- This method starts by assigning true the loading variable. We are still not using this variable but will be shortly.
- Next we create a variable called prefMessage that contains an empty DMInfo object. Since we only need three of our DM_INFO fields to read our data it is faster to copy the empty object and simply assign the values needed (infoDomain, infoName and infoDomainId).
- We call the executeDmInfoAction method again with a id name of 'userPrefs' with a read flag ('r') set in the second parameter. For the payload we pass our prefMessage object in the DMInfo array. Once the CCL call is completed, our callback code will be executed.
- In the callback method, the first task is to see if there are any values assigned to 'userPrefs'. For users who have never clicked our save button, the custom service value 'userPrefs' would not exist. Users who have saved prefs would have a value.
- Inside the customService.isLoaded('userPrefs') if statement, we assign a new variable called config with the JSON found in our first longText entry. Using the JSON.parse method this content becomes a valid JavaScript object we can use and manipulate.
- The second line in the if statement simply assigns the columnConfig object inside the config variable to our columnConfig object that controls the display of the table.
- The final line of code in the loadPreferences method calls our loadAppointments method to load our appointment data.
-
Our last step needed to have our user preferences working is to modify the
app.component.ts file. Simple change your this.appointmentDS.loadAppointments();
call to this.appointmentDS.loadPreferences();
ngOnInit(): void { // Grab any parameters in the URL (Used in Cerner Components) this.activatedRoute.queryParams.subscribe(params => { this.mPage.personId = params['personId'] ? parseInt(params['personId']) : this.mPage.personId; this.mPage.encntrId = params['encounterId'] ? parseInt(params['encounterId']) : this.mPage.encntrId; this.mPage.prsnlId = params['userId'] ? parseInt(params['userId']) : this.mPage.prsnlId; }); // Perform MPage Initialization setTimeout((e: any) => { this.mPage.setMaxInstances(2, true, 'CHART'); // Add your initialization code here - do not place outside setTimeout function this.appointmentDS.loadPreferences(); }, 0); }
- Go ahead and try your MPage. If you saved new configuration settings earlier they should be displayed on screen. You can update your preferences by making changes and clicking the save button.
On the next slide we will add prompts to our MPage allowing users the ability to control how their view their appointments. We will also take what we have just learned and configure our code so users can save their default prompt values.