CRM and CX Blogs by Members
Find insights on SAP customer relationship management and customer experience products in blog posts from community members. Post your own perspective today!
cancel
Showing results for 
Search instead for 
Did you mean: 
R_Zieschang
Contributor
Back to part I

Back to part II

Back to part III

Back to part IV

Part V - Create a complex UI form, work with the eventbus and create a custom printjob to print


Introduction


Welcome to the last part of my blog series about creating cco plugins. If you are new to this blog series, please use the links above to follow the prerequisties for this part as we will dive directly into the topic.

In this part we will learn how to create complex forms in the quick service ui with different components, we will check for mandatory fields and we will get to know how to work with the event bus. We will save this data from the ui form into cco and we will create a custom print job to print the data on our own print layout with apache FOP.

We will use SAP Customer Checkout 2.0 FP10 for this plugin.

Inject Javascript code and custom CSS


As always we create a maven project in which our base class extends the BasePlugin class provided by SAP Customer Checkout.

We will now inject our javascript and css. See screenshot below.


In the next step please add both files.


We are going to fill these files with code later on.

Create user defined table with DAO and DTO


In the next step, we are using the pattern, we already know from part 3. We are creating a new database table within the cco database and make CRUD operations on this table easily accessible with a DAO layer so that we can use our database records in DTOs.

Please checkout part 3 as I won't go into detail in this part. The only difference is, that we are using the singleton pattern. This ensures only one instance of our dao class exists while our plugin runs and the dao instance is easily accessible throughout all classes. Also it removes the necessity to instantiate the class everytime again.

Basically we are defining a static property called instance from type AdditionalInfoDao. The only access to this class is through our method getInstance() which checks if instance is already set, sets it if not and returns it. Done.


The access to the methods of our dao class is as simple as this. In our app class (the one which extends ccos BasePlugin class) we override the startup method to setup our database table. See? No AdditionalInfoDao dao = new AdditionalInfoDao() etc.


Our dto class also looks pretty much the same like our LoyaltyFactorDto class from part3.

Creating a user form in the quick service ui


First thing we will do is go to our CCOm and create a quick selection button which will later trigger our form.

Go to POS configuration -> Quick selections and choose your quick selection and add a quickselection button at you desired place.


 

When the button is created, click on the glasses on the left side which shows "display".


In the next screen click on Edit and add the following js code:
"event": { "eventName": "SHOW_ADD_INFO", "eventPayload" : { "message" : "I am an event payload message" }}

What this code does is, whenever the cashier clicks on this button, it will create an ui event which is pushed into the eventbus of cco. This event later will be handled by our ui coding.

Create ui coding and handle events


Remember we already created and inject a js file we created earlier? Let's jump to it and create some js code.


So there is a lot going on already, lets get into some details. First of all we create our plugin class, the constructor which will be called by cco gets an instance of the pluginService and the eventBus.

In our init method we are subscribing to this eventBus from cco. In the 'handleEvent' scope we are now able to handle our own events but ALSO the events cco pushes in the eventbus.

Before the handling of the events we're getting various objects from the pluginService like the receiptStore, where the current receipt is stored, the userStore to get the current user and the translationService to create multilanguage ui events.

In the 'SHOW_ADD_INFO' event we are checking if there is a receipt in the receiptStore and if not we will show a message box to the cashier. After that we check, if the receipt has an businesspartner set.

If all the checks apply we will send an event to our backend with the customerId we retrieved from the receiptModel.

How to handle these backend events?

Handling plugin events in the backend


I like to have separate classes for handling the backend events. E.g. one class for handling all receipt related events, one class for handling sales item events etc.

For this plugin we will use one class for handling all plugin events. Again we will create a singleton for our event handler.


So in our registerEventHandler method we're adding a new UIEventChannelListener. CCO will call the handleEvent method for all UIEventChannelListener in their list. You'll get the eventId and also the payload.

In our case, we retrieve the customerId from the payload and use our AdditionalInfoDao class to look for a database record in our table for this customer id. If we found something, we're adding the results into an JSON object and use the BroadcasterHolder class to push an event from our backend back to the ui.

The last thing we need to do is to call the registerEventHandler method in our startup Method of our App class.
    @Override
public void startup() {
AdditionalInfoDao.getInstance().setupTable();
BackendEventHandler.getInstance().registerEventHandler();
super.startup();
}

Now we have the knowledge to sent events from our backend to the ui and back.

Pretty easy, isn't it?

Creating the ui form


In the last chapter we sent an event called SHOW_ADD_FORM after we retrieved a database record from our AdditionalInfoDao class. Now lets build a form.

To make things more interesting, we're not gonna build a plain form. No we'll build a form with multiple ribbons. And depending on the chosen ribbon we will make input fields visible or invisible.

So jump back to our js coding and add a new case in our switch.

When we get the SHOW_ADD_FORM in our js coding, we need to implement the following things:

  • create all the data models to store the data

  • create all the ui components like labels, inputfields etc. and bind the data models

  • if we got data from the backend (for this customer there is already a record in the database) we need to set the data

  • create the form

  • react on switching between the ribbons

  • check for mandatory fields

  • send the input data back to the backend


Create data models and ui components


So I will break the coding down to this elements. We implement a method called buildElements.
    buildElements() {
// all our ui components will be bound to a datamodel so that we can easily access the
// values of the cashiers input
this.model1 = new cco.InputModel();
this.modelAmount = new cco.InputModel();
this.modelDecimal = new cco.InputModel();
// the option model is like a select box so we need to add options to select.
this.selectModel = new cco.OptionModel();
this.selectModel.addOption(new cco.Option('option1', 'First option'));
this.selectModel.addOption(new cco.Option('option2', 'Second option'));
this.selectModel.addOption(new cco.Option('option3', 'Third option'));
this.selectModel.selectFirstOption();

// we are creating 2 arrays each containing all ui components for each ribbon.
this.ribbonOneElements = [{
index: [0, 0],
hspan: 8,
vspan: 8,
content: {
// this is our first LabelComponent
component: cco.LabelComponent,
props: {
text: 'Additional Field 1:',
class: 'tableCellLayout'
}
}
}, {
index: [0, 10],
hspan: 20,
vspan: 8,
content: {
// this is our first InputFieldComponent
component: cco.InputFieldComponent,
props: {
// bound the datamodel
id: 'first',
model: this.model1,
class: 'tableCellLayout',
autoSetPrimaryTarget: true
}
}
}, {
index: [16, 0],
hspan: 8,
vspan: 8,
content: {
// this is our second LabelComponent
component: cco.LabelComponent,
props: {
text: 'Additional Field 2:',
class: 'tableCellLayout'
}
}
}, {
index: [16, 10],
hspan: 20,
vspan: 8,
content: {
component: cco.InputFieldComponent,
props: {
id: 'second',
model: this.modelAmount,
class: 'tableCellLayout',
numeric: true,
autoSetPrimaryTarget: true
}
}
}];

this.ribbonTwoElements = [{
index: [0, 0],
hspan: 8,
vspan: 8,
content: {
component: cco.LabelComponent,
props: {
text: 'Additional Field 3:',
class: 'tableCellLayout'
}
}
}, {
index: [0, 10],
hspan: 20,
vspan: 8,
content: {
component: cco.InputFieldComponent,
props: {
id: 'third',
model: this.modelDecimal,
numeric: true,
class: 'tableCellLayout',
autoSetPrimaryTarget: true
}
}
}, {
index: [16, 0],
hspan: 8,
vspan: 8,
content: {
component: cco.LabelComponent,
props: {
text: 'Additional Field 4:',
class: 'tableCellLayout'
}
}
}, {
index: [16, 10],
hspan: 20,
vspan: 8,
content: {
// For our selection model we use the ui component called SelectComponent
component: cco.SelectComponent,
props: {
model: this.selectModel,
class: 'tableCellLayout'
}
}
}];
}

Basically we define our data models and create two json arrays one for each ribbon.

Furthermore we create an object of type GenericInputConfiguration. This configuration will hold three elements. Our StatefulBarComponent which is our ribbons buttons. The two other elements are GridLayoutComponents, one for each ribbon. These GridLayoutComponents will hold our elements we defined in the buildElements method.
createRibbonOneConfiguration() {
// define which text the buttons should show
let buttons = [{
content: 'Tab1'
}, {
content: 'Tab2'
}];

// creating a GenericInputConfiguration containing a GridLayoutComponent
return new cco.GenericInputConfiguration('RibbonForm', '2', 'Component', null, null, cco.GridLayoutComponent, {
props: {
// set this so elements in the gridlayout can overlap. So elements in ribbon 1 and 2 can have the
// same position.
overlappingElements: true,
elements: [{
index: [0, 0],
hspan: 25,
vspan: 15,
content: {
// this is the ui element we are using for the ribbons.
component: cco.StatefulButtonBarComponent,
props: {
// setting the buttons as prop and also our datamodel.
buttons: buttons,
class: 'light',
populateModel: true,
model: this.ribbonSelectModel
}
}
}, {
index: [30, 0],
hspan: 25,
vspan: 10,
content: {
// our father gridlayout will contain another
// gridlayout. One per ribbon.
component: cco.GridLayoutComponent,
props: {
static: {
elements: this.ribbonOneElements
},
// setting the dynamic property to change the css class
// based on the selection of the ribbon.
dynamic: this.dynamicPropsRibbonOne,
cols: 100,
rows: 100
}
}
}, {
index: [30, 0],
hspan: 25,
vspan: 10,
content: {
component: cco.GridLayoutComponent,
props: {
static: {
elements: this.ribbonTwoElements
},
dynamic: this.dynamicPropsRibbonTwo,
cols: 100,
rows: 100
}
}
}],
cols: 100,
rows: 100
}
});
}

Because we want to make the elements of ribbon two invisible, if the cashier clicks on ribbon one (and vice versa) each GridLayoutComponent will also get dynamicProperties attached which we will define later.

Set values from the backend in the models


Remember the data models we defined earlier and bound to certain ui components? If we got any data from the database, we need to set these values to the data models to have the values shown in our form.
if (Object.keys(event.getPayload()).length !== 0) {
// set the values we got from the backend to our datamodels
this.model1.value = event.getPayload().addFld1;
this.modelAmount.value = event.getPayload().addFld2;
this.modelDecimal.value = event.getPayload().addFld3;
this.selectModel.setSelectedOptionKey(event.getPayload().addFld4);
}

Most of values ccos ui data models can be set via the value property, the data model for the selectbox offers a method to set the key of the chosen option.

Create the form


Now that we have all ui components, we also have the configuration of the grid layouts and we set values coming from the database. Let's create the form.

We will push an event into the cco event bus of type SHOW_GENERIC_INPUT. This event will get the configuration as property and also some other properties like a title.
this.eventBus.push('SHOW_GENERIC_INPUT', {
configuration: [
this.createRibbonOneConfiguration(),
],
title: 'Additional Info for customer ' + this.receiptStore.receiptModel.businessPartner.externalID,
showKeyboardSwitchButton: true,
keyboardType: 'keyboard',
widthFactor: 0.8,
heightFactor: 0.8
});

React on switching ribbons


Remember the dynamic properties object we set as property of the two grid layout components? These dynamic properties can be used to dynamically switch properties of ui components. E.g. make them bigger, change the text of buttons etc.
// we are also creating something called DynamicProperties so we can hide ui components when not used e.g.
// when the cashier uses ribbon the first ribbon so all elements on the second ribbon will not be visible.
this.dynamicPropsRibbonOne = new cco.DynamicProperties({
class: this.ribbonSelectModel.getSelected() !== 0 ? 'pluginRibbonHidden' : ''
});

// so we are basically setting a css class when our datamodel behind the ribbons has a certain value.
// the css class can be found in our blogpluginpart5.css
// With the Dynamic Properties you can set all properties of a component. e.g. changing the text, the height, ...
// of a ui component dynamically.
this.dynamicPropsRibbonTwo = new cco.DynamicProperties({
class: this.ribbonSelectModel.getSelected() !== 1 ? 'pluginRibbonHidden' : ''
});

In our example we dynamically change the css class based on which value our ribbonSelectModel is set.

But this is just for the initialization. Next thing is to add an observer to the ribbonSelectModel. The function within the observer will always be called when the state of the ribbonSelectModel (in our case the selected value) changes. So within this observer, we emit a change to our dynamic properties.
// we are setting an observer to the ribbonSelectModel so we can react
// when the cashier clicks on another ribbon
this.ribbonSelectModel.addObserver({
modelChanged: (inputModel) => {
this.dynamicPropsRibbonOne.setAndEmitPropertiesIfChanged({
class: inputModel.getSelected() !== 0 ? 'pluginRibbonHidden' : ''
});
this.dynamicPropsRibbonTwo.setAndEmitPropertiesIfChanged({
class: inputModel.getSelected() !== 1 ? 'pluginRibbonHidden' : ''
});
}
});

Set the css class


Remember the css file we already injected earlier? Remember that we used the dynamicProperties so we can set the class property of our grid layout component? Add the following css class to the css file.
.pluginRibbonHidden {
display: none;
}

Check for mandatory fields


The check for mandatory fields works with the validate callback. This function will be called when the cashier clicks on the finish (green button) of our form.
 // in the validate function we can implement checks to have mandatory field checks etc.
cco.GridLayoutComponent.prototype['validate'] = () => {
if (this.model1.value === '' || this.model1.value === 'MANDATORY') {
this.eventBus.push('SHOW_MESSAGE_BOX', {
'title': this.translationStore.getTranslation('ADD_FLD1_MND', this.user.getLanguageCode()),
'message': this.translationStore.getTranslation('ADD_FLD1_MND', this.user.getLanguageCode())
});
return false;
}
return true;
};

In our case we are checking if our data model model1 has a value. If not we are showing a simple message box to the cashier, that field1 is mandatory. We are using the translation store to make use of the translation files in cco/translations to have a multilanguage capable plugin.

P.S.: You could also use another set of dynamic properties to change the value of model1 to e.g. MANDATORY to show the cashier which field is mandatory.

Send input data back to the backend


Remember when we pushed the SHOW_GENERIC_INPUT event into the eventbus of cco? We now extend this a little to have a callback after the validation was successful.
this.eventBus.push('SHOW_GENERIC_INPUT', {
configuration: [
this.createRibbonOneConfiguration(),
],
title: 'Additional Info for customer ' + this.receiptStore.receiptModel.businessPartner.externalID,
showKeyboardSwitchButton: true,
keyboardType: 'keyboard',
widthFactor: 0.8,
heightFactor: 0.8,
callback: (positive) => {
if (positive) {
let customerId = this.receiptStore.receiptModel.businessPartner.externalID;
let addFld1 = this.model1.value;
let addFld2 = Math.trunc(this.modelAmount.value);
let addFld3 = this.modelDecimal.value.replace(',', '.');
let addFld4 = this.selectModel.getSelectedOptionKey();
this.pluginService.backendPluginEvent('SAVE_ADDITIONAL_CUSTOMER_DATA', {
'customerId': customerId,
'addFld1': addFld1,
'addFld2': addFld2,
'addFld3': addFld3,
'addFld4': addFld4
});
}
}
});

And we send an event to our backend and setting all the necessary data from the models to the event payload.

Done. Now let's have a look at the finished form.




Implement saving ui data


What's missing? Right! We pushed an event to our backend so now we need to save this data! Go back to the BackendEventHandler class and handle the event.
case "SAVE_ADDITIONAL_CUSTOMER_DATA":
customerId = payload.getString("customerId");
String addFld1 = payload.getString("addFld1");
int addFld2 = payload.getInt("addFld2");
Double addFld3 = payload.getDouble("addFld3");
String addFld4 = payload.getString("addFld4");
logger.info("Got data from ui");

AdditionalInfoDto newDto = new AdditionalInfoDto(customerId, addFld1, addFld2, BigDecimal.valueOf(addFld3), addFld4);
AdditionalInfoDao.getInstance().save(newDto);
break;

This is pretty straight forward. Get the data from the payload and save it through our AdditionalInfoDao class.

Done!

Nice!

But we are not done yet!

A custom printjob builder


What do we want to do with our custom printjob builder? We want to print the additional data on a Apache FOP layout if a customer is set in the receipt and the receipt is also printed.

So far so good. What do we need to do?

  • Implement our own printjob builder class extending the BasePrintJobBuilder class from cco

  • Register our own printjob builder

  • Extract the printlayout from the plugin and copy it into the print_templates folder of cco

  • React on the receipt printing to trigger our print job


Implement printjob builder


We extend the BasePrintJobBuilder class from cco. With this done we need to implement some methods. We also use the singleton pattern again to be able to access this class without creating a new instance every time.
public class AdditionalInfoPrintJobBuilder extends BasePrintJobBuilder {

private static final Logger log = Logger.getLogger(AdditionalInfoPrintJobBuilder.class);

private static AdditionalInfoPrintJobBuilder instance;

public static synchronized AdditionalInfoPrintJobBuilder getInstance() {
if (instance == null) {
instance = new AdditionalInfoPrintJobBuilder();
}
return instance;
}

private AdditionalInfoPrintJobBuilder() {
super();
}

@Override
protected boolean needsMerchantCopy(PrintTemplateEntity printTemplateEntity, Object o, CDBSession cdbSession, boolean b) {
return false;
}

@Override
protected boolean isTemplateEnabledForSpecificBuilder(PrintTemplateEntity printTemplateEntity, Object o, CDBSession cdbSession, boolean b) {
return true;
}

@Override
protected void mergeTemplateWithSpecificData(Map<String, Object> map, PrintTemplateEntity printTemplateEntity, Object o, CDBSession cdbSession, boolean b) throws IOException, TemplateException {
log.fine("Add data to map");
AdditionalInfoDto additionalInfoDto = (AdditionalInfoDto) o;

map.put("addInfo", additionalInfoDto);
}

@Override
protected void addSpecificSelectableDataStructure(SelectableDataStructure selectableDataStructure) {
SelectableDataStructure additionalInfoDataStructure = new SelectableDataStructure("AdditionalInfo", SelectableDataStructure.TYPE_LIST, "AdditionalInfo");
this.addSubelementToSelectableStructure(additionalInfoDataStructure, "CustomerId", SelectableDataStructure.TYPE_STRING, "CustomerId");
this.addSubelementToSelectableStructure(additionalInfoDataStructure, "addFld1", SelectableDataStructure.TYPE_STRING, "AddFld1");
this.addSubelementToSelectableStructure(additionalInfoDataStructure, "addFld2", SelectableDataStructure.TYPE_INT, "AddFld2");
this.addSubelementToSelectableStructure(additionalInfoDataStructure, "addFld3", SelectableDataStructure.TYPE_DECIMAL, "AddFld3");
this.addSubelementToSelectableStructure(additionalInfoDataStructure, "addFld4", SelectableDataStructure.TYPE_STRING, "AddFld4");
selectableDataStructure.addSubelement("AdditionalInfo", additionalInfoDataStructure);
}

private void addSubelementToSelectableStructure(SelectableDataStructure parent, String name, String dataType, String descriptionKey) {
SelectableDataStructure childElement = new SelectableDataStructure(name, dataType, descriptionKey);
parent.addSubelement(name, childElement);
}

@Override
public String getDescription() {
return PluginPropertiesConstants.ADDITIONAL_INFO_PRINT_JOB_BUILDER_DESC;
}

@Override
public boolean isResponsibleForDataSource(Object o) {
return o instanceof AdditionalInfoDto;
}
}

This should be mostly self-explanatory. The isResponsibleForDataSource method is called first. You can implement checks here e.g. only print if the additionalField2 is more than 200 or if additionalField4 is Option3.

The addSpecificSelectableDataStructure method can be used, to provide information about the printable data for the printtemplate. You can view this structure in ccos backend -> hardware -> print templates.


In the isTemplateEnabledForSpecificBuilder you will also get the printtemplateEntity as well as the data object itself. You could use this method to implement checks based on configuration of the printtemplate. e.g. a certain printer name is set.

Use the mergeTemplateWithSpecificData method to set your data object (in our case the AdditionalInfoDto) to the print data.

Register the printjob builder


After this implementation we need to register this printjob builder in cco. So we are extending our startup method in our App class.
@Override
public void startup() {
AdditionalInfoDao.getInstance().setupTable();
BackendEventHandler.getInstance().registerEventHandler();
addPrintJobBuilder();
super.startup();
}

The addPrintJobBuilder method is pretty easy:
private void addPrintJobBuilder() {
PrintJobManager.INSTANCE.addPrintJobBuilders(AdditionalInfoPrintJobBuilder.getInstance());
}

Now we are able to add another print template in ccos backend like this:


Now we need to add a print template which is extracted and copied to the print_templates folder when our plugin starts.

 Extract print template


We are extending our startup method again.
@Override
public void startup() {
AdditionalInfoDao.getInstance().setupTable();
BackendEventHandler.getInstance().registerEventHandler();
addPrintJobBuilder();
deployPrintTemplate();
super.startup();
}

Implementing the deployPrinttemplate method:
private void deployPrintTemplate() {
try {
String templateFileName = PluginPropertiesConstants.ADDITIONAL_INFO_PRINT_TEMPLATE + ".xsl";
extractResource("/resources/" + templateFileName, CConfig.getPrintTemplatePath(), false);
} catch (WrongUsageException ex) {
log.severe(PluginPropertiesConstants.EXCEPTION_PRINT_TEMPLATE_COULD_NOT_BE_DEPLOYED);
}
}

public void extractResource(String resName, String path, boolean overwrite) throws WrongUsageException {
InputStream oIn = this.getClass().getResourceAsStream(resName);
if (null != oIn) {
try {
String outName = resName.substring(resName.lastIndexOf("/"));
String fileName = path + File.separator + outName;
File oFl = new File(fileName);
if (!oFl.exists() || overwrite) {
OutputStream oOut = new FileOutputStream(oFl);
IOUtils.copy(oIn, oOut);
oOut.close();
}
oIn.close();
} catch (IOException oX) {
throw new WrongUsageException("Error extracting: " + oX.getMessage());
}
} else {
throw new WrongUsageException("Could not find resource '" + resName + "'.");
}
}

Our plugin will extract the print template from the resources, and copy this to the print_templates folder but it wont overwrite it, if the template is already existing.

React on the receipt print


We are hooking into the printReceipt method of the PrintReceiptPosService class. This method will be triggered with the initial print as well as reprints.
@PluginAt(pluginClass = PrintReceiptPosService.class, method = "printReceipt", where = PluginAt.POSITION.AFTER)
public Object onReceiptPrinted(Object proxy, Object[] args, Object ret, StackTraceElement caller) {
ReceiptEntity receipt = (ReceiptEntity) args[0];
if (null != receipt.getBusinessPartner()) {
AdditionalInfoDto addInfo = AdditionalInfoDao.getInstance().findOne(receipt.getBusinessPartner().getExternalId());
if (addInfo.getCustomerId() != null) {
triggerPrint(addInfo, receipt.getCountPrintouts() > 1);
}
}
return ret;
}

private void triggerPrint(AdditionalInfoDto additionalInfoDto, boolean isReprint) {
try (CDBSession session = CDBSessionFactory.instance.createSession()) {
List<PrintJob<?>> printJobs = PrintJobManager.INSTANCE.createPrintJobs(additionalInfoDto, session, isReprint);
PrintEngine.INSTANCE.print(printJobs, false);
} catch (IOException | WrongUsageException | ParserConfigurationException | SAXException | TemplateException | TransformerException | InterruptedException e) {
e.printStackTrace();
}
}

Last thing is the print template itself, but I won't go into much details, because this would be too much for this part. Just have a look at it in git.

That is basically all. Congratulations!

Plugin in action


The form with data from the database:


Mandatory field check:


Data for a new customer is saved to the database:


And our print template printed after the normal receipt print:



Wrap up


We learned how to inject javascript and css code and we learned how to create rather complex forms in the quick service ui. Furthermore we got to know how to work with the cco eventbus, pushing and handling events in the ui as well as in the backend.

Also we learned how to implement a custom print job builder to create our own print jobs with our own data.

If you have any questions, please do not hesitate to drop me a line.

The code is hosted on gitlab.

https://gitlab.com/ccoplugins/blogpluginpart5

The plugin is licensed under the MIT License.
19 Comments